Paste a story. Get a film.
The pack outlines a story into scenes, stages each scene into shots of 5-15 seconds, casts it with Krea2, and renders every shot with MiniMax H3 reference-to-video. Character references and location plates are re-sent as references on every shot, so identity and place hold without anything having to be inherited across a boundary. On top of that, boundaries inside a scene are joined into one continuous take — see Continuity.
Everything runs locally.
Requires a current ComfyUI on Python 3.10+.
cd ComfyUI/custom_nodes
git clone https://github.com/lumosai8/MinimaxStoryBuilder.git
cd MinimaxStoryBuilder
pip install -r requirements.txtrequirements.txt pulls in imageio-ffmpeg, which bundles a static ffmpeg binary used to join the
rendered shots into the finished film. There are no other pip dependencies — everything else is
ComfyUI itself. Restart ComfyUI and the nodes appear in search under MiniMax Story Builder; the
example workflow is in the sidebar under Node Library → MinimaxStoryBuilder → example workflows.
Drop these into ComfyUI's models/ folders. The example workflow expects exactly these paths and
file names — repoint the loader nodes if yours are named differently.
| File | Folder | Used by |
|---|---|---|
Krea2_Turbo_convrot_int8mixed.safetensors |
diffusion_models/Krea2/ |
Casting |
krea2_charactersheet_full_v1.safetensors |
loras/Krea2/ |
Casting LoRA (recommended) |
qwen_image_vae.safetensors |
vae/Qwen/ |
Casting |
qwen3vl_4b_fp8_scaled.safetensors |
text_encoders/Flux/ |
Casting CLIP, and the Planner LLM |
qwen_3_8b_fp8mixed.safetensors |
text_encoders/Flux/ |
Stronger Planner LLM (optional) |
minimax_h3_hybrid_fl2va_ref2va_b20-49-int8.safetensors |
diffusion_models/Minimax/ |
Director + Voice Casting |
minimax_h3_video_vae_fp16.safetensors |
vae/Minimax/ |
Director |
minimax_h3_audio_vae_fp32.safetensors |
vae/Minimax/ |
Director + Voice Casting |
qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors |
text_encoders/Minimax/ |
H3's text encoder |
minimax_h3_latent_upscaler_3d_fp16.safetensors |
latent_upscale_models/ |
Director's fast path — download |
The latent upscaler is optional; without it the Director renders at full resolution in one pass, which is slower.
Planning needs a language model. Three ways to wire one, cheapest first:
-
A plain CLIP text encoder — the
qwen3vl_4bencoder above plugs straight into the Story Planner. Zero extra setup. -
Story LLM (llama.cpp)— points at a.ggufyou drop inmodels/LLM/(subfolders are fine) and launches a bundledllama-serverto run it. This repository does not ship those binaries (they are ~80 MB and platform-specific): install llama.cpp yourself — a prebuilt CUDA release orbuildfrom source — and start ComfyUI with the override set:export MSB_LLAMA_SERVER=/path/to/llama-server -
Story LLM (OpenAI)— any OpenAI-compatible/chat/completionsendpoint: OpenAI itself, OpenRouter, or a local Ollama / LM Studio / vLLM server. No local model and no llama.cpp needed; planning runs on the API.
Optional: ComfyUI-OrbitSheets improves turnaround casting with vision-judged frame selection (see Character references below).
┌─ Story LoRA Stack (Krea2) ─┐
[story text] ─> Story Planner ─> Story Casting ─> Story Voice Casting ─> Story Director
(CLIP) (Krea2 m/c/v) (H3 m/c/v/avae) (H3 m/c/v/avae)
└─ Story LoRA Stack (H3) ──────────────────────────────────┘
Voices are optional and come from H3 itself. Story Voice Casting (H3) renders a 5-second clip
of each character speaking, generated from the character sheet made during casting, and keeps only
the audio — the video half is never decoded. Because the voice comes from the face H3 will actually
render, it matches without being steered into matching, and the pack needs no TTS engine and no
other custom node. Leave the Director's story_voices input unconnected to skip voices entirely.
| Node | Does |
|---|---|
| Story Planner | Casts the story, outlines it into scenes, then stages each scene into shots |
| Story Plan from JSON | Replays an edited plan without re-running the text encoder |
| Story Casting (Krea2) | Renders a face portrait (or a hero turnaround sheet) per person, a plate per location |
| Story Voice Casting (H3) | Renders one 5s voice sample per character with H3, from their sheet (optional) |
| Story Director (MiniMax H3) | Renders every shot, joins them, grows a preview filmstrip |
| Story Editor (Planner node button) | Full-screen timeline: trim, re-render a shot, record narration, export |
| Story LoRA Stack | Four LoRA slots; chain stacks for more |
style_preset on the Story Planner sets the look of everything — character references, location
plates, and every shot. It renders as a thumbnail grid on the node, so you pick the look by eye:
auto · photorealistic · cinematic film · raw documentary footage · 35mm film ·
16mm grainy film · vintage VHS · anime · ghibli · 3D animated feature ·
stop-motion claymation · comic book · watercolour · oil painting · film noir ·
cyberpunk · ukiyo-e · custom
Each preset is a natural-language look description tuned for Krea2, and its thumbnail is rendered by
the same Krea2 model the node casts with, so the preview matches what you'll get. auto keeps
whatever style the text encoder reads out of your prose. Every other preset overrides it.
style_notes is appended to any preset — "shot on Arri Alexa, shallow depth of field" — and with
the custom preset it becomes the whole style.
The resolved style lands in the plan JSON, so you can re-style a finished film through Story Plan from JSON without running the LLM again.
Reference-to-video identity lives on face size. In a wide shot the face lands on too few pixels for the reference to hold and comes back smudged — a limit of ref2v on this model, not of the reference image.
So any shot with a person in it is pinned to medium, medium close-up, or close-up framing, and the director LLM is told to stage action toward the lens rather than across a room. Beats with nobody on screen are free to go wide.
H3 scales a reference down to the generation's pixel area before encoding it, so the layout of a reference decides how much real detail reaches the face — and the face is the only thing carrying identity across a cut. A uniform six-panel grid spends five sixths of the budget on views the framing rule above never renders.
Story Casting has a ref_mode:
| mode | what it sends H3 | face detail | cost |
|---|---|---|---|
portrait |
one tight head-and-shoulders close-up | ~640px | 1 image render |
turnaround |
that portrait as a big hero panel, with five orbited views in a strip below | ~658px | + 1 H3 take per subject |
sheet |
the original six-panel grid | ~142px | 1 image render |
portrait is the default: nearly all the face detail, none of the extra cost.
Krea2 renders the close-up. That image becomes [Shot 1] and H3's first frame, and H3 cuts to the other views around it — full body, left profile, right profile, rear, and a frightened expression plate. Locations get the same treatment from their wide plate: right, opposite side, left, and a high overhead.
Hard cuts, not a camera move. A pan or an arc gives the model something to get wrong and it
drifts; a locked-off static shot has no motion to get wrong, so each view is re-established from
the description instead. This is the approach proven in the ComfyUI-OrbitSheets pack, and when
that pack is installed its vision-judged OrbitSheetsFrameSelect picks the keeper frame from each
shot. Without it, a sharpness-scored fallback runs — less reliable, since H3 gives its shots
uneven lengths, but never blurred.
Two inversions from how OrbitSheets does it, both deliberate:
- The anchor is the face, not the full body. The hero panel is what the sheet exists to carry, so it never passes through H3's video decoder — it stays a native image render. The cost is that H3 infers the body from a head-and-shoulders frame, which the repeated appearance text holds together.
- The layout is a hero, not a grid. One square panel at full sheet width, the rest in a single strip beneath. The strip costs about a tenth of the area and the hero keeps the rest.
The sheet is built at 704px wide on purpose. That lands just under H3's pixel cap at the default canvas, so nothing is downscaled. Building it bigger buys nothing — a 1408px sheet gets scaled straight back down to the same face resolution.
Story Casting also takes a character_refs mapping in code (build_cast(..., character_refs=…))
— any name present there is used as-is and never rendered, so one hand-made portrait can be
dropped into an otherwise automatic cast.
Without a voice reference, H3 invents a fresh voice for each character in every shot — the same drift the character references fix for faces.
Story Voice Casting (H3) closes it, using the same model that renders the film. For each
character it renders a 5-second clip of them speaking, generated from the character sheet casting
already made, and keeps only the audio — the video half of that latent is thrown away without ever
being decoded. That sample then rides each shot as an <Audio j> timbre reference:
<Subject 1> is tall, grey beard, wire glasses in <Picture 1>.
<Audio 1> is the voice-timbre reference for <Subject 1> (S1).
It is guidance, not cloning — H3 speaks your written dialogue in that timbre rather than replaying the sample.
The point of generating it with H3 rather than a text-to-speech model: the voice comes from the
face H3 will actually render, so it matches by construction instead of having to be steered into
matching with a written description. The planner's voice line is still used — it becomes the
delivery note on the spoken line, so "deep and resonant, calm baritone" shapes what H3 reaches for.
Place the node between Free VRAM and the Director and give it the same H3 model/clip/vae/audio_vae,
so H3 loads once and serves both.
Three limits, all H3's:
- 3 audio references per shot against 9 images, so slots go to characters who actually speak in that shot — a silent character needs no timbre.
- 15 seconds of reference audio in total, across all three. H3's shortest generation is 5.17s, and three of those is 15.5s — over — so each sample is trimmed to 5.0s.
- 2 seconds minimum per reference, comfortably met.
Voices are entirely optional: leave the Director's story_voices unconnected and every character
simply gets whatever voice H3 invents for them, shot by shot.
example_workflows/story_to_film.json — a complete, wired graph. Load it from the ComfyUI
sidebar (Node Library → MinimaxStoryBuilder → example workflows), or drag the file onto the
canvas.
It ships pointed at the model files listed under Installation — repoint the
loader nodes if your files are named differently — and renders the whole story
(max_shots 0). Speed comes from the H3 4-step turbo LoRA at 8 steps — swap the turbo LoRA out
and lift steps to 20 once a test render looks right. Set max_shots to a small number if you
want to preview only the opening shots before committing to a full render.
Paste your own story over the sample in the Story Planner node.
Planning runs in three passes rather than one:
- Cast — read the whole story, name everyone who speaks, acts or is seen, plus any crowd that appears as a group and any voice that is heard but never shown.
- Outline — read the whole story again and return a scene list: where it divides, who is in each scene, whether it is present time or a memory, whether it moves at a montage's pace or dwells on one moment, and how many shots that scene needs.
- Staging — one call per scene, carrying the whole outline for continuity but only that scene's own text to stage.
The split is the point. Staging a whole film in one call asks for tens of thousands of tokens in a single reply, and what comes back is uniformly shallow — every shot the same length, the same two beats, the same single cut. One scene at a time costs more calls and produces a plan that actually varies.
pace is what stops a list of actions collapsing into a summary. "I ate, watched my show,
then took a shower" is three actions, and it must become three shots — or three [Shot N]
blocks inside one take, one per action — never a single beat that says "she eats dinner and
watches TV". The outline marks such a passage montage and buys it the shot budget to do that;
dwell is the opposite instruction, one moment expanded and no cuts at all.
Shot length is per shot. H3 accepts frame counts only on the 17k+5 grid, trained between
124 and 362 frames, and the staging pass sizes each shot to what it carries:
| frames | seconds | typically |
|---|---|---|
| 124 | 5.17 | a glance, a single reaction |
| 226 | 9.42 | an ordinary beat |
| 328 | 13.67 | a long build |
| 362 | 15.08 | the ceiling |
Set shot_count to pin the film to an exact number of shots — for pairing with a voiceover
whose length you have measured. Left at 0, the outline decides, and auto_shot_chars only
supplies a soft target (~200 characters a shot, a rough 15 s of speech) so an unusually long or
short story still lands in the right ballpark.
Depends on the story, and the pack tries to get this right rather than always answering yes.
- A narrator who acts — walks, opens doors, speaks, is seen — is cast as
the narratorand appears like anyone else. - A narrator who only narrates is not cast at all. A history, a report, a fable told about other people: whoever is speaking has no body in the film. Casting them would put a stranger on screen in every shot of a story they are not in.
- Per shot, they appear only if that shot's beats actually mention them. The staging pass reconciles each shot's cast against its own beats: anyone the beats name is added, anyone they never name — and who speaks no line and is not the shot's POV — is dropped.
- In a POV shot they are declared but not visible; the camera is their eyes.
- A shot can legitimately have nobody in it — a place, an object, a stretch of time.
A voice heard but never seen — someone on the phone, a shout through a door — is cast with
"seen": false. They carry dialogue and get a voice reference, but never a portrait and never a
<Subject> on screen; their lines render as an off-screen voice with no lip-sync demand.
A shot is normally one room, and a change of place is normally where one scene ends and the next begins. But some passages are the movement — "I took an early shift. I came home at around 2PM" is a workplace and then a hallway — and staging only the last place loses half the sentence.
When a take moves, both location plates are sent. Each is declared as its own <Subject N>,
cited in retention_analysis only for the [Shot N] it actually appears in, and the cut names
the room it cuts into:
<Subject 2> is a fluorescent-lit corner shop at dawn (bound to <Picture 2>).
<Subject 3> is a dim narrow hallway with beige wallpaper (bound to <Picture 3>).
...
<Subject 2> (appears in [Shot 1]): fully_preserved - the shop environment is retained.
<Subject 3> (appears in [Shot 2]): fully_preserved - the apartment-hallway environment is retained.
...
[Shot 2] At 00:06.000, the shot cuts to medium close-up in <Subject 3>. <Subject 1> climbs the
stairs and stops at her own door.
The header switches from Location: the shop. to Locations, in order: the shop, then apartment-hallway. so it does not pin the whole clip to the room it opened in.
The mechanism is a beat's own location: a beat marked "cut": true that names where it cuts
to. A beat that changes place without naming one renders the new action in the old room —
a location plate is a reference image, so the wrong room comes back convincingly rather than
vaguely. For the same reason, a shot whose location the model omitted falls back to its own
scene's location, never to the film's first.
The outline marks such a scene with moves_to, and the staging call is told outright that the
scene travels and must show both rooms.
Three things the planner marks on a shot that change how it renders:
-
pov— the camera is a character's eyes and that character is not visible. Reserved for shots where a barrier is the point: a door crack, a peephole, a keyhole, a window, a screen, a scope — or someone looking down at their own hands. Without it, a passage about peeking through a door crack gets staged with the observer standing in the room they are peeking into.It is deliberately not used for ordinary looking. "She watches them" is shot the normal way — her face, then what she sees — because a face reacting is most of why a character is on screen. A first-person story is wall to wall "I watched" and "I heard", so a model reading that literally would turn the whole film into POV and never show the protagonist at all; at most half the shots in a scene may be POV, enforced in code.
A POV character keeps their reference image if there is room (a sleeve or a hand entering frame still has to match) but goes last in the reference queue, is marked
weak_reference, is left out of the summary, and swaps the framing rule onto whatever is being looked at. -
time—present/flashback/memory/dream/later. H3 has no abstract notion of "earlier", so a time jump is described as a physical change in the picture: grade, contrast, grain, wardrobe. A cut inside a take can carry its owntime. -
vocals— deliberate non-speech human sound: singing, humming, chanting, laughter. This exists because the blanket "no dialogue, no vocalization" clamp otherwise contradicts a soundscape built around a group singing, and H3 resolves a contradiction like that by inventing something. Withvocalsset, speech is still forbidden but the vocal is not.
The Planner node carries an Open Story Editor button: a full-screen timeline for the film the graph just planned.
- Shots lie on a VIDEO track at their real lengths, with a filmstrip per clip and badges for POV, crowds, montages and time jumps. The SUBS track shows each shot's portion of the story.
- Dragging a clip's edge trims it for export — it moves the in/out point inside the clip that already rendered, costs nothing, and never invalidates the render. Double-click an edge to reset. Generation length is a separate thing and lives in the plan, because changing it throws the render away.
- The media pool holds the cast, places and crowds. Editing a character or a place re-renders its reference; editing a crowd's description just lands in the plan, because a crowd has no reference to re-render — the description is the mechanism.
- Selecting a clip opens an inspector with the exact prompt H3 will be sent. Editing it stores an override that is sent verbatim, and ↻ Re-render rebuilds just that shot.
- ⏺ Record narration captures a voiceover to picture and lays it on the NARR track, where it can be dragged in time. It is mixed in at export as a second audio source: every shot keeps the sound H3 generated with it.
- The cut is saved in
timeline.json, deliberately outsideplan.json—plan.jsonis digested to key the render cache, so a trim stored there would throw away every rendered shot the moment a handle moved. - ⬇ Export film assembles it. With no trims, no mutes and no narration it takes the original stream-copy path and costs a file copy; anything the editor actually changed needs one re-encode, because stream copy can only cut on keyframes.
Story Director has a continuity setting:
| mode | behaviour |
|---|---|
off |
hard cut at every boundary |
same_location |
(default) join consecutive shots that share a location, cut when the story moves |
always |
join every boundary |
A joined shot takes the previous shot's last 22 frames (~0.9 s) and their audio, and anchors
them at frame 0 of the new shot with MiniMaxH3AddGuide. The prompt gets a matching instruction
not to restage or re-establish. The take flows on instead of cutting.
Three details that matter:
- 22, not 21. Guide clips sit on the same 17k+5 grid as the video, so the valid lengths are 5, 22, 39… H3 floors anything else downwards — a 21-frame batch silently becomes a 5-frame guide.
- The guide frames come back. Keyframe latents are re-injected at every sampling step and never denoised, so the new shot's head reproduces the old shot's tail. The director trims those frames (and the matching audio) before joining, which is why the shot is requested 17 frames longer than its text needs.
- The ceiling still wins. If a shot's own length plus the guide would exceed 362 frames, the boundary stays a hard cut and the shot keeps its full narration. It is never truncated to make room for a join.
This is not the same as frame chaining, and it is why drift does not compound: the character sheets and location plate re-anchor identity from scratch on every shot, guide or no guide.
Shots use H3's native six-part presentation, matching the <Picture i> / <Audio j> /
<Video k> labels its tokenizer emits:
subject_definitions:
<Subject 1> is copper curls, oval face, athletic build in <Picture 1>.
<Subject 2> is 5-6 people — a mix of ages and builds, varied hair and everyday clothing. They
have no reference picture: each one is a separate individual with their own distinct face,
build, hair and clothing. None of them shares the face, hair, or clothing of <Subject 1>.
<Subject 3> is a tar rooftop under low orange smoke (bound to <Picture 2>).
summary:
[reference generation] The target video shows <Subject 1>, <Subject 2> in <Subject 3>. Mara
pushes the hatch open and finds the roof already occupied.
retention_analysis:
<Subject 1> (appears in [Shot 1]): fully_preserved - copper curls, oval face are retained.
<Subject 2> (appears in [Shot 2]): weak_reference - the group is generated from this
description; its members are distinct individuals, not repeated copies of each other or of
any referenced subject.
<Subject 3> (appears in [Shot 1], [Shot 2]): fully_preserved - the rooftop environment is retained.
detailed_description:
[Shot 1] cinematic realism, medium close-up.
<Subject 1>, copper curls, oval face, athletic build, pushes the hatch open and steps onto the tar roof.
[Shot 2] At 00:05.000, the shot cuts to a wider angle. <Subject 2> turn toward her.
overall_soundscape:
A hinge groans, wind crosses the roof.
dialogue:
<Subject 1> (S1) says flatly, <d>[English] We do not have long.</d>.
non_diegetic_music:
Low sustained strings.
Every subject is named inside detailed_description, not only declared above it. This is the
part that is easy to get wrong: a prompt that declares <Subject 1> and then writes "Mara pushes
the hatch open" has given the model a phrase it has no binding for, and the reference is only
loosely attached to the action it is supposed to govern. So names are substituted for their
labels, expanded with the subject's identifying features on first appearance and by bare label
afterwards, exactly as H3's own worked example does.
A crowd is a subject with no picture. H3 defines <Subject N> as reusable visible content —
it does not have to come from a reference image. That is the only way to put several unnamed
people on screen: a beat that says "the guests crowd around her" with nothing but referenced
faces available gets rendered by copying one of those faces onto every member of the group. So
crowds are cast separately (groups in the plan), declared with an explicit description of how
they differ from each other, marked weak_reference, and held apart from the referenced cast
by name. They cost no <Picture> slot.
retention_analysis cites the shots a subject is actually in. The shot labels come from
writing the description, not from the shot's cast list — claiming a subject is preserved in a
[Shot N] they never appear in is an instruction to put them in it.
Subjects and pictures are separate namespaces, and each subject says which picture it came
from. <Subject N> counts every subject — people, crowds, rooms. <Picture i> counts only the
images actually sent. A crowd has no picture, so the two diverge, and the binding is stated
explicitly rather than implied by position:
<Subject 1> is copper curls, oval face in <Picture 1>.
<Subject 2> is tall, grey beard, wire glasses in <Picture 2>.
<Subject 3> is 5-6 people — mixed ages and builds. They have no reference picture…
<Subject 4> is a tar rooftop (bound to <Picture 3>).
<Subject 5> is a concrete stairwell (bound to <Picture 4>).
As many references as the shot needs, up to H3's nine. Locations are never dropped — both plates survive a shot that travels — and characters are trimmed from the end of the LLM's most-important-first ordering. A point-of-view character goes to the back of that queue whatever the ordering: they are behind the lens, so at most a hand or a sleeve of theirs is on screen, and left in place they would spend a scarce slot on a body nobody sees.
A character who does not get a picture is still declared — from their description alone,
marked weak_reference, exactly as a crowd is. Writing is trait 7 in {{c7}} for someone who
was trimmed substitutes to the bare name (is trait 7 in c7) and pairs it with a retention line
ordering H3 to preserve a reference it never received. Nobody is silently dropped from a shot
because the images ran out; the director logs which characters this happened to.
Reference ordinals are never written by the LLM. <Picture i> is assigned by position in the
list handed to H3, and that list changes shot to shot as characters enter and leave — so the same
character is <Picture 1> in one shot and <Picture 2> in the next. The LLM writes named
placeholders ({{Mara}}) and the director resolves them once the real ref list exists. Getting
this wrong tells the model the wrong face belongs to the wrong name, and it will believe you.
Planner CLIP — any text encoder with a language-model head. A Krea2 (Qwen3-VL) encoder works
and is already loaded for casting; qwen_3_8b_fp8mixed is a stronger planner if you have the
headroom. Models with tied embeddings are fine: comfy falls back to the embed_tokens matrix.
Casting — Krea2 Turbo, qwen_image_vae, 8 steps, cfg 1.0, euler/simple. Consider
krea2_charactersheet_full_v1 in the LoRA stack.
Director — H3 fl2va or ref2va (or the hybrid loader), CLIP type minimax,
minimax_h3_video_vae_fp16 + minimax_h3_audio_vae_fp32, 20 steps, res_multistep/simple,
1344×768. Drop to a turbo LoRA and 4–8 steps while iterating.
Output — wire frames and audio into CreateVideo at 24 fps, then SaveVideo.
ref_image_sizedefaults tomatch.maxkeeps the reference at its full 2048 short edge for the strongest identity, but those tokens ride every sampling step and cost several times the time. Packing more characters into one sheet is the cheaper way to spend that budget.- Max 9 reference images per shot. The location is never dropped; characters are trimmed from the end of the director's most-important-first ordering.
- VRAM. Krea2 and H3's Qwen3-VL-32B encoder will not co-reside on most cards. Casting finishes and holds its output as pixels before the director loads H3.
- Caching. Sheets are keyed by a content hash of prompt and sampler settings, so editing one character's description re-rolls only that character.
- Iterating. Save
plan_json, edit the one shot you dislike, and feed it back through Story Plan from JSON. The film re-renders without touching the text encoder.
cd custom_nodes/MinimaxStoryBuilder && python -m pytest tests/ -qCovers the frame grid (including the continuity guide's headroom), text preservation through
segmentation, the placeholder→<Picture i> mapping, the hero sheet's face-pixel arithmetic,
beat timing and montage cuts, the staging pass's reconciliation of a shot's cast against what its
beats actually say, crowd/POV/time rendering in the H3 prompt, and the editor's trim clamping.
No GPU or ComfyUI runtime required.