Wire H3 long videos to plan scene refs
This commit is contained in:
@@ -34,11 +34,12 @@
|
|||||||
- 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_9`, 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`, optional `plan`, optional `plan_scene_index`, 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.
|
- 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.
|
||||||
|
- A connected H3 plan can now supply the current scene’s 9-image bundle directly; any directly-wired `ref_image_*` socket overrides the same numbered plan slot.
|
||||||
|
|
||||||
- `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`
|
||||||
|
|||||||
+100
-32
@@ -54,17 +54,28 @@ import comfy.nested_tensor
|
|||||||
import comfy.model_management as mm
|
import comfy.model_management as mm
|
||||||
import node_helpers
|
import node_helpers
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from . import dumas_h3_overlay as _overlay
|
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
|
except ImportError: # loaded as a bare file (test_prompt_logic.py), not as a package
|
||||||
import importlib.util as _ilu
|
import importlib.util as _ilu
|
||||||
import os as _os
|
import os as _os
|
||||||
|
import sys as _sys
|
||||||
_spec = _ilu.spec_from_file_location(
|
_spec = _ilu.spec_from_file_location(
|
||||||
"dumas_h3_overlay",
|
"dumas_h3_overlay",
|
||||||
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_h3_overlay.py"),
|
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "dumas_h3_overlay.py"),
|
||||||
)
|
)
|
||||||
_overlay = _ilu.module_from_spec(_spec)
|
_overlay = _ilu.module_from_spec(_spec)
|
||||||
_spec.loader.exec_module(_overlay)
|
_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
|
AUDIO_LATENT_FPS = 40
|
||||||
GB = 1024 ** 3
|
GB = 1024 ** 3
|
||||||
@@ -265,6 +276,7 @@ ADDED_WIDGETS = (
|
|||||||
"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",
|
"ref_image_5", "ref_image_6", "ref_image_7", "ref_image_8", "ref_image_9",
|
||||||
|
"plan", "plan_scene_index",
|
||||||
)
|
)
|
||||||
|
|
||||||
NL = "\n"
|
NL = "\n"
|
||||||
@@ -4003,6 +4015,27 @@ def _connected_refs(ref_slots):
|
|||||||
return [ref for ref in (ref_slots or []) if ref is not None]
|
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):
|
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.
|
||||||
|
|
||||||
@@ -5267,9 +5300,29 @@ class H3LongVideos:
|
|||||||
"ref_image_7": ("IMAGE", {"tooltip": "Reference image <Picture 7>."}),
|
"ref_image_7": ("IMAGE", {"tooltip": "Reference image <Picture 7>."}),
|
||||||
"ref_image_8": ("IMAGE", {"tooltip": "Reference image <Picture 8>."}),
|
"ref_image_8": ("IMAGE", {"tooltip": "Reference image <Picture 8>."}),
|
||||||
"ref_image_9": ("IMAGE", {"tooltip": "Reference image <Picture 9>."}),
|
"ref_image_9": ("IMAGE", {"tooltip": "Reference image <Picture 9>."}),
|
||||||
"plan_only": ("BOOLEAN", {"default": False,
|
"plan": (
|
||||||
"tooltip": "Preview the shot split WITHOUT rendering. Uses THIS node's own settings (no "
|
"H3_CHAIN_PLAN",
|
||||||
"second node, no duplicate entry): returns the plan in 'info' and the "
|
{
|
||||||
|
"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."}),
|
"shots/frames/seconds outputs near-instantly. Turn off to render for real."}),
|
||||||
"fps": ("INT", {"default": 24, "min": 1, "max": 60,
|
"fps": ("INT", {"default": 24, "min": 1, "max": 60,
|
||||||
"tooltip": "DISPLAY ONLY -- H3 always renders 24 fps. The model's frame grid and its "
|
"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_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_5=None, ref_image_6=None, ref_image_7=None, ref_image_8=None,
|
||||||
ref_image_9=None,
|
ref_image_9=None,
|
||||||
|
plan=None, plan_scene_index=1,
|
||||||
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):
|
||||||
|
|
||||||
@@ -5782,10 +5836,18 @@ class H3LongVideos:
|
|||||||
fps_note = ("" if int(fps) == H3_FPS else
|
fps_note = ("" if int(fps) == H3_FPS else
|
||||||
f"fps widget is {int(fps)} but H3 always renders {H3_FPS} fps -- all durations "
|
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")
|
f"computed at {H3_FPS}; set your video-save node to {H3_FPS} too")
|
||||||
fps = H3_FPS
|
fps = H3_FPS
|
||||||
w, h = parse_resolution(resolution)
|
w, h = parse_resolution(resolution)
|
||||||
# A pixel budget overrides the preset's SIZE while keeping its aspect ratio,
|
direct_ref_slots = (
|
||||||
# so the dropdown chooses the shape and this chooses how big. Scaling from
|
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
|
# 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
|
# size exactly -- the preset NAMES are approximations (1344x768 is 7:4, not
|
||||||
# 16:9), so computing from a nominal ratio would not.
|
# 16:9), so computing from a nominal ratio would not.
|
||||||
@@ -6042,14 +6104,10 @@ 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.
|
||||||
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))
|
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
|
||||||
# prompts and falls back to first shot when nothing is tagged --
|
# prompts and falls back to first shot when nothing is tagged --
|
||||||
# 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.
|
||||||
@@ -6060,11 +6118,17 @@ class H3LongVideos:
|
|||||||
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(ref_slots, 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) "
|
src = []
|
||||||
f"{','.join(str(n) for n in on) or 'none'} ({how}) -> those shots keep "
|
if direct_ref_count:
|
||||||
f"the previous frame as their keyframe too, unless ref_noise_aug was lowered")
|
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 "") + \
|
plan = ((anchor_note + " ") if anchor_note else "") + \
|
||||||
preflight_txt + \
|
preflight_txt + \
|
||||||
(("DIALOGUE MAY BE CUT OFF -- " + "; ".join(fit_warnings) + ". ") if fit_warnings else "") + \
|
(("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
|
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 = [
|
connected_ref_count = len(_connected_refs(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,
|
|
||||||
]
|
|
||||||
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
|
||||||
@@ -6129,7 +6189,7 @@ class H3LongVideos:
|
|||||||
# 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. 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:
|
for n in dropped:
|
||||||
if n not in ref_missing:
|
if n not in ref_missing:
|
||||||
ref_missing.append(n)
|
ref_missing.append(n)
|
||||||
@@ -6149,7 +6209,7 @@ class H3LongVideos:
|
|||||||
shot_refs = shot_refs + [handoff]
|
shot_refs = shot_refs + [handoff]
|
||||||
ref_carried.append(i + 1)
|
ref_carried.append(i + 1)
|
||||||
else:
|
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
|
# ComfyUI 0.31+ lets references and a keyframe ride TOGETHER, and only
|
||||||
# the tagged branch above was ever updated for it. Everywhere else a
|
# the tagged branch above was ever updated for it. Everywhere else a
|
||||||
# ref-conditioned shot still dropped its handoff, as 0.30 required:
|
# ref-conditioned shot still dropped its handoff, as 0.30 required:
|
||||||
@@ -6412,13 +6472,19 @@ 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]
|
||||||
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_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) "
|
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
|
||||||
and float(ref_noise_aug) < 0.999 else "")
|
and float(ref_noise_aug) < 0.999 else "")
|
||||||
+ (f"; shot(s) {','.join(str(n) for n in kept)} keep the handoff" if kept
|
+ (f"; source {' + '.join(ref_source)}" if ref_source else "")
|
||||||
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"; 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"frame as a real KEYFRAME alongside their references, so they anchor "
|
||||||
f"rather than cut" if ref_keyframed else "")
|
f"rather than cut" if ref_keyframed else "")
|
||||||
@@ -6431,7 +6497,9 @@ class H3LongVideos:
|
|||||||
+ ref_note_missing)
|
+ ref_note_missing)
|
||||||
elif connected_ref_count:
|
elif connected_ref_count:
|
||||||
ref_note = (f" ref2va: {connected_ref_count} 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"
|
||||||
|
+ (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:
|
else:
|
||||||
ref_note = ""
|
ref_note = ""
|
||||||
info = ((anchor_note + " ") if anchor_note else "") + \
|
info = ((anchor_note + " ") if anchor_note else "") + \
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
"comfy.nested_tensor",
|
"comfy.nested_tensor",
|
||||||
"comfy.model_management",
|
"comfy.model_management",
|
||||||
"node_helpers",
|
"node_helpers",
|
||||||
|
"numpy",
|
||||||
|
"PIL",
|
||||||
|
"PIL.Image",
|
||||||
|
"folder_paths",
|
||||||
|
"dumas_image_nodes",
|
||||||
"dumas_h3_longvideos",
|
"dumas_h3_longvideos",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -26,6 +31,17 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
cuda=types.SimpleNamespace(OutOfMemoryError=RuntimeError),
|
cuda=types.SimpleNamespace(OutOfMemoryError=RuntimeError),
|
||||||
float32="float32",
|
float32="float32",
|
||||||
)
|
)
|
||||||
|
fake_numpy = types.SimpleNamespace(
|
||||||
|
clip=lambda array, _low, _high: array,
|
||||||
|
uint8="uint8",
|
||||||
|
)
|
||||||
|
fake_pil_image_module = types.SimpleNamespace(fromarray=lambda _array: None)
|
||||||
|
fake_pil_module = types.SimpleNamespace(Image=fake_pil_image_module)
|
||||||
|
fake_folder_paths = types.SimpleNamespace(
|
||||||
|
get_temp_directory=lambda: "/tmp",
|
||||||
|
get_output_directory=lambda: "/tmp",
|
||||||
|
get_save_image_path=lambda prefix, _out, _width, _height: ("/tmp", prefix, 1, "", prefix),
|
||||||
|
)
|
||||||
fake_nodes = types.SimpleNamespace(common_ksampler=lambda *args, **kwargs: ({},))
|
fake_nodes = types.SimpleNamespace(common_ksampler=lambda *args, **kwargs: ({},))
|
||||||
fake_comfy_samplers = types.SimpleNamespace(
|
fake_comfy_samplers = types.SimpleNamespace(
|
||||||
KSampler=types.SimpleNamespace(SAMPLERS=("res_multistep",), SCHEDULERS=("simple",))
|
KSampler=types.SimpleNamespace(SAMPLERS=("res_multistep",), SCHEDULERS=("simple",))
|
||||||
@@ -48,6 +64,10 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
sys.modules["torch"] = fake_torch
|
sys.modules["torch"] = fake_torch
|
||||||
|
sys.modules["numpy"] = fake_numpy
|
||||||
|
sys.modules["PIL"] = fake_pil_module
|
||||||
|
sys.modules["PIL.Image"] = fake_pil_image_module
|
||||||
|
sys.modules["folder_paths"] = fake_folder_paths
|
||||||
sys.modules["nodes"] = fake_nodes
|
sys.modules["nodes"] = fake_nodes
|
||||||
sys.modules["comfy"] = fake_comfy
|
sys.modules["comfy"] = fake_comfy
|
||||||
sys.modules["comfy.utils"] = fake_comfy_utils
|
sys.modules["comfy.utils"] = fake_comfy_utils
|
||||||
@@ -56,6 +76,7 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
sys.modules["comfy.model_management"] = fake_mm
|
sys.modules["comfy.model_management"] = fake_mm
|
||||||
sys.modules["node_helpers"] = types.SimpleNamespace()
|
sys.modules["node_helpers"] = types.SimpleNamespace()
|
||||||
|
|
||||||
|
cls.image_module = importlib.import_module("dumas_image_nodes")
|
||||||
cls.module = importlib.import_module("dumas_h3_longvideos")
|
cls.module = importlib.import_module("dumas_h3_longvideos")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -176,6 +197,33 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
|
|
||||||
for index in range(1, 10):
|
for index in range(1, 10):
|
||||||
self.assertIn(f"ref_image_{index}", optional)
|
self.assertIn(f"ref_image_{index}", optional)
|
||||||
|
self.assertIn("plan", optional)
|
||||||
|
self.assertIn("plan_scene_index", optional)
|
||||||
|
|
||||||
|
def test_plan_scene_refs_reads_bound_images(self):
|
||||||
|
attach = self.image_module.DumasH3PlanAttachSceneImagesNode()
|
||||||
|
plan = {"shots": [{"id": "one"}, {"id": "two"}]}
|
||||||
|
image2 = object()
|
||||||
|
image7 = object()
|
||||||
|
|
||||||
|
plan, _ = attach.attach(plan=plan, scene_index=2, image2=image2, image7=image7)
|
||||||
|
refs = self.module._plan_scene_refs(plan, 2)
|
||||||
|
|
||||||
|
self.assertEqual(len(refs), 9)
|
||||||
|
self.assertIsNone(refs[0])
|
||||||
|
self.assertIs(refs[1], image2)
|
||||||
|
self.assertIs(refs[6], image7)
|
||||||
|
|
||||||
|
def test_merge_ref_slots_prefers_direct_refs_over_plan_refs(self):
|
||||||
|
merged = self.module._merge_ref_slots(
|
||||||
|
(None, "direct2", None, None, "direct5", None, None, None, None),
|
||||||
|
("plan1", "plan2", "plan3", None, "plan5", None, "plan7", None, None),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
merged,
|
||||||
|
("plan1", "direct2", "plan3", None, "direct5", None, "plan7", None, None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user