Tighten tagged ref routing and anchor presets

This commit is contained in:
2026-08-28 15:45:22 +00:00
parent ddd7dbb5af
commit e29d01ad24
4 changed files with 56 additions and 19 deletions
+25 -14
View File
@@ -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 <Picture N> 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 <Picture 2> alone still resolves. Character names in the beat can also "
"pull their matching character refs into the real image-conditioning list. "
"so <Picture 2> 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 <Picture N> 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)
+1 -4
View File
@@ -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(
[
(
+29
View File
@@ -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 <Picture 2>.",
refs,
include_named=False,
)
self.assertEqual(text, "Mara waits in the hangar near <Picture 1>.")
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
+1 -1
View File
@@ -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()