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`.
|
- 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)`
|
- `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`
|
- 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.
|
- 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.
|
- 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`
|
- `Dumas H3 Shot Length`
|
||||||
- Inputs: `shot_seconds`, `fps`, optional `cap_to_h3_max`
|
- 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",
|
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
|
||||||
"motion_guard", "contact_guard",
|
"motion_guard", "contact_guard",
|
||||||
"auto_soundscape", "allow_nonspeech_vocals",
|
"auto_soundscape", "allow_nonspeech_vocals",
|
||||||
|
"ref_image_5", "ref_image_6", "ref_image_7", "ref_image_8", "ref_image_9",
|
||||||
)
|
)
|
||||||
|
|
||||||
NL = "\n"
|
NL = "\n"
|
||||||
@@ -3997,6 +3998,11 @@ def picture_tags(text):
|
|||||||
return sorted({int(m.group(1)) for m in _PICTURE_TAG.finditer(text or "")})
|
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):
|
def resolve_tagged_refs(text, ref_list):
|
||||||
"""(rewritten text, images, dropped) for the <Picture N> tags in ONE shot.
|
"""(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
|
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 [])]
|
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]
|
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)}
|
||||||
|
|
||||||
@@ -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
|
back as a soft signal (the model is shown where the last
|
||||||
shot ended rather than told to start exactly there), and it
|
shot ended rather than told to start exactly there), and it
|
||||||
stays a single ref2va task, so nothing conflicts."""
|
stays a single ref2va task, so nothing conflicts."""
|
||||||
if not ref_list:
|
refs = _connected_refs(ref_list)
|
||||||
|
if not refs:
|
||||||
return []
|
return []
|
||||||
if ref_mode == "first shot":
|
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":
|
if ref_mode == "every shot":
|
||||||
return list(ref_list)
|
return list(refs)
|
||||||
if ref_mode == "every shot + handoff ref":
|
if ref_mode == "every shot + handoff ref":
|
||||||
return list(ref_list) + ([handoff] if handoff is not None else [])
|
return list(refs) + ([handoff] if handoff is not None else [])
|
||||||
return list(ref_list) if shot_index == 0 else [] # unknown value -> safest
|
return list(refs) if shot_index == 0 else [] # unknown value -> safest
|
||||||
|
|
||||||
|
|
||||||
# --- text-encoder / DiT compatibility -------------------------------------
|
# --- text-encoder / DiT compatibility -------------------------------------
|
||||||
@@ -5239,11 +5246,15 @@ class H3LongVideos:
|
|||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"first_frame": ("IMAGE",),
|
"first_frame": ("IMAGE",),
|
||||||
# ref2va inputs. Order matters and is the ONLY thing that decides the
|
# ref2va inputs. SOCKET NUMBER matters: prompt tags refer to these
|
||||||
# roster: the tokenizer labels these <Picture 1>..<Picture 4> in the
|
# exact slots, even when some intermediate sockets are left empty.
|
||||||
# order they appear here, then appends the prompt. Refer to them by
|
# A shot using only ref_image_7 is still tagged as <Picture 7> in the
|
||||||
# those tags in the prompt if you want a reference bound to a named
|
# prompt and renumbered only for the per-shot tokenizer payload.
|
||||||
# character ("Kristy, <Picture 1>, walks in").
|
# 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 "
|
"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> "
|
"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 "
|
"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_2": ("IMAGE", {"tooltip": "Reference image <Picture 2>."}),
|
||||||
"ref_image_3": ("IMAGE", {"tooltip": "Reference image <Picture 3>."}),
|
"ref_image_3": ("IMAGE", {"tooltip": "Reference image <Picture 3>."}),
|
||||||
"ref_image_4": ("IMAGE", {"tooltip": "Reference image <Picture 4>."}),
|
"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,
|
"plan_only": ("BOOLEAN", {"default": False,
|
||||||
"tooltip": "Preview the shot split WITHOUT rendering. Uses THIS node's own settings (no "
|
"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 "
|
"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_text="", intro_position="center", intro_seconds=3.0, intro_fade=0.6,
|
||||||
intro_size=9.0, overlay_font="arial.ttf", overlay_stroke=0,
|
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_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,
|
ref_mode="where tagged", ref_image_size="match", ref_noise_aug=0.999,
|
||||||
graph=None, node_id=None):
|
graph=None, node_id=None):
|
||||||
|
|
||||||
@@ -6024,8 +6042,11 @@ class H3LongVideos:
|
|||||||
else "prompt/soundscape silencing only"))
|
else "prompt/soundscape silencing only"))
|
||||||
# Same reference accounting the render reports: which shots lose the
|
# Same reference accounting the render reports: which shots lose the
|
||||||
# handoff is a composition decision, so it belongs in the preview.
|
# 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)
|
ref_slots = [
|
||||||
if r is not None])
|
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 = ""
|
plan_ref = ""
|
||||||
if n_refs:
|
if n_refs:
|
||||||
# Mirror the render's placement exactly: 'where tagged' reads the
|
# 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
|
# reporting by ref_mode alone described shots the render never gave
|
||||||
# references to.
|
# references to.
|
||||||
if ref_mode == "where tagged" and any(picture_tags(g) for g in gens):
|
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"
|
how = "placed by <Picture N> tags"
|
||||||
else:
|
else:
|
||||||
mode_eff = "first shot" if ref_mode == "where tagged" else ref_mode
|
mode_eff = "first shot" if ref_mode == "where tagged" else ref_mode
|
||||||
on = [n + 1 for n in range(shots)
|
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}'"
|
how = (f"ref_mode '{mode_eff}'"
|
||||||
+ (" -- no tags found anywhere" if ref_mode == "where tagged" else ""))
|
+ (" -- 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) "
|
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
|
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
|
||||||
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_shots = [] # which shots ended up ref-conditioned
|
||||||
ref_missing = [] # <Picture N> tags naming an unconnected slot
|
ref_missing = [] # <Picture N> tags naming an unconnected slot
|
||||||
ref_carried = [] # tagged shots that kept continuity as an extra ref
|
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
|
# 'where tagged' reads the prompt instead of counting shots. If references are
|
||||||
# connected but nothing is tagged anywhere, fall back to first-shot placement
|
# connected but nothing is tagged anywhere, fall back to first-shot placement
|
||||||
# rather than silently conditioning nothing at all.
|
# 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)
|
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"
|
ref_mode = "first shot"
|
||||||
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
|
||||||
@@ -6384,10 +6409,10 @@ class H3LongVideos:
|
|||||||
f"dropped from the text")
|
f"dropped from the text")
|
||||||
else:
|
else:
|
||||||
ref_note_missing = ""
|
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]
|
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_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"{','.join(str(n) for n in ref_shots)} "
|
||||||
f"({ref_placement})"
|
f"({ref_placement})"
|
||||||
+ (f", ref_noise_aug {ref_noise_aug:.3f}" if ref_noise_aug is not None
|
+ (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)
|
+ ("" if (ref_keyframed or ref_carried or kept)
|
||||||
else ", so every cut between beats is a CUT, not a continuous take")
|
else ", so every cut between beats is a CUT, not a continuous take")
|
||||||
+ ref_note_missing)
|
+ ref_note_missing)
|
||||||
elif ref_list:
|
elif connected_ref_count:
|
||||||
ref_note = (f" ref2va: {len(ref_list)} reference image(s) connected but ref_mode "
|
ref_note = (f" ref2va: {connected_ref_count} reference image(s) connected but ref_mode "
|
||||||
f"'{ref_mode}' applied them to no shot")
|
f"'{ref_mode}' applied them to no shot")
|
||||||
else:
|
else:
|
||||||
ref_note = ""
|
ref_note = ""
|
||||||
|
|||||||
@@ -132,6 +132,51 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
fn(spoken)
|
fn(spoken)
|
||||||
self.assertGreaterEqual(fn.cache_info().hits, 2)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user