Expand H3 ref2v slots and tag routing
This commit is contained in:
@@ -34,10 +34,11 @@
|
||||
- Reads back the nine optional images for a selected MiniMax H3 plan scene, for example by connecting the current `clip_index`.
|
||||
|
||||
- `Dumas H3 Long Videos (FL2VA + REF2VA)`
|
||||
- Inputs: H3 model stack, prompt socket, optional `first_frame`, optional `ref_image_1`..`ref_image_4`, plus the upstream long-video control surface for pacing, continuity, audio, overlays, and guards
|
||||
- Inputs: H3 model stack, prompt socket, optional `first_frame`, optional `ref_image_1`..`ref_image_9`, plus the upstream long-video control surface for pacing, continuity, audio, overlays, and guards
|
||||
- Outputs: `images`, `audio`, `info`, `script`, `frames_per_shot`, `total_frames`, `shots`, `video_seconds`, `fps`, `fps_int`, `latent`, `soundscape`
|
||||
- First-pass Dumas port of the `MiniMax-H3-Longvideos` sampler, brought in as a local starting point for long-form H3 chaining work.
|
||||
- Keeps the upstream split-beats / handoff / ref-routing behavior close to source so future Dumas-specific improvements can be compared against a known baseline.
|
||||
- Prompt `<Picture N>` tags now map to the actual ref socket numbers you wire, even with gaps such as only `ref_image_2` and `ref_image_7` connected.
|
||||
|
||||
- `Dumas H3 Shot Length`
|
||||
- Inputs: `shot_seconds`, `fps`, optional `cap_to_h3_max`
|
||||
|
||||
+47
-22
@@ -264,6 +264,7 @@ ADDED_WIDGETS = (
|
||||
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
|
||||
"motion_guard", "contact_guard",
|
||||
"auto_soundscape", "allow_nonspeech_vocals",
|
||||
"ref_image_5", "ref_image_6", "ref_image_7", "ref_image_8", "ref_image_9",
|
||||
)
|
||||
|
||||
NL = "\n"
|
||||
@@ -3997,6 +3998,11 @@ def picture_tags(text):
|
||||
return sorted({int(m.group(1)) for m in _PICTURE_TAG.finditer(text or "")})
|
||||
|
||||
|
||||
def _connected_refs(ref_slots):
|
||||
"""Connected refs only, preserving slot order and skipping empty sockets."""
|
||||
return [ref for ref in (ref_slots or []) if ref is not None]
|
||||
|
||||
|
||||
def resolve_tagged_refs(text, ref_list):
|
||||
"""(rewritten text, images, dropped) for the <Picture N> tags in ONE shot.
|
||||
|
||||
@@ -4009,7 +4015,7 @@ def resolve_tagged_refs(text, ref_list):
|
||||
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."""
|
||||
wanted = picture_tags(text)
|
||||
live = [n for n in wanted if 1 <= n <= len(ref_list or [])]
|
||||
live = [n for n in wanted if 1 <= n <= len(ref_list or []) and ref_list[n - 1] is not None]
|
||||
dropped = [n for n in wanted if n not in live]
|
||||
renumber = {old: new for new, old in enumerate(live, 1)}
|
||||
|
||||
@@ -4047,15 +4053,16 @@ def shot_references(ref_list, ref_mode, shot_index, handoff):
|
||||
back as a soft signal (the model is shown where the last
|
||||
shot ended rather than told to start exactly there), and it
|
||||
stays a single ref2va task, so nothing conflicts."""
|
||||
if not ref_list:
|
||||
refs = _connected_refs(ref_list)
|
||||
if not refs:
|
||||
return []
|
||||
if ref_mode == "first shot":
|
||||
return list(ref_list) if shot_index == 0 else []
|
||||
return list(refs) if shot_index == 0 else []
|
||||
if ref_mode == "every shot":
|
||||
return list(ref_list)
|
||||
return list(refs)
|
||||
if ref_mode == "every shot + handoff ref":
|
||||
return list(ref_list) + ([handoff] if handoff is not None else [])
|
||||
return list(ref_list) if shot_index == 0 else [] # unknown value -> safest
|
||||
return list(refs) + ([handoff] if handoff is not None else [])
|
||||
return list(refs) if shot_index == 0 else [] # unknown value -> safest
|
||||
|
||||
|
||||
# --- text-encoder / DiT compatibility -------------------------------------
|
||||
@@ -5239,11 +5246,15 @@ class H3LongVideos:
|
||||
},
|
||||
"optional": {
|
||||
"first_frame": ("IMAGE",),
|
||||
# ref2va inputs. Order matters and is the ONLY thing that decides the
|
||||
# roster: the tokenizer labels these <Picture 1>..<Picture 4> in the
|
||||
# order they appear here, then appends the prompt. Refer to them by
|
||||
# those tags in the prompt if you want a reference bound to a named
|
||||
# character ("Kristy, <Picture 1>, walks in").
|
||||
# ref2va inputs. SOCKET NUMBER matters: prompt tags refer to these
|
||||
# exact slots, even when some intermediate sockets are left empty.
|
||||
# A shot using only ref_image_7 is still tagged as <Picture 7> in the
|
||||
# prompt and renumbered only for the per-shot tokenizer payload.
|
||||
# When a mode uses all connected refs, they keep socket order.
|
||||
# The tokenizer labels the carried refs <Picture 1>..<Picture N> in
|
||||
# the order they are handed that shot.
|
||||
# Refer to socket tags in the prompt if you want a reference bound to
|
||||
# a named character ("Kristy, <Picture 7>, walks in").
|
||||
"ref_image_1": ("IMAGE", {"tooltip": "Reference image <Picture 1> -- identity/appearance "
|
||||
"carried into the shots. Which shots receive it is set by ref_mode (or <Picture N> "
|
||||
"tags in the beats); a referenced shot ALSO carries the previous frame as its "
|
||||
@@ -5251,6 +5262,11 @@ class H3LongVideos:
|
||||
"ref_image_2": ("IMAGE", {"tooltip": "Reference image <Picture 2>."}),
|
||||
"ref_image_3": ("IMAGE", {"tooltip": "Reference image <Picture 3>."}),
|
||||
"ref_image_4": ("IMAGE", {"tooltip": "Reference image <Picture 4>."}),
|
||||
"ref_image_5": ("IMAGE", {"tooltip": "Reference image <Picture 5>."}),
|
||||
"ref_image_6": ("IMAGE", {"tooltip": "Reference image <Picture 6>."}),
|
||||
"ref_image_7": ("IMAGE", {"tooltip": "Reference image <Picture 7>."}),
|
||||
"ref_image_8": ("IMAGE", {"tooltip": "Reference image <Picture 8>."}),
|
||||
"ref_image_9": ("IMAGE", {"tooltip": "Reference image <Picture 9>."}),
|
||||
"plan_only": ("BOOLEAN", {"default": False,
|
||||
"tooltip": "Preview the shot split WITHOUT rendering. Uses THIS node's own settings (no "
|
||||
"second node, no duplicate entry): returns the plan in 'info' and the "
|
||||
@@ -5746,6 +5762,8 @@ class H3LongVideos:
|
||||
intro_text="", intro_position="center", intro_seconds=3.0, intro_fade=0.6,
|
||||
intro_size=9.0, overlay_font="arial.ttf", overlay_stroke=0,
|
||||
ref_image_1=None, ref_image_2=None, ref_image_3=None, ref_image_4=None,
|
||||
ref_image_5=None, ref_image_6=None, ref_image_7=None, ref_image_8=None,
|
||||
ref_image_9=None,
|
||||
ref_mode="where tagged", ref_image_size="match", ref_noise_aug=0.999,
|
||||
graph=None, node_id=None):
|
||||
|
||||
@@ -6024,8 +6042,11 @@ class H3LongVideos:
|
||||
else "prompt/soundscape silencing only"))
|
||||
# Same reference accounting the render reports: which shots lose the
|
||||
# handoff is a composition decision, so it belongs in the preview.
|
||||
n_refs = len([r for r in (ref_image_1, ref_image_2, ref_image_3, ref_image_4)
|
||||
if r is not None])
|
||||
ref_slots = [
|
||||
ref_image_1, ref_image_2, ref_image_3, ref_image_4, ref_image_5,
|
||||
ref_image_6, ref_image_7, ref_image_8, ref_image_9,
|
||||
]
|
||||
n_refs = len(_connected_refs(ref_slots))
|
||||
plan_ref = ""
|
||||
if n_refs:
|
||||
# Mirror the render's placement exactly: 'where tagged' reads the
|
||||
@@ -6033,12 +6054,12 @@ class H3LongVideos:
|
||||
# reporting by ref_mode alone described shots the render never gave
|
||||
# references to.
|
||||
if ref_mode == "where tagged" and any(picture_tags(g) for g in gens):
|
||||
on = [n + 1 for n, g in enumerate(gens) if picture_tags(g)]
|
||||
on = [n + 1 for n, g in enumerate(gens) if resolve_tagged_refs(g, ref_slots)[1]]
|
||||
how = "placed by <Picture N> tags"
|
||||
else:
|
||||
mode_eff = "first shot" if ref_mode == "where tagged" else ref_mode
|
||||
on = [n + 1 for n in range(shots)
|
||||
if shot_references([1] * n_refs, mode_eff, n, 1 if n else None)]
|
||||
if shot_references(ref_slots, mode_eff, n, 1 if n else None)]
|
||||
how = (f"ref_mode '{mode_eff}'"
|
||||
+ (" -- no tags found anywhere" if ref_mode == "where tagged" else ""))
|
||||
plan_ref = (f" ref2va: {n_refs} reference image(s) at '{ref_image_size}' on shot(s) "
|
||||
@@ -6076,7 +6097,11 @@ class H3LongVideos:
|
||||
latent_chunks = [] # per-shot sampled latents, pre-decode
|
||||
mouth_settled = [] # shots seeded from a settled (closed) mouth
|
||||
handoff, sr = first_frame, None
|
||||
ref_list = [r for r in (ref_image_1, ref_image_2, ref_image_3, ref_image_4) if r is not None]
|
||||
ref_list = [
|
||||
ref_image_1, ref_image_2, ref_image_3, ref_image_4, ref_image_5,
|
||||
ref_image_6, ref_image_7, ref_image_8, ref_image_9,
|
||||
]
|
||||
connected_ref_count = len(_connected_refs(ref_list))
|
||||
ref_shots = [] # which shots ended up ref-conditioned
|
||||
ref_missing = [] # <Picture N> tags naming an unconnected slot
|
||||
ref_carried = [] # tagged shots that kept continuity as an extra ref
|
||||
@@ -6084,9 +6109,9 @@ class H3LongVideos:
|
||||
# 'where tagged' reads the prompt instead of counting shots. If references are
|
||||
# connected but nothing is tagged anywhere, fall back to first-shot placement
|
||||
# rather than silently conditioning nothing at all.
|
||||
tag_driven = bool(ref_list) and ref_mode == "where tagged" and any(
|
||||
tag_driven = bool(connected_ref_count) and ref_mode == "where tagged" and any(
|
||||
picture_tags(g) for g in gens)
|
||||
if ref_list and ref_mode == "where tagged" and not tag_driven:
|
||||
if connected_ref_count and ref_mode == "where tagged" and not tag_driven:
|
||||
ref_mode = "first shot"
|
||||
if cleanup_between_shots:
|
||||
_deep_cleanup() # start the first (heaviest) shot with max free VRAM
|
||||
@@ -6384,10 +6409,10 @@ class H3LongVideos:
|
||||
f"dropped from the text")
|
||||
else:
|
||||
ref_note_missing = ""
|
||||
if ref_list 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]
|
||||
ref_placement = "placed by <Picture N> tags" if tag_driven else f"ref_mode '{ref_mode}'"
|
||||
ref_note = (f" ref2va: {len(ref_list)} reference image(s) at '{ref_image_size}' on shot(s) "
|
||||
ref_note = (f" ref2va: {connected_ref_count} reference image(s) at '{ref_image_size}' on shot(s) "
|
||||
f"{','.join(str(n) for n in ref_shots)} "
|
||||
f"({ref_placement})"
|
||||
+ (f", ref_noise_aug {ref_noise_aug:.3f}" if ref_noise_aug is not None
|
||||
@@ -6404,8 +6429,8 @@ class H3LongVideos:
|
||||
+ ("" if (ref_keyframed or ref_carried or kept)
|
||||
else ", so every cut between beats is a CUT, not a continuous take")
|
||||
+ ref_note_missing)
|
||||
elif ref_list:
|
||||
ref_note = (f" ref2va: {len(ref_list)} reference image(s) connected but ref_mode "
|
||||
elif connected_ref_count:
|
||||
ref_note = (f" ref2va: {connected_ref_count} reference image(s) connected but ref_mode "
|
||||
f"'{ref_mode}' applied them to no shot")
|
||||
else:
|
||||
ref_note = ""
|
||||
|
||||
@@ -132,6 +132,51 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
||||
fn(spoken)
|
||||
self.assertGreaterEqual(fn.cache_info().hits, 2)
|
||||
|
||||
def test_resolve_tagged_refs_preserves_sparse_socket_numbers(self):
|
||||
refs = [None, "img2", None, None, None, None, "img7", None, "img9"]
|
||||
|
||||
text, images, dropped = self.module.resolve_tagged_refs(
|
||||
"Mara <Picture 7> turns toward Jon <Picture 2> while <Picture 9> watches.",
|
||||
refs,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
text,
|
||||
"Mara <Picture 2> turns toward Jon <Picture 1> while <Picture 3> watches.",
|
||||
)
|
||||
self.assertEqual(images, ["img2", "img7", "img9"])
|
||||
self.assertEqual(dropped, [])
|
||||
|
||||
def test_resolve_tagged_refs_drops_unconnected_sparse_slots(self):
|
||||
refs = [None, "img2", None, None, None, None, "img7", None, None]
|
||||
|
||||
text, images, dropped = self.module.resolve_tagged_refs(
|
||||
"Use <Picture 7>, skip <Picture 4>, keep <Picture 2>.",
|
||||
refs,
|
||||
)
|
||||
|
||||
self.assertEqual(text, "Use <Picture 2>, skip, keep <Picture 1>.")
|
||||
self.assertEqual(images, ["img2", "img7"])
|
||||
self.assertEqual(dropped, [4])
|
||||
|
||||
def test_shot_references_uses_all_connected_sparse_slots(self):
|
||||
refs = [None, "img2", None, "img4", None, None, "img7", None, None]
|
||||
|
||||
self.assertEqual(
|
||||
self.module.shot_references(refs, "first shot", 0, None),
|
||||
["img2", "img4", "img7"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.module.shot_references(refs, "every shot", 3, None),
|
||||
["img2", "img4", "img7"],
|
||||
)
|
||||
|
||||
def test_input_types_expose_nine_ref_slots(self):
|
||||
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
|
||||
|
||||
for index in range(1, 10):
|
||||
self.assertIn(f"ref_image_{index}", optional)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user