π¬ H3 Master - LD
H3 Master Renders the Whole Chain
- extend_video
- extend_latent
- extend_audio
- latent
- ref_image
- start_image
- pack
- model
- clip
- video_vae
- audio_vae
- sampler
- sigmas
- frames
- audio
- clips
- report
The node that turns Studio's script into an actual movie
H3 Studio writes you a script. H3 Master (π¬ H3 Master - LD, under LD / PromptMaster / Long form, in the same PromptMasterLD pack) renders it: one ----separated script in, one continuous IMAGE+AUDIO video out, in a single queue. Wire Studio's pack output into pack and Master handles everything between - the pack carries the script, seconds, canvas and rails, so wiring it alone gives you a clean chain. (The script textarea is there for when you're writing prompts by hand instead, and its --- format is the same contract Studio's writer already emits, so scripts port unchanged.)
Why does a whole movie need one node instead of a graph? Because clip N+1 needs clip N's decoded pixels, and ComfyUI evaluates a static graph exactly once - a loop can't be drawn. So the loop lives inside the node's run(). And rather than reimplementing H3's chaining, Master calls the community's proven machinery directly: NikoDemon80's MiniMax H3 Motion Context node pins the previous clip's tail as interior keyframes on the next clip's timeline (plus its audio), then trims the duplicated head on both picture and sound. Measured on real renders, joins land at roughly one ordinary frame step of picture and 0.00ms of audio drift. It works because it stopped reinventing the thing that already worked.
The dials that matter
- script - one prompt per clip,
---between them. This is the same contract H3 Studio emits, so scripts port verbatim. - frames_per_clip - snaps to H3's 17k+5 grid (a frame count that lands on a valid H3 length). 328 is about 13.7s; clips after the first render this minus the carry.
- carry_frames - frames of the previous clip pinned into this one, the last ~1s at the seam. Official grid 5/22/39/56; type 1 and it becomes 22.
- multishot mode - with MULTISHOT ticked in Studio, Master renders N clips and joins them rather than a single long shot, which is the sane way to get past H3's ~15s per-clip ceiling.
- seed, width, height - the usual, plus a
seed_per_clipoption to vary seeds per clip.
The optional inputs are where the long-form life gets good. extend_video / extend_audio / extend_latent continue an existing clip instead of starting from black - wire a video loader's frames and the chain starts from that clip's tail, the last carry_frames becoming clip 1's motion context. Connect the matching .safetensors latent and the join is bit-exact. There's also declick_ms (default 1ms - ramps both sides of each join through zero so you don't get an audible tick at every cut), a cache keyed on (seed, index, prompt, geometry) so editing clip 3 re-renders only clip 3 onward, and the music_mp3/music_plan pair if Studio wrote a music-video script.
What comes out
Four outputs: frames (IMAGE), audio (AUDIO), clips (INT, how many clips were rendered and joined), and report (STRING). The frames+audio pair is video-ready - feed it to a save node and you're done. If the joins still feel abrupt, seam_blend adds a few frames of dissolve across each cut using the head frames the trim would otherwise throw away - those frames are a second rendering of the same moment, so the dissolve is real overlap, not a fade to black.
Install, and the honest traps
cd ComfyUI/custom_nodes
git clone https://github.com/Lora-Daddy/PromptMasterLD
cd PromptMasterLD
pip install -r requirements.txt
Restart (or ComfyUI Manager β "PromptMasterLD"). Master relies on the Motion Context node, which the pack vendors under vendor/ComfyUI-H3-Motion-Context - but that upstream pack is GPL-licensed, and bundling it makes this distribution GPL as a whole. If you need permissive, the README tells you to delete that vendor folder, remove it from _VENDORED in __init__.py, and install it separately. H3 Master raises a clear error when it's missing, so the failure mode is loud, not silent.
Two practical traps. Extending is the place people lose a day: without extend_audio, a join is silent on the audio side, because there's no previous clip to carry sound. And remember the licensing map - H3's weights themselves are community-licensed and geofenced out of the US, EU, UK and Korea, so check before you put local weights in production in those regions.
Inputs (32)
| Name | Type | Default | Description |
|---|---|---|---|
| script | STRING | Clip 1 prompt. --- Clip 2 prompt. | One prompt per clip, '---' between them Γ’β¬β the same contract H3 Studio's `prompt` output already emits. |
| width | INT | 51232β4096 | β |
| height | INT | 76832β4096 | β |
| frames_per_clip | INT | 32822β1000 | Snaps to H3's 17k+5 grid. 328 = 13.7s. Clips after the first yield this minus the carry. |
| seed | INT | 00β18446744073709550000 | β |
| carry_frames | INT | 391β56 | Frames of the previous clip pinned into this one β the last ~1s at the seam. Official grid: 5, 22, 39, 56. Type 1 for one second (becomes 22). Off-grid values snap. |
| extend_videoopt | IMAGE | CONTINUE AN EXISTING VIDEO. Wire a video loader's frames here and the chain starts from its TAIL instead of from black β the last `carry_frames` frames become clip 1's motion context, exactly as clip N does for clip N+1. One shot of script extends it; run it again on the result to extend again, so length is unbounded. Leave empty for a fresh video. | |
| extend_latentopt | LATENT | The .safetensors that sits beside the clip in input/h3_extend/. With it the join is BIT-EXACT, the same carry clip N+1 gets inside a chain. It is cut to match extend_video automatically, so trimming the clip and continuing from the middle stays in sync. Without it the tail audio is re-encoded and the join is slightly softer. | |
| extend_audioopt | AUDIO | The sound belonging to extend_video. Supply it or the join is silent on the audio side: without a previous clip there is no context_latent to carry sound, and the motion-context node takes context_audio + the audio VAE instead. | |
| latentopt | LATENT | H3 Studio's `latent` output. Connected, its SHAPE sets width, height AND frames_per_clip for every clip, so the FRAME bar becomes the only place size is decided and the two nodes cannot disagree. video = [B, 24, latent_t, H//16, W//16] and frames = ((latent_t-2)//5)*17+5 inverts exactly. In multishot Studio sizes it from multishot_seconds, so it carries ONE clip's length. The contents are never sampled - each clip builds its own fresh latent. | |
| ref_imageopt | IMAGE | The ORIGINAL, un-fitted image for the vision encoder. Wire your LoadImage here, NOT Studio's start_frame - Studio pre-fits that one to the canvas, so the detail is already gone by the time it arrives. Scaled to a 2048 short edge (H3's own reference ceiling, ~6.3 MP on 2:3) instead of the render canvas. Costs tokens in the text encode only, nothing per sampling step. Falls back to start_image if left unwired. | |
| start_imageopt | IMAGE | I2V. Wire H3 Studio's `start_frame` here. Clip 1 is the ONLY t2v clip in the chain Γ’β¬β every later clip already has its opening pinned to the previous clip's tail Γ’β¬β and it is the clip measured as the weak link. This pins clip 1's opening too. The image is ALSO shown to the text encoder on EVERY clip, so identity and palette are held globally rather than relayed down the chain. That costs nothing per sampling step: it rides in the encode, not as DiT rows. | |
| seed_per_clipopt | BOOLEAN | true | β |
| declick_msopt | FLOAT | 1.00β20 | Ramp both sides of each join through zero. 1ms measures a perfect 0.00x discontinuity; 0 disables and leaves an audible tick. |
| cacheopt | BOOLEAN | true | Key is (seed, index, prompt, geometry). Edit clip 3's text and only 3 onward re-render. |
| previewopt | BOOLEAN | true | β |
| grade_matchopt | BOOLEAN | false | OFF BY DEFAULT, and measured that way. This existed to fight a drift with three real causes: a prompt that opened each shot on 'total black, no picture', a global anchor that motion context was deleting, and its own colour reference chasing the raw input photo instead of the rendered footage. All three are fixed. With nothing left to correct, a non-unity head transform applied to frames that ALREADY match creates the very step it exists to remove. Measured over 4 clips, same script and seed: worst join 0.0299 with it on against 0.0146 with it off, and end-to-end drift +0.0175 against +0.0064. Turn it ON only if you see colour genuinely walking across a long chain; it does flatten per-clip means (0.007 spread vs 0.031) at the cost of the joins. |
| join_sourceopt | BOOLEAN | true | ON: the output is the clip you extended WITH the new part on the end - the longer video you actually asked for. OFF returns only the newly rendered frames, which is what this node used to do and left you joining files by hand. The new clip's carry overlap is trimmed either way, so nothing is repeated at the join. Only does anything when extend_video is wired. |
| music_mp3opt | STRING | Filename in ComfyUI/input of the song. From the Music Video node's `mp3`. | |
| music_planopt | STRING | [] | The beat sheet as JSON Γ’β¬β [{start,end,prompt}]. From the Music Video node's `plan_json`. Its prompts become the shots, in order. |
| seam_blendopt | INT | 00β39 | Frames of DISSOLVE across each join, using the head frames the trim would otherwise delete. Motion context pins clip N+1's first `carry_frames` to clip N's tail, so those frames are a SECOND RENDERING of the same moment. Trimming throws them away and butt-joins two takes; blending morphs one into the other instead, spreading the transition over N frames rather than landing it on one. Costs no length and no frames β the dissolve is written into the previous clip's tail. Safe because the finished video takes the whole MP3 from the mux, so nothing here can click. Cannot exceed carry_frames. 0 = off (butt join, the old behaviour). |
| motion_ref_framesopt | INT | 00β73 | Frames of the PREVIOUS clip's tail sent as a video_audio reference Γ’β¬β what the standalone Motion-Context workflow feeds and this node did not. 0 = off. 22 or 39 are the useful sizes; core snaps DOWN to n %% 17 == 5, so 30 silently becomes 22. A still reference says who she is. Only a video reference says how fast she was moving and which way, and a join is a question about velocity Γ’β¬β which is why identity refs alone left the cut visible. |
| lipsync_vocalsopt | FLOAT | 0.000β1 | Fraction of VOCALS in the audio the chain sees as <Audio 1>. The mouth is driven by this; the finished video still gets the full track from the save node's mux_mp3. 0.0 = off, the full mix (what H3 has always been given). 1.0 = the isolated vocal stem. 0.9 = 90% vocals with the backing folded back under it, level-matched Γ’β¬β the voice dominates but some rhythm survives to hang timing on. Measured on a real track: drums are 51% of the stem energy and vocals 17%, and the vocal stem is near-silent for HALF the song. Most of what the mix spends its energy on cannot move a mouth. Separation is ~4s per song and cached beside the mp3. |
| identity_refsopt | INT | 00β9 | Extra reference IMAGES sampled evenly across the PREVIOUS clip, added beside the global anchor to hold identity Γ’β¬β the same face and clothes, seen moving, not just one still. 0 = off (anchor only). COSTS ROWS: each one adds (h//16)*(w//16) cond rows Γ’β¬β 1024 each at 512x512 Γ’β¬β on top of the keyframes, audio ref and video latent. Going over this model's token ceiling does not raise a clean error, it takes the server down. Raise it a step at a time and watch the logged row count. |
| motion_ref_scaleopt | FLOAT | 0.330β1 | Linear size of the motion reference, as a fraction of the render canvas. Only does anything when motion_ref_frames is on. A reference block bills (h//16)*(w//16) rows PER REF LATENT FRAME - a far steeper rate than the picture's own. Measured at 960x736: the render costs ~705 rows per latent step, a reference 2760 EACH. So a 22-frame motion ref at full canvas is 19320 rows against ~3300 of headroom, and even the smallest legal ref, 5 frames, overruns. Over the ceiling does not raise - it takes the server down. 0.33 (default) costs about a ninth of that and fits. Full resolution buys nothing here: the stills carry identity, this block is asked only where the camera was and which way things moved, and Qwen only ever sees it at 2 fps. 1.0 = full canvas (check the logged row count first). 0 = off, also full canvas. |
| packopt | LDP_PACK | H3 Studio pack. Script, seconds, canvas, stills, song, and the DiT rails ride here. Wire this alone for a clean chain. | |
| modelopt | MODEL | β | |
| clipopt | CLIP | β | |
| video_vaeopt | VAE | β | |
| audio_vaeopt | VAE | β | |
| sampleropt | SAMPLER | β | |
| sigmasopt | SIGMAS | β |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| frames | IMAGE | β |
| audio | AUDIO | β |
| clips | INT | β |
| report | STRING | β |