Support
Everything here is free and stays free — the format spec, the nodes, the workflows, the cartridges, the LoRAs. If it saved you a night of debugging (it contains several hundred of mine), tips keep the 5090 warm:
🔁 Liberapay (recurring)
⚡ Or right here: the Civitai tip button on this page sends Buzz directly.
What is new in 2.2.3
Long chains finish now. Three separate faults could waste a long render after every expensive stage had already succeeded — sampled, upscaled, normalised, and then nothing written. All three are closed.
A host-RAM ceiling. The finished timeline was assembled by allocating a second complete copy of itself. Six shots of 243 frames upscaled needed ~70 GB of system RAM at the final step and died on any 64 GB machine. Now ~21 GB. Twelve shots of 192 frames: ~58 GB down to ~31 GB.
An activation reserve that inflated its own estimate. If your chains got slower shot by shot until they crawled, this was why — the measurement counted offloaded weights as free activation space, and each overestimate made the next one worse.
Two switches that did nothing.
sol_attnandchunk_ffnshipped ON while the nodes they gate ship bypassed. Reported by sdktertiaire2.
Every one of these was found by someone running the pack rather than by me testing it, which is worth saying plainly. 2.2.1 and 2.2.2 — three crashes on long or high-resolution chains, and reference renders forcing glasses onto people — came the same way. The version notes carry the detail.
Which DiT to run
If you are on ComfyUI 0.32 or newer, the comfy-native w4a8 build is the one to use. It is 12.5 GB against Q5_1's 21 GB and measured 27% faster at matched shape and seed — independently, on a 5090 and on a 3090. It also loads through ComfyUI's own path rather than the GGUF loader, and needs no custom node to load. On ComfyUI 0.30 it will not load at all (that format did not exist yet); use a GGUF there, or update.
Write a scene. Get one continuous take back - picture and audio, no visible cuts, no colour shift between shots, and faces that hold without supplying a single reference image.
MiniMax-H3 generates blocks of roughly 10-15 seconds. This pack chains them into a scene of any length and joins them so the result reads as one unedited camera take rather than a cut sequence. One script in, one master video out, with a matching audio file.
Two independent video-understanding models were shown a four-shot chain blind, with no context. Both described it as one continuous unedited take - no cuts, colour consistent, audio unbroken.
What is new in 2.2
Everything since 2.1.2, which is where most people still are. Seven point releases folded into one version: five separate defects that stopped the workflow running, a measured campaign against drift over long chains that changed the shipped defaults, and a whole ComfyUI version this pack could not previously run on.
Five things that stopped it running
the Audio Spine produced static on ComfyUI 0.32.0 - that version scales the audio half of the packed latent onto the video schedule, and everything this pack injects was in the stream's native domain, so it landed four times too small and decoded as noise. Three injection sites, not one.
naming an mmproj file broke GGUF text encoders - the explicit path skipped the vision key-renaming step, so the tower loaded under names nothing downstream reads. The bitter part: that setting is documented as the escape hatch for when automatic pairing fails.
The value 1 for reference_image_size is not available- saved widget values written against an older layout, reading two slots early.the full workflow needed a node from a pack that is not in the zip. A node whose class is missing fails the entire prompt, so without ComfyUI-Custom-Scripts the graph could not be queued at all - for the sake of an on-canvas text box.
H3_Seamless_Chain_COREcould not be queued at all, since 2.1.3. Its sampler still carried the widget array from before four dials were removed, sooutput_scalereceivedfalseagainst a minimum of 1.0. The workflow advertised as the one with no third-party dependencies was the one that could not run - and it survived because nobody had ever rendered it. All three bundled workflows are now submit-tested against a running server as part of the release routine, and CORE has rendered: three chained shots, 370 frames, picture and audio.
Drift over long chains, measured
Chained shots accrete detail: each one conditions on the previous shot's own output, so invented texture compounds. Measured over ten shots at 960x544, the memory bank's recency slots turned out to be the carrier, and turning them off is strictly better - texture drift per hop 1.055 to 1.022, chroma 1.086 to 1.039, framing correlation at shot ten 0.976 to 0.995. The important part is that at the old default the drift accelerated, which is what a runaway feedback loop looks like, and at the new one it holds flat. Motion continuity was the obvious worry, since carrying it is what those slots are for; tested on a scene with continuous large-amplitude movement, anchor-only retained motion slightly better. So memory_frames now ships at 0.
Two more defaults changed with it. master_normalize gains a luma+contrast mode, because matching only the mean masks a contrast ratchet compounding underneath it, and pin_renorm holds each pinned latent at shot one's sigma rather than letting the pin's own sigma climb every hop. New dials: pin_frames, pin_noise, pin_renorm. Residual drift is about 1.02 per hop - not zero, and said plainly.
join_anchor_noise and handoff_release now ship at 0 because they are inert under context_pin: one noises keyframes the mode never creates, the other belongs to a different continuity mode. A non-zero value on a dead control reads as a tuned setting.
ComfyUI 0.32.0
Supported, and it needed real work rather than a version bump - see the Audio Spine fix above. 0.30.0 is unchanged and still supported, and 0.33.1 is now in production use on a second machine - a user has confirmed 0.30.0 on Ubuntu as well. Both are now tested before a release goes out, because three of the last four user reports were things that only appear on a runtime that was not being tested.
Bypassed nodes now say what they need
Four nodes ship bypassed because their packs are not in the zip. That is the right default - a bypassed node is dropped from the prompt entirely, so the graph queues without them - but a bypassed node sitting on the canvas invites you to un-bypass it, and doing that without the pack installed breaks the workflow with no explanation. The group title, each node's own title, a note above the cluster and the VRAM / SPEED note now all say which pack each one needs. The part people lose: it takes two steps in order - install the pack, Ctrl+B to un-bypass, then turn its switch on. Neither step works alone.
What is new in 2.0 and 2.1
Seamless joins, by a different mechanism
Earlier versions chained by handing the next shot the previous shot's last frame. That still works and is still here. 2.0 adds context_pin: the previous shot's last 22 frames ride into the next shot as raw latents - bit-identical, never decoded to pixels and re-encoded - placed at interior keyframe coordinates, alongside a timeline-placed audio reference. The regenerated head is trimmed on decode. Colour, motion and voice cross the join as data rather than as a description.
Seam audio that does not clip words
The boundary audio cut used to land blindly at the start of the incoming shot, which shaved the attack off any word the model placed there. It now lands in the quietest gap inside the shot's first 0.75 seconds, then welds with a 40 ms equal-power crossfade. A line that begins early survives.
A VRAM and speed panel
Try a render with every switch off and the reserve at 0 before touching any of this. That is the verified recipe, and in practice the fastest route to a working chain - the activation reserve measures each shape and conditioning payload as it renders and sizes the pool itself, and it holds on 24 GB cards as well as 32 GB. These switches exist to dig out of a spill the console has already named, not for pre-emptive tuning.
One panel with three lazily gated model patches - memory-efficient attention, chunked feed-forward, and a block cache - plus an activation-reserve control. Every switch off reproduces the shipped recipe exactly, and an off gate means the patch node never executes at all.
An activation reserve that understands payload
Memory measurements are now keyed by shape and conditioning payload, so a bare first shot and a reference-laden later shot no longer share one estimate. A measured pool is no longer overridden by a fixed floor, an unseen payload variant estimates from a measured sibling, and - the important part - a spill into system RAM is detected and named in the console instead of presenting as an unexplained five-times slowdown.
Boundary rules the prompt writer applies for you
The LLM prompt writer gained a join_style control. Set it to a chained style and the render-verified join rules are appended to its system prompt automatically, so generated scripts obey them without you memorising anything.
How identity holds with no reference images
This is the part most people do not expect. A 40-second two-character scene held both faces across every shot with zero reference images supplied. Two mechanisms do it, and neither works alone.
The frame relay carries the instance. Every shot begins from an actual rendered picture of the character - the previous shot's last frame - so the specific face, the wardrobe weathering, the exact hair propagate as pixels rather than being re-imagined from text. Drift can only accumulate per join, not per frame.
Verbatim text carries the category. Repeating each character's appearance description word-for-word in every shot re-asserts everything a picture cannot pin down, and authors anyone who walks into frame partway through. Once they are in a handed-over frame, they join the relay too.
Reference images and the identity bank are still there for long chains, characters who leave frame for a long stretch, or when you need a specific pre-existing face - they are just no longer the price of entry.
Voice identity across the chain
Chained shots used to re-perform the voice from text each time, and it drifted. Two controls end that, and the shipped default turns one of them on:
voice_ref- feed a clean solo speech clip and it anchors every shot as<Audio 1>. Mono clips, sample rate and length are all guarded internally.self_anchor_voice- no file at all. Shot 1's own rendered voice becomes the reference for every later shot. Write shot 1 so the character speaks a clean solo line.
Both ride the checkpoint's reference rows, so they need ref2va. On fl2va they do nothing except cost tokens - see the checkpoint note below.
Which workflow
Three, each with one clear reason to exist.
H3_Seamless_Chain_v2 - everything. Master controls, the LLM writer, VRAM panel, identity and voice anchors, an episode/batch prompt source, FFLF boundary plates and an audio spine. The optional lanes ship gated off, so the default path stays simple.
H3_Seamless_Chain_CORE - the same job with zero third-party packs. Type your shots into the script box and queue. Start here if you want a render before installing anything else.
H3_Keyframes - a different job: a hand-built sampling graph for anchoring a single clip at chosen frame positions with per-anchor condition strength. Not multishot, does not chain.
v2 supersedes the older H3_Multishot_AIO and H3_Multishot_MEMORY graphs - every lane they had is in v2. If you have them open from a previous version they still work; there is simply no longer a reason to reach for them.
Keyframes at any position
Anchor images are not limited to the first and last frame. The pack can place them at arbitrary positions, with per-anchor condition strength. Two implementations exist for the underlying layout patch - this pack's own, and the one in ComfyUI-H3-Motion-Context - and they patch the same place, so exactly one owns it at a time. If Motion Context is installed it wins (its version is a superset) and this pack stands down with a line in the log. If it is not installed, this pack fills the gap. Either way, first and last anchors always work and you never have to choose.
Master controls, and the dials that matter
One panel drives resolution, frames per shot and steps for the sampler, and also feeds the writer so it paces dialogue to the real shot length. The dials worth knowing:
continuity-context_pin(raw-latent join, needs the Motion Context pack),first_frame(the model's own trained hand-off, no extra pack),cutfor episodic work, orflf_chainfor boundary plates.seed_per_shot- leave it on. Measured: varying the seed per shot holds the face; one seed for every shot drifted both face and voice. Identity lives in the conditioning, not the seed.chain_gain_control- set toflattenpast about five shots. Each shot's tail anchors the next and the model returns roughly 1.3x the anchor's texture energy, so sharpness ratchets across a long chain with a visible step at every seam.output_scale/upscale_model- upscaling happens after decode, per shot, so it works with every continuity mode includingcontext_pin, and a long chain never holds a full upscaled master in memory at once.output_scaleis a lanczos resize (measured 1.78x faster than rendering the same output size natively, and visibly softer - resolution, not detail);upscale_modeltakes ComfyUI's own Load Upscale Model (ESRGAN and friends) for synthesized detail. The oldtwo_pass_upscaleis gone - it interpolated the raw latent between passes and produced colour noise in every configuration tested.master_normalize- leave it atluma+contrast. It levels both the brightness AND the contrast of the finished chain against one global target taken from shot 1. Chained shots drift brighter-edged every hop; matching only the mean re-centres that and hands the next shot a higher-contrast start. Measured: texture growth per hop 1.126 -> 1.047 at 960x544 and 1.130 -> 1.055 at 640x352, with contrast spread across shots falling from ~11% to under 0.5%. It runs on decoded frames against per-frame statistics, so there is nothing to tune per resolution.pin_noise- small and scene-dependent, not the anti-ratchet answer it was billed as in 2.1.5. Measured -1.8% per hop at 640x352 and -0.9% at 960x544 on a detail-heavy scene, and it gets worse above 0.10. Leave it at 0.05 or set 0; either way the contrast pass above is what does the work.memory_frames- leave it at 0. The bank's RECENT slots hand each shot's accreted output forward as reference images on top of the pin, so invented detail compounds. Ten shots at 960x544, 2 -> 0: texture 1.055 -> 1.022 per hop, chroma 1.086 -> 1.039, framing at shot 10 0.976 -> 0.995, and the drift stops accelerating. Tested on a moving scene too - motion continuity did not suffer. Raise to 1 only if a busy scene loses continuity between shots.pin_frames- leave it at22. Longer pins also cut the sharpening, but the head trim scales with them, so 39 frames of audio get cut per join and speech planned under the replay gets clipped.Audio drifts the other way: chained audio gets duller per hop where chained video used to get sharper (measured 8-50% of 4-10 kHz energy lost over 8 shots, seed-dependent). There is no per-shot dial for it - the per-shot correctors were removed in 2.1.3 because they cannot reach the raw-latent pin that carries the drift. For cut-grammar content it can be avoided outright:
continuity=cutwithbank_pinned=1, memory_frames=0conditions every shot's audio on shot 1 alone - measured flat.preview_first_shot- writes shot 1 the moment it decodes, so a bad take can be cancelled before the whole chain renders.
Which checkpoint
ref2va ships as the default: it carries the reference rows that voice anchoring and the identity bank need. fl2va is lighter and faster, has no reference rows, and chains just as well - the voice rides the frame relay instead of being pinned. Blind review passed on the fl2va configuration; ref2va ships as default because it makes identity explicit rather than emergent.
Writing a script
One prompt per shot, --- on its own line between them. The mechanics of a join are handled for you; whether the join reads as invisible is decided by the writing. These rules were each written after a specific failure:
The airlock. Every shot after the first opens holding the previous shot's exact closing arrangement, with about two quiet seconds before anyone speaks. The head of a chained shot is discarded on decode - anything you put there is thrown away.
Give the hold something to do. A held framing with nothing happening renders as a literal freeze. Write in a breath, a weight shift, an eyeline change.
Land settled. End each shot back in a stable arrangement, dialogue finished, about two seconds spare.
A line never straddles two shots. Budget it: dialogue at natural pace plus four seconds of quiet must fit. At 362 frames (~15.1s) that is roughly 27 words; at 243 frames (~10.1s), roughly 15.
Repeat the descriptions word-for-word. Each character's appearance and the room and lighting, identical in every shot. Rewording between shots is the single most common cause of a face changing mid-scene.
Camera cuts inside a shot are free and share one audio bed - so put cuts inside shots, and continuity between them. A worked four-shot two-hander ships as example_script.txt.
Quick fixes
A word clips at a join - the script put dialogue too close to a boundary. Move the whole line, do not split it.
Sharpening increases every shot - the texture ratchet. Set
chain_gain_controltoflatten.A render stalls at 0 steps, or runs several times slower than usual - the activation reserve is too small and the driver is paging to system RAM. Raise the VRAM reserve (roughly 20 on a 24 GB card), drop resolution or frames, or remove reference payload. The console now names this when it happens.
LLM API error 404, model not found - the writer's
model_nameis not pulled on the endpoint it is pointed at. See the prompt-writer section above.Red or missing nodes - an optional pack is not installed. The four optional nodes already ship bypassed, so this only bites if you un-bypassed one: re-bypass it with
Ctrl+B, delete it, or use the CORE workflow.A render crawls, GPU pinned at 100% but drawing a fraction of its usual power - the weights and the activation pool together do not fit, and the shortfall spills to system RAM rather than raising an error. The console names the clamp when it happens: if the reserve was clamped below the measured pool, that render will not finish in reasonable time. Drop frames or resolution, turn the accelerator switches on, or load a smaller quantisation of the DiT - a 4-bit checkpoint frees several GB for activations, which is usually the whole difference.
GGUF architecture error - the pack teaches ComfyUI-GGUF the
minimax_h3architecture at startup, so installing it is normally the whole fix. If the error persists, runpython apply_gguf_arch_patch.pyfrom the pack folder once and restart.Audio dulls on a very long chain - expected; restart the chain on a scene cut, where a fresh start costs nothing.
The prompt writer needs a model you actually have
The full workflow ships pointed at a local Ollama with model_name = qwen3:14b. Pull it before the first queue or the run stops immediately with LLM API error 404: model 'qwen3:14b' not found:
ollama pull qwen3:14b
Any OpenAI-compatible endpoint works - put its URL in base_url and the exact tag in model_name; ollama list prints the tags you have and it must match character for character. A remote or hosted endpoint is often the better choice, because a local writer large enough to be good competes with H3 for the same card and on under 32 GB will evict the model mid-render.
Running the writer locally? Turn on unload_model_after on the writer node. It frees that model from Ollama the moment the script is written, so the video model gets the card - otherwise it sits for the server default of five minutes, which is your whole first shot. ComfyUI's own eviction cannot reach it (separate process), and Ollama's OpenAI-compatible endpoint has no keep_alive field to ask with; the switch uses the native endpoint. It is added to the writer at runtime by this pack, and is off by default.
Want no LLM at all? Set the master panel's use_file_prompts to manual entry, delete the writer, and feed your own shot script straight into the sampler's script input - one prompt per shot, separated by --- lines. The CORE workflow already works this way.
Requirements
Always: ComfyUI v0.30.0 through v0.33.1 - tested at three points (0.30.0 on Ubuntu by a user, 0.32.0 here, 0.33.1 in production on a second box) and supported across the range (native MiniMax-H3 support), this node pack, a MiniMax-H3 checkpoint, the H3 text encoder, and the video and audio VAEs. The CORE workflow needs nothing else - it is built from this pack plus stock ComfyUI nodes.
For the FULL workflow. ComfyUI validates every node class in a graph before it will queue, so a missing pack stops the whole workflow - not just its own feature. Each can be removed instead; INSTALL.md gives the one-widget change or node deletion for every entry.
ComfyUI_JoyAI_Echo_GGUF_Nodes - the LLM prompt writer (
JoyEcho_LLMEnhance). It is inside the release zip - copy it intocustom_nodes/with the main pack. It is RealRebelAI's pack, modified with attribution (the NOTICE inside lists every change); use that copy, not upstream - the workflow drives inputs the upstream release does not have, and with upstream those values are dropped silently. Hand-written scripts can delete the writer instead.ComfyUI-H3-Motion-Context -
continuity = context_pin, the shipped default, plus interior keyframe anchors. Removal:continuity = first_frame.RES4LYF - the
beta57scheduler the full workflow ships with. Removal:scheduler = beta(measured cost: lip-sync 8/10 vs 10/10, everything else equal; CORE shipsbetaalready).ComfyUI-sol-attn and comfyui-minimax-h3-blockcache-T8 - the VRAM/SPEED patch switches. The switches ship OFF and the nodes ship bypassed, so the graph queues whether or not you have either pack - a bypassed node is dropped from the prompt entirely. To use one: install its pack, restart,
Ctrl+Bthe node to un-bypass it, then turn its switch on. Both steps, in that order - neither works alone.ComfyUI-Custom-Scripts - the in-canvas script preview, a leaf node; removal: delete it.
ComfyUI-GGUF - quantised checkpoints and encoders.
Models
GGUF quants of the H3 checkpoint are at huggingface.co/joeygambino/MiniMax-H3-GGUF - Q8_0 for 32 GB, Q5_1 for 24-32 GB, Q4_0 for 16 GB. The text encoder and both VAEs are at huggingface.co/Comfy-Org/MiniMax-H3. GGUF encoder quants are at huggingface.co/joeygambino/MiniMax-H3-encoder-GGUF; keep the -mmproj vision sidecar, and load it with this pack's H3 CLIP Loader (safetensors + GGUF) rather than the stock GGUF loader - the vision path is what carries frames between shots.
ComfyUI-GGUF pairs that sidecar by filename, looking only in the encoder's own folder. Rename either file or separate them and the match fails; upstream logs an error and then carries on without the vision tower, which looks like the model ignoring your reference image. This pack's loader raises instead of continuing blind, falls back to the only mmproj beside the encoder when there is exactly one, and gives you an mmproj_name widget to name the file outright - with that set, filenames and folders stop mattering.
What is verified, and what is not
Stated plainly, because a release note is a claim.
Verified: chained renders reviewed blind by two independent models and reported as one continuous take with consistent colour and unbroken audio; holding on both a static talking head and dynamic moving content; a 40-second two-character scene holding both faces with no reference images.
Verified since: all three bundled workflows submit-tested against a running server on ComfyUI 0.30.0 and 0.32.0, with 0.33.1 in production use on a second box; the full workflow rendered end to end from the shipped file on 0.32.0 through both of its script lanes, and read by a blind reviewer as one continuous static take with no cut findable anywhere, correct lip-sync, no drift in framing or colour and clean audio; CORE rendered for the first time at 370 frames.
Not verified: the shipped
ref2vadefault is the same chaining mechanism with reference rows added, but blind review ran on the lighterfl2vapath. Very long chains dull the audio slightly per hop.flf_chainhas not been rendered against a fully colour-matched plate set.
Credits
The context_pin join and interior keyframe anchoring build on ComfyUI-H3-Motion-Context by NikoDemon80. The script preview uses ComfyUI-Custom-Scripts by pythongosssss. GGUF loading builds on ComfyUI-GGUF by city96. Thanks to @viralesveras for contributions to earlier versions.
Free, and it stays free. If it saved you time: Ko-fi · GitHub Sponsors · Liberapay.
Description
2.2.5 — a chain that could refuse to start, an upscale that could eat the host, and one-click install from the Registry
Fixed: chained shots could stall before the first sampling step
The auto-reserve learns how much activation pool a given shape needs by measuring it. On a chained render the later shots often load partially — the weights are already resident, so only part of them streams in. The old code discarded every partial measurement as untrustworthy, which meant a chained shot could never contribute what it learned. If the very first shot was also partial, the cache stayed empty forever, the fallback reserve was too small for the shape, and every subsequent attempt failed the same way. A render could sit there refusing to start with no error to point at.
Partial measurements are now kept when the pool is genuinely large, and clamped by a named floor (_auto_cache_floor) that only ever ratchets upward, never down. Verified live on a six-shot chain: offload dropped from 4308 MB to 638 MB once the cache started learning.
The warning that fires when a shot asks for far more pool than is available now distinguishes the two cases it was conflating. If this shape has been measured before, it says so and gives you the number. If it is the first run at this shape, it says that instead of implying something is broken.
Fixed: output_scale was documented backwards
With an upscale model selected, output_scale is not a multiplier on top of it — the model applies its own fixed factor (4x for the ESRGAN family) and output_scale is the final size you want, not an extra step. The tooltip said otherwise, in both samplers. Both are corrected, and upscale_model_name now explains the interaction rather than leaving you to discover it.
New: the upscale says what it will cost before it runs
Decoded frames accumulate in host RAM until the master is joined, so the bill is per-shot multiplied by shot count. A 1344x768 chain through a 4x model is 5376x3072 per frame, and an output_scale left at a value that looked harmless has taken machines down at the join — after all the sampling was already paid for.
Before sampling starts you now get the projected dimensions, MB per frame, GB per shot and GB total, measured against actual free RAM:
[H3Memory] upscale will produce 5376x3072 (4.0x): 198 MB per frame
[H3Memory] WARNING: that will not fit. Frames are held in host RAM
until the master is joined - lower output_scale to 1.60
or lower, or turn the upscaler off.New: the folder walk prints which index is which file
Queueing a folder told you nothing about the mapping between EPISODE INDEX and the file it would pick, so a cancelled run left you doing off-by-one arithmetic against a single log line to work out where to restart. The ordered list now prints once per folder per session, and each job reports its own position and the index to set to resume there. It re-prints if the folder's file count changes, which is the one case where a remembered listing would mislead.
New: the writer warns when the dialogue does not fit the clip
The node already computed a real speakable word budget from the clip length — subtracting the airlock and replay on chained styles — and handed it to the model. Nothing checked that the model complied. Speech that overruns its clip renders crammed and garbled, and you found out by listening, forty minutes later.
Shots that overrun are now reported at generation time with the numbers. The check is deliberately symmetric: a script that comes back with no dialogue at all is also flagged, because that renders as a silent slideshow and was previously the one failure that passed without comment.
Changed: the story writer stops prescribing creativity
The system prompts had accumulated a five-beat dramatic arc, a banned-cliche list, a "land near seven shots" target and a dialogue quota. None of those are properties of the model — they are one person's taste, hardcoded, and they were flattening every brief toward the same shape.
They are gone. What remains is what the renderer actually needs: valid JSON, identity restated verbatim per shot so the room does not drift, literal physical description over mood language because abstract adjectives have nothing to render, and the note that audio is half the model. The story, its length, its tone and whether any given shot speaks are the model's call now.
Changed: the node menu no longer says "Rift"
Four nodes sat in a category called Rift, which means nothing to anyone who is not me. They are in H3/script now, the labels drop the prefix, and ten console messages say [H3 Multishot] — the name you actually installed — instead of [Rift].
Saved workflows are unaffected. Categories are never written into a graph and display names resolve fresh on load. The underlying class names are deliberately unchanged, because those strings are written into every saved .json and renaming them would turn your nodes into red missing-node boxes.
New: installable from the ComfyUI Registry
The pack is published as comfyui-h3-multishot, so ComfyUI-Manager can find and update it without a manual clone. The zip on this page stays the complete bundle — both node packs, the workflows and the docs — and remains the right choice if you want everything in one drop.
The dependency list is deliberately empty. Every import this pack uses (torch, torchaudio, psutil, av, numpy, PIL) is already guaranteed by ComfyUI itself, and a custom node that declares torch can talk pip into replacing a working CUDA build with a CPU wheel.
Documentation: drift on long chains, and the dials that fight it
SETTINGS.md gains a measured section on why chained shots accrete detail — each conditions on the previous shot's own output, so invented texture compounds — and which controls actually counter it. Measured over ten shots at 960x544:
memory_frames = 0is the big one. The bank's RECENT slots feed accreted output forward on top of the pin.master_normalize = luma+contrastlevels brightness and contrast of the finished chain.pin_renorm = onholds each pinned latent at shot 1's sigma.chain_gain_control = flattenon chains longer than about five shots — texture ratchets roughly 1.3x per join.
Residual with all of those on is about 1.02 per hop. Not zero. Long chains still drift, slowly. pin_noise is small, scene-dependent and gets worse above 0.10; it is not the fix it was described as in 2.1.5.
2.2.4 — references for more than one person, a folder you can queue, and video references
Four things people asked for or tripped over, plus the documentation that should have prevented two of the questions.
Fixed: reference images of different people blended into one face
If you gave the sampler reference pictures of two characters, it rendered something that resembled neither. That was not you holding it wrong. Internally every reference picture was declared to the model as <Picture N> is a reference photograph of <Subject 1> — the same Subject 1, every time. With one character that is correct and it is why single-character references work well. With several you were telling the model that photographs of different people all showed one individual, and it produced the average.
There is a new reference_subjects field on the sampler. Leave it empty and nothing changes — every picture is one person, exactly as before. Fill in comma counts in picture order to group them:
reference_subjects: 3,3 pictures 1-3 are person A, 4-6 are person B
reference_subjects: 2,2,2 three people, two pictures eachEach group is then declared its own subject, with an explicit instruction that it is never blended with the others. Only Subject 1 is described as speaking, because H3's voice conditioning is single-speaker.
New: point the prompt source at a folder and queue the whole thing
start_index has always selected a block inside one file. That works for LPFF-style prompt batches, where one file holds many prompts. It does not work for H3 scripts, because in an H3 script --- separates shots within one scene — concatenating scenes into one file would fuse them into a single enormous take.
Turn on the new walk_folder switch and start_index selects which file instead, walking the chosen folder in sorted order and emitting the whole script. Wire it to a Primitive set to increment, queue thirty times, and thirty finished scenes render unattended.
New: hand H3 an existing clip as a video reference
There is now a V2V REFERENCE lane in the workflow — Load Video into Get Video Components into the new H3 Reference Video node, then into two new sampler inputs. It ships muted; un-mute the three nodes to use it.
Read what this is before you wire it. H3 is told a video reference is "a clip from an earlier moment of this same continuous scene" and asked to keep its framing, camera distance, room contents and colour temperature. It is scene and appearance conditioning. It is not motion control — there is no pose, depth or optical flow path in H3, so your subject will not copy the movement in the clip. It borrows the place and the look, not the action.
Keep the window short. References are subsampled to 2 fps and then ride through every sampling step, so a 25-second clip is roughly 50 reference frames of permanent per-step cost. The H3 Reference Video node trims to a window and prints what that window will cost before you commit to it. Requires ref2va; fl2va has no reference rows and ignores video references entirely.
Fixed: an empty prompt dropdown that blamed the wrong thing
The prompt source finds .txt files through the inspire_prompts folder path, which is registered by comfyui-inspire-pack. Without that pack installed the path does not exist, so the dropdown came back empty however many prompt files you had, and the error blamed the prompts folder. It now tells you which of the three fixes applies: install the pack, register the path yourself in extra_model_paths.yaml, or set manual_path and ignore the dropdown. The dependency is written into INSTALL.md as well.
Documentation: what has to change between shots
Several people have hit chains where shot 3 comes back as a near-copy of shot 2. The prompting guide is partly responsible: rule 5 says to repeat descriptions word-for-word, and never said what must differ. Rule 5 is only about appearance and the room. It is not an instruction to restate the action, and when it gets read that way most of shot 3 is byte-identical to shot 2 — and the model is separately instructed to preserve the subject, the room and the colour temperature, so "keep everything the same" wins.
There is now a sixth rule covering it. The short version: each shot's action must leave the world in a state the previous shot's world was not in, physical and irreversible, not a mood or a camera move. If you can swap two shots' action lines and the script still reads correctly, the model cannot tell them apart either.
Documentation: fl2va is not a smaller file
Both this guide and INSTALL.md described fl2va as "lighter and faster", which everyone reasonably read as a size claim while choosing a file for a 24 GB card. It is not. fl2va and ref2va are exactly the same size at every quant level. "Lighter" only ever meant fewer tokens per sampling step, because there are no reference rows riding along.
What actually separates them was never written down: fl2va lands on a supplied frame and ref2va only nudges toward one. Measured against the same frame, 26.35 dB on fl2va versus 16.15 dB on ref2va with a keyframe at 6 turbo steps — and 16.81 dB at 20 stock steps, which rules out the sampler and leaves the checkpoint. fl2va can also take a first and a last frame and plan a camera move between them, which ref2va cannot do at all. So: ref2va when identity or voice must persist, fl2va when a shot must start exactly where the last one ended.
Improved: the story writer, for thin briefs and long shot counts
Give the writer a couple of characters and a genre with no plot, ask for fifteen shots, and it tended to return atmospheric moments rather than a story. That was the system prompt's fault in four specific ways, all now fixed.
It was calibrated to about seven shots and argued against more — "most scenes land between 4 and 10", "go higher only when the story clearly has that many real beats", plus a padding test. Asked for fifteen, the model was simultaneously told to produce exactly that count and that this many was probably padding, and it resolved the contradiction by rationing one thin premise across the shots. It is now told the opposite: a high count is an instruction to invent more story — another location, a second complication, a character who arrives partway through, a reversal that changes what the earlier shots meant. The no-padding rule is unchanged and is exactly the point; the way to satisfy a high count is to invent enough real plot that no shot is padding.
When the brief names only characters and a genre, the writer is now told plainly that it is the author: decide who wants what, the incident that starts it, the complication, the turn and the ending before writing any shot. Returning a series of moods involving the named characters is called out as the most common failure on a thin brief.
Dialogue density now has a floor. Speech was marked optional per shot and non-speaking shots were actively encouraged, with nothing on the other side, so long pieces came back nearly silent. It now aims for two thirds or more of shots to carry speech, with silent shots as punctuation.
And a new FIT THE SHOT section covering both dialogue and action. Overrunning the clip is what produces crammed, garbled speech and distorted motion, so the writer now budgets each shot — settle, action, line at an unhurried pace, a beat to land on — and cuts the action rather than speeding up the speech. One clear physical action per shot; if more than one is written, split the shot.
Note on saved workflows
Every new control in this release is appended at the end of the node's input list. Widget values are stored positionally, so inserting a control anywhere else silently shifts every value after it in workflows you have already saved. Your existing graphs load unchanged.
2.2.3 — three ways a long render could waste itself, and a switch that did nothing
Everything here is a fix. Nothing changed about how you use the workflow.
Fixed: long multi-shot renders died at the very last step
A long job would sample every shot, upscale every shot, finish master normalize — and then die while assembling the result, with nothing written. Every expensive stage had already succeeded.
RuntimeError: DefaultCPUAllocator: not enough memory:
you tried to allocate 33791016960 bytes.That is host RAM, not VRAM, and it is one contiguous request. Upscaled frames were held on the host as fp32 for the whole run, and the final assembly then allocated a second complete timeline while the first was still alive. Peak was twice the timeline, and the timeline grows as shots × frames × height × width × 3 × 4 bytes with the upscale factor squared. Six shots of 243 frames upscaled is a 36.5 GB timeline and a ~70 GB peak, which no 64 GB machine can satisfy.
Three changes: the timeline is now assembled into one preallocated buffer, releasing each shot as it is copied (bit-exact — the same bytes as before); frames are parked as fp16, which is well clear of the 8-bit the encoder writes anyway; and master normalize upcasts to fp32 for its arithmetic and writes back, so the memory saving costs no precision.
Effect: six shots of 243 frames upscaled drops from ~70 GB peak to ~21 GB. Twelve shots of 192 frames drops from ~58 GB to ~31 GB. Both now fit in 64 GB with the per-shot upscale unchanged. Diagnosed by a second operator on a 3090 box; thank you.
Fixed: the auto-reserve inflated its own estimate until the render spilled
If your chains got slower as they went — shot 1 fine, shot 3 sluggish, shot 4 crawling — this is why. After each shot the pack measures how much activation memory that shape needed. It subtracted the weight bytes currently resident rather than the model's full size, so whenever the model was only partly loaded the measurement came out too large by exactly the amount that had been offloaded. That number then raised the next shot's reserve, which left less room for the weights, which offloaded more.
shot 1 recorded 6.4 GB true 6.4 (full load - correct)
shot 2 recorded 8.0 GB true 5.3 (2749 MB offloaded)
shot 3 recorded 8.9 GB true 4.3 (4784 MB offloaded)
shot 4 asked for 19.9 GB -> clamped -> 262 s/stepThe overstatement equals the offload every time. The measurement now uses the model's full size, and a shot that offloaded weights records nothing at all — it measured a spill, not an activation pool.
The cache also only ever grew, so a single bad shot poisoned a shape permanently. Entries written by earlier versions are dropped once on first load; you will see a line saying so, and those shapes re-measure on their next run.
This does not make an over-committed render fit. If a shot honestly needs more memory than the card has you still have to lower frames_per_shot or resolution — the difference is that the warning now fires on true numbers instead of the reserve quietly climbing.
Fixed: the sol_attn and chunk_ffn switches did nothing
Reported by sdktertiaire2. Those switches shipped ON, but the nodes they gate ship bypassed — they need third-party packs that cannot be bundled (ComfyUI-sol-attn, comfyui-minimax-h3-blockcache-T8). A toggle routing into a disabled node changes nothing and warns about nothing. That contradictory default was mine.
Both now ship OFF so the panel states what the canvas actually does, and the panel is labelled with the fix: install the pack, then select the node and press Ctrl+B to un-bypass it. The switch only routes; it cannot enable a bypassed node. If you do not want those packs, leave the switches off and lose nothing — they are speed and memory optimisations, not quality features. Everything renders identically without them, just slower.
The nodes stay bypassed on purpose. Enabling them by default would hard-fail every install that lacks the optimizer packs.
2.2.2 — the reference block was forcing glasses on everybody
One fix, reported by a user against 2.2.1. If you render with reference images, take this one.
Fixed: reference renders forced glasses onto the subject
Reported on Civitai: a reference image produced a character wearing thick black frames in every shot, and prompting to remove them changed nothing.
The pack injects a retention_analysis block alongside reference images so identity and voice stop drifting across a chain. That block was hardcoded to say the subject "retains the same face, skin, hair, glasses and wardrobe" - so every ref2va render was told to keep glasses whatever the prompt said. And it could not be argued with, because the sampler runs on a BasicGuider: cfg 1.0, no negative branch. There is nothing for a negative statement to subtract from, so "remove the glasses" only put the word into the conditioning a second time.
The block now says "the same face, skin and hair" and names no accessories. Eyewear, hats and jewellery belong to your prompt.
The rule this exposes, and it applies to every prompt you write for this graph: anything hardcoded into unconditional conditioning is permanent from your side. At cfg 1.0 you can add but never subtract, so phrase everything positively - "clear unobstructed eyes", never "no glasses". Negations do not work anywhere in this pack.
2.2.1 — three crashes, all of them after the expensive part
A bug-fix release. Four defects, three reported from real renders on real machines rather than found in review. If you have been unable to finish a long chain, or a high-resolution one died partway with the whole server going down with it, this is the release for you.
Fixed: two crashes that only appear on long or high-resolution chains
Both were DefaultCPUAllocator: not enough memory — system RAM, not VRAM — and both killed the job after it had already done the sampling.
master_normalizebuilt the entire finished timeline four times over. A 12-shot 1088x1920 chain asked for a single 31.2 GB block and died on a 24 GB machine after 81 minutes of work. One of those four copies was pure waste: the code rebuilt a tensor byte-for-byte identical to one already in memory, purely to compute two numbers for a log line, then threw it away. Every statistic it needs is one-dimensional. It now measures per shot and releases each shot as it is consumed, so peak memory is one finished timeline plus one shot instead of four timelines. Output is bit-identical — verified against the old implementation across every colour mode and median width, on the pixels and on the log strings.The upscaler was handed every frame at once. 243 frames at 1472x2560 is an 11.0 GB float32 allocation on top of the input. It now runs in chunks into a preallocated buffer, so peak is one chunk rather than the whole batch. Bit-identical, verified at two chunk sizes. Set
H3_UPSCALE_CHUNKif you want to trade memory for a little speed; default 16.
Fixed: auto-reserve could clamp itself into a hard crash
On a 12-shot 1280x736 run, shot 1 loaded completely and rendered fine. Shot 2 carries the context pin and the reference rows, so its activation-pool requirement roughly doubles — the node correctly costed it at 18.2 GB. It then clamped the pool to 9.4 GB "so the weights still load completely", and the weights loaded partially anyway, 401 MB offloaded. Neither constraint was met and the render aborted inside a CUDA kernel, taking the whole ComfyUI process with it.
Two causes, both fixed. The keepout was 384 MB, which did not cover ComfyUI's own buffer plus the difference between what the driver reports free and what ComfyUI treats as usable — about 400 MB short. It is now 1 GB. And the "this is tight" warning compared the clamped reserve against a previous shot's measurement rather than what this shot had just asked for, so a 9.4 GB clamp looked fine against shot 1's 9.1 GB while the payload needed 18.2. It now compares against the real request and says plainly that this usually dies inside a kernel, and that raising the reserve cannot help because the memory is not there.
Worth knowing regardless of this fix: shot 1 succeeding tells you nothing about shot 2. Shot 1 has no pin and no reference rows. If shot 2 will not fit, lower frames_per_shot or the resolution, or load a smaller quantisation of the DiT.
Fixed: start_image on the memory sampler silently did nothing
Reported by a user. start_image on H3MultishotMemorySampler is an identity reference row, not a first frame — and on an fl2va checkpoint, which has no reference rows, it is built and then ignored entirely. Meanwhile the sibling node H3MultishotSampler has an input with the same name that really is an I2V first frame, which is where the perfectly reasonable expectation comes from. The combination produced no warning at all.
It now prints one, naming both ways to actually open shot 1 on a picture: use H3MultishotSampler, whose start_image is a true first frame, or set continuity=flf_chain here and feed keyframe_images (N+1 stills for N shots). Note that continuity=first_frame is not about your image either — it hands over the previous shot's last frame, and does nothing on shot 1.
Verified
Three chained shots rendered end to end on ComfyUI 0.32.0 with the new auto-reserve: every shot full load: True, which is exactly where the crashing run went partial, and the new warning fired on shot 2 with real numbers and did not stop the render. master_normalize ran on that same 685-frame chain. The chunked upscaler is proven bit-identical by test rather than by render — that graph had upscaling off, so I will not claim end-to-end coverage it did not get.
2.2.0 — everything since 2.1.2, in one post
Seven point releases folded into one version, because 2.1.2 is where most people still are. Five separate defects that stopped the workflow running, a measured campaign against drift over long chains that changed the shipped defaults, and a whole ComfyUI version this pack could not previously run on.
the Audio Spine produced static on ComfyUI 0.32.0
naming an mmproj file broke GGUF text encoders
The value 1 for reference_image_size is not availablethe full workflow needed a node from a pack that is not in the zip
H3_Seamless_Chain_COREcould not be queued at all — the no-third-party-dependencies workflow, un-runnable since 2.1.3
New controls: pin_frames, pin_noise, pin_renorm, and master_normalize's luma+contrast mode. Changed defaults that change your output: memory_frames 2 → 0, and join_anchor_noise / handoff_release to 0 because they are inert under context_pin. New runtime: ComfyUI 0.32.0; 0.30.0 is unchanged and still supported.
Everything below is the original note for each version, newest first.
2.1.9 — CORE could not be queued, and bypassed nodes now say what they need
If you use H3_Seamless_Chain_CORE, it has been un-runnable since 2.1.3. Queueing it failed with:
value_smaller_than_min: Value 0.0 smaller than min of 1.0 - output_scale
Its sampler still carried the widget array it was saved with before 2.1.3 removed the four two_pass_upscale dials. Nineteen saved values against the class's fourteen live widgets, so the frontend put false into output_scale (minimum 1.0) and 1.5 into save_every_shot, and the server rejected the whole prompt. The workflow advertised as the one with no third-party dependencies — the safest thing for a new user to open — was the one that could not run.
It survived because CORE had never been rendered end to end. Our own release checklist said "one CORE render before posting" and that line rode along, unticked, through six releases.
The array is now generated from a name→value map resolved against the server's own schema, with the map stored on the node so it can be re-applied by name. CORE has now rendered: three chained shots at 960x544, 370 frames, picture and audio, holding framing, wardrobe and lighting across both joins. All three bundled workflows are submit-tested against a running server now, not read.
Bypassed nodes now say what they need
Four nodes ship bypassed because their packs are not in this zip, and a node whose class is missing fails the entire prompt. That is the right default — but a bypassed node sitting on the canvas invites you to un-bypass it, and doing that without the pack installed breaks the workflow with no explanation.
It now says so in four places: the group title, each node's own title (which names its pack), a note above the cluster, and the VRAM / SPEED note, which carries the full table with repository URLs.
attention patch,chunk FFN— ComfyUI-sol-attnblock cache— comfyui-minimax-h3-blockcache-T8SCRIPT PREVIEW— ComfyUI-Custom-Scripts. A leaf: leaving it bypassed costs you the on-canvas preview and nothing else.
The part people lose is that it takes two steps, in order: install the pack, Ctrl+B the node to un-bypass it, then turn its switch on. Un-bypassing alone does nothing; flipping the switch while the node is still bypassed does nothing either.
Why not bundle those packs? The prompt-writer pack is bundled because it is modified and pinned to this workflow. These three are not — shipping a second copy of Custom-Scripts, sol-attn or blockcache-T8 inside the zip would shadow whatever you already have installed and freeze it at whatever version we happened to vendor. Naming them and linking them is the honest version.
2.1.8 — the last node that came from outside the zip
SCRIPT PREVIEW is ShowText from ComfyUI-Custom-Scripts, and it shipped active. A node whose class is missing serialises as class_type: null and the server rejects the whole prompt — so without that pack installed the workflow could not be queued, for the sake of an on-canvas text box. The install notes said to delete the node, which only helps someone who reads them before pressing Queue.
It now ships bypassed, the same remedy the three accelerator nodes got in 2.1.4. Install Custom-Scripts and Ctrl+B the node if you want the preview; the writer feeds the sampler either way.
Found by mapping every node type in all three bundled workflows to its owning Python module against a running server — now part of the release routine rather than something I do after someone tells me. The other two workflows were already clean, and everything else in the full one is core ComfyUI, this pack, the writer pack that is in the zip, or one of the three bypassed accelerators.
Verified end to end on ComfyUI 0.32.0
Not by reading the graph — by loading the shipped file in a browser on a 0.32.0 install and pressing Queue. Twice: once through the prompt-writer lane exactly as shipped, once with a hand-written script.
loads with no missing node types, and every sampler widget lands on its own name — the shift behind
The value 1 for reference_image_size is not availablecannot reproduceno null
class_type: the bypassed preview and the three bypassed accelerators all drop cleanlythree chained shots at 960x544, 124 frames each, decoding to 328 frames — exactly 372 minus the two 22-frame
context_pinhead trimsa reviewer given the clip cold, with no idea how it was made, read it as one continuous static take, found no cut or jump anywhere, transcribed every line, called the lip-sync matching, and found no shift in framing, colour, brightness or wardrobe and no hiss, dropout or click in the audio
0.30.0 remains the version everything else here was measured on. Both are tested before release now.
2.1.7 — three things that stopped it running
If you installed from Civitai, you have a package with three separate blockers in it. All three are fixed here. Two of them only ever appear on ComfyUI 0.32.0, which is why they survived several releases — my test rig was 0.30.0 and could not reproduce them. Everything below is now verified on 0.32.0 as well.
Fixed: the Audio Spine produced static on ComfyUI 0.32.0
guide_audio came out as hiss, while the same file through voice_ref or the native node was perfect. Reported against 2.1.2 and still present through 2.1.6.
0.32.0 introduced ModelSamplingAV, which carries the audio half of the packed audio+video latent scaled onto the video schedule — multiplied by shift / audio_shift going in (12/3 = 4 for H3) and divided back coming out. Everything this pack injects into the sampler's latent is in the stream's native domain, so on 0.32 it landed four times too small and decoded as broadband noise. On 0.30.0 there is no such scaling, so the identical code was correct there.
Three injection sites had it, not one: the encoded spine, the previous shot's audio tail (which audio_lock and the latent handoff use), and the encoded room tone for the onset guard. All three now scale at the point of use, reading the factor off the live model object so a changed sigma shift stays correct, and leaving 0.30.0 byte-identical.
Verified by render on 0.32.0 with a real 44.1 kHz stereo voice track: loudness-envelope correlation against the guide +0.965, speech-band energy 34.2% against the guide's 39.3%, and a blind listener transcribing the guide's own words as "clean, no background hiss". The same render was hiss before the fix.
Fixed: naming an mmproj file broke GGUF text encoders
mat1 and mat2 shapes cannot be multiplied (3680x1152 and 3456x1152)— thrown at the handoff into shot 2, on GGUF encoders only, and unaffected by resolution or by switching every image input off.
Setting mmproj_name explicitly took a different code path than (auto) and skipped the vision key-renaming step, so the vision tower loaded under raw llama.cpp names that nothing downstream reads. Same file, two loaders, 19 key names in common out of 351. The bitter part is that mmproj_name is documented as the reliable escape hatch for when filename pairing fails — and it was the broken path.
It now runs the same post-processing as (auto), using ComfyUI-GGUF's own key map rather than a copy, so it follows their changes. Verified: an explicitly named file now produces a state dict identical to (auto). A new guard also fails by name if a chosen mmproj yields no vision tensors, rather than dying in a matmul twenty minutes into a render.
Fixed: The value 1 for reference_image_size is not available
The shipped workflow's saved widget values were written for a layout that did not present sampler_override and scheduler_override as widgets. The current schema does, so everything from index 27 read two slots early and output_scale's 1.0 landed in reference_image_size. This affected 2.1.3, 2.1.4 and 2.1.5. The array is now generated from a name→value map resolved against the server's own schema, with that map stored in the node so it can be re-applied by name if anything shifts again.
Also: memory_frames now defaults to 0
The memory bank's recent slots hand each shot's accreted output forward as reference images, on top of the latent pin, so invented detail compounds. Ten shots at 960x544, moving 2 → 0:
memory_frames 2 memory_frames 0
texture per hop 1.055 1.022
chroma per hop 1.086 1.039
framing correlation at shot 10 0.976 0.995
drift acceleration 4.2 -> 6.7%/hop 2.3 -> 2.0%/hopThe last row matters most. At 2 the drift accelerates, which is what a runaway feedback loop looks like; at 0 it holds flat.
The obvious worry was motion continuity, since carrying it is what the recency slots are for. Tested on a scene with continuous large-amplitude movement: anchor-only retained motion slightly better (−5.9% against −6.5% over four shots) with better framing (0.983 against 0.971). The cost does not exist. If a busy scene ever does lose continuity between shots, raise it to 1.
join_anchor_noise and handoff_release now ship at 0. Both are inert under context_pin — one noises keyframes the mode never creates, the other belongs to latent_handoff — and non-zero values read as tuned settings while doing nothing at all.
2.1.6 — chained shots stop getting brighter-edged
master_normalize matched every frame's mean to one global target, which is why a chain shows no brightness step. It was also masking a second drift it never touched.
Measured on a three-shot chain at 960x544: the mean held flat, 27.31 → 27.43, while the distribution stretched — p25 fell 6 → 2 and p95 rose 85 → 96. Contrast climbing every hop, re-centred each time, and handed to the next shot as a higher-contrast start.
New: master_normalize=luma+contrast, now the default, matches the spread as well as the mean. Rescaling amplitude about each frame's own mean is an affine remap — it moves no edges, so it is not the blur this pack has always ruled out for texture drift.
Texture growth per hop, from a log fit across all shots. 1.000 is no accretion at all:
where luma luma+contrast contrast spread
960x544, in-render, 124f x4 1.126 1.047 11.2% -> 0.3%
960x544, 243f x3 1.199 1.064 12.2% -> 0.4%
640x352, 243f x3 1.130 1.055 7.0% -> 0.2%The baseline ratchet scales with canvas (1.130 at 640x352, 1.199 at 960x544); after normalising it stops caring (1.055 against 1.064). There is nothing in it to tune per resolution — it works on decoded frames with per-frame statistics against one global target.
The target anchors to shot 1, not the timeline median. Contrast only ratchets upward, so shot 1 is the one frame-set with nothing accreted onto it. A median target pulls shot 1 up to meet the drift (+11.7% texture, for no benefit); anchoring to shot 1 leaves it untouched (+0.1%) and only ever pulls later shots down.
1:1 crops of the last shot show no loss of real detail — lamp vent slots, hinge rivets, hair strands and knit weave all survive. What leaves is the invented crispness.
What is left, honestly
About 1.05 per hop. That residual is spatial accretion and this pass cannot reach it: master_normalize runs on the finished master, outside the feedback loop, so it cleans what you see while the next shot is still handed the inflated pin. Four shots is slight. Ten shots is roughly +50%. If you are chaining long, expect it.
Correction to 2.1.5
2.1.5 said pin_noise=0.05 fixed this. It does not. That was measured on two seeds of a single scene at 640x352 — a scene with a nearly black background that barely ratcheted to begin with — and the control that would have caught it, pin_noise=0.00 at the reporting user's own resolution, had never been run. With it run:
canvas 0.00 0.05 change
640x352 1.131 1.111 -1.8%
960x544 1.211 1.201 -0.9%Both on a detail-heavy scene. The dial is small and scene-dependent, it cannot touch the dominant drift in a busy frame, and above 0.10 it gets worse (0.20 measured 1.228 against a 1.211 control). Its range now stops at 0.10 and its tooltip says so. It stays in the pack because it costs nothing and does help where the ratchet is already small — it is not the fix.
Not tested
Portrait canvases. Everything above is landscape, 640x352 and 960x544. The mechanism is resolution-independent by construction and the two landscape sizes agree, but 768x1344 and 736x1280 have not been measured and are not claimed.
If you measure this yourself
Texture comparisons only mean anything while the framing holds. If the model cuts to a different setup, texture reflects content and the number is meaningless — one portrait run here scored a flattering 0.878 per hop purely because shot 3 cut to a close-up of a film reel. Correlate each shot's mean frame against shot 1's before trusting any of it; a held framing sits above 0.95.
That cut is worth a writing rule of its own: do not name a nearby object in a shot's closing beat. “She glances down at the reel” reads as a request for a shot of the reel. Keep closing beats on the speaker's own body.
2.1.5 — widget slots, and two dials
Fixed: the shipped workflow would not queue. The value 1 for reference_image_size is not available. The saved widget array was written for a layout that did not present sampler_override and scheduler_override as widgets, so everything from index 27 read two slots early — output_scale's 1.0 landed in reference_image_size, a combo of match/max. This affected 2.1.3, 2.1.4 and 2.1.5. The array is now generated from a name→value map resolved against /object_info, and the same map is stored in the node's properties so the pack's own JS can re-apply values by name if a future schema change shifts anything again.
New: pin_frames (default 22) exposes the pin length, previously hard-coded. Only the Motion-Context node's latent-aligned values are offered: 22 / 5 / 39 / 56. Longer pins cut the ratchet a little more, but the head trim scales with them, and a blind review of the 39 arm caught a jump at the third join with a line clipped from “So it stays in the drawer” to “…is in the drawer”. Leave it at 22.
2.1.4 — two user-reported breakages
Fixed: the main workflow would not queue without two third-party packs
H3_Seamless_Chain_v2.json carries three optional accelerator nodes — sol_attn and chunk_ffn from ComfyUI-sol-attn, and block_cache from comfyui-minimax-h3-blockcache-T8. They were saved active, so on an install without those packs the graph could not be queued at all:
missing_node_type: Node 'attention patch (gated)' has no class_type— even though the shipped recipe keeps all three gates off and never touches them. They now ship bypassed (purple). A bypassed node is dropped from the prompt entirely and the model passes straight through, so the workflow queues with neither pack installed. To actually use one: install its pack, select the node, Ctrl+B to un-bypass, then turn its gate switch on. Both steps.
Fixed: Value 4 bigger than max of 3: memory_frames on a graph you never edited
Mine, not yours. v1.2 inserted a new seed_per_shot control into the middle of the sampler's input list. ComfyUI stores widget values as a positional array, so in any workflow saved on v1.0 or v1.1 every dial after that point shifted by one — your old anchor_frames was arriving as memory_frames. The error named a dial you had nothing to do with.
Fixed in both directions. Pre-v1.2 workflows are detected as they load and repaired (save the workflow to make it permanent), and the sampler now also stores its values by name, so no future change to the input list can shift anything again. If a bad value still reaches the queue, the validation error explains the shift instead of pointing at the wrong dial.
2.1.3 — the upscale, honestly
two_pass_upscale is removed. It interpolated the raw latent between passes. H3's latent is not a spatially smooth representation, so interpolated values landed off-manifold and the second pass, running at low sigma, had no room to pull them back. Every configuration tested came back as colour-noise mush against clean single-pass controls — including at 14 steps with beta57, the recipe the settings file itself called render-verified, and including shot 1, which carries no pin at all. It was never a context_pin incompatibility. It did not work in any mode, and the guard that used to point at context_pin is gone with it.
Two replacements, both applied after decode, where nothing can leave the latent manifold, and both per shot, so a long chain never holds a full upscaled master in memory at once — which is exactly the failure that cost one user a three-hour render.
output_scale— a lanczos resize of each shot's finished frames. It adds resolution, not detail, and the measurement is worth knowing before you choose: rendering at 448x256 withoutput_scale 1.5reached 672x384 in 45.5 s against 80.9 s rendering 672x384 natively — 1.78x faster, and visibly softer. Concrete pore texture that survives a native render washes out. Use it when the clock matters or the whole-chain upscale will not fit; render native when texture matters.upscale_model— optionalUPSCALE_MODELinput (ComfyUI's own Load Upscale Model — ESRGAN and friends) to synthesise detail instead of resizing, per shot, at the model's own factor. Implemented but not render-verified: no upscale model was installed on the test rig, and I would rather say so than imply otherwise.
Both run after the memory bank has taken its base-resolution reference clip, so conditioning and VRAM are identical to an un-upscaled run, and the latents the node returns stay base-resolution.
New: the sampler returns its latents
The memory sampler now outputs video_latents, audio_latents and head_frames — every shot exactly as sampled, batched along dim 0, untrimmed. Shots after the first open with head_frames of replayed material from the previous tail, and that is only removed at decode, so these do not line up with the master until you trim it yourself. They are deliberately raw: the pin material cannot be recovered later, and trimming on your behalf would throw it away. Requested in issue #12, and the requester picked exactly this shape.
Verified rather than asserted: 124-frame shots return 37 latent rows, matching 5*((F-5)//17)+2, and 124 + 124 − 22 is exactly the 226-frame master.
If you saved your own copy of a v2.1.2 graph, reload the shipped workflow. Removing four widgets shifts the saved widget order on that node.
2.1.2 — reference images, per-shot saves, and six repairs
One new capability and a set of fixes. The chaining itself is untouched: nothing here changes how shots join, so a graph that renders well today renders identically after updating.
New: reference images actually have a way in
The sampler's reference_images input has existed since 2.0, and SETTINGS.md documented it — as unwired, because nothing in the shipped workflow was connected to it. There was no way to use it without building the lane yourself.
The full workflow now carries a REFERENCE lane in the anchors column: two image loaders → ImageBatch → a gate → the sampler. It ships with the gate off, so nothing changes until you turn it on. Point the loaders at portraits of your character, flip the gate, and they ride into every shot as <Picture 1>, <Picture 2>. Chain another ImageBatch for a third and fourth. Needs a ref2va checkpoint.
Unlike the identity anchor above it, these are not a first frame — they do not constrain shot 1's composition, they only carry who the person is. That also makes them the thing that covers shot 1, where the memory bank is still empty and has nothing to carry identity from yet.
New: every shot can be saved as it renders
A chain only became a file at the very end, when the master was muxed. So anything that failed after the last shot destroyed the entire run — and one report was three hours lost to an out-of-memory error at the mux, after every shot had already rendered successfully. The work existed and was thrown away, which is the worst version of that bug rather than the mildest.
save_every_shot (on both samplers) writes each shot to output/video/H3_SHOTS/ the moment it decodes, alongside the master. If the mux dies you have every shot on disk and a joining job instead of a lost day. Files are written before the seam trim, so consecutive shots overlap by about a second — the master is still the clean join, these are the safety copy. Requested in issue #13.
New: custom sigma schedules
The samplers built the schedule themselves from steps + scheduler, with no way to supply your own — so a turbo LoRA that ships the curve it needs could not use it, and ran wrong rather than refusing. Both samplers now take an optional SIGMAS input. Connect one and it replaces the schedule entirely, steps rebinds to len(sigmas)-1 so the two-pass upscale split is taken as a fraction of your curve, and the console states that the steps and scheduler widgets are being ignored instead of silently overriding you. Link-only input, so saved graphs load unchanged. Issue #14.
Fixed: --- separators were ignored in passthrough mode
example_script.txt ships --- separated and every document tells you to write scripts that way, but the writer's passthrough path returned the whole file as ONE shot — which the sampler then repeated to fill shot_count. Pasting a finished four-shot script rendered the entire text as shot 1, four times over. It now splits on exactly the same rule the sampler uses. A single paragraph is still one shot, so .txt batches are unaffected.
Fixed: a stale prompt-set filename killed the whole queue
ComfyUI validates every combo value in a graph before it runs anything. If RiftPromptSource's saved source_file no longer existed — a renamed folder, a workflow shared from another machine, or the prompt lane simply switched to manual — the queue failed with Value not in list and nothing ran, including the lanes that were perfectly fine. Switching to manual did not disable it, because validation happens before the switch is ever consulted.
The node now declares VALIDATE_INPUTS, so the filename is resolved only if the node actually executes. Manual mode now genuinely disables it. If it does run and the file is missing, the error names the file.
Fixed: the writer
These three only matter if you let the LLM write your shots. If you paste your own shot list, they change nothing for you.
Every story came out 15 shots. The system prompt ordered exactly 15 whenever the brief didn't ask for a count, so the model never got to decide. It now counts the story's beats and lands where the story lands — measured 4–7 on ordinary briefs, and about 7 when the brief gives no length signal at all. Seven chained shots is roughly 65 seconds at 243 frames, which clears the 1-minute mark platforms pay on. Ask for a specific count and you still get exactly that count.
The
modedropdown did nothing. The shipped workflow'ssystem_promptbox held a frozen copy of the long-story prompt, and a filled box overrides the per-mode prompt file — so every mode ran the long prompt no matter what the dropdown said, and prompt updates in the pack never reached anyone using the shipped graph. The box now ships empty and the dropdown works. If you saved your own copy of the v2.0 or v2.1 workflow, clear that box by hand — your saved graph still carries the old frozen prompt, and this update cannot reach it. Related:short_storyis now 1–3 shots instead of always exactly 1.A messy answer from the model no longer kills the render. Three separate real failures, all seen on local models: a markdown fence sharing a line with the payload (
```json {"prompts":…) was destroyed by the fence stripper; a reply truncated at the token ceiling left no balanced object to recover; and parsing sat outside the retry loop, so one malformed answer ended a run after a 100–240 s call that a re-ask usually fixes. The order is now clean → parse → retry ×3 → salvage the shots that completed → fail, and the final error namespassthroughmode as the escape hatch.
Verification, plainly. The reference lane, the per-shot saves and the --- fix are all confirmed by a live chained render, not by inspection: the console logs 2 reference image(s) ride in every shot as <Picture 1..2>, PASSTHROUGH: 4 shot(s) (it would have said 1 before the fix), and shot 1/4 saved through shot 4/4 saved, with four files on disk. The three JSON failures are unit-tested against the actual captured payloads that caused them; the live re-run afterwards parsed on the first attempt, so that proved no regression rather than proving the salvage path fires in a real queue. Shot counts were measured over two runs of five briefs on a local 8B: 4–7 shots, ±1 between runs — before the change all five returned 15. The custom sigmas input has not been exercised by a render — it needs a schedule source wired in, and I would rather say so than imply it.
Fixed: the Audio Spine produced static with real audio files
User-reported on ref2va: a voice track through guide_audio came out as noise, while the same file worked through the native reference node. The spine encoded the track at whatever sample rate the file arrived in; the audio VAE expects 32 kHz, and the native node resamples where the spine path did not. Nearly every real voice or music file is 44.1/48 kHz, so the spine's latent was garbage - and the spine locks every sampling step to it, which is why the output was noise rather than merely degraded.
Fixed: the spine resamples to the VAE's rate and upmixes mono to stereo, with a console line saying so. Measured, not asserted: guide-to-output correlation went from 0.06 to 0.97 on a 48 kHz voice track - identical to a native-rate control - and the same at 44.1 kHz mono. While in there, the tooltip's claim that the spine is latent_handoff-only was simply wrong: it works with every continuity mode and is render-verified on context_pin. If you want the music-video path, this is it: wire the song into guide_audio and every shot's audio locks to its slice of the track.
Measured: audio dulls over long chains — and the counters
The docs have long carried a claim (from upstream notes) that audio gets duller the longer the chain runs. Measured, five arms of 8-shot chains, one factor at a time: it is real, and it is the audio twin of the seam sharpening ratchet running the other way. Each shot's audio is regenerated conditioned on the previous shot's; regeneration regresses toward the spectral mean; the output becomes the next shot's conditioning.
With
bank_pinned=0— pure recency conditioning — the voice band collapses: 84–92% of 4–10 kHz energy gone by shot 8. There is no true "bank off": zeroing both bank widgets leaves exactly one recency slot, which is the worst configuration, and the node now warns about it on chains past 4 shots.With the default pinned slot the drift is seed-dependent — 8% to 50% over 8 shots across the seeds tested. The pinned slot slows the loop; it does not break it.
audio_tone_control = flatten(new, default off): EQ-matches every shot's long-term spectral envelope to shot 1's before the weld — constant per-shot gains, clamped ±9 dB, half-strength in the top band so it cannot manufacture hiss. Paired A/B on the worst seed measured: HF loss halved (−49.5% → −23.7%), rolloff drift cut to a third. It reduces the drift, it does not eliminate it — thecontext_pinreplay carries raw latents the EQ cannot reach — and it ships off until ears, not spectra, have judged it. (First ear test: marginally preferred, "but close.")The best-audio recipe needs no new code:
continuity=cut+bank_pinned=1, memory_frames=0gives every shot exactly one audio reference - shot 1, forever. Measured flat (−5% over 8 shots, both seeds tested) where the seamless default lost 8–50%. The joins are cuts rather than a continuous take; identity and scene still hold. Pick by grammar: interviews and episodic content can have the flat audio, one-take content pays the pin's audio tax and levels it withflatten.Picture darkening over chains: same mechanism, and the dial already existed. A user-grade chain measured −1.5 luma per shot, monotonic.
color_level=mvgd— shipped since 2.1 but never verified — holds an 8-shot chain to −1.0 total where uncorrected loses −10.5, at both seeds tested. On chains past ~5 shots, turn on all three drift dials:chain_gain_control=flatten(texture up),audio_tone_control=flatten(sound down),color_level=mvgd(brightness down). One mechanism, three lanes — the model regenerating from its own output.
Correction, 2026-08-12. An earlier build of these notes recommended turning on three per-shot drift dials. A render with all of them on came out +142% texture and +18% brighter over three shots, with a visible brightness step at each join. Per-shot correction cannot work: under context_pin the drift is carried by the raw latent pin, stored before decode, while every one of those dials operates on decoded frames. They fix what you see, not what feeds forward. audio_tone_control is removed; color_level=mvgd is deprecated (29% warmth step at every join, per its own source comment). Use color_level=scene and the new master_normalize=luma — both run on the finished master, outside the loop, driving every frame to one global target, so neither can create a seam. Texture drift has no honest after-the-fact fix: the only lever is blur, and blur removes real detail along with the invented kind.
Install
Unzip. Copy both node folders into
ComfyUI/custom_nodes/:ComfyUI-H3-Multishot/ the sampler and helper nodes ComfyUI_JoyAI_Echo_GGUF_Nodes/ the LLM prompt writer (full workflow only)Restart ComfyUI. ComfyUI v0.30.0 or newer is required — that is the release with native MiniMax-H3 support.
Load a workflow from
workflows/through the workflow menu.
The full workflow needs five packs (two are in this zip): the writer pack below, ComfyUI-H3-Motion-Context for the context_pin default, RES4LYF for the beta57 scheduler, and ComfyUI-sol-attn + comfyui-minimax-h3-blockcache-T8 for the VRAM panel; ComfyUI-Custom-Scripts adds the script preview. ComfyUI validates every node class before it will queue, so a missing pack stops the whole workflow rather than just its own feature — INSTALL.md lists how to remove each one instead if you would rather not install it. CORE needs none of them, and that is tested on a clean install.
On the Motion-Context fork. There is an active fork, ethanfel/ComfyUI-MiniMaxH3-Contex-Loop, well ahead of upstream with a disk-backed chain/loop system. It is a complement, not a replacement: by design it leaves the MiniMaxH3MotionContext node id to NikoDemon80's pack. Install the fork instead of upstream and context_pin still fails, because the node it calls is not there. Install both - they are built to coexist, and this pack works with either one's runtime patches. As of 2.1.1 the error message says so directly when it detects the fork.
The writer pack is RealRebelAI's (github.com/RealRebelAI/ComfyUI_JoyAI_Echo_GGUF_Nodes), modified so the workflow's join rules actually reach the model; NOTICE_RIFT_MODIFICATIONS.md inside it lists every change. If you already have that pack, replace it with this copy. The CORE workflow does not need it at all.
Models you need
checkpoint MiniMax-H3 ref2va (GGUF Q8_0 / Q5_1 / Q4_0) -> models/diffusion_models
text encoder qwen3vl minimax_h3 (+ its -mmproj sidecar) -> models/text_encoders
video VAE minimax_h3_video_vae -> models/vae
audio VAE minimax_h3_audio_vae -> models/vaeGGUF quants: huggingface.co/joeygambino/MiniMax-H3-GGUF — Q8_0 for 32 GB, Q5_1 for 24 GB, Q4_0 below that.
GGUF encoder pairing. ComfyUI-GGUF matches the -mmproj vision sidecar to the encoder by filename, in the encoder's own folder. Rename either, or split them up, and it loads the encoder without its vision tower — which presents as the model ignoring your reference image. This pack's CLIP loader raises instead of continuing blind, uses the only mmproj beside the encoder when there is exactly one, and takes an mmproj_name widget so you can point at the file directly.
Which workflow
H3_Seamless_Chain_CORE — start here. The same seamless chaining with zero third-party packs. Type shots into the script box and queue.
H3_Seamless_Chain_v2 — everything: master controls, LLM writer, VRAM panel, identity and voice anchors, episode/batch prompt source, boundary plates, audio spine. Optional lanes are gated off by default.
H3_Keyframes — one clip, anchors at chosen frame positions, per-anchor condition strength.
The two things that stop people on the first run
1. The prompt writer needs a model you have pulled
The full workflow points at a local Ollama with model_name = qwen3:14b. If it is not pulled, the first queue stops immediately:
LLM API error 404: model 'qwen3:14b' not foundFix: ollama pull qwen3:14b. Any OpenAI-compatible endpoint works — its URL in base_url, its exact tag in model_name. ollama list prints the tags you have, and it must match character for character.
No LLM at all? Set the master panel's use_file_prompts to manual entry, delete the writer, and feed your own script into the sampler's script input — one prompt per shot, separated by --- on its own line. CORE already works this way.
2. A local writer will fight the video model for the card
Turn on unload_model_after on the writer. It frees its own model from Ollama the moment the script is written. Without it the model stays resident for the server's default five minutes — your whole first shot. ComfyUI's own eviction cannot reach it, because Ollama is a separate process with its own allocator, and Ollama's OpenAI-compatible endpoint has no keep_alive field to ask with; the switch calls the native endpoint, which honours it. On under 32 GB, prefer a remote endpoint entirely.
Settings: start here, change nothing
checkpoint ref2va sampler euler
continuity context_pin scheduler beta57 (full) / beta (CORE)
steps 14 fps 24
frames/shot 362 (~15.1s, the trained maximum)
resolution 1280x736 landscape or 768x1344 verticalbeta57 comes from RES4LYF, not stock ComfyUI. Measured on an identical seed it scored 10/10 for lip-sync against 8/10 for stock beta, with image quality, skin texture, artifacts and audio judged equal — so the full workflow ships it and lists RES4LYF as required, while CORE ships beta and keeps its zero-third-party-pack promise. One widget either way.
Leave every VRAM switch off and the reserve at 0, and try a render before touching any of it. The activation reserve measures each shape and conditioning payload as it renders and sizes the pool itself; it holds on 24 GB cards as well as 32 GB. A hand-set reserve overrides that measurement, so a number that suited one shape becomes wrong for the next. Those switches exist to dig out of a spill the console has already named, not for pre-emptive tuning.
Resolution cannot change mid-chain, and the mux must stay at 24 fps — other rates audibly shift voice accents. Dial-by-dial reference in SETTINGS.md.
Writing a script that chains cleanly
The previous shot's last ~1 second is replayed at the head of the next and discarded. Four rules follow, and breaking them is what produces mid-word chops and pose jumps:
Open holding. Every shot after the first opens in the previous shot's exact closing arrangement, with no dialogue for ~2 seconds. Give it real micro-motion — a breath, a weight shift — so it does not read as a freeze.
Land settled. Every shot ends with ~2 seconds of quiet, back in a stable arrangement, all dialogue finished.
Never split a line across shots. Dialogue plus 4 seconds of hold and settle must fit the shot length. If it does not fit, move the whole line to the next shot.
Repeat descriptions word-for-word. Character appearance and the room/light description, byte-identical in every shot. An unnamed light source gets reinvented per shot, and that is where colour drift starts.
The LLM writer applies these for you. Hand-written scripts must follow them — PROMPTING.md has a worked four-shot example, and example_script.txt is ready to paste.
How the chaining works
context_pin carries the previous shot's last 22 frames as raw latents — never decoded to pixels and re-encoded — placed at interior keyframe coordinates, with a timeline-placed audio reference alongside. The regenerated head is trimmed on decode. Colour, motion and voice cross the boundary as data rather than as a description.
Motion is the clearest case. Hand the next shot a single frame and it knows position but not velocity, so pace can reset at the boundary. Measured on a steady-pace walk: a single-frame anchor with no memory bank wobbled at the join; context_pin held it, and so did the memory bank on its own.
first_frame is the alternative — the model's own trained hand-off, no extra pack, and what CORE ships with. cut for episodic work.
Identity and voice
Nothing wired — the frame relay plus verbatim descriptions hold a face surprisingly well. A ~40 s two-character scene held both faces with no reference images at all.
self_anchor_voice(on) — shot 1's own rendered voice becomes the reference for every later shot. No file needed; write shot 1 with a clean solo line.voice_ref— a clean solo speech clip, pinned across the whole chain including shot 1.reference_images— character portraits carried into every shot as<Picture 1..N>. Bind them in the prompt text.seed_per_shot(leave on) — measured: varying the seed per shot holds the face; one seed for every shot drifted both face and voice. Identity lives in the conditioning, not the seed.
When something goes wrong
404, model not found — the writer's model is not pulled. See above.
A word clips at a join — the script put dialogue too close to a boundary. Move the whole line; do not split it.
Sharpening increases every shot — the texture ratchet. Set
chain_gain_controltoflatten; worth it past about 5 shots.Stalls at 0 steps, or runs several times slower than usual — a VRAM spill, the driver paging to system RAM instead of erroring. The console now names it. Raise the reserve, or drop resolution, frames, or reference payload.
Red or missing nodes — an optional pack is not installed. Delete those nodes, or use CORE.
GGUF architecture error — the pack teaches ComfyUI-GGUF the
minimax_h3architecture at startup. If it persists, runpython apply_gguf_arch_patch.pyfrom the pack folder once and restart.Audio dulls on a very long chain — expected; restart the chain on a scene cut, where a fresh start costs nothing.
Fixed in 2.1.1
context_pindied when ComfyUI-H3-Motion-Context was installed. Both packs patched the same ComfyUI method and Motion-Context refuses to stack on an unrecognised wrapper, so its payload patch failed and the chain errored. This pack's wrapper now does everything theirs does and declares their compatibility marker, so whichever loads first owns the site and the other stands down. Load order no longer matters. Verified with a livecontext_pinrender.seamless_tailcrashed mid-chain with Motion-Context installed ("only first/last keyframe anchors are supported") - after your first shot had already rendered. It needs interior keyframe anchors, which conflict with that pack; it now stops before any sampling with the alternatives named: usecontext_pin, orfirst_frame, or remove that pack.seamlessoften reads as a cut - now labeled. It is a legacy latent-only soft pin kept for comparison; the model satisfies it loosely. For a real join usecontext_pinorfirst_frame. The tooltip and settings reference now say so plainly.
Why these never showed in testing here: an install-layout difference disabled the conflict detection on the dev machine. That detection is fixed, and release testing now runs on a packaged clean install so the class cannot slip through again.
Everything you need is in the zip. One download: both node folders, all three workflows, and the full documentation. Nothing else to fetch, nobody to ask.
What changed in 2.1
The full workflow now works on a clean install. It referenced a prompt-source node that had never been published, and drove the writer through inputs the upstream writer pack does not have — so the boundary rules never reached the model. It rendered, and it rendered worse than it should, with no error to explain why. Both fixed: that node ships here as
RiftPromptSource, and the rules are written into the workflow's own system prompt as well as carried by the writer pack in this zip.The chaining sampler's anchor switches now do something.
voice_ref,reference_images,self_anchor_voice,preview_first_shot,two_pass_upscale,reference_image_sizeand the sampler/scheduler overrides were drawn on the canvas but absent from the class, so ComfyUI stripped them before execution. All real now, render-verified.unload_model_afteron the writer, described above.SHOT COUNT on the master panel drives the sampler and the writer together so they cannot disagree; prompt source switches between a manual scene box and a prompt set, lazily.
Node titles no longer name a checkpoint or a switch position — a title like
H3 model (fl2va)is a lie the moment you change the model.
Two-pass upscale cannot be combined with context_pin or latent_handoff, or with an audio spine: those carry raw latents, or one locked denoise trajectory, across the join, and a two-pass render preserves neither. The node stops with an error naming the conflict rather than quietly producing a weaker join. Two-pass is available on cut, seamless, seamless_tail, first_frame and flf_chain.
Credits
Prompt writer: RealRebelAI (ComfyUI_JoyAI_Echo_GGUF_Nodes, modified — see the NOTICE in the zip). context_pin: NikoDemon80 (ComfyUI-H3-Motion-Context). Two-pass upscaling: Tr1dae (ComfyUI-MiniMaxH3_LatentUpscaler).
FAQ
Comments (9)
Judging by the new features, bug fixes and updates over the last few days, I can imagine that you are already a very busy man but I'd like to add a couple of thoughts/ideas/requests if I may.
You previously pointed me in the direction of the "basic" Multishot sampler in the pack as a means of achieving true first frame I2V. This works but there are a couple of issues this highlighted for me.
Firstly, is it possible to include this method of first frame I2V into the main workflow? Because I edited your published workflow to include the alternative sampler every new release of the workflow puts me back to square one. I know this isn't a huge issue but I thought I'd just put the idea out there.
The second issue is also related to true first frame I2V. More often than not the generated video will start with my source image literally for the first frame before it seems to then drop that approach and switch to T2V based on the output of the REWRITER node which, of course, is based on my prompt that feeds the node. Is it possible to give the REWRITER access to the source I2V image enabling it to use a combination of the source image and the user prompt to generate the shot prompts? enabling a more seamless transition between the starting image and the generated video.
I'm not a developer or a coder, I'm just a tinkerer so I really have no idea how possible or desirable any of this is.
Thanks again for all the hard work you are putting into this workflow and your other assets.
Very cool - one thing not totally clear, when using the v2 workflow (which btw, I might recommend added a Note/textbox inside the workflow with the version number to avoid contusion since its labelled as flat v2)
But anyway, is it possible to provide a fixed start image frame when using ref2va mode? Last I checked it did not work but it was pre 2.2.5 I need to check again. I noticed "MiniMaxH3AddGuide" was added to comfyui a couple days ago, my understanding it is creates this ability not sure if you are already leveraging that?
Yes, since 2.2.5 - with one honest caveat about how hard it holds.
How to do it in the v2 canvas: the plates lane. Set continuity=flf_chain and load the BOUNDARY PLATE images - **PLATE 0 is your fixed start frame**. One thing flf_chain requires: ALL the plates (N+1 stills for N shots), not just plate 0 - with only one loaded, every shot pins between that image and itself. Best source is one wide still you crop the other plates out of, so they share a real 3-D scene.
If you want ONLY a start image (no full plate set) while keeping ref2va references: that is the experimental H3 Keyframe Inject node - exactly what 2.2.5's refs+keyframes coexistence fix unblocked (they used to crash together).
The caveat: ref2va holds a keyframe softly. Measured on the same scene:
fl2va reproduces a pinned frame at ~26 dB (it was trained for exactly that task); ref2va + keyframe lands ~16 dB - strong resemblance, not a pixel lock.
ref2va was trained to bind identity from reference rows, not to open on an exact picture.
So:
- Same person, same look, opening close to your image -> ref2va + frame-0 keyframe. Works now.
- Exact pixel-locked first frame -> fl2va checkpoint with continuity=first_frame. That is the trained mechanism.
One trap: the memory sampler's start_image input is NOT a first frame in ref2va mode - it's an identity reference row (the console warns about this). The keyframe route above is the right lever.
On MiniMaxH3AddGuide: same mechanism class - it injects guide latents at positions, which is what the pack's own keyframe path has done since before core added that node. You're not missing anything by the workflow not using it; the pack's H3Keyframes feeds the identical conditioning, plus the refs-coexistence fix core's node doesn't carry.
Version note in the canvas: good idea, doing it - next release the workflow carries its version in a Note so "v2" stops meaning two things.
@joeygambino maybe I am confused but the "H3 Keyframe Inject" note passes "conditioning" through as output, but in the v2 workflow I dont see where that is exposed? seems to be handled internally via the memory node - does a different sampler need to be used in that case or different workflow entirely.
edit: one cool thing I tried that kind of works OK is to try loading one frame in as if it were the "reference video" and added to the prompt "Start Frame is <Video 1>." and this actually worked decently without needing to add any node to the v2 workflow...
You're not confused - you're correct, and my last answer was wrong on that point. H3 Keyframe Inject needs a conditioning wire, and the v2 workflow doesn't have one (the sampler builds its conditioning internally). It only fits the expert-style graphs. Sorry for the runaround.
In the v2 workflow you have three real options:
1. Your reference_video trick - keep using it. That's a genuinely good find, and it works for a real reason: one frame into reference_video rides the same soft appearance-anchor machinery, and the "Start Frame is <Video 1>." line ties the prompt to it. Same softness class as any ref2va anchor - strong resemblance, not a pixel lock. I'm adding it to the docs
with your name on the find.
2. flf_chain + all the boundary plates (as before) - harder hold, but needs the full plate set.
3. fl2va checkpoint + continuity=first_frame - the only true pixel lock. And the version Note in the canvas is in the next release, along with this correction to the info boxes.
One other question.. are manual prompts discouraged - should llm be used to maybe help match with any internal prompt "wrangling"?
If manual prompting should we follow standard instructions (6 part prompt for ref2va and 3 part prompt for fl2va) or should it follow a whole different scheme entirely that only the non-manual flow would really work best with?
Manual prompts definitely not discouraged, if you're able to prompt that well. I am not always confident I'm getting enough detail in that I would like, so I tend to manual a story idea + LLM using either minimax-m3:cloud or glm-5.2:cloud - MM is much better at prompting for real life actors - I'll parody Buffy + Doctor Who fighting space vampires or something, and it does a great job of prompting the actual actors somehow. GLM seems better at understanding the structure of an actual story. There may be better ones out there though. I did add a toggle in the LLM node to evict local LLM models after it runs if you choose an abliterated or heretic model.
Not sure if I am stupid or what but after reading all those info boxes in the wf, I still have no idea how to chain FFLF vids lol, what do I do after generating the first clip?
Not stupid - the workflow does this differently than you're expecting, and that's on our info boxes. Here's the whole thing:
**You don't chain clips one at a time. You write all your shots up front and hit Queue ONCE. The chaining happens inside.**
1. In the script box, write each shot's prompt, with --- on its own line between shots. 3 prompts = 3 shots = one continuous video.
2. Set continuity on the sampler to flf_chain.
3. Load the BOUNDARY PLATE images: one more plate than you have shots.
3 shots = 4 plates. PLATE 0 is your opening frame, PLATE 1 is where shot 1
ends and shot 2 begins, and so on. (Easiest way to get plates that match:
crop them all out of one wide picture.)
4. Queue. One video comes out, already joined.
If you already made a clip and want to continue it: grab its last frame (the H3 Last Frame node does this, or just export the final frame), use that image as PLATE 0 of your next run, and keep your character's description word-for-word identical in the new prompts. Then join the two files in any video editor.
That's it. I tend to get too technical and wordy in my descriptions because I want people to have as much information as possible, I just have to learn that people want the solution, not the life story that gets me to it.