diff --git a/dumas_h3_longvideos.py b/dumas_h3_longvideos.py index 125f789..1d4fab1 100644 --- a/dumas_h3_longvideos.py +++ b/dumas_h3_longvideos.py @@ -4405,15 +4405,20 @@ def _named_refs_for_text(text, ref_slots, kinds=None): haystack = str(text or "") matched = [] wanted = {str(k).strip().lower() for k in (kinds or ()) if str(k).strip()} + by_name = {} for slot_number, ref in enumerate(ref_slots or [], 1): if ref is None or _reference_image(ref) is None: continue if wanted and str(ref.get("kind") or "").strip().lower() not in wanted: continue for name in _reference_name_keys(ref): - if re.search(r"\b" + re.escape(name) + r"\b", haystack, re.I): - matched.append((slot_number, ref)) - break + by_name.setdefault(name.lower(), []).append((slot_number, ref, name)) + for entries in by_name.values(): + if len(entries) != 1: + continue + slot_number, ref, name = entries[0] + if re.search(r"\b" + re.escape(name) + r"\b", haystack, re.I): + matched.append((slot_number, ref)) return matched @@ -4533,7 +4538,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): +def resolve_prompt_refs(text, ref_list, include_named=True): """(rewritten text, refs, dropped) for the refs a shot actually carries. Explicit tags still decide which slot numbers the prompt points at, @@ -4545,12 +4550,13 @@ def resolve_prompt_refs(text, ref_list): rewritten, tagged_refs, dropped = resolve_tagged_refs(text, normalized_refs) refs = list(tagged_refs) seen = {id(ref) for ref in refs} - for _slot_number, ref in _named_refs_for_text(rewritten, normalized_refs, kinds=("character", "location")): - marker = id(ref) - if marker in seen: - continue - seen.add(marker) - refs.append(ref) + if include_named: + for _slot_number, ref in _named_refs_for_text(rewritten, normalized_refs, kinds=("character", "location")): + marker = id(ref) + if marker in seen: + continue + seen.add(marker) + refs.append(ref) return rewritten, refs, dropped @@ -5999,8 +6005,9 @@ class H3LongVideos: "for long chains where identity drift matters more than strict per-shot " "routing. 'where tagged' keeps the old strict behavior, including the " "first-shot fallback when no tags are found. Tags are renumbered per shot, " - "so alone still resolves. Character names in the beat can also " - "pull their matching character refs into the real image-conditioning list. " + "so alone still resolves. In 'auto ref2v', character and " + "location names in the beat can also pull their matching refs into the " + "real image-conditioning list; 'where tagged' does NOT do that. " "'first shot' / 'every shot' / " "'every shot + handoff ref' go purely by position. Ignored when no " "ref_image is connected."}), @@ -6702,7 +6709,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_prompt_refs(gen, ref_slots)[1]: + if resolve_prompt_refs(gen, ref_slots, include_named=(shot_mode == "auto ref2v"))[1]: on.append(shot_index + 1) tagged_used = True else: @@ -6796,7 +6803,11 @@ 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_prompt_refs(gen_prompt, ref_slots) + gen_prompt, shot_refs, dropped = resolve_prompt_refs( + gen_prompt, + ref_slots, + include_named=(shot_mode == "auto ref2v"), + ) for n in dropped: if n not in ref_missing: ref_missing.append(n) diff --git a/dumas_image_nodes.py b/dumas_image_nodes.py index 0f7753e..3bb208c 100644 --- a/dumas_image_nodes.py +++ b/dumas_image_nodes.py @@ -26,10 +26,7 @@ _H3_PLAN_IMAGE_BINDINGS = OrderedDict() _H3_PLAN_IMAGE_BINDINGS_CAP = 128 _H3_PLAN_IMAGE_SLOTS = 9 _FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif") -_ANCHOR_STYLE_H3_NOTE = ( - " Keep this anchor focused on persistent camera language, lighting, texture, environment treatment, and tone; " - "do not name characters or describe one-off actions." -) +_ANCHOR_STYLE_H3_NOTE = "" _ANCHOR_STYLE_PRESETS = OrderedDict( [ ( diff --git a/tests/test_dumas_h3_longvideos.py b/tests/test_dumas_h3_longvideos.py index e869935..21bcf79 100644 --- a/tests/test_dumas_h3_longvideos.py +++ b/tests/test_dumas_h3_longvideos.py @@ -407,6 +407,22 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): ) 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_shot_references_uses_all_connected_sparse_slots(self): refs = [ None, @@ -522,6 +538,19 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): self.assertIn("Location context for Hangar: wet concrete floor.", context) self.assertIn("Location notes for Hangar: cold industrial lighting.", context) + def test_reference_context_skips_ambiguous_name_matches(self): + refs = [ + {"kind": "character", "image": "img1", "name": "Alex", "description": "short dark hair"}, + {"kind": "character", "image": "img2", "name": "Alex", "description": "tall blond hair"}, + ] + + context = self.module._reference_context_for_text( + "[Generation 1] Alex enters the room.", + refs, + ) + + self.assertEqual(context, "") + def test_run_uses_legacy_ref_image_inputs_when_new_slots_are_empty(self): calls = {} original_parse_resolution = self.module.parse_resolution diff --git a/tests/test_dumas_image_nodes.py b/tests/test_dumas_image_nodes.py index 84226e4..0ebe895 100644 --- a/tests/test_dumas_image_nodes.py +++ b/tests/test_dumas_image_nodes.py @@ -375,7 +375,7 @@ class DumasImageNodeTests(unittest.TestCase): self.assertIn("found-footage", result[0]) self.assertIn("real time", result[0]) - self.assertIn("persistent camera language", result[0]) + self.assertNotIn("persistent camera language", result[0]) def test_anchor_style_node_prefers_manual_description_edits(self): node = self.image_nodes.DumasAnchorStyleNode()