Story Frame Generator
One JSON in, a whole consistent story out — without wiring six samplers
- reference_images
- frames
- log
- frame_count
The name is the whole pitch. Story Frame Generator reads a JSON description of a sequence of frames and renders the entire chain - one text-to-image shot, then image-to-image edits where each frame continues from the previous one - inside a single node, in a single queue run. No six-sampler spaghetti, no re-queueing the graph ten times while you manually index frames. It's the headline node of the ComfyUI-ZFRNodes pack, and the reason the pack exists.
Why does this need its own node at all? Because ComfyUI's graph has no feedback loops: a node's output can't feed back into the next iteration within one run. Generating "frame N edits frame N-1" normally means hand-chaining samplers or re-queuing with a manual index. This node moves the loop inside itself, in pure Python, and keeps the resolution locked across the chain so you don't get the gradual "fading" drift that plagues chained sequences when reference and sampler sizes drift apart.
How it works
The chain is deliberately simple. Frame 1 is a text-to-image pass: empty latent, denoise 1.0. Every following frame is image-to-image: the previous frame gets VAE-encoded and injected into the positive conditioning via Flux2's ReferenceLatent, then a new image is sampled that continues from it. Models, CLIP, and VAE are loaded once and reused across every frame, and after each one the node frees intermediate tensors and calls soft_empty_cache(), so long stories don't eat your VRAM alive. Empty latents come from ComfyUI's own EmptyLatentImage, which keeps channel count and device sane on whatever host workflow you drop this into.
This is a Flux2 machine. The pack was built and tested with flux-2-klein, and the i2i path leans hard on ReferenceLatent, so models that support reference-latent conditioning work best. Klein is the right companion anyway - it's the default local editor of this era, and JSON-structured prompts work well with it.
The inputs that matter
The star input is prompts_json - the frame JSON. text_to_image frames can have a prompt as a plain string or an object (which gets joined into key: value lines); image_to_image frames take a plain string. A minimal example:
{
"title": "The Awakening Cat",
"total_frames": 2,
"frames": [
{"frame": 1, "type": "text_to_image",
"prompt": {"Subject": "A black cat on a velvet cushion",
"Style": "Documentary photography"}},
{"frame": 2, "type": "image_to_image",
"prompt": "Change the cat's posture. Preserve everything else."}
]
}
Beyond that, the loaders and sampler settings are the same vocabulary as any Flux workflow: unet_name, vae_name, clip_name, clip_type (pulled live from ComfyUI), steps, cfg, guidance, sampler_name, scheduler. Defaults are 8 steps / cfg 1.0 / guidance 3.5 / euler / simple - the standard distilled-Klein recipe.
Two pairs deserve your attention. First, LoRAs: lora_name_t2i and lora_name_i2i are separate, each with its own strength and trigger_words. Trigger words get prepended to that frame's prompt (trigger, <prompt>) before encoding, which is how activation-keyword LoRAs get fired. Second, seed_mode has three options - fixed, increment, or random per frame.
If you care about consistency across a character, connect reference_images (an IMAGE batch, e.g. a contact sheet from Sheet Compositor): every frame is then conditioned on those references, which is the pack's identity-lock trick. i2i_size_mode decides resolution stability: scale_to_megapixels (default) locks the i2i size for the whole chain, match_first_frame reuses frame 1's size. Outputs: frames (one IMAGE batch, normalized to frame 1's size - wire it into a Preview or Save Image), log, and frame_count.
Install and gotchas
Install via ComfyUI Manager (search "ComfyUI-ZFRNodes") or:
cd /path/to/ComfyUI/custom_nodes
git clone https://github.com/zfrsgtcu/ComfyUI-ZFRNodes.git
Then fully restart ComfyUI - a browser refresh isn't enough, because the Python process caches loaded modules and the pack's frontend script needs registering. Requirements are just numpy, torch, and Pillow, which ComfyUI already ships; the pack downloads no models itself. You bring the Flux2 model, VAE, and text encoder.
Where people get burned: a mismatched model/type pair errors inside sampling - pick clip_type that matches the model you loaded. If your story fades after a few frames, check i2i_size_mode before anything else. And remember the loop is real work: 12 frames at 960×1200 is twelve full samples, so budget your patience (and your queue) accordingly.
Inputs (27)
| Name | Type | Default | Description |
|---|---|---|---|
| prompts_json | STRING | — | |
| unet_name | COMBO | 0 options: | |
| vae_name | COMBO | 0 options: | |
| clip_name | COMBO | 0 options: | |
| clip_type | COMBO | flux2 | 28 options: stable_diffusion, stable_cascade, sd3, stable_audio, mochi, ltxv, +22 |
| lora_name_t2i | COMBO | 1 options: None | |
| lora_strength_t2i | FLOAT | 1.00-10–10 | — |
| trigger_words_t2i | STRING | — | |
| lora_name_i2i | COMBO | 1 options: None | |
| lora_strength_i2i | FLOAT | 0.90-10–10 | — |
| trigger_words_i2i | STRING | — | |
| width | INT | 96064–8192 | — |
| height | INT | 120064–8192 | — |
| steps | INT | 81–200 | — |
| cfg | FLOAT | 1.00–30 | — |
| guidance | FLOAT | 3.50–100 | — |
| denoise | FLOAT | 1.000–1 | — |
| sampler_name | COMBO | euler | 44 options: euler, euler_cfg_pp, euler_ancestral, euler_ancestral_cfg_pp, heun, heunpp2, +38 |
| scheduler | COMBO | simple | 9 options: simple, sgm_uniform, karras, exponential, ddim_uniform, beta, +3 |
| seed | INT | 00–18446744073709550000 | — |
| seed_mode | COMBO | random | 3 options: fixed, increment, random |
| reference_megapixels | FLOAT | 1.000.1–16 | — |
| i2i_size_mode | COMBO | scale_to_megapixels | 2 options: scale_to_megapixels, match_first_frame |
| reference_imagesopt | IMAGE | — | |
| save_to_diskopt | BOOLEAN | true | — |
| output_subdiropt | STRING | story_frames | — |
| filename_prefixopt | STRING | frame | — |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| frames | IMAGE | — |
| log | STRING | — |
| frame_count | INT | — |