Simplify H3 beat timing and cleanup
This commit is contained in:
+71
-102
@@ -271,7 +271,7 @@ def split_paragraphs(text, delimiter):
|
||||
# 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",
|
||||
"beat_split",
|
||||
"watermark_text", "watermark_position", "watermark_size", "watermark_opacity",
|
||||
"watermark_margin", "intro_text", "intro_position", "intro_seconds",
|
||||
"intro_fade", "intro_size", "overlay_font", "overlay_stroke",
|
||||
@@ -3093,19 +3093,18 @@ def beat_seconds_directive(beat):
|
||||
beat_seconds_directive = lru_cache(maxsize=2048)(beat_seconds_directive)
|
||||
|
||||
|
||||
def plan_beat_frames(beats, fps, budget, per_beat=True):
|
||||
def plan_beat_frames(beats, fps, budget):
|
||||
"""Per-beat shot lengths in frames. Returns (lengths, notes).
|
||||
|
||||
`budget` is the CEILING -- the VRAM budget, or a forced shot_seconds already
|
||||
clamped to it. Per-beat sizing can only ever make a shot shorter than that
|
||||
ceiling, never longer. Priority per beat:
|
||||
`budget` is the socket-defined ceiling used for the beat-shot maximum.
|
||||
Priority per beat:
|
||||
|
||||
1. an explicit 'seconds: N' line in the beat -- always honored, down to
|
||||
H3's real 5-frame minimum, because you stated a duration outright;
|
||||
2. its own content -- action clauses and quoted dialogue (see
|
||||
estimate_beat_seconds), floored at MIN_CONTENT_FRAMES so a shot always
|
||||
has room for one action;
|
||||
3. with per_beat off, the ceiling, exactly as before.
|
||||
3. otherwise its own content estimate.
|
||||
|
||||
Why estimate at all, when action prose has no *reliable* duration? Because the
|
||||
alternative is not "no guess" -- it is "guess the maximum", which is what giving
|
||||
@@ -3115,28 +3114,25 @@ def plan_beat_frames(beats, fps, budget, per_beat=True):
|
||||
shot continues from the handoff frame; leaning long costs a jacket that takes
|
||||
itself off and puts itself back on."""
|
||||
beats = beats if beats else [""]
|
||||
# MIN_SHOT_FRAMES is the floor of the *VRAM budget* -- the shortest shot the node
|
||||
# falls back to when it has to guess with no information at all. It must not raise
|
||||
# a length that came from you or from the beat's own content: `max(floor, ...)`
|
||||
# silently turned every request below ~5.2s into 124f, so 1s/2s/3s/4s all rendered
|
||||
# identically and both the widget and the `seconds:` directive looked broken.
|
||||
cap = max(5, int(budget))
|
||||
# MIN_SHOT_FRAMES is the floor when the node has to guess with no information
|
||||
# at all. A stated or estimated length is not silently rewritten upward.
|
||||
cap = max(5, int(budget))
|
||||
content_floor = align_frame_count(MIN_CONTENT_FRAMES)
|
||||
out, notes = [], []
|
||||
fps = max(1, int(fps))
|
||||
for i, b in enumerate(beats, 1):
|
||||
want, src, floor = beat_seconds_directive(b), "seconds:", 5
|
||||
snap = align_frame_count # a stated length is never rounded DOWN
|
||||
if want is None:
|
||||
want = estimate_beat_seconds(b) if per_beat else 0.0
|
||||
src, floor, snap = "content", content_floor, align_frame_count_nearest
|
||||
if want <= 0: # no signal -> the ceiling
|
||||
out.append(cap)
|
||||
continue
|
||||
n = min(cap, max(floor, snap(int(round(want * fps)))))
|
||||
out.append(n)
|
||||
if n != cap:
|
||||
notes.append(f"shot {i}: {n}f (~{n / fps:.1f}s, from {src})")
|
||||
want, src, floor = beat_seconds_directive(b), "seconds:", 5
|
||||
snap = align_frame_count # a stated length is never rounded DOWN
|
||||
if want is None:
|
||||
want = estimate_beat_seconds(b)
|
||||
src, floor, snap = "content", content_floor, align_frame_count_nearest
|
||||
if want <= 0: # no signal -> the ceiling
|
||||
out.append(cap)
|
||||
continue
|
||||
n = max(floor, snap(int(round(want * fps))))
|
||||
out.append(n)
|
||||
if n != cap:
|
||||
notes.append(f"shot {i}: {n}f (~{n / fps:.1f}s, from {src})")
|
||||
return out, notes
|
||||
|
||||
|
||||
@@ -3878,24 +3874,24 @@ def estimate_shot_frames(total_gb, resident_gb, headroom_gb, pixels=None, free_g
|
||||
return max(floor, align_frame_count(min(H3_MAX_FRAMES, frames)))
|
||||
|
||||
|
||||
def resolve_shot_frames(shot_seconds, fps, total_gb, resident_gb, headroom_gb,
|
||||
allow_oversize=False, pixels=None, free_gb=None):
|
||||
def resolve_shot_frames(shot_seconds, fps, total_gb, resident_gb, headroom_gb,
|
||||
pixels=None, free_gb=None):
|
||||
"""Returns (frames, note).
|
||||
|
||||
Auto mode (shot_seconds <= 0): frames = the VRAM budget estimate (resolution-
|
||||
scaled). Forced mode: the requested length is clamped DOWN to the budget
|
||||
unless allow_oversize is set. When VRAM is unknown the request is honored."""
|
||||
budget = estimate_shot_frames(total_gb, resident_gb, headroom_gb, pixels, free_gb)
|
||||
if not (shot_seconds and float(shot_seconds) > 0):
|
||||
return budget, ""
|
||||
requested = align_frame_count(min(H3_MAX_FRAMES, max(5, round(float(shot_seconds) * fps))))
|
||||
if total_gb <= 0 or requested <= budget:
|
||||
return requested, ""
|
||||
if allow_oversize:
|
||||
return requested, (f"OVERSIZE: {requested}f requested vs {budget}f budget -- honoring it; "
|
||||
f"may spill to system RAM (slow) or OOM")
|
||||
return budget, (f"requested {requested}f (~{requested/max(1,fps):.1f}s) exceeds the ~{budget}f VRAM "
|
||||
f"budget -- clamped to {budget}f (~{budget/max(1,fps):.1f}s). Set allow_oversize_shots to override")
|
||||
Auto mode (shot_seconds <= 0): frames = the VRAM budget estimate (resolution-
|
||||
scaled). Forced mode: the requested length is honored as requested. When VRAM
|
||||
is unknown the request is also honored."""
|
||||
budget = estimate_shot_frames(total_gb, resident_gb, headroom_gb, pixels, free_gb)
|
||||
if not (shot_seconds and float(shot_seconds) > 0):
|
||||
return budget, ""
|
||||
requested = align_frame_count(min(H3_MAX_FRAMES, max(5, round(float(shot_seconds) * fps))))
|
||||
if total_gb <= 0:
|
||||
return requested, ""
|
||||
if requested > budget:
|
||||
return requested, (f"requested {requested}f (~{requested/max(1,fps):.1f}s) exceeds the "
|
||||
f"~{budget}f VRAM budget -- honoring it; may spill to system RAM "
|
||||
f"(slow) or OOM")
|
||||
return requested, ""
|
||||
|
||||
|
||||
def _is_oom(e):
|
||||
@@ -6045,21 +6041,16 @@ class H3LongVideos:
|
||||
"shot instead of the literal last frame. Set 2-4 if chained shots open "
|
||||
"with moving/talking mouths -- it avoids seeding the next shot with a "
|
||||
"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": "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."}),
|
||||
"shot_seconds": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 15.1, "step": 0.5,
|
||||
"forceInput": True,
|
||||
"tooltip": "GLOBAL per-shot maximum in seconds. 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. If the "
|
||||
"requested duration exceeds your hardware, the node will keep it and let the "
|
||||
"render fail instead of shrinking it. Total video length is the sum of the "
|
||||
"beat shots, not simply beat-count x this value."}),
|
||||
"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 "
|
||||
@@ -6093,12 +6084,7 @@ class H3LongVideos:
|
||||
"decode_tile_size": ("INT", {"default": 0, "min": 0, "max": 1024, "step": 32,
|
||||
"tooltip": "Spatial tile size for the VAE decode (tile_x/tile_y). 0 = ComfyUI default. "
|
||||
"Try 256 on a tight card at 1344x768."}),
|
||||
"cleanup_between_shots": ("BOOLEAN", {"default": True,
|
||||
"tooltip": "Between beats, move each shot's decoded video+audio to system RAM and run "
|
||||
"a full VRAM+RAM purge (GC + CUDA cache), so a long chain doesn't accumulate "
|
||||
"on the GPU and OOM. Recommended on 16GB. Turn off only on a big card where "
|
||||
"you want to skip the per-shot cleanup cost."}),
|
||||
"upscale": (["off", "rtx", "model", "lanczos"], {"default": "off",
|
||||
"upscale": (["off", "rtx", "model", "lanczos"], {"default": "off",
|
||||
"tooltip": "Optional post-pass on the finished frames. 'rtx' = NVIDIA RTX Video Super "
|
||||
"Resolution (Tensor Cores -- fastest and best for video; needs the "
|
||||
"Nvidia_RTX_Nodes_ComfyUI pack, falls back automatically if absent). 'model' = "
|
||||
@@ -6195,20 +6181,6 @@ class H3LongVideos:
|
||||
"When this is filled in, EVERY paragraph of the prompt box is a beat/shot -- "
|
||||
"nothing is consumed as the identity anchor. Put the permanent identity here "
|
||||
"(hair, face, build, age) and the clothing in character_memory."}),
|
||||
"per_beat_length": ("BOOLEAN", {"default": True,
|
||||
"tooltip": "PACING. Size each shot from what its beat actually stages, instead of giving "
|
||||
"every shot the same length. ON (default): a beat's time is ~2s of setup plus "
|
||||
"~2.5s per action clause, or its spoken line, whichever is longer -- so 'she "
|
||||
"takes off her jacket and drops it on the bench' gets ~7s and a three-part "
|
||||
"beat gets more. OFF: every shot gets the full ceiling. WHY IT MATTERS: a 3s "
|
||||
"action in a 12s shot leaves 9 seconds the model was told nothing about, and "
|
||||
"it fills them by repeating or REVERSING the action -- which is why clothing "
|
||||
"comes off and goes back on. The estimate leans SHORT on purpose: an "
|
||||
"unfinished action is continued by the next shot from the handoff frame, "
|
||||
"while an overlong one is unrecoverable. Never exceeds the ceiling "
|
||||
"(shot_seconds or the VRAM budget) and always lands on the 17n+5 grid. "
|
||||
"Override any single beat with 'seconds: 8' on its own line inside that "
|
||||
"paragraph -- that wins over everything, including this toggle."}),
|
||||
"auto_soundscape": (["off", "fill if blank", "always"], {"default": "fill if blank",
|
||||
"tooltip": "Build the ambient bed from the scene instead of typing one. Reads the "
|
||||
"ANCHOR (the soundscape is global, so it must describe the PLACE, not "
|
||||
@@ -6509,9 +6481,8 @@ class H3LongVideos:
|
||||
shift_video=12.0, shift_audio=3.0, trim_seam=True, vary_seed_per_shot=False,
|
||||
handoff_offset=0, vram_headroom_gb=1.5, allow_res_backoff=True,
|
||||
decode_tile_frames=0, decode_tile_size=0,
|
||||
cleanup_between_shots=True,
|
||||
anchor_override="", shot_seconds=0.0, allow_oversize_shots=False,
|
||||
per_beat_length=True, beat_split="auto",
|
||||
anchor_override="", shot_seconds=0.0,
|
||||
beat_split="auto",
|
||||
character_memory="", auto_wardrobe=True, auto_props=True, prevent_nudity=True,
|
||||
exposed_terms="", anatomy_guard="auto", lock_restraints=True,
|
||||
solidity_guard="auto", motion_guard="auto", contact_guard="auto",
|
||||
@@ -6660,8 +6631,8 @@ class H3LongVideos:
|
||||
streaming = total_gb > 0 and resident_gb > 0 and resident_gb > total_gb
|
||||
lora_gb = lora_overhead_gb(model)
|
||||
eff_headroom = vram_headroom_gb + lora_gb
|
||||
ln, ln_note = resolve_shot_frames(shot_seconds, fps, total_gb, resident_gb,
|
||||
eff_headroom, allow_oversize_shots, w * h, free_gb)
|
||||
ln, ln_note = resolve_shot_frames(shot_seconds, fps, total_gb, resident_gb,
|
||||
eff_headroom, w * h, free_gb)
|
||||
if lora_gb:
|
||||
ln_note = ((ln_note + " ") if ln_note else "") + (
|
||||
f"reserved ~{lora_gb:.1f}GB for bypass-LoRA adapters (they stay resident in bf16 "
|
||||
@@ -6733,32 +6704,29 @@ class H3LongVideos:
|
||||
# this". Forcing a length used to DISABLE per-beat sizing entirely, which is
|
||||
# why a plan made with a forced length disagreed with the auto render: two
|
||||
# different code paths for the same question.
|
||||
lens, len_notes = plan_beat_frames(beats, fps, ln, per_beat=bool(per_beat_length))
|
||||
lens, len_notes = plan_beat_frames(beats, fps, ln)
|
||||
secs = [n / fps for n in lens]
|
||||
if len_notes:
|
||||
n_short = sum(1 for n in lens if n < ln)
|
||||
ln_note = ((ln_note + " ") if ln_note else "") + (
|
||||
f"per-beat pacing sized {n_short} of {len(lens)} shot(s) under the {ln}f "
|
||||
f"(~{ln / fps:.1f}s) ceiling from their own content: " + "; ".join(len_notes)
|
||||
+ ". Turn per_beat_length OFF to give every shot the full ceiling")
|
||||
f"content pacing sized {n_short} of {len(lens)} shot(s) under the {ln}f "
|
||||
f"(~{ln / fps:.1f}s) ceiling from their own content: " + "; ".join(len_notes))
|
||||
# With pacing OFF, every beat gets the ceiling whether it has anything to fill
|
||||
# it with or not -- so say which beats are too thin for the length they got.
|
||||
# This is the failure that reads as an action repeating or playing backwards.
|
||||
pace_warnings = pacing_warnings(beats, lens, fps)
|
||||
if pace_warnings:
|
||||
ln_note = ((ln_note + " ") if ln_note else "") + (
|
||||
"THIN BEATS -- the model must invent the remaining time, which it fills by "
|
||||
"repeating or REVERSING the action: " + "; ".join(pace_warnings)
|
||||
+ ". Add a second clause to the beat, set 'seconds:' on it, or turn "
|
||||
"per_beat_length ON to size shots from their content")
|
||||
"THIN BEATS -- the model must invent the remaining time, which it fills by "
|
||||
"repeating or REVERSING the action: " + "; ".join(pace_warnings)
|
||||
+ ". Add a second clause to the beat, or set 'seconds:' on it")
|
||||
fit_warnings = dialogue_fit_warnings(beats, secs)
|
||||
# The opposite error, and the one that babbles: far more shot than line.
|
||||
filler_warnings = dialogue_filler_warnings(beats, secs)
|
||||
if filler_warnings:
|
||||
ln_note = ((ln_note + " ") if ln_note else "") + (
|
||||
"BABBLE RISK -- " + "; ".join(filler_warnings)
|
||||
+ ". Turn per_beat_length ON to size these shots from their line, or set "
|
||||
"'seconds:' on the beat")
|
||||
"BABBLE RISK -- " + "; ".join(filler_warnings)
|
||||
+ ". Set 'seconds:' on the beat if you want a fixed longer take")
|
||||
wardrobe_notes = []
|
||||
strip_shots = [] # shots that newly bared a zone -> the NEXT shot starts fresh
|
||||
raw_gens = distribute_generations(anchor, beats, global_soundscape.strip(),
|
||||
@@ -6933,22 +6901,23 @@ class H3LongVideos:
|
||||
|
||||
spk = speech_flags(beats) # which shots have real (quoted) dialogue
|
||||
vram_trace = [] # free VRAM after each shot
|
||||
muted_flags = [] # which shots were audio-silenced
|
||||
hoff = max(0, int(handoff_offset))
|
||||
backoff, video_chunks, audio_chunks = [], [], []
|
||||
latent_chunks = [] # per-shot sampled latents, pre-decode
|
||||
mouth_settled = [] # shots seeded from a settled (closed) mouth
|
||||
muted_flags = [] # which shots were audio-silenced
|
||||
hoff = max(0, int(handoff_offset))
|
||||
backoff, video_chunks, audio_chunks = [], [], []
|
||||
latent_chunks = [] # per-shot sampled latents, pre-decode
|
||||
mouth_settled = [] # shots seeded from a settled (closed) mouth
|
||||
handoff, sr = first_frame, None
|
||||
ref_shots = [] # which shots ended up ref-conditioned
|
||||
ref_missing = [] # <Picture N> tags naming an unconnected slot
|
||||
ref_carried = [] # tagged shots that kept continuity as an extra ref
|
||||
ref_keyframed = [] # tagged shots that kept it as a real keyframe
|
||||
ref_mode_used = []
|
||||
continuity_used = []
|
||||
ref_aug_used = []
|
||||
shot_timings = []
|
||||
if cleanup_between_shots:
|
||||
_deep_cleanup() # start the first (heaviest) shot with max free VRAM
|
||||
ref_mode_used = []
|
||||
continuity_used = []
|
||||
ref_aug_used = []
|
||||
shot_timings = []
|
||||
cleanup_between_shots = True
|
||||
if cleanup_between_shots:
|
||||
_deep_cleanup() # start the first (heaviest) shot with max free VRAM
|
||||
|
||||
shot_lens = (lens + [ln] * len(gens))[:len(gens)]
|
||||
for i, base_prompt in enumerate(raw_gens):
|
||||
|
||||
Reference in New Issue
Block a user