Tidy H3 long videos input layout

This commit is contained in:
2026-08-28 11:36:39 +00:00
parent 9b862928ba
commit eeca789c27
2 changed files with 98 additions and 56 deletions
+70 -40
View File
@@ -267,7 +267,8 @@ def split_paragraphs(text, delimiter):
# Widgets added after the node's original 36-widget layout. Kept LAST in
# INPUT_TYPES so a workflow saved before they existed still maps its stored values
# onto the right widgets (ComfyUI matches them by position, not by name).
# APPEND to this tuple when adding a widget; never insert into the middle.
# APPEND widget names to this tuple; never insert into the middle. Pure sockets
# that carry no widget value can stay grouped in INPUT_TYPES without being listed.
ADDED_WIDGETS = (
"beat_split", "per_beat_length",
"watermark_text", "watermark_position", "watermark_size", "watermark_opacity",
@@ -277,7 +278,6 @@ ADDED_WIDGETS = (
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
"motion_guard", "contact_guard",
"auto_soundscape", "allow_nonspeech_vocals",
"ref_5", "ref_6", "ref_7", "ref_8", "ref_9",
"detail_pass", "detail_sampler_name", "detail_scheduler",
"detail_steps", "detail_denoise",
)
@@ -5744,13 +5744,26 @@ class H3LongVideos:
"tooltip": "Base H3 wants ~20 (res_multistep + simple). Drop to 6-8 ONLY with a "
"working distill/turbo LoRA or a low-step MXFP8 checkpoint -- on the "
"bare base model, low steps are the #1 cause of soft output."}),
"cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "res_multistep"}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "simple"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}),
"cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1,
"tooltip": "H3 is effectively CFG-free here. Leave this at 1.0 unless you are "
"deliberately testing unusual sampler behaviour; higher values do not act "
"like a normal SD/Flux CFG boost and are not the fix for weak identity."}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "res_multistep",
"tooltip": "Main-pass sampler. Base H3 is tuned around res_multistep; changing this is "
"a real behaviour change, not a cosmetic preference. Treat it as an advanced "
"override."}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "simple",
"tooltip": "Main-pass scheduler. Base H3 is tuned around simple when paired with "
"res_multistep and the default model-sampling shifts."}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True,
"tooltip": "Base seed for the whole chain. With vary_seed_per_shot OFF, every beat is "
"sampled from the same noise field for better continuity. With it ON, this "
"becomes the first seed and later beats use seed+1, seed+2, ..."}),
},
"optional": {
"first_frame": ("IMAGE",),
"first_frame": ("IMAGE", {"tooltip": "Optional opening keyframe for shot 1. Leave empty to "
"start from pure text/reference conditioning. On later shots the node normally uses the "
"previous shot's last frame automatically; this socket is only the initial handoff."}),
# ref2va inputs. SOCKET NUMBER matters: prompt tags refer to these
# exact slots, even when some intermediate sockets are left empty.
# A shot using only ref_image_7 is still tagged as <Picture 7> in the
@@ -5760,19 +5773,20 @@ class H3LongVideos:
# the order they are handed that shot.
# Refer to socket tags in the prompt if you want a reference bound to
# a named character ("Kristy, <Picture 7>, walks in").
"ref_1": ("REFERENCE", {"tooltip": "Reference object for <Picture 1> -- image plus identity/environment metadata "
"carried into the shots. Which shots receive it is set by ref_mode (or <Picture N> "
"tags in the beats); a referenced shot ALSO carries the previous frame as its "
"keyframe, so taking a reference never costs continuity."}),
"ref_2": ("REFERENCE", {"tooltip": "Reference object for <Picture 2>."}),
"ref_3": ("REFERENCE", {"tooltip": "Reference object for <Picture 3>."}),
"ref_4": ("REFERENCE", {"tooltip": "Reference object for <Picture 4>."}),
"ref_5": ("REFERENCE", {"tooltip": "Reference object for <Picture 5>."}),
"ref_6": ("REFERENCE", {"tooltip": "Reference object for <Picture 6>."}),
"ref_7": ("REFERENCE", {"tooltip": "Reference object for <Picture 7>."}),
"ref_8": ("REFERENCE", {"tooltip": "Reference object for <Picture 8>."}),
"ref_9": ("REFERENCE", {"tooltip": "Reference object for <Picture 9>."}),
"ref_image_1": ("*", {"tooltip": "Legacy alias for ref_1. Accepts old IMAGE wiring or a REFERENCE payload."}),
"ref_1": ("REFERENCE", {"tooltip": "Reference slot <Picture 1>. Feed a Dumas Character "
"Reference or Dumas Location Reference here. The image is used for real H3 visual "
"conditioning; the structured metadata is also used for prompt context."}),
"ref_2": ("REFERENCE", {"tooltip": "Reference slot <Picture 2>. Socket numbers matter: "
"tag this exact slot as <Picture 2> when you want explicit placement in a beat."}),
"ref_3": ("REFERENCE", {"tooltip": "Reference slot <Picture 3>."}),
"ref_4": ("REFERENCE", {"tooltip": "Reference slot <Picture 4>."}),
"ref_5": ("REFERENCE", {"tooltip": "Reference slot <Picture 5>."}),
"ref_6": ("REFERENCE", {"tooltip": "Reference slot <Picture 6>."}),
"ref_7": ("REFERENCE", {"tooltip": "Reference slot <Picture 7>."}),
"ref_8": ("REFERENCE", {"tooltip": "Reference slot <Picture 8>."}),
"ref_9": ("REFERENCE", {"tooltip": "Reference slot <Picture 9>."}),
"ref_image_1": ("*", {"tooltip": "Legacy alias for ref_1. Accepts old IMAGE wiring or a "
"REFERENCE payload. Keep old graphs working without rewiring."}),
"ref_image_2": ("*", {"tooltip": "Legacy alias for ref_2. Accepts old IMAGE wiring or a REFERENCE payload."}),
"ref_image_3": ("*", {"tooltip": "Legacy alias for ref_3. Accepts old IMAGE wiring or a REFERENCE payload."}),
"ref_image_4": ("*", {"tooltip": "Legacy alias for ref_4. Accepts old IMAGE wiring or a REFERENCE payload."}),
@@ -5782,9 +5796,10 @@ class H3LongVideos:
"ref_image_8": ("*", {"tooltip": "Legacy alias for ref_8. Accepts old IMAGE wiring or a REFERENCE payload."}),
"ref_image_9": ("*", {"tooltip": "Legacy alias for ref_9. Accepts old IMAGE wiring or a REFERENCE payload."}),
"plan_only": ("BOOLEAN", {"default": False,
"tooltip": "Preview the shot split WITHOUT rendering. Uses THIS node's own settings (no "
"second node, no duplicate entry): returns the plan in 'info' and the "
"shots/frames/seconds outputs near-instantly. Turn off to render for real."}),
"tooltip": "Preview the shot split and timing WITHOUT sampling pixels. Uses this "
"node's current settings and returns the plan in `info` almost instantly. "
"Good for checking beat counts, shot ceilings, ref placement and clamps "
"before a long render."}),
"fps": ("INT", {"default": 24, "min": 1, "max": 60,
"tooltip": "DISPLAY ONLY -- H3 always renders 24 fps. The model's frame grid and its "
"audio latent are both defined against 24, so this node computes every "
@@ -5846,21 +5861,27 @@ class H3LongVideos:
"mid-word open-mouth pose. Trims the matching audio tail too. 0 = last frame."}),
"shot_seconds": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 15.1, "step": 0.5,
"forceInput": True,
"tooltip": "Length of EACH shot in seconds, taken from a connected input -- "
"H3 Shot Length is the intended source, since it also reports the "
"matching frame count on the 17k+5 grid.\n\n"
"Leave it UNCONNECTED for auto: the largest shot that fits at the "
"chosen size, which is what a 0 in the old widget did. One "
"paragraph = one shot, so total video = (paragraph count) x this. "
"Max ~15s."}),
"tooltip": "GLOBAL per-shot ceiling in seconds, not 'force every beat to exactly "
"this length'. H3 Shot Length is the intended source because it snaps to "
"H3's frame grid and reports the real duration.\n\n"
"Leave UNCONNECTED for auto: the node picks the largest shot that fits "
"the current size/VRAM budget. Connect a value to cap every beat at that "
"length. A beat's own `seconds:` directive can still ask for less, and any "
"request above H3's hard ~15.1s single-shot limit is clamped. Total video "
"length is the sum of the beat shots, not simply beat-count x this value."}),
"allow_oversize_shots": ("BOOLEAN", {"default": False,
"tooltip": "OFF (default): a forced shot_seconds that won't fit VRAM is clamped DOWN to "
"what fits, and the clamp is reported in info. ON: honor the requested length "
"even if it exceeds the budget -- the render may spill into system RAM (slow) "
"or OOM. Only affects forced shot_seconds, not auto."}),
"vram_headroom_gb": ("FLOAT", {"default": 1.5, "min": 0.0, "max": 32.0, "step": 0.5}),
"vram_headroom_gb": ("FLOAT", {"default": 1.5, "min": 0.0, "max": 32.0, "step": 0.5,
"tooltip": "Safety margin RESERVED from free VRAM before the node budgets shot length. "
"Higher = shorter safer shots. Lower = longer shots but more risk of spill or "
"OOM during sampling/decode peaks. 1.5GB is the conservative default."}),
"allow_res_backoff": ("BOOLEAN", {"default": True,
"tooltip": "If VRAM is tight, step resolution down instead of failing."}),
"tooltip": "If a render does not fit, try stepping the resolution down instead of just "
"failing. Helps salvage long jobs automatically, but the later shots may come "
"back smaller and the latent output can no longer join cleanly across sizes."}),
# ON by default: the prompt-side clauses ASK H3 not to vocalize (and now
# condition the soundscape field too), but asking is not a guarantee --
# babble under a silent shot was the one artifact that survived both.
@@ -5944,7 +5965,7 @@ class H3LongVideos:
"white as asked; 2-3 makes it survive a bright sky or a white wall."}),
"ref_mode": (["auto ref2v", "where tagged", "first shot", "every shot", "every shot + handoff ref"],
{"default": "auto ref2v",
"tooltip": "Which shots the ref_image inputs condition. 'auto ref2v' (default) is "
"tooltip": "Which shots the reference IMAGE inputs condition. 'auto ref2v' (default) is "
"the reference-to-video bias: if the prompt uses <Picture N> tags, those "
"tags decide which shot gets which ref; if there are NO tags anywhere, the "
"node conditions EVERY shot with the connected refs rather than collapsing "
@@ -5952,7 +5973,9 @@ class H3LongVideos:
"for long chains where identity drift matters more than strict per-shot "
"routing. 'where tagged' keeps the old strict behavior, including the "
"first-shot fallback when no tags are found. Tags are renumbered per shot, "
"so <Picture 2> alone still resolves. 'first shot' / 'every shot' / "
"so <Picture 2> alone still resolves. Character names in the beat can also "
"pull their matching character refs into the real image-conditioning list. "
"'first shot' / 'every shot' / "
"'every shot + handoff ref' go purely by position. Ignored when no "
"ref_image is connected."}),
"ref_noise_aug": ("FLOAT", {"default": 0.95, "min": 0.50, "max": 1.0, "step": 0.005,
@@ -6189,16 +6212,23 @@ class H3LongVideos:
"detail_pass": ("BOOLEAN", {"default": False,
"tooltip": "Run a second refinement sampler on each beat BEFORE any upscale. "
"It reuses the same conditioning and keeps the output video-only by "
"preserving the first pass's audio latent. Good for extra detail without "
"building a separate graph."}),
"preserving the first pass's audio latent. Use it for detail cleanup, not "
"for huge rewrites: too many steps or too much denoise can pull identity or "
"continuity away from the main pass."}),
"detail_sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler",
"tooltip": "Sampler used for the optional refinement pass."}),
"tooltip": "Sampler for the optional refinement pass. Euler is the maintained default "
"direction for this lane."}),
"detail_scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "beta",
"tooltip": "Scheduler used for the optional refinement pass."}),
"tooltip": "Scheduler for the optional refinement pass. Beta is the maintained default "
"direction for the H3 enhancement lane."}),
"detail_steps": ("INT", {"default": 8, "min": 1, "max": 200,
"tooltip": "Steps for the optional refinement pass."}),
"tooltip": "Extra steps for the refinement pass only. Start around 4-8. More is not "
"automatically better; once the pass starts rewriting instead of polishing, "
"identity and continuity can drift."}),
"detail_denoise": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 1.0, "step": 0.01,
"tooltip": "How hard the refinement pass is allowed to rewrite the beat latent."}),
"tooltip": "How hard the refinement pass is allowed to rewrite the beat latent. Start "
"around 0.20-0.35 for gentle cleanup; 0.4+ is stronger and can noticeably "
"change faces, motion or composition."}),
},
# Read-only graph access, for SLA-LoRA detection: a LoRA's filename is
# the only thing that identifies an SLA build, and the graph is the only
+12
View File
@@ -432,12 +432,24 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
for index in range(1, 10):
self.assertIn(f"ref_{index}", optional)
names = list(optional)
ref_positions = [names.index(f"ref_{index}") for index in range(1, 10)]
self.assertEqual(ref_positions, list(range(ref_positions[0], ref_positions[0] + 9)))
def test_input_types_keep_legacy_ref_image_aliases(self):
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
for index in range(1, 10):
self.assertIn(f"ref_image_{index}", optional)
def test_shot_seconds_tooltip_describes_ceiling_behavior(self):
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
tooltip = optional["shot_seconds"][1]["tooltip"]
self.assertIn("GLOBAL per-shot ceiling", tooltip)
self.assertIn("not 'force every beat to exactly this length'", tooltip)
self.assertIn("hard ~15.1s single-shot limit", tooltip)
def test_run_defaults_match_declared_ref_widget_defaults(self):
node = self.module.H3LongVideos()
optional = node.INPUT_TYPES()["optional"]