From e098e44bf1f871808a0a79a155a7284b2ff195c5 Mon Sep 17 00:00:00 2001 From: Chris Dumas Date: Thu, 3 Sep 2026 08:56:43 +0000 Subject: [PATCH] Simplify H3 beat timing and cleanup --- H3_LONG_VIDEOS_GUIDE.md | 72 +++---------- dumas_h3_longvideos.py | 173 ++++++++++++------------------ js/h3_longvideos/index.js | 6 +- tests/test_dumas_h3_longvideos.py | 21 +++- 4 files changed, 105 insertions(+), 167 deletions(-) diff --git a/H3_LONG_VIDEOS_GUIDE.md b/H3_LONG_VIDEOS_GUIDE.md index 46e8c7a..11c6a59 100644 --- a/H3_LONG_VIDEOS_GUIDE.md +++ b/H3_LONG_VIDEOS_GUIDE.md @@ -10,7 +10,7 @@ The node does two jobs at once: Think of it like this: - `prompt` decides what happens. -- `shot_seconds`, `per_beat_length`, and beat-level `seconds:` decide how long each beat is allowed to be. +- `shot_seconds` and beat-level `seconds:` decide how long each beat is allowed to be. - `first_frame` and the handoff system decide continuity. - `ref_1` to `ref_9` decide identity and location reference conditioning. @@ -375,60 +375,38 @@ This is the most important group to understand. These settings work together: - `shot_seconds` -- `per_beat_length` - beat-level `seconds:` or `duration:` -- `allow_oversize_shots` - `megapixels` - `vram_headroom_gb` - `allow_res_backoff` ### The Short Version -`shot_seconds` is a ceiling, not a promise. +`shot_seconds` is the socket-defined maximum. -A beat-level `seconds:` line is a request, not absolute law. +A beat-level `seconds:` line wins for that beat. -The final beat length is limited by: - -- H3's hard single-shot max -- the VRAM budget -- the current resolution and pixel budget +If a requested length is too much for the hardware, the node keeps it and lets the render fail instead of quietly shrinking it. ### Exact Decision Order For each beat, the node decides length like this: 1. If the beat has `seconds:` or `duration:`, use that as the requested length. -2. Otherwise, if `per_beat_length` is on, estimate the beat length from its content. -3. Otherwise, use the global shot ceiling. -4. Clamp that result to the global `shot_seconds` ceiling if one is set. -5. Clamp again if H3 or VRAM will not allow that size. -6. Snap the result to H3's frame grid. +2. Otherwise, estimate the beat length from its content. +3. Snap the result to H3's frame grid. ### `shot_seconds` -Global per-shot ceiling. +Global per-shot maximum. Important: - This does not mean every beat will be exactly this long. -- It means no beat is allowed to be longer than this, unless other limits clamp it first. +- It means beats are sized against this socket value when the node needs a maximum. If left unconnected or set to `0`, the node uses the largest safe shot length it can budget automatically. -### `per_beat_length` - -When on, the node estimates beat length from the beat content if there is no explicit beat-level `seconds:` line. - -That means: - -- short simple beats become shorter shots -- more complex beats become longer shots - -When off: - -- every beat uses the full global ceiling unless that beat has its own explicit `seconds:` line - ### Beat-Level `seconds:` Or `duration:` Inside a beat, you can write: @@ -445,29 +423,7 @@ duration: 8 This has the highest priority for that beat. -But it can still be clamped down if: - -- it exceeds the global `shot_seconds` ceiling -- it exceeds H3's hard max shot length -- it exceeds the VRAM budget and `allow_oversize_shots` is off - -### `allow_oversize_shots` - -This only affects forced global `shot_seconds`. - -When off: - -- too-long requests are clamped down to what fits - -When on: - -- the node will try to honor the requested global ceiling even if it exceeds the safe VRAM budget - -Trade-off: - -- slower renders -- possible spill to system RAM -- possible OOM +It is not silently shortened by the node. If you ask for more than your hardware can handle, the render can fail. This does not bypass H3's hard max shot size. @@ -1287,9 +1243,9 @@ Useful on tighter cards at larger resolutions. ### `cleanup_between_shots` -Moves completed shot data off the GPU and clears memory between beats. +The node always moves completed shot data off the GPU and clears memory between beats. -Good default for long chains. +This is hard-coded for long-chain stability. ### `upscale` @@ -1313,7 +1269,7 @@ Use them in this order: 1. Get the render stable. 2. If decode fails, use decode tiling. -3. If shot length is too short, reduce pixel cost before forcing oversize. +3. If shot length is too short, reduce pixel cost before forcing a longer shot. 4. Only upscale after the base render is working. ## Group 10: Overlay Controls @@ -1408,7 +1364,7 @@ Recommended pattern: - one character ref in `ref_1` - `ref_mode = auto ref2v` - no explicit `` tags needed -- `per_beat_length = on` +- put `seconds:` on a beat when you want a specific length ### Two Characters Plus One Location @@ -1422,7 +1378,7 @@ Recommended pattern: - use `shot_seconds` as a safe ceiling - put `seconds:` inside beats that need exact control - check `plan_only` first -- expect clamping if you ask for more than H3 or VRAM allows +- expect an OOM if you ask for more than H3 or your hardware allows ## Best Way To Check What Will Really Happen diff --git a/dumas_h3_longvideos.py b/dumas_h3_longvideos.py index 476a59f..ce7960b 100644 --- a/dumas_h3_longvideos.py +++ b/dumas_h3_longvideos.py @@ -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 = [] # 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): diff --git a/js/h3_longvideos/index.js b/js/h3_longvideos/index.js index 72ac9ea..4c30e34 100644 --- a/js/h3_longvideos/index.js +++ b/js/h3_longvideos/index.js @@ -12,7 +12,7 @@ const GROUPS = [ id: "prompt", label: "Prompt", defaultCollapsed: false, - widgets: ["prompt", "resolution", "megapixels", "beat_split", "anchor_override", "per_beat_length", "shot_seconds", "plan_only", "fps"], + widgets: ["prompt", "resolution", "megapixels", "beat_split", "anchor_override", "shot_seconds", "plan_only", "fps"], }, { id: "refs", @@ -27,8 +27,8 @@ const GROUPS = [ widgets: [ "steps", "cfg", "sampler_name", "scheduler", "seed", "apply_model_sampling", "shift_video", "shift_audio", - "allow_oversize_shots", "vram_headroom_gb", "allow_res_backoff", - "decode_tile_frames", "decode_tile_size", "cleanup_between_shots", + "vram_headroom_gb", "allow_res_backoff", + "decode_tile_frames", "decode_tile_size", ], }, { diff --git a/tests/test_dumas_h3_longvideos.py b/tests/test_dumas_h3_longvideos.py index 4fc65e4..8337616 100644 --- a/tests/test_dumas_h3_longvideos.py +++ b/tests/test_dumas_h3_longvideos.py @@ -734,14 +734,27 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): for index in range(1, 10): self.assertNotIn(f"ref_image_{index}", optional) + self.assertNotIn("per_beat_length", optional) + self.assertNotIn("cleanup_between_shots", 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) + self.assertIn("GLOBAL per-shot maximum", tooltip) + self.assertIn("A beat's own `seconds:` directive can still ask for less", tooltip) + self.assertIn("honoring it; may spill to system RAM (slow) or OOM", tooltip) + + def test_resolve_shot_frames_honors_forced_request_over_budget(self): + original_estimate_shot_frames = self.module.estimate_shot_frames + try: + self.module.estimate_shot_frames = lambda *_args, **_kwargs: 73 + frames, note = self.module.resolve_shot_frames(10.0, 24, 16.0, 8.0, 1.5) + + self.assertEqual(frames, 243) + self.assertIn("honoring it", note) + finally: + self.module.estimate_shot_frames = original_estimate_shot_frames def test_run_defaults_match_declared_ref_widget_defaults(self): node = self.module.H3LongVideos() @@ -1152,7 +1165,7 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): module.quant_accel_note = lambda *_args, **_kwargs: "" module.lora_active = lambda _model: False module.resolve_shot_frames = lambda *_args, **_kwargs: (73, "") - module.plan_beat_frames = lambda beats, fps, budget, per_beat=True: ([73] * len(beats), []) + module.plan_beat_frames = lambda beats, fps, budget: ([73] * len(beats), []) module.dialogue_fit_warnings = lambda *_args, **_kwargs: [] module.dialogue_filler_warnings = lambda *_args, **_kwargs: [] module.distribute_generations = lambda anchor, beats, *_args, **_kwargs: [