Fix named character ref routing

This commit is contained in:
2026-08-29 10:51:01 +00:00
parent 08365c7df5
commit 0bac83a689
2 changed files with 91 additions and 43 deletions
+70 -43
View File
@@ -4631,6 +4631,42 @@ def resolve_prompt_refs(text, ref_list, include_named=True, normalized_refs=None
seen.add(marker) seen.add(marker)
refs.append(ref) refs.append(ref)
return rewritten, refs, dropped return rewritten, refs, dropped
def resolve_shot_references(text, ref_list, ref_mode, shot_index, handoff, connected_refs=None, normalized_refs=None):
"""(rewritten text, refs, dropped, shot_tag_driven, effective_mode) for one beat.
Each beat should be able to pick up character names on its own. A stray
<Picture N> tag in some other beat should not make name-matched characters
disappear from the untagged beats in the same chain."""
refs = connected_refs if connected_refs is not None else _connected_refs(ref_list, normalized_refs=normalized_refs)
if not refs:
return text, [], [], False, ref_mode
normalized_refs = normalized_refs if normalized_refs is not None else _normalized_ref_slots(ref_list)
shot_tags = picture_tags(text)
if ref_mode == "auto ref2v":
rewritten, shot_refs, dropped = resolve_prompt_refs(
text,
ref_list,
include_named=True,
normalized_refs=normalized_refs,
)
if shot_tags:
return rewritten, shot_refs, dropped, True, "auto ref2v"
if shot_refs:
return rewritten, shot_refs, dropped, False, "auto ref2v"
return text, shot_references(ref_list, "every shot", shot_index, handoff, connected_refs=refs), [], False, "every shot"
if ref_mode == "where tagged":
if shot_tags:
rewritten, shot_refs, dropped = resolve_prompt_refs(
text,
ref_list,
include_named=False,
normalized_refs=normalized_refs,
)
return rewritten, shot_refs, dropped, True, "where tagged"
return text, shot_references(ref_list, "first shot", shot_index, handoff, connected_refs=refs), [], False, "first shot"
return text, shot_references(ref_list, ref_mode, shot_index, handoff, connected_refs=refs), [], False, ref_mode
def shot_references(ref_list, ref_mode, shot_index, handoff, connected_refs=None): def shot_references(ref_list, ref_mode, shot_index, handoff, connected_refs=None):
@@ -6798,21 +6834,20 @@ class H3LongVideos:
effective_modes = [] effective_modes = []
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: _rewritten, refs, _dropped, shot_tag_driven, mode_eff = resolve_shot_references(
if resolve_prompt_refs( gen,
gen, ref_slots,
ref_slots, shot_mode,
include_named=(shot_mode == "auto ref2v"), shot_index,
normalized_refs=normalized_ref_slots, 1 if shot_index else None,
)[1]: connected_refs=connected_refs,
on.append(shot_index + 1) normalized_refs=normalized_ref_slots,
tagged_used = True )
else: effective_modes.append(mode_eff)
mode_eff = ("every shot" if shot_mode == "auto ref2v" if shot_tag_driven:
else "first shot" if shot_mode == "where tagged" else shot_mode) tagged_used = tagged_used or bool(refs)
effective_modes.append(mode_eff) if refs:
if shot_references(ref_slots, mode_eff, shot_index, 1 if shot_index else None): on.append(shot_index + 1)
on.append(shot_index + 1)
if tagged_used: if tagged_used:
how = "placed by <Picture N> tags" how = "placed by <Picture N> tags"
else: else:
@@ -6855,11 +6890,11 @@ class H3LongVideos:
sum(plan_lens), shots, total, float(fps), int(fps), sum(plan_lens), shots, total, float(fps), int(fps),
_empty_av_latent(w, h, 5, fps)[0], global_soundscape, [], []) _empty_av_latent(w, h, 5, fps)[0], global_soundscape, [], [])
spk = speech_flags(beats) # which shots have real (quoted) dialogue spk = speech_flags(beats) # which shots have real (quoted) dialogue
vram_trace = [] # free VRAM after each shot vram_trace = [] # free VRAM after each shot
muted_flags = [] # which shots were audio-silenced muted_flags = [] # which shots were audio-silenced
hoff = max(0, int(handoff_offset)) hoff = max(0, int(handoff_offset))
backoff, video_chunks, audio_chunks = [], [], [] backoff, video_chunks, audio_chunks = [], [], []
latent_chunks = [] # per-shot sampled latents, pre-decode latent_chunks = [] # per-shot sampled latents, pre-decode
mouth_settled = [] # shots seeded from a settled (closed) mouth mouth_settled = [] # shots seeded from a settled (closed) mouth
handoff, sr = first_frame, None handoff, sr = first_frame, None
@@ -6871,6 +6906,7 @@ class H3LongVideos:
ref_mode_used = [] ref_mode_used = []
continuity_used = [] continuity_used = []
ref_aug_used = [] ref_aug_used = []
any_explicit_picture_tags = False
shot_timings = [] shot_timings = []
if cleanup_between_shots: if cleanup_between_shots:
_deep_cleanup() # start the first (heaviest) shot with max free VRAM _deep_cleanup() # start the first (heaviest) shot with max free VRAM
@@ -6888,34 +6924,25 @@ class H3LongVideos:
shot_continuity = beat_continuity_directive(beat_text) or "auto" shot_continuity = beat_continuity_directive(beat_text) or "auto"
shot_ref_noise_aug = beat_ref_noise_aug_directive(beat_text) shot_ref_noise_aug = beat_ref_noise_aug_directive(beat_text)
shot_aug = ref_noise_aug if shot_ref_noise_aug is None else shot_ref_noise_aug shot_aug = ref_noise_aug if shot_ref_noise_aug is None else shot_ref_noise_aug
shot_tag_driven = bool(connected_ref_count) and shot_mode in ("where tagged", "auto ref2v") and any_tags_anywhere gen_prompt, shot_refs, dropped, shot_tag_driven, shot_mode_eff = resolve_shot_references(
if shot_tag_driven: gen_prompt,
shot_mode_eff = shot_mode ref_slots,
else: shot_mode,
shot_mode_eff = ("every shot" if shot_mode == "auto ref2v" i,
else "first shot" if shot_mode == "where tagged" else shot_mode) handoff,
connected_refs=connected_refs,
normalized_refs=normalized_ref_slots,
)
carry_keyframe = False # tagged shot keeps its handoff as a keyframe carry_keyframe = False # tagged shot keeps its handoff as a keyframe
if shot_tag_driven: if shot_tag_driven:
any_explicit_picture_tags = True
# 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. Named character matches are already included
gen_prompt, shot_refs, dropped = resolve_prompt_refs( # in the same pass; the tag only makes the slot mapping explicit.
gen_prompt,
ref_slots,
include_named=(shot_mode == "auto ref2v"),
normalized_refs=normalized_ref_slots,
)
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)
else:
shot_refs = shot_references(
ref_slots,
shot_mode_eff,
i,
handoff,
connected_refs=connected_refs,
)
# A shot that follows a strip starts FRESH. Continuing from a frame that # A shot that follows a strip starts FRESH. Continuing from a frame that
# still shows the garment is how it reappears -- the picture outvotes the # still shows the garment is how it reappears -- the picture outvotes the
# text every time. Costs a cut exactly where the state changes, which is # text every time. Costs a cut exactly where the state changes, which is
@@ -6975,7 +7002,7 @@ class H3LongVideos:
"soft carry" if shot_handoff is not None else "hard cut") "soft carry" if shot_handoff is not None else "hard cut")
if carry_keyframe and (i + 1) not in ref_keyframed and handoff is not None: if carry_keyframe and (i + 1) not in ref_keyframed and handoff is not None:
ref_keyframed.append(i + 1) ref_keyframed.append(i + 1)
ref_mode_used.append(shot_mode if shot_tag_driven else shot_mode_eff) ref_mode_used.append(shot_mode_eff)
continuity_used.append(continuity_label) continuity_used.append(continuity_label)
ref_aug_used.append(shot_aug) ref_aug_used.append(shot_aug)
if shot_refs: if shot_refs:
@@ -7248,7 +7275,7 @@ class H3LongVideos:
if connected_ref_count and ref_shots: if connected_ref_count and ref_shots:
kept = [n for n in range(1, len(gens) + 1) if n not in ref_shots] kept = [n for n in range(1, len(gens) + 1) if n not in ref_shots]
distinct_ref_modes = list(dict.fromkeys(ref_mode_used)) distinct_ref_modes = list(dict.fromkeys(ref_mode_used))
tagged_used = any(mode in ("where tagged", "auto ref2v") for mode in ref_mode_used) and any_tags_anywhere tagged_used = any_explicit_picture_tags
ref_placement = ("placed by <Picture N> tags" if tagged_used else ref_placement = ("placed by <Picture N> tags" if tagged_used else
f"ref_mode '{distinct_ref_modes[0]}'" if len(distinct_ref_modes) == 1 else f"ref_mode '{distinct_ref_modes[0]}'" if len(distinct_ref_modes) == 1 else
"mixed per-shot ref_mode") "mixed per-shot ref_mode")
+21
View File
@@ -458,6 +458,27 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
self.assertEqual([self.module._reference_image(ref) for ref in references], ["img2"]) self.assertEqual([self.module._reference_image(ref) for ref in references], ["img2"])
self.assertEqual(dropped, []) self.assertEqual(dropped, [])
def test_resolve_shot_references_uses_named_characters_without_picture_tags(self):
refs = [
{"kind": "character", "image": "img1", "name": "Mara"},
{"kind": "character", "image": "img2", "name": "Jon"},
{"kind": "location", "image": "img3", "name": "Hangar"},
]
text, references, dropped, shot_tag_driven, mode_eff = self.module.resolve_shot_references(
"[Generation 1] Mara crosses the hangar.",
refs,
"auto ref2v",
0,
None,
)
self.assertEqual(text, "[Generation 1] Mara crosses the hangar.")
self.assertEqual([self.module._reference_image(ref) for ref in references], ["img1"])
self.assertEqual(dropped, [])
self.assertFalse(shot_tag_driven)
self.assertEqual(mode_eff, "auto ref2v")
def test_shot_references_uses_all_connected_sparse_slots(self): def test_shot_references_uses_all_connected_sparse_slots(self):
refs = [ refs = [
None, None,