diff --git a/dumas_h3_longvideos.py b/dumas_h3_longvideos.py index ad22b9d..262ffd4 100644 --- a/dumas_h3_longvideos.py +++ b/dumas_h3_longvideos.py @@ -4631,6 +4631,42 @@ def resolve_prompt_refs(text, ref_list, include_named=True, normalized_refs=None seen.add(marker) refs.append(ref) return rewritten, refs, dropped + + +def resolve_shot_references(text, ref_list, ref_mode, shot_index, handoff, connected_refs=None, normalized_refs=None): + """(rewritten text, refs, dropped, shot_tag_driven, effective_mode) for one beat. + + Each beat should be able to pick up character names on its own. A stray + tag in some other beat should not make name-matched characters + disappear from the untagged beats in the same chain.""" + refs = connected_refs if connected_refs is not None else _connected_refs(ref_list, normalized_refs=normalized_refs) + if not refs: + return text, [], [], False, ref_mode + normalized_refs = normalized_refs if normalized_refs is not None else _normalized_ref_slots(ref_list) + shot_tags = picture_tags(text) + if ref_mode == "auto ref2v": + rewritten, shot_refs, dropped = resolve_prompt_refs( + text, + ref_list, + include_named=True, + normalized_refs=normalized_refs, + ) + if shot_tags: + return rewritten, shot_refs, dropped, True, "auto ref2v" + if shot_refs: + return rewritten, shot_refs, dropped, False, "auto ref2v" + return text, shot_references(ref_list, "every shot", shot_index, handoff, connected_refs=refs), [], False, "every shot" + if ref_mode == "where tagged": + if shot_tags: + rewritten, shot_refs, dropped = resolve_prompt_refs( + text, + ref_list, + include_named=False, + normalized_refs=normalized_refs, + ) + return rewritten, shot_refs, dropped, True, "where tagged" + return text, shot_references(ref_list, "first shot", shot_index, handoff, connected_refs=refs), [], False, "first shot" + return text, shot_references(ref_list, ref_mode, shot_index, handoff, connected_refs=refs), [], False, ref_mode def shot_references(ref_list, ref_mode, shot_index, handoff, connected_refs=None): @@ -6798,21 +6834,20 @@ class H3LongVideos: effective_modes = [] 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"), - normalized_refs=normalized_ref_slots, - )[1]: - on.append(shot_index + 1) - tagged_used = True - else: - mode_eff = ("every shot" if shot_mode == "auto ref2v" - else "first shot" if shot_mode == "where tagged" else shot_mode) - effective_modes.append(mode_eff) - if shot_references(ref_slots, mode_eff, shot_index, 1 if shot_index else None): - on.append(shot_index + 1) + _rewritten, refs, _dropped, shot_tag_driven, mode_eff = resolve_shot_references( + gen, + ref_slots, + shot_mode, + shot_index, + 1 if shot_index else None, + connected_refs=connected_refs, + normalized_refs=normalized_ref_slots, + ) + effective_modes.append(mode_eff) + if shot_tag_driven: + tagged_used = tagged_used or bool(refs) + if refs: + on.append(shot_index + 1) if tagged_used: how = "placed by tags" else: @@ -6855,11 +6890,11 @@ class H3LongVideos: sum(plan_lens), shots, total, float(fps), int(fps), _empty_av_latent(w, h, 5, fps)[0], global_soundscape, [], []) - 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 = [], [], [] + 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 handoff, sr = first_frame, None @@ -6871,6 +6906,7 @@ class H3LongVideos: ref_mode_used = [] continuity_used = [] ref_aug_used = [] + any_explicit_picture_tags = False shot_timings = [] if cleanup_between_shots: _deep_cleanup() # start the first (heaviest) shot with max free VRAM @@ -6888,34 +6924,25 @@ class H3LongVideos: shot_continuity = beat_continuity_directive(beat_text) or "auto" shot_ref_noise_aug = beat_ref_noise_aug_directive(beat_text) shot_aug = ref_noise_aug if shot_ref_noise_aug is None else shot_ref_noise_aug - shot_tag_driven = bool(connected_ref_count) and shot_mode in ("where tagged", "auto ref2v") and any_tags_anywhere - if shot_tag_driven: - shot_mode_eff = shot_mode - else: - shot_mode_eff = ("every shot" if shot_mode == "auto ref2v" - else "first shot" if shot_mode == "where tagged" else shot_mode) + gen_prompt, shot_refs, dropped, shot_tag_driven, shot_mode_eff = resolve_shot_references( + gen_prompt, + ref_slots, + shot_mode, + i, + handoff, + connected_refs=connected_refs, + normalized_refs=normalized_ref_slots, + ) carry_keyframe = False # tagged shot keeps its handoff as a keyframe if shot_tag_driven: + any_explicit_picture_tags = True # The prompt itself says where each reference belongs: the shot whose # text names gets image N, renumbered to match what that - # shot actually carries. Every untagged shot keeps its handoff. - gen_prompt, shot_refs, dropped = resolve_prompt_refs( - gen_prompt, - ref_slots, - include_named=(shot_mode == "auto ref2v"), - normalized_refs=normalized_ref_slots, - ) + # shot actually carries. Named character matches are already included + # in the same pass; the tag only makes the slot mapping explicit. 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, - 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 @@ -6975,7 +7002,7 @@ class H3LongVideos: "soft carry" if shot_handoff is not None else "hard cut") if carry_keyframe and (i + 1) not in ref_keyframed and handoff is not None: ref_keyframed.append(i + 1) - ref_mode_used.append(shot_mode if shot_tag_driven else shot_mode_eff) + ref_mode_used.append(shot_mode_eff) continuity_used.append(continuity_label) ref_aug_used.append(shot_aug) if shot_refs: @@ -7248,7 +7275,7 @@ class H3LongVideos: if connected_ref_count and ref_shots: kept = [n for n in range(1, len(gens) + 1) if n not in ref_shots] distinct_ref_modes = list(dict.fromkeys(ref_mode_used)) - tagged_used = any(mode in ("where tagged", "auto ref2v") for mode in ref_mode_used) and any_tags_anywhere + tagged_used = any_explicit_picture_tags ref_placement = ("placed by tags" if tagged_used else f"ref_mode '{distinct_ref_modes[0]}'" if len(distinct_ref_modes) == 1 else "mixed per-shot ref_mode") diff --git a/tests/test_dumas_h3_longvideos.py b/tests/test_dumas_h3_longvideos.py index 01610be..b4c3330 100644 --- a/tests/test_dumas_h3_longvideos.py +++ b/tests/test_dumas_h3_longvideos.py @@ -458,6 +458,27 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): self.assertEqual([self.module._reference_image(ref) for ref in references], ["img2"]) self.assertEqual(dropped, []) + def test_resolve_shot_references_uses_named_characters_without_picture_tags(self): + refs = [ + {"kind": "character", "image": "img1", "name": "Mara"}, + {"kind": "character", "image": "img2", "name": "Jon"}, + {"kind": "location", "image": "img3", "name": "Hangar"}, + ] + + text, references, dropped, shot_tag_driven, mode_eff = self.module.resolve_shot_references( + "[Generation 1] Mara crosses the hangar.", + refs, + "auto ref2v", + 0, + None, + ) + + self.assertEqual(text, "[Generation 1] Mara crosses the hangar.") + self.assertEqual([self.module._reference_image(ref) for ref in references], ["img1"]) + self.assertEqual(dropped, []) + self.assertFalse(shot_tag_driven) + self.assertEqual(mode_eff, "auto ref2v") + def test_shot_references_uses_all_connected_sparse_slots(self): refs = [ None,