From 2d30c4f5645d76be686f2f0e8cda2a3cc0f9972c Mon Sep 17 00:00:00 2001 From: IAMCCS Date: Mon, 4 May 2026 07:08:57 +0200 Subject: [PATCH] Updated IAMCCS-nodes to version 1.4.4 --- CHANGELOG.md | 4 + README.md | 2 +- __init__.py | 3 + iamccs_ltx2_tools.py | 160 +- iamccs_supernodes_auimg2vid_exec_backend.py | 2582 +++++++++++++++++-- iamccs_supernodes_second_stage.py | 12 +- version.json | 2 +- web/iamccs_supernodes_exec_ui.js | 2582 ++++++++++++++++--- 8 files changed, 4749 insertions(+), 598 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a79e05..302f0a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # IAMCCS Nodes - Changelog +## 🆕 2026-05-04 - version 1.4.4 — Supernodes v.2 and bug fixed pplus utilities added + +## 🆕 2026-04-24 - version 1.4.3 — Supwenodes, more LTX and WAN 2.2 nodes and utilities added + ## 🆕 2026-04-13 - version 1.4.1 — Multigen wrappers added ## 🆕 2026-03-27 - version 1.4.0 — Audio extension ltx-2.3 diff --git a/README.md b/README.md index 1540984..3f38378 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ### Category: ComfyUI Custom Nodes ### Main Feature: Fix for LoRA loading in native WANAnimate workflows + general nodes 4 ComfyUI -Version: 1.4.3 +Version: 1.4.4 (Supernodes and wan 2.2 + LTX 2.2 utilities added) ## IAMCCS SuperNodes requirements diff --git a/__init__.py b/__init__.py index 7c668b8..a08d246 100644 --- a/__init__.py +++ b/__init__.py @@ -59,6 +59,7 @@ from .iamccs_ltx2_tools import ( IAMCCS_LTX2_ImageBatchCropByPad, IAMCCS_SegmentPlanner, IAMCCS_SegmentPlannerSettings, + IAMCCS_AudioSegmentAutoPlanner, IAMCCS_SegmentPlannerLinked, IAMCCS_SegmentPlanFromPlanner, IAMCCS_SourceRangeFromSegmentPlan, @@ -313,6 +314,7 @@ NODE_CLASS_MAPPINGS = { "IAMCCS_LTX2_ImageBatchCropByPad": IAMCCS_LTX2_ImageBatchCropByPad, "IAMCCS_SegmentPlanner": IAMCCS_SegmentPlanner, "IAMCCS_SegmentPlannerSettings": IAMCCS_SegmentPlannerSettings, + "IAMCCS_AudioSegmentAutoPlanner": IAMCCS_AudioSegmentAutoPlanner, "IAMCCS_SegmentPlannerLinked": IAMCCS_SegmentPlannerLinked, "IAMCCS_SegmentPlanFromPlanner": IAMCCS_SegmentPlanFromPlanner, "IAMCCS_SourceRangeFromSegmentPlan": IAMCCS_SourceRangeFromSegmentPlan, @@ -501,6 +503,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "IAMCCS_LTX2_ImageBatchCropByPad": "LTX-2 Crop By Pad (IMAGE batch)", "IAMCCS_SegmentPlanner": "Segment Planner (song -> LTX frames)", "IAMCCS_SegmentPlannerSettings": "Segment Planner Settings (shared)", + "IAMCCS_AudioSegmentAutoPlanner": "Audio Segment Auto Planner (audio -> segmenti)", "IAMCCS_SegmentPlannerLinked": "Segment Planner Linked (shared inputs)", "IAMCCS_SegmentPlanFromPlanner": "Segment Plan From Planner (per index)", "IAMCCS_SourceRangeFromSegmentPlan": "Source Range From Segment Plan", diff --git a/iamccs_ltx2_tools.py b/iamccs_ltx2_tools.py index 29ea50c..0531b4b 100644 --- a/iamccs_ltx2_tools.py +++ b/iamccs_ltx2_tools.py @@ -10,7 +10,7 @@ from __future__ import annotations import logging import math -from typing import Tuple +from typing import Any, Mapping, Tuple import torch @@ -23,6 +23,22 @@ _F = torch.nn.functional _log = logging.getLogger("IAMCCS.LTX2.Tools") +def _audio_duration_seconds(audio: Any) -> float: + if not isinstance(audio, Mapping): + raise ValueError("audio input is required") + waveform = audio.get("waveform") + sample_rate = int(audio.get("sample_rate", 0) or 0) + if waveform is None or sample_rate <= 0: + raise ValueError("audio input must contain waveform and sample_rate") + try: + total_samples = int(waveform.shape[-1]) + except Exception as exc: + raise ValueError("audio waveform has no sample dimension") from exc + if total_samples <= 0: + raise ValueError("audio waveform is empty") + return float(total_samples) / float(sample_rate) + + def _to_grayscale(image: torch.Tensor) -> torch.Tensor: # image: [B,H,W,C] float in [0,1] if image.shape[-1] == 1: @@ -893,6 +909,148 @@ class IAMCCS_SegmentPlannerSettings(IAMCCS_SegmentPlanner): ) +class IAMCCS_AudioSegmentAutoPlanner(IAMCCS_SegmentPlanner): + SPLIT_BY_SEGMENTS = "choose segment count (audio / segments)" + SPLIT_BY_SECONDS = "choose seconds per segment (auto count)" + SEGMENT_COUNT_CHOICES = (1, 2, 3, 4, 5, 6, 8, 10, 12) + SEGMENT_SECONDS_CHOICES = (5.0, 8.0, 10.0, 12.0, 15.0, 20.0, 30.0) + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "audio": ("AUDIO",), + "fps": ("FLOAT", {"default": 25.0, "min": 0.001, "max": 240.0, "step": 0.01}), + "split_mode": ([cls.SPLIT_BY_SEGMENTS, cls.SPLIT_BY_SECONDS], {"default": cls.SPLIT_BY_SEGMENTS}), + "segment_count": (list(cls.SEGMENT_COUNT_CHOICES), {"default": 2}), + "target_segment_seconds": (list(cls.SEGMENT_SECONDS_CHOICES), {"default": 10.0}), + "overlap_frames": ("INT", {"default": 9, "min": 0, "max": 4096, "step": 1}), + "ltx_round_mode": (["up", "nearest", "down"], {"default": "up"}), + } + } + + RETURN_TYPES = ( + "FLOAT", "FLOAT", "STRING", "STRING", "INT", + "INT", "INT", "INT", "INT", "INT", "INT", "FLOAT", "STRING", + ) + RETURN_NAMES = ( + "audio_duration_s", + "segment_duration_s", + "planning_mode", + "segment_preset", + "overlap_frames", + "segment_count", + "total_frames", + "unique_segment_frames", + "first_segment_raw_frames", + "continuation_raw_frames", + "last_segment_unique_frames", + "fps_out", + "report", + ) + FUNCTION = "plan_audio_segments" + CATEGORY = "IAMCCS/LTX-2" + + def _preset_from_seconds(self, seconds: float) -> str: + preset, _rec = self._recommend_profile_for_segment_seconds(float(seconds)) + if preset is not None: + return str(preset) + choices = ((5.0, "5sec"), (10.0, "10sec"), (15.0, "15sec"), (20.0, "20sec")) + return min(choices, key=lambda item: abs(float(seconds) - item[0]))[1] + + @staticmethod + def _first_number(value: Any, default: float) -> float: + if isinstance(value, (int, float)): + return float(value) + text = str(value or "").strip() + token = "" + seen_decimal = False + for ch in text: + if ch.isdigit(): + token += ch + elif ch == "." and token and not seen_decimal: + token += ch + seen_decimal = True + elif token: + break + if not token or token == ".": + return float(default) + try: + return float(token) + except Exception: + return float(default) + + def _normalize_split_mode(self, split_mode: Any) -> tuple[str, str]: + text = str(split_mode or "").strip().lower() + if text in ("fixed_segment_seconds", "choose_seconds_per_segment_auto_count"): + return "fixed_segment_seconds", self.SPLIT_BY_SECONDS + if "seconds per segment" in text or "auto count" in text: + return "fixed_segment_seconds", self.SPLIT_BY_SECONDS + return "fixed_segment_count", self.SPLIT_BY_SEGMENTS + + def plan_audio_segments( + self, + audio, + fps: float, + split_mode: str, + segment_count: int, + target_segment_seconds: float, + overlap_frames: int, + ltx_round_mode: str, + ): + audio_duration_s = max(0.01, _audio_duration_seconds(audio)) + fps = max(0.001, float(fps)) + split_mode_key, split_mode_label = self._normalize_split_mode(split_mode) + requested_segment_count = max(1, int(round(self._first_number(segment_count, 2)))) + target_segment_seconds = max(0.01, float(self._first_number(target_segment_seconds, 10.0))) + + if split_mode_key == "fixed_segment_seconds": + segment_duration_s = target_segment_seconds + else: + segment_duration_s = max(0.01, audio_duration_s / float(requested_segment_count)) + + segment_preset = self._preset_from_seconds(segment_duration_s) + planning_mode = "manual_segment_seconds" + + planner_result = self.plan( + audio_duration_s, + fps, + segment_duration_s, + planning_mode, + segment_preset, + int(overlap_frames), + str(ltx_round_mode), + 0, + ) + + computed_segment_count = int(planner_result[4]) + report = ( + f"audio_duration={audio_duration_s:.3f}s @ {fps:.3f}fps | split_mode={split_mode_key} ({split_mode_label}) | " + f"requested_segments={requested_segment_count} | computed_segments={computed_segment_count} | " + f"segment_duration={segment_duration_s:.3f}s | total={int(planner_result[0])}f | " + f"unique={int(planner_result[1])}f | first_raw={int(planner_result[2])}f | " + f"continuation_raw={int(planner_result[3])}f | last_unique={int(planner_result[6])}f | " + f"preset={segment_preset} | overlap={int(overlap_frames)}f | ltx_round={ltx_round_mode}" + ) + _log.info("[AudioSegmentAutoPlanner] %s", report) + + return ( + float(audio_duration_s), + float(segment_duration_s), + planning_mode, + segment_preset, + int(overlap_frames), + computed_segment_count, + int(planner_result[0]), + int(planner_result[1]), + int(planner_result[2]), + int(planner_result[3]), + int(planner_result[6]), + float(planner_result[17]), + report, + ) + + class IAMCCS_SegmentPlannerLinked(IAMCCS_SegmentPlanner): @classmethod def INPUT_TYPES(cls): diff --git a/iamccs_supernodes_auimg2vid_exec_backend.py b/iamccs_supernodes_auimg2vid_exec_backend.py index 4fbd399..fd3096c 100644 --- a/iamccs_supernodes_auimg2vid_exec_backend.py +++ b/iamccs_supernodes_auimg2vid_exec_backend.py @@ -20,6 +20,7 @@ from comfy_extras.nodes_lt import ( LTXVCropGuides, LTXVImgToVideoInplace, LTXVPreprocess, + LTXVScheduler, LTXVSeparateAVLatent, ModelSamplingLTXV, ) @@ -32,6 +33,21 @@ from .iamccs_supernodes_linx import SUPERNODE_LINX_TYPE, build_stage_linx_payloa _SAMPLER_NAMES = tuple(comfy.samplers.SAMPLER_NAMES) _REFERENCE_MANUAL_SIGMAS = "1., 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0" +_DEFAULT_GENERATED_FPS = 25.0 +_REFERENCE_AUDIO_IMG2VID_FPS = 25.0 +_REFERENCE_AUDIO_IMG2VID_WIDTH = 1280 +_REFERENCE_AUDIO_IMG2VID_HEIGHT = 720 +_STALE_AUDIO_IMG2VID_WIDTH = 768 +_STALE_AUDIO_IMG2VID_HEIGHT = 512 +_AUDIO_IMG2VID_CONTRACT_VERSION = "2026-05-03_workflow_kjresize_downround_v5" +_LEGACY_AUDIO_IMG2VID_CONTRACT_VERSION = "2026-05-03_legacy_exact_workflows_v1" +# Code by Carmine Cristallo Scalzi AI research (IAMCCS) patreon.com/IAMCCS +_REFERENCE_AUDIO_IMG2VID_DIVISIBLE_BY = 32 +_REFERENCE_AUDIO_IMG2VID_NEGATIVE = "closed mouth, smiling without speaking, singing concert, dancing, exaggerated head movement, blurry mouth" +_REFERENCE_AUDIO_IMG2VID_TAIL_STRENGTH = 0.9 +_REFERENCE_AUDIO_IMG2VID_TAIL_PREPROCESS_CRF = 28 +_LEGACY_EXACT_TAIL_STRENGTH = 1.0 +_LEGACY_EXACT_TAIL_PREPROCESS_CRF = 33 _PLANNER_AUDIO_MODES = ( "melband_vocals_duration_math", "raw_audio_only", @@ -46,10 +62,14 @@ _MEDIA_MODES = ( "img2vid_pure", "t2v_pure", ) -_GENERATION_TYPE_MODES = ( +_AUDIO_IMAGE_GENERATION_TYPE = "audio+image2video" +_AUDIO_IMAGE_LEGACY_GENERATION_TYPES = { "aud+img2video_simple", "aud+img2video_2_segments", "aud+img2video_infinite", +} +_GENERATION_TYPE_MODES = ( + _AUDIO_IMAGE_GENERATION_TYPE, "text+audio2video", "img2video", "text2video", @@ -57,25 +77,32 @@ _GENERATION_TYPE_MODES = ( _RENDER_BACKEND_MODES = ( "auto", "single_best", + "legacy backend", + "legacy_single", + "legacy_two_segments", + "legacy_loop", "two_segments_normal_vram", + "three_segments_normal_vram", "loop_normal_vram", "loop_low_ram_disk", ) +_LEGACY_AUDIO_IMG2VID_BACKENDS = {"legacy_single", "legacy_two_segments", "legacy_loop"} _MODULAR_DECODE_MODES = ( "inherit_render_backend", - "low_ram_disk", - "very_low_ram_disk", + "normal_tiled_iamccs", "normal_tiled_vhs", + "low_ram", + "low_ram_disk", + "very_low_ram", + "very_low_ram_disk", "high_vram", "custom_mode", ) _VAE_DECODE_MODES = ( - "inherit_render_backend", - "low_ram_disk", - "very_low_ram_disk", - "normal_tiled_vhs", + "normal_tiled_iamccs", + "low_ram", + "very_low_ram", "high_vram", - "custom_mode", ) try: @@ -89,14 +116,55 @@ try: ) except Exception: _LATENT_UPSCALE_MODEL_NAMES = ( - "ltx-2.3-spatial-upscaler-x2-1.1.safetensors", "ltx-2.3-spatial-upscaler-x2-1.0.safetensors", + "ltx-2.3-spatial-upscaler-x2-1.1.safetensors", ) _MELBAND_MODEL_NAMES = ("MelBandRoformer_fp32.safetensors",) if not _MELBAND_MODEL_NAMES: _MELBAND_MODEL_NAMES = ("MelBandRoformer_fp32.safetensors",) +_MELBAND_MODEL_NAMES = tuple(sorted( + (str(name) for name in _MELBAND_MODEL_NAMES if str(name)), + key=lambda name: ( + 0 if name == "MelBandRoformer_fp32.safetensors" else + 1 if ("melband" in name.lower() and "fp32" in name.lower()) else + 2, + name.lower(), + ), +)) + + +def _resolve_melband_model_name(requested=None): + requested_name = str(requested or "").strip() + names = tuple(str(name) for name in (_MELBAND_MODEL_NAMES or ()) if str(name)) + preferred = None + for name in names: + if name == "MelBandRoformer_fp32.safetensors": + preferred = name + break + if preferred is None: + for name in names: + lowered = name.lower() + if "melband" in lowered and "fp32" in lowered: + preferred = name + break + if requested_name and "fp16" not in requested_name.lower(): + return requested_name + return preferred or requested_name or "MelBandRoformer_fp32.safetensors" + + +def _reference_ltx23_upscale_model_name(): + preferred = "ltx-2.3-spatial-upscaler-x2-1.0.safetensors" + names = tuple(str(name) for name in (_LATENT_UPSCALE_MODEL_NAMES or ()) if str(name)) + for name in names: + if name == preferred: + return name + for name in names: + if "ltx-2.3-spatial-upscaler-x2-1.0" in name.lower(): + return name + return names[0] if names else preferred + def _node_class(name): cls = comfy_nodes.NODE_CLASS_MAPPINGS.get(name) @@ -172,6 +240,34 @@ def _to_text(data, key, default): return str(value if value is not None else default) +def _to_bool(data, key, default): + value = data.get(key, default) + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return bool(value) + text = str(value if value is not None else default).strip().lower() + if text in {"1", "true", "yes", "on"}: + return True + if text in {"0", "false", "no", "off", ""}: + return False + return bool(default) + + +def _debug_verbose_enabled(debug_verbose=False, linx=None): + if _to_bool({"value": debug_verbose}, "value", False): + return True + inherited = linx_output(linx, "debug_verbose", linx_resource(linx, "debug_verbose", False)) + return _to_bool({"value": inherited}, "value", False) + + +def _append_debug_report(report, debug_report): + debug_text = str(debug_report or "").strip() + if not debug_text: + return str(report or "") + return f"{report}\n{debug_text}" + + def _same_as_default(value, default): if isinstance(default, float): try: @@ -206,6 +302,57 @@ def _first_line(text): return str(text or "").splitlines()[0].strip() +def _pipeline_debug_new(name, enabled=False): + return {"name": str(name), "lines": [], "enabled": bool(enabled)} + + +def _pipeline_debug_preview(value, limit=120): + if isinstance(value, torch.Tensor): + shape = "x".join(str(x) for x in tuple(value.shape)) + return f"tensor(shape={shape}, dtype={value.dtype}, device={value.device})" + if isinstance(value, dict): + samples = value.get("samples") + if isinstance(samples, torch.Tensor): + shape = "x".join(str(x) for x in tuple(samples.shape)) + return f"latent(samples={shape}, dtype={samples.dtype}, device={samples.device})" + if "waveform" in value: + waveform = value.get("waveform") + sample_rate = value.get("sample_rate", "?") + return f"audio(waveform={_pipeline_debug_preview(waveform, limit)}, sample_rate={sample_rate})" + return f"dict(keys={','.join(str(k) for k in list(value.keys())[:12])})" + if isinstance(value, (list, tuple)): + return f"{type(value).__name__}(len={len(value)})" + if isinstance(value, (str, int, float, bool)) or value is None: + text = str(value) + else: + text = value.__class__.__name__ + text = " ".join(text.split()) + return text if len(text) <= int(limit) else text[: int(limit) - 3] + "..." + + +def _pipeline_debug_step(trace, step_name, **fields): + if not isinstance(trace, dict) or not bool(trace.get("enabled", False)): + return "" + lines = trace.setdefault("lines", []) + idx = len(lines) + 1 + parts = [f"{key}={_pipeline_debug_preview(value)}" for key, value in fields.items()] + line = f"{idx:02d}. {step_name}" + if parts: + line += " | " + " | ".join(parts) + lines.append(line) + print(f"[IAMCCS PipelineDebug:{trace.get('name', 'pipeline')}] {line}") + return line + + +def _pipeline_debug_text(trace): + if not isinstance(trace, dict) or not bool(trace.get("enabled", False)): + return "" + lines = trace.get("lines") if isinstance(trace, dict) else None + if not lines: + return "" + return "Pipeline debug trace:\n" + "\n".join(str(line) for line in lines) + + def _planner_chip(duration_seconds, planned, planning_mode, segment_preset): profile_label = str(planned[20]) if str(planning_mode) == "explicit_preset_seconds" else str(segment_preset) return ( @@ -245,14 +392,94 @@ def _normalize_planner_mode(value): def _normalize_segment_preset(value): - text = str(value or "15sec") + text = str(value or "10sec") if text == "videoclip": return "10sec" if text == "monologue": return "15sec" if text in {"5sec", "10sec", "15sec", "20sec"}: return text - return "15sec" + return "10sec" + + +def _segment_preset_from_seconds(seconds): + value = float(seconds or 10.0) + choices = ((5.0, "5sec"), (10.0, "10sec"), (15.0, "15sec"), (20.0, "20sec")) + return min(choices, key=lambda item: abs(value - item[0]))[1] + + +_PLANNER_ROUTE_MODES = ( + "single generation (duration only)", + "choose segment count (audio / segments)", + "choose seconds per segment (auto count)", +) +_PLANNER_AUDIO_IMG2VID_BACKENDS = ( + "modern pipeline", + "legacy exact pipeline", +) +_PLANNER_AUDIO_IMG2VID_MODES = ( + "single generation", + "2 segments", + "3 segments", + "loop / 4+ segments", +) + + +def _normalize_audio_img2vid_backend_choice(value): + text = str(value or "modern").strip().lower() + if "legacy" in text: + return "legacy" + return "modern" + + +def _normalize_audio_img2vid_mode_choice(value, route_mode=None, segment_count=None): + text = str(value or "").strip().lower() + if "single" in text or text in {"1", "one"}: + return "single" + if text in {"2", "2_segments", "two_segments"} or "2 segment" in text: + return "2_segments" + if text in {"3", "3_segments", "three_segments"} or "3 segment" in text: + return "3_segments" + if "loop" in text or "auto" in text or "infinite" in text: + return "loop" + route_key = _normalize_planner_route_mode(route_mode) + if route_key == "single_generation": + return "single" + count = max(1, int(segment_count or 2)) + if count == 1: + return "single" + if count == 2: + return "2_segments" + if count == 3: + return "3_segments" + return "loop" + + +def _audio_img2vid_backend_from_planner(backend_choice, mode_choice): + family = _normalize_audio_img2vid_backend_choice(backend_choice) + mode = _normalize_audio_img2vid_mode_choice(mode_choice) + if family == "legacy": + if mode == "single": + return "legacy_single" + if mode == "2_segments": + return "legacy_two_segments" + return "legacy_loop" + if mode == "single": + return "single_best" + if mode == "2_segments": + return "two_segments_normal_vram" + if mode == "3_segments": + return "three_segments_normal_vram" + return "loop_normal_vram" + + +def _normalize_planner_route_mode(value): + text = str(value or "").strip().lower() + if "single" in text or text in {"1", "one", "single_generation"}: + return "single_generation" + if "seconds per segment" in text or "auto count" in text or text in {"fixed_segment_seconds", "choose_seconds_per_segment_auto_count"}: + return "fixed_segment_seconds" + return "fixed_segment_count" def _decode_settings(decode_backend): @@ -289,7 +516,22 @@ def _normalize_backend_mode(mode): "auto": "auto", "single_workflow1_best": "single_best", "single_best": "single_best", + "legacy backend": "legacy_single", + "legacy_backend": "legacy_single", + "legacy_github": "legacy_single", + "legacy_github_audimg": "legacy_single", + "legacy_github_single": "legacy_single", + "legacy single": "legacy_single", + "legacy_single": "legacy_single", + "legacy_github_two_segments": "legacy_two_segments", + "legacy 2 segments": "legacy_two_segments", + "legacy_two_segments": "legacy_two_segments", + "legacy_github_loop": "legacy_loop", + "legacy loop": "legacy_loop", + "legacy_loop": "legacy_loop", "two_segments_normal_vram": "two_segments_normal_vram", + "three_segments_normal_vram": "three_segments_normal_vram", + "3_segments_normal_vram": "three_segments_normal_vram", "loop_normal_vram": "loop_normal_vram", "loop_low_ram_disk": "loop_low_ram_disk", } @@ -308,9 +550,11 @@ def _normalize_modular_decode_mode(mode, backend_mode=None): "low_ram_disk": "low_ram_disk", "very_low_ram": "very_low_ram_disk", "very_low_ram_disk": "very_low_ram_disk", - "normal": "normal_tiled_vhs_ready", - "normal_tiled_vhs": "normal_tiled_vhs_ready", - "normal_tiled_vhs_ready": "normal_tiled_vhs_ready", + "normal": "normal_tiled_vhs", + "normal_tiled": "normal_tiled_vhs", + "normal_tiled_iamccs": "normal_tiled_vhs", + "normal_tiled_vhs": "normal_tiled_vhs", + "normal_tiled_vhs_ready": "normal_tiled_vhs", "high": "high_vram_direct", "high_vram": "high_vram_direct", "high_vram_direct": "high_vram_direct", @@ -321,38 +565,45 @@ def _normalize_modular_decode_mode(mode, backend_mode=None): return normalized if _normalize_backend_mode(backend_mode) == "loop_low_ram_disk": return "low_ram_disk" - return "normal_tiled_vhs_ready" + return "normal_tiled_vhs" def _modular_decode_to_vae_mode(modular_decode): mapping = { - "inherit_from_backend": "normal_tiled", + "inherit_from_backend": "normal_tiled_vhs", "low_ram": "low_ram_disk", "low_ram_disk": "low_ram_disk", "very_low_ram": "very_low_ram_disk", "very_low_ram_disk": "very_low_ram_disk", - "normal": "normal_tiled", - "normal_tiled_vhs_ready": "normal_tiled", + "normal": "normal_tiled_vhs", + "normal_tiled": "normal_tiled_vhs", + "normal_tiled_iamccs": "normal_tiled_vhs", + "normal_tiled_vhs": "normal_tiled_vhs", + "normal_tiled_vhs_ready": "normal_tiled_vhs", "high": "high_vram", "high_vram_direct": "high_vram", - "custom_mode": "custom_mode", + "custom_mode": "normal_tiled_vhs", } - return mapping.get(str(modular_decode), "low_ram_disk") + return mapping.get(str(modular_decode), "normal_tiled_vhs") def _resolve_generation_type(generation_type, generation_mode, backend_mode, media_mode): - requested = str(generation_type or "aud+img2video_infinite") + requested = str(generation_type or _AUDIO_IMAGE_GENERATION_TYPE) mapping = { + _AUDIO_IMAGE_GENERATION_TYPE: ("img2vid", "auto", "input_audio_img2vid"), "aud+img2video_simple": ("img2vid", "single_best", "input_audio_img2vid"), "aud+img2video_2_segments": ("img2vid", "two_segments_normal_vram", "input_audio_img2vid"), - "aud+img2video_infinite": ("img2vid", "loop_normal_vram", "input_audio_img2vid"), - "text+audio2video": ("t2v", "loop_normal_vram", "input_audio_t2v"), + "aud+img2video_infinite": ("img2vid", "auto", "input_audio_img2vid"), + "text+audio2video": ("t2v", "single_best", "input_audio_t2v"), "img2video": ("img2vid", "single_best", "img2vid_pure"), "text2video": ("t2v", "single_best", "t2v_pure"), } if requested not in mapping: - requested = "aud+img2video_infinite" + requested = _AUDIO_IMAGE_GENERATION_TYPE resolved_generation, resolved_backend, resolved_media = mapping[requested] + requested_backend = _normalize_backend_mode(backend_mode) + if requested == _AUDIO_IMAGE_GENERATION_TYPE and requested_backend in _LEGACY_AUDIO_IMG2VID_BACKENDS: + resolved_backend = requested_backend return requested, resolved_generation, resolved_backend, resolved_media @@ -373,6 +624,7 @@ def _resolve_media_mode(media_mode, generation_mode): "generation_mode": "t2v", "uses_input_audio": True, "generates_audio": False, + "exports_audio": True, } if requested_mode == "input_audio_img2vid": return { @@ -380,6 +632,7 @@ def _resolve_media_mode(media_mode, generation_mode): "generation_mode": "img2vid", "uses_input_audio": True, "generates_audio": False, + "exports_audio": True, } if requested_mode in {"generated_audio_t2v", "t2v_pure"}: return { @@ -387,6 +640,7 @@ def _resolve_media_mode(media_mode, generation_mode): "generation_mode": "t2v", "uses_input_audio": False, "generates_audio": True, + "exports_audio": True, } if requested_mode in {"generated_audio_img2vid", "img2vid_pure"}: return { @@ -394,6 +648,7 @@ def _resolve_media_mode(media_mode, generation_mode): "generation_mode": "img2vid", "uses_input_audio": False, "generates_audio": True, + "exports_audio": True, } return { @@ -401,9 +656,72 @@ def _resolve_media_mode(media_mode, generation_mode): "generation_mode": requested_generation, "uses_input_audio": True, "generates_audio": False, + "exports_audio": True, } +def _linx_models_only(existing_linx): + if not isinstance(existing_linx, dict): + return existing_linx + source_resources = existing_linx.get("resources") or {} + if not isinstance(source_resources, dict): + source_resources = {} + keep_keys = ("model", "clip", "vae", "audio_vae", "second_stage_model") + resources = { + key: source_resources.get(key) + for key in keep_keys + if source_resources.get(key) is not None + } + linx_payload = { + "type": SUPERNODE_LINX_TYPE, + "inheritance_scope": "models_only_no_audio", + } + if resources: + linx_payload["resources"] = resources + linx_payload["resource_keys"] = sorted(resources.keys()) + source_map = existing_linx.get("resource_sources") or {} + if isinstance(source_map, dict): + linx_payload["resource_sources"] = { + key: source_map.get(key, "upstream_models_only") + for key in resources.keys() + } + return linx_payload + + +def _pure_media_no_external_audio(media_mode, generation_type): + return str(media_mode or "") in {"img2vid_pure", "t2v_pure"} or str(generation_type or "") in {"img2video", "text2video"} + + +def _ltx_compatible_frame_count(frame_count, round_mode="up"): + target = max(1, int(math.ceil(float(frame_count or 1)))) + if target <= 1: + return 1 + down = max(1, ((target - 1) // 8) * 8 + 1) + up = max(1, int(math.ceil(max(0, target - 1) / 8.0)) * 8 + 1) + mode = str(round_mode or "up").strip().lower() + if mode == "down": + return down + if mode == "nearest": + return down if abs(target - down) <= abs(up - target) else up + return up + + +def _generated_duration_plan_payload(duration_seconds, fps): + duration = max(0.1, float(duration_seconds or 10.0)) + fps_value = max(0.001, float(fps or _DEFAULT_GENERATED_FPS)) + requested_frames = max(1, int(math.ceil(duration * fps_value))) + total_frames = _ltx_compatible_frame_count(requested_frames, "nearest") + rounded_duration = float(total_frames) / fps_value + return ( + f"pipeline_kind=au_img2vid_exec; total_duration_seconds={rounded_duration:.6f}; fps={fps_value:.6f}; " + f"segment_seconds={rounded_duration:.6f}; requested_duration_seconds={duration:.6f}; " + f"planning_mode=manual_segment_seconds; segment_preset=generated_reference; " + f"content_profile=generated_reference; overlap_frames=9; ltx_round_mode=nearest; total_frames={int(total_frames)}; " + f"target_frame_count={int(total_frames)}; target_frame_count_source=render_generated_duration; " + f"requested_frames={int(requested_frames)}; generated_media_duration=true; generated_media_ltx_rounded=true" + ) + + def _motion_guidance_strength(base_strength, motion_intensity): base_value = max(0.0, float(base_strength)) intensity_value = max(0.01, float(motion_intensity or 1.0)) @@ -414,18 +732,40 @@ def _resolve_backend_route(requested_backend_mode, planner_segment_count, modula backend_mode = _normalize_backend_mode(requested_backend_mode) planner_segment_count = max(1, int(planner_segment_count or 1)) modular_decode = _normalize_modular_decode_mode(modular_decode, backend_mode) + decode_is_disk = modular_decode in {"low_ram_disk", "very_low_ram_disk"} + + if backend_mode == "legacy_single": + return backend_mode, 1, True, False, modular_decode + if backend_mode == "legacy_two_segments": + return backend_mode, 2, False, not decode_is_disk, modular_decode + if backend_mode == "legacy_loop": + return backend_mode, max(2, planner_segment_count), False, not decode_is_disk, modular_decode if backend_mode == "single_best": return backend_mode, 1, True, False, modular_decode if backend_mode == "two_segments_normal_vram": - return backend_mode, 2, False, True, "normal_tiled_vhs_ready" + if decode_is_disk: + return "loop_low_ram_disk", 2, False, False, modular_decode + return backend_mode, 2, False, True, modular_decode + if backend_mode == "three_segments_normal_vram": + if decode_is_disk: + return "loop_low_ram_disk", 3, False, False, modular_decode + return backend_mode, 3, False, True, modular_decode if backend_mode == "loop_normal_vram": - return backend_mode, max(2, planner_segment_count), False, True, "normal_tiled_vhs_ready" + if decode_is_disk: + return "loop_low_ram_disk", max(2, planner_segment_count), False, False, modular_decode + return backend_mode, max(2, planner_segment_count), False, True, modular_decode if backend_mode == "loop_low_ram_disk": - return backend_mode, max(2, planner_segment_count), False, False, "low_ram_disk" - use_single_best = planner_segment_count <= 1 - use_in_memory_loop = (not use_single_best) and modular_decode != "low_ram_disk" - resolved_backend = "single_best" if use_single_best else ("loop_normal_vram" if use_in_memory_loop else "loop_low_ram_disk") - return resolved_backend, planner_segment_count, use_single_best, use_in_memory_loop, modular_decode + return backend_mode, max(2, planner_segment_count), False, False, modular_decode if decode_is_disk else "low_ram_disk" + + if planner_segment_count <= 1: + return "single_best", 1, True, False, modular_decode + if decode_is_disk: + return "loop_low_ram_disk", planner_segment_count, False, False, modular_decode + if planner_segment_count == 2: + return "two_segments_normal_vram", 2, False, True, modular_decode + if planner_segment_count == 3: + return "three_segments_normal_vram", 3, False, True, modular_decode + return "loop_normal_vram", planner_segment_count, False, True, modular_decode def _render_status(duration_seconds, planner_data, modular_decode, continuity_mode, anti_drift_mode, second_stage_enabled, audio_concat_enabled): @@ -449,7 +789,7 @@ def _downstream_stage_hints(stage_mode): def _manual_sigmas(sigmas_text): values = [] - for item in str(sigmas_text or "").split(","): + for item in str(sigmas_text or "").replace("\n", ",").split(","): item = item.strip() if not item: continue @@ -535,14 +875,38 @@ def _compute_audio_duration_frames_with_tail(audio, fps): return max(1, int((float(seconds) * fps_value) + 1.0)), "waveform_plus_tail" +def _extract_melband_vocals(audio, melband_model_name): + if audio is None: + raise ValueError("MelBand vocals extraction requires an audio input") + model_name = _resolve_melband_model_name(melband_model_name) + loader = _node_class("MelBandRoFormerModelLoader")() + melband_model = _invoke_node( + loader, + ("execute", "load", "load_model", "loadmodel"), + positional_variants=[(model_name,)], + keyword_variants=[{"model_name": model_name}, {"model": model_name}], + )[0] + sampler = _node_class("MelBandRoFormerSampler")() + vocals = _invoke_node( + sampler, + ("execute", "sample", "process"), + positional_variants=[(melband_model, audio)], + keyword_variants=[{"model": melband_model, "audio": audio}], + )[0] + return vocals, model_name + + def _prepare_planner_audio(audio, audio_preprocess_mode, melband_model_name): mode = _normalize_audio_preprocess_mode(audio_preprocess_mode) - model_name = str(melband_model_name or "MelBandRoformer_fp32.safetensors") + model_name = _resolve_melband_model_name(melband_model_name) + model_resolution_report = "" raw_seconds, raw_source = _compute_audio_duration_seconds(audio) result = { "raw_audio": audio, "conditioning_audio_single": audio, "conditioning_audio_segmented": audio, + "ltx_conditioning_audio": audio, + "ltx_conditioning_source": "raw_audio", "duration_audio": audio, "duration_seconds": raw_seconds, "duration_source": raw_source, @@ -561,30 +925,20 @@ def _prepare_planner_audio(audio, audio_preprocess_mode, melband_model_name): return result try: - loader = _node_class("MelBandRoFormerModelLoader")() - melband_model = _invoke_node( - loader, - ("execute", "load", "load_model", "loadmodel"), - positional_variants=[(model_name,)], - keyword_variants=[{"model_name": model_name}, {"model": model_name}], - )[0] - sampler = _node_class("MelBandRoFormerSampler")() - vocals = _invoke_node( - sampler, - ("execute", "sample", "process"), - positional_variants=[(melband_model, audio)], - keyword_variants=[{"model": melband_model, "audio": audio}], - )[0] + vocals, model_name = _extract_melband_vocals(audio, model_name) duration_seconds, duration_source = _compute_audio_duration_seconds(vocals) result.update({ "conditioning_audio_single": vocals, "conditioning_duration_audio": vocals, "conditioning_duration_seconds": duration_seconds, "conditioning_duration_source": duration_source, + "ltx_conditioning_audio": audio, + "ltx_conditioning_source": "raw_audio_with_melband_duration_math", "preprocess_report": ( - f"audio_preprocess=melband_vocals_duration_math | model={model_name} | " + f"audio_preprocess=melband_vocals_duration_math | model={model_name}{model_resolution_report} | " f"global_duration_source={raw_source} | conditioning_duration_source={duration_source} | " - f"global_duration_seconds={raw_seconds:.6f} | conditioning_duration_seconds={duration_seconds:.6f}" + f"global_duration_seconds={raw_seconds:.6f} | conditioning_duration_seconds={duration_seconds:.6f} | " + "ltx_audio_vae_source=raw_audio" ), "melband_enabled": True, }) @@ -616,23 +970,127 @@ def _scheduler_sigmas(model, scheduler_name="simple", steps=8, denoise=1.0): return _manual_sigmas(_REFERENCE_MANUAL_SIGMAS) -def _decode_images_in_memory(video_latent, vae, tile_size=512, overlap=64): +def _ltx_scheduler_sigmas(steps, max_shift, base_shift, sigma_terminal, latent): + return LTXVScheduler.execute( + max(1, int(steps or 20)), + float(max_shift), + float(base_shift), + True, + float(sigma_terminal), + latent, + )[0] + + +def _is_single_best_reference_audio_img2vid(generation_type): + return str(generation_type or "") in {_AUDIO_IMAGE_GENERATION_TYPE, *_AUDIO_IMAGE_LEGACY_GENERATION_TYPES} + + +def _is_text_audio2video(generation_type): + return str(generation_type or "") == "text+audio2video" + + +def _effective_sampler_name(sampler_name, generation_mode, media_mode, generation_type=None): + # Regression guard: frontend sampler widgets are the source of truth. + # Generation-type defaults belong in the UI; the backend must not silently + # remap t2v/i2v sampler choices after the user changes them. + return str(sampler_name or "euler") + + +def _effective_cfg(cfg, generation_mode, media_mode): + return float(cfg) + + +def _effective_ltx_steps(steps, generation_mode, media_mode): + return max(1, int(steps or 8)) + + +def _main_sigmas( + uses_input_audio, + steps, + max_shift, + base_shift, + sigma_terminal, + latent, + generation_mode, + media_mode, + manual_sigmas, + generation_type=None, + scheduler_model=None, +): + if uses_input_audio: + if _is_single_best_reference_audio_img2vid(generation_type) and scheduler_model is not None: + effective_steps = max(1, int(steps or 8)) + sigmas = _scheduler_sigmas(scheduler_model, "simple", effective_steps, 1.0) + return sigmas, f"basic_scheduler(simple,{effective_steps} steps,denoise=1.0)" + sigmas = _manual_sigmas(manual_sigmas) + return sigmas, f"manual({int(sigmas.numel())} values)" + effective_steps = _effective_ltx_steps(steps, generation_mode, media_mode) + sigmas = _ltx_scheduler_sigmas(effective_steps, max_shift, base_shift, sigma_terminal, latent) + return sigmas, f"ltx_scheduler({int(sigmas.numel()) - 1} steps,terminal={float(sigma_terminal):.3f})" + + +def _decode_images_in_memory( + video_latent, + vae, + tile_size=512, + overlap=64, + temporal_size=256, + temporal_overlap=32, + cleanup_before_decode=False, + tiling_mode="manual", +): decode_node = _node_class("IAMCCS_VAEDecodeTiledSafe")() return decode_node.decode( video_latent, vae, True, - "auto", + str(tiling_mode or "manual"), int(tile_size), int(overlap), - 256, - 32, - False, + int(temporal_size), + int(temporal_overlap), + bool(cleanup_before_decode), False, 0, )[0] +def _decode_images_for_vae_mode( + video_latent, + vae, + resolved_decode_mode, + tile_size=512, + overlap=64, + temporal_size=256, + temporal_overlap=32, + cleanup_before_decode=False, +): + mode = str(resolved_decode_mode or "normal_tiled_vhs") + if mode in {"normal_tiled_vhs", "custom_mode"}: + images = _decode_images_in_memory( + video_latent, + vae, + int(tile_size), + int(overlap), + int(temporal_size), + int(temporal_overlap), + bool(cleanup_before_decode), + "manual", + ) + return images, ( + "decode_node=IAMCCS_VAEDecodeTiledSafe | " + f"mode=normal_tiled_vhs | tiling_mode=manual | tile_size={int(tile_size)} | overlap={int(overlap)} | " + f"temporal_size={int(temporal_size)} | temporal_overlap={int(temporal_overlap)} | " + f"cleanup_before_decode={'on' if bool(cleanup_before_decode) else 'off'}" + ) + if mode == "high_vram": + if bool(cleanup_before_decode): + _soft_cleanup() + images = comfy_nodes.VAEDecode().decode(vae, video_latent)[0] + return images, f"decode_node=VAEDecode | mode=high_vram | cleanup_before_decode={'on' if bool(cleanup_before_decode) else 'off'}" + raise ValueError(f"Unsupported in-memory VAE decode mode: {mode}") + + def _load_images_from_dir_for_output(frames_dir): import numpy as np from PIL import Image @@ -658,6 +1116,55 @@ def _load_images_from_dir_for_output(frames_dir): return torch.stack(images, dim=0) +def _coerce_frame_count(value, default=0): + try: + count = int(float(value)) + except Exception: + return int(default) + return count if count > 0 else int(default) + + +def _resolve_linx_frame_target(linx): + if bool(linx_output(linx, "disable_vae_frame_align", False) or linx_resource(linx, "disable_vae_frame_align", False)): + return 0, "disabled_by_render" + for key in ( + "vae_target_frame_count", + "render_target_frame_count", + "target_frame_count", + "render_frame_count", + "audio_duration_frames_with_tail", + "total_frames", + ): + count = _coerce_frame_count(linx_output(linx, key, None), 0) + if count > 0: + return count, f"linx_output:{key}" + count = _coerce_frame_count(linx_resource(linx, key, None), 0) + if count > 0: + return count, f"linx_resource:{key}" + payload = str(linx_resource(linx, "planner_payload", "") or linx_output(linx, "plan_payload", "") or "") + payload_data = _parse_payload(payload) + for key in ("target_frame_count", "audio_duration_frames_with_tail", "total_frames"): + count = _to_int(payload_data, key, 0) + if count > 0: + return count, f"planner_payload:{key}" + return 0, "none" + + +def _align_images_to_frame_count(images, target_frame_count): + target = _coerce_frame_count(target_frame_count, 0) + if images is None or target <= 0: + return images, "frame_align=off" + current = int(images.shape[0]) + if current == target: + return images, f"frame_align=matched target={target}" + if current <= 0: + return images, f"frame_align=skipped_empty target={target}" + if current < target: + pad = images[-1:].repeat(target - current, 1, 1, 1) + return torch.cat([images, pad], dim=0), f"frame_align=padded {current}->{target}" + return images[:target], f"frame_align=trimmed {current}->{target}" + + def _resize_image_to(image, width, height): if image is None: raise ValueError("image is required for resize") @@ -665,6 +1172,96 @@ def _resize_image_to(image, width, height): return resized.movedim(1, -1) +def _round_up_to_multiple(value, divisor): + value = int(value) + divisor = int(divisor) + if divisor <= 1: + return max(1, value) + return max(divisor, ((max(1, value) + divisor - 1) // divisor) * divisor) + + +def _round_down_to_multiple(value, divisor): + value = int(value) + divisor = int(divisor) + if divisor <= 1: + return max(1, value) + rounded = value - (value % divisor) + return max(divisor, rounded) + + +def _resize_image_like_reference_audio_img2vid(image, width, height): + if image is None: + raise ValueError("image is required for reference audio+image resize") + requested_width = int(width) + requested_height = int(height) + try: + resize_cls = comfy_nodes.NODE_CLASS_MAPPINGS.get("ImageResizeKJv2") + if resize_cls is not None: + resized, resized_width, resized_height, _ = resize_cls().resize( + image, + requested_width, + requested_height, + "crop", + "lanczos", + _REFERENCE_AUDIO_IMG2VID_DIVISIBLE_BY, + "0, 0, 0", + "top", + None, + device="cpu", + ) + return ( + resized, + int(resized_width), + int(resized_height), + f"ImageResizeKJv2(requested={requested_width}x{requested_height},actual={int(resized_width)}x{int(resized_height)},method=lanczos,mode=crop,crop_position=top,divisible_by={_REFERENCE_AUDIO_IMG2VID_DIVISIBLE_BY})", + ) + except Exception as exc: + fallback_reason = f"fallback_after={type(exc).__name__}:{exc}" + else: + fallback_reason = "fallback_after=ImageResizeKJv2_missing" + + target_width = _round_down_to_multiple(requested_width, _REFERENCE_AUDIO_IMG2VID_DIVISIBLE_BY) + target_height = _round_down_to_multiple(requested_height, _REFERENCE_AUDIO_IMG2VID_DIVISIBLE_BY) + old_height = int(image.shape[-3]) + old_width = int(image.shape[-2]) + old_aspect = float(old_width) / float(max(1, old_height)) + new_aspect = float(target_width) / float(max(1, target_height)) + if old_aspect > new_aspect: + crop_width = max(1, round(old_height * new_aspect)) + crop_height = old_height + crop_x = max(0, (old_width - crop_width) // 2) + crop_y = 0 + else: + crop_width = old_width + crop_height = max(1, round(old_width / new_aspect)) + crop_x = 0 + crop_y = 0 + cropped = image.narrow(-2, crop_x, crop_width).narrow(-3, crop_y, crop_height) + resized = comfy.utils.common_upscale(cropped.movedim(-1, 1), target_width, target_height, "lanczos", "disabled") + return ( + resized.movedim(1, -1), + int(target_width), + int(target_height), + f"manual_reference_resize(requested={requested_width}x{requested_height},actual={target_width}x{target_height},method=lanczos,mode=crop,crop_position=top,divisible_by={_REFERENCE_AUDIO_IMG2VID_DIVISIBLE_BY},{fallback_reason})", + ) + + +def _normalize_reference_audio_img2vid_requested_resolution(width, height): + requested_width = int(width) + requested_height = int(height) + # Code by Carmine Cristallo Scalzi AI research (IAMCCS) + # patreon.com/IAMCCS + # + # Regression guard for old SuperNode saves: 1280x736 was the generic LTX + # default, but the working A+I2V reference workflow feeds ImageResizeKJv2 + # with 1280x720. KJ then applies its own divisible_by=32 down-rounding. + if requested_width == _REFERENCE_AUDIO_IMG2VID_WIDTH and requested_height == 736: + return _REFERENCE_AUDIO_IMG2VID_WIDTH, _REFERENCE_AUDIO_IMG2VID_HEIGHT, "stale_supernode_1280x736_normalized_to_reference_1280x720" + if requested_width == _STALE_AUDIO_IMG2VID_WIDTH and requested_height == _STALE_AUDIO_IMG2VID_HEIGHT: + return _REFERENCE_AUDIO_IMG2VID_WIDTH, _REFERENCE_AUDIO_IMG2VID_HEIGHT, "stale_supernode_768x512_normalized_to_reference_1280x720" + return requested_width, requested_height, "" + + def _pixel_dims_from_latent(latent, vae): samples = latent["samples"] _, _, _, latent_h, latent_w = samples.shape @@ -716,6 +1313,32 @@ def _frame_files_in_dir(path): return names +def _align_frame_dir_to_frame_count(path, target_frame_count, prefix="frame"): + target = _coerce_frame_count(target_frame_count, 0) + files = _frame_files_in_dir(path) + current = len(files) + if target <= 0: + return current, "frame_dir_align=off" + if current == target: + return current, f"frame_dir_align=matched target={target}" + if current <= 0: + return current, f"frame_dir_align=skipped_empty target={target}" + if current > target: + for file_path in files[target:]: + try: + os.remove(file_path) + except Exception: + pass + return target, f"frame_dir_align=trimmed {current}->{target}" + + last_path = files[-1] + ext = os.path.splitext(last_path)[1] or ".png" + for index in range(current, target): + dst = os.path.join(str(path), f"{prefix}_{index:06d}{ext}") + shutil.copy2(last_path, dst) + return target, f"frame_dir_align=padded {current}->{target}" + + def _json_safe_metadata(value): if value is None or isinstance(value, (str, int, float, bool)): return value @@ -726,6 +1349,13 @@ def _json_safe_metadata(value): return str(value) +def _prompt_excerpt(text, limit=180): + cleaned = " ".join(str(text or "").split()) + if len(cleaned) <= int(limit): + return cleaned + return cleaned[: max(0, int(limit) - 3)] + "..." + + def _save_video_metadata_sidecar(video_path, metadata): if not str(video_path or "").strip() or not metadata: return "" @@ -828,6 +1458,27 @@ def _stage2_payload_from_exec_widgets( ) +def _apply_reference_stage2_defaults( + generation_mode, + media_mode, + second_stage_mode, + second_stage_scale_mode, + second_stage_reinject_strength, + second_stage_cfg, + second_stage_manual_sigmas, + second_stage_step_count, +): + return ( + second_stage_mode, + second_stage_scale_mode, + second_stage_reinject_strength, + second_stage_cfg, + second_stage_manual_sigmas, + second_stage_step_count, + "", + ) + + def _continuity_mode_from_payload(continuity_payload, fallback_mode, fallback_interval, fallback_strength): data = _parse_payload(continuity_payload) if not data: @@ -893,6 +1544,40 @@ def _clone_latent(latent): return cloned +def _apply_iamccs_audio_latent_zero_mask(audio_latent): + audio_samples = audio_latent.get("samples") if isinstance(audio_latent, dict) else None + if audio_samples is None: + raise ValueError("audio latent missing samples for IAMCCS audio+image mask") + audio_zero_mask = torch.zeros_like(audio_samples, dtype=torch.float32, device=audio_samples.device) + masked_audio_latent = audio_latent.copy() + masked_audio_latent["noise_mask"] = audio_zero_mask + return masked_audio_latent, audio_zero_mask + + +def _apply_workflow_solid_mask_to_audio_latent(audio_latent, width, height): + # Match the working IAMCCS A+I2V workflows: ImageResizeKJv2 runtime + # dimensions feed SolidMask, then SolidMask feeds SetLatentNoiseMask. + mask_width = max(1, int(width)) + mask_height = max(1, int(height)) + audio_mask = SolidMask.execute(0.0, mask_width, mask_height)[0] + masked_audio_latent = comfy_nodes.SetLatentNoiseMask().set_mask(audio_latent, audio_mask)[0] + return masked_audio_latent, audio_mask, mask_width, mask_height + + +def _apply_legacy_backend_mask_to_audio_latent(audio_latent): + audio_mask = SolidMask.execute(0.0, 1024, 1024)[0] + masked_audio_latent = comfy_nodes.SetLatentNoiseMask().set_mask(audio_latent, audio_mask)[0] + return masked_audio_latent, audio_mask + + +def _is_legacy_audio_img2vid_backend(backend_mode): + return _normalize_backend_mode(backend_mode) in _LEGACY_AUDIO_IMG2VID_BACKENDS + + +def _is_legacy_single_audio_img2vid_backend(backend_mode): + return _normalize_backend_mode(backend_mode) == "legacy_single" + + def _latent_time_scale_factor(vae): try: scale = int(getattr(vae, "downscale_index_formula", (8, 1, 1))[0]) @@ -1001,14 +1686,19 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: return { "required": { "audio": ("AUDIO",), - "fps": ("FLOAT", {"default": 24.0, "min": 0.001, "max": 240.0, "step": 0.01}), - "segment_seconds": ("FLOAT", {"default": 10.0, "min": 0.01, "max": 3600.0, "step": 0.01}), + "fps": ("FLOAT", {"default": 25.0, "min": 0.001, "max": 240.0, "step": 0.01}), + "segment_seconds": ([5.0, 10.0, 15.0, 20.0, 30.0, 60.0], {"default": 10.0}), "planning_mode": (["manual_segment_seconds", "explicit_preset_seconds"], {"default": "manual_segment_seconds"}), - "segment_preset": (["5sec", "10sec", "15sec", "20sec"], {"default": "15sec"}), + "segment_preset": (["5sec", "10sec", "15sec", "20sec"], {"default": "10sec"}), "overlap_frames": ("INT", {"default": 9, "min": 0, "max": 4096, "step": 1}), "ltx_round_mode": (["up", "nearest", "down"], {"default": "up"}), "audio_preprocess_mode": (_PLANNER_AUDIO_MODES, {"default": "melband_vocals_duration_math"}), "melband_model_name": (_MELBAND_MODEL_NAMES, {"default": _MELBAND_MODEL_NAMES[0] if _MELBAND_MODEL_NAMES else "MelBandRoformer_fp32.safetensors"}), + "audio_img2vid_backend": (_PLANNER_AUDIO_IMG2VID_BACKENDS, {"default": "modern pipeline"}), + "audio_img2vid_mode": (_PLANNER_AUDIO_IMG2VID_MODES, {"default": "single generation"}), + "route_mode": (_PLANNER_ROUTE_MODES, {"default": "choose segment count (audio / segments)"}), + "segment_count": ([1, 2, 3, 4, 5, 6, 8, 10, 12], {"default": 2}), + "single_duration_seconds": ([5.0, 10.0, 15.0, 20.0, 30.0, 60.0], {"default": 10.0}), } , "optional": { @@ -1018,20 +1708,116 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: "audio_vae": ("VAE",), "linx": (SUPERNODE_LINX_TYPE,), "audio_concat_payload": ("STRING",), + "debug_verbose": ("BOOLEAN", {"default": False}), } } - def plan(self, audio, fps, segment_seconds, planning_mode, segment_preset, overlap_frames, ltx_round_mode, audio_preprocess_mode, melband_model_name, model=None, clip=None, vae=None, audio_vae=None, linx=None, audio_concat_payload=""): + def plan(self, audio, fps, segment_seconds, planning_mode, segment_preset, overlap_frames, ltx_round_mode, audio_preprocess_mode, melband_model_name, audio_img2vid_backend="modern", audio_img2vid_mode="single", route_mode="choose segment count (audio / segments)", segment_count=2, single_duration_seconds=10.0, debug_verbose=False, model=None, clip=None, vae=None, audio_vae=None, linx=None, audio_concat_payload=""): + debug_verbose = _debug_verbose_enabled(debug_verbose, linx) + pipeline_debug = _pipeline_debug_new("exec_planner", debug_verbose) + _pipeline_debug_step( + pipeline_debug, + "input_received", + audio=audio, + fps=fps, + segment_seconds=segment_seconds, + planning_mode=planning_mode, + segment_preset=segment_preset, + overlap_frames=overlap_frames, + ltx_round_mode=ltx_round_mode, + audio_preprocess_mode=audio_preprocess_mode, + melband_model_name=melband_model_name, + audio_img2vid_backend=audio_img2vid_backend, + audio_img2vid_mode=audio_img2vid_mode, + route_mode=route_mode, + segment_count=segment_count, + single_duration_seconds=single_duration_seconds, + linx_keys=",".join(str(k) for k in ((linx or {}).get("resource_keys") or [])), + ) planning_mode = _normalize_planner_mode(planning_mode) segment_preset = _normalize_segment_preset(segment_preset) audio_preprocess_mode = _normalize_audio_preprocess_mode(audio_preprocess_mode) + audio_img2vid_backend = _normalize_audio_img2vid_backend_choice(audio_img2vid_backend) + audio_img2vid_mode = _normalize_audio_img2vid_mode_choice(audio_img2vid_mode, route_mode, segment_count) + audio_img2vid_render_backend = _audio_img2vid_backend_from_planner(audio_img2vid_backend, audio_img2vid_mode) + _pipeline_debug_step( + pipeline_debug, + "planner_widgets_normalized", + planning_mode=planning_mode, + segment_preset=segment_preset, + audio_preprocess_mode=audio_preprocess_mode, + audio_img2vid_backend=audio_img2vid_backend, + audio_img2vid_mode=audio_img2vid_mode, + audio_img2vid_render_backend=audio_img2vid_render_backend, + ) audio_plan = _prepare_planner_audio(audio, audio_preprocess_mode, melband_model_name) + _pipeline_debug_step( + pipeline_debug, + "audio_preprocessed", + raw_audio=audio_plan["raw_audio"], + conditioning_audio_single=audio_plan["conditioning_audio_single"], + conditioning_audio_segmented=audio_plan["conditioning_audio_segmented"], + ltx_conditioning_audio=audio_plan["ltx_conditioning_audio"], + ltx_conditioning_source=audio_plan["ltx_conditioning_source"], + duration_audio=audio_plan["duration_audio"], + duration_seconds=audio_plan["duration_seconds"], + melband_enabled=audio_plan["melband_enabled"], + preprocess_report=audio_plan["preprocess_report"], + ) audio_duration_frames_with_tail, audio_duration_frames_source = _compute_audio_duration_frames_with_tail(audio_plan["duration_audio"], fps) audio_plan["duration_frames_with_tail"] = int(audio_duration_frames_with_tail) audio_plan["duration_frames_source"] = str(audio_duration_frames_source) duration_seconds = _duration_hint_from_payload(audio_concat_payload) + _pipeline_debug_step( + pipeline_debug, + "duration_resolved", + audio_concat_payload_present=bool(audio_concat_payload), + audio_concat_duration=duration_seconds, + audio_duration_seconds=audio_plan["duration_seconds"], + audio_duration_frames_with_tail=audio_duration_frames_with_tail, + audio_duration_frames_source=audio_duration_frames_source, + ) if duration_seconds is None: duration_seconds = float(audio_plan["duration_seconds"]) + + if audio_img2vid_mode == "single": + route_mode = "single generation (duration only)" + segment_count = 1 + else: + route_mode = "choose segment count (audio / segments)" + if audio_img2vid_mode == "2_segments": + segment_count = 2 + elif audio_img2vid_mode == "3_segments": + segment_count = 3 + else: + segment_count = max(4, int(segment_count or 4)) + + route_key = _normalize_planner_route_mode(route_mode) + auto_report = "" + if route_key == "single_generation": + duration_seconds = max(0.1, float(single_duration_seconds or segment_seconds or 10.0)) + segment_seconds = duration_seconds + planning_mode = "manual_segment_seconds" + segment_preset = _segment_preset_from_seconds(segment_seconds) + else: + auto_planner = _node_class("IAMCCS_AudioSegmentAutoPlanner")() + split_mode = "choose seconds per segment (auto count)" if route_key == "fixed_segment_seconds" else "choose segment count (audio / segments)" + auto_planned = auto_planner.plan_audio_segments( + audio_plan["duration_audio"], + fps, + split_mode, + segment_count, + segment_seconds, + overlap_frames, + ltx_round_mode, + ) + duration_seconds = float(auto_planned[0]) + segment_seconds = float(auto_planned[1]) + planning_mode = str(auto_planned[2]) + segment_preset = str(auto_planned[3]) + overlap_frames = int(auto_planned[4]) + auto_report = str(auto_planned[12]) + planner = _node_class("IAMCCS_SegmentPlanner")() planned = planner.plan( duration_seconds, @@ -1043,10 +1829,30 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: ltx_round_mode, 0, ) + target_frame_count = int(planned[0]) if route_key == "single_generation" else int(audio_duration_frames_with_tail) + target_frame_count_source = "planner_single_duration" if route_key == "single_generation" else "planner_audio_duration_with_tail" + _pipeline_debug_step( + pipeline_debug, + "segment_plan_created", + duration_seconds=duration_seconds, + total_frames=int(planned[0]), + segment_count=int(planned[4]), + first_segment_raw_frames=int(planned[2]), + continuation_raw_frames=int(planned[3]), + recommended_overlap_frames=int(planned[18]), + recommended_audio_left_context_s=float(planned[19]), + recommended_extension_preset=planned[20], + route_mode=route_mode, + route_key=route_key, + auto_planner_report=auto_report, + ) payload = ( f"pipeline_kind=au_img2vid_exec; total_duration_seconds={duration_seconds:.6f}; fps={float(fps):.6f}; " f"segment_seconds={float(segment_seconds):.6f}; planning_mode={planning_mode}; segment_preset={segment_preset}; content_profile={segment_preset}; " f"audio_duration_frames_with_tail={int(audio_duration_frames_with_tail)}; audio_duration_frames_source={audio_duration_frames_source}; " + f"target_frame_count={int(target_frame_count)}; target_frame_count_source={target_frame_count_source}; " + f"audio_img2vid_backend_family={audio_img2vid_backend}; audio_img2vid_mode={audio_img2vid_mode}; " + f"audio_img2vid_render_backend={audio_img2vid_render_backend}; " f"overlap_frames={int(overlap_frames)}; ltx_round_mode={ltx_round_mode}; total_frames={int(planned[0])}; " f"unique_segment_frames={int(planned[1])}; first_segment_raw_frames={int(planned[2])}; continuation_raw_frames={int(planned[3])}; " f"segment_count={int(planned[4])}; continuation_loops={int(planned[5])}; last_segment_unique_frames={int(planned[6])}; " @@ -1054,14 +1860,17 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: f"recommended_extension_preset={planned[20]}" ) planner_chip = _planner_chip(duration_seconds, planned, planning_mode, segment_preset) + planner_debug_report = _pipeline_debug_text(pipeline_debug) report = ( f"Executable planner. duration={duration_seconds:.3f}s @ {float(fps):.3f}fps | " f"segments={int(planned[4])} | first_raw={int(planned[2])}f | continuation_raw={int(planned[3])}f | " f"recommended_overlap={int(planned[18])}f | left_context={float(planned[19]):.3f}s | " f"audio_frames_with_tail={int(audio_duration_frames_with_tail)}f ({audio_duration_frames_source}) | " f"audio_preprocess_mode={audio_preprocess_mode} | melband_model={melband_model_name} | " + f"a2i_backend={audio_img2vid_backend}/{audio_img2vid_mode}->{audio_img2vid_render_backend} | " f"melband_enabled={bool(audio_plan['melband_enabled'])} | {audio_plan['preprocess_report']}" ) + report = _append_debug_report(report, planner_debug_report) linx = build_stage_linx_payload( linx, "exec_planner", @@ -1075,8 +1884,15 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: "content_profile": str(segment_preset), "recommended_extension_preset": str(planned[20]), "audio_concat_duration_override": float(duration_seconds), + "route_mode": str(route_mode), + "route_key": str(route_key), + "audio_img2vid_backend_family": str(audio_img2vid_backend), + "audio_img2vid_mode": str(audio_img2vid_mode), + "audio_img2vid_render_backend": str(audio_img2vid_render_backend), "audio_preprocess_mode": str(audio_preprocess_mode), "melband_model_name": str(melband_model_name), + "pipeline_debug_steps": len(pipeline_debug.get("lines") or []), + "debug_verbose": bool(debug_verbose), }, report, requires={ @@ -1097,6 +1913,11 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: "plan_payload": payload, "planned_duration_seconds": float(duration_seconds), "total_frames": int(planned[0]), + "target_frame_count": int(target_frame_count), + "target_frame_count_source": str(target_frame_count_source), + "audio_img2vid_backend_family": str(audio_img2vid_backend), + "audio_img2vid_mode": str(audio_img2vid_mode), + "audio_img2vid_render_backend": str(audio_img2vid_render_backend), "audio_duration_frames_with_tail": int(audio_duration_frames_with_tail), "segment_count": int(planned[4]), "first_segment_raw_frames": int(planned[2]), @@ -1104,14 +1925,20 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: "recommended_overlap_frames": int(planned[18]), "recommended_audio_left_context_s": float(planned[19]), "planner_chip": planner_chip, + "pipeline_debug": list(pipeline_debug.get("lines") or []), + "debug_verbose": bool(debug_verbose), }, resources={ - "audio": audio, - "audio_raw": audio_plan["raw_audio"], - "audio_conditioning_single": audio_plan["conditioning_audio_single"], - "audio_conditioning_segmented": audio_plan["conditioning_audio_segmented"], - "audio_duration_source": audio_plan["duration_audio"], - "audio_duration_frames_with_tail": int(audio_duration_frames_with_tail), + "audio": audio, + "audio_raw": audio_plan["raw_audio"], + "audio_conditioning_single": audio_plan["conditioning_audio_single"], + "audio_conditioning_segmented": audio_plan["conditioning_audio_segmented"], + "audio_ltx_conditioning": audio_plan["ltx_conditioning_audio"], + "audio_ltx_conditioning_source": audio_plan["ltx_conditioning_source"], + "audio_duration_source": audio_plan["duration_audio"], + "target_frame_count": int(target_frame_count), + "target_frame_count_source": str(target_frame_count_source), + "audio_duration_frames_with_tail": int(audio_duration_frames_with_tail), "audio_duration_frames_source": str(audio_duration_frames_source), "model": model, "clip": clip, @@ -1119,8 +1946,11 @@ class IAMCCS_GC_AUIMG2VIDExecutablePlanner: "audio_vae": audio_vae, "fps": float(fps), "planner_payload": payload, + "audio_preprocess_mode": str(audio_preprocess_mode), + "melband_model_name": str(melband_model_name), "audio_preprocess_report": audio_plan["preprocess_report"], "melband_enabled": bool(audio_plan["melband_enabled"]), + "debug_verbose": bool(debug_verbose), }, ) return ( @@ -1146,47 +1976,49 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: def INPUT_TYPES(cls): return { "required": { - "image": ("IMAGE",), - "generation_type": (_GENERATION_TYPE_MODES, {"default": "aud+img2video_infinite"}), - "ui_preset": (["custom", "low_ram_safe", "balanced", "high_quality", "fast_preview", "loop_lipsync_safe", "img2vid_generated_audio", "t2v_generated_audio", "img2vid_pure", "t2v_pure", "loop_img2vid_pure_normal_vram", "loop_t2v_pure_normal_vram", "loop_img2vid_pure_low_ram", "motion_controlled"], {"default": "custom"}), + "generation_type": (_GENERATION_TYPE_MODES, {"default": _AUDIO_IMAGE_GENERATION_TYPE}), + "ui_preset": (["custom", "low_ram_safe", "balanced", "high_quality", "fast_preview", "motion_controlled", "loop_lipsync_safe", "img2vid_generated_audio", "t2v_generated_audio", "img2vid_pure", "t2v_pure", "loop_img2vid_pure_normal_vram", "loop_t2v_pure_normal_vram", "loop_img2vid_pure_low_ram"], {"default": "custom"}), "generated_media_duration_seconds": ("FLOAT", {"default": 10.0, "min": 0.1, "max": 120.0, "step": 0.1}), + "generated_media_fps": ("FLOAT", {"default": 25.0, "min": 1.0, "max": 240.0, "step": 0.01}), "generation_mode": (["img2vid", "t2v"], {"default": "img2vid"}), "backend_mode": (_RENDER_BACKEND_MODES, {"default": "auto"}), "positive_text": ("STRING", {"default": "cinematic motion, detailed scene", "multiline": True}), "negative_text": ("STRING", {"default": "blurry, low quality, artifacts", "multiline": True}), - "width": ("INT", {"default": 768, "min": 64, "max": 8192, "step": 32}), - "height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 32}), - "steps": ("INT", {"default": 20, "min": 1, "max": 200, "step": 1}), + "width": ("INT", {"default": 1280, "min": 64, "max": 8192, "step": 32}), + "height": ("INT", {"default": 720, "min": 64, "max": 8192, "step": 32}), + "steps": ("INT", {"default": 8, "min": 1, "max": 200, "step": 1}), "cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1}), - "sampler_name": (_SAMPLER_NAMES, {"default": "euler"}), + "sampler_name": (_SAMPLER_NAMES, {"default": "lcm" if "lcm" in _SAMPLER_NAMES else "euler"}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF, "step": 1}), "max_shift": ("FLOAT", {"default": 2.05, "min": 0.0, "max": 100.0, "step": 0.01}), "base_shift": ("FLOAT", {"default": 0.95, "min": 0.0, "max": 100.0, "step": 0.01}), "sigma_terminal": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 0.99, "step": 0.01}), "manual_sigmas": ("STRING", {"default": _REFERENCE_MANUAL_SIGMAS, "multiline": True}), - "image_strength": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), - "image_compression": ("INT", {"default": 35, "min": 0, "max": 100, "step": 1}), + "image_strength": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01}), + "image_compression": ("INT", {"default": 33, "min": 0, "max": 100, "step": 1}), "audio_context_mode": (["left_context_only", "right_context_only", "symmetric_context", "no_overlap"], {"default": "left_context_only"}), - "audio_left_context_s": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 30.0, "step": 0.01}), + "audio_left_context_s": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 30.0, "step": 0.01}), "audio_right_context_s": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 30.0, "step": 0.01}), "stitch_preset": (["custom", "lossless_refresh_24fps", "lossless_refresh_strong_24fps", "videoclip_audio_24fps", "monologue_audio_24fps", "target_extension_ltx2", "cut_bestofk_16", "cut_bestofk_16_luma", "cut_bestofk_32", "micro_crossfade_3"], {"default": "custom"}), "overlap_side": (["source", "new_images"], {"default": "source"}), "overlap_mode": (["cut", "linear_blend", "ease_in_out", "filmic_crossfade"], {"default": "cut"}), "start_frames_rule": (["none", "ltx2_round_down", "ltx2_nearest"], {"default": "none"}), - "continuity_anchor_mode": (["off", "tail_only", "periodic_tail_only", "periodic_tail_then_source_refresh", "tail_then_source_refresh", "periodic_source_refresh", "always_source_refresh"], {"default": "tail_only"}), + "color_match_mode": (["none", "luma_only", "per_channel"], {"default": "none"}), + "color_match_strength": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.05}), + "continuity_anchor_mode": (["off", "tail_only", "periodic_tail_only", "periodic_tail_then_source_refresh", "tail_then_source_refresh", "periodic_source_refresh", "always_source_refresh"], {"default": "off"}), "anchor_refresh_interval": ("INT", {"default": 2, "min": 1, "max": 128, "step": 1}), - "anchor_image_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01}), + "anchor_image_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "anti_drift_mode": (["off", "rolling_adain", "dual_reference_adain"], {"default": "off"}), - "anti_drift_strength": ("FLOAT", {"default": 0.18, "min": 0.0, "max": 1.0, "step": 0.01}), - "identity_persistence_strength": ("FLOAT", {"default": 0.08, "min": 0.0, "max": 1.0, "step": 0.01}), + "anti_drift_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "identity_persistence_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "vae_mode": (_MODULAR_DECODE_MODES, {"default": "inherit_render_backend"}), "downstream_stage_mode": (["finalize_only", "upscale_ready", "detailer_ready", "upscale_then_detailer"], {"default": "finalize_only"}), "output_root": ("STRING", {"default": "iamccs_gc_auimg2vid/exec_run"}), "segment_overlay_mode": (["off", "segment_label", "custom_text"], {"default": "off"}), "segment_overlay_text": ("STRING", {"default": "seg {segment_number}/{segment_count}", "multiline": True}), "second_stage_mode": (["off", "latent_refine_3step", "latent_upscale_refine_x2_beta"], {"default": "off"}), - "stage2_model_policy": (["stage2_model_if_connected", "keep_stage1_model"], {"default": "stage2_model_if_connected"}), - "second_stage_upscale_model": (_LATENT_UPSCALE_MODEL_NAMES, {"default": _LATENT_UPSCALE_MODEL_NAMES[0] if _LATENT_UPSCALE_MODEL_NAMES else "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"}), + "stage2_model_policy": (["stage2_model_if_connected", "replace_stage1_if_connected", "prefer_stage2_else_primary", "keep_stage1_model"], {"default": "stage2_model_if_connected"}), + "second_stage_upscale_model": (_LATENT_UPSCALE_MODEL_NAMES, {"default": _reference_ltx23_upscale_model_name()}), "second_stage_reinject_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "second_stage_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1}), "second_stage_manual_sigmas": ("STRING", {"default": "0.909375, 0.725, 0.421875, 0.0", "multiline": True}), @@ -1195,22 +2027,80 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "motion_intensity": ("FLOAT", {"default": 1.0, "min": 0.25, "max": 4.0, "step": 0.05}), }, "optional": { - "linx": (SUPERNODE_LINX_TYPE,), - "audio": ("AUDIO",), - "model": ("MODEL",), - "clip": ("CLIP",), - "vae": ("VAE",), - "audio_vae": ("VAE",), + "image": ("IMAGE", {"lazy": True}), + "linx": (SUPERNODE_LINX_TYPE, {"lazy": True}), + "audio": ("AUDIO", {"lazy": True}), + "model": ("MODEL", {"lazy": True}), + "clip": ("CLIP", {"lazy": True}), + "vae": ("VAE", {"lazy": True}), + "audio_vae": ("VAE", {"lazy": True}), "plan_payload": ("STRING",), - "refresh_image": ("IMAGE",), + "refresh_image": ("IMAGE", {"lazy": True}), "second_stage_linx": (SUPERNODE_LINX_TYPE,), "stage2_model": ("MODEL",), + "show_manual_sigmas": ("BOOLEAN", {"default": False}), + "debug_verbose": ("BOOLEAN", {"default": False}), }, "hidden": { "unique_id": "UNIQUE_ID", }, } + def check_lazy_status( + self, + generation_type=_AUDIO_IMAGE_GENERATION_TYPE, + generation_mode="img2vid", + backend_mode="auto", + media_mode="auto_from_generation_mode", + second_stage_mode="off", + image=None, + linx=None, + audio=None, + model=None, + clip=None, + vae=None, + audio_vae=None, + second_stage_linx=None, + stage2_model=None, + **kwargs, + ): + generation_type, generation_mode, backend_mode, media_mode = _resolve_generation_type( + generation_type, + generation_mode, + backend_mode, + media_mode, + ) + media_settings = _resolve_media_mode(media_mode, generation_mode) + needed = [] + + if str(media_settings["generation_mode"]) != "t2v" and image is None: + needed.append("image") + + if bool(media_settings["uses_input_audio"]): + if linx is None: + needed.append("linx") + return needed + for name, value in ( + ("audio", audio), + ("model", model), + ("clip", clip), + ("vae", vae), + ("audio_vae", audio_vae), + ): + if value is None and linx_resource(linx, name, None) is None: + needed.append(name) + else: + for name, value in ( + ("model", model), + ("clip", clip), + ("vae", vae), + ("audio_vae", audio_vae), + ): + if value is None: + needed.append(name) + + return needed + def _planner_settings(self, plan_payload, linx, fps, segment_seconds, planning_mode, segment_preset, overlap_frames, ltx_round_mode): data = _parse_payload(plan_payload) if isinstance(linx, dict): @@ -1227,7 +2117,19 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "segment_preset": _normalize_segment_preset(segment_preset), "overlap_frames": int(overlap_frames), "ltx_round_mode": str(ltx_round_mode), + "audio_img2vid_backend_family": "modern", + "audio_img2vid_mode": "single", + "audio_img2vid_render_backend": "single_best", } + backend_family = _normalize_audio_img2vid_backend_choice(_to_text(data, "audio_img2vid_backend_family", "modern")) + backend_mode_choice = _normalize_audio_img2vid_mode_choice( + _to_text(data, "audio_img2vid_mode", ""), + _to_text(data, "route_mode", ""), + _to_int(data, "segment_count", 2), + ) + render_backend = _normalize_backend_mode( + _to_text(data, "audio_img2vid_render_backend", _audio_img2vid_backend_from_planner(backend_family, backend_mode_choice)) + ) return { "duration_seconds": _to_float(data, "total_duration_seconds", None), "fps": _to_float(data, "fps", fps), @@ -1236,11 +2138,13 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "segment_preset": _normalize_segment_preset(_to_text(data, "segment_preset", _to_text(data, "content_profile", segment_preset))), "overlap_frames": _to_int(data, "overlap_frames", overlap_frames), "ltx_round_mode": _to_text(data, "ltx_round_mode", ltx_round_mode), + "audio_img2vid_backend_family": backend_family, + "audio_img2vid_mode": backend_mode_choice, + "audio_img2vid_render_backend": render_backend, } def render( self, - image, generation_mode, backend_mode, positive_text, @@ -1264,6 +2168,8 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: overlap_side, overlap_mode, start_frames_rule, + color_match_mode, + color_match_strength, continuity_anchor_mode, anchor_refresh_interval, anchor_image_strength, @@ -1282,11 +2188,15 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: second_stage_cfg, second_stage_manual_sigmas, media_mode, - vram_flush, - motion_intensity, - ui_preset="custom", + vram_flush, + motion_intensity, + show_manual_sigmas=False, + debug_verbose=False, + ui_preset="custom", generated_media_duration_seconds=10.0, - generation_type="aud+img2video_infinite", + generated_media_fps=25.0, + generation_type=_AUDIO_IMAGE_GENERATION_TYPE, + image=None, linx=None, audio=None, model=None, @@ -1299,48 +2209,121 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: stage2_model=None, unique_id=None, ): - model = _require_runtime_value(_input_or_linx(model, linx, "model"), "model") - clip = _require_runtime_value(_input_or_linx(clip, linx, "clip"), "clip") - vae = _require_runtime_value(_input_or_linx(vae, linx, "vae"), "vae") - audio_vae = _require_runtime_value(_input_or_linx(audio_vae, linx, "audio_vae"), "audio_vae") - plan_payload = str(plan_payload or linx_resource(linx, "planner_payload") or linx_output(linx, "plan_payload") or "") - + debug_verbose = _debug_verbose_enabled(debug_verbose, linx) + pipeline_debug = _pipeline_debug_new("exec_render", debug_verbose) + _pipeline_debug_step( + pipeline_debug, + "input_received", + generation_type=generation_type, + generation_mode=generation_mode, + backend_mode=backend_mode, + media_mode=media_mode, + ui_preset=ui_preset, + image_connected=image is not None, + audio_connected=audio is not None, + linx_keys=",".join(str(k) for k in ((linx or {}).get("resource_keys") or [])), + ) generation_type, generation_mode, backend_mode, media_mode = _resolve_generation_type( generation_type, generation_mode, backend_mode, media_mode, ) - - fps_value = float(linx_resource(linx, "fps", 24.0) or 24.0) - input_audio_probe = _input_or_linx(audio, linx, "audio") + _pipeline_debug_step( + pipeline_debug, + "generation_type_resolved", + generation_type=generation_type, + generation_mode=generation_mode, + backend_mode=backend_mode, + media_mode=media_mode, + ) + reference_audio_img2vid = _is_single_best_reference_audio_img2vid(generation_type) + legacy_audio_img2vid_backend = reference_audio_img2vid and _is_legacy_audio_img2vid_backend(backend_mode) requested_media_probe = str(media_mode or "auto_from_generation_mode") media_probe = _resolve_media_mode(requested_media_probe, generation_mode) + input_audio_probe = _input_or_linx(audio, linx, "audio") if requested_media_probe == "auto_from_generation_mode" and input_audio_probe is None: fallback_media_probe = "generated_audio_t2v" if str(generation_mode) == "t2v" else "generated_audio_img2vid" media_probe = _resolve_media_mode(fallback_media_probe, generation_mode) + _pipeline_debug_step( + pipeline_debug, + "media_probe", + requested_media=requested_media_probe, + resolved_media=media_probe.get("mode"), + uses_input_audio=bool(media_probe.get("uses_input_audio")), + generates_audio=bool(media_probe.get("generates_audio")), + fallback_without_audio=(requested_media_probe == "auto_from_generation_mode" and input_audio_probe is None), + ) + + if not bool(media_probe["uses_input_audio"]): + linx = _linx_models_only(linx) + _pipeline_debug_step(pipeline_debug, "linx_pruned_for_non_audio_mode", linx_keys=",".join(str(k) for k in ((linx or {}).get("resource_keys") or []))) + + model = _require_runtime_value(_input_or_linx(model, linx, "model"), "model") + clip = _require_runtime_value(_input_or_linx(clip, linx, "clip"), "clip") + vae = _require_runtime_value(_input_or_linx(vae, linx, "vae"), "vae") + audio_vae = _require_runtime_value(_input_or_linx(audio_vae, linx, "audio_vae"), "audio_vae") + uses_input_audio_probe = bool(media_probe["uses_input_audio"]) + fps_default = _REFERENCE_AUDIO_IMG2VID_FPS if reference_audio_img2vid else (24.0 if uses_input_audio_probe else float(generated_media_fps or 25.0)) + fps_value = float(linx_resource(linx, "fps", fps_default) or fps_default) if uses_input_audio_probe else max(1.0, float(generated_media_fps or fps_default)) + _pipeline_debug_step( + pipeline_debug, + "resources_resolved", + model=model, + clip=clip, + vae=vae, + audio_vae=audio_vae, + fps=fps_value, + fps_source="planner_linx" if uses_input_audio_probe else "render_widget", + ) + plan_payload = "" if not bool(media_probe["uses_input_audio"]) else str(plan_payload or linx_resource(linx, "planner_payload") or linx_output(linx, "plan_payload") or "") if not plan_payload and not bool(media_probe["uses_input_audio"]): - generated_duration = max(0.1, float(generated_media_duration_seconds or 10.0)) - plan_payload = ( - f"pipeline_kind=au_img2vid_exec; total_duration_seconds={generated_duration:.6f}; fps={float(fps_value):.6f}; " - f"segment_seconds={generated_duration:.6f}; planning_mode=manual_segment_seconds; segment_preset=10sec; " - f"content_profile=10sec; overlap_frames=9; ltx_round_mode=up" + plan_payload = _generated_duration_plan_payload(generated_media_duration_seconds, fps_value) + generated_plan_data = _parse_payload(plan_payload) + _pipeline_debug_step( + pipeline_debug, + "generated_duration_plan_created", + duration_seconds=generated_media_duration_seconds, + fps=fps_value, + requested_frames=_to_int(generated_plan_data, "requested_frames", 0), + total_frames=_to_int(generated_plan_data, "total_frames", 0), + ltx_rounded=_to_bool(generated_plan_data, "generated_media_ltx_rounded", False), ) if not plan_payload: + _pipeline_debug_step(pipeline_debug, "error_missing_plan_payload", uses_input_audio=bool(media_probe["uses_input_audio"])) raise ValueError("plan_payload is required via Exec Planner/linx unless Media Mode is generated-audio/pure") + payload_backend_data = _parse_payload(plan_payload) + planner_backend_raw = _to_text(payload_backend_data, "audio_img2vid_render_backend", "").strip() + if reference_audio_img2vid and planner_backend_raw: + backend_mode = _normalize_backend_mode(planner_backend_raw) backend_mode = _normalize_backend_mode(backend_mode) + legacy_audio_img2vid_backend = reference_audio_img2vid and _is_legacy_audio_img2vid_backend(backend_mode) + if reference_audio_img2vid: + _pipeline_debug_step( + pipeline_debug, + "audio_img_contract_loaded", + contract_version=_AUDIO_IMG2VID_CONTRACT_VERSION, + default_resolution=f"{_REFERENCE_AUDIO_IMG2VID_WIDTH}x{_REFERENCE_AUDIO_IMG2VID_HEIGHT}", + resolution_source="frontend_width_height_to_ImageResizeKJv2_actual_outputs", + resize_divisible_by=_REFERENCE_AUDIO_IMG2VID_DIVISIBLE_BY, + examples="1280x720 matches the working workflow input; 1920x1080 follows ImageResizeKJv2 actual width/height", + planner_backend_raw=planner_backend_raw or "none", + audio_mask_contract="legacy_1024_solidmask" if legacy_audio_img2vid_backend else "workflow_solidmask_from_ImageResizeKJv2_width_height", + ) modular_decode = _normalize_modular_decode_mode( _inherit_widget_value(vae_mode, "inherit_render_backend", linx, "decode_mode"), backend_mode, ) + _pipeline_debug_step(pipeline_debug, "decode_policy_resolved", backend_mode=backend_mode, modular_decode=modular_decode, vae_mode_widget=vae_mode) output_root = str(_inherit_widget_value(output_root, "iamccs_gc_auimg2vid/exec_run", linx, "output_root")) audio_concat_payload = str(linx_output(linx, "audio_concat_payload", "") or "") continuity_payload = str(linx_output(linx, "continuity_payload", "") or "") requested_media_mode = str(media_mode or "auto_from_generation_mode") media_settings = _resolve_media_mode(requested_media_mode, generation_mode) - input_audio = _input_or_linx(audio, linx, "audio") - input_audio_source = "direct" if audio is not None else ("linx" if linx_resource(linx, "audio", None) is not None else "missing") + linx_audio = linx_resource(linx, "audio", None) + input_audio = linx_audio if linx_audio is not None else audio + input_audio_source = "planner_linx" if linx_audio is not None else ("direct" if audio is not None else "missing") if requested_media_mode == "auto_from_generation_mode" and input_audio is None: fallback_media_mode = "generated_audio_t2v" if str(generation_mode) == "t2v" else "generated_audio_img2vid" media_settings = _resolve_media_mode(fallback_media_mode, generation_mode) @@ -1348,35 +2331,110 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: effective_media_mode = str(media_settings["mode"]) uses_input_audio = bool(media_settings["uses_input_audio"]) generates_audio = bool(media_settings["generates_audio"]) - audio = _require_runtime_value(input_audio, "audio") if uses_input_audio else input_audio - raw_audio = (_input_or_linx(audio, linx, "audio_raw") or audio) if uses_input_audio else None - single_conditioning_audio = (_input_or_linx(None, linx, "audio_conditioning_single") or raw_audio) if uses_input_audio else None - segmented_audio = (_input_or_linx(None, linx, "audio_conditioning_segmented") or raw_audio) if uses_input_audio else None - audio_preprocess_report = str( - linx_resource(linx, "audio_preprocess_report", "audio_preprocess=unknown") - or "audio_preprocess=unknown" - ) if uses_input_audio else "audio_preprocess=generated_audio_empty_latent" + exports_audio = bool(media_settings.get("exports_audio", uses_input_audio or generates_audio)) + _pipeline_debug_step( + pipeline_debug, + "media_route_resolved", + requested_media=requested_media_mode, + effective_media=effective_media_mode, + generation_mode=generation_mode, + uses_input_audio=uses_input_audio, + generates_audio=generates_audio, + exports_audio=exports_audio, + input_audio_source=input_audio_source, + ) + if str(generation_mode) != "t2v" and image is None: + _pipeline_debug_step(pipeline_debug, "error_missing_image", generation_mode=generation_mode, media_mode=effective_media_mode) + raise ValueError("image is required for img2video generation types") + audio = _require_runtime_value(input_audio, "audio") if uses_input_audio else None + raw_audio = (linx_resource(linx, "audio_raw", None) or audio) if uses_input_audio else None + segmented_audio = (linx_resource(linx, "audio_conditioning_segmented", None) or raw_audio) if uses_input_audio else None + melband_single_audio = linx_resource(linx, "audio_conditioning_single", None) if uses_input_audio else None + ltx_conditioning_audio = linx_resource(linx, "audio_ltx_conditioning", None) if uses_input_audio else None + ltx_conditioning_source = str( + linx_resource(linx, "audio_ltx_conditioning_source", "") + or "" + ) if uses_input_audio else "" + planner_audio_preprocess_report = str( + linx_resource(linx, "audio_preprocess_report", "") + or "" + ) if uses_input_audio else "" + if uses_input_audio and reference_audio_img2vid: + # BEST AUDIO2IMG2VIDEO_SINGLE sends MelBand vocals into LTXVAudioVAEEncode. + # If an older/simpler workflow forgot to route the planner vocals, recover + # vocals from raw audio here without changing the user's MelBand choice. + recovered_melband_report = "" + if melband_single_audio is None and raw_audio is not None: + recovered_audio_plan = _prepare_planner_audio( + raw_audio, + "melband_vocals_duration_math", + "MelBandRoformer_fp32.safetensors", + ) + recovered_melband_audio = recovered_audio_plan.get("conditioning_audio_single") + if recovered_melband_audio is not None: + melband_single_audio = recovered_melband_audio + melband_enabled = True + recovered_melband_report = " | render_recovered_melband_vocals=yes" + single_conditioning_audio = melband_single_audio or segmented_audio or ltx_conditioning_audio or raw_audio + else: + single_conditioning_audio = (ltx_conditioning_audio or raw_audio or segmented_audio or melband_single_audio) if uses_input_audio else None + audio_preprocess_report = (planner_audio_preprocess_report or "audio_preprocess=unknown") if uses_input_audio else "audio_preprocess=generated_audio_empty_latent" melband_enabled = bool(linx_resource(linx, "melband_enabled", False)) if uses_input_audio else False + if uses_input_audio and reference_audio_img2vid and melband_single_audio is not None: + melband_enabled = True + planner_audio_preprocess_mode = _normalize_audio_preprocess_mode( + linx_resource( + linx, + "audio_preprocess_mode", + "melband_vocals_duration_math" if melband_enabled else "raw_audio_only", + ) + ) + planner_melband_model_name = _resolve_melband_model_name( + linx_resource(linx, "melband_model_name", _MELBAND_MODEL_NAMES[0] if _MELBAND_MODEL_NAMES else "MelBandRoformer_fp32.safetensors") + ) if uses_input_audio and single_conditioning_audio is None: + _pipeline_debug_step(pipeline_debug, "error_missing_audio_conditioning", raw_audio=raw_audio is not None, segmented_audio=segmented_audio is not None) raise ValueError("input-audio generation selected, but no audio conditioning reached Exec Render") if uses_input_audio: - if linx_resource(linx, "audio_conditioning_single", None) is not None: - conditioning_audio_source = "linx_audio_conditioning_single" + if reference_audio_img2vid and melband_single_audio is not None: + conditioning_audio_source = "best_workflow:audio_conditioning_single_melband_vocals" + recovered_melband_report + elif reference_audio_img2vid and segmented_audio is not None: + conditioning_audio_source = "reference_canvas:audio_conditioning_segmented_fallback" + elif ltx_conditioning_audio is not None: + conditioning_audio_source = f"linx_audio_ltx_conditioning:{ltx_conditioning_source or 'raw_audio'}" elif raw_audio is not None: conditioning_audio_source = f"{input_audio_source}_raw_audio" + elif segmented_audio is not None: + conditioning_audio_source = "linx_audio_conditioning_segmented" + elif melband_single_audio is not None: + conditioning_audio_source = "linx_audio_conditioning_single_fallback" else: conditioning_audio_source = "missing" else: conditioning_audio_source = "generated_or_pure_audio_latent" + _pipeline_debug_step( + pipeline_debug, + "audio_route", + raw_audio=raw_audio, + single_conditioning_audio=single_conditioning_audio, + segmented_audio=segmented_audio, + melband_single_audio=melband_single_audio, + ltx_conditioning_audio=ltx_conditioning_audio, + ltx_conditioning_source=ltx_conditioning_source, + conditioning_audio_source=conditioning_audio_source, + melband_enabled=melband_enabled, + audio_preprocess_report=audio_preprocess_report, + ) print( "[IAMCCS SuperNodes Render] " f"audio_route generation_type={generation_type} generation_mode={generation_mode} " f"backend={backend_mode} media={effective_media_mode} uses_input_audio={uses_input_audio} " - f"generates_audio={generates_audio} input_audio={input_audio_source} " + f"generates_audio={generates_audio} exports_audio={exports_audio} input_audio={input_audio_source} " f"raw_audio={'yes' if raw_audio is not None else 'no'} conditioning_audio={conditioning_audio_source} " f"melband={melband_enabled} resource_keys={','.join(str(k) for k in ((linx or {}).get('resource_keys') or []))}" ) motion_intensity = max(0.25, min(4.0, float(motion_intensity or 1.0))) + _pipeline_debug_step(pipeline_debug, "motion_and_stage2_widgets", motion_intensity=motion_intensity, second_stage_mode=second_stage_mode, stage2_model_policy=stage2_model_policy) second_stage_payload = _stage2_payload_from_exec_widgets( second_stage_mode, stage2_model_policy, @@ -1401,6 +2459,17 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: 9, "up", ) + if reference_audio_img2vid: + _pipeline_debug_step( + pipeline_debug, + "reference_audio_img2vid_route", + fps=planner_settings["fps"], + duration_rule="25fps keeps reference frame math; other fps use LTX 8n+1 rounding", + image_preprocess="best_workflow:ImageResizeKJv2_lanczos_crop_top_before_ltx_preprocess", + audio_mask="legacy_backend:SolidMask_1024_to_SetLatentNoiseMask" if legacy_audio_img2vid_backend else "workflow:SolidMask_from_ImageResizeKJv2_width_height_to_SetLatentNoiseMask", + audio_vae_source=conditioning_audio_source, + contract_version=_AUDIO_IMG2VID_CONTRACT_VERSION, + ) total_duration_seconds = None if total_duration_seconds is None: total_duration_seconds = planner_settings["duration_seconds"] @@ -1426,8 +2495,22 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: generated_audio_decode_node = _node_class("LTXVAudioVAEDecode")() if generates_audio else None vram_flush_node = _node_class("IAMCCS_VRAMFlushLatent")() if bool(vram_flush) else None + positive_text = str(positive_text or "").strip() + negative_text = str(negative_text or "").strip() + negative_source = "frontend" + if not positive_text: + _pipeline_debug_step(pipeline_debug, "error_empty_prompt", positive_text=positive_text) + raise ValueError("positive_text is empty; text/video generation would ignore the prompt") + prompt_excerpt = _prompt_excerpt(positive_text) + _pipeline_debug_step(pipeline_debug, "prompt_route", positive=prompt_excerpt, negative_chars=len(negative_text), negative_source=negative_source) + print( + "[IAMCCS SuperNodes Render] " + f"prompt_route generation_type={generation_type} media={effective_media_mode} " + f"positive=\"{prompt_excerpt}\"" + ) positive = comfy_nodes.CLIPTextEncode().encode(clip, positive_text)[0] negative = comfy_nodes.CLIPTextEncode().encode(clip, negative_text)[0] + _pipeline_debug_step(pipeline_debug, "prompt_encoded", positive=positive, negative=negative) run_root = _resolve_output_path(output_root) run_name = f"run_{unique_id or 'manual'}_{int(seed)}" @@ -1437,6 +2520,7 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: start_dir = os.path.join(run_dir, "start") _ensure_clean_dir(run_dir) os.makedirs(segments_dir, exist_ok=True) + _pipeline_debug_step(pipeline_debug, "run_dirs_ready", run_dir=run_dir, segments_dir=segments_dir, extended_dir=extended_dir, start_dir=start_dir) fps_value = float(planner_settings["fps"]) segment_seconds_value = float(planner_settings["segment_seconds"]) @@ -1466,30 +2550,78 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: planner_segment_count, modular_decode, ) + legacy_audio_img2vid_backend = reference_audio_img2vid and _is_legacy_audio_img2vid_backend(backend_mode) + legacy_planner_adapter = bool(legacy_audio_img2vid_backend) + _pipeline_debug_step( + pipeline_debug, + "planner_route", + total_duration=total_duration_seconds, + fps=fps_value, + segment_seconds=segment_seconds_value, + planner_segment_count=planner_segment_count, + backend_requested=requested_backend_mode, + backend_resolved=backend_mode, + segment_count=segment_count, + use_single_best=use_single_best, + use_in_memory_loop=use_in_memory_loop, + modular_decode=modular_decode, + recommended_left_context=recommended_left_context, + legacy_planner_adapter=legacy_planner_adapter, + planner_audio_img2vid_backend=planner_settings.get("audio_img2vid_backend_family"), + planner_audio_img2vid_mode=planner_settings.get("audio_img2vid_mode"), + planner_render_backend=planner_settings.get("audio_img2vid_render_backend"), + ) planner_report_line = ( f"Planner settings used. mode={planner_settings['planning_mode']} | " f"segment_preset={planner_settings['segment_preset']} | " f"segment_seconds={float(planner_settings['segment_seconds']):.3f}s | " f"overlap={int(planner_settings['overlap_frames'])}f | " - f"ltx_round={planner_settings['ltx_round_mode']}" + f"ltx_round={planner_settings['ltx_round_mode']} | " + f"a2i_backend={planner_settings.get('audio_img2vid_backend_family', 'modern')}/" + f"{planner_settings.get('audio_img2vid_mode', 'single')}->" + f"{planner_settings.get('audio_img2vid_render_backend', backend_mode)}" ) if str(planner_settings["planning_mode"]) == "explicit_preset_seconds": planner_report_line += " | explicit_preset_seconds overrides segment_seconds with the selected 5/10/15/20 second preset" + if legacy_planner_adapter: + planner_report_line += " | legacy planner adapter: using legacy-compatible fps/segment_seconds/overlap/ltx_round/segment_count only" conditioned_positive, conditioned_negative = LTXVConditioning.execute(positive, negative, fps_value) decode_settings = _decode_settings("low_ram_disk") - internal_decode_image_format = "png" - internal_decode_jpg_quality = 100 - continuity_settings = _continuity_mode_from_payload( - continuity_payload, - continuity_anchor_mode, - anchor_refresh_interval, - anchor_image_strength, - ) + internal_decode_image_format = "jpg" if legacy_audio_img2vid_backend else "png" + internal_decode_jpg_quality = 95 if legacy_audio_img2vid_backend else 100 + continuity_anchor_mode = str(continuity_anchor_mode or "off") + if continuity_anchor_mode == "off": + continuity_settings = { + "mode": "off", + "interval": int(anchor_refresh_interval), + "strength": 0.0, + } + else: + continuity_settings = _continuity_mode_from_payload( + continuity_payload, + continuity_anchor_mode, + anchor_refresh_interval, + anchor_image_strength, + ) anti_drift_mode = str(anti_drift_mode or "off") anti_drift_strength = max(0.0, float(anti_drift_strength)) identity_persistence_strength = max(0.0, float(identity_persistence_strength)) + if str(continuity_settings["mode"]) == "off": + anti_drift_mode = "off" + anti_drift_strength = 0.0 + identity_persistence_strength = 0.0 + _pipeline_debug_step( + pipeline_debug, + "continuity_resolved", + anchor_mode=continuity_settings["mode"], + anchor_interval=continuity_settings["interval"], + anchor_strength=continuity_settings["strength"], + anti_drift_mode=anti_drift_mode, + anti_drift_strength=anti_drift_strength, + identity_persistence_strength=identity_persistence_strength, + ) refresh_source_image = refresh_image if refresh_image is not None else image stage2_model_active = _resolve_stage2_model(model, second_stage_model, second_stage_payload) stage2_data = _parse_payload(second_stage_payload) @@ -1498,52 +2630,333 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: second_stage_upscale_model_name = _to_text( stage2_data, "second_stage_upscale_model", - _LATENT_UPSCALE_MODEL_NAMES[0] if _LATENT_UPSCALE_MODEL_NAMES else "ltx-2.3-spatial-upscaler-x2-1.1.safetensors", + _reference_ltx23_upscale_model_name(), ) second_stage_reinject_strength = _to_float(stage2_data, "second_stage_reinject_strength", 0.0) second_stage_cfg = _to_float(stage2_data, "second_stage_cfg", 1.0) second_stage_manual_sigmas = _to_text(stage2_data, "second_stage_manual_sigmas", "0.909375, 0.725, 0.421875, 0.0") second_stage_step_count = _to_int(stage2_data, "second_stage_steps", 3) + ( + second_stage_mode, + second_stage_scale_mode, + second_stage_reinject_strength, + second_stage_cfg, + second_stage_manual_sigmas, + second_stage_step_count, + reference_stage2_auto, + ) = _apply_reference_stage2_defaults( + generation_mode, + effective_media_mode, + second_stage_mode, + second_stage_scale_mode, + second_stage_reinject_strength, + second_stage_cfg, + second_stage_manual_sigmas, + second_stage_step_count, + ) + if ( + str(second_stage_mode) != "off" + and _to_text(stage2_data, "stage2_model_policy", "stage2_model_if_connected") == "stage2_model_if_connected" + and second_stage_model is None + ): + second_stage_mode = "off" + second_stage_scale_mode = "same_resolution_refine" + reference_stage2_auto = "" second_stage_model_source = "stage2_model" if second_stage_model is not None and stage2_model_active is second_stage_model else "stage1_model" + _pipeline_debug_step( + pipeline_debug, + "second_stage_resolved", + mode=second_stage_mode, + scale_mode=second_stage_scale_mode, + reinject_strength=second_stage_reinject_strength, + cfg=second_stage_cfg, + steps=second_stage_step_count, + model_source=second_stage_model_source, + model_connected=second_stage_model is not None, + auto_reference=reference_stage2_auto, + ) identity_reference_latent = None rolling_reference_latent = None if use_single_best: payload_data = _parse_payload(plan_payload) - planner_total_frames = max(1, _to_int(payload_data, "total_frames", int(planner_head[0]) or 1)) - audio_total_frames_with_tail = max(0, _to_int(payload_data, "audio_duration_frames_with_tail", 0)) - total_frames = max(1, planner_total_frames, audio_total_frames_with_tail) + frame_round_mode = _to_text(payload_data, "ltx_round_mode", "up") + planner_total_frames_raw = max(1, _to_int(payload_data, "total_frames", int(planner_head[0]) or 1)) + planner_total_frames = _ltx_compatible_frame_count(planner_total_frames_raw, frame_round_mode) + audio_total_frames_with_tail_raw = max( + 0, + _to_int( + payload_data, + "audio_duration_frames_with_tail", + _to_int( + {"audio_duration_frames_with_tail": linx_output(linx, "audio_duration_frames_with_tail", None)}, + "audio_duration_frames_with_tail", + 0, + ), + ), + ) + audio_total_frames_with_tail = _ltx_compatible_frame_count(audio_total_frames_with_tail_raw, frame_round_mode) if audio_total_frames_with_tail_raw else 0 + total_frames_raw = max(1, planner_total_frames_raw, audio_total_frames_with_tail_raw) + total_frames = _ltx_compatible_frame_count(total_frames_raw, frame_round_mode) + if reference_audio_img2vid: + reference_uses_direct_frame_math = abs(float(fps_value) - float(_REFERENCE_AUDIO_IMG2VID_FPS)) <= 0.001 + reference_planner_frames_raw = max(1, int((float(total_duration_seconds) * float(fps_value)) + 1.0)) + reference_audio_frames_raw = 0 + reference_audio_frames_source = "missing" + reference_duration_audio = raw_audio or audio or single_conditioning_audio + if reference_duration_audio is not None: + reference_audio_frames_raw, reference_audio_frames_source = _compute_audio_duration_frames_with_tail( + reference_duration_audio, + fps_value, + ) + planner_total_frames_raw = int(reference_planner_frames_raw) + audio_total_frames_with_tail_raw = int(reference_audio_frames_raw) + total_frames_raw = max(1, audio_total_frames_with_tail_raw or planner_total_frames_raw) + if reference_uses_direct_frame_math: + frame_round_mode = "reference_25fps_formula_no_ltx_round" + planner_total_frames = int(reference_planner_frames_raw) + audio_total_frames_with_tail = int(reference_audio_frames_raw) + total_frames = int(total_frames_raw) + else: + compatible_round_mode = _to_text(payload_data, "ltx_round_mode", frame_round_mode) or "up" + frame_round_mode = f"{compatible_round_mode}_ltx_8n_plus_1" + planner_total_frames = _ltx_compatible_frame_count(reference_planner_frames_raw, compatible_round_mode) + audio_total_frames_with_tail = _ltx_compatible_frame_count(reference_audio_frames_raw, compatible_round_mode) if reference_audio_frames_raw else 0 + total_frames = _ltx_compatible_frame_count(total_frames_raw, compatible_round_mode) + _pipeline_debug_step( + pipeline_debug, + "single_reference_audio_img2vid_duration", + fps=fps_value, + duration_seconds=total_duration_seconds, + planner_formula_frames=reference_planner_frames_raw, + audio_formula_frames=reference_audio_frames_raw, + audio_formula_source=reference_audio_frames_source, + ltx_8n_plus_1=not reference_uses_direct_frame_math, + chosen_total_frames=total_frames, + ) + _pipeline_debug_step( + pipeline_debug, + "single_duration_protection", + frame_round_mode=frame_round_mode, + planner_total_frames_raw=planner_total_frames_raw, + planner_total_frames=planner_total_frames, + audio_total_frames_with_tail_raw=audio_total_frames_with_tail_raw, + audio_total_frames_with_tail=audio_total_frames_with_tail, + chosen_total_frames_raw=total_frames_raw, + chosen_total_frames=total_frames, + ) + if reference_audio_img2vid: + effective_image_strength = float(image_strength) + _pipeline_debug_step( + pipeline_debug, + "strict_reference_single_enabled", + contract="legacy_exact_single" if legacy_audio_img2vid_backend else "workflow_1_AU_IMG2VID_SINGLE", + image_strength=effective_image_strength, + motion_intensity_ignored=motion_intensity, + model_sampling="legacy_single:no_ModelSamplingLTXV" if legacy_audio_img2vid_backend else "none_like_reference_canvas", + scheduler="ManualSigmas" if legacy_audio_img2vid_backend else "BasicScheduler(simple)", + sampler_node="SamplerCustomAdvanced" if legacy_audio_img2vid_backend else "IAMCCS_SamplerAdvancedVersion1", + ) + else: + effective_image_strength = _motion_guidance_strength(image_strength, motion_intensity) + guidance_image = None + resize_report = "image_resize=not_used" + latent_width = int(width) + latent_height = int(height) + if str(generation_mode) != "t2v" and reference_audio_img2vid and legacy_audio_img2vid_backend: + ( + legacy_requested_width, + legacy_requested_height, + legacy_resolution_fix, + ) = _normalize_reference_audio_img2vid_requested_resolution(width, height) + guidance_image, latent_width, latent_height, resize_report = _resize_image_like_reference_audio_img2vid( + image, + legacy_requested_width, + legacy_requested_height, + ) + if legacy_resolution_fix: + resize_report = f"{resize_report} | {legacy_resolution_fix}" + width = int(latent_width) + height = int(latent_height) + _pipeline_debug_step( + pipeline_debug, + "single_legacy_exact_image_resized", + source_image=image, + resized_image=guidance_image, + requested_width=int(legacy_requested_width), + requested_height=int(legacy_requested_height), + actual_width=int(width), + actual_height=int(height), + resize_report=resize_report, + contract_version=_LEGACY_AUDIO_IMG2VID_CONTRACT_VERSION, + ) + elif str(generation_mode) != "t2v" and reference_audio_img2vid: + # The frontend is the source of truth for resolution; the lipsync fix is + # isolated to the audio-shaped noise mask below. + ( + reference_requested_width, + reference_requested_height, + reference_resolution_fix, + ) = _normalize_reference_audio_img2vid_requested_resolution(width, height) + guidance_image, latent_width, latent_height, resize_report = _resize_image_like_reference_audio_img2vid( + image, + reference_requested_width, + reference_requested_height, + ) + if reference_resolution_fix: + resize_report = f"{resize_report} | {reference_resolution_fix}" + width = int(latent_width) + height = int(latent_height) + _pipeline_debug_step( + pipeline_debug, + "single_reference_best_image_resized", + source_image=image, + resized_image=guidance_image, + requested_width=reference_requested_width, + requested_height=reference_requested_height, + actual_width=width, + actual_height=height, + resize_report=resize_report, + ) video_latent = EmptyLTXVLatentVideo.execute(int(width), int(height), total_frames, 1)[0] - effective_image_strength = _motion_guidance_strength(image_strength, motion_intensity) + _pipeline_debug_step(pipeline_debug, "single_video_latent_created", video_latent=video_latent, width=width, height=height, frames=total_frames) if str(generation_mode) != "t2v": - preprocessed_image = LTXVPreprocess.execute(image, int(image_compression))[0] + if guidance_image is None: + guidance_image = image + preprocessed_image = LTXVPreprocess.execute(guidance_image, int(image_compression))[0] video_latent = LTXVImgToVideoInplace.execute(vae, preprocessed_image, video_latent, float(effective_image_strength), False)[0] + _pipeline_debug_step(pipeline_debug, "single_image_conditioning_applied", image=guidance_image, image_compression=image_compression, image_strength=effective_image_strength, image_resize=resize_report, video_latent=video_latent) + else: + _pipeline_debug_step(pipeline_debug, "single_image_conditioning_skipped", generation_mode=generation_mode) if generates_audio: audio_latent = generated_audio_latent_node.execute(int(total_frames), max(1, int(round(fps_value))), 1, audio_vae)[0] + _pipeline_debug_step(pipeline_debug, "single_audio_latent_generated", audio_latent=audio_latent, frames=total_frames, fps=fps_value) else: audio_latent = LTXVAudioVAEEncode.execute(single_conditioning_audio, audio_vae)[0] - audio_mask = SolidMask.execute(0.0, 1024, 1024)[0] - audio_latent = comfy_nodes.SetLatentNoiseMask().set_mask(audio_latent, audio_mask)[0] + _pipeline_debug_step(pipeline_debug, "single_audio_latent_encoded", conditioning_audio=single_conditioning_audio, audio_latent=audio_latent) + if reference_audio_img2vid or _is_text_audio2video(generation_type): + if legacy_audio_img2vid_backend: + audio_latent, audio_mask = _apply_legacy_backend_mask_to_audio_latent(audio_latent) + mask_source = "legacy_backend:SolidMask_1024_to_SetLatentNoiseMask" + mask_width = 1024 + mask_height = 1024 + else: + audio_latent, audio_mask, mask_width, mask_height = _apply_workflow_solid_mask_to_audio_latent( + audio_latent, + width, + height, + ) + mask_source = "workflow:SolidMask_from_ImageResizeKJv2_width_height_to_SetLatentNoiseMask" + _pipeline_debug_step( + pipeline_debug, + "single_input_audio_noise_mask_applied", + mask=audio_mask, + mask_shape=f"solid_mask_{int(mask_width)}x{int(mask_height)}", + mask_source=mask_source, + audio_latent=audio_latent, + ) _soft_cleanup() av_latent = LTXVConcatAVLatent.execute(video_latent, audio_latent)[0] - accelerated_model, accelerator_report = _accelerate_exec_model_if_available(model) - model_for_segment = accelerated_model - guider = CFGGuider.execute(model_for_segment, conditioned_positive, conditioned_negative, float(cfg))[0] - sampler = KSamplerSelect.execute("lcm")[0] - sigmas = _scheduler_sigmas(model_for_segment, "simple", 8, 1.0) + effective_sampler = _effective_sampler_name(sampler_name, generation_mode, effective_media_mode, generation_type) + effective_cfg_value = _effective_cfg(cfg, generation_mode, effective_media_mode) + if reference_audio_img2vid: + model_sampling_report = "legacy_exact:no_ModelSamplingLTXV_in_source_workflows" if legacy_audio_img2vid_backend else "strict_reference:no_ModelSamplingLTXV" + model_for_segment = model + if legacy_audio_img2vid_backend: + sigmas = _manual_sigmas(manual_sigmas or _REFERENCE_MANUAL_SIGMAS) + sigmas_report = f"legacy_exact_manual_sigmas({int(sigmas.numel())} values)" + else: + sigmas = _scheduler_sigmas(model, "simple", max(1, int(steps or 8)), 1.0) + sigmas_report = f"strict_reference_basic_scheduler(simple,{max(1, int(steps or 8))} steps,denoise=1.0)" + route_extra_report = "legacy_backend_mask=SolidMask_1024" if legacy_audio_img2vid_backend else "mask=workflow_solidmask_from_resize_dims" + _pipeline_debug_step( + pipeline_debug, + "single_av_latent_concat", + av_latent=av_latent, + sampler=effective_sampler, + cfg=effective_cfg_value, + model_sampling=model_sampling_report, + route_contract="legacy_exact_single" if legacy_audio_img2vid_backend else "strict_workflow1_canvas_reference", + ) + else: + accelerated_model, accelerator_report = _accelerate_exec_model_if_available(model) + model_sampling_report = f"ModelSamplingLTXV(max_shift={float(max_shift):.3f},base_shift={float(base_shift):.3f})" + model_for_segment = ModelSamplingLTXV.execute(accelerated_model, float(max_shift), float(base_shift), av_latent)[0] + sigmas, sigmas_report = _main_sigmas( + uses_input_audio, + steps, + max_shift, + base_shift, + sigma_terminal, + av_latent, + generation_mode, + effective_media_mode, + manual_sigmas, + generation_type, + model_for_segment, + ) + route_extra_report = accelerator_report + _pipeline_debug_step( + pipeline_debug, + "single_av_latent_concat", + av_latent=av_latent, + sampler=effective_sampler, + cfg=effective_cfg_value, + accelerator=accelerator_report, + model_sampling=model_sampling_report, + ) + guider = CFGGuider.execute(model_for_segment, conditioned_positive, conditioned_negative, float(effective_cfg_value))[0] + sampler = KSamplerSelect.execute(str(effective_sampler))[0] + sampler_node_name = "SamplerCustomAdvanced" + reference_sampler_cls = comfy_nodes.NODE_CLASS_MAPPINGS.get("IAMCCS_SamplerAdvancedVersion1") if reference_audio_img2vid else None + if reference_sampler_cls is not None: + sampler_node_name = "IAMCCS_SamplerAdvancedVersion1(disable_progress=True,cleanup=True)" + _pipeline_debug_step( + pipeline_debug, + "single_sampler_prepared", + sampler=effective_sampler, + sampler_node=sampler_node_name, + sigmas=sigmas, + sigmas_report=sigmas_report, + seed=seed, + ) noise = RandomNoise.execute(int(seed))[0] - sampled_av = _node_class("IAMCCS_SamplerAdvancedVersion1")().sample( - noise, - guider, - sampler, - sigmas, - av_latent, - True, - True, - )[0] + if reference_sampler_cls is not None: + sampled_av = reference_sampler_cls().sample( + noise, + guider, + sampler, + sigmas, + av_latent, + True, + True, + )[0] + else: + sampled_av = SamplerCustomAdvanced.sample( + noise, + guider, + sampler, + sigmas, + av_latent, + )[0] sampled_video, sampled_audio_latent = LTXVSeparateAVLatent.execute(sampled_av) - sampled_video = LTXVCropGuides.execute(conditioned_positive, conditioned_negative, sampled_video)[2] + if reference_audio_img2vid: + if legacy_audio_img2vid_backend: + crop_guides_report = "legacy_exact:skipped_no_LTXVCropGuides_in_source_workflows" + else: + crop_guides_report = "skipped_best_workflow_no_LTXVCropGuides_after_sampler" + else: + sampled_video = LTXVCropGuides.execute(conditioned_positive, conditioned_negative, sampled_video)[2] + crop_guides_report = "applied" + _pipeline_debug_step( + pipeline_debug, + "single_sampled", + sampler_node=sampler_node_name, + crop_guides=crop_guides_report, + sampled_video=sampled_video, + sampled_audio_latent=sampled_audio_latent, + ) stage2_model_active = _resolve_stage2_model(model, second_stage_model, second_stage_payload) stage2_data = _parse_payload(second_stage_payload) @@ -1552,13 +2965,49 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: second_stage_upscale_model_name = _to_text( stage2_data, "second_stage_upscale_model", - _LATENT_UPSCALE_MODEL_NAMES[0] if _LATENT_UPSCALE_MODEL_NAMES else "ltx-2.3-spatial-upscaler-x2-1.1.safetensors", + _reference_ltx23_upscale_model_name(), ) second_stage_reinject_strength = _to_float(stage2_data, "second_stage_reinject_strength", 0.0) second_stage_cfg = _to_float(stage2_data, "second_stage_cfg", 1.0) second_stage_manual_sigmas = _to_text(stage2_data, "second_stage_manual_sigmas", "0.909375, 0.725, 0.421875, 0.0") second_stage_step_count = _to_int(stage2_data, "second_stage_steps", 3) + ( + second_stage_mode, + second_stage_scale_mode, + second_stage_reinject_strength, + second_stage_cfg, + second_stage_manual_sigmas, + second_stage_step_count, + reference_stage2_auto, + ) = _apply_reference_stage2_defaults( + generation_mode, + effective_media_mode, + second_stage_mode, + second_stage_scale_mode, + second_stage_reinject_strength, + second_stage_cfg, + second_stage_manual_sigmas, + second_stage_step_count, + ) + if ( + str(second_stage_mode) != "off" + and _to_text(stage2_data, "stage2_model_policy", "stage2_model_if_connected") == "stage2_model_if_connected" + and second_stage_model is None + ): + second_stage_mode = "off" + second_stage_scale_mode = "same_resolution_refine" + reference_stage2_auto = "" second_stage_model_source = "stage2_model" if second_stage_model is not None and stage2_model_active is second_stage_model else "stage1_model" + _pipeline_debug_step( + pipeline_debug, + "single_second_stage_resolved", + mode=second_stage_mode, + scale_mode=second_stage_scale_mode, + reinject_strength=second_stage_reinject_strength, + cfg=second_stage_cfg, + steps=second_stage_step_count, + model_source=second_stage_model_source, + ) if str(second_stage_mode) in {"latent_refine_3step", "latent_upscale_refine", "latent_upscale_refine_x2_beta"}: stage2_positive, stage2_negative, cropped_video_latent = LTXVCropGuides.execute( @@ -1592,7 +3041,7 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: latent_stage2 = vram_flush_node.run(latent_stage2)[0] model_stage2 = ModelSamplingLTXV.execute(stage2_model_active, float(max_shift), float(base_shift), latent_stage2)[0] guider_stage2 = CFGGuider.execute(model_stage2, stage2_positive, stage2_negative, float(second_stage_cfg))[0] - sampler_stage2 = KSamplerSelect.execute("euler")[0] + sampler_stage2 = KSamplerSelect.execute(str(effective_sampler))[0] sigmas_stage2 = _manual_sigmas(second_stage_manual_sigmas) noise_stage2 = RandomNoise.execute(int(seed))[0] sampled_stage2_av = SamplerCustomAdvanced.sample( @@ -1602,24 +3051,47 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: sigmas_stage2, latent_stage2, )[0] - sampled_video = LTXVSeparateAVLatent.execute(sampled_stage2_av)[0] + sampled_video, sampled_audio_latent = LTXVSeparateAVLatent.execute(sampled_stage2_av) + _pipeline_debug_step( + pipeline_debug, + "single_second_stage_applied", + mode=second_stage_mode, + scale_mode=second_stage_scale_mode, + sigmas_stage2=sigmas_stage2, + sampled_video=sampled_video, + sampled_audio_latent=sampled_audio_latent, + ) + else: + _pipeline_debug_step(pipeline_debug, "single_second_stage_skipped", mode=second_stage_mode) - rendered_audio = generated_audio_decode_node.execute(sampled_audio_latent, audio_vae)[0] if generates_audio else raw_audio + if uses_input_audio: + rendered_audio = raw_audio + _pipeline_debug_step(pipeline_debug, "single_audio_export_input_passthrough", rendered_audio=rendered_audio) + elif generates_audio and exports_audio: + rendered_audio = generated_audio_decode_node.execute(sampled_audio_latent, audio_vae)[0] + _pipeline_debug_step(pipeline_debug, "single_audio_export_generated", rendered_audio=rendered_audio) + else: + rendered_audio = None + _pipeline_debug_step(pipeline_debug, "single_audio_export_off", exports_audio=exports_audio, generates_audio=generates_audio) + disable_vae_frame_align = bool(reference_audio_img2vid) + single_debug_report = _pipeline_debug_text(pipeline_debug) report = ( f"duration {float(total_duration_seconds):.2f}s | fps {float(fps_value):.2f} | total {int(total_frames)}f | segments 1 | " - f"backend_requested={requested_backend_mode} | backend_resolved=single_best | media_mode={effective_media_mode} | generation_mode {generation_mode} | " - f"stage2 {'on' if str(second_stage_mode) != 'off' else 'off'} | decode_mode={modular_decode} | " - f"conditioning {'generated_audio_empty_latent' if generates_audio else ('melband_vocals_duration_math' if single_conditioning_audio is not raw_audio else 'raw_audio_only')} | " + f"backend_requested={requested_backend_mode} | backend_resolved={backend_mode} | media_mode={effective_media_mode} | generation_mode {generation_mode} | " + f"stage2 {('auto_' + str(reference_stage2_auto) + '_refine') if reference_stage2_auto else ('on' if str(second_stage_mode) != 'off' else 'off')} | decode_mode={modular_decode} | " + f"conditioning {'generated_audio_empty_latent' if generates_audio else conditioning_audio_source} | audio_export={'yes' if rendered_audio is not None else 'no'} | " f"melband_enabled={melband_enabled}\n" + f"Prompt route. positive=\"{prompt_excerpt}\"\n" f"Planner settings used. mode={planner_settings['planning_mode']} | segment_preset={planner_settings['segment_preset']} | " f"segment_seconds={float(planner_settings['segment_seconds']):.3f}s | overlap={int(planner_settings['overlap_frames'])}f | " f"ltx_round={planner_settings['ltx_round_mode']}\n" f"Single duration protection. planner_total={int(planner_total_frames)}f | audio_total_with_tail={int(audio_total_frames_with_tail)}f | chosen_total={int(total_frames)}f\n" f"Audio preprocess. {audio_preprocess_report}\n" - f"Single route details. sampler=lcm | scheduler=simple(steps=8, denoise=1.0) | sampler_node=IAMCCS_SamplerAdvancedVersion1 | cleanup_before_sampling=soft_cleanup | model_sampling=workflow_single_match(no_extra_ModelSamplingLTXV) | motion_intensity={motion_intensity:.2f} | vram_flush={'on' if bool(vram_flush) else 'off'} | {accelerator_report}\n" - f"Executable AU+IMG2VID render completed. single generation backend=workflow1_best | latent handed to VAE stage" + f"Single route details. sampler={effective_sampler} | cfg={float(effective_cfg_value):.3f} | sigmas={sigmas_report} | sampler_node={sampler_node_name} | cleanup_before_sampling=soft_cleanup | model_sampling={model_sampling_report} | motion_intensity={'ignored_strict_reference' if reference_audio_img2vid else f'{motion_intensity:.2f}'} | vram_flush={'on' if bool(vram_flush) else 'off'} | vae_frame_align={'off_official_audio_img2vid' if disable_vae_frame_align else 'on'} | {route_extra_report}\n" + f"Executable AU+IMG2VID render completed. single generation backend={'legacy_single' if legacy_audio_img2vid_backend else ('strict_workflow1_canvas_reference' if reference_audio_img2vid else 'workflow1_best')} | latent handed to VAE stage" ) + report = _append_debug_report(report, single_debug_report) render_linx = build_stage_linx_payload( linx, "exec_render", @@ -1631,9 +3103,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "generation_mode": str(generation_mode), "fps": float(fps_value), "modular_decode": str(modular_decode), - "motion_intensity": float(motion_intensity), - "generated_media_duration_seconds": float(generated_media_duration_seconds), - "generation_type": str(generation_type), + "motion_intensity": float(motion_intensity), + "generated_media_duration_seconds": float(generated_media_duration_seconds), + "generated_media_fps": float(fps_value), + "generation_type": str(generation_type), + "debug_verbose": bool(debug_verbose), + "target_frame_count": int(total_frames), + "target_frame_count_source": "render_single_planner_audio" if uses_input_audio else "render_single_generated_duration", + "disable_vae_frame_align": disable_vae_frame_align, "vram_flush": bool(vram_flush), "segment_count": 1, "segments_rendered": 1, @@ -1641,12 +3118,13 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "second_stage_scale_mode": str(second_stage_scale_mode), "second_stage_steps": int(second_stage_step_count), "second_stage_model_source": str(second_stage_model_source), + "pipeline_debug_steps": len(pipeline_debug.get("lines") or []), }, report, unique_id=unique_id, requires={ "resources": {"model": "MODEL", "clip": "CLIP", "vae": "VAE", "audio_vae": "VAE", "planner_payload": "STRING", "fps": "FLOAT"}, - "input_audio_modes": {"audio": "AUDIO", "audio_conditioning_single": "AUDIO", "audio_conditioning_segmented": "AUDIO"}, + "input_audio_modes": {"audio": "AUDIO", "audio_ltx_conditioning": "AUDIO", "audio_conditioning_single": "AUDIO", "audio_conditioning_segmented": "AUDIO"}, "pure_or_generated_modes": {"audio": "optional final audio only"}, }, slot_map={ @@ -1670,9 +3148,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "decode_mode": str(modular_decode), "media_mode": str(effective_media_mode), "generation_type": str(generation_type), + "target_frame_count": int(total_frames), + "target_frame_count_source": "render_single_planner_audio" if uses_input_audio else "render_single_generated_duration", + "disable_vae_frame_align": disable_vae_frame_align, + "pipeline_debug": list(pipeline_debug.get("lines") or []), + "debug_verbose": bool(debug_verbose), }, resources={ - "audio": rendered_audio, + "audio": rendered_audio if exports_audio else None, "model": model, "clip": clip, "vae": vae, @@ -1683,6 +3166,10 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "planner_payload": str(plan_payload), "media_mode": str(effective_media_mode), "generation_mode": str(generation_mode), + "target_frame_count": int(total_frames), + "target_frame_count_source": "render_single_planner_audio" if uses_input_audio else "render_single_generated_duration", + "disable_vae_frame_align": disable_vae_frame_align, + "debug_verbose": bool(debug_verbose), "video_latent": sampled_video, "rendered_images": None, "second_stage_model": stage2_model_active, @@ -1699,6 +3186,97 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: in_memory_stitch_preset = str(stitch_preset or "custom") if in_memory_stitch_preset in {"lossless_refresh_24fps", "lossless_refresh_strong_24fps", "videoclip_audio_24fps", "monologue_audio_24fps"}: in_memory_stitch_preset = "custom" + effective_sampler = _effective_sampler_name(sampler_name, generation_mode, effective_media_mode, generation_type) + effective_cfg_value = _effective_cfg(cfg, generation_mode, effective_media_mode) + if reference_audio_img2vid and str(generation_mode) != "t2v" and legacy_audio_img2vid_backend: + original_image = image + ( + legacy_requested_width, + legacy_requested_height, + legacy_resolution_fix, + ) = _normalize_reference_audio_img2vid_requested_resolution(width, height) + resized_image, latent_width, latent_height, resize_report = _resize_image_like_reference_audio_img2vid( + image, + legacy_requested_width, + legacy_requested_height, + ) + width = int(latent_width) + height = int(latent_height) + image = resized_image + loop_resize_report = f"legacy_exact_resize_before_LTXVPreprocess | {resize_report}" + if legacy_resolution_fix: + loop_resize_report = f"{loop_resize_report} | {legacy_resolution_fix}" + refresh_resize_report = "legacy_github:refresh_source=main_image" + if refresh_image is None: + refresh_source_image = resized_image + else: + refresh_source_image, _, _, refresh_resize_report = _resize_image_like_reference_audio_img2vid( + refresh_source_image, + width, + height, + ) + _pipeline_debug_step( + pipeline_debug, + "loop_legacy_image_prepared", + source_image=original_image, + resized_image=resized_image, + requested_width=int(legacy_requested_width), + requested_height=int(legacy_requested_height), + actual_width=int(latent_width), + actual_height=int(latent_height), + resize_report=loop_resize_report, + refresh_resize_report=refresh_resize_report, + contract_version=_LEGACY_AUDIO_IMG2VID_CONTRACT_VERSION, + ) + elif reference_audio_img2vid and str(generation_mode) != "t2v": + ( + loop_requested_width, + loop_requested_height, + loop_resolution_fix, + ) = _normalize_reference_audio_img2vid_requested_resolution(width, height) + original_image = image + resized_image, latent_width, latent_height, loop_resize_report = _resize_image_like_reference_audio_img2vid( + image, + loop_requested_width, + loop_requested_height, + ) + if loop_resolution_fix: + loop_resize_report = f"{loop_resize_report} | {loop_resolution_fix}" + width = int(latent_width) + height = int(latent_height) + image = resized_image + if refresh_image is None: + refresh_source_image = resized_image + refresh_resize_report = "refresh_source=main_image" + else: + refresh_source_image, _, _, refresh_resize_report = _resize_image_like_reference_audio_img2vid( + refresh_source_image, + width, + height, + ) + _pipeline_debug_step( + pipeline_debug, + "loop_reference_image_resized", + source_image=original_image, + resized_image=resized_image, + requested_width=loop_requested_width, + requested_height=loop_requested_height, + actual_width=width, + actual_height=height, + resize_report=loop_resize_report, + refresh_resize_report=refresh_resize_report, + contract_version=_AUDIO_IMG2VID_CONTRACT_VERSION, + ) + _pipeline_debug_step( + pipeline_debug, + "loop_backend_ready", + segment_count=segment_count, + use_in_memory_loop=use_in_memory_loop, + sampler=effective_sampler, + cfg=effective_cfg_value, + decode_mode=modular_decode, + stitch_preset=stitch_preset, + ) for segment_index in range(segment_count): plan_segment = planner_node.plan( @@ -1732,12 +3310,49 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: remaining_frames_after = int(math_out[7]) is_last_segment = int(math_out[8]) - conditioning_audio = audio_extender_node.slice_segment( - segmented_audio, + workflow_melband_route = bool( + reference_audio_img2vid + and not legacy_audio_img2vid_backend + and planner_audio_preprocess_mode == "melband_vocals_duration_math" + and melband_enabled + ) + post_extender_melband = False + if legacy_audio_img2vid_backend: + extender_source_audio = segmented_audio or raw_audio or melband_single_audio + extender_source_label = "legacy:audio_conditioning_segmented_or_raw" + segment_audio_context_mode = audio_context_mode + segment_audio_left_context_s = float(audio_left_context_s) + segment_audio_right_context_s = float(audio_right_context_s) + elif workflow_melband_route and int(segment_index) == 0: + extender_source_audio = raw_audio or segmented_audio + extender_source_label = "workflow:seg0_raw_full_audio" + post_extender_melband = True + elif workflow_melband_route: + extender_source_audio = melband_single_audio or segmented_audio or raw_audio + extender_source_label = "workflow:full_melband_vocals" + else: + extender_source_audio = segmented_audio or raw_audio or melband_single_audio + extender_source_label = "raw_audio_only:raw_or_segmented_audio" + if extender_source_audio is None: + raise ValueError("input-audio segment render selected, but no audio reached IAMCCS_AudioExtender") + + if legacy_audio_img2vid_backend: + pass + elif reference_audio_img2vid and int(segment_index) == 0: + segment_audio_context_mode = "no_overlap" + segment_audio_left_context_s = 0.0 + segment_audio_right_context_s = 0.0 + else: + segment_audio_context_mode = audio_context_mode + segment_audio_left_context_s = float(audio_left_context_s) + segment_audio_right_context_s = float(audio_right_context_s) + + conditioning_slice = audio_extender_node.slice_segment( + extender_source_audio, fps_value, - audio_context_mode, - float(audio_left_context_s), - float(audio_right_context_s), + segment_audio_context_mode, + segment_audio_left_context_s, + segment_audio_right_context_s, "use_timeline_cursor", "snap_to_video_duration", "soft_clamp", @@ -1751,6 +3366,25 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: effective_unique_frames, current_segment_unique_frames, )[0] + if post_extender_melband: + try: + conditioning_audio, segment_melband_model_name = _extract_melband_vocals( + conditioning_slice, + planner_melband_model_name, + ) + except Exception as exc: + raise RuntimeError(f"segment 0 MelBand vocals extraction failed after AudioExtender: {exc}") from exc + conditioning_audio_source = ( + f"workflow:seg0_raw_audio_extender_then_melband_vocals:{segment_melband_model_name}" + ) + else: + conditioning_audio = conditioning_slice + if legacy_audio_img2vid_backend: + conditioning_audio_source = "legacy:audio_extender_then_audio_vae" + elif workflow_melband_route: + conditioning_audio_source = "workflow:full_melband_vocals_then_audio_extender" + else: + conditioning_audio_source = "raw_audio_only:audio_extender_then_audio_vae" else: segment_start_frames = int(cursor_frames) effective_unique_frames = int(current_segment_unique_frames) @@ -1758,16 +3392,50 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: remaining_frames_after = max(0, int(planner_head[0]) - int(cursor_frames_out)) is_last_segment = 1 if int(segment_index) >= int(segment_count) - 1 or int(remaining_frames_after) <= 0 else 0 conditioning_audio = None + conditioning_slice = None + conditioning_audio_source = "generated_audio_empty_latent" + extender_source_label = "none" + segment_audio_context_mode = "none" + segment_audio_left_context_s = 0.0 + segment_audio_right_context_s = 0.0 + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_timeline", + raw_frames=current_segment_raw_frames, + unique_frames=current_segment_unique_frames, + generated_frames_for_timeline=generated_frames_for_timeline, + segment_start_frames=segment_start_frames, + effective_unique_frames=effective_unique_frames, + remaining_frames_after=remaining_frames_after, + is_last_segment=is_last_segment, + audio_conditioning_route=conditioning_audio_source, + audio_extender_input=extender_source_label, + audio_context_mode=segment_audio_context_mode, + audio_left_context_s=segment_audio_left_context_s, + audio_right_context_s=segment_audio_right_context_s, + conditioning_slice=conditioning_slice, + conditioning_audio=conditioning_audio, + ) video_latent = EmptyLTXVLatentVideo.execute(int(width), int(height), current_segment_raw_frames, 1)[0] is_t2v = generation_mode == "t2v" uses_source_anchor = False if is_t2v else _use_source_anchor(segment_index, continuity_settings["mode"], continuity_settings["interval"]) uses_tail_source_anchor = False if is_t2v else _use_tail_then_source_anchor(segment_index, continuity_settings["mode"], continuity_settings["interval"]) - effective_image_strength = _motion_guidance_strength(image_strength, motion_intensity) - effective_anchor_strength = _motion_guidance_strength(continuity_settings["strength"], motion_intensity) + if reference_audio_img2vid: + if legacy_audio_img2vid_backend: + effective_image_strength = float(image_strength) + effective_anchor_strength = float(continuity_settings["strength"]) + else: + effective_image_strength = float(image_strength) + effective_anchor_strength = float(continuity_settings["strength"]) + else: + effective_image_strength = _motion_guidance_strength(image_strength, motion_intensity) + effective_anchor_strength = _motion_guidance_strength(continuity_settings["strength"], motion_intensity) init_mode = "t2v_empty" if is_t2v and segment_index == 0 else "tail" tail_refresh_report = "none" anchor_refresh_report = "off" + tail_inject_strength = None + tail_preprocess_crf = None if uses_source_anchor: anchor_image = refresh_source_image if segment_index > 0 and continuity_settings["mode"] == "periodic_source_refresh": @@ -1778,6 +3446,20 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: init_mode = "source" anchor_refresh_report = f"source_head:{source_strength:.2f}" elif segment_index > 0: + tail_inject_strength = ( + float(_LEGACY_EXACT_TAIL_STRENGTH) + if legacy_audio_img2vid_backend + else float(_REFERENCE_AUDIO_IMG2VID_TAIL_STRENGTH) + if reference_audio_img2vid + else float(effective_image_strength) + ) + tail_preprocess_crf = ( + int(_LEGACY_EXACT_TAIL_PREPROCESS_CRF) + if legacy_audio_img2vid_backend + else int(_REFERENCE_AUDIO_IMG2VID_TAIL_PREPROCESS_CRF) + if reference_audio_img2vid + else int(image_compression) + ) if use_in_memory_loop: video_latent, _, tail_refresh_report = start_inject_images_node.inject( current_start_images, @@ -1786,9 +3468,9 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "all", max(1, overlap_frames_value), 0, - float(effective_image_strength), + tail_inject_strength, True, - int(image_compression), + tail_preprocess_crf, ) else: video_latent, _, tail_refresh_report = start_inject_node.inject( @@ -1798,9 +3480,9 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "all", max(1, overlap_frames_value), 0, - float(effective_image_strength), + tail_inject_strength, True, - int(image_compression), + tail_preprocess_crf, ) if uses_tail_source_anchor: protected_tail_slots = _protected_prefix_latent_frames(vae, overlap_frames_value) @@ -1818,6 +3500,20 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: 0, ) init_mode = "tail+source_refresh" + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_video_init", + video_latent=video_latent, + init_mode=init_mode, + source_anchor=uses_source_anchor, + tail_source_anchor=uses_tail_source_anchor, + image_strength=effective_image_strength, + anchor_strength=effective_anchor_strength, + tail_refresh=tail_refresh_report, + anchor_refresh=anchor_refresh_report, + tail_inject_strength=tail_inject_strength, + tail_preprocess_crf=tail_preprocess_crf, + ) if generates_audio: audio_latent = generated_audio_latent_node.execute( @@ -1828,23 +3524,112 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: )[0] else: audio_latent = LTXVAudioVAEEncode.execute(conditioning_audio, audio_vae)[0] - audio_mask = SolidMask.execute(0.0, 1024, 1024)[0] - audio_latent = comfy_nodes.SetLatentNoiseMask().set_mask(audio_latent, audio_mask)[0] + if reference_audio_img2vid: + if legacy_audio_img2vid_backend: + audio_latent, audio_mask = _apply_legacy_backend_mask_to_audio_latent(audio_latent) + mask_width = 1024 + mask_height = 1024 + mask_source = "legacy_github:SolidMask_1024_to_SetLatentNoiseMask" + else: + audio_latent, audio_mask, mask_width, mask_height = _apply_workflow_solid_mask_to_audio_latent( + audio_latent, + width, + height, + ) + mask_source = "workflow:SolidMask_from_ImageResizeKJv2_width_height_to_SetLatentNoiseMask" + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_reference_audio_noise_mask_applied", + mask=audio_mask, + mask_shape=f"solid_mask_{int(mask_width)}x{int(mask_height)}", + mask_source=mask_source, + audio_latent=audio_latent, + ) + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_audio_latent", + mode="generated" if generates_audio else "encoded_input", + audio_latent=audio_latent, + ) segment_positive = conditioned_positive segment_negative = conditioned_negative _hard_unload_all_models() av_latent = LTXVConcatAVLatent.execute(video_latent, audio_latent)[0] - model_for_segment = ModelSamplingLTXV.execute(model, float(max_shift), float(base_shift), av_latent)[0] - guider = CFGGuider.execute(model_for_segment, segment_positive, segment_negative, float(cfg))[0] - sampler = KSamplerSelect.execute("lcm")[0] - sigmas = _scheduler_sigmas(model_for_segment, "simple", 8, 1.0) + if reference_audio_img2vid and legacy_audio_img2vid_backend: + model_for_segment = model + loop_model_sampling_report = "legacy_exact:no_ModelSamplingLTXV_in_source_workflows" + elif reference_audio_img2vid: + model_for_segment = model + loop_model_sampling_report = "reference_audio_img2vid:no_ModelSamplingLTXV" + else: + model_for_segment = ModelSamplingLTXV.execute(model, float(max_shift), float(base_shift), av_latent)[0] + loop_model_sampling_report = f"ModelSamplingLTXV(max_shift={float(max_shift):.3f},base_shift={float(base_shift):.3f})" + guider = CFGGuider.execute(model_for_segment, segment_positive, segment_negative, float(effective_cfg_value))[0] + sampler = KSamplerSelect.execute(str(effective_sampler))[0] + if legacy_audio_img2vid_backend: + sigmas = _manual_sigmas(manual_sigmas or _REFERENCE_MANUAL_SIGMAS) + sigmas_report = f"legacy_exact_manual_sigmas({int(sigmas.numel())} values)" + else: + sigmas, sigmas_report = _main_sigmas( + uses_input_audio, + steps, + max_shift, + base_shift, + sigma_terminal, + av_latent, + generation_mode, + effective_media_mode, + manual_sigmas, + generation_type, + model_for_segment, + ) + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_sampler_prepared", + av_latent=av_latent, + sampler=effective_sampler, + cfg=effective_cfg_value, + sigmas=sigmas, + sigmas_report=sigmas_report, + seed=int(seed) + segment_index, + model_sampling=loop_model_sampling_report, + sampler_node="SamplerCustomAdvanced" if legacy_audio_img2vid_backend else ("IAMCCS_SamplerAdvancedVersion1(disable_progress=True,cleanup=True)" if reference_audio_img2vid and comfy_nodes.NODE_CLASS_MAPPINGS.get("IAMCCS_SamplerAdvancedVersion1") is not None else "SamplerCustomAdvanced"), + ) noise = RandomNoise.execute(int(seed) + segment_index)[0] - sampled_av = SamplerCustomAdvanced.sample(noise, guider, sampler, sigmas, av_latent)[0] + reference_sampler_cls = comfy_nodes.NODE_CLASS_MAPPINGS.get("IAMCCS_SamplerAdvancedVersion1") if reference_audio_img2vid and not legacy_audio_img2vid_backend else None + if reference_sampler_cls is not None: + sampled_av = reference_sampler_cls().sample( + noise, + guider, + sampler, + sigmas, + av_latent, + True, + True, + )[0] + loop_sampler_node_name = "IAMCCS_SamplerAdvancedVersion1(disable_progress=True,cleanup=True)" + else: + sampled_av = SamplerCustomAdvanced.sample(noise, guider, sampler, sigmas, av_latent)[0] + loop_sampler_node_name = "SamplerCustomAdvanced" sampled_video, sampled_audio_latent = LTXVSeparateAVLatent.execute(sampled_av) - segment_positive, segment_negative, sampled_video = LTXVCropGuides.execute( - segment_positive, - segment_negative, - sampled_video, + if reference_audio_img2vid and legacy_audio_img2vid_backend: + crop_guides_report = "legacy_exact:skipped_no_LTXVCropGuides_in_source_workflows" + elif reference_audio_img2vid: + crop_guides_report = "skipped_reference_audio_img2vid" + else: + segment_positive, segment_negative, sampled_video = LTXVCropGuides.execute( + segment_positive, + segment_negative, + sampled_video, + ) + crop_guides_report = "applied" + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_sampled", + sampler_node=loop_sampler_node_name, + crop_guides=crop_guides_report, + sampled_video=sampled_video, + sampled_audio_latent=sampled_audio_latent, ) stage_mode = str(second_stage_mode) @@ -1877,7 +3662,7 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: elif uses_source_anchor and segment_index > 0: reinject_strength = min(reinject_strength, float(effective_anchor_strength)) reinject_strength = _motion_guidance_strength(reinject_strength, motion_intensity) - if reinject_strength > 0.0 and not (is_t2v and segment_index == 0): + if reinject_strength > 0.0 and not is_t2v: stage2_width, stage2_height = _pixel_dims_from_latent(stage2_video_latent, vae) resized_guidance_image = _resize_image_to(guidance_image, stage2_width, stage2_height) preprocessed_guidance = LTXVPreprocess.execute(resized_guidance_image, int(image_compression))[0] @@ -1895,7 +3680,7 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: latent_stage2 = vram_flush_node.run(latent_stage2)[0] model_stage2 = ModelSamplingLTXV.execute(stage2_model_active, float(max_shift), float(base_shift), latent_stage2)[0] guider_stage2 = CFGGuider.execute(model_stage2, stage2_positive, stage2_negative, float(second_stage_cfg))[0] - sampler_stage2 = KSamplerSelect.execute("euler")[0] + sampler_stage2 = KSamplerSelect.execute(str(effective_sampler))[0] sigmas_stage2 = _manual_sigmas(second_stage_manual_sigmas) noise_stage2 = RandomNoise.execute(int(seed) + segment_index)[0] sampled_stage2_av = SamplerCustomAdvanced.sample( @@ -1905,12 +3690,24 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: sigmas_stage2, latent_stage2, )[0] - sampled_video = LTXVSeparateAVLatent.execute(sampled_stage2_av)[0] + sampled_video, sampled_audio_latent = LTXVSeparateAVLatent.execute(sampled_stage2_av) stage2_applied = True + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_second_stage_applied", + mode=second_stage_mode, + scale_mode=second_stage_scale_mode, + guidance_source=guidance_source, + reinject_strength=reinject_strength, + sigmas_stage2=sigmas_stage2, + sampled_video=sampled_video, + sampled_audio_latent=sampled_audio_latent, + ) else: guidance_source = "none" + _pipeline_debug_step(pipeline_debug, f"segment_{segment_index}_second_stage_skipped", mode=second_stage_mode) stage2_segment_report = ( - f"on({int(second_stage_step_count)}step,{second_stage_model_source},{second_stage_scale_mode})" + f"{('auto_' + str(reference_stage2_auto)) if reference_stage2_auto else 'on'}({int(second_stage_step_count)}step,{second_stage_model_source},{second_stage_scale_mode})" if stage2_applied else "off" ) @@ -1940,6 +3737,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: anti_drift_parts.append(f"identity:{identity_persistence_strength:.2f}") if anti_drift_parts: anti_drift_report = "+".join(anti_drift_parts) + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_anti_drift", + anti_drift_mode=anti_drift_mode, + anti_drift_report=anti_drift_report, + rolling_reference=rolling_reference_latent is not None, + identity_reference=identity_reference_latent is not None, + ) if identity_reference_latent is None: identity_reference_latent = _clone_latent(sampled_video) @@ -1947,9 +3752,15 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: _hard_unload_all_models() if use_in_memory_loop: - decoded_images = _decode_images_in_memory(sampled_video, vae, 512, 64) + decoded_images, loop_decode_report = _decode_images_for_vae_mode( + sampled_video, + vae, + _modular_decode_to_vae_mode(modular_decode), + 512, + 64, + ) frames_saved = int(decoded_images.shape[0]) - overlay_report = "overlay=off(in-memory)" + overlay_report = f"overlay=off(in-memory) decode={loop_decode_report}" if segment_index == 0: ext_out = extension_node_mem.process_extension( decoded_images, @@ -1960,9 +3771,9 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "none", "none", start_frames_rule, - "none", - 0.0, - 0, + color_match_mode, + float(color_match_strength), + 8, "none", 0, 1.0, @@ -1983,9 +3794,9 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "none", "none", start_frames_rule, - "none", - 0.0, - 0, + color_match_mode, + float(color_match_strength), + 8, "none", 0, 1.0, @@ -2074,6 +3885,16 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: 1, ) + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_decoded_and_stitched", + decode_backend="in_memory" if use_in_memory_loop else "disk", + frames_saved=int(frames_saved), + overlay_report=overlay_report, + extension_report=ext_out[5], + extended_dir=extended_dir if not use_in_memory_loop else "in-memory", + start_dir=start_dir if not use_in_memory_loop else "in-memory", + ) gate_out = audio_gate_node.decide( remaining_frames_after, effective_unique_frames, @@ -2087,6 +3908,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: segment_reports.append( f"seg{segment_index}: raw={current_segment_raw_frames}f unique={current_segment_unique_frames}f effective={effective_unique_frames}f saved={int(frames_saved)}f init={init_mode} tail={tail_refresh_report} anchor={anchor_refresh_report} stage2={stage2_segment_report} guidance={guidance_source} anti_drift={anti_drift_report} {overlay_report} | {ext_out[5]}" ) + _pipeline_debug_step( + pipeline_debug, + f"segment_{segment_index}_gate", + gate_decision=int(gate_out[0]), + cursor_frames=cursor_frames, + rendered_segments=rendered_segments, + is_last_segment=is_last_segment, + ) _soft_cleanup() if int(gate_out[0]) == 0: break @@ -2094,23 +3923,33 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: final_frames_dir = extended_dir if not use_in_memory_loop else "" final_start_dir = start_dir if not use_in_memory_loop else "" final_report_hint = final_frames_dir if final_frames_dir else "(in-memory images)" - rendered_audio = raw_audio - if generates_audio: - total_audio_frames = max( - 1, - _to_int( - _parse_payload(plan_payload), - "total_frames", - int(round(float(total_duration_seconds) * float(fps_value))), - ), - ) - rendered_audio_latent = generated_audio_latent_node.execute( - int(total_audio_frames), - max(1, int(round(fps_value))), - 1, - audio_vae, - )[0] - rendered_audio = generated_audio_decode_node.execute(rendered_audio_latent, audio_vae)[0] + rendered_audio = raw_audio if uses_input_audio else None + loop_audio_export_note = "input_audio_passthrough" if uses_input_audio else "off" + if generates_audio and exports_audio: + loop_audio_export_note = "off(loop_generated_audio_not_muxed)" + _pipeline_debug_step( + pipeline_debug, + "loop_completed", + rendered_segments=rendered_segments, + segment_count=segment_count, + final_frames_dir=final_frames_dir, + final_start_dir=final_start_dir, + audio_export=rendered_audio is not None, + audio_export_note=loop_audio_export_note, + ) + disable_vae_frame_align = bool(reference_audio_img2vid) + loop_debug_report = _pipeline_debug_text(pipeline_debug) + plan_data_for_frame_target = _parse_payload(plan_payload) + render_target_frame_count = ( + _to_int(plan_data_for_frame_target, "target_frame_count", 0) + or _to_int(plan_data_for_frame_target, "audio_duration_frames_with_tail", 0) + or _to_int(plan_data_for_frame_target, "total_frames", 0) + ) + render_target_frame_count_source = "planner_payload" if render_target_frame_count > 0 else "render_actual_frames" + if render_target_frame_count <= 0 and final_frames_dir: + render_target_frame_count = len(_frame_files_in_dir(final_frames_dir)) + if render_target_frame_count <= 0 and current_extended_images is not None: + render_target_frame_count = int(current_extended_images.shape[0]) report = ( _render_status( @@ -2132,12 +3971,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: + "\n" + planner_report_line + "\n" + + f"Prompt route. positive=\"{prompt_excerpt}\"\n" + f"Audio preprocess. melband_enabled={melband_enabled} | {audio_preprocess_report}\n" - + f"Render route. backend_requested={requested_backend_mode} | backend_resolved={backend_mode} | media_mode={effective_media_mode} | decode_mode={modular_decode} | generation_mode={generation_mode} | motion_intensity={motion_intensity:.2f} | vram_flush={'on' if bool(vram_flush) else 'off'}\n" + + f"Render route. backend_requested={requested_backend_mode} | backend_resolved={backend_mode} | media_mode={effective_media_mode} | decode_mode={modular_decode} | generation_mode={generation_mode} | sampler={effective_sampler} | cfg={float(effective_cfg_value):.3f} | sigmas={sigmas_report} | audio_export={'yes' if rendered_audio is not None else 'no'} | audio_export_note={loop_audio_export_note} | motion_intensity={motion_intensity:.2f} | vram_flush={'on' if bool(vram_flush) else 'off'} | vae_frame_align={'off_iamccs_audio_img2vid' if disable_vae_frame_align else 'on'}\n" + f"Executable AU+IMG2VID render completed. segments_rendered={rendered_segments}/{segment_count} | " f"frames_dir={final_report_hint} | start_dir={final_start_dir or '(in-memory start_images)'}\n" + "\n".join(segment_reports) ) + report = _append_debug_report(report, loop_debug_report) render_linx = build_stage_linx_payload( linx, "exec_render", @@ -2155,9 +3996,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "anti_drift_mode": str(anti_drift_mode), "anti_drift_strength": float(anti_drift_strength), "identity_persistence_strength": float(identity_persistence_strength), - "motion_intensity": float(motion_intensity), - "generated_media_duration_seconds": float(generated_media_duration_seconds), - "vram_flush": bool(vram_flush), + "motion_intensity": float(motion_intensity), + "generated_media_duration_seconds": float(generated_media_duration_seconds), + "generated_media_fps": float(fps_value), + "target_frame_count": int(render_target_frame_count), + "target_frame_count_source": str(render_target_frame_count_source), + "disable_vae_frame_align": disable_vae_frame_align, + "vram_flush": bool(vram_flush), + "debug_verbose": bool(debug_verbose), "second_stage_mode": str(second_stage_mode), "second_stage_scale_mode": str(second_stage_scale_mode), "second_stage_upscale_model_name": str(second_stage_upscale_model_name), @@ -2166,12 +4012,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "downstream_stage_mode": str(downstream_stage_mode), "segment_count": int(segment_count), "segments_rendered": int(rendered_segments), + "pipeline_debug_steps": len(pipeline_debug.get("lines") or []), + "debug_verbose": bool(debug_verbose), }, report, unique_id=unique_id, requires={ "resources": {"model": "MODEL", "clip": "CLIP", "vae": "VAE", "audio_vae": "VAE", "planner_payload": "STRING", "fps": "FLOAT"}, - "input_audio_modes": {"audio": "AUDIO", "audio_conditioning_single": "AUDIO", "audio_conditioning_segmented": "AUDIO"}, + "input_audio_modes": {"audio": "AUDIO", "audio_ltx_conditioning": "AUDIO", "audio_conditioning_single": "AUDIO", "audio_conditioning_segmented": "AUDIO"}, "pure_or_generated_modes": {"audio": "optional final audio only"}, }, slot_map={ @@ -2183,6 +4031,8 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: policies={ "decode_mode": str(modular_decode), "stitch_preset": str(stitch_preset), + "color_match_mode": str(color_match_mode), + "color_match_strength": float(color_match_strength), "audio_context_mode": str(audio_context_mode), "continuity_anchor_mode": str(continuity_settings["mode"]), "anchor_refresh_interval": int(continuity_settings["interval"]), @@ -2203,9 +4053,14 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "decode_mode": str(modular_decode), "media_mode": str(effective_media_mode), "generation_type": str(generation_type), + "target_frame_count": int(render_target_frame_count), + "target_frame_count_source": str(render_target_frame_count_source), + "disable_vae_frame_align": disable_vae_frame_align, + "pipeline_debug": list(pipeline_debug.get("lines") or []), + "debug_verbose": bool(debug_verbose), }, resources={ - "audio": rendered_audio, + "audio": rendered_audio if exports_audio else None, "model": model, "clip": clip, "vae": vae, @@ -2216,6 +4071,10 @@ class IAMCCS_GC_AUIMG2VIDExecutableRender: "planner_payload": str(plan_payload), "media_mode": str(effective_media_mode), "generation_mode": str(generation_mode), + "target_frame_count": int(render_target_frame_count), + "target_frame_count_source": str(render_target_frame_count_source), + "disable_vae_frame_align": disable_vae_frame_align, + "debug_verbose": bool(debug_verbose), "second_stage_model": stage2_model_active, "second_stage_payload": second_stage_payload, "anti_drift_mode": str(anti_drift_mode), @@ -2239,7 +4098,7 @@ class IAMCCS_GC_AUIMG2VIDExecutableFinalize: "required": { "frames_dir": ("STRING",), "audio": ("AUDIO",), - "frame_rate": ("FLOAT", {"default": 24.0, "min": 1.0, "max": 240.0, "step": 0.01}), + "frame_rate": ("FLOAT", {"default": _DEFAULT_GENERATED_FPS, "min": 1.0, "max": 240.0, "step": 0.01}), "filename_prefix": ("STRING", {"default": "IAMCCS/GC_AUIMG2VID_EXEC"}), "crf": ("INT", {"default": 19, "min": 0, "max": 51, "step": 1}), "pix_fmt": (["yuv420p", "yuv444p"], {"default": "yuv420p"}), @@ -2247,10 +4106,25 @@ class IAMCCS_GC_AUIMG2VIDExecutableFinalize: }, "optional": { "linx": (SUPERNODE_LINX_TYPE,), + "debug_verbose": ("BOOLEAN", {"default": False}), }, } - def finalize(self, frames_dir, audio, frame_rate, filename_prefix, crf, pix_fmt, trim_to_audio, linx=None): + def finalize(self, frames_dir, audio, frame_rate, filename_prefix, crf, pix_fmt, trim_to_audio, debug_verbose=False, linx=None): + debug_verbose = _debug_verbose_enabled(debug_verbose, linx) + pipeline_debug = _pipeline_debug_new("exec_finalize", debug_verbose) + _pipeline_debug_step( + pipeline_debug, + "input_received", + frames_dir=frames_dir, + audio=audio, + frame_rate=frame_rate, + filename_prefix=filename_prefix, + crf=crf, + pix_fmt=pix_fmt, + trim_to_audio=trim_to_audio, + linx_keys=",".join(str(k) for k in ((linx or {}).get("resource_keys") or [])), + ) combine_node = _node_class("IAMCCS_VideoCombineFromDir")() video_path, report = combine_node.combine( frames_dir, @@ -2261,6 +4135,8 @@ class IAMCCS_GC_AUIMG2VIDExecutableFinalize: bool(trim_to_audio), audio, ) + _pipeline_debug_step(pipeline_debug, "vhs_combine", video_path=video_path, combine_report=report) + report = _append_debug_report(report, _pipeline_debug_text(pipeline_debug)) finalize_linx = build_stage_linx_payload( linx, "exec_finalize", @@ -2269,6 +4145,7 @@ class IAMCCS_GC_AUIMG2VIDExecutableFinalize: "filename_prefix": str(filename_prefix), "frame_rate": float(frame_rate), "trim_to_audio": bool(trim_to_audio), + "pipeline_debug_steps": len(pipeline_debug.get("lines") or []), }, report, slot_map={ @@ -2279,6 +4156,11 @@ class IAMCCS_GC_AUIMG2VIDExecutableFinalize: outputs={ "video_path": video_path, "frames_dir": str(frames_dir), + "pipeline_debug": list(pipeline_debug.get("lines") or []), + "debug_verbose": bool(debug_verbose), + }, + resources={ + "debug_verbose": bool(debug_verbose), }, ) return (video_path, finalize_linx, report) @@ -2295,8 +4177,8 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: def INPUT_TYPES(cls): return { "required": { - "frame_rate": ("FLOAT", {"default": 24.0, "min": 1.0, "max": 240.0, "step": 0.01}), - "decode_mode": (_VAE_DECODE_MODES, {"default": "inherit_render_backend"}), + "frame_rate": ("FLOAT", {"default": _DEFAULT_GENERATED_FPS, "min": 1.0, "max": 240.0, "step": 0.01}), + "decode_mode": (_VAE_DECODE_MODES, {"default": "normal_tiled_iamccs"}), "filename_prefix": ("STRING", {"default": "IAMCCS/GC_AUIMG2VID_EXEC"}), "output_root": ("STRING", {"default": "iamccs_gc_auimg2vid/final_vae"}), "frames_subdir": ("STRING", {"default": "frames"}), @@ -2304,19 +4186,23 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: "jpg_quality": ("INT", {"default": 95, "min": 1, "max": 100, "step": 1}), "tiled_tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), "tiled_overlap": ("INT", {"default": 64, "min": 0, "max": 512, "step": 1}), + "tiled_temporal_size": ("INT", {"default": 256, "min": 8, "max": 4096, "step": 4}), + "tiled_temporal_overlap": ("INT", {"default": 32, "min": 0, "max": 1024, "step": 4}), + "cleanup_before_decode": ("BOOLEAN", {"default": False}), "crf": ("INT", {"default": 19, "min": 0, "max": 51, "step": 1}), "pix_fmt": (["yuv420p", "yuv444p"], {"default": "yuv420p"}), "trim_to_audio": ("BOOLEAN", {"default": True}), "save_metadata": ("BOOLEAN", {"default": False}), "vram_flush": ("BOOLEAN", {"default": False}), - "ui_preset": (["custom", "very_low_ram_decode", "low_ram_safe", "balanced", "high_quality", "fast_preview"], {"default": "custom"}), + "ui_preset": (["custom", "balanced", "high_quality", "fast_preview", "low_ram_safe", "very_low_ram_decode"], {"default": "custom"}), }, "optional": { - "audio": ("AUDIO",), + "audio": ("AUDIO", {"lazy": True}), "video_latent": ("LATENT",), "vae": ("VAE",), "frames_dir": ("STRING",), "linx": (SUPERNODE_LINX_TYPE,), + "debug_verbose": ("BOOLEAN", {"default": False}), }, "hidden": { "prompt": "PROMPT", @@ -2324,6 +4210,42 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: }, } + def check_lazy_status( + self, + frame_rate=_DEFAULT_GENERATED_FPS, + decode_mode="normal_tiled_iamccs", + filename_prefix="IAMCCS/GC_AUIMG2VID_EXEC", + output_root="iamccs_gc_auimg2vid/final_vae", + frames_subdir="frames", + image_format="jpg", + jpg_quality=95, + tiled_tile_size=512, + tiled_overlap=64, + tiled_temporal_size=256, + tiled_temporal_overlap=32, + cleanup_before_decode=False, + crf=19, + pix_fmt="yuv420p", + trim_to_audio=True, + save_metadata=False, + vram_flush=False, + ui_preset="custom", + debug_verbose=False, + audio=None, + video_latent=None, + vae=None, + frames_dir="", + linx=None, + **kwargs, + ): + media_mode_hint = str(linx_output(linx, "media_mode", "") or linx_resource(linx, "media_mode", "") or "") + generation_type_hint = str(linx_output(linx, "generation_type", "") or "") + if _pure_media_no_external_audio(media_mode_hint, generation_type_hint): + return [] + if linx_resource(linx, "audio", None) is not None: + return [] + return ["audio"] if audio is None else [] + def decode_and_combine( self, frame_rate, @@ -2335,12 +4257,16 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: jpg_quality, tiled_tile_size, tiled_overlap, + tiled_temporal_size, + tiled_temporal_overlap, + cleanup_before_decode, crf, pix_fmt, trim_to_audio, save_metadata, vram_flush=False, ui_preset="custom", + debug_verbose=False, audio=None, video_latent=None, vae=None, @@ -2349,12 +4275,54 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: prompt=None, extra_pnginfo=None, ): - audio = _require_runtime_value(_input_or_linx(audio, linx, "audio"), "audio") + debug_verbose = _debug_verbose_enabled(debug_verbose, linx) + pipeline_debug = _pipeline_debug_new("exec_vae", debug_verbose) + _pipeline_debug_step( + pipeline_debug, + "input_received", + decode_mode=decode_mode, + frame_rate=frame_rate, + audio_connected=audio is not None, + video_latent_connected=video_latent is not None, + vae_connected=vae is not None, + frames_dir=frames_dir, + linx_keys=",".join(str(k) for k in ((linx or {}).get("resource_keys") or [])), + ) + media_mode_hint = str(linx_output(linx, "media_mode", "") or linx_resource(linx, "media_mode", "") or "") + generation_type_hint = str(linx_output(linx, "generation_type", "") or "") + pure_no_audio = _pure_media_no_external_audio(media_mode_hint, generation_type_hint) + if pure_no_audio: + audio = linx_resource(linx, "audio", None) + audio_source = "linx_generated" if audio is not None else "pure_mode_silent" + else: + linx_audio = linx_resource(linx, "audio", None) + audio = linx_audio if linx_audio is not None else audio + audio_source = "linx_input_passthrough" if linx_audio is not None else ("direct_input" if audio is not None else "missing_input") + if audio is None and not pure_no_audio: + _pipeline_debug_step(pipeline_debug, "error_missing_audio", pure_no_audio=pure_no_audio, media_mode=media_mode_hint, generation_type=generation_type_hint) + audio = _require_runtime_value(audio, "audio") + _pipeline_debug_step( + pipeline_debug, + "audio_resolved", + pure_no_external_audio=pure_no_audio, + media_mode_hint=media_mode_hint, + generation_type_hint=generation_type_hint, + audio_source=audio_source, + audio=audio, + ) vae = _input_or_linx(vae, linx, "vae") if video_latent is None: video_latent = _input_or_linx(None, linx, "video_latent") rendered_images = _input_or_linx(None, linx, "rendered_images") - frame_rate = float(_inherit_widget_value(frame_rate, 24.0, linx, "fps")) + _pipeline_debug_step( + pipeline_debug, + "video_source_resolved", + vae=vae, + video_latent=video_latent, + rendered_images=rendered_images, + frames_dir=frames_dir, + ) + frame_rate = float(_inherit_widget_value(frame_rate, _DEFAULT_GENERATED_FPS, linx, "fps")) decode_mode = _normalize_modular_decode_mode( _inherit_widget_value(decode_mode, "inherit_render_backend", linx, "decode_mode"), linx_output(linx, "backend_mode", "auto"), @@ -2363,29 +4331,51 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: run_root = _resolve_output_path(output_root) target_frames_dir = os.path.join(run_root, str(frames_subdir or "frames")) actual_frames_dir = str(frames_dir or "").strip() + target_frame_count, target_frame_count_source = _resolve_linx_frame_target(linx) decode_report = "" + frame_align_report = "frame_align=off" resolved_decode_mode = _modular_decode_to_vae_mode(decode_mode) render_backend_mode = str(linx_output(linx, "backend_mode", "auto") or "auto") images_out = None vram_flush_enabled = bool(vram_flush) + _pipeline_debug_step( + pipeline_debug, + "decode_route_resolved", + decode_requested=decode_mode, + decode_resolved=resolved_decode_mode, + render_backend=render_backend_mode, + output_root=output_root, + target_frames_dir=target_frames_dir, + tile_size=tiled_tile_size, + overlap=tiled_overlap, + temporal_size=tiled_temporal_size, + temporal_overlap=tiled_temporal_overlap, + cleanup_before_decode=cleanup_before_decode, + target_frame_count=target_frame_count, + target_frame_count_source=target_frame_count_source, + ) if vram_flush_enabled: _hard_unload_all_models() + _pipeline_debug_step(pipeline_debug, "pre_decode_vram_flush", vram_flush=True) if rendered_images is not None: - images_out = rendered_images + images_out, frame_align_report = _align_images_to_frame_count(rendered_images, target_frame_count) actual_frames_dir, frames_saved = _images_to_dir( - rendered_images, + images_out, target_frames_dir, "frame", image_format, int(jpg_quality), True, ) - decode_report = f"decode_mode={decode_mode} used in-memory rendered images -> saved {frames_saved} frames to {actual_frames_dir}" + decode_report = f"decode_mode={decode_mode} used in-memory rendered images | {frame_align_report} -> saved {frames_saved} frames to {actual_frames_dir}" + _pipeline_debug_step(pipeline_debug, "decode_from_rendered_images", frames_saved=frames_saved, actual_frames_dir=actual_frames_dir, images_out=images_out, frame_align_report=frame_align_report) elif video_latent is not None and vae is not None: if str(resolved_decode_mode) in {"low_ram_disk", "very_low_ram_disk"}: vae_decode_node = _node_class("IAMCCS_VAEDecodeToDisk")() - very_low_ram = str(resolved_decode_mode) == "very_low_ram_disk" + if bool(cleanup_before_decode): + _soft_cleanup() + _pipeline_debug_step(pipeline_debug, "cleanup_before_disk_decode", cleanup_before_decode=True) actual_frames_dir, _, _ = vae_decode_node.decode_to_disk( video_latent, vae, @@ -2394,43 +4384,79 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: image_format, int(jpg_quality), True, - "manual" if very_low_ram else "auto", - 256 if very_low_ram else 512, - 32 if very_low_ram else 64, + "manual", + int(tiled_tile_size), + int(tiled_overlap), False, os.path.join(run_root, "seam_debug"), True, True, 0, ) - decode_report = f"decode_mode={resolved_decode_mode} -> {actual_frames_dir}" + frames_saved, frame_align_report = _align_frame_dir_to_frame_count(actual_frames_dir, target_frame_count, "frame") + decode_report = ( + f"decode_node=IAMCCS_VAEDecodeToDisk | mode={resolved_decode_mode} | " + f"tiling_mode=manual | tile_size={int(tiled_tile_size)} | overlap={int(tiled_overlap)} | " + f"temporal_size={int(tiled_temporal_size)} | temporal_overlap={int(tiled_temporal_overlap)} | " + f"cleanup_before_decode={'on' if bool(cleanup_before_decode) else 'off'} | " + f"image_format={image_format} | jpg_quality={int(jpg_quality)} | {frame_align_report} -> {actual_frames_dir}" + ) images_out = _load_images_from_dir_for_output(actual_frames_dir) + _pipeline_debug_step(pipeline_debug, "decode_to_disk", frames_saved=frames_saved, actual_frames_dir=actual_frames_dir, images_out=images_out, decode_report=decode_report, frame_align_report=frame_align_report) else: - actual_decode_mode = str(resolved_decode_mode) - if actual_decode_mode == "custom_mode": - actual_decode_mode = "normal_tiled" - if actual_decode_mode == "normal_tiled": - decoded_images = _decode_images_in_memory(video_latent, vae, int(tiled_tile_size), int(tiled_overlap)) - else: - decoded_images = comfy_nodes.VAEDecode().decode(vae, video_latent)[0] - images_out = decoded_images + decoded_images, vae_decode_report = _decode_images_for_vae_mode( + video_latent, + vae, + resolved_decode_mode, + int(tiled_tile_size), + int(tiled_overlap), + int(tiled_temporal_size), + int(tiled_temporal_overlap), + bool(cleanup_before_decode), + ) + images_out, frame_align_report = _align_images_to_frame_count(decoded_images, target_frame_count) actual_frames_dir, frames_saved = _images_to_dir( - decoded_images, + images_out, target_frames_dir, "frame", image_format, int(jpg_quality), True, ) - decode_report = f"decode_mode={decode_mode} -> saved {frames_saved} frames to {actual_frames_dir}" + decode_report = f"{vae_decode_report} -> saved {frames_saved} frames to {actual_frames_dir}" + if frame_align_report != "frame_align=off": + decode_report = f"{vae_decode_report} | {frame_align_report} -> saved {frames_saved} frames to {actual_frames_dir}" + _pipeline_debug_step(pipeline_debug, "decode_in_memory", frames_saved=frames_saved, actual_frames_dir=actual_frames_dir, images_out=images_out, decode_report=decode_report, frame_align_report=frame_align_report) elif actual_frames_dir: - decode_report = f"decode_mode={decode_mode} bypassed because frames_dir was provided directly: {actual_frames_dir}" + upstream_decode = "unknown" + if str(resolved_decode_mode) == "normal_tiled_vhs": + upstream_decode = "IAMCCS_VAEDecodeTiledSafe" + elif str(resolved_decode_mode) == "high_vram": + upstream_decode = "VAEDecode" + elif str(resolved_decode_mode) in {"low_ram_disk", "very_low_ram_disk"}: + upstream_decode = "IAMCCS_VAEDecodeToDisk" + decode_report = ( + f"decode_node=predecoded_frames | requested={decode_mode} | resolved={resolved_decode_mode} | " + f"upstream_decode_node={upstream_decode} | frames_dir={actual_frames_dir}" + ) + frames_saved, frame_align_report = _align_frame_dir_to_frame_count(actual_frames_dir, target_frame_count, "frame") + if frame_align_report != "frame_dir_align=off": + decode_report = f"{decode_report} | {frame_align_report}" images_out = _load_images_from_dir_for_output(actual_frames_dir) + _pipeline_debug_step(pipeline_debug, "decode_predecoded_frames", frames_saved=frames_saved, actual_frames_dir=actual_frames_dir, images_out=images_out, upstream_decode=upstream_decode, frame_align_report=frame_align_report) else: + _pipeline_debug_step(pipeline_debug, "error_missing_video_source", video_latent=video_latent, vae=vae, frames_dir=actual_frames_dir) raise ValueError("Executable VAE requires either video_latent+vae or frames_dir") if vram_flush_enabled: _soft_cleanup() + _pipeline_debug_step(pipeline_debug, "post_decode_soft_cleanup", vram_flush=True) + + print( + "[IAMCCS SuperNodes VAE] " + f"decode_route requested={decode_mode} resolved={resolved_decode_mode} " + f"backend={render_backend_mode} | {decode_report}" + ) combine_node = _node_class("IAMCCS_VideoCombineFromDir")() video_path, combine_report = combine_node.combine( @@ -2439,9 +4465,24 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: filename_prefix, int(crf), pix_fmt, - bool(trim_to_audio), + bool(trim_to_audio) and audio is not None, audio, ) + combine_report = f"finalize_node=IAMCCS_VideoCombineFromDir | {combine_report}" + _pipeline_debug_step( + pipeline_debug, + "vhs_combine", + video_path=video_path, + frame_rate=frame_rate, + crf=crf, + pix_fmt=pix_fmt, + trim_to_audio=bool(trim_to_audio) and audio is not None, + audio_mux=audio is not None, + target_frame_count=target_frame_count, + target_frame_count_source=target_frame_count_source, + frame_align_report=frame_align_report, + combine_report=combine_report, + ) metadata_report = "metadata=off" if bool(save_metadata): metadata_payload = { @@ -2458,9 +4499,15 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: "jpg_quality": int(jpg_quality), "tiled_tile_size": int(tiled_tile_size), "tiled_overlap": int(tiled_overlap), + "tiled_temporal_size": int(tiled_temporal_size), + "tiled_temporal_overlap": int(tiled_temporal_overlap), + "cleanup_before_decode": bool(cleanup_before_decode), "crf": int(crf), "pix_fmt": str(pix_fmt), "trim_to_audio": bool(trim_to_audio), + "target_frame_count": int(target_frame_count), + "target_frame_count_source": str(target_frame_count_source), + "frame_align_report": str(frame_align_report), "save_metadata": bool(save_metadata), "vram_flush": bool(vram_flush_enabled), "ui_preset": str(ui_preset), @@ -2472,11 +4519,19 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: metadata_path = _save_video_metadata_sidecar(video_path, metadata_payload) if metadata_path: metadata_report = f"metadata={metadata_path}" + _pipeline_debug_step(pipeline_debug, "metadata", save_metadata=save_metadata, metadata_report=metadata_report) + vae_debug_report = _pipeline_debug_text(pipeline_debug) report = ( f"Executable VAE. backend_mode={render_backend_mode} | decode_requested={decode_mode} | " - f"decode_resolved={resolved_decode_mode} | frame_rate={float(frame_rate):.3f} | vram_flush={'on' if vram_flush_enabled else 'off'}\n" + f"decode_resolved={resolved_decode_mode} | frame_rate={float(frame_rate):.3f} | " + f"target_frame_count={int(target_frame_count)} ({target_frame_count_source}) | {frame_align_report} | " + f"tile_size={int(tiled_tile_size)} | overlap={int(tiled_overlap)} | " + f"temporal_size={int(tiled_temporal_size)} | temporal_overlap={int(tiled_temporal_overlap)} | " + f"cleanup_before_decode={'on' if bool(cleanup_before_decode) else 'off'} | " + f"audio_mux={'on' if audio is not None else 'off'} | audio_source={audio_source} | vram_flush={'on' if vram_flush_enabled else 'off'}\n" f"{decode_report} | {combine_report} | {metadata_report}" ) + report = _append_debug_report(report, vae_debug_report) vae_linx = build_stage_linx_payload( linx, "exec_vae", @@ -2487,13 +4542,21 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: "frames_dir": str(actual_frames_dir), "tiled_tile_size": int(tiled_tile_size), "tiled_overlap": int(tiled_overlap), + "tiled_temporal_size": int(tiled_temporal_size), + "tiled_temporal_overlap": int(tiled_temporal_overlap), + "cleanup_before_decode": bool(cleanup_before_decode), + "debug_verbose": bool(debug_verbose), + "target_frame_count": int(target_frame_count), + "target_frame_count_source": str(target_frame_count_source), + "frame_align_report": str(frame_align_report), "save_metadata": bool(save_metadata), "vram_flush": bool(vram_flush_enabled), "ui_preset": str(ui_preset), + "pipeline_debug_steps": len(pipeline_debug.get("lines") or []), }, report, requires={ - "resources": {"audio": "AUDIO", "vae": "VAE", "fps": "FLOAT"}, + "resources": {"audio": "AUDIO from input-audio modes or generated pure-mode linx", "vae": "VAE", "fps": "FLOAT"}, "one_of": {"video_source": ["video_latent", "rendered_images", "frames_dir"]}, }, slot_map={ @@ -2504,15 +4567,26 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: outputs={ "video_path": video_path, "frames_dir": str(actual_frames_dir), + "audio_passthrough": "present" if audio is not None else "missing", "metadata_saved": bool(save_metadata), + "target_frame_count": int(target_frame_count), + "target_frame_count_source": str(target_frame_count_source), + "frame_align_report": str(frame_align_report), + "pipeline_debug": list(pipeline_debug.get("lines") or []), + "debug_verbose": bool(debug_verbose), }, resources={ "audio": audio, + "audio_passthrough": audio, "vae": vae, "fps": float(frame_rate), + "target_frame_count": int(target_frame_count), + "target_frame_count_source": str(target_frame_count_source), + "frame_align_report": str(frame_align_report), "decode_mode": str(resolved_decode_mode), "output_root": str(output_root), "rendered_images": images_out, + "debug_verbose": bool(debug_verbose), }, ) if images_out is None: @@ -2522,7 +4596,3 @@ class IAMCCS_GC_AUIMG2VIDExecutableVAE: - - - - diff --git a/iamccs_supernodes_second_stage.py b/iamccs_supernodes_second_stage.py index aaa8300..697669f 100644 --- a/iamccs_supernodes_second_stage.py +++ b/iamccs_supernodes_second_stage.py @@ -42,7 +42,7 @@ class IAMCCS_SuperNodes_SecondStage: "required": { **merge_backend_panel_inputs({ "second_stage_mode": (["off", "latent_upscale_refine", "latent_upscale_refine_x2_beta"],), - "stage2_model_policy": (["replace_stage1_if_connected", "keep_stage1_model", "prefer_stage2_else_primary"],), + "stage2_model_policy": (["replace_stage1_if_connected", "stage2_model_if_connected", "prefer_stage2_else_primary", "keep_stage1_model"],), "second_stage_upscale_model": (_LATENT_UPSCALE_MODEL_NAMES, {"default": _LATENT_UPSCALE_MODEL_NAMES[0]}), "second_stage_reinject_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), "second_stage_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1}), @@ -51,7 +51,7 @@ class IAMCCS_SuperNodes_SecondStage: }, "optional": { "stage2_model": ("MODEL", {"lazy": True}), - "linx": (SUPERNODE_LINX_TYPE,), + "linx": (SUPERNODE_LINX_TYPE, {"lazy": True}), "model": ("MODEL", {"lazy": True}), }, "hidden": { @@ -59,11 +59,13 @@ class IAMCCS_SuperNodes_SecondStage: }, } - def check_lazy_status(self, second_stage_mode, stage2_model=None, model=None, **kwargs): + def check_lazy_status(self, second_stage_mode, stage2_model_policy="replace_stage1_if_connected", stage2_model=None, model=None, linx=None, **kwargs): if str(second_stage_mode) == "off": return [] + if str(stage2_model_policy) == "keep_stage1_model": + return [] if model is not None or linx is not None else ["model", "linx"] needed = [] - if stage2_model is None: + if stage2_model is None and model is None: needed.append("stage2_model") return needed @@ -137,7 +139,7 @@ class IAMCCS_SuperNodes_SecondStage: selected_model = primary_model policy = str(stage2_model_policy) if connected_stage2_model is not None: - if policy in {"replace_stage1_if_connected", "prefer_stage2_else_primary"}: + if policy in {"replace_stage1_if_connected", "stage2_model_if_connected", "prefer_stage2_else_primary"}: selected_model = connected_stage2_model elif selected_model is None: selected_model = connected_stage2_model diff --git a/version.json b/version.json index 8ae1cc6..ef14f64 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "name": "iamccs-nodes", - "version": "1.4.3", + "version": "1.4.4", "author": "Carmine Cristallo Scalzi (IAMCCS)", "description": "IAMCCS nodes for ComfyUI: IAMCCS echosystem for ComfyUI, nodes 4 LoRA, WAN 2.2, WAN 2.1 and LTX-2 pipelines, WANIMAGEMOTION pro added." } diff --git a/web/iamccs_supernodes_exec_ui.js b/web/iamccs_supernodes_exec_ui.js index d23e355..6a9b8c4 100644 --- a/web/iamccs_supernodes_exec_ui.js +++ b/web/iamccs_supernodes_exec_ui.js @@ -1,76 +1,59 @@ import { app } from "../../../scripts/app.js"; +const IAMCCS_SUPERNODES_EXEC_UI_VERSION = "2026-05-03-text-audio-duration-v6"; + const PRESET_CONFIGS = { "IAMCCS-SuperNodes AU+IMG2VID Exec Render": { presetWidget: "ui_preset", defaultPreset: "custom", values: { - low_ram_safe: { backend_mode: "auto", vae_mode: "very_low_ram_disk", steps: 16, image_compression: 40, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "tail_only", anchor_refresh_interval: 2, anchor_image_strength: 0.2, anti_drift_mode: "rolling_adain", anti_drift_strength: 0.1, identity_persistence_strength: 0.0, generation_mode: "img2vid", media_mode: "auto_from_generation_mode", vram_flush: true, motion_intensity: 1.0, second_stage_mode: "off", stage2_model_policy: "stage2_model_if_connected", second_stage_reinject_strength: 0.0 }, - balanced: { backend_mode: "auto", vae_mode: "normal_tiled_vhs", steps: 20, image_compression: 28, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "tail_only", anchor_refresh_interval: 2, anchor_image_strength: 0.25, anti_drift_mode: "rolling_adain", anti_drift_strength: 0.12, identity_persistence_strength: 0.0, generation_mode: "img2vid", media_mode: "auto_from_generation_mode", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off", stage2_model_policy: "stage2_model_if_connected", second_stage_reinject_strength: 0.0 }, - high_quality: { backend_mode: "auto", vae_mode: "high_vram", steps: 24, image_compression: 20, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "tail_only", anchor_refresh_interval: 1, anchor_image_strength: 0.3, anti_drift_mode: "dual_reference_adain", anti_drift_strength: 0.16, identity_persistence_strength: 0.06, generation_mode: "img2vid", media_mode: "auto_from_generation_mode", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off", stage2_model_policy: "stage2_model_if_connected", second_stage_reinject_strength: 0.0 }, - fast_preview: { backend_mode: "single_best", vae_mode: "low_ram_disk", steps: 12, image_compression: 45, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0, generation_mode: "img2vid", media_mode: "auto_from_generation_mode", vram_flush: true, motion_intensity: 1.0, second_stage_mode: "off", stage2_model_policy: "stage2_model_if_connected", second_stage_reinject_strength: 0.0 }, - loop_lipsync_safe: { backend_mode: "loop_normal_vram", vae_mode: "normal_tiled_vhs", stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "tail_only", anchor_refresh_interval: 2, anchor_image_strength: 0.2, anti_drift_mode: "rolling_adain", anti_drift_strength: 0.1, generation_mode: "img2vid", media_mode: "input_audio_img2vid", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off" }, - img2vid_generated_audio: { backend_mode: "single_best", generation_mode: "img2vid", media_mode: "generated_audio_img2vid", generated_media_duration_seconds: 10.0, continuity_anchor_mode: "off", stitch_preset: "custom", start_frames_rule: "none", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off" }, - t2v_generated_audio: { backend_mode: "single_best", generation_mode: "t2v", media_mode: "generated_audio_t2v", generated_media_duration_seconds: 10.0, continuity_anchor_mode: "off", stitch_preset: "custom", start_frames_rule: "none", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off" }, - img2vid_pure: { backend_mode: "single_best", generation_mode: "img2vid", media_mode: "img2vid_pure", generated_media_duration_seconds: 10.0, continuity_anchor_mode: "off", stitch_preset: "custom", start_frames_rule: "none", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off" }, - t2v_pure: { backend_mode: "single_best", generation_mode: "t2v", media_mode: "t2v_pure", generated_media_duration_seconds: 10.0, continuity_anchor_mode: "off", stitch_preset: "custom", start_frames_rule: "none", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off" }, - loop_img2vid_pure_normal_vram: { backend_mode: "loop_normal_vram", vae_mode: "normal_tiled_vhs", generation_mode: "img2vid", media_mode: "img2vid_pure", generated_media_duration_seconds: 20.0, continuity_anchor_mode: "tail_only", stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off", anti_drift_mode: "rolling_adain", anti_drift_strength: 0.08 }, - loop_t2v_pure_normal_vram: { backend_mode: "loop_normal_vram", vae_mode: "normal_tiled_vhs", generation_mode: "t2v", media_mode: "t2v_pure", generated_media_duration_seconds: 20.0, continuity_anchor_mode: "tail_only", stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", vram_flush: false, motion_intensity: 1.0, second_stage_mode: "off", anti_drift_mode: "rolling_adain", anti_drift_strength: 0.08 }, - loop_img2vid_pure_low_ram: { backend_mode: "loop_low_ram_disk", vae_mode: "low_ram_disk", generation_mode: "img2vid", media_mode: "img2vid_pure", generated_media_duration_seconds: 20.0, continuity_anchor_mode: "tail_only", stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", vram_flush: true, motion_intensity: 1.0, second_stage_mode: "off", anti_drift_mode: "rolling_adain", anti_drift_strength: 0.08 }, - motion_controlled: { backend_mode: "auto", stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", anchor_image_strength: 0.18, anti_drift_mode: "rolling_adain", anti_drift_strength: 0.08, motion_intensity: 1.35, image_strength: 0.78, image_compression: 32, second_stage_mode: "off" }, + low_ram_safe: { vae_mode: "very_low_ram_disk", steps: 16, image_compression: 40, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "off", anchor_refresh_interval: 2, anchor_image_strength: 0.0, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0, vram_flush: true, motion_intensity: 1.0 }, + balanced: { vae_mode: "normal_tiled_vhs", steps: 20, image_compression: 28, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "off", anchor_refresh_interval: 2, anchor_image_strength: 0.0, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0, vram_flush: false, motion_intensity: 1.0 }, + high_quality: { vae_mode: "high_vram", steps: 24, image_compression: 20, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "off", anchor_refresh_interval: 1, anchor_image_strength: 0.0, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0, vram_flush: false, motion_intensity: 1.0 }, + fast_preview: { vae_mode: "low_ram_disk", steps: 12, image_compression: 45, stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0, vram_flush: true, motion_intensity: 1.0 }, + motion_controlled: { stitch_preset: "custom", overlap_side: "source", overlap_mode: "cut", start_frames_rule: "none", continuity_anchor_mode: "off", anchor_image_strength: 0.0, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0, motion_intensity: 1.35, image_strength: 0.78, image_compression: 32 }, }, visibility: { - low_ram_safe: { anchor_image_strength: true, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: false, output_root: true }, - balanced: { anchor_image_strength: true, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: false, output_root: true }, - high_quality: { anchor_image_strength: true, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: true, output_root: true }, + low_ram_safe: { anchor_image_strength: false, anti_drift_mode: true, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, + balanced: { anchor_image_strength: false, anti_drift_mode: true, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, + high_quality: { anchor_image_strength: false, anti_drift_mode: true, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, fast_preview: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: false }, - loop_lipsync_safe: { anchor_image_strength: true, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: false, output_root: true }, - img2vid_generated_audio: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, - t2v_generated_audio: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, - img2vid_pure: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, - t2v_pure: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, - loop_img2vid_pure_normal_vram: { anchor_image_strength: false, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: false, output_root: true }, - loop_t2v_pure_normal_vram: { anchor_image_strength: false, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: false, output_root: true }, - loop_img2vid_pure_low_ram: { anchor_image_strength: false, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: false, output_root: true }, - motion_controlled: { anchor_image_strength: true, anti_drift_mode: true, anti_drift_strength: true, identity_persistence_strength: false, output_root: true }, + motion_controlled: { anchor_image_strength: false, anti_drift_mode: true, anti_drift_strength: false, identity_persistence_strength: false, output_root: true }, }, }, "IAMCCS-SuperNodes AU+IMG2VID Exec VAE": { presetWidget: "ui_preset", defaultPreset: "custom", values: { - very_low_ram_decode: { decode_mode: "very_low_ram_disk", vram_flush: true, jpg_quality: 90, crf: 22, tiled_tile_size: 256, tiled_overlap: 32 }, - low_ram_safe: { decode_mode: "very_low_ram_disk", vram_flush: true, jpg_quality: 92, crf: 21, tiled_tile_size: 256, tiled_overlap: 32 }, - balanced: { decode_mode: "normal_tiled_vhs", vram_flush: false, jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 }, - high_quality: { decode_mode: "high_vram", vram_flush: false, jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 }, - fast_preview: { decode_mode: "low_ram_disk", vram_flush: true, jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 }, + balanced: { decode_mode: "normal_tiled_vhs", vram_flush: false, jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64, tiled_temporal_size: 256, tiled_temporal_overlap: 32, cleanup_before_decode: false }, + high_quality: { decode_mode: "high_vram", vram_flush: false, jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96, tiled_temporal_size: 256, tiled_temporal_overlap: 32, cleanup_before_decode: false }, + fast_preview: { decode_mode: "low_ram_disk", vram_flush: true, jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48, tiled_temporal_size: 128, tiled_temporal_overlap: 16, cleanup_before_decode: true }, + low_ram_safe: { decode_mode: "low_ram_disk", vram_flush: true, jpg_quality: 92, crf: 21, tiled_tile_size: 384, tiled_overlap: 48, tiled_temporal_size: 128, tiled_temporal_overlap: 16, cleanup_before_decode: true }, + very_low_ram_decode: { decode_mode: "very_low_ram_disk", vram_flush: true, jpg_quality: 90, crf: 22, tiled_tile_size: 256, tiled_overlap: 32, tiled_temporal_size: 128, tiled_temporal_overlap: 16, cleanup_before_decode: true }, }, visibility: { - very_low_ram_decode: { frames_subdir: true, image_format: true, jpg_quality: true }, - low_ram_safe: { frames_subdir: true, image_format: true, jpg_quality: true }, - balanced: { frames_subdir: true, image_format: true, jpg_quality: true }, - high_quality: { frames_subdir: true, image_format: true, jpg_quality: true }, - fast_preview: { frames_subdir: true, image_format: true, jpg_quality: true }, + balanced: {}, + high_quality: {}, + fast_preview: {}, + low_ram_safe: {}, + very_low_ram_decode: {}, }, }, }; const NODE_GROUPS = { "IAMCCS-SuperNodes AU+IMG2VID Exec Planner": [ - { key: "timeline", label: "Timeline", color: "#2f8f80", widgets: ["fps", "segment_seconds"] }, - { key: "planning", label: "Planning", color: "#a07b2c", widgets: ["planning_mode", "segment_preset", "overlap_frames", "ltx_round_mode"] }, + { key: "route", label: "A+I2V Route", color: "#2f8f80", widgets: ["audio_img2vid_backend", "audio_img2vid_mode", "single_duration_seconds", "segment_count", "segment_seconds", "overlap_frames", "ltx_round_mode", "fps", "debug_verbose"] }, + { key: "audio", label: "Audio Source", color: "#3d8a5c", widgets: ["audio_preprocess_mode", "melband_model_name"] }, ], "IAMCCS-SuperNodes AU+IMG2VID Exec Render": [ - { key: "generation", label: "Generation Type", color: "#8a5ca0", widgets: ["generation_type", "generated_media_duration_seconds"] }, - { key: "preset", label: "Preset", color: "#557ea6", widgets: ["ui_preset"] }, + { key: "generation", label: "Generation", color: "#8a5ca0", widgets: ["generation_type", "generated_media_duration_seconds", "generated_media_fps", "debug_verbose"] }, { key: "prompts", label: "Prompting", color: "#9a6b2f", widgets: ["positive_text", "negative_text"] }, { key: "video", label: "Video", color: "#3f6fb0", widgets: ["width", "height"] }, - { key: "sampling", label: "Sampling", color: "#8352a6", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "motion_intensity", "image_compression"] }, - { key: "audio", label: "Audio", color: "#3d8a5c", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] }, - { key: "stitch", label: "Stitch", color: "#a4673d", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule"] }, - { key: "anchor", label: "Anchor", color: "#b35c5c", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] }, - { key: "modular", label: "Modular", color: "#46769a", widgets: ["vae_mode", "vram_flush", "downstream_stage_mode", "output_root"] }, - { key: "debug", label: "Debug", color: "#8a8a36", widgets: ["segment_overlay_mode", "segment_overlay_text"] }, + { key: "sampling", label: "Sampling", color: "#8352a6", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "show_manual_sigmas", "manual_sigmas", "image_strength", "image_compression"] }, + { key: "transition", label: "Transition / Stitch", color: "#7a7040", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", "color_match_mode", "color_match_strength"] }, + { key: "audio_context", label: "Audio Context", color: "#477c7a", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] }, + { key: "latent_refresh", label: "Latent Refresh (beta)", color: "#b35c5c", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] }, { key: "stage2", label: "Second Stage", color: "#8a5ca0", widgets: ["second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"] }, ], "IAMCCS-SuperNodes Second Stage": [ @@ -80,12 +63,11 @@ const NODE_GROUPS = { { key: "concat", label: "Audio Concat", color: "#4e8f6b", widgets: ["concat_mode", "clip_durations_seconds", "gap_seconds", "intro_seconds", "outro_seconds"] }, ], "IAMCCS-SuperNodes AU+IMG2VID Exec VAE": [ - { key: "preset", label: "Preset", color: "#557ea6", widgets: ["ui_preset"] }, - { key: "decode", label: "Decode", color: "#4b79a6", widgets: ["frame_rate", "decode_mode", "output_root", "frames_subdir", "image_format", "jpg_quality", "tiled_tile_size", "tiled_overlap"] }, - { key: "final", label: "Finalize", color: "#9b7441", widgets: ["filename_prefix", "crf", "pix_fmt", "trim_to_audio", "save_metadata", "vram_flush"] }, + { key: "mode", label: "VAE Mode", color: "#4b79a6", widgets: ["decode_mode", "frame_rate", "tiled_tile_size", "tiled_overlap", "tiled_temporal_size", "tiled_temporal_overlap", "cleanup_before_decode", "frames_subdir", "image_format", "jpg_quality", "output_root", "vram_flush"] }, + { key: "output", label: "Output", color: "#9b7441", widgets: ["filename_prefix", "crf", "trim_to_audio", "debug_verbose"] }, ], "IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [ - { key: "output", label: "Output", color: "#6a85a0", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] }, + { key: "output", label: "Output", color: "#6a85a0", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio", "debug_verbose"] }, ], }; @@ -94,32 +76,478 @@ const SECTION_BUTTON_COLLAPSED_BORDER = "#6d7785"; const SECTION_BUTTON_RADIUS = 6; const SECTION_BUTTON_TEXT = "#f8fbff"; const SECTION_BUTTON_HEIGHT = 30; -const RENDER_INTERNAL_WIDGETS = new Set(["backend_mode", "generation_mode", "media_mode"]); +const DEFAULT_COLLAPSED_SECTION_KEYS = new Set(["latent_refresh", "stage2", "output"]); +const RENDER_INTERNAL_WIDGETS = new Set([ + "ui_preset", + "backend_mode", + "generation_mode", + "media_mode", + "vae_mode", + "motion_intensity", + "vram_flush", + "downstream_stage_mode", + "output_root", + "segment_overlay_mode", + "segment_overlay_text", +]); const GENERATED_DURATION_TYPES = new Set(["img2video", "text2video"]); -const GENERATION_TYPE_CONFIGS = { - "aud+img2video_simple": { generation_mode: "img2vid", backend_mode: "single_best", media_mode: "input_audio_img2vid" }, - "aud+img2video_2_segments": { generation_mode: "img2vid", backend_mode: "two_segments_normal_vram", media_mode: "input_audio_img2vid" }, - "aud+img2video_infinite": { generation_mode: "img2vid", backend_mode: "loop_normal_vram", media_mode: "input_audio_img2vid" }, - "text+audio2video": { generation_mode: "t2v", backend_mode: "loop_normal_vram", media_mode: "input_audio_t2v" }, - img2video: { generation_mode: "img2vid", backend_mode: "single_best", media_mode: "img2vid_pure" }, - text2video: { generation_mode: "t2v", backend_mode: "single_best", media_mode: "t2v_pure" }, +const REF_MAIN_SIGMAS = "1., 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0"; +const REF_STAGE2_SIGMAS = "0.909375, 0.725, 0.421875, 0.0"; +const CARTOON_STAGE2_SIGMAS = "0.8025, 0.6332, 0.3425, 0.0"; +const REF_LTX23_UPSCALE_MODEL = "ltx-2.3-spatial-upscaler-x2-1.0.safetensors"; +const DEFAULT_VIDEO_WIDTH = 1280; +const DEFAULT_VIDEO_HEIGHT = 736; +const DEFAULT_GENERATED_DURATION_SECONDS = 10.0; +const DEFAULT_GENERATED_FPS = 25.0; +const DEFAULT_VAE_FRAME_RATE = DEFAULT_GENERATED_FPS; +const REFERENCE_AUDIO_IMG2VID_TYPE = "audio+image2video"; +const AUDIO_IMAGE_LEGACY_TYPES = new Set(["aud+img2video_simple", "aud+img2video_2_segments", "aud+img2video_infinite"]); +const REFERENCE_AUDIO_IMG2VID_FPS = 25.0; +const REFERENCE_AUDIO_IMG2VID_WIDTH = DEFAULT_VIDEO_WIDTH; +const REFERENCE_AUDIO_IMG2VID_HEIGHT = 720; +const STALE_AUDIO_IMG2VID_WIDTH = 768; +const STALE_AUDIO_IMG2VID_HEIGHT = 512; +const REFERENCE_AUDIO_IMG2VID_NEGATIVE = "closed mouth, smiling without speaking, singing concert, dancing, exaggerated head movement, blurry mouth"; +const COMMON_GENERATION_DEFAULTS = { + ui_preset: "custom", + stitch_preset: "custom", + overlap_side: "source", + overlap_mode: "cut", + start_frames_rule: "none", + color_match_mode: "none", + color_match_strength: 0.25, + vram_flush: false, + motion_intensity: 1.0, + stage2_model_policy: "stage2_model_if_connected", + continuity_anchor_mode: "off", + anchor_image_strength: 0.0, + anti_drift_mode: "off", + anti_drift_strength: 0.0, + identity_persistence_strength: 0.0, + second_stage_mode: "off", + second_stage_reinject_strength: 0.0, + show_manual_sigmas: false, }; +// Regression guard: these sampling defaults are the contract for the +// generation_type switch. They are applied only when the type changes; after +// that, the user-selected frontend values must remain the backend inputs. +const GENERATION_SAMPLING_DEFAULTS = { + "audio+image2video": { + steps: 8, + cfg: 1.0, + sampler_name: "lcm", + manual_sigmas: "", + image_strength: 0.8, + image_compression: 33, + }, + "aud+img2video_simple": { + steps: 8, + cfg: 1.0, + sampler_name: "lcm", + manual_sigmas: "", + image_strength: 0.8, + image_compression: 33, + }, + "aud+img2video_2_segments": { + steps: 8, + cfg: 1.0, + sampler_name: "euler", + show_manual_sigmas: true, + manual_sigmas: REF_MAIN_SIGMAS, + image_strength: 0.9, + image_compression: 35, + }, + "aud+img2video_infinite": { + steps: 8, + cfg: 1.0, + sampler_name: "euler", + show_manual_sigmas: true, + manual_sigmas: REF_MAIN_SIGMAS, + image_strength: 0.9, + image_compression: 35, + }, + "text+audio2video": { + steps: 8, + cfg: 1.0, + sampler_name: "euler", + show_manual_sigmas: true, + manual_sigmas: REF_MAIN_SIGMAS, + image_strength: 0.0, + image_compression: 35, + }, + img2video: { + steps: 8, + cfg: 1.0, + sampler_name: "lcm", + max_shift: 2.05, + base_shift: 0.95, + sigma_terminal: 0.1, + manual_sigmas: "", + image_strength: 1.0, + image_compression: 33, + }, + text2video: { + steps: 8, + cfg: 1.0, + sampler_name: "euler", + max_shift: 2.05, + base_shift: 0.95, + sigma_terminal: 0.1, + manual_sigmas: "", + image_strength: 0.0, + image_compression: 33, + }, +}; +const ANCHOR_MODE_DEFAULTS = { + off: { anchor_refresh_interval: 2, anchor_image_strength: 0.0, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 }, + tail_only: { anchor_refresh_interval: 2, anchor_image_strength: 0.25, anti_drift_mode: "rolling_adain", anti_drift_strength: 0.12, identity_persistence_strength: 0.0 }, + periodic_tail_only: { anchor_refresh_interval: 2, anchor_image_strength: 0.25, anti_drift_mode: "rolling_adain", anti_drift_strength: 0.12, identity_persistence_strength: 0.0 }, + tail_then_source_refresh: { anchor_refresh_interval: 2, anchor_image_strength: 0.45, anti_drift_mode: "dual_reference_adain", anti_drift_strength: 0.16, identity_persistence_strength: 0.06 }, + periodic_tail_then_source_refresh: { anchor_refresh_interval: 2, anchor_image_strength: 0.45, anti_drift_mode: "dual_reference_adain", anti_drift_strength: 0.16, identity_persistence_strength: 0.06 }, + periodic_source_refresh: { anchor_refresh_interval: 2, anchor_image_strength: 0.45, anti_drift_mode: "dual_reference_adain", anti_drift_strength: 0.16, identity_persistence_strength: 0.06 }, + always_source_refresh: { anchor_refresh_interval: 1, anchor_image_strength: 0.45, anti_drift_mode: "dual_reference_adain", anti_drift_strength: 0.16, identity_persistence_strength: 0.06 }, +}; +const GENERATION_TYPE_CONFIGS = { + "audio+image2video": { ...COMMON_GENERATION_DEFAULTS, ...GENERATION_SAMPLING_DEFAULTS["audio+image2video"], generation_mode: "img2vid", backend_mode: "auto", media_mode: "input_audio_img2vid", width: REFERENCE_AUDIO_IMG2VID_WIDTH, height: REFERENCE_AUDIO_IMG2VID_HEIGHT, vae_mode: "normal_tiled_iamccs", second_stage_cfg: 1.0, second_stage_manual_sigmas: REF_STAGE2_SIGMAS }, + "aud+img2video_simple": { ...COMMON_GENERATION_DEFAULTS, ...GENERATION_SAMPLING_DEFAULTS["audio+image2video"], generation_mode: "img2vid", backend_mode: "single_best", media_mode: "input_audio_img2vid", width: REFERENCE_AUDIO_IMG2VID_WIDTH, height: REFERENCE_AUDIO_IMG2VID_HEIGHT, vae_mode: "normal_tiled_iamccs", second_stage_cfg: 1.0, second_stage_manual_sigmas: REF_STAGE2_SIGMAS }, + "aud+img2video_2_segments": { ...COMMON_GENERATION_DEFAULTS, ...GENERATION_SAMPLING_DEFAULTS["audio+image2video"], generation_mode: "img2vid", backend_mode: "two_segments_normal_vram", media_mode: "input_audio_img2vid", width: REFERENCE_AUDIO_IMG2VID_WIDTH, height: REFERENCE_AUDIO_IMG2VID_HEIGHT, vae_mode: "normal_tiled_iamccs", second_stage_cfg: 1.0, second_stage_manual_sigmas: REF_STAGE2_SIGMAS }, + "aud+img2video_infinite": { ...COMMON_GENERATION_DEFAULTS, ...GENERATION_SAMPLING_DEFAULTS["audio+image2video"], generation_mode: "img2vid", backend_mode: "auto", media_mode: "input_audio_img2vid", width: REFERENCE_AUDIO_IMG2VID_WIDTH, height: REFERENCE_AUDIO_IMG2VID_HEIGHT, vae_mode: "normal_tiled_iamccs", second_stage_cfg: 1.0, second_stage_manual_sigmas: REF_STAGE2_SIGMAS }, + "text+audio2video": { ...COMMON_GENERATION_DEFAULTS, ...GENERATION_SAMPLING_DEFAULTS["text+audio2video"], generation_mode: "t2v", backend_mode: "single_best", media_mode: "input_audio_t2v", width: DEFAULT_VIDEO_WIDTH, height: DEFAULT_VIDEO_HEIGHT, vae_mode: "normal_tiled_iamccs", second_stage_cfg: 1.0, second_stage_manual_sigmas: CARTOON_STAGE2_SIGMAS }, + img2video: { ...COMMON_GENERATION_DEFAULTS, ...GENERATION_SAMPLING_DEFAULTS.img2video, generation_mode: "img2vid", backend_mode: "single_best", media_mode: "img2vid_pure", width: DEFAULT_VIDEO_WIDTH, height: DEFAULT_VIDEO_HEIGHT, generated_media_duration_seconds: DEFAULT_GENERATED_DURATION_SECONDS, generated_media_fps: DEFAULT_GENERATED_FPS, vae_mode: "normal_tiled_vhs", second_stage_upscale_model: REF_LTX23_UPSCALE_MODEL, second_stage_cfg: 1.0, second_stage_manual_sigmas: REF_STAGE2_SIGMAS }, + text2video: { ...COMMON_GENERATION_DEFAULTS, ...GENERATION_SAMPLING_DEFAULTS.text2video, generation_mode: "t2v", backend_mode: "single_best", media_mode: "t2v_pure", width: DEFAULT_VIDEO_WIDTH, height: DEFAULT_VIDEO_HEIGHT, generated_media_duration_seconds: DEFAULT_GENERATED_DURATION_SECONDS, generated_media_fps: DEFAULT_GENERATED_FPS, vae_mode: "normal_tiled_vhs", second_stage_upscale_model: REF_LTX23_UPSCALE_MODEL, second_stage_cfg: 1.0, second_stage_manual_sigmas: REF_STAGE2_SIGMAS }, +}; +const RENDER_UI_PRESET_LEGACY_VALUES = [ + "loop_lipsync_safe", + "img2vid_generated_audio", + "t2v_generated_audio", + "img2vid_pure", + "t2v_pure", + "loop_img2vid_pure_normal_vram", + "loop_t2v_pure_normal_vram", + "loop_img2vid_pure_low_ram", +]; const RENDER_UI_PRESET_VALUES = new Set(["custom", ...Object.keys(PRESET_CONFIGS["IAMCCS-SuperNodes AU+IMG2VID Exec Render"]?.values || {})]); -const RENDER_UI_PRESET_VISIBLE_VALUES = ["custom", "low_ram_safe", "balanced", "high_quality", "fast_preview", "loop_lipsync_safe", "motion_controlled"]; +const RENDER_UI_PRESET_VISIBLE_VALUES = ["custom", "low_ram_safe", "balanced", "high_quality", "fast_preview", "motion_controlled"]; +const RENDER_BACKEND_MODE_VISIBLE_VALUES = [ + "auto", + "single_best", + "legacy_single", + "legacy_two_segments", + "legacy_loop", + "two_segments_normal_vram", + "three_segments_normal_vram", + "loop_normal_vram", + "loop_low_ram_disk", +]; +const RENDER_BACKEND_MODE_VALUES = new Set([...RENDER_BACKEND_MODE_VISIBLE_VALUES, "legacy backend"]); +const RENDER_STITCH_PRESET_VALUES = new Set([ + "custom", + "lossless_refresh_24fps", + "lossless_refresh_strong_24fps", + "videoclip_audio_24fps", + "monologue_audio_24fps", + "target_extension_ltx2", + "cut_bestofk_16", + "cut_bestofk_16_luma", + "cut_bestofk_32", + "micro_crossfade_3", +]); +const RENDER_OVERLAP_SIDE_VALUES = new Set(["source", "new_images"]); +const RENDER_OVERLAP_MODE_VALUES = new Set(["cut", "linear_blend", "ease_in_out", "filmic_crossfade"]); +const RENDER_START_FRAMES_RULE_VALUES = new Set(["none", "ltx2_round_down", "ltx2_nearest"]); +const RENDER_COLOR_MATCH_MODE_VALUES = new Set(["none", "luma_only", "per_channel"]); +const RENDER_AUDIO_CONTEXT_MODE_VALUES = new Set(["left_context_only", "right_context_only", "symmetric_context", "no_overlap"]); +const LEGACY_RENDER_BACKEND_DEFAULTS = { + legacy_single: { + width: REFERENCE_AUDIO_IMG2VID_WIDTH, + height: REFERENCE_AUDIO_IMG2VID_HEIGHT, + steps: 8, + cfg: 1.0, + sampler_name: "euler", + manual_sigmas: REF_MAIN_SIGMAS, + image_strength: 1.0, + image_compression: 33, + max_shift: 2.05, + base_shift: 0.95, + sigma_terminal: 0.1, + audio_context_mode: "left_context_only", + audio_left_context_s: 0.5, + audio_right_context_s: 0.0, + stitch_preset: "custom", + overlap_side: "source", + overlap_mode: "cut", + start_frames_rule: "none", + color_match_mode: "none", + color_match_strength: 0.25, + continuity_anchor_mode: "off", + anchor_refresh_interval: 2, + anchor_image_strength: 0.0, + anti_drift_mode: "off", + anti_drift_strength: 0.0, + identity_persistence_strength: 0.0, + second_stage_mode: "off", + second_stage_cfg: 1.0, + second_stage_manual_sigmas: "0.909375, 0.725, 0.421875, 0.0", + }, + legacy_two_segments: { + width: REFERENCE_AUDIO_IMG2VID_WIDTH, + height: REFERENCE_AUDIO_IMG2VID_HEIGHT, + steps: 8, + cfg: 1.0, + sampler_name: "euler", + manual_sigmas: REF_MAIN_SIGMAS, + image_strength: 1.0, + image_compression: 33, + max_shift: 2.05, + base_shift: 0.95, + sigma_terminal: 0.1, + audio_context_mode: "left_context_only", + audio_left_context_s: 0.5, + audio_right_context_s: 0.0, + stitch_preset: "custom", + overlap_side: "source", + overlap_mode: "cut", + start_frames_rule: "none", + color_match_mode: "none", + color_match_strength: 0.25, + continuity_anchor_mode: "off", + anchor_refresh_interval: 2, + anchor_image_strength: 0.0, + anti_drift_mode: "off", + anti_drift_strength: 0.0, + identity_persistence_strength: 0.0, + second_stage_mode: "off", + second_stage_cfg: 1.0, + second_stage_manual_sigmas: "0.909375, 0.725, 0.421875, 0.0", + }, + legacy_loop: { + width: REFERENCE_AUDIO_IMG2VID_WIDTH, + height: REFERENCE_AUDIO_IMG2VID_HEIGHT, + steps: 8, + cfg: 1.0, + sampler_name: "euler", + manual_sigmas: REF_MAIN_SIGMAS, + image_strength: 1.0, + image_compression: 33, + max_shift: 2.05, + base_shift: 0.95, + sigma_terminal: 0.1, + audio_context_mode: "left_context_only", + audio_left_context_s: 0.5, + audio_right_context_s: 0.0, + stitch_preset: "custom", + overlap_side: "source", + overlap_mode: "cut", + start_frames_rule: "none", + color_match_mode: "none", + color_match_strength: 0.25, + continuity_anchor_mode: "off", + anchor_refresh_interval: 2, + anchor_image_strength: 0.0, + anti_drift_mode: "off", + anti_drift_strength: 0.0, + identity_persistence_strength: 0.0, + second_stage_mode: "off", + second_stage_cfg: 1.0, + second_stage_manual_sigmas: "0.909375, 0.725, 0.421875, 0.0", + }, +}; +const VAE_TILED_DECODE_MODES = new Set(["normal_tiled_iamccs", "normal_tiled_vhs", "custom_mode", "inherit_render_backend"]); +const VAE_DISK_DECODE_MODES = new Set(["low_ram", "low_ram_disk", "very_low_ram", "very_low_ram_disk"]); +const VAE_DECODE_MODE_VALUES = new Set(["normal_tiled_iamccs", "low_ram", "very_low_ram", "high_vram", "inherit_render_backend", "normal_tiled_vhs", "low_ram_disk", "very_low_ram_disk", "custom_mode"]); +const VAE_UI_PRESET_VALUES = new Set(["custom", ...Object.keys(PRESET_CONFIGS["IAMCCS-SuperNodes AU+IMG2VID Exec VAE"]?.values || {})]); +const VAE_DECODE_MODE_DEFAULTS = { + normal_tiled_iamccs: { tiled_tile_size: 512, tiled_overlap: 64, tiled_temporal_size: 256, tiled_temporal_overlap: 32, cleanup_before_decode: false }, + inherit_render_backend: { tiled_tile_size: 512, tiled_overlap: 64, tiled_temporal_size: 256, tiled_temporal_overlap: 32, cleanup_before_decode: false }, + normal_tiled_vhs: { tiled_tile_size: 512, tiled_overlap: 64, tiled_temporal_size: 256, tiled_temporal_overlap: 32, cleanup_before_decode: false }, + custom_mode: { tiled_tile_size: 512, tiled_overlap: 64, tiled_temporal_size: 256, tiled_temporal_overlap: 32, cleanup_before_decode: false }, + low_ram: { tiled_tile_size: 384, tiled_overlap: 48, tiled_temporal_size: 128, tiled_temporal_overlap: 16, cleanup_before_decode: true }, + low_ram_disk: { tiled_tile_size: 384, tiled_overlap: 48, tiled_temporal_size: 128, tiled_temporal_overlap: 16, cleanup_before_decode: true }, + very_low_ram: { tiled_tile_size: 256, tiled_overlap: 32, tiled_temporal_size: 128, tiled_temporal_overlap: 16, cleanup_before_decode: true }, + very_low_ram_disk: { tiled_tile_size: 256, tiled_overlap: 32, tiled_temporal_size: 128, tiled_temporal_overlap: 16, cleanup_before_decode: true }, + high_vram: { tiled_tile_size: 768, tiled_overlap: 96, tiled_temporal_size: 256, tiled_temporal_overlap: 32, cleanup_before_decode: false }, +}; const RENDER_GENERATION_TYPE_VALUES = new Set(Object.keys(GENERATION_TYPE_CONFIGS)); -const RENDER_LEGACY_WIDGET_ORDER = [ + +function normalizeReferenceAudioImg2VidValues(valuesByName) { + const generationType = String(valuesByName?.generation_type || ""); + if (!valuesByName || (generationType !== REFERENCE_AUDIO_IMG2VID_TYPE && !AUDIO_IMAGE_LEGACY_TYPES.has(generationType))) { + return valuesByName; + } + const config = GENERATION_TYPE_CONFIGS[REFERENCE_AUDIO_IMG2VID_TYPE]; + valuesByName.generation_type = REFERENCE_AUDIO_IMG2VID_TYPE; + valuesByName.generation_mode = config.generation_mode; + valuesByName.backend_mode = config.backend_mode; + valuesByName.media_mode = config.media_mode; + + const ensureNumber = (widgetName, fallback) => { + if (!Number.isFinite(Number(valuesByName[widgetName])) || isSectionButtonCaption(valuesByName[widgetName])) { + valuesByName[widgetName] = fallback; + } + }; + const ensureText = (widgetName, fallback) => { + const text = String(valuesByName[widgetName] ?? ""); + if (!text || isSectionButtonCaption(text)) { + valuesByName[widgetName] = fallback; + } + }; + + ensureText("ui_preset", "custom"); + ensureNumber("generated_media_fps", REFERENCE_AUDIO_IMG2VID_FPS); + if (isSectionButtonCaption(String(valuesByName.negative_text ?? ""))) { + valuesByName.negative_text = ""; + } + ensureNumber("width", config.width); + ensureNumber("height", config.height); + ensureNumber("steps", config.steps); + ensureNumber("cfg", config.cfg); + ensureText("sampler_name", config.sampler_name); + ensureText("manual_sigmas", config.manual_sigmas); + ensureNumber("image_strength", config.image_strength); + ensureNumber("image_compression", config.image_compression); + ensureText("vae_mode", config.vae_mode); + ensureText("second_stage_mode", "off"); + ensureNumber("second_stage_reinject_strength", 0.0); + ensureNumber("second_stage_cfg", config.second_stage_cfg); + ensureText("second_stage_manual_sigmas", config.second_stage_manual_sigmas); + ensureText("color_match_mode", "none"); + ensureNumber("color_match_strength", 0.25); + ensureText("continuity_anchor_mode", "off"); + ensureNumber("anchor_image_strength", 0.0); + ensureText("anti_drift_mode", "off"); + ensureNumber("anti_drift_strength", 0.0); + ensureNumber("identity_persistence_strength", 0.0); + return valuesByName; +} +const RENDER_LEGACY_WIDGET_ORDER_PRE_ADVANCED_MANUAL_SIGMAS = [ "generation_mode", "backend_mode", "positive_text", "negative_text", "width", "height", "steps", "cfg", "sampler_name", "seed", "control_after_generate", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression", "audio_context_mode", "audio_left_context_s", "audio_right_context_s", - "stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", + "stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", "color_match_mode", "color_match_strength", "continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength", "vae_mode", "downstream_stage_mode", "output_root", "segment_overlay_mode", "segment_overlay_text", "second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas", - "media_mode", "vram_flush", "motion_intensity", "ui_preset", "generated_media_duration_seconds", "generation_type", + "media_mode", "vram_flush", "motion_intensity", "debug_verbose", "ui_preset", "generated_media_duration_seconds", "generation_type", +]; +const RENDER_LEGACY_WIDGET_ORDER = [ + "generation_mode", "backend_mode", "positive_text", "negative_text", "width", "height", "steps", "cfg", "sampler_name", "seed", "control_after_generate", + "max_shift", "base_shift", "sigma_terminal", "show_manual_sigmas", "manual_sigmas", "image_strength", "image_compression", + "audio_context_mode", "audio_left_context_s", "audio_right_context_s", + "stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", "color_match_mode", "color_match_strength", + "continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength", + "vae_mode", "downstream_stage_mode", "output_root", "segment_overlay_mode", "segment_overlay_text", + "second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas", + "media_mode", "vram_flush", "motion_intensity", "debug_verbose", "ui_preset", "generated_media_duration_seconds", "generation_type", +]; +const RENDER_CURRENT_WIDGET_ORDER_PRE_GENERATED_FPS_PRE_ADVANCED_MANUAL_SIGMAS = [ + "generation_type", "ui_preset", "generated_media_duration_seconds", "generation_mode", "backend_mode", "positive_text", "negative_text", + "width", "height", "steps", "cfg", "sampler_name", "seed", "control_after_generate", + "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression", + "audio_context_mode", "audio_left_context_s", "audio_right_context_s", + "stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", "color_match_mode", "color_match_strength", + "continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength", + "vae_mode", "downstream_stage_mode", "output_root", "segment_overlay_mode", "segment_overlay_text", + "second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas", + "media_mode", "vram_flush", "motion_intensity", "debug_verbose", +]; +const RENDER_CURRENT_WIDGET_ORDER_PRE_GENERATED_FPS = [ + "generation_type", "ui_preset", "generated_media_duration_seconds", "generation_mode", "backend_mode", "positive_text", "negative_text", + "width", "height", "steps", "cfg", "sampler_name", "seed", "control_after_generate", + "max_shift", "base_shift", "sigma_terminal", "show_manual_sigmas", "manual_sigmas", "image_strength", "image_compression", + "audio_context_mode", "audio_left_context_s", "audio_right_context_s", + "stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", "color_match_mode", "color_match_strength", + "continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength", + "vae_mode", "downstream_stage_mode", "output_root", "segment_overlay_mode", "segment_overlay_text", + "second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas", + "media_mode", "vram_flush", "motion_intensity", "debug_verbose", +]; +const RENDER_CURRENT_WIDGET_ORDER_PRE_ADVANCED_MANUAL_SIGMAS = [ + "generation_type", "ui_preset", "generated_media_duration_seconds", "generated_media_fps", "generation_mode", "backend_mode", "positive_text", "negative_text", + "width", "height", "steps", "cfg", "sampler_name", "seed", "control_after_generate", + "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression", + "audio_context_mode", "audio_left_context_s", "audio_right_context_s", + "stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", "color_match_mode", "color_match_strength", + "continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength", + "vae_mode", "downstream_stage_mode", "output_root", "segment_overlay_mode", "segment_overlay_text", + "second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas", + "media_mode", "vram_flush", "motion_intensity", "debug_verbose", +]; +const RENDER_CURRENT_WIDGET_ORDER = [ + "generation_type", "ui_preset", "generated_media_duration_seconds", "generated_media_fps", "generation_mode", "backend_mode", "positive_text", "negative_text", + "width", "height", "steps", "cfg", "sampler_name", "seed", "control_after_generate", + "max_shift", "base_shift", "sigma_terminal", "show_manual_sigmas", "manual_sigmas", "image_strength", "image_compression", + "audio_context_mode", "audio_left_context_s", "audio_right_context_s", + "stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule", "color_match_mode", "color_match_strength", + "continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength", + "vae_mode", "downstream_stage_mode", "output_root", "segment_overlay_mode", "segment_overlay_text", + "second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas", + "media_mode", "vram_flush", "motion_intensity", "debug_verbose", +]; +const VAE_LEGACY_WIDGET_ORDER = [ + "frame_rate", "decode_mode", "tiled_tile_size", "tiled_overlap", "frames_subdir", "image_format", "jpg_quality", + "output_root", "filename_prefix", "crf", "pix_fmt", "trim_to_audio", "save_metadata", "vram_flush", "ui_preset", "debug_verbose", +]; +const VAE_LEGACY_PY_WIDGET_ORDER = [ + "frame_rate", "decode_mode", "filename_prefix", "output_root", "frames_subdir", "image_format", "jpg_quality", + "tiled_tile_size", "tiled_overlap", "crf", "pix_fmt", "trim_to_audio", "save_metadata", "vram_flush", "ui_preset", "debug_verbose", +]; +const VAE_CURRENT_WIDGET_ORDER = [ + "frame_rate", "decode_mode", "tiled_tile_size", "tiled_overlap", "tiled_temporal_size", "tiled_temporal_overlap", + "cleanup_before_decode", "frames_subdir", "image_format", "jpg_quality", "output_root", "filename_prefix", + "crf", "pix_fmt", "trim_to_audio", "save_metadata", "vram_flush", "ui_preset", "debug_verbose", +]; +const VAE_CURRENT_PY_WIDGET_ORDER = [ + "frame_rate", "decode_mode", "filename_prefix", "output_root", "frames_subdir", "image_format", "jpg_quality", + "tiled_tile_size", "tiled_overlap", "tiled_temporal_size", "tiled_temporal_overlap", "cleanup_before_decode", + "crf", "pix_fmt", "trim_to_audio", "save_metadata", "vram_flush", "ui_preset", "debug_verbose", +]; +const PLANNER_ROUTE_MODE_VALUES = new Set([ + "single generation (duration only)", + "choose segment count (audio / segments)", + "choose seconds per segment (auto count)", +]); +const PLANNER_A2I_BACKEND_MODERN = "modern pipeline"; +const PLANNER_A2I_BACKEND_LEGACY = "legacy exact pipeline"; +const PLANNER_A2I_MODE_SINGLE = "single generation"; +const PLANNER_A2I_MODE_2_SEGMENTS = "2 segments"; +const PLANNER_A2I_MODE_3_SEGMENTS = "3 segments"; +const PLANNER_A2I_MODE_LOOP = "loop / 4+ segments"; +const PLANNER_AUDIO_IMG2VID_BACKEND_VALUES = new Set([PLANNER_A2I_BACKEND_MODERN, PLANNER_A2I_BACKEND_LEGACY, "modern", "legacy"]); +const PLANNER_AUDIO_IMG2VID_MODE_VALUES = new Set([PLANNER_A2I_MODE_SINGLE, PLANNER_A2I_MODE_2_SEGMENTS, PLANNER_A2I_MODE_3_SEGMENTS, PLANNER_A2I_MODE_LOOP, "single", "2_segments", "3_segments", "loop"]); +const PLANNER_MODE_VALUES = new Set(["manual_segment_seconds", "explicit_preset_seconds"]); +const PLANNER_SEGMENT_PRESET_VALUES = new Set(["5sec", "10sec", "15sec", "20sec"]); +const PLANNER_ROUND_MODE_VALUES = new Set(["up", "nearest", "down"]); +const PLANNER_AUDIO_MODE_VALUES = new Set(["melband_vocals_duration_math", "raw_audio_only"]); +const PLANNER_VISUAL_WIDGET_ORDER = [ + "audio_img2vid_backend", "audio_img2vid_mode", "single_duration_seconds", "segment_count", "segment_seconds", "fps", + "audio_preprocess_mode", "melband_model_name", "planning_mode", "segment_preset", + "overlap_frames", "ltx_round_mode", "route_mode", "audio_concat_payload", "debug_verbose", +]; +const PLANNER_LEGACY_VISUAL_WIDGET_ORDER = [ + "route_mode", "single_duration_seconds", "segment_count", "segment_seconds", "fps", + "audio_preprocess_mode", "melband_model_name", "planning_mode", "segment_preset", + "overlap_frames", "ltx_round_mode", "audio_concat_payload", "debug_verbose", +]; +const PLANNER_PY_WIDGET_ORDER = [ + "fps", "segment_seconds", "planning_mode", "segment_preset", "overlap_frames", "ltx_round_mode", + "audio_preprocess_mode", "melband_model_name", "audio_img2vid_backend", "audio_img2vid_mode", "route_mode", "segment_count", + "single_duration_seconds", "audio_concat_payload", "debug_verbose", +]; +const PLANNER_LEGACY_PY_WIDGET_ORDER = [ + "fps", "segment_seconds", "planning_mode", "segment_preset", "overlap_frames", "ltx_round_mode", + "audio_preprocess_mode", "melband_model_name", "route_mode", "segment_count", + "single_duration_seconds", "audio_concat_payload", "debug_verbose", ]; +function hasWidgetList(node) { + return !!node && Array.isArray(node.widgets); +} + function findWidget(node, name) { - return node.widgets?.find((widget) => widget?.name === name); + if (!hasWidgetList(node)) { + return undefined; + } + return node.widgets.find((widget) => widget?.name === name); } function setWidgetVisibility(widget, visible) { @@ -154,7 +582,13 @@ function setWidgetVisibility(widget, visible) { } function fitNodeToWidgets(node) { + if (!node) { + return; + } const size = node.computeSize?.() || node.size; + if (!Array.isArray(size) || typeof node.setSize !== "function") { + return; + } node.setSize([ Math.max(node.size?.[0] || 0, size[0]), Math.max(size[1], size[1] + 8), @@ -169,6 +603,176 @@ function setWidgetLabel(node, widgetName, label) { widget.label = label; } +function markCanvasDirty() { + try { + app.graph?.setDirtyCanvas?.(true, true); + } catch (error) { + warnUiError("markCanvasDirty", error); + } +} + +function warnUiError(scope, error) { + console.warn(`[IAMCCS SuperNodes] UI ${scope} skipped`, error); +} + +function safeCall(scope, fn, fallback = undefined) { + try { + return fn(); + } catch (error) { + warnUiError(scope, error); + return fallback; + } +} + +function areNodeLinksResolved(node) { + if (!node || !app?.graph || !Array.isArray(node.inputs) || !Array.isArray(node.outputs)) { + return false; + } + const checkLinkId = (linkId) => { + if (linkId === undefined || linkId === null) { + return true; + } + return !!getGraphLink(linkId); + }; + for (const input of node.inputs) { + if (!input) { + continue; + } + if (!checkLinkId(input.link)) { + return false; + } + if (Array.isArray(input.links) && input.links.some((linkId) => !checkLinkId(linkId))) { + return false; + } + } + for (const output of node.outputs) { + if (!output || !Array.isArray(output.links)) { + continue; + } + if (output.links.some((linkId) => !checkLinkId(linkId))) { + return false; + } + } + return true; +} + +function isGraphReadyForLinxSync(node) { + if (!node || !app?.graph) { + return false; + } + if (node.__iamccsConfiguring) { + return false; + } + return Array.isArray(node.inputs) && Array.isArray(node.outputs) && areNodeLinksResolved(node); +} + +function runLinxSyncWhenReady(node, scope, fn, options = {}) { + const maxAttempts = Number.isFinite(options.maxAttempts) ? Math.max(1, Number(options.maxAttempts)) : 12; + const delayMs = Number.isFinite(options.delayMs) ? Math.max(0, Number(options.delayMs)) : 25; + const run = (attempt = 0) => safeCall(scope, () => { + if (!isGraphReadyForLinxSync(node)) { + if (attempt + 1 < maxAttempts) { + setTimeout(() => run(attempt + 1), delayMs); + } + return; + } + fn(); + }); + // Memory fix: never traverse LINX synchronously while LiteGraph is + // rebuilding a workflow. Widget/index normalization stays synchronous; + // only link graph sync is deferred to avoid undefined output on reload. + if (options.defer || !isGraphReadyForLinxSync(node)) { + setTimeout(() => run(0), delayMs); + return; + } + run(0); +} + +function syncReferenceAudioImg2VidTimingWhenReady(renderNode, timingOptions = {}, reason = "reference_timing", options = {}) { + runLinxSyncWhenReady(renderNode, `syncReferenceAudioImg2VidTiming.${reason}`, () => { + syncReferenceAudioImg2VidTiming(renderNode, timingOptions); + }, options); +} + +function syncTextAudio2VideoPlannerModeWhenReady(renderNode, reason = "text_audio_planner", options = {}) { + runLinxSyncWhenReady(renderNode, `syncTextAudio2VideoPlannerMode.${reason}`, () => { + syncTextAudio2VideoPlannerMode(renderNode); + }, options); +} + +function syncLegacyBackendPlannerModeWhenReady(renderNode, reason = "legacy_backend_planner", options = {}) { + runLinxSyncWhenReady(renderNode, `syncLegacyBackendPlannerMode.${reason}`, () => { + syncLegacyBackendPlannerMode(renderNode, options); + }, options); +} + +function syncRenderLinxWhenReady(renderNode, reason = "render", options = {}) { + runLinxSyncWhenReady(renderNode, `syncRenderLinx.${reason}`, () => { + syncDownstreamVaeDecodeModes(renderNode); + syncReferenceAudioImg2VidTiming(renderNode, { applyFpsDefault: false }); + syncTextAudio2VideoPlannerMode(renderNode); + }, options); +} + +function syncPlannerFpsDownstreamWhenReady(plannerNode, reason = "planner", options = {}) { + runLinxSyncWhenReady(plannerNode, `syncPlannerFpsDownstream.${reason}`, () => { + syncPlannerFpsDownstream(plannerNode, options); + }, options); +} + +function syncVaeUpstreamWhenReady(vaeNode, reason = "vae", options = {}) { + runLinxSyncWhenReady(vaeNode, `syncVaeUpstream.${reason}`, () => { + syncUpstreamFpsFromVae(vaeNode); + syncUpstreamDecodeFromVae(vaeNode); + }, options); +} + +function getGraphLink(linkId) { + const links = app.graph?.links; + if (!links || linkId === undefined || linkId === null) { + return null; + } + const wanted = Number(linkId); + try { + if (typeof links.get === "function") { + return links.get(linkId) || links.get(String(linkId)) || links.get(wanted) || null; + } + const direct = links[linkId] || links[String(linkId)]; + if (direct) { + const directId = Array.isArray(direct) ? direct[0] : direct.id; + if (directId === undefined || Number(directId) === wanted) { + return direct; + } + } + if (Array.isArray(links)) { + return links.find((link) => { + if (!link) { + return false; + } + const id = Array.isArray(link) ? link[0] : link.id; + return Number(id) === wanted; + }) || null; + } + } catch (error) { + warnUiError("getGraphLink", error); + } + return null; +} + +function getLinkTargetId(link) { + if (Array.isArray(link)) { + return link[3]; + } + return link?.target_id ?? link?.targetId ?? link?.target?.id ?? link?.target; +} + +function getLinkSourceId(link) { + if (Array.isArray(link)) { + return link[1]; + } + return link?.origin_id ?? link?.originId ?? link?.source_id ?? link?.sourceId ?? link?.origin?.id ?? link?.source?.id ?? link?.source; +} + function sectionButtonCaption(group, isExpanded) { return `${isExpanded ? "[-]" : "[+]"} ${group.label}`; } @@ -238,6 +842,9 @@ function installSectionButtonDraw(button) { } function insertWidget(node, widget, index) { + if (!hasWidgetList(node) || !widget) { + return; + } const widgets = node.widgets || []; const currentIndex = widgets.indexOf(widget); if (currentIndex >= 0) { @@ -247,7 +854,7 @@ function insertWidget(node, widget, index) { } function moveWidgetsAfter(node, anchorName, widgetNames) { - if (!node.widgets?.length) { + if (!hasWidgetList(node) || !node.widgets.length) { return; } const moving = []; @@ -265,20 +872,102 @@ function moveWidgetsAfter(node, anchorName, widgetNames) { node.widgets.splice(insertAt, 0, ...moving); } -function normalizeRenderWidgetOrder(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { +function reorderWidgetsByName(node, orderedNames) { + if (!hasWidgetList(node) || !node.widgets.length) { return; } + const ordered = []; + const used = new Set(); + for (const name of orderedNames) { + const index = node.widgets.findIndex((widget) => widget?.name === name && !used.has(widget)); + if (index >= 0) { + ordered.push(node.widgets[index]); + used.add(node.widgets[index]); + } + } + const rest = node.widgets.filter((widget) => !used.has(widget)); + node.widgets.splice(0, node.widgets.length, ...ordered, ...rest); +} + +function normalizePlannerWidgetOrder(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + return; + } + reorderWidgetsByName(node, [ + "audio_img2vid_backend", + "audio_img2vid_mode", + "single_duration_seconds", + "segment_count", + "segment_seconds", + "fps", + "audio_preprocess_mode", + "melband_model_name", + "planning_mode", + "segment_preset", + "overlap_frames", + "ltx_round_mode", + "route_mode", + "audio_concat_payload", + ]); +} + +function normalizeRenderWidgetOrder(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return; + } + moveWidgetsAfter(node, "sigma_terminal", ["show_manual_sigmas", "manual_sigmas"]); + moveWidgetsAfter(node, "start_frames_rule", ["color_match_mode", "color_match_strength"]); moveWidgetsAfter(node, "second_stage_mode", ["stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"]); } -function setWidgetValue(node, widgetName, value) { +function normalizeVaeWidgetOrder(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + return; + } + moveWidgetsAfter(node, "decode_mode", ["tiled_tile_size", "tiled_overlap", "tiled_temporal_size", "tiled_temporal_overlap", "cleanup_before_decode"]); + moveWidgetsAfter(node, "cleanup_before_decode", ["frame_rate", "frames_subdir", "image_format", "jpg_quality", "output_root", "filename_prefix"]); + moveWidgetsAfter(node, "filename_prefix", ["crf", "pix_fmt", "trim_to_audio", "save_metadata", "vram_flush", "debug_verbose"]); +} + +function setWidgetValue(node, widgetName, value, options = {}) { const widget = findWidget(node, widgetName); - if (!widget || widget.value === value) { + if (!widget) { + return; + } + let nextValue = value; + const values = Array.isArray(widget.options?.values) ? widget.options.values : []; + if (typeof value === "string" && values.length > 0 && !values.includes(value)) { + const wanted = value.toLowerCase(); + const wantedStem = wanted.replace(/\.[^.]+$/, ""); + const matched = values.find((option) => String(option).toLowerCase() === wanted) + || values.find((option) => String(option).toLowerCase().includes(wantedStem)); + if (matched !== undefined) { + nextValue = matched; + } + } + if (widget.value === nextValue) { + return; + } + widget.value = nextValue; + if (options.notify !== false) { + widget.callback?.(nextValue); + } +} + +function coerceWidgetCallbackValue(widget, args) { + if (!widget || !args?.length) { + return; + } + const value = args[0]; + if (value === undefined || value === widget || value?.name) { return; } widget.value = value; - widget.callback?.(value); +} + +function readFiniteWidgetNumber(node, widgetName, fallback = null) { + const value = Number(findWidget(node, widgetName)?.value); + return Number.isFinite(value) ? value : fallback; } const EXEC_PLANNER_PRESETS = { @@ -308,6 +997,20 @@ function getExecPlannerPresetForSeconds(seconds) { return null; } +function getExecPlannerPresetNameForSeconds(seconds) { + const value = Number(seconds); + if (!Number.isFinite(value)) { + return "10sec"; + } + for (const presetName of ["5sec", "10sec", "15sec", "20sec"]) { + const rec = EXEC_PLANNER_PRESETS[presetName]; + if (Math.abs(value - rec.seconds) <= 0.001) { + return presetName; + } + } + return "10sec"; +} + function ltxSafeFramesAtLeast(frameCount) { const frames = Math.max(1, Math.round(Number(frameCount) || 1)); const remainder = (frames - 1) % 8; @@ -321,28 +1024,28 @@ function readPlannerNumber(node, widgetName, fallback) { function getExecPlannerLiveConfig(node) { const fps = Math.max(0.001, readPlannerNumber(node, "fps", 24.0)); + const routeMode = String(findWidget(node, "route_mode")?.value || "choose segment count (audio / segments)"); const planningMode = String(findWidget(node, "planning_mode")?.value || "manual_segment_seconds"); - const segmentPreset = String(findWidget(node, "segment_preset")?.value || "15sec"); + const segmentPreset = String(findWidget(node, "segment_preset")?.value || "10sec"); const presetRec = getExecPlannerPreset(segmentPreset); - let seconds = Math.max(0.01, readPlannerNumber(node, "segment_seconds", presetRec.seconds)); + const singleDuration = Math.max(0.1, readPlannerNumber(node, "single_duration_seconds", 10.0)); + const segmentCount = Math.max(1, Math.round(readPlannerNumber(node, "segment_count", 2))); + let seconds = routeMode.includes("single") ? singleDuration : Math.max(0.01, readPlannerNumber(node, "segment_seconds", presetRec.seconds)); const overlap = Math.max(0, Math.round(readPlannerNumber(node, "overlap_frames", 9))); - let overlapSource = "manual/default"; + let overlapSource = routeMode.includes("segment count") ? `${segmentCount} segment request` : "manual/default"; - if (planningMode === "explicit_preset_seconds") { - seconds = presetRec.seconds; - overlapSource = `${segmentPreset} preset/manual`; - } else { + if (routeMode.includes("seconds per segment")) { const secondsRec = getExecPlannerPresetForSeconds(seconds); if (secondsRec) { overlapSource = `${secondsRec.seconds}s duration`; } } - return { fps, planningMode, segmentPreset, seconds, overlap, overlapSource }; + return { fps, planningMode, segmentPreset, seconds, overlap, overlapSource, routeMode, segmentCount, singleDuration }; } function updateExecPlannerLivePreview(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { return; } node.properties = node.properties || {}; @@ -384,7 +1087,7 @@ function updateExecPlannerLivePreview(node) { } function syncExecPlannerExplicitPreset(node, sourceWidgetName = "") { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { return; } const planningMode = String(findWidget(node, "planning_mode")?.value || "manual_segment_seconds"); @@ -399,13 +1102,16 @@ function syncExecPlannerExplicitPreset(node, sourceWidgetName = "") { } function installExecPlannerExplicitPresetSync(node) { + if (!node) { + return; + } if (node._iamccsExecPlannerExplicitPresetSyncInstalled) { syncExecPlannerExplicitPreset(node); updateExecPlannerLivePreview(node); return; } node._iamccsExecPlannerExplicitPresetSyncInstalled = true; - for (const widgetName of ["fps", "segment_seconds", "planning_mode", "segment_preset", "overlap_frames", "ltx_round_mode"]) { + for (const widgetName of ["fps", "audio_img2vid_backend", "audio_img2vid_mode", "route_mode", "single_duration_seconds", "segment_count", "segment_seconds", "planning_mode", "segment_preset", "overlap_frames", "ltx_round_mode", "audio_preprocess_mode"]) { const widget = findWidget(node, widgetName); if (!widget) { continue; @@ -413,71 +1119,475 @@ function installExecPlannerExplicitPresetSync(node) { const originalCallback = widget.callback; widget.callback = (...args) => { originalCallback?.apply(widget, args); - if (widgetName === "planning_mode") { + if (widgetName === "planning_mode" || widgetName === "route_mode" || widgetName === "audio_preprocess_mode" || widgetName === "audio_img2vid_backend" || widgetName === "audio_img2vid_mode") { + if (widgetName === "audio_img2vid_backend" || widgetName === "audio_img2vid_mode") { + setWidgetValue(node, "audio_img2vid_backend", plannerA2IBackendDisplay(findWidget(node, "audio_img2vid_backend")?.value), { notify: false }); + setWidgetValue(node, "audio_img2vid_mode", plannerA2IModeDisplay(findWidget(node, "audio_img2vid_mode")?.value), { notify: false }); + syncPlannerA2IModeToInternalWidgets(node); + if (normalizePlannerA2IBackend(findWidget(node, "audio_img2vid_backend")?.value) === "legacy") { + setWidgetValue(node, "fps", 24, { notify: false }); + setWidgetValue(node, "audio_preprocess_mode", "raw_audio_only", { notify: false }); + setWidgetValue(node, "planning_mode", "manual_segment_seconds", { notify: false }); + setWidgetValue(node, "overlap_frames", 9, { notify: false }); + setWidgetValue(node, "ltx_round_mode", "up", { notify: false }); + } + } applyPlannerModeVisibility(node); } else { syncExecPlannerExplicitPreset(node, widgetName); } updateExecPlannerLivePreview(node); - app.graph.setDirtyCanvas(true, true); + const syncOptions = (widgetName === "audio_img2vid_backend" || widgetName === "audio_img2vid_mode") + ? { defer: true, applyRenderDefaults: true } + : {}; + syncPlannerFpsDownstreamWhenReady(node, "planner_widget", syncOptions); + markCanvasDirty(); }; } syncExecPlannerExplicitPreset(node); updateExecPlannerLivePreview(node); + syncPlannerFpsDownstreamWhenReady(node, "planner_install", { defer: true }); } function getLinxTargets(node) { const results = []; - for (const output of node.outputs || []) { - if (output?.type !== "IAMCCS_SUPERNODE_LINX") { - continue; - } - for (const linkId of output.links || []) { - const link = app.graph.links?.[linkId]; - if (!link) { + try { + for (const output of Array.isArray(node?.outputs) ? node.outputs : []) { + if (!output || output.type !== "IAMCCS_SUPERNODE_LINX") { continue; } - const targetNode = app.graph.getNodeById?.(link.target_id); - if (targetNode) { - results.push(targetNode); + const linkIds = Array.isArray(output.links) + ? output.links.filter((linkId) => linkId !== undefined && linkId !== null) + : []; + for (const linkId of linkIds) { + const link = getGraphLink(linkId); + if (!link) { + continue; + } + const targetId = getLinkTargetId(link); + if (targetId === undefined || targetId === null) { + continue; + } + const targetNode = app.graph?.getNodeById?.(targetId); + if (targetNode) { + results.push(targetNode); + } } } + } catch (error) { + console.warn("[IAMCCS SuperNodes] Ignored invalid LINX link while syncing UI", error); } return results; } -function applyVaeDecodeModeVisibility(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { +function getLinxSources(node) { + const results = []; + try { + for (const input of Array.isArray(node?.inputs) ? node.inputs : []) { + if (!input || (input.type && input.type !== "IAMCCS_SUPERNODE_LINX")) { + continue; + } + const linkIds = []; + if (input.link !== undefined && input.link !== null) { + linkIds.push(input.link); + } + if (Array.isArray(input.links)) { + linkIds.push(...input.links.filter((linkId) => linkId !== undefined && linkId !== null)); + } + for (const linkId of linkIds) { + const link = getGraphLink(linkId); + if (!link) { + continue; + } + const sourceId = getLinkSourceId(link); + if (sourceId === undefined || sourceId === null) { + continue; + } + const sourceNode = app.graph?.getNodeById?.(sourceId); + if (sourceNode) { + results.push(sourceNode); + } + } + } + } catch (error) { + console.warn("[IAMCCS SuperNodes] Ignored invalid LINX source while syncing UI", error); + } + return results; +} + +function getUpstreamLinxNodes(startNode) { + const results = []; + const visited = new Set(); + const queue = [...getLinxSources(startNode)]; + while (queue.length > 0) { + const node = queue.shift(); + if (!node || visited.has(node.id)) { + continue; + } + visited.add(node.id); + results.push(node); + queue.push(...getLinxSources(node)); + } + return results; +} + +function getRenderResolvedFps(renderNode) { + if (!renderNode || renderNode?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return null; + } + if (renderUsesGeneratedDuration(renderNode)) { + return readFiniteWidgetNumber(renderNode, "generated_media_fps", DEFAULT_GENERATED_FPS); + } + const plannerNode = getUpstreamLinxNodes(renderNode).find((node) => node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner"); + const plannerFps = readFiniteWidgetNumber(plannerNode, "fps", null); + return Number.isFinite(plannerFps) + ? plannerFps + : (renderUsesReferenceAudioImg2Vid(renderNode) ? REFERENCE_AUDIO_IMG2VID_FPS : null); +} + +function renderUsesReferenceAudioImg2Vid(renderNode) { + if (!renderNode || renderNode?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return false; + } + return String(findWidget(renderNode, "generation_type")?.value || "") === REFERENCE_AUDIO_IMG2VID_TYPE; +} + +function renderUsesTextAudio2Video(renderNode) { + if (!renderNode || renderNode?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return false; + } + return String(findWidget(renderNode, "generation_type")?.value || "") === "text+audio2video"; +} + +function syncTextAudio2VideoPlannerMode(renderNode) { + if (!renderUsesTextAudio2Video(renderNode)) { return; } - const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram_disk"); - const isLowRam = decodeMode === "low_ram_disk" || decodeMode === "very_low_ram_disk"; - const isNormal = decodeMode === "normal_tiled_vhs" || decodeMode === "custom_mode" || decodeMode === "inherit_render_backend"; - setWidgetVisibility(findWidget(node, "frames_subdir"), isLowRam); - setWidgetVisibility(findWidget(node, "image_format"), isLowRam); - setWidgetVisibility(findWidget(node, "jpg_quality"), isLowRam); - setWidgetVisibility(findWidget(node, "tiled_tile_size"), isNormal); - setWidgetVisibility(findWidget(node, "tiled_overlap"), isNormal); + for (const node of getUpstreamLinxNodes(renderNode)) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + continue; + } + const segmentSeconds = readPlannerNumber(node, "segment_seconds", 10.0); + const currentSingleDuration = readPlannerNumber(node, "single_duration_seconds", NaN); + setWidgetValue(node, "audio_img2vid_backend", PLANNER_A2I_BACKEND_MODERN, { notify: false }); + setWidgetValue(node, "audio_img2vid_mode", PLANNER_A2I_MODE_SINGLE, { notify: false }); + setWidgetValue(node, "route_mode", "single generation (duration only)", { notify: false }); + setWidgetValue(node, "segment_count", 1, { notify: false }); + if (!Number.isFinite(currentSingleDuration) || currentSingleDuration <= 0) { + setWidgetValue(node, "single_duration_seconds", segmentSeconds, { notify: false }); + } + applyPlannerModeVisibility(node); + updateExecPlannerLivePreview(node); + syncPlannerFpsDownstreamWhenReady(node, "text_audio_generation_type", { defer: true }); + } +} + +function syncLegacyBackendPlannerMode(renderNode, options = {}) { + if (!renderUsesReferenceAudioImg2Vid(renderNode)) { + return; + } + const applyDefaults = options.applyDefaults === true; + const backendMode = normalizeRenderBackendValue(findWidget(renderNode, "backend_mode")?.value); + if (!LEGACY_RENDER_BACKEND_DEFAULTS[backendMode]) { + return; + } + for (const node of getUpstreamLinxNodes(renderNode)) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + continue; + } + const currentSegmentSeconds = Math.max(0.01, readPlannerNumber(node, "segment_seconds", 10.0)); + const currentSingleDuration = Math.max(0.1, readPlannerNumber(node, "single_duration_seconds", currentSegmentSeconds)); + const currentSegmentCount = Math.max(1, Math.round(readPlannerNumber(node, "segment_count", 2))); + const segmentSeconds = Number.isFinite(currentSegmentSeconds) ? currentSegmentSeconds : 10.0; + const singleDuration = Number.isFinite(currentSingleDuration) ? currentSingleDuration : segmentSeconds; + let targetSegmentCount = currentSegmentCount; + setWidgetValue(node, "audio_img2vid_backend", PLANNER_A2I_BACKEND_LEGACY, { notify: false }); + if (backendMode === "legacy_single") { + setWidgetValue(node, "audio_img2vid_mode", PLANNER_A2I_MODE_SINGLE, { notify: false }); + setWidgetValue(node, "route_mode", "single generation (duration only)", { notify: false }); + setWidgetValue(node, "segment_count", 1, { notify: false }); + setWidgetValue(node, "single_duration_seconds", singleDuration, { notify: false }); + } else { + if (backendMode === "legacy_two_segments") { + targetSegmentCount = 2; + setWidgetValue(node, "audio_img2vid_mode", PLANNER_A2I_MODE_2_SEGMENTS, { notify: false }); + } else { + targetSegmentCount = Math.max(2, currentSegmentCount || 2); + setWidgetValue(node, "audio_img2vid_mode", PLANNER_A2I_MODE_LOOP, { notify: false }); + } + setWidgetValue(node, "route_mode", "choose segment count (audio / segments)", { notify: false }); + setWidgetValue(node, "segment_count", targetSegmentCount, { notify: false }); + setWidgetValue(node, "segment_seconds", segmentSeconds, { notify: false }); + } + if (applyDefaults) { + setWidgetValue(node, "fps", 24, { notify: false }); + setWidgetValue(node, "planning_mode", "manual_segment_seconds", { notify: false }); + setWidgetValue(node, "segment_preset", getExecPlannerPresetNameForSeconds(segmentSeconds), { notify: false }); + setWidgetValue(node, "overlap_frames", 9, { notify: false }); + setWidgetValue(node, "ltx_round_mode", "up", { notify: false }); + setWidgetValue(node, "audio_preprocess_mode", "raw_audio_only", { notify: false }); + } + applyPlannerModeVisibility(node, { skipPresetSync: true }); + updateExecPlannerLivePreview(node); + syncPlannerFpsDownstreamWhenReady(node, `legacy_backend_${backendMode}`, { defer: true }); + } +} + +function syncReferenceAudioImg2VidTiming(renderNode, options = {}) { + if (!renderUsesReferenceAudioImg2Vid(renderNode)) { + return; + } + const applyFpsDefault = options.applyFpsDefault === true; + const renderBackendMode = normalizeRenderBackendValue(findWidget(renderNode, "backend_mode")?.value); + const usesLegacyBackend = Boolean(LEGACY_RENDER_BACKEND_DEFAULTS[renderBackendMode]); + const defaultFps = usesLegacyBackend ? 24.0 : REFERENCE_AUDIO_IMG2VID_FPS; + let resolvedFps = null; + for (const node of getUpstreamLinxNodes(renderNode)) { + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + if (applyFpsDefault) { + setWidgetValue(node, "fps", defaultFps, { notify: false }); + } + resolvedFps = readFiniteWidgetNumber(node, "fps", resolvedFps); + applyPlannerModeVisibility(node, { skipPresetSync: true }); + updateExecPlannerLivePreview(node); + } + } + if (!Number.isFinite(resolvedFps) && applyFpsDefault) { + resolvedFps = defaultFps; + } + const visited = new Set(); + const queue = [...getLinxTargets(renderNode)]; + while (queue.length > 0) { + const node = queue.shift(); + if (!node || visited.has(node.id)) { + continue; + } + visited.add(node.id); + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + if (Number.isFinite(resolvedFps)) { + setWidgetValue(node, "frame_rate", resolvedFps, { notify: false }); + } + if (applyFpsDefault) { + setWidgetValue(node, "decode_mode", "normal_tiled_iamccs", { notify: false }); + setWidgetValue(node, "trim_to_audio", usesLegacyBackend, { notify: false }); + applyVaeDecodeModeDefaults(node, "normal_tiled_iamccs"); + applyVaeDecodeModeVisibility(node); + } + continue; + } + if (node?.comfyClass === "IAMCCS-SuperNodes Second Stage") { + queue.push(...getLinxTargets(node)); + } + } +} + +function syncUpstreamFpsFromVae(vaeNode) { + if (vaeNode?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + return; + } + const frameRate = readFiniteWidgetNumber(vaeNode, "frame_rate", null); + if (!Number.isFinite(frameRate)) { + return; + } + for (const node of getUpstreamLinxNodes(vaeNode)) { + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && (renderUsesGeneratedDuration(node) || renderUsesReferenceAudioImg2Vid(node))) { + setWidgetValue(node, "generated_media_fps", frameRate, { notify: false }); + sanitizeRenderWidgetValues(node); + applyRenderGeneratedDurationVisibility(node); + applyRenderInternalWidgetVisibility(node); + } + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + setWidgetValue(node, "fps", frameRate, { notify: false }); + updateExecPlannerLivePreview(node); + } + } +} + +function syncPlannerFpsDownstream(plannerNode, options = {}) { + if (plannerNode?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + return; + } + const visited = new Set(); + const queue = [...getLinxTargets(plannerNode)]; + while (queue.length > 0) { + const node = queue.shift(); + if (!node || visited.has(node.id)) { + continue; + } + visited.add(node.id); + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (renderUsesReferenceAudioImg2Vid(node)) { + const backendMode = resolvePlannerA2IRenderBackend(plannerNode); + const backendWidget = findWidget(node, "backend_mode"); + const previousBackend = normalizeRenderBackendValue(backendWidget?.value); + if (backendWidget && (previousBackend !== backendMode || options.applyRenderDefaults === true)) { + setWidgetValue(node, "backend_mode", backendMode, { notify: false }); + applyRenderBackendModeDefaults(node, backendMode); + } + sanitizeRenderWidgetValues(node); + applyRenderInternalWidgetVisibility(node); + } + syncRenderLinxWhenReady(node, "planner_downstream_render", { defer: true }); + continue; + } + queue.push(...getLinxTargets(node)); + } +} + +function syncUpstreamDecodeFromVae(vaeNode) { + if (vaeNode?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + return; + } + const decodeMode = String(findWidget(vaeNode, "decode_mode")?.value || ""); + if (!VAE_DECODE_MODE_VALUES.has(decodeMode) || decodeMode === "inherit_render_backend") { + return; + } + for (const node of getUpstreamLinxNodes(vaeNode)) { + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + setWidgetValue(node, "vae_mode", decodeMode, { notify: false }); + sanitizeRenderWidgetValues(node); + applyRenderInternalWidgetVisibility(node); + } + } +} + +function applyVaeDecodeModeVisibility(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + return; + } + normalizeVaeWidgetOrder(node); + const decodeMode = String(findWidget(node, "decode_mode")?.value || "normal_tiled_iamccs"); + const normalizedMode = decodeMode === "normal_tiled_vhs" ? "normal_tiled_iamccs" : decodeMode; + const isLowRam = normalizedMode === "low_ram" || normalizedMode === "low_ram_disk"; + const isVeryLowRam = normalizedMode === "very_low_ram" || normalizedMode === "very_low_ram_disk"; + const isHighVram = normalizedMode === "high_vram"; + const isTiled = normalizedMode === "normal_tiled_iamccs" || normalizedMode === "custom_mode" || normalizedMode === "inherit_render_backend"; + const supportsDecodeSettings = isTiled || isLowRam || isVeryLowRam; + const modeExpanded = node.properties?.iamccs_section_mode !== false; + const outputExpanded = !!node.properties?.iamccs_section_output; + + setWidgetVisibility(findWidget(node, "ui_preset"), false); + setWidgetVisibility(findWidget(node, "pix_fmt"), false); + setWidgetVisibility(findWidget(node, "save_metadata"), false); + + setWidgetLabel(node, "decode_mode", "VAE Mode"); + setWidgetLabel(node, "frame_rate", "FPS"); + setWidgetLabel(node, "tiled_tile_size", isVeryLowRam ? "Very Low Tile Size" : isLowRam ? "Low RAM Tile Size" : "Tile Size"); + setWidgetLabel(node, "tiled_overlap", isVeryLowRam ? "Very Low Overlap" : isLowRam ? "Low RAM Overlap" : "Tile Overlap"); + setWidgetLabel(node, "tiled_temporal_size", isVeryLowRam ? "Very Low Temporal Size" : isLowRam ? "Low RAM Temporal Size" : "Temporal Size"); + setWidgetLabel(node, "tiled_temporal_overlap", isVeryLowRam ? "Very Low Temporal Overlap" : isLowRam ? "Low RAM Temporal Overlap" : "Temporal Overlap"); + setWidgetLabel(node, "cleanup_before_decode", "Cleanup Before Decode"); + setWidgetLabel(node, "frames_subdir", isVeryLowRam ? "Very Low Frames Folder" : "Low RAM Frames Folder"); + setWidgetLabel(node, "image_format", "Disk Frame Format"); + setWidgetLabel(node, "jpg_quality", "Disk JPG Quality"); + setWidgetLabel(node, "output_root", isVeryLowRam ? "Very Low Output Root" : "Low RAM Output Root"); + setWidgetLabel(node, "vram_flush", "Flush VRAM"); + setWidgetLabel(node, "debug_verbose", "Verbose Debug"); + + setWidgetVisibility(findWidget(node, "decode_mode"), modeExpanded); + setWidgetVisibility(findWidget(node, "frame_rate"), modeExpanded); + setWidgetVisibility(findWidget(node, "tiled_tile_size"), modeExpanded && supportsDecodeSettings); + setWidgetVisibility(findWidget(node, "tiled_overlap"), modeExpanded && supportsDecodeSettings); + setWidgetVisibility(findWidget(node, "tiled_temporal_size"), modeExpanded && supportsDecodeSettings); + setWidgetVisibility(findWidget(node, "tiled_temporal_overlap"), modeExpanded && supportsDecodeSettings); + setWidgetVisibility(findWidget(node, "cleanup_before_decode"), modeExpanded && supportsDecodeSettings && !isHighVram); + setWidgetVisibility(findWidget(node, "frames_subdir"), modeExpanded && (isLowRam || isVeryLowRam)); + setWidgetVisibility(findWidget(node, "image_format"), modeExpanded && (isLowRam || isVeryLowRam)); + setWidgetVisibility(findWidget(node, "jpg_quality"), modeExpanded && (isLowRam || isVeryLowRam)); + setWidgetVisibility(findWidget(node, "output_root"), modeExpanded && (isLowRam || isVeryLowRam)); + setWidgetVisibility(findWidget(node, "vram_flush"), modeExpanded && (isLowRam || isVeryLowRam)); + setWidgetVisibility(findWidget(node, "filename_prefix"), outputExpanded); + setWidgetVisibility(findWidget(node, "crf"), outputExpanded); + setWidgetVisibility(findWidget(node, "trim_to_audio"), outputExpanded); + setWidgetVisibility(findWidget(node, "debug_verbose"), outputExpanded); + + for (const stale of ["decode_settings"]) { + const staleButton = node.widgets?.find?.((widget) => widget?._iamccsSectionKey === stale); + if (staleButton) { + setWidgetVisibility(staleButton, false); + } + } fitNodeToWidgets(node); } +function applyVaeDecodeModeDefaults(node, modeOverride = null) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + return; + } + const decodeMode = String(modeOverride || findWidget(node, "decode_mode")?.value || "normal_tiled_iamccs"); + if (modeOverride !== null) { + const widget = findWidget(node, "decode_mode"); + if (widget) { + widget.value = decodeMode; + } + } + const defaults = VAE_DECODE_MODE_DEFAULTS[decodeMode]; + if (!defaults) { + return; + } + Object.entries(defaults).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue)); +} + +function applyRenderAnchorModeDefaults(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return; + } + const anchorMode = String(findWidget(node, "continuity_anchor_mode")?.value || "off"); + const defaults = ANCHOR_MODE_DEFAULTS[anchorMode]; + if (!defaults) { + return; + } + Object.entries(defaults).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue)); +} + +function normalizeRenderBackendValue(value) { + const text = String(value || "auto"); + if (text === "legacy backend") { + return "legacy_single"; + } + return text; +} + +function applyRenderBackendModeDefaults(node, modeOverride = null) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return; + } + const generationType = String(findWidget(node, "generation_type")?.value || ""); + if (generationType !== REFERENCE_AUDIO_IMG2VID_TYPE) { + return; + } + const backendMode = normalizeRenderBackendValue(modeOverride ?? findWidget(node, "backend_mode")?.value); + const defaults = LEGACY_RENDER_BACKEND_DEFAULTS[backendMode]; + if (!defaults) { + return; + } + const backendWidget = findWidget(node, "backend_mode"); + if (backendWidget && backendWidget.value !== backendMode) { + backendWidget.value = backendMode; + } + Object.entries(defaults).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue, { notify: false })); +} + function applyRenderAnchorVisibility(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } const anchorMode = String(findWidget(node, "continuity_anchor_mode")?.value || "off"); const antiDriftMode = String(findWidget(node, "anti_drift_mode")?.value || "off"); - const anchorsEnabled = anchorMode !== "off"; + const sectionExpanded = !!node.properties?.iamccs_section_latent_refresh; + const anchorsEnabled = sectionExpanded && anchorMode !== "off"; const periodicAnchor = anchorMode.startsWith("periodic_"); + setWidgetVisibility(findWidget(node, "continuity_anchor_mode"), sectionExpanded); setWidgetVisibility(findWidget(node, "anchor_refresh_interval"), anchorsEnabled && periodicAnchor); setWidgetVisibility(findWidget(node, "anchor_image_strength"), anchorsEnabled); - setWidgetVisibility(findWidget(node, "anti_drift_strength"), antiDriftMode !== "off"); - setWidgetVisibility(findWidget(node, "identity_persistence_strength"), antiDriftMode === "dual_reference_adain"); + setWidgetVisibility(findWidget(node, "anti_drift_mode"), anchorsEnabled); + setWidgetVisibility(findWidget(node, "anti_drift_strength"), anchorsEnabled && antiDriftMode !== "off"); + setWidgetVisibility(findWidget(node, "identity_persistence_strength"), anchorsEnabled && antiDriftMode === "dual_reference_adain"); fitNodeToWidgets(node); } function applyRenderAnchorLabels(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } setWidgetLabel(node, "backend_mode", "Render Backend"); @@ -485,24 +1595,70 @@ function applyRenderAnchorLabels(node) { setWidgetLabel(node, "media_mode", "Media Mode"); setWidgetLabel(node, "generation_type", "Generation Type"); setWidgetLabel(node, "generated_media_duration_seconds", "Generated Duration"); - setWidgetLabel(node, "continuity_anchor_mode", "Anchor Refresh"); - setWidgetLabel(node, "anchor_refresh_interval", "Refresh Interval"); - setWidgetLabel(node, "anchor_image_strength", "Anchor Guidance Strength"); + setWidgetLabel(node, "generated_media_fps", "Generated FPS"); + setWidgetLabel(node, "stitch_preset", "Stitch Preset"); + setWidgetLabel(node, "overlap_side", "Overlap Side"); + setWidgetLabel(node, "overlap_mode", "Blend Mode"); + setWidgetLabel(node, "start_frames_rule", "Start Frames Rule"); + setWidgetLabel(node, "color_match_mode", "Color Match"); + setWidgetLabel(node, "color_match_strength", "Color Strength"); + setWidgetLabel(node, "audio_context_mode", "Audio Context"); + setWidgetLabel(node, "audio_left_context_s", "Left Context (s)"); + setWidgetLabel(node, "audio_right_context_s", "Right Context (s)"); + setWidgetLabel(node, "continuity_anchor_mode", "Latent Refresh Mode"); + setWidgetLabel(node, "anchor_refresh_interval", "Refresh Every N Segments"); + setWidgetLabel(node, "anchor_image_strength", "Reference Strength"); setWidgetLabel(node, "motion_intensity", "Motion Intensity"); setWidgetLabel(node, "vram_flush", "VRAM Flush"); setWidgetLabel(node, "second_stage_mode", "Second Stage"); setWidgetLabel(node, "stage2_model_policy", "Stage2 Model Policy"); setWidgetLabel(node, "second_stage_upscale_model", "2x Upscale Model"); setWidgetLabel(node, "second_stage_reinject_strength", "Anchor Reinject Strength"); + setWidgetLabel(node, "debug_verbose", "Verbose Debug"); } -function syncRenderGenerationType(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { +function markRenderGenerationTypeDefaultsApplied(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } - const generationType = String(findWidget(node, "generation_type")?.value || "aud+img2video_infinite"); - const config = GENERATION_TYPE_CONFIGS[generationType] || GENERATION_TYPE_CONFIGS["aud+img2video_infinite"]; + node.properties = node.properties || {}; + const generationType = String(findWidget(node, "generation_type")?.value || "audio+image2video"); + node.properties.iamccs_last_generation_type = generationType; + node.properties.iamccs_generation_defaults_applied = true; +} + +function syncRenderGenerationType(node, options = {}) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return; + } + node.properties = node.properties || {}; + const generationType = String(findWidget(node, "generation_type")?.value || "audio+image2video"); + const alreadyApplied = node.properties.iamccs_generation_defaults_applied === true; + const lastGenerationType = String(node.properties.iamccs_last_generation_type || ""); + if (!options.force && alreadyApplied && lastGenerationType === generationType) { + syncReferenceAudioImg2VidTimingWhenReady(node, { applyFpsDefault: false }, "generation_type_unchanged", { defer: true }); + return; + } + const config = GENERATION_TYPE_CONFIGS[generationType] || GENERATION_TYPE_CONFIGS["audio+image2video"]; Object.entries(config).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue)); + if (generationType === REFERENCE_AUDIO_IMG2VID_TYPE) { + setWidgetValue(node, "generated_media_fps", REFERENCE_AUDIO_IMG2VID_FPS, { notify: false }); + } + applyRenderAnchorModeDefaults(node); + markRenderGenerationTypeDefaultsApplied(node); + syncReferenceAudioImg2VidTimingWhenReady(node, { applyFpsDefault: generationType === REFERENCE_AUDIO_IMG2VID_TYPE }, "generation_type_defaults", { defer: true }); + syncTextAudio2VideoPlannerModeWhenReady(node, "generation_type_defaults", { defer: true }); +} + +function applyRenderGenerationTypeChange(node) { + syncRenderGenerationType(node, { force: true }); + sanitizeRenderWidgetValues(node); + applyRenderAnchorVisibility(node); + applyRenderSecondStageVisibility(node); + applyRenderInternalWidgetVisibility(node); + syncTextAudio2VideoPlannerModeWhenReady(node, "generation_type_change", { defer: true }); + syncRenderLinxWhenReady(node, "generation_type_change"); + markCanvasDirty(); } function renderUsesGeneratedDuration(node) { @@ -510,12 +1666,33 @@ function renderUsesGeneratedDuration(node) { return GENERATED_DURATION_TYPES.has(generationType); } + function applyRenderGeneratedDurationVisibility(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } const generationExpanded = node.properties?.iamccs_section_generation !== false; setWidgetVisibility(findWidget(node, "generated_media_duration_seconds"), generationExpanded && renderUsesGeneratedDuration(node)); + setWidgetVisibility(findWidget(node, "generated_media_fps"), generationExpanded && renderUsesGeneratedDuration(node)); + fitNodeToWidgets(node); +} + +function applyRenderManualSigmasVisibility(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + return; + } + const samplingExpanded = node.properties?.iamccs_section_sampling !== false; + const showWidget = findWidget(node, "show_manual_sigmas"); + const manualWidget = findWidget(node, "manual_sigmas"); + const backendMode = normalizeRenderBackendValue(findWidget(node, "backend_mode")?.value); + const isLegacyBackend = Boolean(LEGACY_RENDER_BACKEND_DEFAULTS[backendMode]); + setWidgetLabel(node, "show_manual_sigmas", "Advanced Manual Sigmas"); + setWidgetLabel(node, "manual_sigmas", "Manual Sigmas"); + if (isLegacyBackend && manualWidget && !String(manualWidget.value || "").trim()) { + manualWidget.value = REF_MAIN_SIGMAS; + } + setWidgetVisibility(showWidget, samplingExpanded); + setWidgetVisibility(manualWidget, samplingExpanded && showWidget?.value === true); fitNodeToWidgets(node); } @@ -532,36 +1709,51 @@ function inferRenderGenerationType(valuesByName) { if (generationMode === "t2v" || mediaMode === "input_audio_t2v" || mediaMode === "generated_audio_t2v") { return "text+audio2video"; } - if (backendMode === "two_segments_normal_vram") { - return "aud+img2video_2_segments"; + if ( + backendMode === "two_segments_normal_vram" + || backendMode === "three_segments_normal_vram" + || backendMode === "loop_normal_vram" + || backendMode === "loop_low_ram_disk" + || backendMode === "legacy backend" + || backendMode === "legacy_single" + || backendMode === "legacy_two_segments" + || backendMode === "legacy_loop" + || backendMode === "auto" + ) { + return "audio+image2video"; } - if (backendMode === "loop_normal_vram" || backendMode === "loop_low_ram_disk" || backendMode === "auto") { - return "aud+img2video_infinite"; - } - return "aud+img2video_simple"; + return "audio+image2video"; } function applyRenderPresetDropdownOptions(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } const widget = findWidget(node, "ui_preset"); if (widget?.options) { widget.options.values = RENDER_UI_PRESET_VISIBLE_VALUES; } + const backendWidget = findWidget(node, "backend_mode"); + if (backendWidget?.options) { + backendWidget.options.values = RENDER_BACKEND_MODE_VISIBLE_VALUES; + } } function applyRenderInternalWidgetVisibility(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } for (const widgetName of RENDER_INTERNAL_WIDGETS) { setWidgetVisibility(findWidget(node, widgetName), false); } + const generationType = String(findWidget(node, "generation_type")?.value || "audio+image2video"); + const generationExpanded = node.properties?.iamccs_section_generation !== false; + setWidgetVisibility(findWidget(node, "backend_mode"), false); + applyRenderManualSigmasVisibility(node); } function applyRenderSecondStageVisibility(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } @@ -577,7 +1769,11 @@ function applyRenderSecondStageVisibility(node) { } function syncDownstreamVaeDecodeModes(renderNode) { - const renderDecode = String(findWidget(renderNode, "vae_mode")?.value || "low_ram_disk"); + const renderDecode = String(findWidget(renderNode, "vae_mode")?.value || "normal_tiled_iamccs"); + const renderFps = getRenderResolvedFps(renderNode); + const renderBackendMode = normalizeRenderBackendValue(findWidget(renderNode, "backend_mode")?.value); + const usesLegacyBackend = Boolean(LEGACY_RENDER_BACKEND_DEFAULTS[renderBackendMode]); + const shouldSyncFps = Number.isFinite(renderFps); const visited = new Set(); const queue = [...getLinxTargets(renderNode)]; while (queue.length > 0) { @@ -586,14 +1782,25 @@ function syncDownstreamVaeDecodeModes(renderNode) { continue; } visited.add(node.id); - if (node.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { - setWidgetValue(node, "decode_mode", renderDecode); + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + const decodeWidget = findWidget(node, "decode_mode"); + if (decodeWidget && String(decodeWidget.value || "") === "inherit_render_backend") { + setWidgetValue(node, "decode_mode", renderDecode); + } + if (shouldSyncFps) { + setWidgetValue(node, "frame_rate", renderFps); + } + node.properties = node.properties || {}; + if (usesLegacyBackend && node.properties.iamccs_legacy_trim_default_applied_for_backend !== renderBackendMode) { + setWidgetValue(node, "trim_to_audio", true, { notify: false }); + node.properties.iamccs_legacy_trim_default_applied_for_backend = renderBackendMode; + } setWidgetValue(node, "ui_preset", "custom"); applyVaeDecodeModeVisibility(node); - app.graph.setDirtyCanvas(true, true); + markCanvasDirty(); continue; } - if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") { + if (node?.comfyClass === "IAMCCS-SuperNodes Second Stage") { queue.push(...getLinxTargets(node)); } } @@ -618,13 +1825,14 @@ function applyPresetConfig(node, nodeName) { setWidgetVisibility(findWidget(node, widgetName), !!isVisible); }); if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + sanitizeVaeWidgetValues(node); applyVaeDecodeModeVisibility(node); } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + sanitizeRenderWidgetValues(node); applyRenderAnchorLabels(node); applyRenderAnchorVisibility(node); applyRenderSecondStageVisibility(node); - syncRenderGenerationType(node); applyRenderGeneratedDurationVisibility(node); applyRenderPresetDropdownOptions(node); } @@ -632,18 +1840,268 @@ function applyPresetConfig(node, nodeName) { } function getSerializableWidgets(node) { - return (node.widgets || []).filter((widget) => widget && widget.serialize !== false && !widget._iamccsSectionKey); + if (!hasWidgetList(node)) { + return []; + } + return node.widgets.filter((widget) => widget && widget.serialize !== false && !widget._iamccsSectionKey); } function isSectionButtonCaption(value) { const text = String(value ?? ""); - return /^\[[+-]\]\s/.test(text) || text.startsWith("▶ ") || text.startsWith("▼ "); + return /^\[[+-]\]\s/.test(text) || text.startsWith("â–¶ ") || text.startsWith("â–¼ "); } function sanitizeSerializedValues(values) { return (values || []).filter((value) => value !== null && value !== undefined && !isSectionButtonCaption(value)); } +function applyMissingGenerationDefaults(valuesByName, defaults) { + if (!valuesByName || !defaults) { + return valuesByName; + } + Object.entries(defaults).forEach(([widgetName, widgetValue]) => { + const current = valuesByName[widgetName]; + const invalidNumber = typeof widgetValue === "number" && !Number.isFinite(Number(current)); + if (current === undefined || current === null || isSectionButtonCaption(current) || invalidNumber) { + valuesByName[widgetName] = widgetValue; + } + }); + return valuesByName; +} + +function isSerializedBoolean(value) { + if (typeof value === "boolean") { + return true; + } + const text = String(value ?? "").toLowerCase(); + return text === "true" || text === "false"; +} + +function chooseRenderSerializedOrder(cleanValues, currentOrder, previousOrder) { + const advancedIndex = currentOrder.indexOf("show_manual_sigmas"); + const colorAwareOrder = (order) => { + const colorIndex = order.indexOf("color_match_mode"); + if (colorIndex < 0) { + return order; + } + if (colorIndex >= cleanValues.length) { + return order.filter((name) => name !== "color_match_mode" && name !== "color_match_strength"); + } + const colorValue = String(cleanValues[colorIndex] ?? ""); + if (!RENDER_COLOR_MATCH_MODE_VALUES.has(colorValue)) { + return order.filter((name) => name !== "color_match_mode" && name !== "color_match_strength"); + } + return order; + }; + if (advancedIndex < 0 || advancedIndex >= cleanValues.length) { + return colorAwareOrder(previousOrder || currentOrder); + } + return colorAwareOrder(isSerializedBoolean(cleanValues[advancedIndex]) ? currentOrder : (previousOrder || currentOrder)); +} + +function readPlannerSerializedValues(cleanValues) { + const valuesByName = { + audio_img2vid_backend: PLANNER_A2I_BACKEND_MODERN, + audio_img2vid_mode: PLANNER_A2I_MODE_SINGLE, + route_mode: "choose segment count (audio / segments)", + single_duration_seconds: 10.0, + segment_count: 2, + segment_seconds: 10.0, + fps: REFERENCE_AUDIO_IMG2VID_FPS, + audio_preprocess_mode: "melband_vocals_duration_math", + melband_model_name: "MelBandRoformer_fp32.safetensors", + planning_mode: "manual_segment_seconds", + segment_preset: "10sec", + overlap_frames: 9, + ltx_round_mode: "up", + audio_concat_payload: "", + debug_verbose: false, + }; + const first = String(cleanValues[0] ?? ""); + let order = PLANNER_PY_WIDGET_ORDER; + if (PLANNER_AUDIO_IMG2VID_BACKEND_VALUES.has(first)) { + order = PLANNER_VISUAL_WIDGET_ORDER; + } else if (PLANNER_ROUTE_MODE_VALUES.has(first)) { + order = PLANNER_LEGACY_VISUAL_WIDGET_ORDER; + } else if (PLANNER_ROUTE_MODE_VALUES.has(String(cleanValues[8] ?? ""))) { + order = PLANNER_LEGACY_PY_WIDGET_ORDER; + } + order.forEach((widgetName, index) => { + if (index < cleanValues.length) { + valuesByName[widgetName] = cleanValues[index]; + } + }); + if (!PLANNER_AUDIO_IMG2VID_BACKEND_VALUES.has(String(valuesByName.audio_img2vid_backend || ""))) { + valuesByName.audio_img2vid_backend = PLANNER_A2I_BACKEND_MODERN; + } + if (!PLANNER_AUDIO_IMG2VID_MODE_VALUES.has(String(valuesByName.audio_img2vid_mode || ""))) { + const routeMode = String(valuesByName.route_mode || ""); + const count = Number(valuesByName.segment_count || 2); + valuesByName.audio_img2vid_mode = routeMode.includes("single") + ? PLANNER_A2I_MODE_SINGLE + : count === 2 + ? PLANNER_A2I_MODE_2_SEGMENTS + : count === 3 + ? PLANNER_A2I_MODE_3_SEGMENTS + : PLANNER_A2I_MODE_LOOP; + } + valuesByName.audio_img2vid_backend = plannerA2IBackendDisplay(valuesByName.audio_img2vid_backend); + valuesByName.audio_img2vid_mode = plannerA2IModeDisplay(valuesByName.audio_img2vid_mode); + if (!PLANNER_ROUTE_MODE_VALUES.has(String(valuesByName.route_mode || ""))) { + valuesByName.route_mode = "choose segment count (audio / segments)"; + } + if (!PLANNER_MODE_VALUES.has(String(valuesByName.planning_mode || ""))) { + valuesByName.planning_mode = "manual_segment_seconds"; + } + if (!PLANNER_SEGMENT_PRESET_VALUES.has(String(valuesByName.segment_preset || ""))) { + valuesByName.segment_preset = "10sec"; + } + if (!PLANNER_ROUND_MODE_VALUES.has(String(valuesByName.ltx_round_mode || ""))) { + valuesByName.ltx_round_mode = "up"; + } + if (!PLANNER_AUDIO_MODE_VALUES.has(String(valuesByName.audio_preprocess_mode || ""))) { + valuesByName.audio_preprocess_mode = "melband_vocals_duration_math"; + } + for (const [widgetName, fallback] of Object.entries({ + fps: REFERENCE_AUDIO_IMG2VID_FPS, + segment_seconds: 10.0, + overlap_frames: 9, + segment_count: 2, + single_duration_seconds: 10.0, + })) { + if (!Number.isFinite(Number(valuesByName[widgetName])) || isSectionButtonCaption(valuesByName[widgetName])) { + valuesByName[widgetName] = fallback; + } + } + if (typeof valuesByName.debug_verbose !== "boolean") { + valuesByName.debug_verbose = false; + } + return valuesByName; +} + +function readRenderSerializedValues(cleanValues) { + const valuesByName = {}; + if (RENDER_GENERATION_TYPE_VALUES.has(String(cleanValues[0] || ""))) { + const hasGeneratedFps = !["img2vid", "t2v"].includes(String(cleanValues[3] || "")); + const renderOrder = hasGeneratedFps + ? chooseRenderSerializedOrder(cleanValues, RENDER_CURRENT_WIDGET_ORDER, RENDER_CURRENT_WIDGET_ORDER_PRE_ADVANCED_MANUAL_SIGMAS) + : chooseRenderSerializedOrder(cleanValues, RENDER_CURRENT_WIDGET_ORDER_PRE_GENERATED_FPS, RENDER_CURRENT_WIDGET_ORDER_PRE_GENERATED_FPS_PRE_ADVANCED_MANUAL_SIGMAS); + renderOrder.forEach((widgetName, index) => { + if (index < cleanValues.length) { + valuesByName[widgetName] = cleanValues[index]; + } + }); + valuesByName.show_manual_sigmas = isSerializedBoolean(valuesByName.show_manual_sigmas) + ? String(valuesByName.show_manual_sigmas).toLowerCase() === "true" + : false; + const generationConfig = GENERATION_TYPE_CONFIGS[String(valuesByName.generation_type)] || GENERATION_TYPE_CONFIGS["audio+image2video"]; + valuesByName.ui_preset = RENDER_UI_PRESET_VALUES.has(String(valuesByName.ui_preset || "")) ? valuesByName.ui_preset : "custom"; + valuesByName.generated_media_duration_seconds = Number.isFinite(Number(valuesByName.generated_media_duration_seconds)) + ? Number(valuesByName.generated_media_duration_seconds) + : 10.0; + valuesByName.generated_media_fps = Number.isFinite(Number(valuesByName.generated_media_fps)) + ? Number(valuesByName.generated_media_fps) + : Number(generationConfig.generated_media_fps || DEFAULT_GENERATED_FPS); + return normalizeReferenceAudioImg2VidValues(valuesByName); + } + if (["img2vid", "t2v"].includes(String(cleanValues[0] || ""))) { + const renderOrder = chooseRenderSerializedOrder(cleanValues, RENDER_LEGACY_WIDGET_ORDER, RENDER_LEGACY_WIDGET_ORDER_PRE_ADVANCED_MANUAL_SIGMAS); + renderOrder.forEach((widgetName, index) => { + if (index < cleanValues.length) { + valuesByName[widgetName] = cleanValues[index]; + } + }); + valuesByName.show_manual_sigmas = isSerializedBoolean(valuesByName.show_manual_sigmas) + ? String(valuesByName.show_manual_sigmas).toLowerCase() === "true" + : false; + valuesByName.generation_type = RENDER_GENERATION_TYPE_VALUES.has(String(valuesByName.generation_type || "")) + ? valuesByName.generation_type + : inferRenderGenerationType(valuesByName); + valuesByName.ui_preset = RENDER_UI_PRESET_VALUES.has(String(valuesByName.ui_preset || "")) ? valuesByName.ui_preset : "custom"; + valuesByName.generated_media_duration_seconds = Number.isFinite(Number(valuesByName.generated_media_duration_seconds)) + ? Number(valuesByName.generated_media_duration_seconds) + : 10.0; + const generationConfig = GENERATION_TYPE_CONFIGS[String(valuesByName.generation_type)] || GENERATION_TYPE_CONFIGS["audio+image2video"]; + applyMissingGenerationDefaults(valuesByName, generationConfig); + valuesByName.generated_media_fps = Number.isFinite(Number(valuesByName.generated_media_fps)) + ? Number(valuesByName.generated_media_fps) + : Number(generationConfig.generated_media_fps || DEFAULT_GENERATED_FPS); + return normalizeReferenceAudioImg2VidValues(valuesByName); + } + return null; +} + +function readVaeSerializedValues(cleanValues) { + const valuesByName = { + tiled_temporal_size: 256, + tiled_temporal_overlap: 32, + cleanup_before_decode: false, + }; + const decodeFirstOrder = [ + "decode_mode", "tiled_tile_size", "tiled_overlap", "tiled_temporal_size", "tiled_temporal_overlap", + "cleanup_before_decode", "frame_rate", "frames_subdir", "image_format", "jpg_quality", "output_root", + "filename_prefix", "crf", "pix_fmt", "trim_to_audio", "save_metadata", "vram_flush", "ui_preset", "debug_verbose", + ]; + let order = VAE_CURRENT_WIDGET_ORDER; + if (VAE_DECODE_MODE_VALUES.has(String(cleanValues[0] || ""))) { + order = decodeFirstOrder; + } else if (typeof cleanValues[2] === "string") { + order = cleanValues.length >= VAE_CURRENT_PY_WIDGET_ORDER.length + ? VAE_CURRENT_PY_WIDGET_ORDER + : VAE_LEGACY_PY_WIDGET_ORDER; + } else if (typeof cleanValues[4] === "string") { + order = VAE_LEGACY_WIDGET_ORDER; + } + order.forEach((widgetName, index) => { + if (index < cleanValues.length) { + valuesByName[widgetName] = cleanValues[index]; + } + }); + valuesByName.ui_preset = VAE_UI_PRESET_VALUES.has(String(valuesByName.ui_preset || "")) ? valuesByName.ui_preset : "custom"; + valuesByName.decode_mode = VAE_DECODE_MODE_VALUES.has(String(valuesByName.decode_mode || "")) ? valuesByName.decode_mode : "normal_tiled_iamccs"; + return valuesByName; +} + +function applyValuesByNameToWidgets(node, valuesByName) { + if (!valuesByName) { + return; + } + for (const widget of getSerializableWidgets(node)) { + if (Object.prototype.hasOwnProperty.call(valuesByName, widget.name)) { + widget.value = valuesByName[widget.name]; + } + } +} + +function normalizeConfigureWidgetValues(node, nodeName, info) { + if (!info || !Array.isArray(info.widgets_values) || !hasWidgetList(node) || !node.widgets.length) { + return; + } + const cleanValues = sanitizeSerializedValues(info.widgets_values); + let valuesByName = null; + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + valuesByName = readRenderSerializedValues(cleanValues); + } else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + valuesByName = readVaeSerializedValues(cleanValues); + } else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + valuesByName = readPlannerSerializedValues(cleanValues); + } + if (!valuesByName) { + return; + } + const normalized = []; + for (const widget of node.widgets) { + if (!widget || widget.serialize === false || widget._iamccsSectionKey) { + normalized.push(null); + } else if (Object.prototype.hasOwnProperty.call(valuesByName, widget.name)) { + normalized.push(valuesByName[widget.name]); + } else { + normalized.push(widget.value ?? null); + } + } + info.widgets_values = normalized; +} + function sanitizeRenderCombo(node, widgetName, fallback, validValues) { const widget = findWidget(node, widgetName); if (!widget) { @@ -655,54 +2113,281 @@ function sanitizeRenderCombo(node, widgetName, fallback, validValues) { } } +function sanitizeRenderNumber(node, widgetName, fallback) { + const widget = findWidget(node, widgetName); + if (!widget) { + return; + } + const value = Number(widget.value); + if (isSectionButtonCaption(widget.value) || !Number.isFinite(value)) { + widget.value = fallback; + } +} + +function sanitizeBooleanWidget(node, widgetName, fallback) { + const widget = findWidget(node, widgetName); + if (!widget) { + return; + } + if (isSectionButtonCaption(widget.value) || typeof widget.value !== "boolean") { + widget.value = !!fallback; + } +} + function sanitizeRenderWidgetValues(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { return; } applyRenderPresetDropdownOptions(node); sanitizeRenderCombo(node, "ui_preset", "custom", RENDER_UI_PRESET_VALUES); - sanitizeRenderCombo(node, "generation_type", "aud+img2video_infinite", RENDER_GENERATION_TYPE_VALUES); + sanitizeRenderCombo(node, "generation_type", "audio+image2video", RENDER_GENERATION_TYPE_VALUES); const durationWidget = findWidget(node, "generated_media_duration_seconds"); if (durationWidget && (isSectionButtonCaption(durationWidget.value) || !Number.isFinite(Number(durationWidget.value)))) { durationWidget.value = 10.0; } + const generationType = String(findWidget(node, "generation_type")?.value || "audio+image2video"); + const generationConfig = GENERATION_TYPE_CONFIGS[generationType] || GENERATION_TYPE_CONFIGS["audio+image2video"]; + const backendWidget = findWidget(node, "backend_mode"); + if (String(backendWidget?.value || "") === "legacy backend") { + backendWidget.value = "legacy_single"; + } + sanitizeRenderCombo(node, "backend_mode", generationConfig.backend_mode || "auto", RENDER_BACKEND_MODE_VALUES); + sanitizeRenderCombo(node, "stitch_preset", "custom", RENDER_STITCH_PRESET_VALUES); + sanitizeRenderCombo(node, "overlap_side", "source", RENDER_OVERLAP_SIDE_VALUES); + sanitizeRenderCombo(node, "overlap_mode", "cut", RENDER_OVERLAP_MODE_VALUES); + sanitizeRenderCombo(node, "start_frames_rule", "none", RENDER_START_FRAMES_RULE_VALUES); + sanitizeRenderCombo(node, "color_match_mode", "none", RENDER_COLOR_MATCH_MODE_VALUES); + sanitizeRenderCombo(node, "audio_context_mode", "left_context_only", RENDER_AUDIO_CONTEXT_MODE_VALUES); + sanitizeRenderNumber(node, "generated_media_fps", DEFAULT_GENERATED_FPS); + sanitizeRenderNumber(node, "width", DEFAULT_VIDEO_WIDTH); + sanitizeRenderNumber(node, "height", generationConfig.height || DEFAULT_VIDEO_HEIGHT); + if (generationType === REFERENCE_AUDIO_IMG2VID_TYPE) { + const widthWidget = findWidget(node, "width"); + const heightWidget = findWidget(node, "height"); + // Code by Carmine Cristallo Scalzi AI research (IAMCCS) + // patreon.com/IAMCCS + // Old SuperNode saves carried the generic 1280x736 LTX value. The + // working A+I2V reference workflow feeds ImageResizeKJv2 with 1280x720. + if (Number(widthWidget?.value) === REFERENCE_AUDIO_IMG2VID_WIDTH && Number(heightWidget?.value) === DEFAULT_VIDEO_HEIGHT) { + heightWidget.value = REFERENCE_AUDIO_IMG2VID_HEIGHT; + } + if (Number(widthWidget?.value) === STALE_AUDIO_IMG2VID_WIDTH && Number(heightWidget?.value) === STALE_AUDIO_IMG2VID_HEIGHT) { + widthWidget.value = REFERENCE_AUDIO_IMG2VID_WIDTH; + heightWidget.value = REFERENCE_AUDIO_IMG2VID_HEIGHT; + } + } + sanitizeRenderNumber(node, "steps", 8); + sanitizeRenderNumber(node, "cfg", 1.0); + sanitizeRenderNumber(node, "seed", 0); + sanitizeRenderNumber(node, "max_shift", 2.05); + sanitizeRenderNumber(node, "base_shift", 0.95); + sanitizeRenderNumber(node, "sigma_terminal", 0.1); + sanitizeRenderNumber(node, "image_strength", 0.9); + sanitizeRenderNumber(node, "image_compression", 35); + sanitizeRenderNumber(node, "color_match_strength", 0.25); + sanitizeRenderNumber(node, "audio_left_context_s", 0.5); + sanitizeRenderNumber(node, "audio_right_context_s", 0.0); + sanitizeRenderNumber(node, "anchor_refresh_interval", 2); + sanitizeRenderNumber(node, "anchor_image_strength", 0.0); + sanitizeRenderNumber(node, "anti_drift_strength", 0.0); + sanitizeRenderNumber(node, "identity_persistence_strength", 0.0); + sanitizeRenderNumber(node, "second_stage_reinject_strength", 0.0); + sanitizeRenderNumber(node, "second_stage_cfg", 1.0); + sanitizeRenderNumber(node, "motion_intensity", 1.0); + sanitizeBooleanWidget(node, "show_manual_sigmas", false); + sanitizeBooleanWidget(node, "debug_verbose", false); + const normalizedBackendMode = normalizeRenderBackendValue(findWidget(node, "backend_mode")?.value); + if (LEGACY_RENDER_BACKEND_DEFAULTS[normalizedBackendMode]) { + const manualWidget = findWidget(node, "manual_sigmas"); + if (manualWidget && !String(manualWidget.value || "").trim()) { + manualWidget.value = REF_MAIN_SIGMAS; + } + } - syncRenderGenerationType(node); applyRenderGeneratedDurationVisibility(node); + applyRenderManualSigmasVisibility(node); +} + +function sanitizeVaeWidgetValues(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + return; + } + sanitizeRenderCombo(node, "ui_preset", "custom", VAE_UI_PRESET_VALUES); + sanitizeRenderCombo(node, "decode_mode", "normal_tiled_iamccs", VAE_DECODE_MODE_VALUES); + sanitizeRenderNumber(node, "frame_rate", DEFAULT_VAE_FRAME_RATE); + sanitizeRenderNumber(node, "jpg_quality", 95); + sanitizeRenderNumber(node, "tiled_tile_size", 512); + sanitizeRenderNumber(node, "tiled_overlap", 64); + sanitizeRenderNumber(node, "tiled_temporal_size", 256); + sanitizeRenderNumber(node, "tiled_temporal_overlap", 32); + sanitizeBooleanWidget(node, "cleanup_before_decode", false); + sanitizeRenderNumber(node, "crf", 19); + sanitizeBooleanWidget(node, "trim_to_audio", true); + sanitizeBooleanWidget(node, "save_metadata", false); + sanitizeBooleanWidget(node, "vram_flush", false); + sanitizeBooleanWidget(node, "debug_verbose", false); +} + +function sanitizePlannerWidgetValues(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + return; + } + sanitizeRenderCombo(node, "audio_img2vid_backend", PLANNER_A2I_BACKEND_MODERN, PLANNER_AUDIO_IMG2VID_BACKEND_VALUES); + sanitizeRenderCombo(node, "audio_img2vid_mode", PLANNER_A2I_MODE_SINGLE, PLANNER_AUDIO_IMG2VID_MODE_VALUES); + setWidgetValue(node, "audio_img2vid_backend", plannerA2IBackendDisplay(findWidget(node, "audio_img2vid_backend")?.value), { notify: false }); + setWidgetValue(node, "audio_img2vid_mode", plannerA2IModeDisplay(findWidget(node, "audio_img2vid_mode")?.value), { notify: false }); + sanitizeRenderCombo(node, "route_mode", "choose segment count (audio / segments)", PLANNER_ROUTE_MODE_VALUES); + sanitizeRenderCombo(node, "planning_mode", "manual_segment_seconds", PLANNER_MODE_VALUES); + sanitizeRenderCombo(node, "segment_preset", "10sec", PLANNER_SEGMENT_PRESET_VALUES); + sanitizeRenderCombo(node, "ltx_round_mode", "up", PLANNER_ROUND_MODE_VALUES); + sanitizeRenderCombo(node, "audio_preprocess_mode", "melband_vocals_duration_math", PLANNER_AUDIO_MODE_VALUES); + sanitizeRenderNumber(node, "fps", REFERENCE_AUDIO_IMG2VID_FPS); + sanitizeRenderNumber(node, "segment_seconds", 10.0); + sanitizeRenderNumber(node, "overlap_frames", 9); + sanitizeRenderNumber(node, "segment_count", 2); + sanitizeRenderNumber(node, "single_duration_seconds", 10.0); + sanitizeBooleanWidget(node, "debug_verbose", false); +} + +function normalizePlannerA2IBackend(value) { + const text = String(value || "").trim().toLowerCase(); + return text.includes("legacy") ? "legacy" : "modern"; +} + +function plannerA2IBackendDisplay(value) { + return normalizePlannerA2IBackend(value) === "legacy" ? PLANNER_A2I_BACKEND_LEGACY : PLANNER_A2I_BACKEND_MODERN; +} + +function normalizePlannerA2IMode(value) { + const text = String(value || "").trim().toLowerCase(); + if (text.includes("single") || text === "1" || text === "one") { + return "single"; + } + if (text === "2" || text === "2_segments" || text.includes("2 segment")) { + return "2_segments"; + } + if (text === "3" || text === "3_segments" || text.includes("3 segment")) { + return "3_segments"; + } + if (text.includes("loop") || text.includes("4+")) { + return "loop"; + } + return "single"; +} + +function plannerA2IModeDisplay(value) { + const mode = normalizePlannerA2IMode(value); + if (mode === "2_segments") { + return PLANNER_A2I_MODE_2_SEGMENTS; + } + if (mode === "3_segments") { + return PLANNER_A2I_MODE_3_SEGMENTS; + } + if (mode === "loop") { + return PLANNER_A2I_MODE_LOOP; + } + return PLANNER_A2I_MODE_SINGLE; +} + +function resolvePlannerA2IRenderBackend(plannerNode) { + const family = normalizePlannerA2IBackend(findWidget(plannerNode, "audio_img2vid_backend")?.value); + const mode = normalizePlannerA2IMode(findWidget(plannerNode, "audio_img2vid_mode")?.value); + if (family === "legacy") { + if (mode === "single") { + return "legacy_single"; + } + if (mode === "2_segments") { + return "legacy_two_segments"; + } + return "legacy_loop"; + } + if (mode === "single") { + return "single_best"; + } + if (mode === "2_segments") { + return "two_segments_normal_vram"; + } + if (mode === "3_segments") { + return "three_segments_normal_vram"; + } + return "loop_normal_vram"; +} + +function syncPlannerA2IModeToInternalWidgets(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + return; + } + const mode = normalizePlannerA2IMode(findWidget(node, "audio_img2vid_mode")?.value); + if (mode === "single") { + setWidgetValue(node, "route_mode", "single generation (duration only)", { notify: false }); + setWidgetValue(node, "segment_count", 1, { notify: false }); + } else { + setWidgetValue(node, "route_mode", "choose segment count (audio / segments)", { notify: false }); + if (mode === "2_segments") { + setWidgetValue(node, "segment_count", 2, { notify: false }); + } else if (mode === "3_segments") { + setWidgetValue(node, "segment_count", 3, { notify: false }); + } else { + const count = Math.max(4, Math.round(readPlannerNumber(node, "segment_count", 4))); + setWidgetValue(node, "segment_count", count, { notify: false }); + } + } +} + +function plannerTargetsReferenceAudioImg2Vid(plannerNode) { + if (plannerNode?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + return false; + } + const visited = new Set(); + const queue = [...getLinxTargets(plannerNode)]; + let sawRender = false; + while (queue.length > 0) { + const node = queue.shift(); + if (!node || visited.has(node.id)) { + continue; + } + visited.add(node.id); + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + sawRender = true; + if (renderUsesReferenceAudioImg2Vid(node)) { + return true; + } + continue; + } + queue.push(...getLinxTargets(node)); + } + return !sawRender; } function rehydrateSerializedWidgets(node, serializedValues) { - if (!Array.isArray(serializedValues) || !node.widgets?.length) { + if (!Array.isArray(serializedValues) || !hasWidgetList(node) || !node.widgets.length) { + return; + } + + const cleanValues = sanitizeSerializedValues(serializedValues); + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && RENDER_GENERATION_TYPE_VALUES.has(String(cleanValues[0] || ""))) { + applyValuesByNameToWidgets(node, readRenderSerializedValues(cleanValues)); + syncReferenceAudioImg2VidTimingWhenReady(node, {}, "rehydrate_generation_type", { defer: true }); + return; + } + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && ["img2vid", "t2v"].includes(String(cleanValues[0] || ""))) { + applyValuesByNameToWidgets(node, readRenderSerializedValues(cleanValues)); + syncReferenceAudioImg2VidTimingWhenReady(node, {}, "rehydrate_legacy_mode", { defer: true }); + return; + } + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + applyValuesByNameToWidgets(node, readVaeSerializedValues(cleanValues)); + sanitizeVaeWidgetValues(node); + return; + } + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + applyValuesByNameToWidgets(node, readPlannerSerializedValues(cleanValues)); + sanitizePlannerWidgetValues(node); return; } const widgets = getSerializableWidgets(node); - const cleanValues = sanitizeSerializedValues(serializedValues); - if (node.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && ["img2vid", "t2v"].includes(String(cleanValues[0] || ""))) { - const valuesByName = {}; - RENDER_LEGACY_WIDGET_ORDER.forEach((widgetName, index) => { - if (index < cleanValues.length) { - valuesByName[widgetName] = cleanValues[index]; - } - }); - valuesByName.generation_type = RENDER_GENERATION_TYPE_VALUES.has(String(valuesByName.generation_type || "")) - ? valuesByName.generation_type - : inferRenderGenerationType(valuesByName); - valuesByName.ui_preset = RENDER_UI_PRESET_VALUES.has(String(valuesByName.ui_preset || "")) ? valuesByName.ui_preset : "custom"; - valuesByName.generated_media_duration_seconds = Number.isFinite(Number(valuesByName.generated_media_duration_seconds)) - ? Number(valuesByName.generated_media_duration_seconds) - : 10.0; - const generationConfig = GENERATION_TYPE_CONFIGS[String(valuesByName.generation_type)] || GENERATION_TYPE_CONFIGS["aud+img2video_infinite"]; - Object.assign(valuesByName, generationConfig); - for (const widget of widgets) { - if (Object.prototype.hasOwnProperty.call(valuesByName, widget.name)) { - widget.value = valuesByName[widget.name]; - } - } - return; - } - const count = Math.min(widgets.length, cleanValues.length); for (let index = 0; index < count; index += 1) { const widget = widgets[index]; @@ -714,10 +2399,13 @@ function rehydrateSerializedWidgets(node, serializedValues) { } function addSectionButton(node, group, index) { + if (!hasWidgetList(node) || !group) { + return null; + } // Duplicate guard: if a button for this section key is already in the widget list, // return it immediately. This prevents double-insertion if onNodeCreated or // enhanceNodeLayout is called more than once on the same node instance. - const existing = node.widgets?.find((w) => w._iamccsSectionKey === group.key); + const existing = node.widgets.find((w) => w._iamccsSectionKey === group.key); if (existing) { installSectionButtonDraw(existing); return existing; @@ -726,7 +2414,7 @@ function addSectionButton(node, group, index) { node.properties = node.properties || {}; const propKey = `iamccs_section_${group.key}`; if (node.properties[propKey] === undefined) { - node.properties[propKey] = true; + node.properties[propKey] = !DEFAULT_COLLAPSED_SECTION_KEYS.has(group.key); } const sectionButton = node.addWidget("button", "", "", () => { @@ -752,6 +2440,10 @@ function addSectionButton(node, group, index) { } function applyGroupVisibility(node, group, propKey, button) { + if (!node || !group || !button) { + return; + } + node.properties = node.properties || {}; const isExpanded = !!node.properties[propKey]; const caption = sectionButtonCaption(group, isExpanded); button.name = caption; @@ -765,35 +2457,104 @@ function applyGroupVisibility(node, group, propKey, button) { border_color: style.border, color: SECTION_BUTTON_TEXT, }; + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner" && ["route", "audio"].includes(group.key)) { + applyPlannerModeVisibility(node, { skipPresetSync: true }); + markCanvasDirty(); + return; + } for (const widgetName of group.widgets) { setWidgetVisibility(findWidget(node, widgetName), isExpanded); } - if (node.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && group.key === "generation") { + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && group.key === "generation") { applyRenderGeneratedDurationVisibility(node); + applyRenderInternalWidgetVisibility(node); } - if (node.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && group.key === "stage2") { + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && group.key === "sampling") { + applyRenderManualSigmasVisibility(node); + } + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && group.key === "stage2") { applyRenderSecondStageVisibility(node); } + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && (group.key === "anchor" || group.key === "latent_refresh")) { + applyRenderAnchorVisibility(node); + } + if (node?.comfyClass === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE" && ["mode", "output", "decode"].includes(group.key)) { + applyVaeDecodeModeVisibility(node); + } fitNodeToWidgets(node); - app.graph.setDirtyCanvas(true, true); + markCanvasDirty(); } -function applyPlannerModeVisibility(node) { - if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { +function applyPlannerModeVisibility(node, options = {}) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { return; } - const planningMode = String(findWidget(node, "planning_mode")?.value || "manual_segment_seconds"); - const planningExpanded = !!node.properties?.iamccs_section_planning; - const showSegmentPreset = planningExpanded && planningMode === "explicit_preset_seconds"; - setWidgetVisibility(findWidget(node, "segment_preset"), showSegmentPreset); - syncExecPlannerExplicitPreset(node); + sanitizePlannerWidgetValues(node); + const showA2IControls = plannerTargetsReferenceAudioImg2Vid(node); + if (showA2IControls) { + syncPlannerA2IModeToInternalWidgets(node); + } + const routeMode = String(findWidget(node, "route_mode")?.value || "choose segment count (audio / segments)"); + const mode = showA2IControls + ? normalizePlannerA2IMode(findWidget(node, "audio_img2vid_mode")?.value) + : (routeMode.includes("single") ? "single" : "loop"); + const routeExpanded = node.properties?.iamccs_section_route !== false; + const audioExpanded = node.properties?.iamccs_section_audio !== false; + const isSingle = mode === "single"; + const isLoop = mode === "loop"; + const audioMode = String(findWidget(node, "audio_preprocess_mode")?.value || "melband_vocals_duration_math"); + + setWidgetLabel(node, "audio_img2vid_backend", "Pipeline Family"); + setWidgetLabel(node, "audio_img2vid_mode", "Pipeline Shape"); + setWidgetLabel(node, "route_mode", "Internal Length Rule"); + setWidgetLabel(node, "single_duration_seconds", "Duration (s)"); + setWidgetLabel(node, "segment_count", "Segments"); + setWidgetLabel(node, "segment_seconds", "Seconds / Segment"); + setWidgetLabel(node, "fps", "FPS"); + setWidgetLabel(node, "audio_preprocess_mode", "Audio Mode"); + setWidgetLabel(node, "melband_model_name", "MelBand Model"); + setWidgetLabel(node, "overlap_frames", "Overlap Frames"); + setWidgetLabel(node, "ltx_round_mode", "Frame Rounding"); + setWidgetLabel(node, "debug_verbose", "Verbose Debug"); + + setWidgetVisibility(findWidget(node, "audio_img2vid_backend"), routeExpanded && showA2IControls); + setWidgetVisibility(findWidget(node, "audio_img2vid_mode"), routeExpanded && showA2IControls); + setWidgetVisibility(findWidget(node, "route_mode"), false); + setWidgetVisibility(findWidget(node, "fps"), routeExpanded); + setWidgetVisibility(findWidget(node, "single_duration_seconds"), routeExpanded && isSingle); + setWidgetVisibility(findWidget(node, "segment_count"), routeExpanded && isLoop); + setWidgetVisibility(findWidget(node, "segment_seconds"), routeExpanded && !isSingle); + setWidgetVisibility(findWidget(node, "planning_mode"), false); + setWidgetVisibility(findWidget(node, "segment_preset"), false); + setWidgetVisibility(findWidget(node, "overlap_frames"), routeExpanded && !isSingle); + setWidgetVisibility(findWidget(node, "ltx_round_mode"), routeExpanded); + setWidgetVisibility(findWidget(node, "audio_concat_payload"), false); + setWidgetVisibility(findWidget(node, "debug_verbose"), routeExpanded); + sanitizeBooleanWidget(node, "debug_verbose", false); + setWidgetVisibility(findWidget(node, "audio_preprocess_mode"), audioExpanded); + setWidgetVisibility(findWidget(node, "melband_model_name"), audioExpanded && audioMode !== "raw_audio_only"); + if (!options.skipPresetSync) { + syncExecPlannerExplicitPreset(node); + } fitNodeToWidgets(node); } +function removeStaleSectionButtons(node, nodeName) { + if (!hasWidgetList(node)) { + return; + } + const validKeys = new Set((NODE_GROUPS[nodeName] || []).map((group) => group.key)); + node.widgets = node.widgets.filter((widget) => !widget?._iamccsSectionKey || validKeys.has(widget._iamccsSectionKey)); +} + function refreshNodeLayoutState(node, nodeName) { + if (!hasWidgetList(node)) { + return; + } + removeStaleSectionButtons(node, nodeName); const groups = NODE_GROUPS[nodeName] || []; for (const group of groups) { - const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key); + const button = node.widgets.find((widget) => widget?._iamccsSectionKey === group.key); if (!button) { continue; } @@ -811,11 +2572,14 @@ function refreshNodeLayoutState(node, nodeName) { } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + sanitizeVaeWidgetValues(node); applyVaeDecodeModeVisibility(node); } else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + sanitizePlannerWidgetValues(node); applyPlannerModeVisibility(node); } else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { sanitizeRenderWidgetValues(node); + applyRenderAnchorVisibility(node); applyRenderSecondStageVisibility(node); applyRenderInternalWidgetVisibility(node); } else { @@ -824,7 +2588,7 @@ function refreshNodeLayoutState(node, nodeName) { } function addPlannerChip(node) { - if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) { + if (!hasWidgetList(node) || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) { return; } const chipWidget = { @@ -863,7 +2627,7 @@ function addPlannerChip(node) { } function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) { - if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) { + if (!hasWidgetList(node) || node.widgets.some((widget) => widget?.name === widgetName)) { return; } const statusWidget = { @@ -901,8 +2665,20 @@ function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) { node.widgets.push(statusWidget); } +function applyPlannerOutputLabels(node) { + if (node?.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner" || !Array.isArray(node.outputs)) { + return; + } + const labels = ["plan (direct)", "duration", "frames", "segments", "first frames", "left ctx", "summary", "linx", "report"]; + node.outputs.forEach((output, index) => { + if (output && labels[index]) { + output.name = labels[index]; + } + }); +} + function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) { - if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) { + if (!hasWidgetList(node) || node.widgets.some((widget) => widget?.name === widgetName)) { return; } const statusWidget = { @@ -945,18 +2721,27 @@ function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, tex function enhanceNodeLayout(node, nodeName) { const groups = NODE_GROUPS[nodeName] || []; - if (!groups.length || !node.widgets?.length) { + if (!groups.length || !hasWidgetList(node) || !node.widgets.length) { return; } + removeStaleSectionButtons(node, nodeName); + + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + normalizePlannerWidgetOrder(node); + } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { normalizeRenderWidgetOrder(node); } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + normalizeVaeWidgetOrder(node); + } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + applyPlannerOutputLabels(node); + node.widgets = node.widgets.filter((widget) => widget?.name !== "planner_details_preview"); addPlannerChip(node); addMultiLineStatusBox(node, "iamccsPlannerLivePreview", "planner_live_preview", "#1d2f38", "#67a7bb", "#e5f3f7"); - addMultiLineStatusBox(node, "iamccsPlannerDetails", "planner_details_preview", "#24362d", "#72ab8e", "#e4f2e8"); } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { addStatusBox(node, "iamccsRenderStatus", "render_status_preview", "#1e2f3f", "#6f93ba", "#e4edf7"); @@ -965,6 +2750,7 @@ function enhanceNodeLayout(node, nodeName) { addStatusBox(node, "iamccsSecondStageStatus", "second_stage_status_preview", "#30243d", "#9f77bf", "#f1e7f8"); } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + sanitizeVaeWidgetValues(node); applyVaeDecodeModeVisibility(node); } @@ -981,7 +2767,7 @@ function enhanceNodeLayout(node, nodeName) { } app.registerExtension({ - name: "IAMCCS.SuperNodesExecUI", + name: `IAMCCS.SuperNodesExecUI.${IAMCCS_SUPERNODES_EXEC_UI_VERSION}`, async beforeRegisterNodeDef(nodeType, nodeData) { const nodeName = nodeData?.name; @@ -991,187 +2777,320 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { - const result = onNodeCreated?.apply(this, arguments); - this.properties = this.properties || {}; - // MUST run synchronously, BEFORE configure() assigns widgets_values by index. - // Section buttons inserted here occupy their index slots so that the null - // placeholders in widgets_values land on buttons, not on real widgets. - // (Using setTimeout here was the root cause of NaN on load / after undo.) - enhanceNodeLayout(this, nodeName); - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { - sanitizeRenderWidgetValues(this); - applyRenderInternalWidgetVisibility(this); - } - const config = PRESET_CONFIGS[nodeName]; - const presetWidget = config ? findWidget(this, config.presetWidget) : null; - if (presetWidget) { - const originalCallback = presetWidget.callback; - presetWidget.callback = (...args) => { - originalCallback?.apply(presetWidget, args); - applyPresetConfig(this, nodeName); - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { - syncDownstreamVaeDecodeModes(this); - } - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { - applyVaeDecodeModeVisibility(this); - } - app.graph.setDirtyCanvas(true, true); - }; - } - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { - const vaeModeWidget = findWidget(this, "vae_mode"); - if (vaeModeWidget) { - const originalCallback = vaeModeWidget.callback; - vaeModeWidget.callback = (...args) => { - originalCallback?.apply(vaeModeWidget, args); - syncDownstreamVaeDecodeModes(this); - app.graph.setDirtyCanvas(true, true); - }; - syncDownstreamVaeDecodeModes(this); + const result = safeCall(`${nodeName}.onNodeCreated.original`, () => onNodeCreated?.apply(this, arguments)); + safeCall(`${nodeName}.onNodeCreated.iamccs`, () => { + this.properties = this.properties || {}; + // MUST run synchronously, BEFORE configure() assigns widgets_values by index. + // Section buttons inserted here occupy their index slots so that the null + // placeholders in widgets_values land on buttons, not on real widgets. + // (Using setTimeout here was the root cause of NaN on load / after undo.) + enhanceNodeLayout(this, nodeName); + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + // Do not apply generation defaults here: workflow reload has + // not restored widgets_values yet. Defaults are applied only + // from the generation_type callback/user change path. + sanitizeRenderWidgetValues(this); + applyRenderInternalWidgetVisibility(this); + syncRenderLinxWhenReady(this, "node_created_render", { defer: true }); } - for (const widgetName of ["generation_type", "generated_media_duration_seconds", "second_stage_mode", "continuity_anchor_mode", "anti_drift_mode"]) { - const widget = findWidget(this, widgetName); - if (!widget) { - continue; + const config = PRESET_CONFIGS[nodeName]; + const presetWidget = config ? findWidget(this, config.presetWidget) : null; + if (presetWidget) { + const originalCallback = presetWidget.callback; + presetWidget.callback = (...args) => safeCall(`${nodeName}.${presetWidget.name}.callback`, () => { + coerceWidgetCallbackValue(presetWidget, args); + originalCallback?.apply(presetWidget, args); + applyPresetConfig(this, nodeName); + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + syncRenderLinxWhenReady(this, "preset_widget"); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + applyVaeDecodeModeVisibility(this); + syncVaeUpstreamWhenReady(this, "preset_widget"); + } + markCanvasDirty(); + }); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + const vaeModeWidget = findWidget(this, "vae_mode"); + if (vaeModeWidget) { + const originalCallback = vaeModeWidget.callback; + vaeModeWidget.callback = (...args) => safeCall(`${nodeName}.vae_mode.callback`, () => { + coerceWidgetCallbackValue(vaeModeWidget, args); + originalCallback?.apply(vaeModeWidget, args); + syncRenderLinxWhenReady(this, "vae_mode_widget"); + markCanvasDirty(); + }); + syncRenderLinxWhenReady(this, "vae_mode_install", { defer: true }); } - const originalCallback = widget.callback; - widget.callback = (...args) => { - originalCallback?.apply(widget, args); - sanitizeRenderWidgetValues(this); - applyRenderAnchorVisibility(this); - applyRenderSecondStageVisibility(this); - applyRenderInternalWidgetVisibility(this); - app.graph.setDirtyCanvas(true, true); - }; + const generationTypeWidget = findWidget(this, "generation_type"); + if (generationTypeWidget) { + const originalCallback = generationTypeWidget.callback; + generationTypeWidget.callback = (...args) => safeCall(`${nodeName}.generation_type.callback`, () => { + coerceWidgetCallbackValue(generationTypeWidget, args); + originalCallback?.apply(generationTypeWidget, args); + applyRenderGenerationTypeChange(this); + }); + } + for (const widgetName of ["backend_mode", "generated_media_duration_seconds", "generated_media_fps", "show_manual_sigmas", "second_stage_mode", "continuity_anchor_mode", "anti_drift_mode"]) { + const widget = findWidget(this, widgetName); + if (!widget) { + continue; + } + const originalCallback = widget.callback; + widget.callback = (...args) => safeCall(`${nodeName}.${widgetName}.callback`, () => { + coerceWidgetCallbackValue(widget, args); + originalCallback?.apply(widget, args); + if (widgetName === "backend_mode") { + applyRenderBackendModeDefaults(this, widget.value); + } + if (widgetName === "continuity_anchor_mode") { + applyRenderAnchorModeDefaults(this); + } + sanitizeRenderWidgetValues(this); + applyRenderAnchorVisibility(this); + applyRenderSecondStageVisibility(this); + applyRenderInternalWidgetVisibility(this); + syncRenderLinxWhenReady(this, `${widgetName}_widget`); + markCanvasDirty(); + }); + } + sanitizeRenderWidgetValues(this); + applyRenderSecondStageVisibility(this); + applyRenderInternalWidgetVisibility(this); + syncRenderLinxWhenReady(this, "node_created_render_final", { defer: true }); } - sanitizeRenderWidgetValues(this); - applyRenderSecondStageVisibility(this); - applyRenderInternalWidgetVisibility(this); - } - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { - installExecPlannerExplicitPresetSync(this); - applyPlannerModeVisibility(this); - } - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { - const decodeModeWidget = findWidget(this, "decode_mode"); - if (decodeModeWidget) { - const originalCallback = decodeModeWidget.callback; - decodeModeWidget.callback = (...args) => { - originalCallback?.apply(decodeModeWidget, args); - applyVaeDecodeModeVisibility(this); - app.graph.setDirtyCanvas(true, true); - }; + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + installExecPlannerExplicitPresetSync(this); + applyPlannerModeVisibility(this); } - } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + sanitizeVaeWidgetValues(this); + const decodeModeWidget = findWidget(this, "decode_mode"); + if (decodeModeWidget) { + const originalCallback = decodeModeWidget.callback; + decodeModeWidget.callback = (...args) => safeCall(`${nodeName}.decode_mode.callback`, () => { + coerceWidgetCallbackValue(decodeModeWidget, args); + originalCallback?.apply(decodeModeWidget, args); + applyVaeDecodeModeDefaults(this, decodeModeWidget.value); + sanitizeVaeWidgetValues(this); + applyVaeDecodeModeVisibility(this); + syncVaeUpstreamWhenReady(this, "decode_mode_widget"); + markCanvasDirty(); + }); + } + const frameRateWidget = findWidget(this, "frame_rate"); + if (frameRateWidget) { + const originalCallback = frameRateWidget.callback; + frameRateWidget.callback = (...args) => safeCall(`${nodeName}.frame_rate.callback`, () => { + coerceWidgetCallbackValue(frameRateWidget, args); + originalCallback?.apply(frameRateWidget, args); + sanitizeVaeWidgetValues(this); + syncVaeUpstreamWhenReady(this, "frame_rate_widget"); + markCanvasDirty(); + }); + } + applyVaeDecodeModeVisibility(this); + syncVaeUpstreamWhenReady(this, "node_created_vae", { defer: true }); + } + }); return result; }; const onConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function (info) { - const result = onConfigure?.apply(this, arguments); - this.properties = this.properties || {}; - enhanceNodeLayout(this, nodeName); - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { - sanitizeRenderWidgetValues(this); - applyRenderInternalWidgetVisibility(this); + let result; + this.__iamccsConfiguring = true; + try { + safeCall(`${nodeName}.onConfigure.preNormalize`, () => { + this.properties = this.properties || {}; + enhanceNodeLayout(this, nodeName); + normalizeConfigureWidgetValues(this, nodeName, info); + }); + result = safeCall(`${nodeName}.onConfigure.original`, () => onConfigure?.apply(this, arguments)); + safeCall(`${nodeName}.onConfigure.iamccs`, () => { + this.properties = this.properties || {}; + enhanceNodeLayout(this, nodeName); + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + sanitizeRenderWidgetValues(this); + applyRenderInternalWidgetVisibility(this); + } + rehydrateSerializedWidgets(this, info?.widgets_values); + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + sanitizeRenderWidgetValues(this); + markRenderGenerationTypeDefaultsApplied(this); + applyRenderInternalWidgetVisibility(this); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + sanitizeVaeWidgetValues(this); + applyVaeDecodeModeVisibility(this); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { + installExecPlannerExplicitPresetSync(this); + } + refreshNodeLayoutState(this, nodeName); + }); + } finally { + this.__iamccsConfiguring = false; } - rehydrateSerializedWidgets(this, info?.widgets_values); - if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { - sanitizeRenderWidgetValues(this); - applyRenderInternalWidgetVisibility(this); + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + syncRenderLinxWhenReady(this, "on_configure_render", { defer: true }); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + syncVaeUpstreamWhenReady(this, "on_configure_vae", { defer: true }); } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { - installExecPlannerExplicitPresetSync(this); + syncPlannerFpsDownstreamWhenReady(this, "on_configure_planner", { defer: true }); } - refreshNodeLayoutState(this, nodeName); + return result; + }; + + const onWidgetChanged = nodeType.prototype.onWidgetChanged; + nodeType.prototype.onWidgetChanged = function (name) { + const result = safeCall(`${nodeName}.onWidgetChanged.original`, () => onWidgetChanged?.apply(this, arguments)); + safeCall(`${nodeName}.onWidgetChanged.iamccs`, () => { + const changedName = typeof name === "string" ? name : name?.name; + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && changedName === "generation_type") { + applyRenderGenerationTypeChange(this); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && changedName === "backend_mode") { + applyRenderBackendModeDefaults(this); + sanitizeRenderWidgetValues(this); + applyRenderInternalWidgetVisibility(this); + syncRenderLinxWhenReady(this, "widget_changed_backend_mode"); + markCanvasDirty(); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE" && changedName === "decode_mode") { + applyVaeDecodeModeDefaults(this); + sanitizeVaeWidgetValues(this); + applyVaeDecodeModeVisibility(this); + syncVaeUpstreamWhenReady(this, "widget_changed_decode_mode"); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE" && changedName === "frame_rate") { + sanitizeVaeWidgetValues(this); + syncVaeUpstreamWhenReady(this, "widget_changed_frame_rate"); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && changedName === "generated_media_fps") { + sanitizeRenderWidgetValues(this); + syncRenderLinxWhenReady(this, "widget_changed_generated_media_fps"); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render" && changedName === "show_manual_sigmas") { + sanitizeRenderWidgetValues(this); + applyRenderManualSigmasVisibility(this); + markCanvasDirty(); + } + }); + return result; + }; + + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function () { + const result = safeCall(`${nodeName}.onConnectionsChange.original`, () => onConnectionsChange?.apply(this, arguments)); + safeCall(`${nodeName}.onConnectionsChange.iamccs`, () => { + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { + sanitizeRenderWidgetValues(this); + syncRenderLinxWhenReady(this, "connections_change_render", { defer: true }); + } + if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") { + sanitizeVaeWidgetValues(this); + applyVaeDecodeModeVisibility(this); + syncVaeUpstreamWhenReady(this, "connections_change_vae", { defer: true }); + } + }); return result; }; if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") { const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); - this.properties = this.properties || {}; - if (Array.isArray(message?.planner_chip) && message.planner_chip.length > 0) { - this.properties.iamccsPlannerChip = String(message.planner_chip[0] || ""); - } - const details = []; - const duration = Array.isArray(message?.planned_duration_seconds) && message.planned_duration_seconds.length > 0 - ? message.planned_duration_seconds[0] - : Array.isArray(message?.duration_seconds) && message.duration_seconds.length > 0 - ? message.duration_seconds[0] - : null; - const totalFrames = Array.isArray(message?.planned_total_frames) && message.planned_total_frames.length > 0 - ? message.planned_total_frames[0] - : Array.isArray(message?.total_frames) && message.total_frames.length > 0 - ? message.total_frames[0] - : null; - const segments = Array.isArray(message?.planned_segment_count) && message.planned_segment_count.length > 0 - ? message.planned_segment_count[0] - : Array.isArray(message?.segment_count) && message.segment_count.length > 0 - ? message.segment_count[0] - : null; - if (duration !== null) { - this.properties.iamccsPlannerDuration = Number(duration || 0); - } - if (totalFrames !== null) { - this.properties.iamccsPlannerTotalFrames = Number(totalFrames || 0); - } - if (segments !== null) { - this.properties.iamccsPlannerSegmentCount = Number(segments || 0); - } - if (duration !== null) { - details.push(`duration ${Number(duration || 0).toFixed(2)}s`); - } - if (totalFrames !== null) { - details.push(`total ${totalFrames}f`); - } - if (segments !== null) { - details.push(`segments ${segments}`); - } - if (Array.isArray(message?.recommended_overlap_frames) && message.recommended_overlap_frames.length > 0) { - details.push(`overlap ${message.recommended_overlap_frames[0]}f`); - } - if (Array.isArray(message?.recommended_audio_left_context_s) && message.recommended_audio_left_context_s.length > 0) { - details.push(`left ctx ${Number(message.recommended_audio_left_context_s[0] || 0).toFixed(2)}s`); - } - const plannerReport = Array.isArray(message?.planning_report) && message.planning_report.length > 0 - ? message.planning_report[0] - : Array.isArray(message?.report) && message.report.length > 0 - ? message.report[0] - : null; - if (plannerReport !== null) { - details.push(String(plannerReport || "")); - } - updateExecPlannerLivePreview(this); - if (details.length > 0) { - this.properties.iamccsPlannerDetails = details.join("\n"); - app.graph.setDirtyCanvas(true, true); - } + safeCall(`${nodeName}.onExecuted.original`, () => onExecuted?.apply(this, arguments)); + safeCall(`${nodeName}.onExecuted.iamccs`, () => { + this.properties = this.properties || {}; + if (Array.isArray(message?.planner_chip) && message.planner_chip.length > 0) { + this.properties.iamccsPlannerChip = String(message.planner_chip[0] || ""); + } + const details = []; + const duration = Array.isArray(message?.planned_duration_seconds) && message.planned_duration_seconds.length > 0 + ? message.planned_duration_seconds[0] + : Array.isArray(message?.duration_seconds) && message.duration_seconds.length > 0 + ? message.duration_seconds[0] + : null; + const totalFrames = Array.isArray(message?.planned_total_frames) && message.planned_total_frames.length > 0 + ? message.planned_total_frames[0] + : Array.isArray(message?.total_frames) && message.total_frames.length > 0 + ? message.total_frames[0] + : null; + const segments = Array.isArray(message?.planned_segment_count) && message.planned_segment_count.length > 0 + ? message.planned_segment_count[0] + : Array.isArray(message?.segment_count) && message.segment_count.length > 0 + ? message.segment_count[0] + : null; + if (duration !== null) { + this.properties.iamccsPlannerDuration = Number(duration || 0); + } + if (totalFrames !== null) { + this.properties.iamccsPlannerTotalFrames = Number(totalFrames || 0); + } + if (segments !== null) { + this.properties.iamccsPlannerSegmentCount = Number(segments || 0); + } + if (duration !== null) { + details.push(`duration ${Number(duration || 0).toFixed(2)}s`); + } + if (totalFrames !== null) { + details.push(`total ${totalFrames}f`); + } + if (segments !== null) { + details.push(`segments ${segments}`); + } + if (Array.isArray(message?.recommended_overlap_frames) && message.recommended_overlap_frames.length > 0) { + details.push(`overlap ${message.recommended_overlap_frames[0]}f`); + } + if (Array.isArray(message?.recommended_audio_left_context_s) && message.recommended_audio_left_context_s.length > 0) { + details.push(`left ctx ${Number(message.recommended_audio_left_context_s[0] || 0).toFixed(2)}s`); + } + const plannerReport = Array.isArray(message?.planning_report) && message.planning_report.length > 0 + ? message.planning_report[0] + : Array.isArray(message?.report) && message.report.length > 0 + ? message.report[0] + : null; + if (plannerReport !== null) { + details.push(String(plannerReport || "")); + } + updateExecPlannerLivePreview(this); + if (details.length > 0) { + this.properties.iamccsPlannerDetails = details.join("\n"); + markCanvasDirty(); + } + }); }; } if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") { const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); - if (Array.isArray(message?.report) && message.report.length > 0) { - this.properties = this.properties || {}; - this.properties.iamccsRenderStatus = String(message.report[0] || "").split("\n")[0]; - app.graph.setDirtyCanvas(true, true); - } + safeCall(`${nodeName}.onExecuted.original`, () => onExecuted?.apply(this, arguments)); + safeCall(`${nodeName}.onExecuted.iamccs`, () => { + if (Array.isArray(message?.report) && message.report.length > 0) { + this.properties = this.properties || {}; + this.properties.iamccsRenderStatus = String(message.report[0] || "").split("\n")[0]; + markCanvasDirty(); + } + }); }; } if (nodeName === "IAMCCS-SuperNodes Second Stage") { const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); - if (Array.isArray(message?.report) && message.report.length > 0) { - this.properties = this.properties || {}; - this.properties.iamccsSecondStageStatus = String(message.report[0] || "").split("\n")[0]; - app.graph.setDirtyCanvas(true, true); - } + safeCall(`${nodeName}.onExecuted.original`, () => onExecuted?.apply(this, arguments)); + safeCall(`${nodeName}.onExecuted.iamccs`, () => { + if (Array.isArray(message?.report) && message.report.length > 0) { + this.properties = this.properties || {}; + this.properties.iamccsSecondStageStatus = String(message.report[0] || "").split("\n")[0]; + markCanvasDirty(); + } + }); }; } }, @@ -1181,8 +3100,3 @@ app.registerExtension({ - - - - -