Wire H3 long videos to plan scene refs
This commit is contained in:
+100
-32
@@ -54,17 +54,28 @@ import comfy.nested_tensor
|
||||
import comfy.model_management as mm
|
||||
import node_helpers
|
||||
|
||||
try:
|
||||
try:
|
||||
from . import dumas_h3_overlay as _overlay
|
||||
from . import dumas_image_nodes as _image_nodes
|
||||
except ImportError: # loaded as a bare file (test_prompt_logic.py), not as a package
|
||||
import importlib.util as _ilu
|
||||
import os as _os
|
||||
import sys as _sys
|
||||
_spec = _ilu.spec_from_file_location(
|
||||
"dumas_h3_overlay",
|
||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_h3_overlay.py"),
|
||||
)
|
||||
_overlay = _ilu.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_overlay)
|
||||
_overlay = _ilu.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_overlay)
|
||||
_image_nodes = _sys.modules.get("dumas_image_nodes")
|
||||
if _image_nodes is None:
|
||||
_img_spec = _ilu.spec_from_file_location(
|
||||
"dumas_image_nodes",
|
||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_image_nodes.py"),
|
||||
)
|
||||
_image_nodes = _ilu.module_from_spec(_img_spec)
|
||||
_sys.modules["dumas_image_nodes"] = _image_nodes
|
||||
_img_spec.loader.exec_module(_image_nodes)
|
||||
|
||||
AUDIO_LATENT_FPS = 40
|
||||
GB = 1024 ** 3
|
||||
@@ -265,6 +276,7 @@ ADDED_WIDGETS = (
|
||||
"motion_guard", "contact_guard",
|
||||
"auto_soundscape", "allow_nonspeech_vocals",
|
||||
"ref_image_5", "ref_image_6", "ref_image_7", "ref_image_8", "ref_image_9",
|
||||
"plan", "plan_scene_index",
|
||||
)
|
||||
|
||||
NL = "\n"
|
||||
@@ -4003,6 +4015,27 @@ def _connected_refs(ref_slots):
|
||||
return [ref for ref in (ref_slots or []) if ref is not None]
|
||||
|
||||
|
||||
def _plan_scene_refs(plan, scene_index):
|
||||
"""Nine ref slots for one plan scene, or all-empty when no usable binding exists."""
|
||||
if plan is None:
|
||||
return (None,) * _image_nodes._H3_PLAN_IMAGE_SLOTS
|
||||
extracted = _image_nodes.DumasH3PlanExtractSceneImagesNode().extract(plan, scene_index)
|
||||
return tuple(extracted[1:1 + _image_nodes._H3_PLAN_IMAGE_SLOTS])
|
||||
|
||||
|
||||
def _merge_ref_slots(direct_slots, plan_slots):
|
||||
"""Directly-wired refs win; plan-scene refs fill the empty sockets."""
|
||||
direct_slots = tuple(direct_slots or ())
|
||||
plan_slots = tuple(plan_slots or ())
|
||||
width = max(len(direct_slots), len(plan_slots), _image_nodes._H3_PLAN_IMAGE_SLOTS)
|
||||
out = []
|
||||
for index in range(width):
|
||||
direct = direct_slots[index] if index < len(direct_slots) else None
|
||||
plan_ref = plan_slots[index] if index < len(plan_slots) else None
|
||||
out.append(direct if direct is not None else plan_ref)
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def resolve_tagged_refs(text, ref_list):
|
||||
"""(rewritten text, images, dropped) for the <Picture N> tags in ONE shot.
|
||||
|
||||
@@ -5267,9 +5300,29 @@ class H3LongVideos:
|
||||
"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 "
|
||||
"plan": (
|
||||
"H3_CHAIN_PLAN",
|
||||
{
|
||||
"tooltip": "Optional H3 plan enriched by Dumas H3 Plan Attach Scene Images. "
|
||||
"When connected, this node can pull the selected scene's nine "
|
||||
"image slots directly instead of rewiring them by hand."
|
||||
},
|
||||
),
|
||||
"plan_scene_index": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 9999,
|
||||
"step": 1,
|
||||
"tooltip": "1-based plan scene index to read from `plan`. "
|
||||
"Any directly-wired ref_image socket overrides the same slot "
|
||||
"from the plan scene."
|
||||
},
|
||||
),
|
||||
"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 "
|
||||
"shots/frames/seconds outputs near-instantly. Turn off to render for real."}),
|
||||
"fps": ("INT", {"default": 24, "min": 1, "max": 60,
|
||||
"tooltip": "DISPLAY ONLY -- H3 always renders 24 fps. The model's frame grid and its "
|
||||
@@ -5764,6 +5817,7 @@ class H3LongVideos:
|
||||
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,
|
||||
plan=None, plan_scene_index=1,
|
||||
ref_mode="where tagged", ref_image_size="match", ref_noise_aug=0.999,
|
||||
graph=None, node_id=None):
|
||||
|
||||
@@ -5782,10 +5836,18 @@ class H3LongVideos:
|
||||
fps_note = ("" if int(fps) == H3_FPS else
|
||||
f"fps widget is {int(fps)} but H3 always renders {H3_FPS} fps -- all durations "
|
||||
f"computed at {H3_FPS}; set your video-save node to {H3_FPS} too")
|
||||
fps = H3_FPS
|
||||
w, h = parse_resolution(resolution)
|
||||
# A pixel budget overrides the preset's SIZE while keeping its aspect ratio,
|
||||
# so the dropdown chooses the shape and this chooses how big. Scaling from
|
||||
fps = H3_FPS
|
||||
w, h = parse_resolution(resolution)
|
||||
direct_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,
|
||||
)
|
||||
plan_ref_slots = _plan_scene_refs(plan, plan_scene_index)
|
||||
ref_slots = _merge_ref_slots(direct_ref_slots, plan_ref_slots)
|
||||
plan_ref_count = len(_connected_refs(plan_ref_slots))
|
||||
direct_ref_count = len(_connected_refs(direct_ref_slots))
|
||||
# A pixel budget overrides the preset's SIZE while keeping its aspect ratio,
|
||||
# so the dropdown chooses the shape and this chooses how big. Scaling from
|
||||
# the preset's own dimensions is what makes 1.00MP reproduce each native
|
||||
# size exactly -- the preset NAMES are approximations (1344x768 is 7:4, not
|
||||
# 16:9), so computing from a nominal ratio would not.
|
||||
@@ -6042,14 +6104,10 @@ 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.
|
||||
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
|
||||
plan_ref = ""
|
||||
if n_refs:
|
||||
# Mirror the render's placement exactly: 'where tagged' reads the
|
||||
# prompts and falls back to first shot when nothing is tagged --
|
||||
# reporting by ref_mode alone described shots the render never gave
|
||||
# references to.
|
||||
@@ -6060,11 +6118,17 @@ class H3LongVideos:
|
||||
mode_eff = "first shot" if ref_mode == "where tagged" else ref_mode
|
||||
on = [n + 1 for n in range(shots)
|
||||
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) "
|
||||
f"{','.join(str(n) for n in on) or 'none'} ({how}) -> those shots keep "
|
||||
f"the previous frame as their keyframe too, unless ref_noise_aug was lowered")
|
||||
how = (f"ref_mode '{mode_eff}'"
|
||||
+ (" -- no tags found anywhere" if ref_mode == "where tagged" else ""))
|
||||
src = []
|
||||
if direct_ref_count:
|
||||
src.append(f"{direct_ref_count} direct")
|
||||
if plan_ref_count:
|
||||
src.append(f"{plan_ref_count} from plan scene {int(plan_scene_index)}")
|
||||
plan_ref = (f" ref2va: {n_refs} reference image(s) at '{ref_image_size}' on shot(s) "
|
||||
f"{','.join(str(n) for n in on) or 'none'} ({how}) -> those shots keep "
|
||||
f"the previous frame as their keyframe too, unless ref_noise_aug was lowered"
|
||||
+ (f" [source: {', '.join(src)}]" if src else ""))
|
||||
plan = ((anchor_note + " ") if anchor_note else "") + \
|
||||
preflight_txt + \
|
||||
(("DIALOGUE MAY BE CUT OFF -- " + "; ".join(fit_warnings) + ". ") if fit_warnings else "") + \
|
||||
@@ -6097,11 +6161,7 @@ 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 = [
|
||||
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))
|
||||
connected_ref_count = len(_connected_refs(ref_slots))
|
||||
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
|
||||
@@ -6129,7 +6189,7 @@ class H3LongVideos:
|
||||
# The prompt itself says where each reference belongs: the shot whose
|
||||
# text names <Picture N> gets image N, renumbered to match what that
|
||||
# shot actually carries. Every untagged shot keeps its handoff.
|
||||
gen_prompt, shot_refs, dropped = resolve_tagged_refs(gen_prompt, ref_list)
|
||||
gen_prompt, shot_refs, dropped = resolve_tagged_refs(gen_prompt, ref_slots)
|
||||
for n in dropped:
|
||||
if n not in ref_missing:
|
||||
ref_missing.append(n)
|
||||
@@ -6149,7 +6209,7 @@ class H3LongVideos:
|
||||
shot_refs = shot_refs + [handoff]
|
||||
ref_carried.append(i + 1)
|
||||
else:
|
||||
shot_refs = shot_references(ref_list, ref_mode, i, handoff)
|
||||
shot_refs = shot_references(ref_slots, ref_mode, i, handoff)
|
||||
# ComfyUI 0.31+ lets references and a keyframe ride TOGETHER, and only
|
||||
# the tagged branch above was ever updated for it. Everywhere else a
|
||||
# ref-conditioned shot still dropped its handoff, as 0.30 required:
|
||||
@@ -6412,13 +6472,19 @@ class H3LongVideos:
|
||||
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_source = []
|
||||
if direct_ref_count:
|
||||
ref_source.append(f"{direct_ref_count} direct")
|
||||
if plan_ref_count:
|
||||
ref_source.append(f"{plan_ref_count} from plan scene {int(plan_scene_index)}")
|
||||
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
|
||||
and float(ref_noise_aug) < 0.999 else "")
|
||||
+ (f"; shot(s) {','.join(str(n) for n in kept)} keep the handoff" if kept
|
||||
else "")
|
||||
+ (f"; source {' + '.join(ref_source)}" if ref_source else "")
|
||||
+ (f"; shot(s) {','.join(str(n) for n in kept)} keep the handoff" if kept
|
||||
else "")
|
||||
+ (f"; shot(s) {','.join(str(n) for n in ref_keyframed)} carry the previous "
|
||||
f"frame as a real KEYFRAME alongside their references, so they anchor "
|
||||
f"rather than cut" if ref_keyframed else "")
|
||||
@@ -6431,7 +6497,9 @@ class H3LongVideos:
|
||||
+ ref_note_missing)
|
||||
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")
|
||||
f"'{ref_mode}' applied them to no shot"
|
||||
+ (f" (source {' + '.join(([f'{direct_ref_count} direct'] if direct_ref_count else []) + ([f'{plan_ref_count} from plan scene {int(plan_scene_index)}'] if plan_ref_count else []))})"
|
||||
if (direct_ref_count or plan_ref_count) else ""))
|
||||
else:
|
||||
ref_note = ""
|
||||
info = ((anchor_note + " ") if anchor_note else "") + \
|
||||
|
||||
Reference in New Issue
Block a user