Tighten tagged ref routing and anchor presets
This commit is contained in:
+17
-6
@@ -4405,15 +4405,20 @@ def _named_refs_for_text(text, ref_slots, kinds=None):
|
|||||||
haystack = str(text or "")
|
haystack = str(text or "")
|
||||||
matched = []
|
matched = []
|
||||||
wanted = {str(k).strip().lower() for k in (kinds or ()) if str(k).strip()}
|
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):
|
for slot_number, ref in enumerate(ref_slots or [], 1):
|
||||||
if ref is None or _reference_image(ref) is None:
|
if ref is None or _reference_image(ref) is None:
|
||||||
continue
|
continue
|
||||||
if wanted and str(ref.get("kind") or "").strip().lower() not in wanted:
|
if wanted and str(ref.get("kind") or "").strip().lower() not in wanted:
|
||||||
continue
|
continue
|
||||||
for name in _reference_name_keys(ref):
|
for name in _reference_name_keys(ref):
|
||||||
|
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):
|
if re.search(r"\b" + re.escape(name) + r"\b", haystack, re.I):
|
||||||
matched.append((slot_number, ref))
|
matched.append((slot_number, ref))
|
||||||
break
|
|
||||||
return matched
|
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
|
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.
|
"""(rewritten text, refs, dropped) for the refs a shot actually carries.
|
||||||
|
|
||||||
Explicit <Picture N> tags still decide which slot numbers the prompt points at,
|
Explicit <Picture N> tags still decide which slot numbers the prompt points at,
|
||||||
@@ -4545,6 +4550,7 @@ def resolve_prompt_refs(text, ref_list):
|
|||||||
rewritten, tagged_refs, dropped = resolve_tagged_refs(text, normalized_refs)
|
rewritten, tagged_refs, dropped = resolve_tagged_refs(text, normalized_refs)
|
||||||
refs = list(tagged_refs)
|
refs = list(tagged_refs)
|
||||||
seen = {id(ref) for ref in refs}
|
seen = {id(ref) for ref in refs}
|
||||||
|
if include_named:
|
||||||
for _slot_number, ref in _named_refs_for_text(rewritten, normalized_refs, kinds=("character", "location")):
|
for _slot_number, ref in _named_refs_for_text(rewritten, normalized_refs, kinds=("character", "location")):
|
||||||
marker = id(ref)
|
marker = id(ref)
|
||||||
if marker in seen:
|
if marker in seen:
|
||||||
@@ -5999,8 +6005,9 @@ class H3LongVideos:
|
|||||||
"for long chains where identity drift matters more than strict per-shot "
|
"for long chains where identity drift matters more than strict per-shot "
|
||||||
"routing. 'where tagged' keeps the old strict behavior, including the "
|
"routing. 'where tagged' keeps the old strict behavior, including the "
|
||||||
"first-shot fallback when no tags are found. Tags are renumbered per shot, "
|
"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 "
|
"so <Picture 2> alone still resolves. In 'auto ref2v', character and "
|
||||||
"pull their matching character refs into the real image-conditioning list. "
|
"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' / "
|
"'first shot' / 'every shot' / "
|
||||||
"'every shot + handoff ref' go purely by position. Ignored when no "
|
"'every shot + handoff ref' go purely by position. Ignored when no "
|
||||||
"ref_image is connected."}),
|
"ref_image is connected."}),
|
||||||
@@ -6702,7 +6709,7 @@ class H3LongVideos:
|
|||||||
for shot_index, gen in enumerate(gens):
|
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
|
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 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)
|
on.append(shot_index + 1)
|
||||||
tagged_used = True
|
tagged_used = True
|
||||||
else:
|
else:
|
||||||
@@ -6796,7 +6803,11 @@ class H3LongVideos:
|
|||||||
# The prompt itself says where each reference belongs: the shot whose
|
# The prompt itself says where each reference belongs: the shot whose
|
||||||
# text names <Picture N> gets image N, renumbered to match what that
|
# text names <Picture N> gets image N, renumbered to match what that
|
||||||
# shot actually carries. Every untagged shot keeps its handoff.
|
# 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:
|
for n in dropped:
|
||||||
if n not in ref_missing:
|
if n not in ref_missing:
|
||||||
ref_missing.append(n)
|
ref_missing.append(n)
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ _H3_PLAN_IMAGE_BINDINGS = OrderedDict()
|
|||||||
_H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
_H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
||||||
_H3_PLAN_IMAGE_SLOTS = 9
|
_H3_PLAN_IMAGE_SLOTS = 9
|
||||||
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
|
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
|
||||||
_ANCHOR_STYLE_H3_NOTE = (
|
_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_PRESETS = OrderedDict(
|
_ANCHOR_STYLE_PRESETS = OrderedDict(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -407,6 +407,22 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(dropped, [])
|
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):
|
def test_shot_references_uses_all_connected_sparse_slots(self):
|
||||||
refs = [
|
refs = [
|
||||||
None,
|
None,
|
||||||
@@ -522,6 +538,19 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
self.assertIn("Location context for Hangar: wet concrete floor.", context)
|
self.assertIn("Location context for Hangar: wet concrete floor.", context)
|
||||||
self.assertIn("Location notes for Hangar: cold industrial lighting.", 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):
|
def test_run_uses_legacy_ref_image_inputs_when_new_slots_are_empty(self):
|
||||||
calls = {}
|
calls = {}
|
||||||
original_parse_resolution = self.module.parse_resolution
|
original_parse_resolution = self.module.parse_resolution
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIn("found-footage", result[0])
|
self.assertIn("found-footage", result[0])
|
||||||
self.assertIn("real time", 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):
|
def test_anchor_style_node_prefers_manual_description_edits(self):
|
||||||
node = self.image_nodes.DumasAnchorStyleNode()
|
node = self.image_nodes.DumasAnchorStyleNode()
|
||||||
|
|||||||
Reference in New Issue
Block a user