diff --git a/dumas_h3_longvideos.py b/dumas_h3_longvideos.py index 9e1e54f..e938243 100644 --- a/dumas_h3_longvideos.py +++ b/dumas_h3_longvideos.py @@ -4558,7 +4558,7 @@ def _connected_refs(ref_slots): return [ref for ref in (ref_slots or []) if _reference_image(ref) is not None] -def resolve_tagged_refs(text, ref_list): +def resolve_tagged_refs(text, ref_list): """(rewritten text, images, dropped) for the tags in ONE shot. The tokenizer numbers references by their position in the list it is handed, so @@ -4579,11 +4579,31 @@ def resolve_tagged_refs(text, ref_list): return f"" if n in renumber else "" out = _PICTURE_TAG.sub(sub, text or "") - if dropped: # tidy the gap a removed tag leaves behind - out = re.sub(r"\s+([,.;:])", r"\1", out) - out = re.sub(r"(,\s*){2,}", ", ", out) - out = re.sub(r"\s{2,}", " ", out) - return out.strip(), [ref_list[n - 1] for n in live], dropped + if dropped: # tidy the gap a removed tag leaves behind + out = re.sub(r"\s+([,.;:])", r"\1", out) + out = re.sub(r"(,\s*){2,}", ", ", out) + out = re.sub(r"\s{2,}", " ", out) + return out.strip(), [ref_list[n - 1] for n in live], dropped + + +def resolve_prompt_refs(text, ref_list): + """(rewritten text, refs, dropped) for the refs a shot actually carries. + + Explicit tags still decide which slot numbers the prompt points at, + but named character matches must ride into the real ref-image list too. Without + 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.""" + rewritten, tagged_refs, dropped = resolve_tagged_refs(text, ref_list) + refs = list(tagged_refs) + seen = {id(ref) for ref in refs} + for _slot_number, ref in _named_character_refs_for_text(rewritten, ref_list): + marker = id(ref) + if marker in seen: + continue + seen.add(marker) + refs.append(ref) + return rewritten, refs, dropped def shot_references(ref_list, ref_mode, shot_index, handoff): @@ -6750,7 +6770,7 @@ 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_tagged_refs(gen, ref_slots)[1]: + if resolve_prompt_refs(gen, ref_slots)[1]: on.append(shot_index + 1) tagged_used = True else: @@ -6845,7 +6865,7 @@ class H3LongVideos: # 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_tagged_refs(gen_prompt, ref_slots) + gen_prompt, shot_refs, dropped = resolve_prompt_refs(gen_prompt, ref_slots) for n in dropped: if n not in ref_missing: ref_missing.append(n) diff --git a/tests/test_dumas_h3_longvideos.py b/tests/test_dumas_h3_longvideos.py index 97e77e8..e3be60c 100644 --- a/tests/test_dumas_h3_longvideos.py +++ b/tests/test_dumas_h3_longvideos.py @@ -423,6 +423,41 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): ) self.assertEqual(dropped, [4]) + def test_resolve_prompt_refs_keeps_named_character_images_alongside_tagged_location(self): + refs = [ + {"kind": "character", "image": "img1", "name": "Mara"}, + {"kind": "character", "image": "img2", "name": "Jon"}, + {"kind": "location", "image": "img3", "name": "Hangar"}, + ] + + text, references, dropped = self.module.resolve_prompt_refs( + "Mara and Jon argue inside .", + refs, + ) + + self.assertEqual(text, "Mara and Jon argue inside .") + self.assertEqual( + [self.module._reference_image(ref) for ref in references], + ["img3", "img1", "img2"], + ) + self.assertEqual(dropped, []) + + def test_resolve_prompt_refs_where_tagged_mode_stays_tag_only(self): + refs = [ + {"kind": "character", "image": "img1", "name": "Mara"}, + {"kind": "location", "image": "img2", "name": "Hangar"}, + ] + + text, references, dropped = self.module.resolve_prompt_refs( + "Mara waits in the hangar near .", + refs, + include_named=False, + ) + + self.assertEqual(text, "Mara waits in the hangar near .") + 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"}, @@ -444,6 +479,21 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): self.assertFalse(shot_tag_driven) self.assertEqual(mode_eff, "auto ref2v") + def test_resolve_prompt_refs_prioritizes_characters_before_locations(self): + refs = [ + {"kind": "location", "image": "img1", "name": "Hangar"}, + {"kind": "character", "image": "img2", "name": "Mara"}, + ] + + text, references, dropped = self.module.resolve_prompt_refs( + "[Generation 1] Mara waits in the hangar.", + refs, + ) + + self.assertEqual(text, "[Generation 1] Mara waits in the hangar.") + self.assertEqual([self.module._reference_image(ref) for ref in references], ["img2", "img1"]) + self.assertEqual(dropped, []) + def test_shot_references_uses_all_connected_sparse_slots(self): refs = [ None, @@ -697,6 +747,21 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): self.assertIn("Persistent appearance for Mara: silver hair.", context) self.assertNotIn("Persistent wardrobe/style for Mara: red jacket.", context) + def test_resolve_prompt_refs_adds_named_location_refs(self): + refs = [ + {"kind": "location", "image": "img2", "name": "Hangar", "description": "wet concrete floor"}, + ] + + rewritten, matched, dropped = self.module.resolve_prompt_refs( + "[Generation 1] They wait in the hangar.", + refs, + ) + + self.assertEqual(rewritten, "[Generation 1] They wait in the hangar.") + self.assertEqual(dropped, []) + self.assertEqual(len(matched), 1) + self.assertEqual(matched[0]["name"], "Hangar") + def test_resolve_tagged_refs_drops_reference_without_image(self): refs = [ {"kind": "character", "image": None, "name": "Mara"},