Tighten tagged ref routing and anchor presets
This commit is contained in:
+25
-14
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user