Harden and streamline H3 reference matching
This commit is contained in:
+13
-11
@@ -944,23 +944,23 @@ Character wardrobe seeding:
|
|||||||
What the long-videos code actually uses:
|
What the long-videos code actually uses:
|
||||||
|
|
||||||
- `image`: the real location reference image H3 sees when that slot is selected for a shot
|
- `image`: the real location reference image H3 sees when that slot is selected for a shot
|
||||||
- `description`: injected as location context when that slot is explicitly tagged
|
- `description`: injected as location context when that location ref is matched
|
||||||
- `general`: injected as location notes when that slot is explicitly tagged
|
- `general`: injected as location notes when that location ref is matched
|
||||||
- `name`, `location_id`, `alias`: used mainly as labels in summaries and injected sentences, not for automatic prose matching
|
- `name`, `location_id`, `alias`: used for location-name matching in beat text and as labels in injected sentences
|
||||||
|
|
||||||
Important difference from character refs:
|
Important difference from character refs:
|
||||||
|
|
||||||
- location refs are **not** auto-matched from location names in beat prose
|
- location refs can now be matched from location names or aliases in beat prose
|
||||||
- if you write `in the cafe` and do not tag `<Picture N>`, the location image may still be in the shot because of `ref_mode`, but the location text metadata is not auto-injected just from that name
|
- explicit `<Picture N>` tags are still the strongest way to force an exact slot on an exact beat
|
||||||
|
|
||||||
### When location data gets used
|
### When location data gets used
|
||||||
|
|
||||||
Location node data has two separate paths:
|
Location node data has two separate paths:
|
||||||
|
|
||||||
1. The location image can be attached to shots through normal ref routing.
|
1. The location image can be attached to shots through normal ref routing.
|
||||||
2. The location text metadata is only injected when you explicitly tag that slot with `<Picture N>`.
|
2. The location text metadata is injected when the beat tags that slot with `<Picture N>` or names the location by `name`, `id`, or `alias`.
|
||||||
|
|
||||||
So location references are more tag-driven than character references.
|
So location references are no longer tag-only, but tags are still the safest exact-routing tool.
|
||||||
|
|
||||||
Simple usage patterns:
|
Simple usage patterns:
|
||||||
|
|
||||||
@@ -972,7 +972,8 @@ What to remember:
|
|||||||
|
|
||||||
- tags control explicit slot placement
|
- tags control explicit slot placement
|
||||||
- names can pull matching character refs into the real image-conditioning list
|
- names can pull matching character refs into the real image-conditioning list
|
||||||
- location refs are often tagged because they are about where the shot happens
|
- names can now pull matching location refs into the real image-conditioning list too
|
||||||
|
- location refs are still often tagged because they are about where the shot happens
|
||||||
- character refs are often named because they are about who is in the shot
|
- character refs are often named because they are about who is in the shot
|
||||||
|
|
||||||
### Scenarios
|
### Scenarios
|
||||||
@@ -1021,12 +1022,13 @@ Setup:
|
|||||||
|
|
||||||
What happens:
|
What happens:
|
||||||
|
|
||||||
- with `auto ref2v` or `every shot`, the cafe image may still be conditioning the shot
|
- the word `cafe` can now match the location ref by name or alias
|
||||||
- but the location `description` and `general` fields are not auto-injected from the word `cafe`
|
- that means the location image and location text context can both be pulled in even without a tag
|
||||||
|
- `<Picture 3>` is still better if you want exact manual slot routing on that specific beat
|
||||||
|
|
||||||
What to do:
|
What to do:
|
||||||
|
|
||||||
- add `<Picture 3>` on the beat where the location wording matters
|
- add `<Picture 3>` when you want to force that exact location slot on that exact beat
|
||||||
|
|
||||||
### Scenario: character face is partly right, but identity is weak
|
### Scenario: character face is partly right, but identity is weak
|
||||||
|
|
||||||
|
|||||||
+42
-17
@@ -3288,9 +3288,10 @@ def annotate_script_debug(gens, anatomy_shots, anatomy_mode, ref_slots):
|
|||||||
def annotate_script_refs(gens, ref_slots):
|
def annotate_script_refs(gens, ref_slots):
|
||||||
"""Per-shot reference routing summary for the script socket."""
|
"""Per-shot reference routing summary for the script socket."""
|
||||||
lines = []
|
lines = []
|
||||||
|
normalized_slots = _normalized_ref_slots(ref_slots)
|
||||||
for shot_index, gen in enumerate(gens or [], 1):
|
for shot_index, gen in enumerate(gens or [], 1):
|
||||||
tagged = _slot_refs_for_text(gen, ref_slots)
|
tagged = _slot_refs_for_text(gen, normalized_slots)
|
||||||
named = _named_character_refs_for_text(gen, ref_slots)
|
named = _named_refs_for_text(gen, normalized_slots, kinds=("character", "location"))
|
||||||
seen = set()
|
seen = set()
|
||||||
merged = []
|
merged = []
|
||||||
for slot_number, ref in tagged:
|
for slot_number, ref in tagged:
|
||||||
@@ -4282,7 +4283,10 @@ def _reference_slot(ref, slot_index=None):
|
|||||||
|
|
||||||
def _reference_image(ref):
|
def _reference_image(ref):
|
||||||
try:
|
try:
|
||||||
normalized = _reference_slot(ref)
|
if isinstance(ref, dict):
|
||||||
|
normalized = _image_nodes.normalize_reference(ref, allow_image_fallback=False)
|
||||||
|
else:
|
||||||
|
normalized = _reference_slot(ref)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
return normalized.get("image")
|
return normalized.get("image")
|
||||||
@@ -4372,25 +4376,39 @@ def _reference_name_keys(ref):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_ref_slots(ref_slots):
|
||||||
|
"""Normalize every connected ref once so downstream helpers can reuse them."""
|
||||||
|
out = []
|
||||||
|
for slot_number, raw in enumerate(ref_slots or [], 1):
|
||||||
|
if raw is None:
|
||||||
|
out.append(None)
|
||||||
|
elif isinstance(raw, dict) and "image" in raw and raw.get("image") is None:
|
||||||
|
out.append(_image_nodes.normalize_reference(raw, picture_id=slot_number, allow_image_fallback=False))
|
||||||
|
else:
|
||||||
|
out.append(_reference_slot(raw, slot_number))
|
||||||
|
return tuple(out)
|
||||||
|
|
||||||
|
|
||||||
def _slot_refs_for_text(text, ref_slots):
|
def _slot_refs_for_text(text, ref_slots):
|
||||||
refs = []
|
refs = []
|
||||||
for slot_number in picture_tags(text):
|
for slot_number in picture_tags(text):
|
||||||
if not (1 <= slot_number <= len(ref_slots or [])):
|
if not (1 <= slot_number <= len(ref_slots or [])):
|
||||||
continue
|
continue
|
||||||
raw = ref_slots[slot_number - 1]
|
ref = ref_slots[slot_number - 1]
|
||||||
ref = _reference_slot(raw, slot_number)
|
|
||||||
if _reference_image(ref) is None:
|
if _reference_image(ref) is None:
|
||||||
continue
|
continue
|
||||||
refs.append((slot_number, ref))
|
refs.append((slot_number, ref))
|
||||||
return refs
|
return refs
|
||||||
|
|
||||||
|
|
||||||
def _named_character_refs_for_text(text, ref_slots):
|
def _named_refs_for_text(text, ref_slots, kinds=None):
|
||||||
haystack = str(text or "")
|
haystack = str(text or "")
|
||||||
matched = []
|
matched = []
|
||||||
for slot_number, raw in enumerate(ref_slots or [], 1):
|
wanted = {str(k).strip().lower() for k in (kinds or ()) if str(k).strip()}
|
||||||
ref = _reference_slot(raw, slot_number)
|
for slot_number, ref in enumerate(ref_slots or [], 1):
|
||||||
if ref.get("kind") != "character" or _reference_image(ref) is None:
|
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
|
continue
|
||||||
for name in _reference_name_keys(ref):
|
for name in _reference_name_keys(ref):
|
||||||
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):
|
||||||
@@ -4402,7 +4420,11 @@ def _named_character_refs_for_text(text, ref_slots):
|
|||||||
def _matched_reference_slots(text, ref_slots):
|
def _matched_reference_slots(text, ref_slots):
|
||||||
matched = []
|
matched = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for slot_number, ref in _slot_refs_for_text(text, ref_slots) + _named_character_refs_for_text(text, ref_slots):
|
normalized_slots = _normalized_ref_slots(ref_slots)
|
||||||
|
for slot_number, ref in (
|
||||||
|
_slot_refs_for_text(text, normalized_slots)
|
||||||
|
+ _named_refs_for_text(text, normalized_slots, kinds=("character", "location"))
|
||||||
|
):
|
||||||
if slot_number in seen:
|
if slot_number in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(slot_number)
|
seen.add(slot_number)
|
||||||
@@ -4456,8 +4478,9 @@ def _inject_reference_context(block, context):
|
|||||||
def _reference_character_memory(ref_slots):
|
def _reference_character_memory(ref_slots):
|
||||||
lines = []
|
lines = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for slot_number, raw in enumerate(ref_slots or [], 1):
|
for slot_number, ref in enumerate(_normalized_ref_slots(ref_slots), 1):
|
||||||
ref = _reference_slot(raw, slot_number)
|
if ref is None:
|
||||||
|
continue
|
||||||
if ref.get("kind") != "character":
|
if ref.get("kind") != "character":
|
||||||
continue
|
continue
|
||||||
wardrobe = _reference_text(ref.get("wardrobe"))
|
wardrobe = _reference_text(ref.get("wardrobe"))
|
||||||
@@ -4478,7 +4501,7 @@ def _reference_character_memory(ref_slots):
|
|||||||
|
|
||||||
def _connected_refs(ref_slots):
|
def _connected_refs(ref_slots):
|
||||||
"""Connected refs only, preserving slot order and skipping empty sockets."""
|
"""Connected refs only, preserving slot order and skipping empty sockets."""
|
||||||
return [ref for ref in (ref_slots or []) if _reference_image(ref) is not None]
|
return [ref for ref in _normalized_ref_slots(ref_slots) if _reference_image(ref) is not None]
|
||||||
|
|
||||||
|
|
||||||
def resolve_tagged_refs(text, ref_list):
|
def resolve_tagged_refs(text, ref_list):
|
||||||
@@ -4493,7 +4516,8 @@ def resolve_tagged_refs(text, ref_list):
|
|||||||
A tag naming a slot with no image connected refers to nothing at all, so it is
|
A tag naming a slot with no image connected refers to nothing at all, so it is
|
||||||
removed from the text rather than left to confuse the encoder, and reported."""
|
removed from the text rather than left to confuse the encoder, and reported."""
|
||||||
wanted = picture_tags(text)
|
wanted = picture_tags(text)
|
||||||
live = [n for n in wanted if 1 <= n <= len(ref_list or []) and ref_list[n - 1] is not None]
|
normalized_refs = _normalized_ref_slots(ref_list)
|
||||||
|
live = [n for n in wanted if 1 <= n <= len(normalized_refs) and _reference_image(normalized_refs[n - 1]) is not None]
|
||||||
dropped = [n for n in wanted if n not in live]
|
dropped = [n for n in wanted if n not in live]
|
||||||
renumber = {old: new for new, old in enumerate(live, 1)}
|
renumber = {old: new for new, old in enumerate(live, 1)}
|
||||||
|
|
||||||
@@ -4506,7 +4530,7 @@ def resolve_tagged_refs(text, ref_list):
|
|||||||
out = re.sub(r"\s+([,.;:])", r"\1", out)
|
out = re.sub(r"\s+([,.;:])", r"\1", out)
|
||||||
out = re.sub(r"(,\s*){2,}", ", ", out)
|
out = re.sub(r"(,\s*){2,}", ", ", 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
|
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):
|
||||||
@@ -4517,10 +4541,11 @@ def resolve_prompt_refs(text, ref_list):
|
|||||||
that split, a shot could inherit the facts/context for "Mara" and "Jon" while
|
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
|
only carrying a tagged location image, which reads exactly like the names were
|
||||||
understood but the faces were ignored."""
|
understood but the faces were ignored."""
|
||||||
rewritten, tagged_refs, dropped = resolve_tagged_refs(text, ref_list)
|
normalized_refs = _normalized_ref_slots(ref_list)
|
||||||
|
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}
|
||||||
for _slot_number, ref in _named_character_refs_for_text(rewritten, ref_list):
|
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:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -509,6 +509,19 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
||||||
self.assertIn("Character notes for Mara: wears a long grey coat.", context)
|
self.assertIn("Character notes for Mara: wears a long grey coat.", context)
|
||||||
|
|
||||||
|
def test_reference_context_matches_location_names_without_picture_tag(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar", "aliases": ["loading bay"], "description": "wet concrete floor", "general": "cold industrial lighting"},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = self.module._reference_context_for_text(
|
||||||
|
"[Generation 1] They argue in the hangar near the loading bay.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("Location context for Hangar: wet concrete floor.", context)
|
||||||
|
self.assertIn("Location notes for Hangar: cold industrial lighting.", 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
|
||||||
@@ -634,6 +647,37 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
self.assertIn("Persistent appearance for Mara: silver hair.", context)
|
self.assertIn("Persistent appearance for Mara: silver hair.", context)
|
||||||
self.assertNotIn("Persistent wardrobe/style for Mara: red jacket.", 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"},
|
||||||
|
{"kind": "character", "image": "img2", "name": "Jon"},
|
||||||
|
]
|
||||||
|
|
||||||
|
rewritten, matched, dropped = self.module.resolve_tagged_refs(
|
||||||
|
"[Generation 1] <Picture 1> faces <Picture 2>.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rewritten, "[Generation 1] faces <Picture 1>.")
|
||||||
|
self.assertEqual(dropped, [1])
|
||||||
|
self.assertEqual(len(matched), 1)
|
||||||
|
self.assertEqual(matched[0]["name"], "Jon")
|
||||||
|
|
||||||
def test_reference_context_injects_immediately_after_generation_label(self):
|
def test_reference_context_injects_immediately_after_generation_label(self):
|
||||||
block = (
|
block = (
|
||||||
"[Generation 1] Classic sitcom lighting and staging. "
|
"[Generation 1] Classic sitcom lighting and staging. "
|
||||||
|
|||||||
Reference in New Issue
Block a user