From 9cbab72e86069fb410d5cd795c1c0397161d685c Mon Sep 17 00:00:00 2001 From: Chris Dumas Date: Fri, 28 Aug 2026 22:09:12 +0000 Subject: [PATCH] Optimize long videos ref bookkeeping --- dumas_h3_longvideos.py | 68 +++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/dumas_h3_longvideos.py b/dumas_h3_longvideos.py index 1d4fab1..d5a8b1c 100644 --- a/dumas_h3_longvideos.py +++ b/dumas_h3_longvideos.py @@ -4422,10 +4422,10 @@ def _named_refs_for_text(text, ref_slots, kinds=None): return matched -def _matched_reference_slots(text, ref_slots): +def _matched_reference_slots(text, ref_slots, normalized_refs=None): matched = [] seen = set() - normalized_slots = _normalized_ref_slots(ref_slots) + normalized_slots = normalized_refs if normalized_refs is not None else _normalized_ref_slots(ref_slots) for slot_number, ref in ( _slot_refs_for_text(text, normalized_slots) + _named_refs_for_text(text, normalized_slots, kinds=("character", "location")) @@ -4437,9 +4437,16 @@ def _matched_reference_slots(text, ref_slots): return matched -def _reference_context_for_text(text, ref_slots, include_character_wardrobe=True): +def _reference_context_for_text( + text, + ref_slots, + include_character_wardrobe=True, + normalized_refs=None, +): parts = [] - for slot_number, ref in _matched_reference_slots(text, ref_slots): + if normalized_refs is None: + normalized_refs = _normalized_ref_slots(ref_slots) + for slot_number, ref in _matched_reference_slots(text, ref_slots, normalized_refs=normalized_refs): label = _reference_text(ref.get("name")) or _reference_text(ref.get("id")) or f"reference {slot_number}" description = _reference_sentence(ref.get("description")) wardrobe = (_reference_sentence(ref.get("wardrobe")) @@ -4480,10 +4487,12 @@ def _inject_reference_context(block, context): ) -def _reference_character_memory(ref_slots): +def _reference_character_memory(ref_slots, normalized_refs=None): lines = [] seen = set() - for slot_number, ref in enumerate(_normalized_ref_slots(ref_slots), 1): + if normalized_refs is None: + normalized_refs = _normalized_ref_slots(ref_slots) + for slot_number, ref in enumerate(normalized_refs, 1): if ref is None: continue if ref.get("kind") != "character": @@ -4504,12 +4513,14 @@ def _reference_character_memory(ref_slots): return "\n".join(lines) -def _connected_refs(ref_slots): +def _connected_refs(ref_slots, normalized_refs=None): """Connected refs only, preserving slot order and skipping empty sockets.""" - return [ref for ref in _normalized_ref_slots(ref_slots) if _reference_image(ref) is not None] + if normalized_refs is None: + normalized_refs = _normalized_ref_slots(ref_slots) + return [ref for ref in normalized_refs if _reference_image(ref) is not None] -def resolve_tagged_refs(text, ref_list): +def resolve_tagged_refs(text, ref_list, normalized_refs=None): """(rewritten text, images, dropped) for the tags in ONE shot. The tokenizer numbers references by their position in the list it is handed, so @@ -4521,7 +4532,8 @@ def resolve_tagged_refs(text, ref_list): A tag naming a slot with no image connected refers to nothing at all, so it is removed from the text rather than left to confuse the encoder, and reported.""" wanted = picture_tags(text) - normalized_refs = _normalized_ref_slots(ref_list) + if normalized_refs is None: + normalized_refs = _normalized_ref_slots(ref_list) live = [n for n in wanted if 1 <= n <= len(normalized_refs) and _reference_image(normalized_refs[n - 1]) is not None] dropped = [n for n in wanted if n not in live] renumber = {old: new for new, old in enumerate(live, 1)} @@ -4538,7 +4550,7 @@ def resolve_tagged_refs(text, ref_list): return out.strip(), [normalized_refs[n - 1] for n in live], dropped -def resolve_prompt_refs(text, ref_list, include_named=True): +def resolve_prompt_refs(text, ref_list, include_named=True, normalized_refs=None): """(rewritten text, refs, dropped) for the refs a shot actually carries. Explicit tags still decide which slot numbers the prompt points at, @@ -4546,8 +4558,9 @@ def resolve_prompt_refs(text, ref_list, include_named=True): that split, a shot could inherit the facts/context for "Mara" and "Jon" while only carrying a tagged location image, which reads exactly like the names were understood but the faces were ignored.""" - normalized_refs = _normalized_ref_slots(ref_list) - rewritten, tagged_refs, dropped = resolve_tagged_refs(text, normalized_refs) + if normalized_refs is None: + normalized_refs = _normalized_ref_slots(ref_list) + rewritten, tagged_refs, dropped = resolve_tagged_refs(text, normalized_refs, normalized_refs=normalized_refs) refs = list(tagged_refs) seen = {id(ref) for ref in refs} if include_named: @@ -4560,7 +4573,7 @@ def resolve_prompt_refs(text, ref_list, include_named=True): return rewritten, refs, dropped -def shot_references(ref_list, ref_mode, shot_index, handoff): +def shot_references(ref_list, ref_mode, shot_index, handoff, connected_refs=None): """Pure: which reference images shot `shot_index` is conditioned on, or [] when the shot should use the keyframe handoff instead. @@ -4586,7 +4599,7 @@ def shot_references(ref_list, ref_mode, shot_index, handoff): back as a soft signal (the model is shown where the last shot ended rather than told to start exactly there), and it stays a single ref2va task, so nothing conflicts.""" - refs = _connected_refs(ref_list) + refs = connected_refs if connected_refs is not None else _connected_refs(ref_list) if not refs: return [] if ref_mode == "auto ref2v": @@ -6397,7 +6410,9 @@ class H3LongVideos: ), legacy_ref_slots) ) ref_slots = direct_ref_slots - direct_ref_count = len(_connected_refs(direct_ref_slots)) + normalized_ref_slots = _normalized_ref_slots(ref_slots) + connected_refs = [ref for ref in normalized_ref_slots if _reference_image(ref) is not None] + direct_ref_count = len(connected_refs) explicit_character_memory = (character_memory or "").strip() derived_character_memory = _reference_character_memory(ref_slots) effective_character_memory = explicit_character_memory or derived_character_memory @@ -6620,6 +6635,7 @@ class H3LongVideos: block, ref_slots, include_character_wardrobe=not bool(explicit_character_memory), + normalized_refs=normalized_ref_slots, ) if context: block = _inject_reference_context(block, context) @@ -6696,7 +6712,7 @@ class H3LongVideos: else "prompt/soundscape silencing only")) # Same reference accounting the render reports: which shots lose the # handoff is a composition decision, so it belongs in the preview. - n_refs = len(_connected_refs(ref_slots)) + n_refs = len(connected_refs) plan_ref = "" if n_refs: # Mirror the render's placement exactly: 'where tagged' reads the @@ -6709,7 +6725,12 @@ class H3LongVideos: for shot_index, gen in enumerate(gens): shot_mode = beat_ref_mode_directive(beats[shot_index] if shot_index < len(beats) else "") or ref_mode if shot_mode in ("where tagged", "auto ref2v") and any_tags_anywhere: - if resolve_prompt_refs(gen, ref_slots, include_named=(shot_mode == "auto ref2v"))[1]: + if resolve_prompt_refs( + gen, + ref_slots, + include_named=(shot_mode == "auto ref2v"), + normalized_refs=normalized_ref_slots, + )[1]: on.append(shot_index + 1) tagged_used = True else: @@ -6768,7 +6789,7 @@ class H3LongVideos: latent_chunks = [] # per-shot sampled latents, pre-decode mouth_settled = [] # shots seeded from a settled (closed) mouth handoff, sr = first_frame, None - connected_ref_count = len(_connected_refs(ref_slots)) + connected_ref_count = len(connected_refs) 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 @@ -6807,12 +6828,19 @@ class H3LongVideos: gen_prompt, ref_slots, include_named=(shot_mode == "auto ref2v"), + normalized_refs=normalized_ref_slots, ) for n in dropped: if n not in ref_missing: ref_missing.append(n) else: - shot_refs = shot_references(ref_slots, shot_mode_eff, i, handoff) + shot_refs = shot_references( + ref_slots, + shot_mode_eff, + i, + handoff, + connected_refs=connected_refs, + ) # A shot that follows a strip starts FRESH. Continuing from a frame that # still shows the garment is how it reappears -- the picture outvotes the # text every time. Costs a cut exactly where the state changes, which is