π§ͺ PerfectLab - Shot Series
Describe the person once, get the whole contact sheet
- PROMPT
- WIDTH
- HEIGHT
- SEED
Consistency work has a boring plumbing problem in front of the interesting one. Even with your character locked - same LoRA, same person block - you still have to write "extreme close-up, sitting on a stool, arms crossed, 4:5" for shot one, then do it again by hand thirty times, keeping framing matched to aspect ratio yourself. π§ͺ PerfectLab - Shot Series stops you doing that.
You describe the person once. The node walks nine axes on its own and hands you a numbered session: seven framings, thirty-two poses, twenty-eight expressions, twelve focus targets, twenty-four hairstyles, fourteen outfits, twelve footwear options, eight lighting setups, seven formats.
Counted, not rolled - and that's the whole trick
This is where it differs from every "random prompt" node you've used. The axes aren't sampled, they're strided: (run * stride) % length, with the stride chosen coprime to the list length. Walk a 32-item pose list that way and you visit all thirty-two before repeating, in a fixed order that looks shuffled. A different salt per axis keeps two same-length axes from marching in lockstep - precisely the failure you'd get from a naive i % n.
The practical consequences are good. A given seed always produces the same session, and re-running with a bigger count extends the series rather than reshuffling it - generate twelve, like them, ask for fifty more, keep the first twelve. You also get coverage: every framing, not seven variations of a close-up.
Framing and format stay coupled: close shots land in 1:1 / 4:5 / 3:4, medium in 4:5 / 3:4 / 2:3, full-body and wide in 2:3 / 9:16 / 3:4 / 16:9 - all scaled to the same ~1.5 MP area and snapped to a multiple of 16, so the latent is always valid and your VRAM bill doesn't wander.
The inputs that matter
count(1β100, default 12) is how many shots.seedfixes the session; shot i is emitted with seedseed + i.templateis a sentence with thirteen slots you can move or delete:{framing},{person},{scene},{hair},{pose},{outfit},{footwear},{expression},{placement},{lighting},{style},{quality}- plus{focus}. Empty it and you get the built-in default line.person(andscene,style) are optional text inputs - wire any text node in.person_wideis the smart one: for full-body and wide framings the node swaps to the compact description if you supplied one, because a paragraph of face detail is wasted on a two-thirds-body shot.- The nine
vary_*booleans turn axes off.axis_datais the hidden JSON the panel's pin lists write - that's how you lock a value. quality_lineis appended as{quality}so you can keep your own footer text out of the template.
All four outputs are lists: PROMPT, WIDTH, HEIGHT, SEED. Wire PROMPT into your text encode, WIDTH/HEIGHT into an Empty Latent Image (convert those widgets to inputs), and SEED into the KSampler if you want each shot on its own deterministic seed. ComfyUI's list handling does the iterating - you queue once and come back to a contact sheet.
Install
No Python dependencies: requirements.txt is empty on purpose and pyproject.toml lists none, so nothing can conflict with your existing environment.
cd ComfyUI/custom_nodes
git clone https://github.com/perfectmodelslab/ComfyUI-PerfectLab PerfectLab
Restart ComfyUI and hard-refresh the tab (Ctrl+F5). Same repo as the Prompt Assistant - one clone gives you both nodes. Needs Python 3.10+.
Where people get burned
Unchecking a vary_* box does not freeze the value you're looking at. With vary_pose off, the node uses seed % length - a single fixed pose, but a seed-chosen one, which will not be the pose you liked. To actually pin something, click that axis row in the panel and pick from the unfolded list; that writes axis_data. This is the single most-reported misunderstanding of the node and it's baked into the logic, not a bug.
placement is the odd one out. It isn't an axis you can pin - it cycles by index regardless of your settings. It's in the template as {placement}, so delete the slot if you don't want it.
Two things need to match up. count decides the list length on all four outputs. If an upstream node also emits a list, mismatched lengths are how you end up queueing 96 images when you asked for 12. And if you ever route PROMPT through a node that collapses the list, the batch dies right there - the whole session becomes one image.
Panel missing or unstyled. Restart the server (it only scans custom_nodes at startup) and hard-refresh the browser. If the node is absent entirely, check the console for IMPORT FAILED or an "An error occurred while retrieving information" message naming PerfectLabAssistant.
Deleting template slots leaves stray commas. The node collapses runs of commas for you, but if you hand a mangled line to something strict, that's where it came from.
Inputs (18)
| Name | Type | Default | Description |
|---|---|---|---|
| count | INT | 121β100 | β |
| seed | INT | 00β18446744073709550000 | β |
| vary_framing | BOOLEAN | true | β |
| vary_pose | BOOLEAN | true | β |
| vary_expression | BOOLEAN | true | β |
| vary_focus | BOOLEAN | true | β |
| vary_ratio | BOOLEAN | true | β |
| template | STRING | {framing}, focused on {focus}. {scene} {person}, {hair}, {pose}, wearing {outfit} and {footwear}, {placement}, {expression}. {lighting}. {style} {quality} | β |
| quality_line | STRING | editorial photography, 85mm, natural light | β |
| vary_hair | BOOLEAN | true | β |
| vary_outfit | BOOLEAN | true | β |
| vary_footwear | BOOLEAN | true | β |
| vary_lighting | BOOLEAN | true | β |
| axis_data | STRING | {} | β |
| personopt | STRING | β | |
| person_wideopt | STRING | β | |
| sceneopt | STRING | β | |
| styleopt | STRING | β |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| PROMPT | STRING | β |
| WIDTH | INT | β |
| HEIGHT | INT | β |
| SEED | INT | β |