Updated IAMCCS-nodes to version 1.4.2

This commit is contained in:
IAMCCS
2026-04-27 08:00:07 +02:00
parent d11592ca6b
commit 105532a8ab
42 changed files with 17318 additions and 149 deletions
+5 -1
View File
@@ -9,7 +9,11 @@
### Category: ComfyUI Custom Nodes
### Main Feature: Fix for LoRA loading in native WANAnimate workflows + general nodes 4 ComfyUI
Versuin: 1.4.0
Version: 1.4.2
## 🆕 Sdded new LTX-2.3 nodes for v2v, au+img2vid (instructions: patreon.com/IAMCCS)
Version: 1.4.0
## 🆕 Dataset Creation Workflow Companions
+172
View File
@@ -22,6 +22,18 @@ from .iamccs_wan_lora_stack_simple import (
from .iamccs_wan_lora_schedule import (
IAMCCS_WanLoRASchedule,
)
from .iamccs_wan_lora_hooks import (
IAMCCS_WanLoRAHookSchedule,
IAMCCS_ApplyLoRAHooksToConditioning,
IAMCCS_ApplyScheduledWanLoRAFromConditioning,
IAMCCS_BuildScheduledWanModelBank,
IAMCCS_SelectScheduledWanModelFromConditioning,
IAMCCS_SelectScheduledWanModelPairFromConditioning,
)
from .iamccs_wan_lora_runtime_bridge import (
IAMCCS_WanLoRARuntimeBridge,
IAMCCS_ModelWithLoRA_RuntimeBridge,
)
from .iamccs_ltx2_lora_stack import (
IAMCCS_LTX2_LoRAStack,
@@ -46,6 +58,8 @@ from .iamccs_ltx2_tools import (
IAMCCS_LTX2_ImageBatchPadReflect,
IAMCCS_LTX2_ImageBatchCropByPad,
IAMCCS_SegmentPlanner,
IAMCCS_SegmentPlannerSettings,
IAMCCS_SegmentPlannerLinked,
IAMCCS_SegmentPlanFromPlanner,
IAMCCS_SourceRangeFromSegmentPlan,
IAMCCS_TwoSegmentPlanner,
@@ -72,6 +86,21 @@ from .iamccs_ltx2_extension_module import (
IAMCCS_LTX2_FirstLastLatentControl_Pro,
)
from .iamccs_ltx2_cinematic_flf import (
IAMCCS_LTX2_CinematicShotPlanner,
IAMCCS_LTX2_CinematicRefLatentControl,
IAMCCS_LTX2_AudioPromptDirector,
IAMCCS_LTX2_CinematicPromptRelayAdapter,
IAMCCS_LTX2_CinematicPromptComposer,
IAMCCS_LTX2_CinematicShotLineBuilder,
IAMCCS_LTX2_CinematicV2VTimelineLineBuilder,
IAMCCS_LTX2_CinematicLineStacker,
IAMCCS_LTX2_CinematicMultiGenPlanner,
IAMCCS_LTX2_CinematicShotAudioSelector,
IAMCCS_LTX2_CinematicV2VTimelinePlanner,
IAMCCS_LTX2_CinematicV2VAssetSelector,
)
from .iamccs_ltx2_temporal_overlap_samplers import (
IAMCCS_LTX2_ConditionNextLatentWithPrevOverlap,
IAMCCS_LTX2_InitLatentSampler,
@@ -132,6 +161,10 @@ from .iamccs_multiswitch import (
IAMCCS_MultiSwitch,
)
from .iamccs_lazy_switch import (
IAMCCS_LazyAnySwitch,
)
from .iamccs_hw_supporter import (
IAMCCS_HwSupporter,
IAMCCS_HwSupporterAny,
@@ -202,12 +235,57 @@ from .iamccs_multiline_prompt_splitter import (
IAMCCS_MultilinePromptSplitter8,
)
from .iamccs_supernode_modular import (
IAMCCS_SupernodeBase,
IAMCCS_SupernodeModule,
)
from .iamccs_auimg2vid_goal1 import (
IAMCCS_ProjectTimelinePlanner,
IAMCCS_Ltx2HelperModules_ProjectTimelinePlanner,
IAMCCS_Ltx2HelperModules_Planner,
IAMCCS_Ltx2HelperModules_AudioTimeline,
IAMCCS_Ltx2HelperModules_KeyframeTimeline,
IAMCCS_Ltx2HelperModules_RefreshPolicy,
IAMCCS_Ltx2HelperModules_ReanchorLatent,
IAMCCS_Ltx2HelperModules_DiskExtension,
IAMCCS_Ltx2HelperModules_RuntimeBridge,
IAMCCS_Ltx2HelperModules_Continuity,
IAMCCS_Ltx2HelperModules_Finalize,
IAMCCS_AUIMG2VID_ProjectTimelinePlanner,
IAMCCS_AUIMG2VID_Planner,
IAMCCS_AUIMG2VID_AudioTimeline,
IAMCCS_AUIMG2VID_KeyframeTimeline,
IAMCCS_AUIMG2VID_RefreshPolicy,
IAMCCS_AUIMG2VID_ReanchorLatent,
IAMCCS_AUIMG2VID_DiskExtension,
IAMCCS_AUIMG2VID_RuntimeBridge,
IAMCCS_AUIMG2VID_Continuity,
IAMCCS_AUIMG2VID_Finalize,
)
from .iamccs_supernodes_exec import (
IAMCCS_SuperNodes_AUIMG2VIDExecutablePlanner,
IAMCCS_SuperNodes_AUIMG2VIDExecutableRender,
IAMCCS_SuperNodes_AUIMG2VIDExecutableVAE,
IAMCCS_SuperNodes_AUIMG2VIDExecutableFinalize,
)
from .iamccs_supernodes_second_stage import IAMCCS_SuperNodes_SecondStage
# Nodi principali
NODE_CLASS_MAPPINGS = {
"IAMCCS_WanLoRAStack": IAMCCS_WanLoRAStack,
"IAMCCS_ModelWithLoRA": IAMCCS_ModelWithLoRA,
"IAMCCS_WanLoRAStackModelIO": IAMCCS_WanLoRAStackModelIO,
"IAMCCS_WanLoRASchedule": IAMCCS_WanLoRASchedule,
"IAMCCS_WanLoRAHookSchedule": IAMCCS_WanLoRAHookSchedule,
"IAMCCS_ApplyLoRAHooksToConditioning": IAMCCS_ApplyLoRAHooksToConditioning,
"IAMCCS_ApplyScheduledWanLoRAFromConditioning": IAMCCS_ApplyScheduledWanLoRAFromConditioning,
"IAMCCS_BuildScheduledWanModelBank": IAMCCS_BuildScheduledWanModelBank,
"IAMCCS_SelectScheduledWanModelFromConditioning": IAMCCS_SelectScheduledWanModelFromConditioning,
"IAMCCS_SelectScheduledWanModelPairFromConditioning": IAMCCS_SelectScheduledWanModelPairFromConditioning,
"IAMCCS_WanLoRARuntimeBridge": IAMCCS_WanLoRARuntimeBridge,
"IAMCCS_ModelWithLoRA_RuntimeBridge": IAMCCS_ModelWithLoRA_RuntimeBridge,
# Backward-compatible key (kept as-is for existing workflows)
"iamccs_ltx2_lora_stack": IAMCCS_LTX2_LoRAStack,
# Preferred explicit names
@@ -228,6 +306,8 @@ NODE_CLASS_MAPPINGS = {
"IAMCCS_LTX2_ImageBatchPadReflect": IAMCCS_LTX2_ImageBatchPadReflect,
"IAMCCS_LTX2_ImageBatchCropByPad": IAMCCS_LTX2_ImageBatchCropByPad,
"IAMCCS_SegmentPlanner": IAMCCS_SegmentPlanner,
"IAMCCS_SegmentPlannerSettings": IAMCCS_SegmentPlannerSettings,
"IAMCCS_SegmentPlannerLinked": IAMCCS_SegmentPlannerLinked,
"IAMCCS_SegmentPlanFromPlanner": IAMCCS_SegmentPlanFromPlanner,
"IAMCCS_SourceRangeFromSegmentPlan": IAMCCS_SourceRangeFromSegmentPlan,
"IAMCCS_TwoSegmentPlanner": IAMCCS_TwoSegmentPlanner,
@@ -249,6 +329,18 @@ NODE_CLASS_MAPPINGS = {
"IAMCCS_LTX2_MiddleFrames": IAMCCS_LTX2_MiddleFrames,
"IAMCCS_LTX2_FirstLastLatentControl": IAMCCS_LTX2_FirstLastLatentControl,
"IAMCCS_LTX2_FirstLastLatentControl_Pro": IAMCCS_LTX2_FirstLastLatentControl_Pro,
"IAMCCS_LTX2_CinematicShotPlanner": IAMCCS_LTX2_CinematicShotPlanner,
"IAMCCS_LTX2_CinematicRefLatentControl": IAMCCS_LTX2_CinematicRefLatentControl,
"IAMCCS_LTX2_AudioPromptDirector": IAMCCS_LTX2_AudioPromptDirector,
"IAMCCS_LTX2_CinematicPromptRelayAdapter": IAMCCS_LTX2_CinematicPromptRelayAdapter,
"IAMCCS_LTX2_CinematicPromptComposer": IAMCCS_LTX2_CinematicPromptComposer,
"IAMCCS_LTX2_CinematicShotLineBuilder": IAMCCS_LTX2_CinematicShotLineBuilder,
"IAMCCS_LTX2_CinematicV2VTimelineLineBuilder": IAMCCS_LTX2_CinematicV2VTimelineLineBuilder,
"IAMCCS_LTX2_CinematicLineStacker": IAMCCS_LTX2_CinematicLineStacker,
"IAMCCS_LTX2_CinematicMultiGenPlanner": IAMCCS_LTX2_CinematicMultiGenPlanner,
"IAMCCS_LTX2_CinematicShotAudioSelector": IAMCCS_LTX2_CinematicShotAudioSelector,
"IAMCCS_LTX2_CinematicV2VTimelinePlanner": IAMCCS_LTX2_CinematicV2VTimelinePlanner,
"IAMCCS_LTX2_CinematicV2VAssetSelector": IAMCCS_LTX2_CinematicV2VAssetSelector,
"IAMCCS_LTX2_InitLatentSampler": IAMCCS_LTX2_InitLatentSampler,
"IAMCCS_LTX2_LoopingSampler": IAMCCS_LTX2_LoopingSampler,
"IAMCCS_LTX2_OneShotLowRAMLooper": IAMCCS_LTX2_OneShotLowRAMLooper,
@@ -292,6 +384,7 @@ NODE_CLASS_MAPPINGS = {
"IAMCCS_bus_group": IAMCCS_bus_group,
"IAMCCS_MultiSwitch": IAMCCS_MultiSwitch,
"IAMCCS_LazyAnySwitch": IAMCCS_LazyAnySwitch,
"IAMCCS_HwSupporter": IAMCCS_HwSupporter,
"IAMCCS_HwSupporterAny": IAMCCS_HwSupporterAny,
@@ -328,6 +421,34 @@ NODE_CLASS_MAPPINGS = {
"IAMCCS_QwenMultiGen": IAMCCS_QwenMultiGen,
"IAMCCS_FluxKleinMultiGen": IAMCCS_FluxKleinMultiGen,
"IAMCCS_MultilinePromptSplitter8": IAMCCS_MultilinePromptSplitter8,
"IAMCCS_SupernodeBase": IAMCCS_SupernodeBase,
"IAMCCS_SupernodeModule": IAMCCS_SupernodeModule,
"IAMCCS_ProjectTimelinePlanner": IAMCCS_ProjectTimelinePlanner,
"IAMCCS_Ltx2HelperModules_ProjectTimelinePlanner": IAMCCS_Ltx2HelperModules_ProjectTimelinePlanner,
"IAMCCS_Ltx2HelperModules_Planner": IAMCCS_Ltx2HelperModules_Planner,
"IAMCCS_Ltx2HelperModules_AudioTimeline": IAMCCS_Ltx2HelperModules_AudioTimeline,
"IAMCCS_Ltx2HelperModules_KeyframeTimeline": IAMCCS_Ltx2HelperModules_KeyframeTimeline,
"IAMCCS_Ltx2HelperModules_RefreshPolicy": IAMCCS_Ltx2HelperModules_RefreshPolicy,
"IAMCCS_Ltx2HelperModules_ReanchorLatent": IAMCCS_Ltx2HelperModules_ReanchorLatent,
"IAMCCS_Ltx2HelperModules_DiskExtension": IAMCCS_Ltx2HelperModules_DiskExtension,
"IAMCCS_Ltx2HelperModules_RuntimeBridge": IAMCCS_Ltx2HelperModules_RuntimeBridge,
"IAMCCS_Ltx2HelperModules_Continuity": IAMCCS_Ltx2HelperModules_Continuity,
"IAMCCS_Ltx2HelperModules_Finalize": IAMCCS_Ltx2HelperModules_Finalize,
"IAMCCS_AUIMG2VID_ProjectTimelinePlanner": IAMCCS_AUIMG2VID_ProjectTimelinePlanner,
"IAMCCS_AUIMG2VID_Planner": IAMCCS_AUIMG2VID_Planner,
"IAMCCS_AUIMG2VID_AudioTimeline": IAMCCS_AUIMG2VID_AudioTimeline,
"IAMCCS_AUIMG2VID_KeyframeTimeline": IAMCCS_AUIMG2VID_KeyframeTimeline,
"IAMCCS_AUIMG2VID_RefreshPolicy": IAMCCS_AUIMG2VID_RefreshPolicy,
"IAMCCS_AUIMG2VID_ReanchorLatent": IAMCCS_AUIMG2VID_ReanchorLatent,
"IAMCCS_AUIMG2VID_DiskExtension": IAMCCS_AUIMG2VID_DiskExtension,
"IAMCCS_AUIMG2VID_RuntimeBridge": IAMCCS_AUIMG2VID_RuntimeBridge,
"IAMCCS_AUIMG2VID_Continuity": IAMCCS_AUIMG2VID_Continuity,
"IAMCCS_AUIMG2VID_Finalize": IAMCCS_AUIMG2VID_Finalize,
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": IAMCCS_SuperNodes_AUIMG2VIDExecutablePlanner,
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": IAMCCS_SuperNodes_AUIMG2VIDExecutableRender,
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": IAMCCS_SuperNodes_AUIMG2VIDExecutableVAE,
"IAMCCS-SuperNodes Second Stage": IAMCCS_SuperNodes_SecondStage,
**({"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": IAMCCS_SuperNodes_AUIMG2VIDExecutableFinalize} if IAMCCS_SuperNodes_AUIMG2VIDExecutableFinalize is not None else {}),
# QwenVL First/Last Frame (registered only if QwenVL is installed)
**({"IAMCCS_QWEN_VL_FLF": IAMCCS_QWEN_VL_FLF,
@@ -341,6 +462,14 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"IAMCCS_ModelWithLoRA": "Apply LoRA to MODEL (Native)",
"IAMCCS_WanLoRAStackModelIO": "LoRA Stack (Model In→Out) WAN",
"IAMCCS_WanLoRASchedule": "LoRA Schedule (WAN, ranged)",
"IAMCCS_WanLoRAHookSchedule": "LoRA Schedule (WAN, hooks)",
"IAMCCS_ApplyLoRAHooksToConditioning": "Apply LoRA Hooks to Conditioning",
"IAMCCS_ApplyScheduledWanLoRAFromConditioning": "Apply Scheduled WAN LoRA From Conditioning",
"IAMCCS_BuildScheduledWanModelBank": "Build Scheduled WAN Model Bank",
"IAMCCS_SelectScheduledWanModelFromConditioning": "Select Scheduled WAN Model From Conditioning",
"IAMCCS_SelectScheduledWanModelPairFromConditioning": "Select Scheduled WAN Model Pair From Conditioning",
"IAMCCS_WanLoRARuntimeBridge": "LoRA Runtime Bridge (WAN, loop-safe)",
"IAMCCS_ModelWithLoRA_RuntimeBridge": "Apply LoRA to MODEL (Runtime Bridge)",
"iamccs_ltx2_lora_stack": "iamccs_ltx2_lora_stack (3 slots)",
"IAMCCS_LTX2_LoRAStack": "LoRA Stack (LTX-2, 3 slots)",
"IAMCCS_LTX2_LoRAStackStaged": "LoRA Stack (LTX-2, staged: stage1+stage2) (BETA)",
@@ -359,6 +488,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"IAMCCS_LTX2_ImageBatchPadReflect": "LTX-2 Pad Reflect (IMAGE batch)",
"IAMCCS_LTX2_ImageBatchCropByPad": "LTX-2 Crop By Pad (IMAGE batch)",
"IAMCCS_SegmentPlanner": "Segment Planner (song -> LTX frames)",
"IAMCCS_SegmentPlannerSettings": "Segment Planner Settings (shared)",
"IAMCCS_SegmentPlannerLinked": "Segment Planner Linked (shared inputs)",
"IAMCCS_SegmentPlanFromPlanner": "Segment Plan From Planner (per index)",
"IAMCCS_SourceRangeFromSegmentPlan": "Source Range From Segment Plan",
"IAMCCS_TwoSegmentPlanner": "Two Segment Planner (stable 2SEG)",
@@ -380,6 +511,18 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"IAMCCS_LTX2_MiddleFrames": "LTX-2 Middle Frames (accumulator) 🧷",
"IAMCCS_LTX2_FirstLastLatentControl": "LTX-2 First/Last → Latent (noise_mask) 🎯",
"IAMCCS_LTX2_FirstLastLatentControl_Pro": "LTX-2 First/Last → Latent (Pro, slot caps) 🎯",
"IAMCCS_LTX2_CinematicShotPlanner": "LTX-2 Cinematic Shot Planner",
"IAMCCS_LTX2_CinematicRefLatentControl": "LTX-2 Cinematic Ref -> Latent",
"IAMCCS_LTX2_AudioPromptDirector": "LTX-2 Audio Prompt Director",
"IAMCCS_LTX2_CinematicPromptRelayAdapter": "LTX-2 Cinematic PromptRelay Adapter",
"IAMCCS_LTX2_CinematicPromptComposer": "LTX-2 Cinematic Prompt Composer",
"IAMCCS_LTX2_CinematicShotLineBuilder": "LTX-2 Cinematic Shot Line Builder",
"IAMCCS_LTX2_CinematicV2VTimelineLineBuilder": "LTX-2 Cinematic V2V Line Builder",
"IAMCCS_LTX2_CinematicLineStacker": "LTX-2 Cinematic Line Stacker",
"IAMCCS_LTX2_CinematicMultiGenPlanner": "LTX-2 Cinematic MultiGen Planner",
"IAMCCS_LTX2_CinematicShotAudioSelector": "LTX-2 Cinematic Shot Audio Selector",
"IAMCCS_LTX2_CinematicV2VTimelinePlanner": "LTX-2 Cinematic V2V Timeline Planner",
"IAMCCS_LTX2_CinematicV2VAssetSelector": "LTX-2 Cinematic V2V Asset Selector",
"IAMCCS_LTX2_InitLatentSampler": "LTX-2 Init Latent Sampler 🧱",
"IAMCCS_LTX2_LoopingSampler": "LTX-2 Looping Sampler (temporal overlap) 🧷",
"IAMCCS_LTX2_OneShotLowRAMLooper": "LTX-2 One-Shot Low-RAM Looper 🪶",
@@ -417,12 +560,41 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"IAMCCS_QwenMultiGen": "IAMCCS Qwen Multi-Gen",
"IAMCCS_FluxKleinMultiGen": "Flux Klein Multi-Gen",
"IAMCCS_MultilinePromptSplitter8": "Multiline Prompt Splitter (8 outputs)",
"IAMCCS_SupernodeBase": "Supernode Base (contract + linx)",
"IAMCCS_SupernodeModule": "Supernode Module (cascade contract + linx)",
"IAMCCS_ProjectTimelinePlanner": "IAMCCS Project Timeline Planner",
"IAMCCS_Ltx2HelperModules_ProjectTimelinePlanner": "IAMCCS_Ltx2HelperModules Project Timeline Planner",
"IAMCCS_Ltx2HelperModules_Planner": "IAMCCS_Ltx2HelperModules Planner",
"IAMCCS_Ltx2HelperModules_AudioTimeline": "IAMCCS_Ltx2HelperModules Audio Timeline",
"IAMCCS_Ltx2HelperModules_KeyframeTimeline": "IAMCCS_Ltx2HelperModules Keyframe Timeline",
"IAMCCS_Ltx2HelperModules_RefreshPolicy": "IAMCCS_Ltx2HelperModules Refresh Policy",
"IAMCCS_Ltx2HelperModules_ReanchorLatent": "IAMCCS_Ltx2HelperModules Reanchor Latent",
"IAMCCS_Ltx2HelperModules_DiskExtension": "IAMCCS_Ltx2HelperModules Disk Extension",
"IAMCCS_Ltx2HelperModules_RuntimeBridge": "IAMCCS_Ltx2HelperModules Runtime Bridge",
"IAMCCS_Ltx2HelperModules_Continuity": "IAMCCS_Ltx2HelperModules Continuity",
"IAMCCS_Ltx2HelperModules_Finalize": "IAMCCS_Ltx2HelperModules Finalize",
"IAMCCS_AUIMG2VID_ProjectTimelinePlanner": "AU+IMG2VID Project Timeline Planner (legacy alias)",
"IAMCCS_AUIMG2VID_Planner": "AU+IMG2VID Planner (legacy alias)",
"IAMCCS_AUIMG2VID_AudioTimeline": "AU+IMG2VID Audio Timeline (legacy alias)",
"IAMCCS_AUIMG2VID_KeyframeTimeline": "AU+IMG2VID Keyframe Timeline (legacy alias)",
"IAMCCS_AUIMG2VID_RefreshPolicy": "AU+IMG2VID Refresh Policy (legacy alias)",
"IAMCCS_AUIMG2VID_ReanchorLatent": "AU+IMG2VID Reanchor Latent (legacy alias)",
"IAMCCS_AUIMG2VID_DiskExtension": "AU+IMG2VID Disk Extension (legacy alias)",
"IAMCCS_AUIMG2VID_RuntimeBridge": "AU+IMG2VID Runtime Bridge (legacy alias)",
"IAMCCS_AUIMG2VID_Continuity": "AU+IMG2VID Continuity (legacy alias)",
"IAMCCS_AUIMG2VID_Finalize": "AU+IMG2VID Finalize (legacy alias)",
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": "IAMCCS-SuperNodes AU+IMG2VID Exec Planner",
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": "IAMCCS-SuperNodes AU+IMG2VID Exec Render",
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": "IAMCCS-SuperNodes AU+IMG2VID Exec VAE",
"IAMCCS-SuperNodes Second Stage": "IAMCCS-SuperNodes Second Stage",
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": "IAMCCS-SuperNodes AU+IMG2VID Exec Finalize",
"IAMCCS_SamplerAdvancedVersion1": "Sampler Advanced v1",
"IAMCCS_bus_group": "Bus Group (Mute + Solo) (frontend-only)",
"IAMCCS_MultiSwitch": "MultiSwitch (dynamic inputs)",
"IAMCCS_LazyAnySwitch": "Lazy MultiGen Switch (Qwen / Flux)",
"IAMCCS_WanSviArgs": "Wan SVI Args",
"IAMCCS_WanSviChainRunner": "Wan SVI Chain Runner",
"IAMCCS_WanSviSegmentPick": "Wan SVI Segment Pick",
+745
View File
@@ -0,0 +1,745 @@
# ==========================================================
# __init__.py — Registro nodi IAMCCS
# ==========================================================
import logging
import os
from .iamccs_comfy_compat import apply_iamccs_comfy_compat_patches
apply_iamccs_comfy_compat_patches()
# ComfyUI frontend assets
WEB_DIRECTORY = "web"
from .iamccs_wan_lora_stack import (
IAMCCS_WanLoRAStack,
IAMCCS_ModelWithLoRA,
)
from .iamccs_wan_lora_stack_simple import (
IAMCCS_WanLoRAStackModelIO,
)
from .iamccs_wan_lora_schedule import (
IAMCCS_WanLoRASchedule,
)
from .iamccs_wan_lora_hooks import (
IAMCCS_WanLoRAHookSchedule,
IAMCCS_ApplyLoRAHooksToConditioning,
IAMCCS_ApplyScheduledWanLoRAFromConditioning,
IAMCCS_BuildScheduledWanModelBank,
IAMCCS_SelectScheduledWanModelFromConditioning,
IAMCCS_SelectScheduledWanModelPairFromConditioning,
)
from .iamccs_wan_lora_runtime_bridge import (
IAMCCS_WanLoRARuntimeBridge,
IAMCCS_ModelWithLoRA_RuntimeBridge,
)
from .iamccs_ltx2_lora_stack import (
IAMCCS_LTX2_LoRAStack,
IAMCCS_LTX2_LoRAStackStaged,
IAMCCS_ModelWithLoRA_LTX2,
IAMCCS_ModelWithLoRA_LTX2_Staged,
IAMCCS_LTX2_LoRAStackModelIO,
)
from .iamccs_ltx2_lora_stack_segmented6 import (
IAMCCS_LTX2_LoRAStackSegmented6,
IAMCCS_LTX2_ModelWithLoRA_Segmented6,
)
from .iamccs_ltx2_tools import (
IAMCCS_LTX2_FrameRateSync,
IAMCCS_LTX2_Validator,
IAMCCS_LTX2_TimeFrameCount,
IAMCCS_LTX2_EnsureFrames8nPlus1,
IAMCCS_LTX2_EnsureMinFrames,
IAMCCS_LTX2_ControlPreprocess,
IAMCCS_LTX2_ImageBatchPadReflect,
IAMCCS_LTX2_ImageBatchCropByPad,
IAMCCS_SegmentPlanner,
IAMCCS_SegmentPlanFromPlanner,
IAMCCS_SourceRangeFromSegmentPlan,
IAMCCS_TwoSegmentPlanner,
IAMCCS_ThreeSegmentPlanner,
IAMCCS_SegmentSwitch,
)
from .iamccs_ltx2_extension_module import (
IAMCCS_LTX2_ExtensionModule,
IAMCCS_LTX2_ExtensionModule_Disk,
IAMCCS_LoadImagesFromDirLite,
IAMCCS_SourceFramesToDisk,
IAMCCS_StartDirToVideoLatent,
IAMCCS_VideoCombineFromDir,
IAMCCS_LTX2_ExtensionModule_simple,
IAMCCS_LTX2_GetImageFromBatch,
IAMCCS_LTX2_ReferenceImageSwitch,
IAMCCS_LTX2_ReferenceStartFramesInjector,
IAMCCS_LTX2_FrameCountValidator,
IAMCCS_LTX2_FirstLastFramesController,
IAMCCS_LTX2_ContextLatent,
IAMCCS_LTX2_MiddleFrames,
IAMCCS_LTX2_FirstLastLatentControl,
IAMCCS_LTX2_FirstLastLatentControl_Pro,
)
from .iamccs_ltx2_temporal_overlap_samplers import (
IAMCCS_LTX2_ConditionNextLatentWithPrevOverlap,
IAMCCS_LTX2_InitLatentSampler,
IAMCCS_LTX2_LoopingSampler,
IAMCCS_LTX2_OneShotLowRAMLooper,
IAMCCS_LTX2_ExtendSampler,
)
from .iamccs_wan_svipro_motion import (
IAMCCS_WanImageMotion,
WanImageMotionPro as WanImageMotionProPlus,
IAMCCS_WanImageMotionPro_Simple as IAMCCS_WanImageMotionProPlus_Simple,
IAMCCS_WanImageMotionInductive,
IAMCCS_WanSVIToFLFBridgePro as IAMCCS_WanSVIToFLFBridgeProPlus,
IAMCCS_WanSVIToFLFBridgePro_Simple as IAMCCS_WanSVIToFLFBridgeProPlus_Simple,
WanMotionProTrimmer,
IAMCCS_WanPrevTailPrep,
)
from .iamccs_wan_long_length import (
IAMCCS_WanLongPlanner,
IAMCCS_WanContinuityGuide,
IAMCCS_WanPromptPhasePlanner,
IAMCCS_WanPromptLoopInfo,
IAMCCS_WanIndexedPromptEncode,
IAMCCS_WanImageBatchFrameSelect,
)
from .iamccs_wan_svipro_motion_legacy import (
WanImageMotionProLegacy,
)
from .iamccs_autolink import (
IAMCCS_SetAutoLink,
IAMCCS_GetAutoLink,
IAMCCS_AutoLinkConverter,
IAMCCS_AutoLinkArguments,
)
from .iamccs_gguf_accelerator import (
IAMCCS_GGUF_accelerator,
)
from .iamccs_sampler_advanced_v1 import (
IAMCCS_SamplerAdvancedVersion1,
)
from .iamccs_bus_group import (
IAMCCS_bus_group,
)
from .iamccs_image_resize import (
IAMCCS_ImageResizeBatchSafe,
IAMCCS_LoadResizeSegmentFromDir,
)
from .iamccs_multiswitch import (
IAMCCS_MultiSwitch,
)
from .iamccs_hw_supporter import (
IAMCCS_HwSupporter,
IAMCCS_HwSupporterAny,
IAMCCS_HardMemoryPurge,
IAMCCS_VRAMCleanup,
IAMCCS_VRAMFlushLatent,
IAMCCS_VAEDecodeTiledSafe,
IAMCCS_VAEDecodeToDisk,
)
from .iamccs_hw_probe_node import (
IAMCCS_HWProbeRecommendations,
)
from .iamccs_qwen_vl_flf import (
IAMCCS_QWEN_VL_FLF,
IAMCCS_QWEN_VL_FLF_Advanced,
)
from .iamccs_move_ahead import (
IAMCCS_MoveAhead,
IAMCCS_MoveAheadEnforcer,
IAMCCS_MotionScale,
IAMCCS_MotionScaleAdvanced,
)
from .iamccs_motion_bridge import (
IAMCCS_MotionBridgeSave,
IAMCCS_MotionBridgeLoad,
IAMCCS_LatentTailSlice,
)
from .iamccs_audio_extender import (
IAMCCS_AudioExtensionMath,
IAMCCS_AudioExtender,
IAMCCS_AudioTimelineAssembler,
IAMCCS_AudioTimelineGate,
)
from .iamccs_ltx2_segment_queue import (
IAMCCS_LTX2_BlendLatentBridge,
IAMCCS_LTX2_LastFrameBridgeLoad,
IAMCCS_LTX2_LastFrameBridgeSave,
IAMCCS_LTX2_LoadLatentBridge,
IAMCCS_LTX2_LongVideoWrapperPrep,
IAMCCS_LTX2_LongVideoWrapperPrepDisk,
IAMCCS_LTX2_SaveLatentBridge,
IAMCCS_LTX2_SegmentQueueLoop,
)
from .iamccs_image_resize import (
IAMCCS_ImageResizeBatchSafe,
)
from .iamccs_value_monitor import (
IAMCCS_IntValueMonitor,
)
from .iamccs_flux_klein_multigen import (
IAMCCS_FluxKleinMultiGen,
)
from .iamccs_qwen_multigen import (
IAMCCS_QwenMultiGen,
)
from .iamccs_multiline_prompt_splitter import (
IAMCCS_MultilinePromptSplitter8,
)
from .iamccs_supernode_modular import (
IAMCCS_SupernodeBase,
IAMCCS_SupernodeModule,
)
# Nodi principali
NODE_CLASS_MAPPINGS = {
"IAMCCS_WanLoRAStack": IAMCCS_WanLoRAStack,
"IAMCCS_ModelWithLoRA": IAMCCS_ModelWithLoRA,
"IAMCCS_WanLoRAStackModelIO": IAMCCS_WanLoRAStackModelIO,
"IAMCCS_WanLoRASchedule": IAMCCS_WanLoRASchedule,
"IAMCCS_WanLoRAHookSchedule": IAMCCS_WanLoRAHookSchedule,
"IAMCCS_ApplyLoRAHooksToConditioning": IAMCCS_ApplyLoRAHooksToConditioning,
"IAMCCS_ApplyScheduledWanLoRAFromConditioning": IAMCCS_ApplyScheduledWanLoRAFromConditioning,
"IAMCCS_BuildScheduledWanModelBank": IAMCCS_BuildScheduledWanModelBank,
"IAMCCS_SelectScheduledWanModelFromConditioning": IAMCCS_SelectScheduledWanModelFromConditioning,
"IAMCCS_SelectScheduledWanModelPairFromConditioning": IAMCCS_SelectScheduledWanModelPairFromConditioning,
"IAMCCS_WanLoRARuntimeBridge": IAMCCS_WanLoRARuntimeBridge,
"IAMCCS_ModelWithLoRA_RuntimeBridge": IAMCCS_ModelWithLoRA_RuntimeBridge,
# Backward-compatible key (kept as-is for existing workflows)
"iamccs_ltx2_lora_stack": IAMCCS_LTX2_LoRAStack,
# Preferred explicit names
"IAMCCS_LTX2_LoRAStack": IAMCCS_LTX2_LoRAStack,
"IAMCCS_LTX2_LoRAStackStaged": IAMCCS_LTX2_LoRAStackStaged,
"IAMCCS_ModelWithLoRA_LTX2": IAMCCS_ModelWithLoRA_LTX2,
"IAMCCS_ModelWithLoRA_LTX2_Staged": IAMCCS_ModelWithLoRA_LTX2_Staged,
"IAMCCS_LTX2_LoRAStackModelIO": IAMCCS_LTX2_LoRAStackModelIO,
"IAMCCS_LTX2_LoRAStackSegmented6": IAMCCS_LTX2_LoRAStackSegmented6,
"IAMCCS_LTX2_ModelWithLoRA_Segmented6": IAMCCS_LTX2_ModelWithLoRA_Segmented6,
"IAMCCS_LTX2_FrameRateSync": IAMCCS_LTX2_FrameRateSync,
"IAMCCS_LTX2_Validator": IAMCCS_LTX2_Validator,
"IAMCCS_LTX2_TimeFrameCount": IAMCCS_LTX2_TimeFrameCount,
"IAMCCS_LTX2_EnsureFrames8nPlus1": IAMCCS_LTX2_EnsureFrames8nPlus1,
"IAMCCS_LTX2_EnsureMinFrames": IAMCCS_LTX2_EnsureMinFrames,
"IAMCCS_LTX2_ControlPreprocess": IAMCCS_LTX2_ControlPreprocess,
"IAMCCS_LTX2_ImageBatchPadReflect": IAMCCS_LTX2_ImageBatchPadReflect,
"IAMCCS_LTX2_ImageBatchCropByPad": IAMCCS_LTX2_ImageBatchCropByPad,
"IAMCCS_SegmentPlanner": IAMCCS_SegmentPlanner,
"IAMCCS_SegmentPlanFromPlanner": IAMCCS_SegmentPlanFromPlanner,
"IAMCCS_SourceRangeFromSegmentPlan": IAMCCS_SourceRangeFromSegmentPlan,
"IAMCCS_TwoSegmentPlanner": IAMCCS_TwoSegmentPlanner,
"IAMCCS_ThreeSegmentPlanner": IAMCCS_ThreeSegmentPlanner,
"IAMCCS_SegmentSwitch": IAMCCS_SegmentSwitch,
"IAMCCS_LTX2_ExtensionModule": IAMCCS_LTX2_ExtensionModule,
"IAMCCS_LTX2_ExtensionModule_Disk": IAMCCS_LTX2_ExtensionModule_Disk,
"IAMCCS_LoadImagesFromDirLite": IAMCCS_LoadImagesFromDirLite,
"IAMCCS_SourceFramesToDisk": IAMCCS_SourceFramesToDisk,
"IAMCCS_StartDirToVideoLatent": IAMCCS_StartDirToVideoLatent,
"IAMCCS_VideoCombineFromDir": IAMCCS_VideoCombineFromDir,
"IAMCCS_LTX2_ExtensionModule_simple": IAMCCS_LTX2_ExtensionModule_simple,
"IAMCCS_LTX2_GetImageFromBatch": IAMCCS_LTX2_GetImageFromBatch,
"IAMCCS_LTX2_ReferenceImageSwitch": IAMCCS_LTX2_ReferenceImageSwitch,
"IAMCCS_LTX2_ReferenceStartFramesInjector": IAMCCS_LTX2_ReferenceStartFramesInjector,
"IAMCCS_LTX2_FrameCountValidator": IAMCCS_LTX2_FrameCountValidator,
"IAMCCS_LTX2_FirstLastFramesController": IAMCCS_LTX2_FirstLastFramesController,
"IAMCCS_LTX2_ContextLatent": IAMCCS_LTX2_ContextLatent,
"IAMCCS_LTX2_MiddleFrames": IAMCCS_LTX2_MiddleFrames,
"IAMCCS_LTX2_FirstLastLatentControl": IAMCCS_LTX2_FirstLastLatentControl,
"IAMCCS_LTX2_FirstLastLatentControl_Pro": IAMCCS_LTX2_FirstLastLatentControl_Pro,
"IAMCCS_LTX2_InitLatentSampler": IAMCCS_LTX2_InitLatentSampler,
"IAMCCS_LTX2_LoopingSampler": IAMCCS_LTX2_LoopingSampler,
"IAMCCS_LTX2_OneShotLowRAMLooper": IAMCCS_LTX2_OneShotLowRAMLooper,
"IAMCCS_LTX2_ExtendSampler": IAMCCS_LTX2_ExtendSampler,
"IAMCCS_LTX2_ConditionNextLatentWithPrevOverlap": IAMCCS_LTX2_ConditionNextLatentWithPrevOverlap,
"IAMCCS_WanImageMotion": IAMCCS_WanImageMotion,
# Backward-compat alias: workflow JSONs saved with the _AdaIN name still load.
"IAMCCS_WanImageMotion_AdaIN": IAMCCS_WanImageMotion,
"WanImageMotionPro": WanImageMotionProPlus,
"IAMCCS_WanImageMotionPro_AdaIN": WanImageMotionProPlus,
# Keep the historic key on the current implementation so existing workflows
# load the new Plus node with continuity profiles and presets.
"IAMCCS_WanImageMotionPro": WanImageMotionProPlus,
# Explicit legacy entrypoint for older raw-only behavior.
"WanImageMotionProLegacy": WanImageMotionProLegacy,
"IAMCCS_WanImageMotionProLegacy": WanImageMotionProLegacy,
"WanImageMotionProPlus": WanImageMotionProPlus,
"IAMCCS_WanImageMotionProPlus": WanImageMotionProPlus,
"IAMCCS_WanImageMotionProPlus_Simple": IAMCCS_WanImageMotionProPlus_Simple,
"IAMCCS_WanImageMotionInductive": IAMCCS_WanImageMotionInductive,
"IAMCCS_WanSVIToFLFBridgeProPlus": IAMCCS_WanSVIToFLFBridgeProPlus,
"IAMCCS_WanSVIToFLFBridgeProPlus_Simple": IAMCCS_WanSVIToFLFBridgeProPlus_Simple,
"WanMotionProTrimmer": WanMotionProTrimmer,
"IAMCCS_WanPrevTailPrep": IAMCCS_WanPrevTailPrep,
"IAMCCS_WanLongPlanner": IAMCCS_WanLongPlanner,
"IAMCCS_WanContinuityGuide": IAMCCS_WanContinuityGuide,
"IAMCCS_WanPromptPhasePlanner": IAMCCS_WanPromptPhasePlanner,
"IAMCCS_WanPromptLoopInfo": IAMCCS_WanPromptLoopInfo,
"IAMCCS_WanIndexedPromptEncode": IAMCCS_WanIndexedPromptEncode,
"IAMCCS_WanImageBatchFrameSelect": IAMCCS_WanImageBatchFrameSelect,
"IAMCCS_SetAutoLink": IAMCCS_SetAutoLink,
"IAMCCS_GetAutoLink": IAMCCS_GetAutoLink,
"IAMCCS_AutoLinkConverter": IAMCCS_AutoLinkConverter,
"IAMCCS_AutoLinkArguments": IAMCCS_AutoLinkArguments,
"IAMCCS_GGUF_accelerator": IAMCCS_GGUF_accelerator,
"IAMCCS_SamplerAdvancedVersion1": IAMCCS_SamplerAdvancedVersion1,
"IAMCCS_bus_group": IAMCCS_bus_group,
"IAMCCS_MultiSwitch": IAMCCS_MultiSwitch,
"IAMCCS_HwSupporter": IAMCCS_HwSupporter,
"IAMCCS_HwSupporterAny": IAMCCS_HwSupporterAny,
"IAMCCS_HardMemoryPurge": IAMCCS_HardMemoryPurge,
"IAMCCS_VRAMCleanup": IAMCCS_VRAMCleanup,
"IAMCCS_VRAMFlushLatent": IAMCCS_VRAMFlushLatent,
"IAMCCS_VAEDecodeTiledSafe": IAMCCS_VAEDecodeTiledSafe,
"IAMCCS_VAEDecodeToDisk": IAMCCS_VAEDecodeToDisk,
"IAMCCS_HWProbeRecommendations": IAMCCS_HWProbeRecommendations,
"IAMCCS_MoveAhead": IAMCCS_MoveAhead,
"IAMCCS_MoveAheadEnforcer": IAMCCS_MoveAheadEnforcer,
"IAMCCS_MotionScale": IAMCCS_MotionScale,
"IAMCCS_MotionScaleAdvanced": IAMCCS_MotionScaleAdvanced,
"IAMCCS_MotionBridgeSave": IAMCCS_MotionBridgeSave,
"IAMCCS_MotionBridgeLoad": IAMCCS_MotionBridgeLoad,
"IAMCCS_LatentTailSlice": IAMCCS_LatentTailSlice,
"IAMCCS_AudioExtensionMath": IAMCCS_AudioExtensionMath,
"IAMCCS_AudioExtender": IAMCCS_AudioExtender,
"IAMCCS_AudioTimelineAssembler": IAMCCS_AudioTimelineAssembler,
"IAMCCS_AudioTimelineGate": IAMCCS_AudioTimelineGate,
"IAMCCS_LTX2_LastFrameBridgeSave": IAMCCS_LTX2_LastFrameBridgeSave,
"IAMCCS_LTX2_BlendLatentBridge": IAMCCS_LTX2_BlendLatentBridge,
"IAMCCS_LTX2_LastFrameBridgeLoad": IAMCCS_LTX2_LastFrameBridgeLoad,
"IAMCCS_LTX2_LoadLatentBridge": IAMCCS_LTX2_LoadLatentBridge,
"IAMCCS_LTX2_LongVideoWrapperPrep": IAMCCS_LTX2_LongVideoWrapperPrep,
"IAMCCS_LTX2_LongVideoWrapperPrepDisk": IAMCCS_LTX2_LongVideoWrapperPrepDisk,
"IAMCCS_LTX2_SaveLatentBridge": IAMCCS_LTX2_SaveLatentBridge,
"IAMCCS_LTX2_SegmentQueueLoop": IAMCCS_LTX2_SegmentQueueLoop,
"IAMCCS_ImageResizeBatchSafe": IAMCCS_ImageResizeBatchSafe,
"IAMCCS_LoadResizeSegmentFromDir": IAMCCS_LoadResizeSegmentFromDir,
"IAMCCS_IntValueMonitor": IAMCCS_IntValueMonitor,
"IAMCCS_QwenMultiGen": IAMCCS_QwenMultiGen,
"IAMCCS_FluxKleinMultiGen": IAMCCS_FluxKleinMultiGen,
"IAMCCS_MultilinePromptSplitter8": IAMCCS_MultilinePromptSplitter8,
"IAMCCS_SupernodeBase": IAMCCS_SupernodeBase,
"IAMCCS_SupernodeModule": IAMCCS_SupernodeModule,
# QwenVL First/Last Frame (registered only if QwenVL is installed)
**({"IAMCCS_QWEN_VL_FLF": IAMCCS_QWEN_VL_FLF,
"IAMCCS_QWEN_VL_FLF_Advanced": IAMCCS_QWEN_VL_FLF_Advanced,
} if IAMCCS_QWEN_VL_FLF is not None else {}),
}
NODE_DISPLAY_NAME_MAPPINGS = {
"IAMCCS_WanLoRAStack": "LoRA Stack (WAN-style remap)",
"IAMCCS_ModelWithLoRA": "Apply LoRA to MODEL (Native)",
"IAMCCS_WanLoRAStackModelIO": "LoRA Stack (Model In→Out) WAN",
"IAMCCS_WanLoRASchedule": "LoRA Schedule (WAN, ranged)",
"IAMCCS_WanLoRAHookSchedule": "LoRA Schedule (WAN, hooks)",
"IAMCCS_ApplyLoRAHooksToConditioning": "Apply LoRA Hooks to Conditioning",
"IAMCCS_ApplyScheduledWanLoRAFromConditioning": "Apply Scheduled WAN LoRA From Conditioning",
"IAMCCS_BuildScheduledWanModelBank": "Build Scheduled WAN Model Bank",
"IAMCCS_SelectScheduledWanModelFromConditioning": "Select Scheduled WAN Model From Conditioning",
"IAMCCS_SelectScheduledWanModelPairFromConditioning": "Select Scheduled WAN Model Pair From Conditioning",
"IAMCCS_WanLoRARuntimeBridge": "LoRA Runtime Bridge (WAN, loop-safe)",
"IAMCCS_ModelWithLoRA_RuntimeBridge": "Apply LoRA to MODEL (Runtime Bridge)",
"iamccs_ltx2_lora_stack": "iamccs_ltx2_lora_stack (3 slots)",
"IAMCCS_LTX2_LoRAStack": "LoRA Stack (LTX-2, 3 slots)",
"IAMCCS_LTX2_LoRAStackStaged": "LoRA Stack (LTX-2, staged: stage1+stage2) (BETA)",
"IAMCCS_ModelWithLoRA_LTX2": "Apply LoRA to MODEL (LTX-2, quiet logs)",
"IAMCCS_ModelWithLoRA_LTX2_Staged": "Apply LoRA to MODEL (LTX-2, staged) (BETA)",
"IAMCCS_LTX2_LoRAStackModelIO": "LoRA Stack (Model In→Out) LTX-2",
"IAMCCS_LTX2_LoRAStackSegmented6": "LoRA Stack (LTX-2, segmented: 3 seg × 2 stages)",
"IAMCCS_LTX2_ModelWithLoRA_Segmented6": "Apply LoRA to MODEL (LTX-2, segmented: 3 seg × 2 stages)",
"IAMCCS_LTX2_FrameRateSync": "LTX-2 FrameRate Sync (int+float)",
"IAMCCS_LTX2_Validator": "LTX-2 Validator",
"IAMCCS_LTX2_TimeFrameCount": "LTX-2 TimeFrameCount",
"IAMCCS_LTX2_EnsureFrames8nPlus1": "LTX-2 Ensure Frames (8n + 1)",
"IAMCCS_LTX2_EnsureMinFrames": "LTX-2 Ensure Minimum Frames",
"IAMCCS_LTX2_ControlPreprocess": "LTX-2 Control Preprocess (aux)",
"IAMCCS_LTX2_ImageBatchPadReflect": "LTX-2 Pad Reflect (IMAGE batch)",
"IAMCCS_LTX2_ImageBatchCropByPad": "LTX-2 Crop By Pad (IMAGE batch)",
"IAMCCS_SegmentPlanner": "Segment Planner (song -> LTX frames)",
"IAMCCS_SegmentPlanFromPlanner": "Segment Plan From Planner (per index)",
"IAMCCS_SourceRangeFromSegmentPlan": "Source Range From Segment Plan",
"IAMCCS_TwoSegmentPlanner": "Two Segment Planner (stable 2SEG)",
"IAMCCS_ThreeSegmentPlanner": "Three Segment Planner (stable 3SEG)",
"IAMCCS_SegmentSwitch": "Segment Switch (by segment_index)",
"IAMCCS_LTX2_ExtensionModule": "LTX-2 Extension Module 🎬",
"IAMCCS_LTX2_ExtensionModule_Disk": "LTX-2 Extension Module (Disk / Low RAM) 💾",
"IAMCCS_LoadImagesFromDirLite": "Load Images From Dir (Lite) 📁",
"IAMCCS_SourceFramesToDisk": "Source Frames To Disk 📼💾",
"IAMCCS_StartDirToVideoLatent": "Start Dir To Video Latent 🚀",
"IAMCCS_VideoCombineFromDir": "Video Combine From Dir 🎞️",
"IAMCCS_LTX2_ExtensionModule_simple": "LTX-2 Extension Module (simple) 🎬",
"IAMCCS_LTX2_GetImageFromBatch": "LTX-2 Get Images From Batch 🎞️",
"IAMCCS_LTX2_ReferenceImageSwitch": "LTX-2 Reference Image Switch 🧷",
"IAMCCS_LTX2_ReferenceStartFramesInjector": "LTX-2 Inject Reference Into Start Frames 🧬",
"IAMCCS_LTX2_FrameCountValidator": "LTX-2 Frame Count Validator ✅ (8n+1)",
"IAMCCS_LTX2_FirstLastFramesController": "LTX-2 First/Last Frames Controller 🧲",
"IAMCCS_LTX2_ContextLatent": "LTX-2 Context → Latent (continue) 🧩",
"IAMCCS_LTX2_MiddleFrames": "LTX-2 Middle Frames (accumulator) 🧷",
"IAMCCS_LTX2_FirstLastLatentControl": "LTX-2 First/Last → Latent (noise_mask) 🎯",
"IAMCCS_LTX2_FirstLastLatentControl_Pro": "LTX-2 First/Last → Latent (Pro, slot caps) 🎯",
"IAMCCS_LTX2_InitLatentSampler": "LTX-2 Init Latent Sampler 🧱",
"IAMCCS_LTX2_LoopingSampler": "LTX-2 Looping Sampler (temporal overlap) 🧷",
"IAMCCS_LTX2_OneShotLowRAMLooper": "LTX-2 One-Shot Low-RAM Looper 🪶",
"IAMCCS_LTX2_ExtendSampler": "LTX-2 Extend Sampler (temporal overlap) 🧷",
"IAMCCS_LTX2_ConditionNextLatentWithPrevOverlap": "LTX-2 Condition Next Latent (prev overlap) 🧷",
"IAMCCS_WanImageMotion": "WanImageMotion",
"IAMCCS_WanImageMotion_AdaIN": "WanImageMotion",
"WanImageMotionPro": "WanImageMotionPro Plus",
"IAMCCS_WanImageMotionPro_AdaIN": "WanImageMotionPro Plus",
"WanImageMotionProLegacy": "WanImageMotionPro Legacy",
"IAMCCS_WanImageMotionProLegacy": "WanImageMotionPro Legacy",
"WanImageMotionProPlus": "WanImageMotionPro Plus",
"IAMCCS_WanImageMotionProPlus": "WanImageMotionPro Plus",
"IAMCCS_WanImageMotionProPlus_Simple": "WanImageMotionPro Plus Simple",
"IAMCCS_WanImageMotionInductive": "WanImageMotion Inductive",
"IAMCCS_WanSVIToFLFBridgeProPlus": "Wan SVI→FLF Bridge Pro Plus",
"IAMCCS_WanSVIToFLFBridgeProPlus_Simple": "Wan SVI→FLF Bridge Pro Plus",
"WanMotionProTrimmer": "WanMotionProTrimmer (trim overshoot tail)",
"IAMCCS_WanPrevTailPrep": "Wan Prev Tail Prep",
"IAMCCS_WanLongPlanner": "Wan Long Planner",
"IAMCCS_WanContinuityGuide": "Wan Continuity Guide",
"IAMCCS_WanPromptPhasePlanner": "Wan Prompt Phase Planner",
"IAMCCS_WanPromptLoopInfo": "Wan Prompt Loop Info",
"IAMCCS_WanIndexedPromptEncode": "Wan Indexed Prompt Encode",
"IAMCCS_WanImageBatchFrameSelect": "Wan Image Batch Frame Select",
"IAMCCS_SetAutoLink": "Set AutoLink",
"IAMCCS_GetAutoLink": "Get AutoLink",
"IAMCCS_AutoLinkConverter": "AutoLink Converter",
"IAMCCS_AutoLinkArguments": "AutoLink Arguments",
"IAMCCS_GGUF_accelerator": "GGUF Accelerator (patch_on_device)",
"IAMCCS_IntValueMonitor": "INT Value Monitor",
"IAMCCS_QwenMultiGen": "IAMCCS Qwen Multi-Gen",
"IAMCCS_FluxKleinMultiGen": "Flux Klein Multi-Gen",
"IAMCCS_MultilinePromptSplitter8": "Multiline Prompt Splitter (8 outputs)",
"IAMCCS_SupernodeBase": "Supernode Base (contract + linx)",
"IAMCCS_SupernodeModule": "Supernode Module (cascade contract + linx)",
"IAMCCS_SamplerAdvancedVersion1": "Sampler Advanced v1",
"IAMCCS_bus_group": "Bus Group (Mute + Solo) (frontend-only)",
"IAMCCS_MultiSwitch": "MultiSwitch (dynamic inputs)",
"IAMCCS_WanSviArgs": "Wan SVI Args",
"IAMCCS_WanSviChainRunner": "Wan SVI Chain Runner",
"IAMCCS_WanSviSegmentPick": "Wan SVI Segment Pick",
"IAMCCS_HwSupporter": "HW Supporter (auto VRAM/attention/torch knobs)",
"IAMCCS_HwSupporterAny": "HW Supporter (ANY passthrough)",
"IAMCCS_HardMemoryPurge": "Hard RAM/VRAM Purge (trim working set)",
"IAMCCS_VRAMCleanup": "VRAM Cleanup (unload + empty cache)",
"IAMCCS_VRAMFlushLatent": "VRAM Flush → Latent passthrough (empty cache)",
"IAMCCS_VAEDecodeTiledSafe": "VAE Decode Tiled (safe, optional cleanup)",
"IAMCCS_VAEDecodeToDisk": "VAE Decode → Disk (frames, low RAM)",
"IAMCCS_HWProbeRecommendations": "HW Probe Recommendations (JSON)",
"IAMCCS_MoveAhead": "MoveAhead (FreeLong spectral blend) 🎬",
"IAMCCS_MoveAheadEnforcer": "MoveAhead Enforcer (3-tier motion lock) 🎬",
"IAMCCS_MotionScale": "MotionScale (temporal RoPE scale) ⚡",
"IAMCCS_MotionScaleAdvanced": "MotionScale Advanced (RoPE + theta) ⚡",
"IAMCCS_MotionBridgeSave": "Motion Bridge Save 🎬💾",
"IAMCCS_MotionBridgeLoad": "Motion Bridge Load 🎬📂",
"IAMCCS_LatentTailSlice": "Latent Tail Slice ✂️",
"IAMCCS_AudioExtensionMath": "Audio Extension Math (timeline sync)",
"IAMCCS_AudioExtender": "Audio Extender (segment + overlap)",
"IAMCCS_AudioTimelineAssembler": "Audio Timeline Assembler (full track)",
"IAMCCS_AudioTimelineGate": "Audio Timeline Gate (continue/stop)",
"IAMCCS_LTX2_LastFrameBridgeSave": "LTX-2 Last Frame Bridge Save 🖼️💾",
"IAMCCS_LTX2_BlendLatentBridge": "LTX-2 Blend Latent Bridge 🎚️",
"IAMCCS_LTX2_LastFrameBridgeLoad": "LTX-2 Last Frame Bridge Load 🖼️",
"IAMCCS_LTX2_LoadLatentBridge": "LTX-2 Load Latent Bridge 🧬",
"IAMCCS_LTX2_LongVideoWrapperPrep": "LTX-2 Long Video Wrapper Prep 🧰",
"IAMCCS_LTX2_LongVideoWrapperPrepDisk": "LTX-2 Long Video Wrapper Prep (Disk) 💾🧰",
"IAMCCS_LTX2_SaveLatentBridge": "LTX-2 Save Latent Bridge 💾",
"IAMCCS_LTX2_SegmentQueueLoop": "LTX-2 Segment Queue Loop 🔁",
"IAMCCS_ImageResizeBatchSafe": "Image Resize Batch Safe (IAMCCS)",
"IAMCCS_LoadResizeSegmentFromDir": "Load + Resize Segment From Dir 🧰",
# QwenVL FLF
**({"IAMCCS_QWEN_VL_FLF": "QwenVL FLF — First/Last Frame Prompt 🎬",
"IAMCCS_QWEN_VL_FLF_Advanced": "QwenVL FLF — First/Last Frame Prompt (Advanced) 🎬",
} if IAMCCS_QWEN_VL_FLF is not None else {}),
}
WEB_DIRECTORY = "./web"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
def _print_startup_banner() -> None:
# Print once per process.
if getattr(_print_startup_banner, "_done", False):
return
_print_startup_banner._done = True # type: ignore[attr-defined]
banner = r"""
___ _ __ __ ____ ____ ____ ____ _
|_ _| / \ | \/ |/ ___/ ___|/ ___| | _ \ ___ ___| | _____
| | / _ \ | |\/| | | | | \___ \ | |_) / _ \ / __| |/ / __|
| | / ___ \| | | | |__| |___ ___) || __/ (_) | (__| <\__ \
|___/_/ \_\_| |_|\____\____||____/ |_| \___/ \___|_|\_\___/
"""
log = logging.getLogger("IAMCCS")
log.info("%s", banner)
log.info("by IAMCCS (follow me on patreon.com/IAMCCS or carminecristalloscalzi.com)")
try:
keys = sorted(list(NODE_CLASS_MAPPINGS.keys()))
log.info("IAMCCS nodes loaded: %d", len(keys))
# Keep log readable: print in chunks.
chunk = []
for k in keys:
chunk.append(k)
if len(chunk) >= 10:
log.info("- %s", ", ".join(chunk))
chunk = []
if chunk:
log.info("- %s", ", ".join(chunk))
except Exception:
pass
def setup_api_routes() -> None:
"""IAMCCS API routes used by frontend widgets."""
try:
from server import PromptServer
from aiohttp import web
from .iamccs_hw_probe import recommend_settings
routes = PromptServer.instance.routes
@routes.get("/api/iamccs/hw_probe")
async def iamccs_hw_probe_endpoint(request):
try:
q = request.rel_url.query
def _to_int(x):
try:
return int(float(x))
except Exception:
return None
def _to_float(x):
try:
return float(x)
except Exception:
return None
width = _to_int(q.get("width"))
height = _to_int(q.get("height"))
frames = _to_int(q.get("frames"))
fps = _to_float(q.get("fps"))
data = recommend_settings(width=width, height=height, frames=frames, fps=fps)
logging.getLogger("IAMCCS.API").info(
"[iamccs/hw_probe] cuda=%s vram_gb=%s ram_gb=%s profile=%s vae_tile=%s frames=%s fps=%s",
data.get("hardware", {}).get("cuda_available"),
data.get("hardware", {}).get("cuda_total_vram_gb"),
data.get("hardware", {}).get("system_ram_gb"),
data.get("recommendations", {}).get("hw_supporter", {}).get("profile"),
data.get("recommendations", {}).get("vae_decode", {}).get("tile_size"),
frames,
fps,
)
return web.json_response(data)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
except Exception as e:
# Never hard-fail ComfyUI startup due to optional API endpoints.
logging.getLogger("IAMCCS.API").warning("Could not setup IAMCCS API routes: %r", e)
# Setup API routes when extension loads
setup_api_routes()
# Print banner after we are fully imported and mappings exist.
_print_startup_banner()
def _iamccs_install_ltx2_vae_encode_autofix() -> None:
"""Prevents hard-crash when LTX-2 VAE receives invalid frame counts.
Lightricks video VAE encode requires a frame count of the form 1 + 8*x.
Some workflows can produce off-by-a-few batches (e.g. 240 instead of 241),
which otherwise raises ValueError and stops execution.
This patch pads by repeating the last frame up to the next valid count.
Opt-in via IAMCCS_LTX2_VAE_ENCODE_AUTOFIX=1.
"""
# Default OFF: user requested workflow-level fixes without monkeypatching VAE.
if str(os.getenv("IAMCCS_LTX2_VAE_ENCODE_AUTOFIX", "0")).strip().lower() in {"0", "false", "no", "off"}:
return
log = logging.getLogger("IAMCCS.LTX2.VAE")
try:
import torch
except Exception:
return
try:
from comfy.ldm.lightricks.vae import causal_video_autoencoder as _cvae
except Exception:
# ComfyUI / LTXVideo not installed or import path changed.
return
cls = getattr(_cvae, "CausalVideoAutoencoder", None)
if cls is None:
return
orig_encode = getattr(cls, "encode", None)
if orig_encode is None:
return
if getattr(orig_encode, "__iamccs_ltx2_autofix__", False):
return
def _round_up_8n1(frames: int) -> int:
frames = int(frames)
if frames <= 1:
return 1
rem = (frames - 1) % 8
if rem == 0:
return frames
return frames + (8 - rem)
def _is_valid_8n1(frames: int) -> bool:
frames = int(frames)
return frames >= 1 and (frames - 1) % 8 == 0
def _pad_repeat_last(x: "torch.Tensor", dim: int, pad: int) -> "torch.Tensor":
# Take last slice along `dim` (keeps dimension) and repeat it `pad` times.
slc = [slice(None)] * x.ndim
slc[dim] = slice(-1, None)
last = x[tuple(slc)]
reps = [1] * x.ndim
reps[dim] = int(pad)
last_rep = last.repeat(*reps)
return torch.cat([x, last_rep], dim=dim)
def _candidate_frame_dims(x: "torch.Tensor") -> list[int]:
# Most common layouts:
# - (B, C, T, H, W) -> frames dim = 2
# - (T, H, W, C) -> frames dim = 0 (ComfyUI IMAGE batches)
# We only try dims that are >1 and *not obviously channels*.
dims: list[int] = []
if x.ndim == 5:
# Prefer T, then fallbacks
dims = [2, 0, 1]
elif x.ndim == 4:
dims = [0]
else:
dims = [0]
out: list[int] = []
for d in dims:
try:
size = int(x.shape[d])
except Exception:
continue
if size <= 1:
continue
# Heuristic: channels are usually small (1..4). Don't treat that as frames.
if size in (1, 2, 3, 4) and x.ndim >= 4 and d in (1, 3):
continue
out.append(d)
# Ensure uniqueness, preserve order
seen = set()
unique: list[int] = []
for d in out:
if d in seen:
continue
seen.add(d)
unique.append(d)
return unique
def encode_patched(self, pixels_in: "torch.Tensor"):
try:
return orig_encode(self, pixels_in)
except ValueError as e:
msg = str(e)
if "Invalid number of frames" not in msg:
raise
if not isinstance(pixels_in, torch.Tensor) or pixels_in.ndim < 4:
raise
# Try padding along the most likely frame dimension(s).
last_err: Exception | None = e
for dim in _candidate_frame_dims(pixels_in):
frames_in = int(pixels_in.shape[dim])
if _is_valid_8n1(frames_in):
continue
frames_fixed = _round_up_8n1(frames_in)
pad = frames_fixed - frames_in
if pad <= 0:
continue
try:
pixels_fixed = _pad_repeat_last(pixels_in, dim=dim, pad=pad)
log.warning(
"[LTX2 VAE encode autofix] Padded frames dim=%d %d -> %d (pad=%d) to satisfy 1+8*x rule",
dim,
frames_in,
frames_fixed,
pad,
)
return orig_encode(self, pixels_fixed)
except Exception as ee:
last_err = ee
continue
# If all attempts failed, re-raise the original ValueError.
raise e
encode_patched.__iamccs_ltx2_autofix__ = True
setattr(cls, "encode", encode_patched)
_iamccs_install_ltx2_vae_encode_autofix()
@@ -0,0 +1 @@
Goal 1 AU+IMG2VID modular nodes bootstrap: planner, audio timeline, continuity, finalize contracts.
@@ -0,0 +1,560 @@
// IAMCCS LTX-2 seconds <-> length sync
// Frontend-only helper for:
// - IAMCCS_LTX2_TimeFrameCount
// - IAMCCS_LTX2_Validator
// Uses FPS to convert:
// length(frames) = 1 + seconds * fps
// seconds = (length-1) / fps
import { app } from "../../scripts/app.js";
const TIMEFRAME_TYPE = "IAMCCS_LTX2_TimeFrameCount";
const VALIDATOR_TYPE = "IAMCCS_LTX2_Validator";
const FRAMERATE_SYNC_TYPE = "IAMCCS_LTX2_FrameRateSync";
const SEGMENT_PLANNER_TYPE = "IAMCCS_SegmentPlanner";
const SEGMENT_PLANNER_LINKED_TYPE = "IAMCCS_SegmentPlannerLinked";
const SEGMENT_PLANNER_SETTINGS_TYPE = "IAMCCS_SegmentPlannerSettings";
const SEGMENT_PLANNER_PREVIEW_WIDGET = "iamccs_segmentplanner_live_preview";
const SEGMENT_PLANNER_DETAILS_WIDGET = "iamccs_segmentplanner_live_details";
const SEGMENT_PLANNER_COPY_BUTTON = "iamccs_segmentplanner_copy_button";
const SEGMENT_PLANNER_SETTINGS_REPORT_WIDGET = "iamccs_segmentplanner_settings_live_report";
console.log("[IAMCCS LTX2] Loading seconds/length sync...");
function getWidget(node, name) {
if (!node?.widgets?.length) return null;
return node.widgets.find(w => w?.name === name || w?.label === name) || null;
}
function getInput(node, name) {
if (!node?.inputs?.length) return null;
return node.inputs.find(i => i?.name === name) || null;
}
function getNodeById(nodeId) {
if (nodeId == null) return null;
return app?.graph?._nodes_by_id?.[nodeId] || app?.graph?._nodes?.find(n => n?.id === nodeId) || null;
}
function getLinkedSource(node, inputName) {
const input = getInput(node, inputName);
const linkId = input?.link;
if (linkId == null) return null;
const link = app?.graph?.links?.[linkId];
if (!link) return null;
const originId = Array.isArray(link) ? link[1] : link.origin_id;
const originSlot = Array.isArray(link) ? link[2] : link.origin_slot;
const sourceNode = getNodeById(originId);
if (!sourceNode) return null;
// Defensive: outputs must exist and be an array, originSlot must be valid
let sourceOutput = null;
if (Array.isArray(sourceNode.outputs) && originSlot != null && originSlot >= 0 && originSlot < sourceNode.outputs.length) {
sourceOutput = sourceNode.outputs[originSlot];
}
return {
sourceNode,
sourceOutput,
};
}
function getLinkedPrimitiveValue(node, inputName) {
const source = getLinkedSource(node, inputName);
if (!source?.sourceNode) return null;
const sourceNode = source.sourceNode;
const outputName = String(source.sourceOutput?.name || "");
const candidateWidgetNames = [outputName, inputName, "value", "float", "int", "string", "text"];
// Defensive: try to get value from output if present
if (source.sourceOutput && typeof source.sourceOutput.value !== "undefined") {
return source.sourceOutput.value;
}
for (const widgetName of candidateWidgetNames) {
const widget = getWidget(sourceNode, widgetName);
if (widget?.value != null) return widget.value;
}
if (Array.isArray(sourceNode.widgets) && sourceNode.widgets.length > 0) {
const widgetValue = sourceNode.widgets[0]?.value;
if (widgetValue != null) return widgetValue;
}
return null;
}
function clampNumber(v, min, max) {
const n = Number(v);
if (!Number.isFinite(n)) return min;
return Math.max(min, Math.min(max, n));
}
function getGraphFpsOrDefault(defaultFps = 25) {
try {
const nodes = app?.graph?._nodes || [];
const fr = nodes.find(n => n?.type === FRAMERATE_SYNC_TYPE);
if (!fr) return defaultFps;
const wFps = getWidget(fr, "fps");
const wMode = getWidget(fr, "int_mode");
const fpsIn = clampNumber(wFps?.value ?? defaultFps, 1.0, 240.0);
const mode = String(wMode?.value || "round");
if (mode === "floor") return Math.max(1, Math.floor(fpsIn));
if (mode === "ceil") return Math.max(1, Math.ceil(fpsIn));
if (mode === "fixed") return Math.max(1, Math.round(fpsIn));
// round
return Math.max(1, Math.round(fpsIn));
} catch (e) {
return defaultFps;
}
}
function snapLengthToLtx2RuleUp(length) {
// LTX-2 constraint: 1 + 8*x frames
const n = Math.max(1, Math.round(Number(length) || 1));
const rem = (n - 1) % 8;
if (rem === 0) return n;
return n + (8 - rem);
}
function snapLengthToLtx2Rule(length, mode = "up") {
const n = Math.max(1, Math.round(Number(length) || 1));
const rem = (n - 1) % 8;
if (rem === 0) return n;
const down = Math.max(1, n - rem);
const up = n + (8 - rem);
if (mode === "down") return down;
if (mode === "nearest") return (up - n) <= (n - down) ? up : down;
return up;
}
function ensurePlannerPreviewWidget(node) {
let widget = getWidget(node, SEGMENT_PLANNER_PREVIEW_WIDGET);
if (widget) return widget;
widget = node.addWidget("text", "Live Preview", "", () => {}, { multiline: true });
widget.name = SEGMENT_PLANNER_PREVIEW_WIDGET;
widget.serialize = false;
try {
widget.inputEl?.setAttribute?.("readonly", true);
} catch {
// ignore
}
return widget;
}
function ensurePlannerDetailsWidget(node) {
let widget = getWidget(node, SEGMENT_PLANNER_DETAILS_WIDGET);
if (widget) return widget;
widget = node.addWidget("text", "Plan Details", "", () => {}, { multiline: true });
widget.name = SEGMENT_PLANNER_DETAILS_WIDGET;
widget.serialize = false;
try {
widget.inputEl?.setAttribute?.("readonly", true);
} catch {
// ignore
}
return widget;
}
function ensurePlannerCopyButton(node) {
let widget = getWidget(node, SEGMENT_PLANNER_COPY_BUTTON);
if (widget) return widget;
widget = node.addWidget("button", "Copy Planner Report", null, async () => {
try {
const summary = String(node.properties?.iamccs_segmentplanner_live_preview || "");
const details = String(node.properties?.iamccs_segmentplanner_live_details || "");
const text = [summary, details].filter(Boolean).join("\n\n");
await navigator.clipboard.writeText(text);
} catch {
// ignore
}
});
widget.name = SEGMENT_PLANNER_COPY_BUTTON;
widget.serialize = false;
return widget;
}
function ensurePlannerSettingsReportWidget(node) {
let widget = getWidget(node, SEGMENT_PLANNER_SETTINGS_REPORT_WIDGET);
if (widget) return widget;
widget = node.addWidget("text", "Live Settings", "", () => {}, { multiline: true });
widget.name = SEGMENT_PLANNER_SETTINGS_REPORT_WIDGET;
widget.serialize = false;
try {
widget.inputEl?.setAttribute?.("readonly", true);
} catch {
// ignore
}
return widget;
}
function ensurePlannerNodeSize(node) {
try {
const width = Math.max(460, Number(node.size?.[0] || 0));
const height = Math.max(node?.type === SEGMENT_PLANNER_SETTINGS_TYPE ? 300 : 520, Number(node.size?.[1] || 0));
node.size = [width, height];
} catch {
// ignore
}
}
function refreshLinkedPlannerPreviews() {
try {
const nodes = app?.graph?._nodes || [];
for (const node of nodes) {
if (node?.type === SEGMENT_PLANNER_LINKED_TYPE) {
updateSegmentPlannerPreview(node);
}
}
} catch {
// ignore
}
}
function updateSegmentPlannerSettingsReport(node) {
const wSeg = getWidget(node, "segment_duration_s");
const wPlanning = getWidget(node, "planning_mode");
const wProfile = getWidget(node, "segment_preset") || getWidget(node, "content_profile");
const wOverlap = getWidget(node, "overlap_frames");
const wAutoSync = getWidget(node, "auto_sync_overlap");
if (!wSeg || !wPlanning || !wProfile || !wOverlap || !wAutoSync) return;
const segmentDuration = clampNumber(wSeg.value, 0.01, 3600.0);
const planningMode = String(wPlanning.value || "manual_segment_seconds");
const segmentPreset = String(wProfile.value || "10sec");
const autoSyncOverlap = Boolean(wAutoSync.value);
const rec = getPlannerProfile(segmentPreset);
const explicitPresetMode = planningMode === "explicit_preset_seconds" || planningMode === "auto_profile";
const autoDurationProfile = !explicitPresetMode ? getAutoOverlapProfileForDuration(segmentDuration) : null;
const effectiveSegmentDuration = explicitPresetMode ? rec.segmentSeconds : segmentDuration;
const effectiveOverlapFrames = explicitPresetMode
? rec.overlap
: (autoSyncOverlap ? (autoDurationProfile?.overlap ?? clampNumber(wOverlap.value, 0, 4096)) : clampNumber(wOverlap.value, 0, 4096));
if (autoSyncOverlap && Number(wOverlap.value) !== effectiveOverlapFrames) {
wOverlap.value = effectiveOverlapFrames;
}
const report = [
`segment_duration_s = ${effectiveSegmentDuration.toFixed(3)}`,
`planning_mode = ${planningMode}`,
`segment_preset = ${segmentPreset}`,
`overlap_frames = ${effectiveOverlapFrames}`,
`auto_sync_overlap = ${autoSyncOverlap}`,
`auto_duration_profile = ${autoDurationProfile?.profileLabel || "none"}`,
`recommended_extension_preset = ${rec.preset}`,
].join("\n");
const widget = ensurePlannerSettingsReportWidget(node);
widget.value = report;
node.properties = node.properties || {};
node.properties.iamccs_segmentplanner_settings_live_report = report;
ensurePlannerNodeSize(node);
try {
node.setDirtyCanvas(true, true);
} catch {
// ignore
}
}
function getPlannerProfile(presetOrLegacyProfile) {
const value = String(presetOrLegacyProfile || "15sec");
if (value === "20sec") {
return { segmentSeconds: 20.0, overlap: 17, leftContext: 1.0, preset: "monologue_audio_24fps", profileLabel: "20sec" };
}
if (value === "15sec" || value === "monologue") {
return { segmentSeconds: 15.0, overlap: 13, leftContext: 0.75, preset: "monologue_audio_24fps", profileLabel: "15sec" };
}
if (value === "5sec") {
return { segmentSeconds: 5.0, overlap: 5, leftContext: 0.25, preset: "videoclip_audio_24fps", profileLabel: "5sec" };
}
return { segmentSeconds: 10.0, overlap: 9, leftContext: 0.5, preset: "videoclip_audio_24fps", profileLabel: "10sec" };
}
function getAutoOverlapProfileForDuration(segmentDuration) {
const duration = Number(segmentDuration);
if (!Number.isFinite(duration)) return null;
const profiles = [
getPlannerProfile("5sec"),
getPlannerProfile("10sec"),
getPlannerProfile("15sec"),
getPlannerProfile("20sec"),
];
return profiles.find(profile => Math.abs(duration - profile.segmentSeconds) <= 0.1) || null;
}
function updateSegmentPlannerPreview(node) {
const wSong = getWidget(node, "song_duration_s");
const wFps = getWidget(node, "fps");
const wSeg = getWidget(node, "segment_duration_s");
const wPlanning = getWidget(node, "planning_mode") || getWidget(node, "planning_mode_in");
const wProfile = getWidget(node, "segment_preset") || getWidget(node, "segment_preset_in") || getWidget(node, "content_profile");
const wOverlap = getWidget(node, "overlap_frames") || getWidget(node, "overlap_frames_in");
const wRound = getWidget(node, "ltx_round_mode");
const wIndex = getWidget(node, "segment_index");
if (!wSong || !wFps || !wSeg || !wPlanning || !wProfile || !wOverlap || !wRound || !wIndex) return;
const songDuration = clampNumber(wSong.value, 0.01, 36000);
const fps = clampNumber(wFps.value, 0.001, 240.0);
const linkedSegmentDuration = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "segment_duration_s") : null;
const linkedPlanningMode = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "planning_mode_in") : null;
const linkedSegmentPreset = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "segment_preset_in") : null;
const linkedOverlapFrames = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "overlap_frames_in") : null;
const segmentDuration = clampNumber(linkedSegmentDuration ?? wSeg.value, 0.01, 3600.0);
const planningMode = String(linkedPlanningMode ?? wPlanning.value || "manual_segment_seconds");
const segmentPreset = String(linkedSegmentPreset ?? wProfile.value || "15sec");
const inputOverlapFrames = clampNumber(linkedOverlapFrames ?? wOverlap.value, 0, 4096);
const roundMode = String(wRound.value || "up");
const segmentIndex = Math.max(0, Math.trunc(Number(wIndex.value || 0)));
const rec = getPlannerProfile(segmentPreset);
const explicitPresetMode = planningMode === "explicit_preset_seconds" || planningMode === "auto_profile";
const autoDurationProfile = !explicitPresetMode ? getAutoOverlapProfileForDuration(segmentDuration) : null;
const effectiveSegmentDuration = explicitPresetMode ? rec.segmentSeconds : segmentDuration;
const effectiveOverlapFrames = explicitPresetMode
? rec.overlap
: (autoDurationProfile?.overlap ?? inputOverlapFrames);
if (Number(wOverlap.value) !== effectiveOverlapFrames) {
wOverlap.value = effectiveOverlapFrames;
}
const overlapFrames = clampNumber(wOverlap.value, 0, 4096);
const totalFrames = Math.max(1, Math.round(songDuration * fps));
const uniqueFrames = Math.max(1, Math.round(effectiveSegmentDuration * fps));
const firstRaw = snapLengthToLtx2Rule(uniqueFrames, roundMode);
const nextRaw = snapLengthToLtx2Rule(uniqueFrames + effectiveOverlapFrames, roundMode);
const segments = Math.max(1, Math.ceil(totalFrames / uniqueFrames));
const loops = Math.max(0, segments - 1);
const usedBeforeLast = uniqueFrames * Math.max(0, segments - 1);
const lastUnique = Math.max(1, totalFrames - usedBeforeLast);
const clampedIndex = Math.min(segmentIndex, Math.max(0, segments - 1));
const currentStart = uniqueFrames * clampedIndex;
const currentUnique = Math.max(1, Math.min(uniqueFrames, totalFrames - currentStart));
const currentEnd = Math.min(totalFrames, currentStart + currentUnique);
const currentRemaining = Math.max(0, totalFrames - currentEnd);
const currentRaw = clampedIndex === 0 ? firstRaw : nextRaw;
const currentStartS = currentStart / fps;
const currentEndS = currentEnd / fps;
const summary = [
`total=${totalFrames}f`,
`unique=${uniqueFrames}f`,
`mode=${planningMode}`,
`profile=${segmentPreset}`,
`first_raw=${firstRaw}f`,
`next_raw=${nextRaw}f`,
`segments=${segments}`,
`loops=${loops}`,
`last_unique=${lastUnique}f`,
`seg=${clampedIndex}`,
`cur_raw=${currentRaw}f`,
`cur_unique=${currentUnique}f`,
`cur=[${currentStart}..${currentEnd})`,
`remain=${currentRemaining}f`,
].join(" | ");
const details = [
`song_duration_s = ${songDuration.toFixed(3)}`,
`fps = ${fps.toFixed(3)}`,
`segment_duration_s = ${segmentDuration.toFixed(3)}`,
`effective_segment_duration_s = ${effectiveSegmentDuration.toFixed(3)}`,
`planning_mode = ${planningMode}`,
`segment_preset = ${segmentPreset}`,
`overlap_frames = ${overlapFrames}`,
`effective_overlap_frames = ${effectiveOverlapFrames}`,
`ltx_round_mode = ${roundMode}`,
`segment_index = ${clampedIndex}`,
`total_frames = ${totalFrames}`,
`unique_segment_frames = ${uniqueFrames}`,
`first_segment_raw_frames = ${firstRaw}`,
`continuation_raw_frames = ${nextRaw}`,
`estimated_segments = ${segments}`,
`continuation_loops = ${loops}`,
`last_segment_unique_frames = ${lastUnique}`,
`current_segment_raw_frames = ${currentRaw}`,
`current_segment_unique_frames = ${currentUnique}`,
`current_segment_start_frames = ${currentStart}`,
`current_segment_end_frames = ${currentEnd}`,
`current_remaining_frames_after = ${currentRemaining}`,
`current_segment_start_s = ${currentStartS.toFixed(3)}`,
`current_segment_end_s = ${currentEndS.toFixed(3)}`,
`recommended_audio_left_context_s = ${rec.leftContext.toFixed(2)}`,
`recommended_extension_preset = ${rec.preset}`,
`auto_duration_profile = ${autoDurationProfile?.profileLabel || "none"}`,
].join("\n");
const widget = ensurePlannerPreviewWidget(node);
widget.value = summary;
const detailsWidget = ensurePlannerDetailsWidget(node);
detailsWidget.value = details;
ensurePlannerCopyButton(node);
ensurePlannerNodeSize(node);
node.properties = node.properties || {};
node.properties.iamccs_segmentplanner_live_preview = summary;
node.properties.iamccs_segmentplanner_live_details = details;
try {
node.setDirtyCanvas(true, true);
} catch {
// ignore
}
}
function installSegmentPlannerSync(node) {
const widgets = [
getWidget(node, "song_duration_s"),
getWidget(node, "fps"),
getWidget(node, "segment_duration_s"),
getWidget(node, "planning_mode") || getWidget(node, "planning_mode_in"),
getWidget(node, "segment_preset") || getWidget(node, "segment_preset_in") || getWidget(node, "content_profile"),
getWidget(node, "overlap_frames") || getWidget(node, "overlap_frames_in"),
getWidget(node, "ltx_round_mode"),
getWidget(node, "segment_index"),
].filter(Boolean);
if (!widgets.length || node._iamccsSegmentPlannerSyncInstalled) {
updateSegmentPlannerPreview(node);
return;
}
node._iamccsSegmentPlannerSyncInstalled = true;
ensurePlannerPreviewWidget(node);
ensurePlannerDetailsWidget(node);
ensurePlannerCopyButton(node);
ensurePlannerNodeSize(node);
for (const widget of widgets) {
const prev = widget.callback;
widget.callback = function () {
const r = prev?.apply(this, arguments);
updateSegmentPlannerPreview(node);
return r;
};
}
updateSegmentPlannerPreview(node);
}
function installSegmentPlannerSettingsSync(node) {
const widgets = [
getWidget(node, "segment_duration_s"),
getWidget(node, "planning_mode"),
getWidget(node, "segment_preset"),
getWidget(node, "overlap_frames"),
getWidget(node, "auto_sync_overlap"),
].filter(Boolean);
if (!widgets.length || node._iamccsSegmentPlannerSettingsSyncInstalled) {
updateSegmentPlannerSettingsReport(node);
return;
}
node._iamccsSegmentPlannerSettingsSyncInstalled = true;
ensurePlannerSettingsReportWidget(node);
ensurePlannerNodeSize(node);
for (const widget of widgets) {
const prev = widget.callback;
widget.callback = function () {
const r = prev?.apply(this, arguments);
updateSegmentPlannerSettingsReport(node);
refreshLinkedPlannerPreviews();
return r;
};
}
updateSegmentPlannerSettingsReport(node);
refreshLinkedPlannerPreviews();
}
function installSecondsLengthSync(node, { snapRule = false } = {}) {
const wSeconds = getWidget(node, "seconds");
const wLength = getWidget(node, "length");
if (!wSeconds || !wLength) return;
let updating = false;
const updateLengthFromSeconds = () => {
const fps = getGraphFpsOrDefault(25);
const seconds = clampNumber(wSeconds.value, 0.01, 3600);
let length = clampNumber(1 + Math.round(seconds * fps), 1, 16385);
if (snapRule) length = snapLengthToLtx2RuleUp(length);
wLength.value = length;
};
const updateSecondsFromLength = () => {
const fps = getGraphFpsOrDefault(25);
const length = clampNumber(wLength.value, 1, 16385);
const seconds = (length - 1) / fps;
// keep precision consistent with widget step 0.01
wSeconds.value = Math.round(clampNumber(seconds, 0.01, 3600) * 100) / 100;
};
const hookWidget = (widget, fn) => {
const prev = widget.callback;
widget.callback = function () {
const r = prev?.apply(this, arguments);
if (updating) return r;
updating = true;
try {
fn();
node.setDirtyCanvas(true, true);
} finally {
updating = false;
}
return r;
};
};
hookWidget(wSeconds, updateLengthFromSeconds);
hookWidget(wLength, updateSecondsFromLength);
}
app.registerExtension({
name: "iamccs.ltx2.time_length_sync",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!nodeData?.name) return;
const name = nodeData.name;
if (name !== TIMEFRAME_TYPE && name !== VALIDATOR_TYPE && name !== SEGMENT_PLANNER_TYPE && name !== SEGMENT_PLANNER_LINKED_TYPE && name !== SEGMENT_PLANNER_SETTINGS_TYPE) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated?.apply(this, arguments);
if (name === SEGMENT_PLANNER_TYPE || name === SEGMENT_PLANNER_LINKED_TYPE) {
installSegmentPlannerSync(this);
} else if (name === SEGMENT_PLANNER_SETTINGS_TYPE) {
installSegmentPlannerSettingsSync(this);
} else {
// Snap to 8n+1 on both nodes, since LTX-2 VAE encode requires it.
installSecondsLengthSync(this, { snapRule: true });
}
return r;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const r = onConfigure?.apply(this, arguments);
if (name === SEGMENT_PLANNER_TYPE || name === SEGMENT_PLANNER_LINKED_TYPE) {
installSegmentPlannerSync(this);
updateSegmentPlannerPreview(this);
} else if (name === SEGMENT_PLANNER_SETTINGS_TYPE) {
installSegmentPlannerSettingsSync(this);
updateSegmentPlannerSettingsReport(this);
}
return r;
};
},
});
@@ -0,0 +1,577 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
const NODE_GROUPS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": [
{ key: "timeline", label: "Timeline", color: "#2d6f63", widgets: ["fps", "segment_seconds"] },
{ key: "planning", label: "Planning", color: "#6d5d2a", widgets: ["planning_mode", "content_profile", "overlap_frames", "ltx_round_mode"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "prompts", label: "Prompting", color: "#6b4f2a", widgets: ["positive_text", "negative_text"] },
{ key: "video", label: "Video", color: "#2f4d7a", widgets: ["width", "height"] },
{ key: "sampling", label: "Sampling", color: "#5b3a73", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression"] },
{ key: "audio", label: "Audio", color: "#2f6a47", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] },
{ key: "stitch", label: "Stitch", color: "#7a4f2f", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule"] },
{ key: "anchor", label: "Anchor", color: "#8a4d4d", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] },
{ key: "modular", label: "Modular", color: "#35566e", widgets: ["modular_decode", "downstream_stage_mode", "output_root"] },
{ key: "debug", label: "Debug", color: "#5b5b2f", widgets: ["segment_overlay_mode", "segment_overlay_text"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ key: "stage2", label: "Second Stage", color: "#6f4a7f", widgets: ["second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"] },
],
IAMCCS_GC_AudioConcatSupernode: [
{ key: "concat", label: "Audio Concat", color: "#3e6f55", widgets: ["concat_mode", "clip_durations_seconds", "gap_seconds", "intro_seconds", "outro_seconds"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "decode", label: "Decode", color: "#3e5f82", widgets: ["frame_rate", "decode_mode", "output_root", "frames_subdir", "image_format", "jpg_quality", "tiled_tile_size", "tiled_overlap"] },
{ key: "final", label: "Finalize", color: "#6c5330", widgets: ["filename_prefix", "crf", "pix_fmt", "trim_to_audio", "save_metadata"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#556677", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, 28];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#405060", color: "#f4f7fa" };
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
button.name = `${isExpanded ? "▼" : "▶"} ${group.label}`;
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// 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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(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);
};
}
}
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
const result = onConfigure?.apply(this, arguments);
enhanceNodeLayout(this, nodeName);
rehydrateSerializedWidgets(this, info?.widgets_values);
refreshNodeLayoutState(this, nodeName);
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,522 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
const NODE_GROUPS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": [
{ key: "timeline", label: "Timeline", color: "#2d6f63", widgets: ["fps", "segment_seconds"] },
{ key: "planning", label: "Planning", color: "#6d5d2a", widgets: ["planning_mode", "content_profile", "overlap_frames", "ltx_round_mode"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "prompts", label: "Prompting", color: "#6b4f2a", widgets: ["positive_text", "negative_text"] },
{ key: "video", label: "Video", color: "#2f4d7a", widgets: ["width", "height"] },
{ key: "sampling", label: "Sampling", color: "#5b3a73", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression"] },
{ key: "audio", label: "Audio", color: "#2f6a47", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] },
{ key: "stitch", label: "Stitch", color: "#7a4f2f", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule"] },
{ key: "anchor", label: "Anchor", color: "#8a4d4d", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] },
{ key: "modular", label: "Modular", color: "#35566e", widgets: ["modular_decode", "downstream_stage_mode", "output_root"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ key: "stage2", label: "Second Stage", color: "#6f4a7f", widgets: ["second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"] },
],
IAMCCS_GC_AudioConcatSupernode: [
{ key: "concat", label: "Audio Concat", color: "#3e6f55", widgets: ["concat_mode", "clip_durations_seconds", "gap_seconds", "intro_seconds", "outro_seconds"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "decode", label: "Decode", color: "#3e5f82", widgets: ["frame_rate", "decode_mode", "output_root", "frames_subdir", "image_format", "jpg_quality", "tiled_tile_size", "tiled_overlap"] },
{ key: "final", label: "Finalize", color: "#6c5330", widgets: ["filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#556677", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
fitNodeToWidgets(node);
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, 28];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#405060", color: "#f4f7fa" };
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
button.name = `${isExpanded ? "▼" : "▶"} ${group.label}`;
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// 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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(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);
};
}
}
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,609 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
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"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#557ea6", widgets: ["ui_preset"] },
{ 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", "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: ["modular_decode", "downstream_stage_mode", "output_root"] },
{ key: "debug", label: "Debug", color: "#8a8a36", widgets: ["segment_overlay_mode", "segment_overlay_text"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ 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_GC_AudioConcatSupernode: [
{ 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"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#6a85a0", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
const SECTION_BUTTON_COLLAPSED_BG = "#22303f";
const SECTION_BUTTON_TEXT = "#f8fbff";
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, 34];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#4f6f8f", color: SECTION_BUTTON_TEXT };
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
button.name = `${isExpanded ? "▼" : "▶"} ${group.label} Section`;
button.options = {
bgcolor: isExpanded ? (group.color || "#4f6f8f") : SECTION_BUTTON_COLLAPSED_BG,
color: SECTION_BUTTON_TEXT,
};
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function applyPlannerModeVisibility(node) {
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);
fitNodeToWidgets(node);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
applyPlannerModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// 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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(this);
}
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
const planningModeWidget = findWidget(this, "planning_mode");
if (planningModeWidget) {
const originalCallback = planningModeWidget.callback;
planningModeWidget.callback = (...args) => {
originalCallback?.apply(planningModeWidget, args);
applyPlannerModeVisibility(this);
app.graph.setDirtyCanvas(true, true);
};
}
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);
};
}
}
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
const result = onConfigure?.apply(this, arguments);
enhanceNodeLayout(this, nodeName);
rehydrateSerializedWidgets(this, info?.widgets_values);
refreshNodeLayoutState(this, nodeName);
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,577 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
const NODE_GROUPS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": [
{ key: "timeline", label: "Timeline", color: "#2d6f63", widgets: ["fps", "segment_seconds"] },
{ key: "planning", label: "Planning", color: "#6d5d2a", widgets: ["planning_mode", "segment_preset", "overlap_frames", "ltx_round_mode"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "prompts", label: "Prompting", color: "#6b4f2a", widgets: ["positive_text", "negative_text"] },
{ key: "video", label: "Video", color: "#2f4d7a", widgets: ["width", "height"] },
{ key: "sampling", label: "Sampling", color: "#5b3a73", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression"] },
{ key: "audio", label: "Audio", color: "#2f6a47", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] },
{ key: "stitch", label: "Stitch", color: "#7a4f2f", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule"] },
{ key: "anchor", label: "Anchor", color: "#8a4d4d", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] },
{ key: "modular", label: "Modular", color: "#35566e", widgets: ["modular_decode", "downstream_stage_mode", "output_root"] },
{ key: "debug", label: "Debug", color: "#5b5b2f", widgets: ["segment_overlay_mode", "segment_overlay_text"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ key: "stage2", label: "Second Stage", color: "#6f4a7f", widgets: ["second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"] },
],
IAMCCS_GC_AudioConcatSupernode: [
{ key: "concat", label: "Audio Concat", color: "#3e6f55", widgets: ["concat_mode", "clip_durations_seconds", "gap_seconds", "intro_seconds", "outro_seconds"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "decode", label: "Decode", color: "#3e5f82", widgets: ["frame_rate", "decode_mode", "output_root", "frames_subdir", "image_format", "jpg_quality", "tiled_tile_size", "tiled_overlap"] },
{ key: "final", label: "Finalize", color: "#6c5330", widgets: ["filename_prefix", "crf", "pix_fmt", "trim_to_audio", "save_metadata"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#556677", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, 28];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#405060", color: "#f4f7fa" };
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
button.name = `${isExpanded ? "▼" : "▶"} ${group.label}`;
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// 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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(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);
};
}
}
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
const result = onConfigure?.apply(this, arguments);
enhanceNodeLayout(this, nodeName);
rehydrateSerializedWidgets(this, info?.widgets_values);
refreshNodeLayoutState(this, nodeName);
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,577 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
const NODE_GROUPS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": [
{ key: "timeline", label: "Timeline", color: "#2d6f63", widgets: ["fps", "segment_seconds"] },
{ key: "planning", label: "Planning", color: "#6d5d2a", widgets: ["planning_mode", "content_profile", "overlap_frames", "ltx_round_mode"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "prompts", label: "Prompting", color: "#6b4f2a", widgets: ["positive_text", "negative_text"] },
{ key: "video", label: "Video", color: "#2f4d7a", widgets: ["width", "height"] },
{ key: "sampling", label: "Sampling", color: "#5b3a73", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression"] },
{ key: "audio", label: "Audio", color: "#2f6a47", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] },
{ key: "stitch", label: "Stitch", color: "#7a4f2f", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule"] },
{ key: "anchor", label: "Anchor", color: "#8a4d4d", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] },
{ key: "modular", label: "Modular", color: "#35566e", widgets: ["modular_decode", "downstream_stage_mode", "output_root"] },
{ key: "debug", label: "Debug", color: "#5b5b2f", widgets: ["segment_overlay_mode", "segment_overlay_text"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ key: "stage2", label: "Second Stage", color: "#6f4a7f", widgets: ["second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"] },
],
IAMCCS_GC_AudioConcatSupernode: [
{ key: "concat", label: "Audio Concat", color: "#3e6f55", widgets: ["concat_mode", "clip_durations_seconds", "gap_seconds", "intro_seconds", "outro_seconds"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "decode", label: "Decode", color: "#3e5f82", widgets: ["frame_rate", "decode_mode", "output_root", "frames_subdir", "image_format", "jpg_quality", "tiled_tile_size", "tiled_overlap"] },
{ key: "final", label: "Finalize", color: "#6c5330", widgets: ["filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#556677", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, 28];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#405060", color: "#f4f7fa" };
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
button.name = `${isExpanded ? "▼" : "▶"} ${group.label}`;
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// 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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(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);
};
}
}
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
const result = onConfigure?.apply(this, arguments);
enhanceNodeLayout(this, nodeName);
rehydrateSerializedWidgets(this, info?.widgets_values);
refreshNodeLayoutState(this, nodeName);
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,602 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
const NODE_GROUPS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": [
{ key: "timeline", label: "Timeline", color: "#2d6f63", widgets: ["fps", "segment_seconds"] },
{ key: "planning", label: "Planning", color: "#6d5d2a", widgets: ["planning_mode", "segment_preset", "overlap_frames", "ltx_round_mode"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "prompts", label: "Prompting", color: "#6b4f2a", widgets: ["positive_text", "negative_text"] },
{ key: "video", label: "Video", color: "#2f4d7a", widgets: ["width", "height"] },
{ key: "sampling", label: "Sampling", color: "#5b3a73", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression"] },
{ key: "audio", label: "Audio", color: "#2f6a47", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] },
{ key: "stitch", label: "Stitch", color: "#7a4f2f", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule"] },
{ key: "anchor", label: "Anchor", color: "#8a4d4d", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] },
{ key: "modular", label: "Modular", color: "#35566e", widgets: ["modular_decode", "downstream_stage_mode", "output_root"] },
{ key: "debug", label: "Debug", color: "#5b5b2f", widgets: ["segment_overlay_mode", "segment_overlay_text"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ key: "stage2", label: "Second Stage", color: "#6f4a7f", widgets: ["second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"] },
],
IAMCCS_GC_AudioConcatSupernode: [
{ key: "concat", label: "Audio Concat", color: "#3e6f55", widgets: ["concat_mode", "clip_durations_seconds", "gap_seconds", "intro_seconds", "outro_seconds"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "decode", label: "Decode", color: "#3e5f82", widgets: ["frame_rate", "decode_mode", "output_root", "frames_subdir", "image_format", "jpg_quality", "tiled_tile_size", "tiled_overlap"] },
{ key: "final", label: "Finalize", color: "#6c5330", widgets: ["filename_prefix", "crf", "pix_fmt", "trim_to_audio", "save_metadata"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#556677", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, 28];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#405060", color: "#f4f7fa" };
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
button.name = `${isExpanded ? "▼" : "▶"} ${group.label}`;
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function applyPlannerModeVisibility(node) {
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);
fitNodeToWidgets(node);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
applyPlannerModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// 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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(this);
}
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
const planningModeWidget = findWidget(this, "planning_mode");
if (planningModeWidget) {
const originalCallback = planningModeWidget.callback;
planningModeWidget.callback = (...args) => {
originalCallback?.apply(planningModeWidget, args);
applyPlannerModeVisibility(this);
app.graph.setDirtyCanvas(true, true);
};
}
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);
};
}
}
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
const result = onConfigure?.apply(this, arguments);
enhanceNodeLayout(this, nodeName);
rehydrateSerializedWidgets(this, info?.widgets_values);
refreshNodeLayoutState(this, nodeName);
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,576 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
const NODE_GROUPS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Planner": [
{ key: "timeline", label: "Timeline", color: "#2d6f63", widgets: ["fps", "segment_seconds"] },
{ key: "planning", label: "Planning", color: "#6d5d2a", widgets: ["planning_mode", "content_profile", "overlap_frames", "ltx_round_mode"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "prompts", label: "Prompting", color: "#6b4f2a", widgets: ["positive_text", "negative_text"] },
{ key: "video", label: "Video", color: "#2f4d7a", widgets: ["width", "height"] },
{ key: "sampling", label: "Sampling", color: "#5b3a73", widgets: ["steps", "cfg", "sampler_name", "seed", "max_shift", "base_shift", "sigma_terminal", "manual_sigmas", "image_strength", "image_compression"] },
{ key: "audio", label: "Audio", color: "#2f6a47", widgets: ["audio_context_mode", "audio_left_context_s", "audio_right_context_s"] },
{ key: "stitch", label: "Stitch", color: "#7a4f2f", widgets: ["stitch_preset", "overlap_side", "overlap_mode", "start_frames_rule"] },
{ key: "anchor", label: "Anchor", color: "#8a4d4d", widgets: ["continuity_anchor_mode", "anchor_refresh_interval", "anchor_image_strength", "anti_drift_mode", "anti_drift_strength", "identity_persistence_strength"] },
{ key: "modular", label: "Modular", color: "#35566e", widgets: ["modular_decode", "downstream_stage_mode", "output_root"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ key: "stage2", label: "Second Stage", color: "#6f4a7f", widgets: ["second_stage_mode", "stage2_model_policy", "second_stage_upscale_model", "second_stage_reinject_strength", "second_stage_cfg", "second_stage_manual_sigmas"] },
],
IAMCCS_GC_AudioConcatSupernode: [
{ key: "concat", label: "Audio Concat", color: "#3e6f55", widgets: ["concat_mode", "clip_durations_seconds", "gap_seconds", "intro_seconds", "outro_seconds"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": [
{ key: "preset", label: "Preset", color: "#445c75", widgets: ["ui_preset"] },
{ key: "decode", label: "Decode", color: "#3e5f82", widgets: ["frame_rate", "decode_mode", "output_root", "frames_subdir", "image_format", "jpg_quality", "tiled_tile_size", "tiled_overlap"] },
{ key: "final", label: "Finalize", color: "#6c5330", widgets: ["filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#556677", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, 28];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#405060", color: "#f4f7fa" };
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
button.name = `${isExpanded ? "▼" : "▶"} ${group.label}`;
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// 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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(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);
};
}
}
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
const result = onConfigure?.apply(this, arguments);
enhanceNodeLayout(this, nodeName);
rehydrateSerializedWidgets(this, info?.widgets_values);
refreshNodeLayoutState(this, nodeName);
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,640 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
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"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#557ea6", widgets: ["ui_preset"] },
{ 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", "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: ["modular_decode", "downstream_stage_mode", "output_root"] },
{ key: "debug", label: "Debug", color: "#8a8a36", widgets: ["segment_overlay_mode", "segment_overlay_text"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ 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_GC_AudioConcatSupernode: [
{ 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"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#6a85a0", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
const SECTION_BUTTON_COLLAPSED_BG = "#22303f";
const SECTION_BUTTON_TEXT = "#f8fbff";
const SECTION_BUTTON_HEIGHT = 30;
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function setWidgetLabel(node, widgetName, label) {
const widget = findWidget(node, widgetName);
if (!widget) {
return;
}
widget.label = label;
}
function sectionButtonCaption(group, isExpanded) {
return `${isExpanded ? "▼" : "▶"} ${group.label}`;
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function applyRenderAnchorLabels(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") {
return;
}
setWidgetLabel(node, "continuity_anchor_mode", "Anchor Refresh (second stage only)");
setWidgetLabel(node, "anchor_refresh_interval", "Refresh Interval (second stage only)");
setWidgetLabel(node, "anchor_image_strength", "Anchor Guidance Strength (second stage only)");
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") {
applyRenderAnchorLabels(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, SECTION_BUTTON_HEIGHT];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#4f6f8f", color: SECTION_BUTTON_TEXT };
sectionButton.value = "";
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
const caption = sectionButtonCaption(group, isExpanded);
button.name = caption;
button.label = caption;
button.value = caption;
button.options = {
bgcolor: isExpanded ? (group.color || "#4f6f8f") : SECTION_BUTTON_COLLAPSED_BG,
color: SECTION_BUTTON_TEXT,
};
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function applyPlannerModeVisibility(node) {
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);
fitNodeToWidgets(node);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
applyPlannerModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
addPlannerChip(node);
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");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(this);
}
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
const planningModeWidget = findWidget(this, "planning_mode");
if (planningModeWidget) {
const originalCallback = planningModeWidget.callback;
planningModeWidget.callback = (...args) => {
originalCallback?.apply(planningModeWidget, args);
applyPlannerModeVisibility(this);
app.graph.setDirtyCanvas(true, true);
};
}
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);
};
}
}
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);
rehydrateSerializedWidgets(this, info?.widgets_values);
refreshNodeLayoutState(this, nodeName);
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) {
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`);
}
if (Array.isArray(message?.planning_report) && message.planning_report.length > 0) {
details.push(String(message.planning_report[0] || ""));
}
if (details.length > 0) {
this.properties.iamccsPlannerDetails = details.join("\n");
app.graph.setDirtyCanvas(true, true);
}
};
}
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);
}
};
}
},
});
@@ -0,0 +1,188 @@
import { app } from "../../scripts/app.js";
const LEGACY_LOCAL_KEYS = [
"workflow",
"Comfy.OpenWorkflowsPaths",
"Comfy.ActiveWorkflowIndex",
];
const LEGACY_PREFIXES = [
"Comfy.Workflow.Drafts",
"Comfy.Workflow.DraftOrder",
];
const V2_PREFIX = {
draftIndex: "Comfy.Workflow.DraftIndex.v2:",
draftPayload: "Comfy.Workflow.Draft.v2:",
lastActivePath: "Comfy.Workflow.LastActivePath:",
lastOpenPaths: "Comfy.Workflow.LastOpenPaths:",
};
const MAX_DRAFTS_PER_WORKSPACE = 4;
const MAX_DRAFT_CHARS_PER_WORKSPACE = 2_500_000;
function safeParseJson(json) {
if (!json) return null;
try {
return JSON.parse(json);
} catch {
return null;
}
}
function listStorageKeys(storage) {
const keys = [];
try {
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index);
if (key) keys.push(key);
}
} catch {
return [];
}
return keys;
}
function removeKey(storage, key) {
try {
storage.removeItem(key);
return true;
} catch {
return false;
}
}
function getPayloadKey(workspaceId, draftKey) {
return `${V2_PREFIX.draftPayload}${workspaceId}:${draftKey}`;
}
function cleanupLegacyWorkflowStorage(localStorageRef) {
let removed = 0;
for (const key of LEGACY_LOCAL_KEYS) {
if (removeKey(localStorageRef, key)) removed += 1;
}
for (const key of listStorageKeys(localStorageRef)) {
if (LEGACY_PREFIXES.some((prefix) => key.startsWith(prefix))) {
if (removeKey(localStorageRef, key)) removed += 1;
}
}
return removed;
}
function cleanupWorkspaceDrafts(localStorageRef, workspaceId) {
const indexKey = `${V2_PREFIX.draftIndex}${workspaceId}`;
const rawIndex = localStorageRef.getItem(indexKey);
if (!rawIndex) return { removed: 0, rewritten: false };
const index = safeParseJson(rawIndex);
if (!index || typeof index !== "object" || !Array.isArray(index.order) || typeof index.entries !== "object" || index.entries == null) {
let removed = removeKey(localStorageRef, indexKey) ? 1 : 0;
const payloadPrefix = `${V2_PREFIX.draftPayload}${workspaceId}:`;
for (const key of listStorageKeys(localStorageRef)) {
if (key.startsWith(payloadPrefix) && removeKey(localStorageRef, key)) {
removed += 1;
}
}
return { removed, rewritten: false };
}
const payloadPrefix = `${V2_PREFIX.draftPayload}${workspaceId}:`;
const seenPayloads = new Set();
const keptEntries = {};
const keptOrder = [];
let keptChars = 0;
let keptCount = 0;
let removed = 0;
const newestFirst = [...index.order].reverse();
for (const draftKey of newestFirst) {
const entry = index.entries[draftKey];
if (!entry || typeof entry.path !== "string") continue;
const payloadKey = getPayloadKey(workspaceId, draftKey);
const payloadJson = localStorageRef.getItem(payloadKey);
if (!payloadJson) continue;
const payloadChars = payloadJson.length;
const canKeep = keptCount < MAX_DRAFTS_PER_WORKSPACE && (
keptCount === 0 || keptChars + payloadChars <= MAX_DRAFT_CHARS_PER_WORKSPACE
);
if (canKeep) {
keptEntries[draftKey] = entry;
keptOrder.unshift(draftKey);
keptChars += payloadChars;
keptCount += 1;
seenPayloads.add(payloadKey);
} else if (removeKey(localStorageRef, payloadKey)) {
removed += 1;
}
}
for (const key of listStorageKeys(localStorageRef)) {
if (key.startsWith(payloadPrefix) && !seenPayloads.has(key)) {
if (removeKey(localStorageRef, key)) removed += 1;
}
}
const nextIndex = {
...index,
updatedAt: Date.now(),
order: keptOrder,
entries: keptEntries,
};
const nextIndexJson = JSON.stringify(nextIndex);
const rewritten = nextIndexJson !== rawIndex;
if (rewritten) {
try {
localStorageRef.setItem(indexKey, nextIndexJson);
} catch {
// If index rewrite fails, leaving the trimmed payload set is still better
// than keeping localStorage saturated with stale drafts.
}
}
return { removed, rewritten };
}
function cleanupWorkflowPersistenceStorage() {
try {
const localStorageRef = window.localStorage;
if (!localStorageRef) return;
let removed = cleanupLegacyWorkflowStorage(localStorageRef);
let rewritten = 0;
const workspaceIds = new Set();
for (const key of listStorageKeys(localStorageRef)) {
if (key.startsWith(V2_PREFIX.draftIndex)) {
workspaceIds.add(key.slice(V2_PREFIX.draftIndex.length));
}
}
for (const workspaceId of workspaceIds) {
const result = cleanupWorkspaceDrafts(localStorageRef, workspaceId);
removed += result.removed;
if (result.rewritten) rewritten += 1;
}
if (removed || rewritten) {
console.info("[IAMCCS] Workflow draft storage cleanup applied", {
removedKeys: removed,
rewrittenIndexes: rewritten,
});
}
} catch (error) {
console.warn("[IAMCCS] Workflow draft storage cleanup skipped", error);
}
}
app.registerExtension({
name: "iamccs.workflow_persist_cleanup",
async setup() {
cleanupWorkflowPersistenceStorage();
},
});
@@ -0,0 +1,366 @@
import { app } from "../../scripts/app.js";
const LEGACY_LOCAL_KEYS = [
"workflow",
"Comfy.OpenWorkflowsPaths",
"Comfy.ActiveWorkflowIndex",
];
const LEGACY_PREFIXES = [
"Comfy.Workflow.Drafts",
"Comfy.Workflow.DraftOrder",
];
const V2_PREFIX = {
draftIndex: "Comfy.Workflow.DraftIndex.v2:",
draftPayload: "Comfy.Workflow.Draft.v2:",
lastActivePath: "Comfy.Workflow.LastActivePath:",
lastOpenPaths: "Comfy.Workflow.LastOpenPaths:",
};
const MAX_DRAFTS_PER_WORKSPACE = 4;
const MAX_DRAFT_CHARS_PER_WORKSPACE = 2_500_000;
const STORAGE_PATCH_FLAG = "__iamccsWorkflowDraftQuotaPatchApplied";
function safeParseJson(json) {
if (!json) return null;
try {
return JSON.parse(json);
} catch {
return null;
}
}
function listStorageKeys(storage) {
const keys = [];
try {
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index);
if (key) keys.push(key);
}
} catch {
return [];
}
return keys;
}
function removeKey(storage, key) {
try {
storage.removeItem(key);
return true;
} catch {
return false;
}
}
function getPayloadKey(workspaceId, draftKey) {
return `${V2_PREFIX.draftPayload}${workspaceId}:${draftKey}`;
}
function extractWorkspaceIdFromKey(key) {
if (typeof key !== "string") return null;
if (key.startsWith(V2_PREFIX.draftIndex)) {
return key.slice(V2_PREFIX.draftIndex.length);
}
if (key.startsWith(V2_PREFIX.draftPayload)) {
const rest = key.slice(V2_PREFIX.draftPayload.length);
const separatorIndex = rest.indexOf(":");
return separatorIndex === -1 ? null : rest.slice(0, separatorIndex);
}
if (key.startsWith(V2_PREFIX.lastActivePath)) {
return key.slice(V2_PREFIX.lastActivePath.length);
}
if (key.startsWith(V2_PREFIX.lastOpenPaths)) {
return key.slice(V2_PREFIX.lastOpenPaths.length);
}
return null;
}
function isDraftStorageKey(key) {
return typeof key === "string" && (
key.startsWith(V2_PREFIX.draftIndex)
|| key.startsWith(V2_PREFIX.draftPayload)
|| key.startsWith(V2_PREFIX.lastActivePath)
|| key.startsWith(V2_PREFIX.lastOpenPaths)
|| LEGACY_LOCAL_KEYS.includes(key)
|| LEGACY_PREFIXES.some((prefix) => key.startsWith(prefix))
);
}
function isQuotaExceededError(error) {
if (!error) return false;
const name = String(error.name || "");
const message = String(error.message || "").toLowerCase();
return name === "QuotaExceededError"
|| name === "NS_ERROR_DOM_QUOTA_REACHED"
|| message.includes("quota")
|| message.includes("storage is full");
}
function cleanupWorkspaceTrackingStorage(localStorageRef, workspaceId = null) {
let removed = 0;
for (const key of listStorageKeys(localStorageRef)) {
const isTrackingKey = key.startsWith(V2_PREFIX.lastActivePath)
|| key.startsWith(V2_PREFIX.lastOpenPaths);
if (!isTrackingKey) continue;
if (workspaceId !== null && extractWorkspaceIdFromKey(key) !== workspaceId) {
continue;
}
if (removeKey(localStorageRef, key)) removed += 1;
}
return removed;
}
function cleanupLegacyWorkflowStorage(localStorageRef) {
let removed = 0;
for (const key of LEGACY_LOCAL_KEYS) {
if (removeKey(localStorageRef, key)) removed += 1;
}
for (const key of listStorageKeys(localStorageRef)) {
if (LEGACY_PREFIXES.some((prefix) => key.startsWith(prefix))) {
if (removeKey(localStorageRef, key)) removed += 1;
}
}
return removed;
}
function cleanupWorkspaceDrafts(localStorageRef, workspaceId) {
const indexKey = `${V2_PREFIX.draftIndex}${workspaceId}`;
const rawIndex = localStorageRef.getItem(indexKey);
if (!rawIndex) return { removed: 0, rewritten: false };
const index = safeParseJson(rawIndex);
if (!index || typeof index !== "object" || !Array.isArray(index.order) || typeof index.entries !== "object" || index.entries == null) {
let removed = removeKey(localStorageRef, indexKey) ? 1 : 0;
const payloadPrefix = `${V2_PREFIX.draftPayload}${workspaceId}:`;
for (const key of listStorageKeys(localStorageRef)) {
if (key.startsWith(payloadPrefix) && removeKey(localStorageRef, key)) {
removed += 1;
}
}
return { removed, rewritten: false };
}
const payloadPrefix = `${V2_PREFIX.draftPayload}${workspaceId}:`;
const seenPayloads = new Set();
const keptEntries = {};
const keptOrder = [];
let keptChars = 0;
let keptCount = 0;
let removed = 0;
const newestFirst = [...index.order].reverse();
for (const draftKey of newestFirst) {
const entry = index.entries[draftKey];
if (!entry || typeof entry.path !== "string") continue;
const payloadKey = getPayloadKey(workspaceId, draftKey);
const payloadJson = localStorageRef.getItem(payloadKey);
if (!payloadJson) continue;
const payloadChars = payloadJson.length;
const canKeep = keptCount < MAX_DRAFTS_PER_WORKSPACE && (
keptCount === 0 || keptChars + payloadChars <= MAX_DRAFT_CHARS_PER_WORKSPACE
);
if (canKeep) {
keptEntries[draftKey] = entry;
keptOrder.unshift(draftKey);
keptChars += payloadChars;
keptCount += 1;
seenPayloads.add(payloadKey);
} else if (removeKey(localStorageRef, payloadKey)) {
removed += 1;
}
}
for (const key of listStorageKeys(localStorageRef)) {
if (key.startsWith(payloadPrefix) && !seenPayloads.has(key)) {
if (removeKey(localStorageRef, key)) removed += 1;
}
}
const nextIndex = {
...index,
updatedAt: Date.now(),
order: keptOrder,
entries: keptEntries,
};
const nextIndexJson = JSON.stringify(nextIndex);
const rewritten = nextIndexJson !== rawIndex;
if (rewritten) {
try {
localStorageRef.setItem(indexKey, nextIndexJson);
} catch {
// If index rewrite fails, leaving the trimmed payload set is still better
// than keeping localStorage saturated with stale drafts.
}
}
return { removed, rewritten };
}
function cleanupWorkflowPersistenceStorage() {
try {
const localStorageRef = window.localStorage;
if (!localStorageRef) return;
let removed = cleanupLegacyWorkflowStorage(localStorageRef);
removed += cleanupWorkspaceTrackingStorage(localStorageRef);
let rewritten = 0;
const workspaceIds = new Set();
for (const key of listStorageKeys(localStorageRef)) {
if (key.startsWith(V2_PREFIX.draftIndex)) {
workspaceIds.add(key.slice(V2_PREFIX.draftIndex.length));
}
}
for (const workspaceId of workspaceIds) {
const result = cleanupWorkspaceDrafts(localStorageRef, workspaceId);
removed += result.removed;
if (result.rewritten) rewritten += 1;
}
if (removed || rewritten) {
console.info("[IAMCCS] Workflow draft storage cleanup applied", {
removedKeys: removed,
rewrittenIndexes: rewritten,
});
}
} catch (error) {
console.warn("[IAMCCS] Workflow draft storage cleanup skipped", error);
}
}
function recoverDraftStorage(localStorageRef, key) {
let removed = cleanupLegacyWorkflowStorage(localStorageRef);
let rewritten = 0;
const workspaceIds = new Set();
const workspaceId = extractWorkspaceIdFromKey(key);
if (workspaceId) workspaceIds.add(workspaceId);
for (const storageKey of listStorageKeys(localStorageRef)) {
if (storageKey.startsWith(V2_PREFIX.draftIndex)) {
workspaceIds.add(storageKey.slice(V2_PREFIX.draftIndex.length));
}
}
for (const currentWorkspaceId of workspaceIds) {
removed += cleanupWorkspaceTrackingStorage(localStorageRef, currentWorkspaceId);
const result = cleanupWorkspaceDrafts(localStorageRef, currentWorkspaceId);
removed += result.removed;
if (result.rewritten) rewritten += 1;
}
return { removed, rewritten };
}
function aggressiveRecoverDraftStorage(localStorageRef) {
let removed = cleanupLegacyWorkflowStorage(localStorageRef);
removed += cleanupWorkspaceTrackingStorage(localStorageRef);
for (const storageKey of listStorageKeys(localStorageRef)) {
if (
storageKey.startsWith(V2_PREFIX.draftIndex)
|| storageKey.startsWith(V2_PREFIX.draftPayload)
) {
if (removeKey(localStorageRef, storageKey)) removed += 1;
}
}
return { removed, rewritten: 0 };
}
function installDraftStorageQuotaGuard() {
try {
const localStorageRef = window.localStorage;
if (!localStorageRef || localStorageRef[STORAGE_PATCH_FLAG]) return;
const originalSetItem = localStorageRef.setItem.bind(localStorageRef);
localStorageRef.setItem = function patchedSetItem(key, value) {
try {
return originalSetItem(key, value);
} catch (error) {
if (!isDraftStorageKey(key) || !isQuotaExceededError(error)) {
throw error;
}
const recovery = recoverDraftStorage(localStorageRef, key);
console.warn("[IAMCCS] Workflow draft quota reached, attempting recovery", {
key,
removedKeys: recovery.removed,
rewrittenIndexes: recovery.rewritten,
});
try {
const result = originalSetItem(key, value);
console.info("[IAMCCS] Workflow draft save recovered after storage cleanup", {
key,
removedKeys: recovery.removed,
rewrittenIndexes: recovery.rewritten,
});
return result;
} catch (retryError) {
if (isQuotaExceededError(retryError)) {
const aggressiveRecovery = aggressiveRecoverDraftStorage(localStorageRef);
console.warn("[IAMCCS] Workflow draft quota still exceeded, applying aggressive recovery", {
key,
removedKeys: aggressiveRecovery.removed,
});
try {
const result = originalSetItem(key, value);
console.info("[IAMCCS] Workflow draft save recovered after aggressive cleanup", {
key,
removedKeys: recovery.removed + aggressiveRecovery.removed,
rewrittenIndexes: recovery.rewritten,
});
return result;
} catch (finalRetryError) {
console.error("[IAMCCS] Workflow draft save still exceeds storage quota after aggressive cleanup", {
key,
removedKeys: recovery.removed + aggressiveRecovery.removed,
rewrittenIndexes: recovery.rewritten,
retryError: finalRetryError,
});
throw finalRetryError;
}
}
console.error("[IAMCCS] Workflow draft save still exceeds storage quota after cleanup", {
key,
removedKeys: recovery.removed,
rewrittenIndexes: recovery.rewritten,
retryError,
});
throw retryError;
}
}
};
Object.defineProperty(localStorageRef, STORAGE_PATCH_FLAG, {
value: true,
configurable: false,
enumerable: false,
writable: false,
});
} catch (error) {
console.warn("[IAMCCS] Workflow draft quota guard skipped", error);
}
}
app.registerExtension({
name: "iamccs.workflow_persist_cleanup",
async setup() {
cleanupWorkflowPersistenceStorage();
installDraftStorageQuotaGuard();
},
});
+1
View File
@@ -717,6 +717,7 @@ class IAMCCS_AudioExtender:
segment_start_s = float(start_frames) / float(fps)
segment_end_s = float(start_frames + unique_frames) / float(fps)
track_duration_s = float(total_samples) / float(sample_rate)
conditioning_start_s = segment_start_s
conditioning_end_s = segment_end_s
File diff suppressed because it is too large Load Diff
+20 -5
View File
@@ -215,6 +215,7 @@ class IAMCCS_FluxKleinMultiGen:
"output_height": ("INT", {"default": 1024, "min": 16, "max": 8192, "step": 16}),
"debug_enabled": ("BOOLEAN", {"default": False}),
"debug_prefix": ("STRING", {"default": "flux_klein_debug"}),
"defer_cpu_transfer": ("BOOLEAN", {"default": False}),
},
}
@@ -241,6 +242,7 @@ class IAMCCS_FluxKleinMultiGen:
output_height=1024,
debug_enabled=False,
debug_prefix="flux_klein_debug",
defer_cpu_transfer=False,
):
sep = separator.replace("\\n", "\n")
prompts = [p.strip() for p in (multi_prompt or "").split(sep) if p.strip()]
@@ -339,25 +341,38 @@ class IAMCCS_FluxKleinMultiGen:
decoded = decoded[0]
if decoded.ndim == 3:
decoded = decoded.unsqueeze(0)
decoded = decoded.cpu()
# By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
# By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
# By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
# By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
# By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
if defer_cpu_transfer:
stored_decoded = decoded
else:
stored_decoded = decoded.cpu()
if dbg:
_write_debug(dbg, {
"event": "iteration_result",
"idx": idx, "seed": current_seed,
"prompt": prompt,
"decoded_shape": _shape(decoded),
"decoded_shape": _shape(stored_decoded),
})
results.append(decoded)
results.append(stored_decoded)
if save_images and save_dir:
slug = _prompt_slug(prompt)
path = _unique_path(save_dir, f"{output_prefix}_{slug}", "png")
arr = (decoded[0].numpy() * 255).clip(0, 255).astype(np.uint8)
save_decoded = stored_decoded if not defer_cpu_transfer else decoded.detach().cpu()
arr = (save_decoded[0].numpy() * 255).clip(0, 255).astype(np.uint8)
_PILImage.fromarray(arr).save(path)
if dbg:
_write_debug(dbg, {"event": "run_end", "count": len(results)})
return (torch.cat(results, dim=0), len(prompts))
batched = torch.cat(results, dim=0)
if defer_cpu_transfer:
batched = batched.cpu()
return (batched, len(prompts))
+65
View File
@@ -0,0 +1,65 @@
import logging
_log = logging.getLogger("IAMCCS.LazySwitch")
class IAMCCS_LazyAnySwitch:
"""
By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"select": (["Qwen", "Flux"], {"default": "Qwen"}),
},
"optional": {
"input1": ("*", {"lazy": True}),
"input2": ("*", {"lazy": True}),
},
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("output",)
FUNCTION = "run"
CATEGORY = "IAMCCS/Logic"
@staticmethod
def _normalize_selection(select):
if isinstance(select, str):
value = select.strip().lower()
if value in ("qwen", "input1", "1"):
return 1
if value in ("flux", "input2", "2"):
return 2
try:
return 1 if int(select) <= 1 else 2
except Exception:
return 1
def check_lazy_status(self, select, input1=None, input2=None, **kwargs):
selected = self._normalize_selection(select)
if selected == 1 and input1 is None:
return ["input1"]
if selected == 2 and input2 is None:
return ["input2"]
return []
def run(self, select, input1=None, input2=None):
selected = self._normalize_selection(select)
if selected == 1:
if input1 is None:
raise ValueError("IAMCCS_LazyAnySwitch: input1 is not connected or not available")
_log.info("[IAMCCS_LazyAnySwitch] selecting Qwen/input1")
return (input1,)
if input2 is None:
raise ValueError("IAMCCS_LazyAnySwitch: input2 is not connected or not available")
_log.info("[IAMCCS_LazyAnySwitch] selecting Flux/input2")
return (input2,)
File diff suppressed because it is too large Load Diff
+33 -6
View File
@@ -107,8 +107,35 @@ def _promote_staged_directory(staged_dir: str, target_dir: str):
shutil.rmtree(staged_dir, ignore_errors=True)
def _canonical_image_ext(path: str) -> str:
ext = os.path.splitext(path)[1].lower()
if ext == ".jpeg":
return ".jpg"
return ext
def _save_pil_image(image, out_path: str):
ext = _canonical_image_ext(out_path)
if ext == ".jpg":
image.convert("RGB").save(out_path, format="JPEG", quality=95)
return
if ext == ".webp":
image.convert("RGB").save(out_path, format="WEBP", quality=95)
return
image.save(out_path, format="PNG")
def _copy_frame(src_path: str, dst_path: str):
shutil.copy2(src_path, dst_path)
src_ext = _canonical_image_ext(src_path)
dst_ext = _canonical_image_ext(dst_path)
if src_ext == dst_ext:
shutil.copy2(src_path, dst_path)
return
from PIL import Image # type: ignore
with Image.open(src_path) as image:
_save_pil_image(image, dst_path)
def _blend_frame_pair(src_path: str, dst_path: str, out_path: str, mode: str, alpha: float):
@@ -135,12 +162,12 @@ def _blend_frame_pair(src_path: str, dst_path: str, out_path: str, mode: str, al
blended = (1.0 - a) * src_np + a * dst_np
out = (np.clip(blended, 0.0, 1.0) * 255.0).round().astype(np.uint8)
Image.fromarray(out).save(out_path)
_save_pil_image(Image.fromarray(out), out_path)
def _build_ext(path_a: str, path_b: str) -> str:
ext = os.path.splitext(path_a)[1] or os.path.splitext(path_b)[1]
ext = ext.lower()
ext = _canonical_image_ext(ext)
if ext not in (".png", ".jpg", ".jpeg", ".webp"):
ext = ".png"
return ext
@@ -1599,7 +1626,7 @@ class IAMCCS_VideoCombineFromDir:
seq = self._sequence_pattern(files)
if seq is not None:
pattern, start_number = seq
cmd = [ffmpeg, "-y", "-framerate", f"{frame_rate:.6f}", "-start_number", str(start_number), "-i", pattern]
cmd = [ffmpeg, "-nostdin", "-y", "-framerate", f"{frame_rate:.6f}", "-start_number", str(start_number), "-i", pattern]
else:
list_path = os.path.join(temp_dir, "frames.txt")
with open(list_path, "w", encoding="utf-8") as f:
@@ -1609,7 +1636,7 @@ class IAMCCS_VideoCombineFromDir:
f.write(f"duration {1.0 / frame_rate:.12f}\n")
escaped = files[-1].replace("'", "'\\''")
f.write(f"file '{escaped}'\n")
cmd = [ffmpeg, "-y", "-f", "concat", "-safe", "0", "-i", list_path]
cmd = [ffmpeg, "-nostdin", "-y", "-f", "concat", "-safe", "0", "-i", list_path]
if wav_path:
cmd += ["-i", wav_path]
@@ -1621,7 +1648,7 @@ class IAMCCS_VideoCombineFromDir:
cmd += ["-shortest"]
cmd += [out_path]
result = subprocess.run(cmd, capture_output=True, text=True)
result = subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL)
if result.returncode != 0:
raise RuntimeError(f"ffmpeg failed: {result.stderr.strip() or result.stdout.strip()}")
+12 -2
View File
@@ -108,6 +108,14 @@ def _save_latent_bridge(
if torch.is_tensor(latent_reference)
else None
)
if full_cpu is not None and reference_cpu is not None:
same_storage = False
try:
same_storage = full_cpu.untyped_storage().data_ptr() == reference_cpu.untyped_storage().data_ptr()
except Exception:
same_storage = full_cpu.data_ptr() == reference_cpu.data_ptr()
if same_storage:
reference_cpu = reference_cpu.clone()
seed_offset_cpu = torch.tensor([int(seed_offset_latent_frames or 0)], dtype=torch.int64)
overlap_latent_cpu = torch.tensor([int(overlap_latent_frames or 0)], dtype=torch.int64)
if _safetensors_save_file is not None:
@@ -355,6 +363,7 @@ def _concat_segments(segment_paths, output_path):
cmd = [
ffmpeg_path,
"-nostdin",
"-y",
"-f",
"concat",
@@ -378,7 +387,7 @@ def _concat_segments(segment_paths, output_path):
"+faststart",
output_path,
]
result = subprocess.run(cmd, capture_output=True, text=True)
result = subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL)
try:
os.remove(list_path)
except Exception:
@@ -486,6 +495,7 @@ def _save_segment_from_images(images, audio, frame_rate, output_path):
cmd = [
ffmpeg_path,
"-nostdin",
"-y",
"-framerate",
f"{frame_rate:.6f}",
@@ -508,7 +518,7 @@ def _save_segment_from_images(images, audio, frame_rate, output_path):
cmd += ["-c:a", "aac", "-b:a", "192k", "-ar", "48000", "-shortest"]
cmd += ["-movflags", "+faststart", output_path]
result = subprocess.run(cmd, capture_output=True, text=True)
result = subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL)
if result.returncode != 0:
stderr = result.stderr.strip() or result.stdout.strip()
raise RuntimeError(f"ffmpeg segment encode failed: {stderr}")
+154 -26
View File
@@ -634,8 +634,8 @@ class IAMCCS_SegmentPlanner:
"song_duration_s": ("FLOAT", {"default": 180.0, "min": 0.01, "max": 36000.0, "step": 0.01}),
"fps": ("FLOAT", {"default": 24.0, "min": 0.001, "max": 240.0, "step": 0.01}),
"segment_duration_s": ("FLOAT", {"default": 10.0, "min": 0.01, "max": 3600.0, "step": 0.01}),
"planning_mode": (["manual_segment_seconds", "auto_profile"], {"default": "manual_segment_seconds"}),
"content_profile": (["videoclip", "monologue"], {"default": "videoclip"}),
"planning_mode": (["manual_segment_seconds", "explicit_preset_seconds"], {"default": "manual_segment_seconds"}),
"segment_preset": (["5sec", "10sec", "15sec", "20sec", "videoclip", "monologue"], {"default": "15sec"}),
"overlap_frames": ("INT", {"default": 9, "min": 0, "max": 4096, "step": 1}),
"ltx_round_mode": (["up", "nearest", "down"], {"default": "up"}),
"segment_index": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}),
@@ -690,44 +690,79 @@ class IAMCCS_SegmentPlanner:
return up if (up - frames) <= (frames - down) else down
return up
def _recommend_profile(self, content_profile: str):
profile = str(content_profile or "videoclip")
if profile == "monologue":
def _normalize_segment_preset(self, segment_preset: str) -> str:
value = str(segment_preset or "15sec")
if value == "videoclip":
return "10sec"
if value == "monologue":
return "15sec"
if value in {"5sec", "10sec", "15sec", "20sec"}:
return value
return "15sec"
def _normalize_planning_mode(self, planning_mode: str) -> str:
value = str(planning_mode or "manual_segment_seconds")
if value == "auto_profile":
return "explicit_preset_seconds"
if value in {"manual_segment_seconds", "explicit_preset_seconds"}:
return value
return "manual_segment_seconds"
def _recommend_profile(self, segment_preset: str):
preset = self._normalize_segment_preset(segment_preset)
if preset == "5sec":
return {
"base_target_s": 15.0,
"min_segment_s": 8.0,
"max_segment_s": 15.0,
"overlap_frames": 13,
"segment_seconds": 5.0,
"overlap_frames": 9,
"audio_left_context_s": 0.25,
"extension_preset": "videoclip_audio_24fps",
}
if preset == "20sec":
return {
"segment_seconds": 20.0,
"overlap_frames": 9,
"audio_left_context_s": 1.0,
"extension_preset": "monologue_audio_24fps",
}
if preset == "15sec":
return {
"segment_seconds": 15.0,
"overlap_frames": 9,
"audio_left_context_s": 0.75,
"extension_preset": "monologue_audio_24fps",
}
return {
"base_target_s": 10.0,
"min_segment_s": 5.0,
"max_segment_s": 10.0,
"segment_seconds": 10.0,
"overlap_frames": 9,
"audio_left_context_s": 0.5,
"extension_preset": "videoclip_audio_24fps",
}
def plan(self, song_duration_s: float, fps: float, segment_duration_s: float, planning_mode: str, content_profile: str, overlap_frames: int, ltx_round_mode: str, segment_index: int):
def _recommend_profile_for_segment_seconds(self, segment_duration_s: float):
duration = float(segment_duration_s)
for preset, target_seconds in (("5sec", 5.0), ("10sec", 10.0), ("15sec", 15.0), ("20sec", 20.0)):
if abs(duration - target_seconds) <= 0.1:
return preset, self._recommend_profile(preset)
return None, None
def plan(self, song_duration_s: float, fps: float, segment_duration_s: float, planning_mode: str, segment_preset: str, overlap_frames: int, ltx_round_mode: str, segment_index: int):
song_duration_s = max(0.01, float(song_duration_s))
fps = max(0.001, float(fps))
segment_duration_s = max(0.01, float(segment_duration_s))
overlap_frames = max(0, int(overlap_frames))
segment_index = max(0, int(segment_index))
planning_mode = str(planning_mode or "manual_segment_seconds")
content_profile = str(content_profile or "videoclip")
planning_mode = self._normalize_planning_mode(planning_mode)
segment_preset = self._normalize_segment_preset(segment_preset)
rec = self._recommend_profile(content_profile)
rec = self._recommend_profile(segment_preset)
auto_duration_profile = None
effective_planning_mode = planning_mode
if planning_mode == "auto_profile":
target_s = float(rec["base_target_s"])
estimated_segments_auto = max(1, int(math.ceil(song_duration_s / max(0.01, target_s))))
auto_segment_s = song_duration_s / float(estimated_segments_auto)
auto_segment_s = max(float(rec["min_segment_s"]), min(float(rec["max_segment_s"]), auto_segment_s))
segment_duration_s = max(0.01, float(auto_segment_s))
overlap_frames = int(rec["overlap_frames"])
if planning_mode == "explicit_preset_seconds":
segment_duration_s = max(0.01, float(rec["segment_seconds"]))
else:
auto_duration_profile, auto_rec = self._recommend_profile_for_segment_seconds(segment_duration_s)
if auto_rec is not None:
rec = auto_rec
total_frames = max(1, int(round(song_duration_s * fps)))
unique_segment_frames = max(1, int(round(segment_duration_s * fps)))
@@ -761,7 +796,7 @@ class IAMCCS_SegmentPlanner:
f"overlap={overlap_frames}f | first_raw={first_segment_raw_frames}f | "
f"continuation_raw={continuation_raw_frames}f | segments={estimated_segments} | "
f"loops={continuation_loops} | last_unique={last_segment_unique_frames}f | ltx_round={ltx_round_mode} | "
f"planning={effective_planning_mode} | profile={content_profile}"
f"planning={effective_planning_mode} | segment_preset={segment_preset}"
)
current_segment_report = (
@@ -771,9 +806,9 @@ class IAMCCS_SegmentPlanner:
)
planning_profile_report = (
f"profile={content_profile} | planning_mode={effective_planning_mode} | segment_duration_s={segment_duration_s:.3f} | "
f"segment_preset={segment_preset} | planning_mode={effective_planning_mode} | segment_duration_s={segment_duration_s:.3f} | "
f"recommended_overlap={recommended_overlap_frames}f | recommended_left_context={recommended_audio_left_context_s:.2f}s | "
f"recommended_extension_preset={recommended_extension_preset}"
f"recommended_extension_preset={recommended_extension_preset} | auto_duration_profile={auto_duration_profile or 'none'}"
)
return (
@@ -803,6 +838,99 @@ class IAMCCS_SegmentPlanner:
)
class IAMCCS_SegmentPlannerSettings(IAMCCS_SegmentPlanner):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"segment_duration_s": ("FLOAT", {"default": 10.0, "min": 0.01, "max": 3600.0, "step": 0.01}),
"planning_mode": (["manual_segment_seconds", "explicit_preset_seconds"], {"default": "manual_segment_seconds"}),
"segment_preset": (["5sec", "10sec", "15sec", "20sec", "videoclip", "monologue"], {"default": "10sec"}),
"overlap_frames": ("INT", {"default": 9, "min": 0, "max": 4096, "step": 1}),
"auto_sync_overlap": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("FLOAT", "STRING", "STRING", "INT", "STRING")
RETURN_NAMES = ("segment_duration_s", "planning_mode", "segment_preset", "overlap_frames", "report")
FUNCTION = "build"
CATEGORY = "IAMCCS/LTX-2"
def build(self, segment_duration_s: float, planning_mode: str, segment_preset: str, overlap_frames: int, auto_sync_overlap: bool):
segment_duration_s = max(0.01, float(segment_duration_s))
overlap_frames = max(0, int(overlap_frames))
planning_mode = self._normalize_planning_mode(planning_mode)
segment_preset = self._normalize_segment_preset(segment_preset)
rec = self._recommend_profile(segment_preset)
auto_duration_profile = None
if planning_mode == "explicit_preset_seconds":
segment_duration_s = max(0.01, float(rec["segment_seconds"]))
elif auto_sync_overlap:
auto_duration_profile, auto_rec = self._recommend_profile_for_segment_seconds(segment_duration_s)
if auto_rec is not None:
rec = auto_rec
report = (
f"segment_duration_s={segment_duration_s:.3f} | planning_mode={planning_mode} | "
f"segment_preset={segment_preset} | overlap_frames={overlap_frames} | "
f"auto_sync_overlap={bool(auto_sync_overlap)} | auto_duration_profile={auto_duration_profile or 'none'} | "
"overlap_policy=manual/default_9"
)
return (
float(segment_duration_s),
str(planning_mode),
str(segment_preset),
int(overlap_frames),
report,
)
class IAMCCS_SegmentPlannerLinked(IAMCCS_SegmentPlanner):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"song_duration_s": ("FLOAT", {"default": 180.0, "min": 0.01, "max": 36000.0, "step": 0.01}),
"fps": ("FLOAT", {"default": 24.0, "min": 0.001, "max": 240.0, "step": 0.01}),
"segment_duration_s": ("FLOAT", {"default": 10.0, "min": 0.01, "max": 3600.0, "step": 0.01, "forceInput": True}),
"planning_mode_in": ("STRING", {"default": "manual_segment_seconds", "forceInput": True}),
"segment_preset_in": ("STRING", {"default": "10sec", "forceInput": True}),
"overlap_frames_in": ("INT", {"default": 9, "min": 0, "max": 4096, "step": 1, "forceInput": True}),
"ltx_round_mode": (["up", "nearest", "down"], {"default": "up"}),
"segment_index": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}),
}
}
RETURN_TYPES = IAMCCS_SegmentPlanner.RETURN_TYPES
RETURN_NAMES = IAMCCS_SegmentPlanner.RETURN_NAMES
FUNCTION = "plan_linked"
CATEGORY = "IAMCCS/LTX-2"
def plan_linked(
self,
song_duration_s: float,
fps: float,
segment_duration_s: float,
planning_mode_in: str,
segment_preset_in: str,
overlap_frames_in: int,
ltx_round_mode: str,
segment_index: int,
):
return self.plan(
song_duration_s,
fps,
segment_duration_s,
str(planning_mode_in or "manual_segment_seconds"),
str(segment_preset_in or "10sec"),
int(overlap_frames_in),
ltx_round_mode,
segment_index,
)
class IAMCCS_SegmentPlanFromPlanner:
@classmethod
def INPUT_TYPES(cls):
+232
View File
@@ -0,0 +1,232 @@
from __future__ import annotations
from typing import Any
try:
from .iamccs_flexible_inputs import FlexibleOptionalInputType, any_type
except ImportError:
from iamccs_flexible_inputs import FlexibleOptionalInputType, any_type
_SUPERNODE_CONTRACT_TYPE = "IAMCCS_SUPERNODE_CONTRACT"
_SUPERNODE_LINX_TYPE = "IAMCCS_SUPERNODE_LINX"
_MODULE_INPUT_SLOTS = 6
def _parse_contract(payload: Any) -> dict[str, Any]:
if isinstance(payload, dict):
return dict(payload)
parsed: dict[str, Any] = {}
if payload is None:
return parsed
for item in str(payload).split(";"):
item = item.strip()
if not item or "=" not in item:
continue
key, value = item.split("=", 1)
parsed[key.strip()] = value.strip()
return parsed
def _build_dynamic_optional_slots(type_name: str) -> FlexibleOptionalInputType:
data = {f"module_{index:02d}": (type_name,) for index in range(1, _MODULE_INPUT_SLOTS + 1)}
return FlexibleOptionalInputType(type_name, data=data)
def _collect_named_inputs(kwargs: dict[str, Any], prefix: str) -> dict[str, Any]:
collected: dict[str, Any] = {}
for key, value in kwargs.items():
if not key.startswith(prefix):
continue
if value is None:
continue
collected[key] = value
return collected
def _build_linx_payload(
existing_linx: Any,
*,
pipeline_kind: str,
node_role: str,
node_label: str,
unique_id: Any,
upstream_contract: dict[str, Any],
module_inputs: dict[str, Any],
) -> dict[str, Any]:
if isinstance(existing_linx, dict):
linx_payload = dict(existing_linx)
nodes = list(existing_linx.get("nodes") or [])
else:
linx_payload = {}
nodes = []
linx_payload["type"] = _SUPERNODE_LINX_TYPE
linx_payload["pipeline_kind"] = pipeline_kind
linx_payload["root_contract"] = upstream_contract.get("pipeline_key", pipeline_kind)
linx_payload["nodes"] = nodes
linx_payload["nodes"].append(
{
"id": str(unique_id or node_label or node_role),
"role": node_role,
"label": node_label,
"module_keys": sorted(module_inputs.keys()),
}
)
return linx_payload
class IAMCCS_SupernodeBase:
@classmethod
def INPUT_TYPES(cls):
optional = {
"upstream_contract": (_SUPERNODE_CONTRACT_TYPE,),
"linx": (_SUPERNODE_LINX_TYPE,),
"planner_payload": ("STRING",),
"backend_payload": ("STRING",),
"continuity_payload": ("STRING",),
"second_stage_payload": ("STRING",),
}
optional.update(_build_dynamic_optional_slots(_SUPERNODE_CONTRACT_TYPE))
return {
"required": {
"pipeline_kind": (["v2v", "i2v_flf", "au_img2vid", "audio_concat", "wan_flf", "wan_continuity"],),
"surface_profile": (["compact", "progressive", "debug_surface"], {"default": "compact"}),
"backend_binding": (["ltx_v2v_disk", "ltx_audio_guided_lowram", "ltx_i2v_flf", "wan_continuity", "wan_flf", "custom"], {"default": "custom"}),
"node_label": ("STRING", {"default": "Base Supernode"}),
"notes": ("STRING", {"default": "", "multiline": True}),
},
"optional": optional,
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = (_SUPERNODE_CONTRACT_TYPE, _SUPERNODE_LINX_TYPE, "STRING")
RETURN_NAMES = ("contract", "linx", "report")
FUNCTION = "build"
CATEGORY = "IAMCCS/Supernodes"
def build(
self,
pipeline_kind,
surface_profile,
backend_binding,
node_label,
notes="",
upstream_contract=None,
linx=None,
planner_payload=None,
backend_payload=None,
continuity_payload=None,
second_stage_payload=None,
unique_id=None,
**kwargs,
):
upstream = _parse_contract(upstream_contract)
module_inputs = _collect_named_inputs(kwargs, "module_")
contract = {
"type": _SUPERNODE_CONTRACT_TYPE,
"pipeline_key": f"{pipeline_kind}:{node_label}",
"pipeline_kind": pipeline_kind,
"surface_profile": surface_profile,
"backend_binding": backend_binding,
"role": "base",
"label": str(node_label or "Base Supernode"),
"notes": str(notes or ""),
"payloads": {
"planner_payload": planner_payload,
"backend_payload": backend_payload,
"continuity_payload": continuity_payload,
"second_stage_payload": second_stage_payload,
},
"module_inputs": sorted(module_inputs.keys()),
"upstream": upstream,
}
linx_payload = _build_linx_payload(
linx,
pipeline_kind=pipeline_kind,
node_role="base",
node_label=str(node_label or "Base Supernode"),
unique_id=unique_id,
upstream_contract=contract,
module_inputs=module_inputs,
)
report = (
f"supernode_base pipeline={pipeline_kind} | surface={surface_profile} | backend_binding={backend_binding} | "
f"linked_modules={','.join(sorted(module_inputs.keys())) or 'none'} | upstream={upstream.get('pipeline_key', 'none')}"
)
return (contract, linx_payload, report)
class IAMCCS_SupernodeModule:
@classmethod
def INPUT_TYPES(cls):
optional = {
"parent_contract": (_SUPERNODE_CONTRACT_TYPE,),
"linx": (_SUPERNODE_LINX_TYPE,),
"payload": ("STRING",),
}
optional.update(_build_dynamic_optional_slots(any_type))
return {
"required": {
"pipeline_kind": (["v2v", "i2v_flf", "au_img2vid", "audio_concat", "wan_flf", "wan_continuity"],),
"module_role": (["planner", "backend", "continuity", "audio", "concat", "loop", "second_stage", "output", "custom"],),
"module_mode": (["consume", "augment", "branch"], {"default": "augment"}),
"node_label": ("STRING", {"default": "Module"}),
"module_key": ("STRING", {"default": "module"}),
},
"optional": optional,
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = (_SUPERNODE_CONTRACT_TYPE, _SUPERNODE_LINX_TYPE, "STRING")
RETURN_NAMES = ("contract", "linx", "report")
FUNCTION = "build"
CATEGORY = "IAMCCS/Supernodes"
def build(
self,
pipeline_kind,
module_role,
module_mode,
node_label,
module_key,
parent_contract=None,
linx=None,
payload=None,
unique_id=None,
**kwargs,
):
parent = _parse_contract(parent_contract)
module_inputs = _collect_named_inputs(kwargs, "module_")
contract = {
"type": _SUPERNODE_CONTRACT_TYPE,
"pipeline_key": parent.get("pipeline_key", pipeline_kind),
"pipeline_kind": pipeline_kind,
"role": str(module_role or "custom"),
"mode": str(module_mode or "augment"),
"label": str(node_label or module_role or "Module"),
"module_key": str(module_key or "module"),
"payload": payload,
"module_inputs": sorted(module_inputs.keys()),
"parent": parent,
}
linx_payload = _build_linx_payload(
linx,
pipeline_kind=pipeline_kind,
node_role=str(module_role or "custom"),
node_label=str(node_label or module_role or "Module"),
unique_id=unique_id,
upstream_contract=contract,
module_inputs=module_inputs,
)
report = (
f"supernode_module pipeline={pipeline_kind} | role={module_role} | mode={module_mode} | key={module_key} | "
f"linked_inputs={','.join(sorted(module_inputs.keys())) or 'none'} | parent={parent.get('label', 'none')}"
)
return (contract, linx_payload, report)
+71
View File
@@ -0,0 +1,71 @@
def backend_panel_input_types():
return {
"backend_panel_mode": (["compact", "show_backend_settings"],),
"backend_profile_preset": (
["inherit", "quality_first", "balanced", "speed_first", "low_vram_safe", "audio_sync_strict"],
),
"backend_memory_mode": (
["inherit", "auto", "normal_vram", "low_ram_disk", "cpu_offload_safe"],
),
"backend_quality_mode": (
["inherit", "draft", "balanced", "high_detail", "identity_strict", "temporal_stable"],
),
"backend_settings_override": ("STRING", {"default": "", "multiline": True}),
}
def merge_backend_panel_inputs(required_inputs):
merged = dict(required_inputs)
merged.update(backend_panel_input_types())
return merged
def _sanitize_override(value):
return str(value or "").replace(";", ",").strip() or "none"
def build_backend_panel_payload(
backend_panel_mode,
backend_profile_preset,
backend_memory_mode,
backend_quality_mode,
backend_settings_override,
):
sanitized_override = _sanitize_override(backend_settings_override)
payload = (
f"backend_panel_mode={backend_panel_mode}; backend_profile_preset={backend_profile_preset}; "
f"backend_memory_mode={backend_memory_mode}; backend_quality_mode={backend_quality_mode}; "
f"backend_settings_override={sanitized_override}"
)
summary = (
f"backend_panel={backend_panel_mode} | preset={backend_profile_preset} | "
f"memory={backend_memory_mode} | quality={backend_quality_mode}"
)
return payload, summary
def append_backend_panel_to_payload_and_report(
payload,
report,
backend_panel_mode,
backend_profile_preset,
backend_memory_mode,
backend_quality_mode,
backend_settings_override,
):
panel_payload, panel_summary = build_backend_panel_payload(
backend_panel_mode,
backend_profile_preset,
backend_memory_mode,
backend_quality_mode,
backend_settings_override,
)
if payload:
payload = f"{payload}; {panel_payload}"
else:
payload = panel_payload
if report:
report = f"{report} | {panel_summary}"
else:
report = panel_summary
return payload, report
+25
View File
@@ -0,0 +1,25 @@
from .iamccs_supernodes_auimg2vid_exec_backend import (
IAMCCS_GC_AUIMG2VIDExecutableFinalize as _GCFinalize,
IAMCCS_GC_AUIMG2VIDExecutablePlanner as _GCPlanner,
IAMCCS_GC_AUIMG2VIDExecutableRender as _GCRender,
IAMCCS_GC_AUIMG2VIDExecutableVAE as _GCVAE,
)
class IAMCCS_SuperNodes_AUIMG2VIDExecutablePlanner(_GCPlanner):
CATEGORY = "IAMCCS/SuperNodes"
class IAMCCS_SuperNodes_AUIMG2VIDExecutableRender(_GCRender):
CATEGORY = "IAMCCS/SuperNodes"
class IAMCCS_SuperNodes_AUIMG2VIDExecutableVAE(_GCVAE):
CATEGORY = "IAMCCS/SuperNodes"
if _GCFinalize is not None:
class IAMCCS_SuperNodes_AUIMG2VIDExecutableFinalize(_GCFinalize):
CATEGORY = "IAMCCS/SuperNodes"
else:
IAMCCS_SuperNodes_AUIMG2VIDExecutableFinalize = None
+136
View File
@@ -0,0 +1,136 @@
SUPERNODE_LINX_TYPE = "IAMCCS_SUPERNODE_LINX"
def _clone_chain(existing_linx):
if not isinstance(existing_linx, dict):
return []
chain = existing_linx.get("chain") or []
return [dict(item) for item in chain if isinstance(item, dict)]
def _clone_dict(existing_linx, key):
if not isinstance(existing_linx, dict):
return {}
value = existing_linx.get(key) or {}
if not isinstance(value, dict):
return {}
return dict(value)
def _clone_resources(existing_linx):
if not isinstance(existing_linx, dict):
return {}
value = existing_linx.get("resources") or {}
if not isinstance(value, dict):
return {}
return dict(value)
def linx_resource(existing_linx, key, default=None):
if not isinstance(existing_linx, dict):
return default
resources = existing_linx.get("resources") or {}
if not isinstance(resources, dict):
return default
return resources.get(key, default)
def linx_output(existing_linx, key, default=None):
if not isinstance(existing_linx, dict):
return default
outputs = existing_linx.get("outputs") or {}
if not isinstance(outputs, dict):
return default
return outputs.get(key, default)
def linx_policy(existing_linx, key, default=None):
if not isinstance(existing_linx, dict):
return default
policies = existing_linx.get("policies") or {}
if not isinstance(policies, dict):
return default
return policies.get(key, default)
def build_supernode_linx_payload(existing_linx, node_role, payload, report, unique_id=None):
if isinstance(existing_linx, dict):
linx_payload = dict(existing_linx)
chain = _clone_chain(existing_linx)
else:
linx_payload = {}
chain = []
linx_payload["type"] = SUPERNODE_LINX_TYPE
linx_payload["chain"] = chain
linx_payload["chain"].append(
{
"id": str(unique_id or node_role),
"role": str(node_role),
"payload_preview": str(payload or "")[:240],
"report_preview": str(report or "")[:240],
}
)
return linx_payload
def build_stage_linx_payload(
existing_linx,
stage_name,
stage_kind,
payload,
report,
unique_id=None,
slot_map=None,
downstream_stages=None,
policies=None,
outputs=None,
resources=None,
):
linx_payload = build_supernode_linx_payload(existing_linx, stage_name, payload, report, unique_id=unique_id)
stage_entry = {
"id": str(unique_id or stage_name),
"name": str(stage_name),
"kind": str(stage_kind),
"payload": dict(payload) if isinstance(payload, dict) else {"preview": str(payload or "")[:240]},
"report_preview": str(report or "")[:240],
}
stages = list(linx_payload.get("stages") or [])
stages.append(stage_entry)
linx_payload["stages"] = stages
linx_payload["stage_count"] = len(stages)
merged_slot_map = _clone_dict(linx_payload, "slot_map")
if isinstance(slot_map, dict):
merged_slot_map.update(slot_map)
if merged_slot_map:
linx_payload["slot_map"] = merged_slot_map
merged_policies = _clone_dict(linx_payload, "policies")
if isinstance(policies, dict):
merged_policies.update(policies)
if merged_policies:
linx_payload["policies"] = merged_policies
merged_outputs = _clone_dict(linx_payload, "outputs")
if isinstance(outputs, dict):
merged_outputs.update(outputs)
if merged_outputs:
linx_payload["outputs"] = merged_outputs
merged_resources = _clone_resources(linx_payload)
if isinstance(resources, dict):
for resource_key, resource_value in resources.items():
if resource_value is not None:
merged_resources[str(resource_key)] = resource_value
if merged_resources:
linx_payload["resources"] = merged_resources
linx_payload["resource_keys"] = sorted(str(item) for item in merged_resources.keys())
if downstream_stages is not None:
linx_payload["downstream_stages"] = [str(item) for item in downstream_stages if str(item or "").strip()]
linx_payload["active_stage"] = str(stage_name)
linx_payload["active_stage_kind"] = str(stage_kind)
return linx_payload
+214
View File
@@ -0,0 +1,214 @@
import logging
from .iamccs_supernodes_backend_panels import append_backend_panel_to_payload_and_report, merge_backend_panel_inputs
from .iamccs_supernodes_linx import SUPERNODE_LINX_TYPE, build_stage_linx_payload, linx_resource
_log = logging.getLogger("IAMCCS.SuperNodes.SecondStage")
try:
import folder_paths # type: ignore
_LATENT_UPSCALE_MODEL_NAMES = tuple(folder_paths.get_filename_list("latent_upscale_models"))
except Exception:
_LATENT_UPSCALE_MODEL_NAMES = (
"ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
"ltx-2.3-spatial-upscaler-x2-1.0.safetensors",
)
def _count_sigma_steps(sigmas_text):
values = []
for item in str(sigmas_text or "").replace("\n", ",").split(","):
try:
values.append(float(item.strip()))
except Exception:
pass
return max(0, len(values) - 1)
class IAMCCS_SuperNodes_SecondStage:
CATEGORY = "IAMCCS/SuperNodes"
FUNCTION = "build_second_stage"
RETURN_TYPES = (SUPERNODE_LINX_TYPE, "STRING")
RETURN_NAMES = ("linx", "report")
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
@classmethod
def INPUT_TYPES(cls):
return {
"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"],),
"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}),
"second_stage_manual_sigmas": ("STRING", {"default": "0.909375, 0.725, 0.421875, 0.0"}),
}),
},
"optional": {
"stage2_model": ("MODEL", {"lazy": True}),
"linx": (SUPERNODE_LINX_TYPE,),
"model": ("MODEL", {"lazy": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
def check_lazy_status(self, second_stage_mode, stage2_model=None, model=None, **kwargs):
if str(second_stage_mode) == "off":
return []
needed = []
if stage2_model is None:
needed.append("stage2_model")
return needed
def build_second_stage(
self,
second_stage_mode,
stage2_model_policy,
second_stage_upscale_model,
second_stage_reinject_strength,
second_stage_cfg,
second_stage_manual_sigmas,
backend_panel_mode="compact",
backend_profile_preset="inherit",
backend_memory_mode="inherit",
backend_quality_mode="inherit",
backend_settings_override="",
stage2_model=None,
linx=None,
model=None,
unique_id=None,
):
second_stage_enabled = str(second_stage_mode) != "off"
if not second_stage_enabled:
second_stage_payload = "second_stage_mode=off; second_stage_enabled=false"
report = "Second stage. mode=off | inactive | no model/resource exported | stage1 render path unchanged."
second_stage_payload, report = append_backend_panel_to_payload_and_report(
second_stage_payload,
report,
backend_panel_mode,
backend_profile_preset,
backend_memory_mode,
backend_quality_mode,
backend_settings_override,
)
linx_payload = build_stage_linx_payload(
linx,
"second_stage",
"second_stage",
{
"second_stage_mode": "off",
"second_stage_enabled": False,
"second_stage_payload": second_stage_payload,
},
report,
unique_id=unique_id,
slot_map={
"linx": {"type": SUPERNODE_LINX_TYPE, "role": "stage_linx"},
},
downstream_stages=["render", "vae"],
policies={
"second_stage_enabled": False,
"stage2_model_policy": str(stage2_model_policy),
"second_stage_model_source": "disabled",
"second_stage_scale_mode": "off",
},
outputs={
"second_stage_payload": second_stage_payload,
"second_stage_enabled": False,
"second_stage_steps": 0,
"second_stage_scale_mode": "off",
"stage2_model_connected": False,
"second_stage_model_source": "disabled",
"anchor_refresh_requires_second_stage": False,
},
)
_log.info("[IAMCCS Second Stage] %s", report)
return (linx_payload, report)
primary_model = linx_resource(linx, "model")
connected_stage2_model = model if model is not None else stage2_model
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"}:
selected_model = connected_stage2_model
elif selected_model is None:
selected_model = connected_stage2_model
stage2_steps = _count_sigma_steps(second_stage_manual_sigmas)
model_source = "external_detailer_model" if connected_stage2_model is not None and selected_model is connected_stage2_model else "stage1_model"
scale_mode = "x2_latent_upscale_beta" if str(second_stage_mode) == "latent_upscale_refine_x2_beta" else "same_resolution_refine"
second_stage_payload = (
f"second_stage_mode={second_stage_mode}; stage2_model_policy={stage2_model_policy}; "
f"second_stage_upscale_model={second_stage_upscale_model}; second_stage_scale_mode={scale_mode}; "
f"second_stage_reinject_strength={second_stage_reinject_strength}; "
f"second_stage_cfg={second_stage_cfg}; second_stage_manual_sigmas={second_stage_manual_sigmas}; "
f"second_stage_steps={int(stage2_steps)}; second_stage_model_source={model_source}"
)
report = (
f"Second stage. mode={second_stage_mode} | scale={scale_mode} | policy={stage2_model_policy} | stage2_steps={int(stage2_steps)} | "
f"stage1_model_in_linx={'yes' if primary_model is not None else 'no'} | detailer_model_connected={'yes' if connected_stage2_model is not None else 'no'} | "
f"model_source={model_source} | anchor_refresh_note=render anchor refresh affects the per-segment second-stage reinjection path."
)
_log.info("[IAMCCS Second Stage] %s", report)
second_stage_payload, report = append_backend_panel_to_payload_and_report(
second_stage_payload,
report,
backend_panel_mode,
backend_profile_preset,
backend_memory_mode,
backend_quality_mode,
backend_settings_override,
)
linx_payload = build_stage_linx_payload(
linx,
"second_stage",
"second_stage",
{
"second_stage_mode": str(second_stage_mode),
"stage2_model_policy": str(stage2_model_policy),
"second_stage_upscale_model": str(second_stage_upscale_model),
"second_stage_scale_mode": str(scale_mode),
"second_stage_reinject_strength": float(second_stage_reinject_strength),
"second_stage_cfg": float(second_stage_cfg),
"second_stage_manual_sigmas": str(second_stage_manual_sigmas),
"second_stage_payload": second_stage_payload,
"second_stage_steps": int(stage2_steps),
"second_stage_model_source": model_source,
"anchor_refresh_requires_second_stage": False,
},
report,
unique_id=unique_id,
slot_map={
"linx": {"type": SUPERNODE_LINX_TYPE, "role": "stage_linx"},
},
downstream_stages=["render", "vae"],
policies={
"second_stage_enabled": str(second_stage_mode) != "off",
"stage2_model_policy": str(stage2_model_policy),
"second_stage_model_source": model_source,
"second_stage_scale_mode": str(scale_mode),
},
outputs={
"second_stage_payload": second_stage_payload,
"second_stage_enabled": str(second_stage_mode) != "off",
"second_stage_steps": int(stage2_steps),
"second_stage_scale_mode": str(scale_mode),
"stage2_model_connected": bool(connected_stage2_model is not None),
"second_stage_model_source": model_source,
"anchor_refresh_requires_second_stage": False,
},
resources={
"second_stage_payload": second_stage_payload,
"second_stage_model": selected_model,
},
)
return (linx_payload, report)
+23 -1
View File
@@ -2,6 +2,8 @@ import logging
import math
import re
import node_helpers
log = logging.getLogger("IAMCCS.WanLongLength")
@@ -376,7 +378,7 @@ class IAMCCS_WanIndexedPromptEncode:
"required": {
"clip": ("CLIP",),
"text": ("STRING", {"multiline": True, "default": ""}),
"segment_index": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}),
"segment_index": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1, "lazy": True}),
"index_offset": ("INT", {"default": 0, "min": -100000, "max": 100000, "step": 1}),
"selection_mode": (["direct_index", "three_phase", "cyclic_3", "single"], {"default": "direct_index"}),
"phase1_last_segment": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}),
@@ -405,6 +407,13 @@ class IAMCCS_WanIndexedPromptEncode:
FUNCTION = "encode"
CATEGORY = "IAMCCS/Wan"
def check_lazy_status(self, segment_index=None, **kwargs):
# Break static validation cycles when segment_index is wired to an easy-use
# loop index, but force the executor to resolve it before encode() runs.
if segment_index is None:
return ["segment_index"]
return []
def _pick_index(self, prompts: list[str], segment_index: int, selection_mode: str, phase1_last_segment: int, phase2_last_segment: int, fallback_mode: str) -> int:
count = len(prompts)
if count <= 0:
@@ -467,6 +476,8 @@ class IAMCCS_WanIndexedPromptEncode:
fps=16.0,
bootstrap_outside_loop=True,
):
if segment_index is None:
segment_index = 0
effective_index = int(segment_index) + int(index_offset)
prompts = _split_prompt_bank(text, separator_mode)
selected_index = self._pick_index(prompts, effective_index, selection_mode, int(phase1_last_segment), int(phase2_last_segment), str(fallback_mode))
@@ -485,6 +496,17 @@ class IAMCCS_WanIndexedPromptEncode:
fps,
bool(bootstrap_outside_loop),
)
conditioning = node_helpers.conditioning_set_values(
conditioning,
{
"_iamccs_generation_index": int(effective_index),
"_iamccs_prompt_index": int(max(selected_index, 0)),
"_iamccs_prompt_count": int(len(prompts)),
"_iamccs_total_generations": int(total_generations),
"_iamccs_continuation_generations": int(continuation_generations),
"_iamccs_prompt_selection_mode": str(selection_mode or "direct_index"),
},
)
report = (
f"segment={int(segment_index)} | effective_index={effective_index} | mode={selection_mode} | prompt_index={selected_index} | "
f"prompt_count={len(prompts)} | continuations={continuation_generations} | selected={'yes' if selected_prompt else 'empty'}"
+734
View File
@@ -0,0 +1,734 @@
import logging
import sys
from collections import OrderedDict
import comfy.sd
import comfy.hooks
from .iamccs_wan_lora_schedule import IAMCCS_WanLoRASchedule
_log = logging.getLogger("IAMCCS.LoRA.Hooks")
_ANSI_GREEN = "\033[92m"
_ANSI_YELLOW = "\033[93m"
_ANSI_RESET = "\033[0m"
_MODEL_BANK_TYPE = "IAMCCS_WAN_MODEL_BANK"
# Cache: (id(model_patcher), lora_fingerprint) → patched_model_patcher
# Avoids re-cloning and re-patching when the LoRA stack is identical across loop iterations,
# which ensures model management reuses the same loaded model (no unload/reload overhead).
_MODEL_CACHE: OrderedDict = OrderedDict()
_MAX_MODEL_CACHE = 8
def _flush_log_handlers():
seen_handlers = set()
manager = getattr(logging.Logger, "manager", None)
logger_dict = getattr(manager, "loggerDict", {}) if manager is not None else {}
for logger_name in ("IAMCCS.LoRA.Hooks", None):
logger = logging.getLogger(logger_name) if logger_name else logging.getLogger()
for handler in getattr(logger, "handlers", []):
seen_handlers.add(handler)
for logger in logger_dict.values():
if isinstance(logger, logging.Logger):
for handler in getattr(logger, "handlers", []):
seen_handlers.add(handler)
for handler in seen_handlers:
try:
handler.flush()
except Exception:
pass
for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
if stream is None:
continue
try:
stream.flush()
except Exception:
pass
def _lora_fingerprint(lora_stack):
"""Hashable fingerprint of a lora stack: (name, strength) pairs sorted."""
return tuple(sorted(
(str(e.get("name") or ""), float(e.get("strength") or 0.0))
for e in (lora_stack or [])
))
# ─── helpers shared with stack ────────────────────────────────────────────────
def _entry_summary(entry):
name = entry.get("name", "unnamed")
strength = entry.get("strength", 0.0)
origin = entry.get("origin", "manual")
gen = entry.get("generation_index")
slot = entry.get("slot")
rule = entry.get("rule") or ""
extras = [origin]
if gen is not None:
extras.append(f"gen={gen}")
if slot is not None:
extras.append(f"slot={int(slot):02d}")
if rule:
extras.append(rule)
return f"{name}({strength}) [{ ' | '.join(extras) }]"
def _stack_summary(lora_stack):
if not lora_stack:
return "none"
parts = []
for entry in lora_stack:
name = entry.get("name", "unnamed")
strength = entry.get("strength", 0.0)
origin = entry.get("_iamccs_lora_origin", "manual")
gen = entry.get("_iamccs_generation_index")
slot = entry.get("_iamccs_schedule_slot")
rule = entry.get("_iamccs_schedule_rule") or ""
extras = [origin]
if gen is not None:
extras.append(f"gen={gen}")
if slot is not None:
extras.append(f"slot={int(slot):02d}")
if rule:
extras.append(rule)
parts.append(f"{name}({strength}) [{' | '.join(extras)}]")
return "; ".join(parts)
def _origin_names(lora_stack, origin):
names = []
for entry in lora_stack or []:
if str(entry.get("_iamccs_lora_origin") or "manual") != origin:
continue
name = entry.get("name", "unnamed")
strength = entry.get("strength", 0.0)
slot = entry.get("_iamccs_schedule_slot")
rule = entry.get("_iamccs_schedule_rule") or origin
if slot is not None:
names.append(f"slot={int(slot):02d}:{name}({strength}) [{rule}]")
else:
names.append(f"{name}({strength})")
return "; ".join(names) if names else "none"
def _origin_count(lora_stack, origin):
return sum(
1
for entry in (lora_stack or [])
if str(entry.get("_iamccs_lora_origin") or "manual") == origin
)
def _inject_debug_report(lora_stack):
total_count = len(lora_stack or [])
default_summary = _origin_names(lora_stack, "default")
scheduled_summary = _origin_names(lora_stack, "scheduled")
manual_summary = _origin_names(lora_stack, "manual")
return {
"total_count": total_count,
"default_count": _origin_count(lora_stack, "default"),
"scheduled_count": _origin_count(lora_stack, "scheduled"),
"manual_count": _origin_count(lora_stack, "manual"),
"default_summary": default_summary,
"scheduled_summary": scheduled_summary,
"manual_summary": manual_summary,
"stack_summary": _stack_summary(lora_stack),
"has_default": default_summary != "none",
"has_scheduled": scheduled_summary != "none",
}
def _stack_context(lora_stack):
if not lora_stack:
return ""
prompt_ids = sorted({str(e.get("_iamccs_prompt_id") or "") for e in lora_stack if e.get("_iamccs_prompt_id")})
node_ids = sorted({str(e.get("_iamccs_schedule_node_id") or "") for e in lora_stack if e.get("_iamccs_schedule_node_id")})
log_prefixes = sorted({str(e.get("_iamccs_schedule_log_prefix") or "") for e in lora_stack if e.get("_iamccs_schedule_log_prefix")})
gen_idxs = sorted({int(e["_iamccs_generation_index"]) for e in lora_stack if e.get("_iamccs_generation_index") is not None})
parts = []
if prompt_ids:
parts.append(f"prompt={','.join(prompt_ids)}")
if node_ids:
parts.append(f"schedule_node={','.join(node_ids)}")
if log_prefixes:
parts.append(f"schedule={','.join(log_prefixes)}")
if gen_idxs:
parts.append(f"generation={','.join(str(g) for g in gen_idxs)}")
return " | ".join(parts)
def _prompt_scope_from_lora_stack(lora_stack):
prompt_ids = sorted({
str(entry.get("_iamccs_prompt_id") or "")
for entry in (lora_stack or [])
if entry.get("_iamccs_prompt_id")
})
return ",".join(prompt_ids) if prompt_ids else "no-prompt"
def _extract_generation_index_from_conditioning(*conditioning_values):
for conditioning in conditioning_values:
if not isinstance(conditioning, list):
continue
for item in conditioning:
if not isinstance(item, (list, tuple)) or len(item) < 2:
continue
meta = item[1]
if not isinstance(meta, dict):
continue
for key in (
"_iamccs_generation_index",
"_iamccs_segment_index",
"generation_index",
"segment_index",
):
value = meta.get(key)
if value is None:
continue
try:
return max(0, int(value)), key
except Exception:
continue
return 0, "default:0"
def _conditioning_flags(*conditioning_values):
flags = {
"clip_vision": False,
"reference_latents": False,
"concat_latent_image": False,
"concat_mask": False,
}
for conditioning in conditioning_values:
if not isinstance(conditioning, list):
continue
for item in conditioning:
if not isinstance(item, (list, tuple)) or len(item) < 2:
continue
meta = item[1]
if not isinstance(meta, dict):
continue
flags["clip_vision"] = flags["clip_vision"] or (meta.get("clip_vision_output") is not None)
flags["reference_latents"] = flags["reference_latents"] or bool(meta.get("reference_latents"))
flags["concat_latent_image"] = flags["concat_latent_image"] or (meta.get("concat_latent_image") is not None)
flags["concat_mask"] = flags["concat_mask"] or (meta.get("concat_mask") is not None)
return flags
def _select_model_from_bank(model_bank, generation_index, log_prefix):
generation_count = int((model_bank or {}).get("generation_count") or 1)
generation_index = max(0, min(int(generation_index), generation_count - 1))
generation_to_fingerprint = (model_bank or {}).get("generation_to_fingerprint") or {}
fingerprints_to_model = (model_bank or {}).get("fingerprints_to_model") or {}
reports = (model_bank or {}).get("reports") or {}
fingerprint = generation_to_fingerprint.get(generation_index)
if fingerprint is None:
error = (
f"[{log_prefix}][selector-check] generation={generation_index} missing fingerprint in model bank"
)
_log.error(error)
_flush_log_handlers()
raise RuntimeError(error)
model_out = fingerprints_to_model.get(fingerprint)
if model_out is None:
error = (
f"[{log_prefix}][selector-check] generation={generation_index} missing model variant for fingerprint={fingerprint}"
)
_log.error(error)
_flush_log_handlers()
raise RuntimeError(error)
return generation_index, model_out, reports.get(generation_index, {})
def _apply_lora_stack_to_model(model, lora_stack):
prompt_scope = _prompt_scope_from_lora_stack(lora_stack)
cache_key = (prompt_scope, id(model), _lora_fingerprint(lora_stack))
inject_report = _inject_debug_report(lora_stack)
cached = _MODEL_CACHE.get(cache_key)
if cached is not None:
_log.info(
"[IAMCCS_ApplyLoRAHooksToConditioning] cache hit — reusing patched model (prompt=%s, stack unchanged)",
prompt_scope,
)
_log.info(
"[IAMCCS_LoRAInjectCheck] mode=model_bank_cache_hit | prompt=%s | total=%s | always_on_present=%s | always_on=%s | scheduled_present=%s | scheduled=%s | stack=%s | backend=load_lora_for_models",
prompt_scope,
inject_report["total_count"],
"YES" if inject_report["has_default"] else "NO",
inject_report["default_summary"],
"YES" if inject_report["has_scheduled"] else "NO",
inject_report["scheduled_summary"],
inject_report["stack_summary"],
)
_flush_log_handlers()
return cached, True
model_out = model
patched_names = []
for entry in lora_stack or []:
sd = entry.get("state_dict")
if not isinstance(sd, dict) or not sd:
_log.warning("[IAMCCS_ApplyLoRAHooksToConditioning] skipped '%s': no state_dict", entry.get("name"))
continue
strength = float(entry.get("strength", 1.0) or 1.0)
model_out, _ = comfy.sd.load_lora_for_models(model_out, None, sd, strength, 0.0)
patched_names.append(f"{entry.get('name')}({strength:.3f})")
if patched_names:
_log.info(
"[IAMCCS_ApplyLoRAHooksToConditioning] patched %d LoRA(s) for prompt=%s: %s",
len(patched_names),
prompt_scope,
"; ".join(patched_names),
)
_log.info(
"[IAMCCS_LoRAInjectCheck] mode=model_patch_apply | prompt=%s | total=%s | always_on_present=%s | always_on=%s | scheduled_present=%s | scheduled=%s | stack=%s | backend=load_lora_for_models",
prompt_scope,
inject_report["total_count"],
"YES" if inject_report["has_default"] else "NO",
inject_report["default_summary"],
"YES" if inject_report["has_scheduled"] else "NO",
inject_report["scheduled_summary"],
inject_report["stack_summary"],
)
else:
_log.warning("[IAMCCS_ApplyLoRAHooksToConditioning] no LoRAs were patched (empty/invalid stack)")
_flush_log_handlers()
_MODEL_CACHE[cache_key] = model_out
_MODEL_CACHE.move_to_end(cache_key)
while len(_MODEL_CACHE) > _MAX_MODEL_CACHE:
_MODEL_CACHE.popitem(last=False)
return model_out, False
# ─── Schedule node (outputs LORA stack) ───────────────────────────────────────
class IAMCCS_WanLoRAHookSchedule:
"""Schedule wrapper that outputs a LORA stack (same as IAMCCS_WanLoRASchedule).
Kept as a separate node type so JS UI / workflows can reference it by name."""
@classmethod
def INPUT_TYPES(cls):
return IAMCCS_WanLoRASchedule.INPUT_TYPES()
RETURN_TYPES = ("LORA", "INT", "STRING", "IAMCCS_WAN_LORA_LINX")
RETURN_NAMES = ("lora", "active_slots", "report", "linx")
FUNCTION = "schedule"
CATEGORY = "IAMCCS/LoRA"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
def check_lazy_status(self, generation_index, **kwargs):
# Two-pass lazy protocol: lazy=True in INPUT_TYPES breaks the static
# validation cycle (execution.py L878). This method forces the executor
# to evaluate the upstream (forLoopStart) before calling schedule(),
# so generation_index is always the real iteration index, never None.
if generation_index is None:
return ["generation_index"]
return []
def schedule(self, generation_index, log_prefix, model_type="flow", default_lora=None, linx=None, unique_id=None, **kwargs):
return IAMCCS_WanLoRASchedule().schedule(
generation_index=generation_index,
log_prefix=log_prefix,
model_type=model_type,
default_lora=default_lora,
linx=linx,
unique_id=unique_id,
**kwargs,
)
# ─── Apply node (MODEL + LORA → MODEL + CONDITIONING passthrough) ─────────────
class IAMCCS_ApplyLoRAHooksToConditioning:
"""Apply LoRA schedule to MODEL (add_patches style, not hooks) and pass through conditioning.
Uses comfy.sd.load_lora_for_models — identical to the LoRA Loader node in ComfyUI core.
This avoids the per-step hook weight patching that causes ~15 min stalls on 14B offloaded models.
The MODEL output should go to the KSampler model input; positive/negative are passed through unchanged.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"lora": ("LORA",),
},
}
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "STRING")
RETURN_NAMES = ("model", "positive", "negative", "report")
FUNCTION = "apply"
CATEGORY = "IAMCCS/LoRA"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
def apply(self, model, positive, negative, lora):
stack_summary = _stack_summary(lora)
scheduled_active = "YES" if any(
str(e.get("_iamccs_lora_origin") or "manual") == "scheduled" for e in (lora or [])
) else "NO"
default_summary = _origin_names(lora, "default")
scheduled_summary = _origin_names(lora, "scheduled")
ctx = _stack_context(lora)
if ctx:
_log.info("[IAMCCS_ApplyLoRAHooksToConditioning] apply request | %s", ctx)
_log.info("[IAMCCS_ApplyLoRAHooksToConditioning] active_stack=%s", stack_summary)
_log.info("[IAMCCS_ApplyLoRAHooksToConditioning] scheduled_active=%s", scheduled_active)
_log.info("[IAMCCS_ApplyLoRAHooksToConditioning] applied_default=%s", default_summary)
_log.info("[IAMCCS_ApplyLoRAHooksToConditioning] applied_scheduled=%s", scheduled_summary)
_flush_log_handlers()
model_out, _cached = _apply_lora_stack_to_model(model, lora)
report = (
f"scheduled_active={scheduled_active} | "
f"applied_default={default_summary} | "
f"applied_scheduled={scheduled_summary}"
)
return (model_out, positive, negative, report)
class IAMCCS_BuildScheduledWanModelBank:
"""Build all scheduled WAN model variants outside the loop.
The loop then selects a prebuilt model by generation metadata instead of
patching or scheduling LoRAs inside the model branch.
"""
@classmethod
def INPUT_TYPES(cls):
base = IAMCCS_WanLoRASchedule.INPUT_TYPES()
base_required = dict(base.get("required", {}))
base_required.pop("generation_index", None)
required = {
"model": ("MODEL",),
"generation_count": ("INT", {"default": 4, "min": 1, "max": 512, "step": 1}),
}
required.update(base_required)
optional = dict(base.get("optional", {}))
hidden = {"unique_id": "UNIQUE_ID"}
return {"required": required, "optional": optional, "hidden": hidden}
RETURN_TYPES = (_MODEL_BANK_TYPE, "STRING", "IAMCCS_WAN_LORA_LINX")
RETURN_NAMES = ("model_bank", "report", "linx")
FUNCTION = "build"
CATEGORY = "IAMCCS/LoRA"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
def build(self, model, generation_count, log_prefix, model_type="flow", default_lora=None, linx=None, unique_id=None, **kwargs):
generation_count = max(1, int(generation_count or 1))
fingerprints_to_model = {}
generation_to_fingerprint = {}
generation_reports = {}
linx_payload_out = None
for generation_index in range(generation_count):
lora_stack, _active_slots, schedule_report, linx_payload = IAMCCS_WanLoRASchedule().schedule(
generation_index=generation_index,
log_prefix=log_prefix,
model_type=model_type,
default_lora=default_lora,
linx=linx,
unique_id=unique_id,
**kwargs,
)
linx_payload_out = linx_payload
fingerprint = _lora_fingerprint(lora_stack)
inject_report = _inject_debug_report(lora_stack)
generation_to_fingerprint[generation_index] = fingerprint
generation_reports[generation_index] = {
"schedule": schedule_report,
"default": inject_report["default_summary"],
"scheduled": _origin_names(lora_stack, "scheduled"),
"stack": _stack_summary(lora_stack),
"always_on_present": inject_report["has_default"],
"scheduled_present": inject_report["has_scheduled"],
"fingerprint": repr(fingerprint),
}
_log.info(
"[IAMCCS_BuildScheduledWanModelBank][inject-check] generation=%s/%s | log_prefix=%s | always_on_present=%s | always_on=%s | scheduled_present=%s | scheduled=%s | stack=%s | fingerprint=%s",
generation_index,
generation_count - 1,
log_prefix,
"YES" if inject_report["has_default"] else "NO",
inject_report["default_summary"],
"YES" if inject_report["has_scheduled"] else "NO",
inject_report["scheduled_summary"],
inject_report["stack_summary"],
repr(fingerprint),
)
if fingerprint not in fingerprints_to_model:
model_variant, _cached = _apply_lora_stack_to_model(model, lora_stack)
fingerprints_to_model[fingerprint] = model_variant
bank = {
"generation_count": generation_count,
"generation_to_fingerprint": generation_to_fingerprint,
"fingerprints_to_model": fingerprints_to_model,
"reports": generation_reports,
"log_prefix": str(log_prefix or "WAN LoRA schedule"),
}
summary_parts = []
for generation_index in range(generation_count):
info = generation_reports[generation_index]
summary_parts.append(
f"g{generation_index}:always_on={info['default']} | scheduled={info['scheduled'] if info['scheduled'] != 'none' else 'default_only'}"
)
report = (
f"generation_count={generation_count} | unique_variants={len(fingerprints_to_model)} | "
f"schedule_map={' ; '.join(summary_parts)}"
)
_log.info("[IAMCCS_BuildScheduledWanModelBank] %s", report)
_flush_log_handlers()
return (bank, report, linx_payload_out)
class IAMCCS_SelectScheduledWanModelFromConditioning:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_bank": (_MODEL_BANK_TYPE,),
"positive": ("CONDITIONING", {"lazy": True}),
"negative": ("CONDITIONING", {"lazy": True}),
}
}
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "INT", "STRING")
RETURN_NAMES = ("model", "positive", "negative", "generation_index", "report")
FUNCTION = "select"
CATEGORY = "IAMCCS/LoRA"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
def check_lazy_status(self, positive=None, negative=None, **kwargs):
needed = []
if positive is None:
needed.append("positive")
if negative is None:
needed.append("negative")
return needed
def select(self, model_bank, positive, negative):
generation_index, generation_source = _extract_generation_index_from_conditioning(positive, negative)
log_prefix = str((model_bank or {}).get("log_prefix") or "WAN LoRA schedule")
generation_index, model_out, info = _select_model_from_bank(model_bank, generation_index, log_prefix)
cond_flags = _conditioning_flags(positive, negative)
scheduled = info.get("scheduled", "none")
always_on = info.get("default", "none")
color = _ANSI_GREEN if scheduled != "none" else _ANSI_YELLOW
report = (
f"generation_index={generation_index} | source={generation_source} | "
f"always_on={always_on} | scheduled={scheduled} | schedule={info.get('schedule', 'unknown')}"
)
_log.info(
"%s[IAMCCS_SelectScheduledWanModelFromConditioning] generation=%s source=%s | clip_vision=%s reference_latents=%s concat_latent=%s concat_mask=%s | always_on=%s | scheduled=%s%s",
color,
generation_index,
generation_source,
"YES" if cond_flags["clip_vision"] else "NO",
"YES" if cond_flags["reference_latents"] else "NO",
"YES" if cond_flags["concat_latent_image"] else "NO",
"YES" if cond_flags["concat_mask"] else "NO",
always_on,
scheduled,
_ANSI_RESET,
)
_flush_log_handlers()
return (model_out, positive, negative, generation_index, report)
class IAMCCS_SelectScheduledWanModelPairFromConditioning:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"high_model_bank": (_MODEL_BANK_TYPE,),
"low_model_bank": (_MODEL_BANK_TYPE,),
"positive": ("CONDITIONING", {"lazy": True}),
"negative": ("CONDITIONING", {"lazy": True}),
}
}
RETURN_TYPES = ("MODEL", "MODEL", "CONDITIONING", "CONDITIONING", "INT", "STRING")
RETURN_NAMES = ("high_model", "low_model", "positive", "negative", "generation_index", "report")
FUNCTION = "select_pair"
CATEGORY = "IAMCCS/LoRA"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
def check_lazy_status(self, positive=None, negative=None, **kwargs):
needed = []
if positive is None:
needed.append("positive")
if negative is None:
needed.append("negative")
return needed
def select_pair(self, high_model_bank, low_model_bank, positive, negative):
generation_index, generation_source = _extract_generation_index_from_conditioning(positive, negative)
high_log_prefix = str((high_model_bank or {}).get("log_prefix") or "WAN LoRA schedule HIGH")
low_log_prefix = str((low_model_bank or {}).get("log_prefix") or "WAN LoRA schedule LOW")
generation_index, high_model_out, high_info = _select_model_from_bank(
high_model_bank,
generation_index,
high_log_prefix,
)
generation_index, low_model_out, low_info = _select_model_from_bank(
low_model_bank,
generation_index,
low_log_prefix,
)
cond_flags = _conditioning_flags(positive, negative)
high_default = high_info.get("default", "none")
low_default = low_info.get("default", "none")
high_scheduled = high_info.get("scheduled", "none")
low_scheduled = low_info.get("scheduled", "none")
report = (
f"generation_index={generation_index} | source={generation_source} | "
f"high_always_on={high_default} | low_always_on={low_default} | "
f"high_scheduled={high_scheduled} | low_scheduled={low_scheduled}"
)
_log.info(
"%s[IAMCCS_SelectScheduledWanModelPairFromConditioning] generation=%s source=%s | clip_vision=%s reference_latents=%s concat_latent=%s concat_mask=%s | high_always_on=%s | low_always_on=%s | high_scheduled=%s | low_scheduled=%s%s",
_ANSI_GREEN if (high_scheduled != "none" or low_scheduled != "none") else _ANSI_YELLOW,
generation_index,
generation_source,
"YES" if cond_flags["clip_vision"] else "NO",
"YES" if cond_flags["reference_latents"] else "NO",
"YES" if cond_flags["concat_latent_image"] else "NO",
"YES" if cond_flags["concat_mask"] else "NO",
high_default,
low_default,
high_scheduled,
low_scheduled,
_ANSI_RESET,
)
_flush_log_handlers()
return (high_model_out, low_model_out, positive, negative, generation_index, report)
class IAMCCS_ApplyScheduledWanLoRAFromConditioning:
"""Resolve generation index from conditioning metadata, then apply scheduled WAN LoRAs.
This avoids wiring easy forLoopStart.index into the MODEL branch entirely.
The effective generation index comes from conditioning metadata emitted by
IAMCCS_WanIndexedPromptEncode and therefore stays synchronized with the
prompt/segment that actually reaches the sampler.
"""
@classmethod
def INPUT_TYPES(cls):
base = IAMCCS_WanLoRASchedule.INPUT_TYPES()
base_required = dict(base.get("required", {}))
base_required.pop("generation_index", None)
required = {
"model": ("MODEL",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
}
required.update(base_required)
optional = dict(base.get("optional", {}))
hidden = {"unique_id": "UNIQUE_ID"}
return {"required": required, "optional": optional, "hidden": hidden}
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "INT", "STRING", "IAMCCS_WAN_LORA_LINX")
RETURN_NAMES = ("model", "positive", "negative", "generation_index", "report", "linx")
FUNCTION = "apply_scheduled"
CATEGORY = "IAMCCS/LoRA"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
def apply_scheduled(self, model, positive, negative, log_prefix, model_type="flow", default_lora=None, linx=None, unique_id=None, **kwargs):
generation_index, generation_source = _extract_generation_index_from_conditioning(positive, negative)
cond_flags = _conditioning_flags(positive, negative)
lora, _active_slots, schedule_report, linx_payload = IAMCCS_WanLoRASchedule().schedule(
generation_index=generation_index,
log_prefix=log_prefix,
model_type=model_type,
default_lora=default_lora,
linx=linx,
unique_id=unique_id,
**kwargs,
)
model_out, positive_out, negative_out, apply_report = IAMCCS_ApplyLoRAHooksToConditioning().apply(
model=model,
positive=positive,
negative=negative,
lora=lora,
)
report = (
f"generation_index={int(generation_index)} | "
f"schedule={schedule_report} | "
f"apply={apply_report}"
)
scheduled_summary = _origin_names(lora, "scheduled")
color = _ANSI_GREEN if scheduled_summary != "none" else _ANSI_YELLOW
_log.info(
"%s[IAMCCS_ApplyScheduledWanLoRAFromConditioning] generation=%s source=%s | clip_vision=%s reference_latents=%s concat_latent=%s concat_mask=%s | scheduled=%s%s",
color,
int(generation_index),
generation_source,
"YES" if cond_flags["clip_vision"] else "NO",
"YES" if cond_flags["reference_latents"] else "NO",
"YES" if cond_flags["concat_latent_image"] else "NO",
"YES" if cond_flags["concat_mask"] else "NO",
scheduled_summary,
_ANSI_RESET,
)
_flush_log_handlers()
return (model_out, positive_out, negative_out, int(generation_index), report, linx_payload)
NODE_CLASS_MAPPINGS = {
"IAMCCS_WanLoRAHookSchedule": IAMCCS_WanLoRAHookSchedule,
"IAMCCS_ApplyLoRAHooksToConditioning": IAMCCS_ApplyLoRAHooksToConditioning,
"IAMCCS_ApplyScheduledWanLoRAFromConditioning": IAMCCS_ApplyScheduledWanLoRAFromConditioning,
"IAMCCS_BuildScheduledWanModelBank": IAMCCS_BuildScheduledWanModelBank,
"IAMCCS_SelectScheduledWanModelFromConditioning": IAMCCS_SelectScheduledWanModelFromConditioning,
"IAMCCS_SelectScheduledWanModelPairFromConditioning": IAMCCS_SelectScheduledWanModelPairFromConditioning,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"IAMCCS_WanLoRAHookSchedule": "LoRA Schedule (WAN, hooks)",
"IAMCCS_ApplyLoRAHooksToConditioning": "Apply LoRA Hooks to Conditioning",
"IAMCCS_ApplyScheduledWanLoRAFromConditioning": "Apply Scheduled WAN LoRA From Conditioning",
"IAMCCS_BuildScheduledWanModelBank": "Build Scheduled WAN Model Bank",
"IAMCCS_SelectScheduledWanModelFromConditioning": "Select Scheduled WAN Model From Conditioning",
"IAMCCS_SelectScheduledWanModelPairFromConditioning": "Select Scheduled WAN Model Pair From Conditioning",
}
+216
View File
@@ -0,0 +1,216 @@
import logging
import folder_paths
from .iamccs_wan_lora_schedule import (
_DEFAULT_MODEL_TYPE,
_LINX_TYPE,
_annotate_lora_entry,
_coerce_lora_stack,
_load_internal_lora_entry,
_preset_matches,
_resolve_linx_name,
_stack_names,
)
from .iamccs_wan_lora_stack import (
IAMCCS_ModelWithLoRA,
_lora_stack_debug_context,
_lora_stack_debug_summary,
)
_log = logging.getLogger("IAMCCS.LoRA.RuntimeBridge")
def _runtime_slot_items(linx_payload):
if not isinstance(linx_payload, dict):
return []
slot_map = linx_payload.get("slot_map") or {}
if not isinstance(slot_map, dict):
return []
return sorted(slot_map.items())
def _coerce_generation_index(value) -> int:
try:
return max(0, int(value))
except Exception:
return 0
def _resolved_model_type(model_type_override: str, linx_payload) -> str:
if model_type_override and model_type_override != "inherit":
return str(model_type_override)
if isinstance(linx_payload, dict):
linx_model_type = str(linx_payload.get("model_type") or "").strip()
if linx_model_type:
return linx_model_type
return _DEFAULT_MODEL_TYPE
class IAMCCS_WanLoRARuntimeBridge:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"generation_index": ("INT", {"default": 0, "min": 0, "max": 1000000, "step": 1}),
"inject_tag": ("STRING", {"default": "inject"}),
"log_prefix": ("STRING", {"default": "WAN LoRA runtime bridge"}),
"model_type_override": (["inherit", "wan2x", "flow", "standard"], {"default": "inherit"}),
},
"optional": {
"default_lora": ("LORA",),
"linx": (_LINX_TYPE,),
},
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = ("LORA", "INT", "STRING", "INT")
RETURN_NAMES = ("lora", "active_slots", "report", "generation_index")
FUNCTION = "resolve"
CATEGORY = "IAMCCS/LoRA"
def resolve(self, generation_index, inject_tag, log_prefix, model_type_override="inherit", default_lora=None, linx=None, unique_id=None):
generation_index = _coerce_generation_index(generation_index)
inject_tag = str(inject_tag or "inject")
log_prefix = str(log_prefix or "WAN LoRA runtime bridge")
model_type = _resolved_model_type(model_type_override, linx)
available_loras = set(folder_paths.get_filename_list("loras"))
resolved_stack = []
active_slot_labels = []
load_cache = {}
for entry in _coerce_lora_stack(default_lora):
resolved_stack.append(
_annotate_lora_entry(
entry,
origin=str(entry.get("_iamccs_lora_origin") or "default"),
generation_index=generation_index,
log_prefix=log_prefix,
prompt_id=entry.get("_iamccs_prompt_id"),
unique_id=unique_id,
slot=entry.get("_iamccs_schedule_slot"),
rule=entry.get("_iamccs_schedule_rule"),
)
)
for slot_key, slot_data in _runtime_slot_items(linx):
if not isinstance(slot_data, dict):
continue
try:
slot = int(str(slot_key).split("_")[-1])
except Exception:
continue
local_name = str(slot_data.get("name") or "no")
resolved_name = _resolve_linx_name(local_name, slot, linx, available_loras)
matched, preset_report = _preset_matches(
generation_index,
str(slot_data.get("preset") or "custom range"),
int(slot_data.get("start", slot - 1) or 0),
int(slot_data.get("end", slot - 1) or 0),
)
if not matched:
continue
lora_entry = _load_internal_lora_entry(
resolved_name,
float(slot_data.get("strength") or 0.0),
model_type,
load_cache,
)
if not lora_entry:
continue
resolved_stack.append(
_annotate_lora_entry(
lora_entry,
origin="runtime scheduled",
generation_index=generation_index,
log_prefix=log_prefix,
prompt_id="",
unique_id=unique_id,
slot=slot,
rule=preset_report,
)
)
active_slot_labels.append(f"{slot:02d}:{resolved_name}")
report = (
f"{inject_tag}: generation_index={generation_index}"
f" | active_slots={len(active_slot_labels)}"
f" | model_type={model_type}"
f" | entries={len(resolved_stack)}"
f" | active={', '.join(active_slot_labels) if active_slot_labels else 'default_only'}"
)
if resolved_stack:
_log.info(
"[%s][%s] generation=%s resolved_stack=%s | %s",
log_prefix,
inject_tag,
generation_index,
_lora_stack_debug_summary(resolved_stack),
_lora_stack_debug_context(resolved_stack),
)
else:
_log.warning(
"[%s][%s] generation=%s resolved no active LoRA entries",
log_prefix,
inject_tag,
generation_index,
)
return (resolved_stack, len(active_slot_labels), report, generation_index)
class IAMCCS_ModelWithLoRA_RuntimeBridge:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"lora": ("LORA",),
"generation_index": ("INT", {"default": 0, "min": 0, "max": 1000000, "step": 1}),
"inject_tag": ("STRING", {"default": "inject"}),
"log_prefix": ("STRING", {"default": "WAN LoRA runtime apply"}),
}
}
RETURN_TYPES = ("MODEL", "STRING")
RETURN_NAMES = ("model", "report")
FUNCTION = "apply"
CATEGORY = "IAMCCS/LoRA"
def apply(self, model, lora, generation_index, inject_tag, log_prefix):
generation_index = _coerce_generation_index(generation_index)
inject_tag = str(inject_tag or "inject")
log_prefix = str(log_prefix or "WAN LoRA runtime apply")
report = (
f"{inject_tag}: generation_index={generation_index}"
f" | entries={len(lora or [])}"
f" | stack={_stack_names(lora or [])}"
)
if lora:
_log.info(
"[%s][%s] generation=%s apply request | %s",
log_prefix,
inject_tag,
generation_index,
_lora_stack_debug_summary(lora),
)
else:
_log.warning(
"[%s][%s] generation=%s apply request with empty LoRA stack",
log_prefix,
inject_tag,
generation_index,
)
model_out = IAMCCS_ModelWithLoRA().apply(model, lora)[0]
return (model_out, report)
+244 -23
View File
@@ -1,4 +1,5 @@
import logging
import sys
from collections import OrderedDict
import comfy.utils
@@ -26,6 +27,18 @@ _AUTO_INDEX_STATE: OrderedDict[tuple[str, str], int] = OrderedDict()
_MAX_AUTO_INDEX_STATE = 64
def _is_ignored_lora_name(name: str) -> bool:
normalized = str(name or "").replace("\\", "/").strip()
if not normalized:
return True
leaf = normalized.rsplit("/", 1)[-1]
return normalized.startswith("__MACOSX/") or "/__MACOSX/" in normalized or leaf.startswith("._")
def _get_valid_lora_list() -> list[str]:
return [name for name in folder_paths.get_filename_list("loras") if not _is_ignored_lora_name(name)]
def _annotate_lora_entry(entry: dict, *, origin: str, generation_index: int, log_prefix: str, prompt_id: str | None, unique_id, slot: int | None = None, rule: str | None = None) -> dict:
annotated = dict(entry)
annotated["_iamccs_lora_origin"] = str(origin or "unknown")
@@ -129,6 +142,123 @@ def _stack_names(stack: list[dict]) -> str:
return ", ".join(names) if names else "empty"
def _stack_debug_entries(stack: list[dict]) -> str:
if not stack:
return "empty"
parts = []
for entry in stack:
parts.append(
f"{entry.get('name', 'unnamed')}"
f"(strength={float(entry.get('strength', 0.0) or 0.0):.3f},"
f" origin={entry.get('_iamccs_lora_origin', 'unknown')},"
f" gen={entry.get('_iamccs_generation_index', 'n/a')},"
f" slot={entry.get('_iamccs_schedule_slot', 'n/a')},"
f" rule={entry.get('_iamccs_schedule_rule', 'n/a')})"
)
return "; ".join(parts)
def _diag_value(value) -> str:
text = str(value or "").strip()
return text if text else "-"
def _infer_branch_from_name(name: str) -> str | None:
normalized = str(name or "").replace("\\", "/").lower()
if not normalized or normalized == "no":
return None
low_tokens = ("_low", "-low", "/low", "_ln_", "-ln_", "low_")
high_tokens = ("_high", "-high", "/high", "_hn_", "-hn_", "high_")
if any(token in normalized for token in low_tokens):
return "LOW"
if any(token in normalized for token in high_tokens):
return "HIGH"
return None
def _expected_branch_from_prefix(log_prefix: str) -> str | None:
normalized = f" {str(log_prefix or '').lower()} "
if " low " in normalized or normalized.endswith(" low "):
return "LOW"
if " high " in normalized or normalized.endswith(" high "):
return "HIGH"
return None
def _branch_names(stack: list[dict], branch: str) -> list[str]:
names = []
for entry in stack or []:
name = str(entry.get("name") or "")
if _infer_branch_from_name(name) == branch:
names.append(name)
return names
def _validate_branch_consistency(log_prefix: str, generation_index: int, stack: list[dict]) -> str | None:
expected_branch = _expected_branch_from_prefix(log_prefix)
high_names = _branch_names(stack, "HIGH")
low_names = _branch_names(stack, "LOW")
if high_names and low_names:
return (
f"[{log_prefix}][branch-check] generation={generation_index} MIXED stack detected | "
f"HIGH={', '.join(high_names)} | LOW={', '.join(low_names)}"
)
if expected_branch == "LOW" and high_names:
return (
f"[{log_prefix}][branch-check] generation={generation_index} EXPECTED_LOW but found HIGH entries="
f"{', '.join(high_names)}"
)
if expected_branch == "HIGH" and low_names:
return (
f"[{log_prefix}][branch-check] generation={generation_index} EXPECTED_HIGH but found LOW entries="
f"{', '.join(low_names)}"
)
if expected_branch:
matched = high_names if expected_branch == "HIGH" else low_names
_log.info(
"[%s][branch-check] generation=%s OK expected=%s entries=%s",
log_prefix,
generation_index,
expected_branch,
", ".join(matched) if matched else "none-tagged",
)
return None
def _flush_log_handlers():
seen_handlers = set()
manager = getattr(logging.Logger, "manager", None)
logger_dict = getattr(manager, "loggerDict", {}) if manager is not None else {}
for logger_name in ("IAMCCS.LoRA.Schedule", None):
logger = logging.getLogger(logger_name) if logger_name else logging.getLogger()
for handler in getattr(logger, "handlers", []):
seen_handlers.add(handler)
for logger in logger_dict.values():
if isinstance(logger, logging.Logger):
for handler in getattr(logger, "handlers", []):
seen_handlers.add(handler)
for handler in seen_handlers:
try:
handler.flush()
except Exception:
pass
for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
if stream is None:
continue
try:
stream.flush()
except Exception:
pass
def _candidate_low_names(name: str) -> list[str]:
source = str(name or "")
if not source or source == "no":
@@ -162,24 +292,41 @@ def _candidate_low_names(name: str) -> list[str]:
return out
def _resolve_linx_name(local_name: str, slot: int, linx_payload, available_loras: set[str]) -> str:
def _resolve_linx_name_debug(local_name: str, slot: int, linx_payload, available_loras: set[str]) -> tuple[str, str, dict]:
local = str(local_name or "no")
if local != "no":
return local
return local, "local", {"local_name": local, "suggested_low_name": "", "source_name": ""}
if not isinstance(linx_payload, dict):
return local
return local, "local:no-linx", {"local_name": local, "suggested_low_name": "", "source_name": ""}
slot_map = linx_payload.get("slot_map") or {}
slot_data = slot_map.get(f"slot_{slot:02d}") or {}
suggested = str(slot_data.get("suggested_low_name") or "")
if suggested and suggested in available_loras:
return suggested
return suggested, "linx:suggested_low_name", {
"local_name": local,
"suggested_low_name": suggested,
"source_name": str(slot_data.get("name") or ""),
}
source_name = str(slot_data.get("name") or "")
for candidate in _candidate_low_names(source_name):
if candidate in available_loras:
return candidate
return local
return candidate, "linx:derived_low_candidate", {
"local_name": local,
"suggested_low_name": suggested,
"source_name": source_name,
}
return local, "local:no-match", {
"local_name": local,
"suggested_low_name": suggested,
"source_name": source_name,
}
def _resolve_linx_name(local_name: str, slot: int, linx_payload, available_loras: set[str]) -> str:
resolved, _source, _details = _resolve_linx_name_debug(local_name, slot, linx_payload, available_loras)
return resolved
def _build_linx_payload(model_type: str, kwargs: dict, available_loras: set[str]) -> dict:
@@ -210,6 +357,10 @@ def _load_internal_lora_entry(name: str, strength: float, model_type: str, cache
if not name or name == "no":
return None
if _is_ignored_lora_name(name):
_log.warning("[WAN LoRA schedule] ignored invalid LoRA entry: %s", name)
return None
strength_f = float(strength)
if strength_f == 0.0:
return None
@@ -219,7 +370,17 @@ def _load_internal_lora_entry(name: str, strength: float, model_type: str, cache
state_dict = cache[cache_key]
else:
path = folder_paths.get_full_path_or_raise("loras", name)
state_dict = comfy.utils.load_torch_file(path, safe_load=True)
try:
state_dict = comfy.utils.load_torch_file(path, safe_load=True)
except UnicodeDecodeError:
_log.warning(
"[WAN LoRA schedule] skipped unreadable LoRA file '%s' (invalid safetensors header / likely resource-fork or corrupted file)",
name,
)
return None
except Exception:
_log.exception("[WAN LoRA schedule] failed loading LoRA '%s'", name)
return None
if str(model_type or _DEFAULT_MODEL_TYPE) != "standard":
state_dict = standardize_wan_lora_keys(state_dict)
cache[cache_key] = state_dict
@@ -234,13 +395,14 @@ def _load_internal_lora_entry(name: str, strength: float, model_type: str, cache
class IAMCCS_WanLoRASchedule:
@classmethod
def INPUT_TYPES(cls):
lora_list = folder_paths.get_filename_list("loras") + ["no"]
lora_list = _get_valid_lora_list() + ["no"]
required = {
# NOTE: Must NOT use lazy:True here. With lazy:True, ComfyUI caches the
# node output after the first call (gen_idx=None→0) and skips re-execution
# on subsequent loop iterations when only generation_index changes, causing
# the schedule to always behave as if generation_index==0.
"generation_index": ("INT", {"default": 0, "min": 0, "max": 1000000, "step": 1}),
# lazy=True breaks the static validation cycle created by wiring forLoopStart.index
# here. It only affects validate_inputs (execution.py L878 skips cycle traversal).
# At runtime, since check_lazy_status is NOT defined on this node, ComfyUI evaluates
# all inputs normally before calling schedule() — no caching side-effect.
# IS_CHANGED=nan ensures re-execution every iteration regardless.
"generation_index": ("INT", {"default": 0, "min": 0, "max": 1000000, "step": 1, "lazy": True}),
"log_prefix": ("STRING", {"default": "WAN LoRA schedule"}),
"model_type": (["wan2x", "flow", "standard"], {"default": _DEFAULT_MODEL_TYPE}),
}
@@ -283,24 +445,32 @@ class IAMCCS_WanLoRASchedule:
# would freeze the LoRA selection on the first iteration.
return float("nan")
def check_lazy_status(self, generation_index, **kwargs):
# Two-pass lazy protocol: lazy=True in INPUT_TYPES breaks the static
# validation cycle (execution.py L878). This method forces the executor
# to evaluate the upstream (forLoopStart) before calling schedule(),
# so generation_index is always the real iteration index, never None.
if generation_index is None:
return ["generation_index"]
return []
def schedule(self, generation_index, log_prefix, model_type=_DEFAULT_MODEL_TYPE, default_lora=None, linx=None, unique_id=None, **kwargs):
# Loop-safe mode: when generation_index is left at its hidden default 0,
# use a per-prompt internal counter instead of wiring the easy-use loop
# index into the MODEL branch, which creates a prompt-validation cycle.
# With check_lazy_status in place, generation_index is always the real
# iteration index when a wire is connected (never None at execution time).
# None only occurs in truly unwired / fallback contexts.
generation_source = "input"
if generation_index is None:
generation_index = _next_auto_generation_index(unique_id)
# No upstream link or upstream not resolvable — use literal 0.
# (check_lazy_status would have forced evaluation if a wire existed.)
generation_index = 0
generation_source = "auto:none"
else:
generation_index = int(generation_index)
if generation_index == 0:
generation_index = _next_auto_generation_index(unique_id)
generation_source = "auto:zero"
generation_index = max(0, generation_index)
log_prefix = str(log_prefix or "WAN LoRA schedule")
model_type = str(model_type or _DEFAULT_MODEL_TYPE)
prompt_id = _get_current_prompt_id()
available_loras = set(folder_paths.get_filename_list("loras"))
available_loras = set(_get_valid_lora_list())
out = [
_annotate_lora_entry(
entry,
@@ -315,6 +485,7 @@ class IAMCCS_WanLoRASchedule:
active_slots = []
cache: dict = {}
linx_payload = _build_linx_payload(model_type, kwargs, available_loras)
scheduled_names = []
if out:
_log.info(
@@ -326,6 +497,15 @@ class IAMCCS_WanLoRASchedule:
generation_source,
_stack_names(out),
)
_log.info(
"[%s][diag] prompt=%s node=%s generation=%s | default_stack_debug=%s",
log_prefix,
prompt_id or "unknown",
unique_id if unique_id is not None else "unknown",
generation_index,
_stack_debug_entries(out),
)
_flush_log_handlers()
for slot in range(1, _SLOT_COUNT + 1):
preset = kwargs.get(f"slot_{slot:02d}_preset", "custom range")
@@ -336,12 +516,32 @@ class IAMCCS_WanLoRASchedule:
continue
slot_stack = []
slot_name = _resolve_linx_name(
str(kwargs.get(f"slot_{slot:02d}_lora_name") or "no"),
raw_slot_name = str(kwargs.get(f"slot_{slot:02d}_lora_name") or "no")
slot_name, slot_name_source, slot_name_details = _resolve_linx_name_debug(
raw_slot_name,
slot,
linx,
available_loras,
)
_log.info(
"[%s][diag] prompt=%s node=%s generation=%s slot=%02d matched=%s preset=%s start=%s end=%s raw_name=%s resolved_name=%s resolve_source=%s linx_source_name=%s linx_suggested_low=%s strength=%.3f",
log_prefix,
prompt_id or "unknown",
unique_id if unique_id is not None else "unknown",
generation_index,
slot,
matched,
preset,
start,
end,
_diag_value(raw_slot_name),
_diag_value(slot_name),
slot_name_source,
_diag_value(slot_name_details.get("source_name")),
_diag_value(slot_name_details.get("suggested_low_name")),
float(kwargs.get(f"slot_{slot:02d}_strength", 0.0) or 0.0),
)
_flush_log_handlers()
entry = _load_internal_lora_entry(
slot_name,
float(kwargs.get(f"slot_{slot:02d}_strength", 0.0) or 0.0),
@@ -367,22 +567,43 @@ class IAMCCS_WanLoRASchedule:
out.extend(slot_stack)
active_slots.append(f"slot_{slot:02d}:{preset_report} => {_stack_names(slot_stack)}")
scheduled_names.extend(
f"slot_{slot:02d}:{entry.get('name') or 'unnamed'}({float(entry.get('strength', 0.0) or 0.0)})"
for entry in slot_stack
)
scheduled_active = "YES" if scheduled_names else "NO"
report = (
f"generation_index={generation_index} | active={' + '.join(active_slots) if active_slots else 'default_only'} | "
f"scheduled_active={scheduled_active} | "
f"scheduled_names={'; '.join(scheduled_names) if scheduled_names else 'none'} | "
f"entries={len(out)}"
)
if out:
_log.info("[%s] %s", log_prefix, report)
branch_error = _validate_branch_consistency(log_prefix, generation_index, out)
if branch_error:
_log.error("%s", branch_error)
_flush_log_handlers()
raise RuntimeError(branch_error)
_log.info(
"[%s] generation=%s scheduled_active=%s | scheduled_names=%s",
log_prefix,
generation_index,
scheduled_active,
"; ".join(scheduled_names) if scheduled_names else "none",
)
_log.info(
"[%s] generation=%s resolved_stack=%s",
log_prefix,
generation_index,
_stack_names(out),
)
_flush_log_handlers()
else:
_log.warning("[%s] No LoRA active | generation_index=%s", log_prefix, generation_index)
_flush_log_handlers()
return (out, len(active_slots), report, linx_payload)
+46
View File
@@ -65,6 +65,35 @@ def _lora_stack_debug_context(lora) -> str:
return " | ".join(parts)
def _lora_stack_origin_names(lora, origin: str) -> str:
if not lora:
return "none"
names = []
for entry in lora:
entry_origin = str(entry.get("_iamccs_lora_origin") or "manual")
if entry_origin != origin:
continue
name = str(entry.get("name") or "unnamed")
strength = float(entry.get("strength", 0.0) or 0.0)
slot = entry.get("_iamccs_schedule_slot")
rule = str(entry.get("_iamccs_schedule_rule") or "")
if slot is not None:
names.append(f"slot={int(slot):02d}:{name}({strength}) [{rule or origin}]")
else:
names.append(f"{name}({strength})")
return "; ".join(names) if names else "none"
def _lora_stack_has_origin(lora, origin: str) -> bool:
if not lora:
return False
for entry in lora:
if str(entry.get("_iamccs_lora_origin") or "manual") == origin:
return True
return False
def _cache_put(cache: OrderedDict, key, value, max_size: int):
cache[key] = value
cache.move_to_end(key)
@@ -248,6 +277,14 @@ class IAMCCS_ModelWithLoRA:
}
}
@classmethod
def IS_CHANGED(cls, **kwargs):
# The upstream schedule node may resolve a different per-generation stack
# while the graph wiring itself remains unchanged inside Easy-Use loops.
# Force re-evaluation so the current loop iteration can request fresh LORA
# metadata instead of reusing the first patched MODEL forever.
return float("nan")
RETURN_TYPES = ("MODEL",)
FUNCTION = "apply"
CATEGORY = "IAMCCS/LoRA"
@@ -258,6 +295,9 @@ class IAMCCS_ModelWithLoRA:
stack_context = _lora_stack_debug_context(lora)
stack_summary = _lora_stack_debug_summary(lora)
default_summary = _lora_stack_origin_names(lora, "default")
scheduled_summary = _lora_stack_origin_names(lora, "scheduled")
scheduled_active = "YES" if _lora_stack_has_origin(lora, "scheduled") else "NO"
signature = tuple((str(entry.get("name") or ""), float(entry.get("strength", 0.0) or 0.0)) for entry in lora)
cache_key = (id(model), signature)
cached_model = _PATCHED_MODEL_CACHE.get(cache_key)
@@ -268,6 +308,9 @@ class IAMCCS_ModelWithLoRA:
else:
logging.info(f"[IAMCCS_ModelWithLoRA] ♻ cache hit: {len(signature)} LoRA(s) reused")
logging.info(f"[IAMCCS_ModelWithLoRA] active_stack={stack_summary}")
logging.info(f"[IAMCCS_ModelWithLoRA] scheduled_active={scheduled_active}")
logging.info(f"[IAMCCS_ModelWithLoRA] applied_default={default_summary}")
logging.info(f"[IAMCCS_ModelWithLoRA] applied_scheduled={scheduled_summary}")
return (cached_model,)
model_out = model
@@ -275,6 +318,9 @@ class IAMCCS_ModelWithLoRA:
if stack_context:
logging.info(f"[IAMCCS_ModelWithLoRA] apply request | {stack_context}")
logging.info(f"[IAMCCS_ModelWithLoRA] active_stack={stack_summary}")
logging.info(f"[IAMCCS_ModelWithLoRA] scheduled_active={scheduled_active}")
logging.info(f"[IAMCCS_ModelWithLoRA] applied_default={default_summary}")
logging.info(f"[IAMCCS_ModelWithLoRA] applied_scheduled={scheduled_summary}")
# Installa filtro per sopprimere spam di chiavi opzionali (img_*, diff_m, ecc.)
logger = logging.getLogger()
+75 -1
View File
@@ -217,6 +217,49 @@ def _resolve_prev_tail_profile(
return str(tail_pick_mode or "latest"), max(0, int(skip_last_slots)), float(anchor_blend)
def _ensure_motion_tail_matches_anchor(
motion_tail: torch.Tensor | None,
*,
anchor_reference: torch.Tensor,
logger: logging.Logger,
context: str,
) -> tuple[torch.Tensor | None, bool]:
if motion_tail is None:
return None, True
anchor_shape = tuple(anchor_reference.shape)
motion_shape = tuple(motion_tail.shape)
if motion_tail.dim() != 5 or anchor_reference.dim() != 5:
logger.warning(
"[%s] prev_samples tail rank mismatch, disabling continuity tail. prev=%s anchor=%s",
context,
motion_shape,
anchor_shape,
)
return None, False
if motion_tail.shape[1] != anchor_reference.shape[1]:
logger.warning(
"[%s] prev_samples channel mismatch, disabling continuity tail. prev=%s anchor=%s",
context,
motion_shape,
anchor_shape,
)
return None, False
if motion_tail.shape[3] != anchor_reference.shape[3] or motion_tail.shape[4] != anchor_reference.shape[4]:
logger.warning(
"[%s] prev_samples spatial mismatch, disabling continuity tail. prev=%s anchor=%s",
context,
motion_shape,
anchor_shape,
)
return None, False
return motion_tail, True
def _build_inductive_motion_tail(
prev_samples,
motion_latent_count: int,
@@ -821,7 +864,7 @@ class IAMCCS_WanImageMotion:
),
},
"optional": {
"prev_samples": ("LATENT",),
"prev_samples": ("LATENT", {"lazy": True}),
"clip_vision_output": ("CLIP_VISION_OUTPUT",),
},
}
@@ -833,6 +876,14 @@ class IAMCCS_WanImageMotion:
_log = logging.getLogger("IAMCCS.WanImageMotion")
def check_lazy_status(self, prev_samples=None, use_prev_samples=True, **kwargs):
# Allow prev_samples to participate in loop feedback without tripping
# static prompt validation. If the wire exists but the upstream latent is
# not ready yet, request a second pass after that dependency is executed.
if use_prev_samples and prev_samples is None:
return ["prev_samples"]
return []
def _pick_empty_latent_dtype(self, anchor_dtype: torch.dtype, latent_precision: str) -> torch.dtype:
if latent_precision == "normal":
# Backward-compat alias for older workflows.
@@ -1147,6 +1198,13 @@ class IAMCCS_WanImageMotion:
T_motion,
padding_size,
)
self._log.info(
"[WanImageMotion] conditioning flags | clip_vision=%s add_reference_latents=%s use_prev_samples=%s prev_connected=%s",
"YES" if clip_vision_output is not None else "NO",
"YES" if add_reference_latents else "NO",
"YES" if use_prev_samples else "NO",
"YES" if prev_samples is not None else "NO",
)
if prev_samples is not None and not use_prev_samples:
self._log.info("[WanImageMotion] prev_samples connected but use_prev_samples=False — prev ignored.")
if free_vram is not None:
@@ -1685,6 +1743,14 @@ class WanImageMotionPro:
anchor_reference=anchor_latent,
anchor_blend=resolved_anchor_blend,
)
motion_latent, motion_tail_ok = _ensure_motion_tail_matches_anchor(
motion_latent,
anchor_reference=anchor_latent,
logger=self._log,
context="WanImageMotionPro/raw_mode",
)
if not motion_tail_ok:
motion_count = 0
if apply_motion_to_tail and motion_latent is not None:
motion_latent = _apply_motion_amplitude_to_raw_tail(
@@ -2196,6 +2262,14 @@ class WanImageMotionPro:
anchor_reference=anchor_latent,
anchor_blend=resolved_anchor_blend,
)
motion_latent, motion_tail_ok = _ensure_motion_tail_matches_anchor(
motion_latent,
anchor_reference=anchor_latent,
logger=self._log,
context="WanImageMotionPro",
)
if not motion_tail_ok:
T_motion = 0
has_prev = motion_latent is not None and T_motion > 0
# --- 2. DC Drift Correction ---
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "iamccs-nodes",
"version": "1.4.1",
"version": "1.4.2",
"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."
}
+66
View File
@@ -0,0 +1,66 @@
import { app } from "../../scripts/app.js";
const CINEMATIC_BUILDERS = new Set([
"IAMCCS_LTX2_CinematicPromptComposer",
"IAMCCS_LTX2_CinematicShotLineBuilder",
"IAMCCS_LTX2_CinematicV2VTimelineLineBuilder",
"IAMCCS_LTX2_CinematicLineStacker",
]);
const SIZE_PRESETS = {
IAMCCS_LTX2_CinematicPromptComposer: [520, 620],
IAMCCS_LTX2_CinematicShotLineBuilder: [560, 760],
IAMCCS_LTX2_CinematicV2VTimelineLineBuilder: [590, 840],
IAMCCS_LTX2_CinematicLineStacker: [520, 520],
};
function tuneWidgets(node) {
if (!node?.widgets) return;
for (const widget of node.widgets) {
if (!widget) continue;
const name = String(widget.name || "").toLowerCase();
if (
name.includes("prompt") ||
name.includes("dialogue") ||
name.includes("voice") ||
name.includes("continuity") ||
name.includes("comment") ||
name.startsWith("line_")
) {
widget.computeSize = function(width) {
return [width, name.startsWith("line_") ? 70 : 96];
};
}
}
}
app.registerExtension({
name: "iamccs.cinematic.builders.ui",
async beforeRegisterNodeDef(nodeType, nodeData) {
const name = nodeData?.name || "";
if (!CINEMATIC_BUILDERS.has(name)) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
try {
this.color = "#243447";
this.bgcolor = "#18232f";
this.boxcolor = "#4f9fd8";
this.shape = typeof LiteGraph !== "undefined" ? LiteGraph.BOX_SHAPE : 0;
const preset = SIZE_PRESETS[name];
if (preset && (!this.size || this.size[0] < preset[0])) {
this.size = [...preset];
}
tuneWidgets(this);
this.setDirtyCanvas(true, true);
} catch {
// UI-only enhancement; never block node loading.
}
return result;
};
},
});
+682
View File
@@ -0,0 +1,682 @@
import { app } from "../../scripts/app.js";
const EDITOR_VERSION = "2026-04-25-1";
const CUT_MODES = ["hard_cut", "continuity_cut", "soft_cut", "match_cut"];
const AUDIO_OPTIONS = ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
const REF_OPTIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "1>2", "2>1", "1+2", "2+3", "3+1"];
const SOURCE_OPTIONS = ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
const SOURCE_RANGE_OPTIONS = ["all", "0+96", "24+84", "48+120", "tail120", "0-96", "24-108"];
const V2V_MODES = [
"v2v_source_plus_reference",
"v2v_source_context",
"i2v_from_reference",
"two_segments_if_long",
"loop_if_long",
];
const FRAMING_OPTIONS = [
"custom",
"wide establishing shot",
"medium shot",
"medium close-up",
"close-up",
"extreme close-up",
"over-the-shoulder",
"reverse angle",
"insert detail",
"two-shot",
"tracking shot",
"V2V close-up",
"V2V reverse angle",
"V2V wide shot",
];
const CONFIGS = {
IAMCCS_LTX2_CinematicMultiGenPlanner: {
kind: "multigen",
widgetName: "shot_lines",
title: "IAMCCS MultiGen Timeline Editor",
lineFields: ["seconds", "cut", "ref", "audio", "label", "prompt", "dialogue", "voice"],
},
IAMCCS_LTX2_CinematicV2VTimelinePlanner: {
kind: "v2v",
widgetName: "timeline_lines",
title: "IAMCCS V2V Timeline Editor",
lineFields: ["seconds", "cut", "source", "sourceRange", "ref", "audio", "label", "prompt", "dialogue", "voice", "v2vMode"],
},
};
function nodeClassName(node) {
return String(node?.comfyClass || node?.type || node?.constructor?.comfyClass || "");
}
function getConfigForNodeData(nodeData) {
const name = String(nodeData?.name || "");
return CONFIGS[name] || null;
}
function getConfigForNode(node) {
return CONFIGS[nodeClassName(node)] || null;
}
function getWidget(node, name) {
return node?.widgets?.find((widget) => widget?.name === name || widget?.label === name) || null;
}
function setWidgetValue(node, widgetName, value) {
const widget = getWidget(node, widgetName);
if (!widget) return false;
widget.value = value;
try {
widget.callback?.(value, app.canvas, node);
} catch {
// ignore UI callback issues
}
try {
node.setDirtyCanvas?.(true, true);
app.graph?.setDirtyCanvas?.(true, true);
} catch {
// ignore
}
return true;
}
function splitPipeLine(line, expectedCount) {
const parts = String(line || "").split("|").map((part) => part.trim());
while (parts.length < expectedCount) parts.push("");
if (parts.length > expectedCount) {
const head = parts.slice(0, expectedCount - 1);
const tail = parts.slice(expectedCount - 1).join(" | ");
return [...head, tail];
}
return parts;
}
function cleanLinePart(value) {
return String(value ?? "").replace(/\s+/g, " ").trim();
}
function joinLine(values) {
return values.map(cleanLinePart).join(" | ");
}
function parseRows(text, config) {
const rows = [];
const fields = config.lineFields;
for (const rawLine of String(text || "").split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
const parts = splitPipeLine(line, fields.length);
const row = {};
fields.forEach((field, index) => {
row[field] = parts[index] || "";
});
row.framing = "custom";
rows.push(normalizeRow(row, config.kind));
}
if (!rows.length) {
return config.kind === "v2v" ? presetRows("v2v_two_camera") : presetRows("multigen_dialogue_4");
}
return rows;
}
function normalizeRow(row, kind) {
const out = { ...row };
out.seconds = out.seconds || "4.0";
out.cut = CUT_MODES.includes(out.cut) ? out.cut : "hard_cut";
out.ref = out.ref || "1";
out.audio = AUDIO_OPTIONS.includes(String(out.audio)) ? String(out.audio) : "0";
out.label = out.label || "shot";
out.prompt = out.prompt || (kind === "v2v" ? "V2V cinematic shot, keep source motion" : "cinematic shot, natural acting");
out.dialogue = out.dialogue || "";
out.voice = out.voice || "";
out.framing = out.framing || "custom";
if (kind === "v2v") {
out.source = SOURCE_OPTIONS.includes(String(out.source)) ? String(out.source) : "1";
out.sourceRange = out.sourceRange || "all";
out.v2vMode = V2V_MODES.includes(out.v2vMode) ? out.v2vMode : "v2v_source_plus_reference";
}
return out;
}
function rowToLine(row, config) {
const prompt = buildPromptWithFraming(row);
if (config.kind === "v2v") {
return joinLine([
row.seconds,
row.cut,
row.source,
row.sourceRange,
row.ref,
row.audio,
row.label,
prompt,
row.dialogue,
row.voice,
row.v2vMode,
]);
}
return joinLine([
row.seconds,
row.cut,
row.ref,
row.audio,
row.label,
prompt,
row.dialogue,
row.voice,
]);
}
function rowsToText(rows, config) {
return rows.map((row) => rowToLine(row, config)).join("\n");
}
function buildPromptWithFraming(row) {
const framing = cleanLinePart(row.framing);
const prompt = cleanLinePart(row.prompt);
if (!framing || framing === "custom") return prompt;
if (prompt.toLowerCase().startsWith(framing.toLowerCase())) return prompt;
return `${framing}. ${prompt}`;
}
function presetRows(key) {
const presets = {
multigen_dialogue_4: [
{ seconds: "4.0", cut: "hard_cut", ref: "1", audio: "1", label: "campo_mara", framing: "medium close-up", prompt: "Mara near the kitchen window, she speaks softly but firmly", dialogue: "You left the city without saying goodbye.", voice: "controlled hurt voice" },
{ seconds: "3.5", cut: "hard_cut", ref: "2", audio: "2", label: "controcampo_elio", framing: "reverse angle", prompt: "Elio at the doorway, he answers without stepping in", dialogue: "I thought silence would protect you.", voice: "low ashamed voice" },
{ seconds: "2.5", cut: "hard_cut", ref: "3", audio: "0", label: "insert_key", framing: "insert detail", prompt: "old brass key on the table, rain reflected on metal", dialogue: "", voice: "" },
{ seconds: "4.5", cut: "continuity_cut", ref: "1>2", audio: "1", label: "ritorno_mara", framing: "close-up", prompt: "Mara returns to silence, slow push-in, visible hesitation", dialogue: "It protected only you.", voice: "almost whispered voice" },
],
multigen_dialogue_2: [
{ seconds: "4.0", cut: "hard_cut", ref: "1", audio: "1", label: "campo_A", framing: "medium close-up", prompt: "character A speaks with restrained emotion, coherent eyeline", dialogue: "I knew you would come back.", voice: "tired intimate low voice" },
{ seconds: "3.5", cut: "hard_cut", ref: "2", audio: "2", label: "controcampo_B", framing: "reverse angle", prompt: "character B listens, then answers, natural reaction timing", dialogue: "I never really left.", voice: "calm low voice" },
],
multigen_monologue: [
{ seconds: "6.0", cut: "hard_cut", ref: "1", audio: "1", label: "monologo_closeup", framing: "close-up", prompt: "single character speaking quietly, subtle facial movement, stable identity", dialogue: "I need you to hear the whole story.", voice: "low confessional voice" },
],
v2v_two_camera: [
{ seconds: "4.0", cut: "hard_cut", source: "1", sourceRange: "0+96", ref: "1", audio: "1", label: "camera_A_mara", framing: "V2V close-up", prompt: "Mara speaking, keep source head movement and natural eye line", dialogue: "You left the city without saying goodbye.", voice: "controlled hurt voice", v2vMode: "v2v_source_plus_reference" },
{ seconds: "3.5", cut: "hard_cut", source: "2", sourceRange: "24+84", ref: "2", audio: "2", label: "camera_B_elio", framing: "V2V reverse angle", prompt: "Elio answering, keep source performance and natural eye line", dialogue: "I thought silence would protect you.", voice: "low ashamed voice", v2vMode: "v2v_source_plus_reference" },
{ seconds: "5.0", cut: "continuity_cut", source: "1", sourceRange: "tail120", ref: "1>2", audio: "0", label: "return_camera_A", framing: "V2V close-up", prompt: "continue from camera A, slow push-in, preserve gesture continuity", dialogue: "", voice: "", v2vMode: "two_segments_if_long" },
],
v2v_single: [
{ seconds: "4.0", cut: "hard_cut", source: "1", sourceRange: "all", ref: "1", audio: "1", label: "v2v_single_take", framing: "V2V close-up", prompt: "preserve original motion, improve cinematic light and texture", dialogue: "", voice: "", v2vMode: "v2v_source_plus_reference" },
],
v2v_long: [
{ seconds: "8.0", cut: "continuity_cut", source: "1", sourceRange: "0+192", ref: "1", audio: "1", label: "long_part_1", framing: "V2V tracking shot", prompt: "continuous movement, preserve direction and speed", dialogue: "", voice: "", v2vMode: "two_segments_if_long" },
{ seconds: "8.0", cut: "continuity_cut", source: "1", sourceRange: "tail192", ref: "1", audio: "1", label: "long_part_2", framing: "V2V tracking shot", prompt: "continue same movement, preserve gesture continuity", dialogue: "", voice: "", v2vMode: "two_segments_if_long" },
],
};
return (presets[key] || presets.multigen_dialogue_4).map((row) => normalizeRow(row, key.startsWith("v2v") ? "v2v" : "multigen"));
}
function ensureStyles() {
if (document.getElementById("iamccs-cinematic-timeline-editor-style")) return;
const style = document.createElement("style");
style.id = "iamccs-cinematic-timeline-editor-style";
style.textContent = `
.iamccs-cine-overlay {
position: fixed;
inset: 0;
z-index: 100000;
background: rgba(5, 8, 12, 0.72);
display: flex;
align-items: center;
justify-content: center;
font-family: Inter, Arial, sans-serif;
color: #eaf1f8;
}
.iamccs-cine-modal {
width: min(96vw, 1500px);
height: min(92vh, 920px);
background: #101820;
border: 1px solid #375064;
box-shadow: 0 24px 80px rgba(0,0,0,0.55);
display: grid;
grid-template-rows: auto auto 1fr auto;
overflow: hidden;
}
.iamccs-cine-header,
.iamccs-cine-toolbar,
.iamccs-cine-footer {
padding: 12px 16px;
border-bottom: 1px solid #263847;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.iamccs-cine-footer {
border-top: 1px solid #263847;
border-bottom: 0;
justify-content: space-between;
}
.iamccs-cine-title {
font-size: 18px;
font-weight: 700;
letter-spacing: 0;
margin-right: auto;
}
.iamccs-cine-help {
color: #9eb2c4;
font-size: 12px;
}
.iamccs-cine-btn,
.iamccs-cine-select,
.iamccs-cine-input,
.iamccs-cine-textarea {
background: #172331;
color: #edf6ff;
border: 1px solid #3c5368;
border-radius: 6px;
font-size: 12px;
font-family: Inter, Arial, sans-serif;
}
.iamccs-cine-btn {
padding: 8px 10px;
cursor: pointer;
white-space: nowrap;
}
.iamccs-cine-btn:hover { background: #20344a; }
.iamccs-cine-btn.primary { background: #2d6f9f; border-color: #4d9bd0; }
.iamccs-cine-btn.danger { background: #552734; border-color: #81445a; }
.iamccs-cine-select,
.iamccs-cine-input {
height: 30px;
padding: 4px 6px;
}
.iamccs-cine-textarea {
min-height: 54px;
padding: 6px;
resize: vertical;
line-height: 1.25;
}
.iamccs-cine-body {
overflow: auto;
padding: 12px 16px;
}
.iamccs-cine-table {
border-collapse: separate;
border-spacing: 0;
min-width: 1250px;
width: 100%;
}
.iamccs-cine-table th {
position: sticky;
top: 0;
background: #172331;
z-index: 1;
border-bottom: 1px solid #3c5368;
color: #bcd0df;
font-size: 11px;
text-align: left;
padding: 7px 6px;
}
.iamccs-cine-table td {
border-bottom: 1px solid #253747;
padding: 6px;
vertical-align: top;
}
.iamccs-cine-row-index {
color: #91a9bc;
font-weight: 700;
text-align: center;
padding-top: 12px;
}
.iamccs-cine-row-actions {
display: grid;
grid-template-columns: repeat(2, 30px);
gap: 4px;
}
.iamccs-cine-row-actions button {
height: 28px;
padding: 0;
}
.iamccs-cine-preview {
width: min(720px, 44vw);
min-height: 86px;
max-height: 130px;
color: #c9d8e5;
background: #0b1118;
}
.iamccs-cine-status {
color: #9eb2c4;
font-size: 12px;
}
`;
document.head.appendChild(style);
}
function makeOption(value, label = value) {
const option = document.createElement("option");
option.value = value;
option.textContent = label;
return option;
}
function makeSelect(value, options, onChange) {
const select = document.createElement("select");
select.className = "iamccs-cine-select";
for (const option of options) select.appendChild(makeOption(option));
select.value = String(value || options[0] || "");
if (!options.includes(select.value) && options.length) {
select.appendChild(makeOption(select.value));
}
select.addEventListener("change", () => onChange(select.value));
return select;
}
function makeInput(value, onChange, attrs = {}) {
const input = document.createElement("input");
input.className = "iamccs-cine-input";
input.value = String(value ?? "");
for (const [key, val] of Object.entries(attrs)) input.setAttribute(key, val);
input.addEventListener("input", () => onChange(input.value));
return input;
}
function makeTextarea(value, onChange) {
const textarea = document.createElement("textarea");
textarea.className = "iamccs-cine-textarea";
textarea.value = String(value ?? "");
textarea.addEventListener("input", () => onChange(textarea.value));
return textarea;
}
function stopCanvasKeys(element) {
const stop = (event) => event.stopPropagation();
for (const eventName of ["keydown", "keyup", "keypress", "pointerdown", "pointerup", "wheel"]) {
element.addEventListener(eventName, stop, { capture: true });
}
}
function createDatalist(id, values) {
let list = document.getElementById(id);
if (list) return;
list = document.createElement("datalist");
list.id = id;
for (const value of values) list.appendChild(makeOption(value));
document.body.appendChild(list);
}
function addCell(rowEl, child, width = null) {
const td = document.createElement("td");
if (width) td.style.width = width;
td.appendChild(child);
rowEl.appendChild(td);
return td;
}
function openTimelineEditor(node, config) {
ensureStyles();
createDatalist("iamccs-cine-ref-options", REF_OPTIONS);
createDatalist("iamccs-cine-range-options", SOURCE_RANGE_OPTIONS);
const widget = getWidget(node, config.widgetName);
const rows = parseRows(widget?.value || "", config);
let selectedPreset = config.kind === "v2v" ? "v2v_two_camera" : "multigen_dialogue_4";
const overlay = document.createElement("div");
overlay.className = "iamccs-cine-overlay";
const modal = document.createElement("div");
modal.className = "iamccs-cine-modal";
overlay.appendChild(modal);
stopCanvasKeys(overlay);
const header = document.createElement("div");
header.className = "iamccs-cine-header";
const title = document.createElement("div");
title.className = "iamccs-cine-title";
title.textContent = config.title;
const help = document.createElement("div");
help.className = "iamccs-cine-help";
help.textContent = config.kind === "v2v"
? "Edit V2V source, range, reference, audio and prompt. Apply writes timeline_lines."
: "Edit shots with dropdowns and prompt boxes. Apply writes shot_lines.";
header.append(title, help);
const toolbar = document.createElement("div");
toolbar.className = "iamccs-cine-toolbar";
const presetSelect = makeSelect(selectedPreset, config.kind === "v2v"
? ["v2v_two_camera", "v2v_single", "v2v_long"]
: ["multigen_dialogue_4", "multigen_dialogue_2", "multigen_monologue"], (value) => {
selectedPreset = value;
});
const loadPreset = button("Load preset", () => {
rows.splice(0, rows.length, ...presetRows(selectedPreset));
renderRows();
updatePreview();
});
const addRow = button("Add row", () => {
const base = rows[rows.length - 1] || (config.kind === "v2v" ? presetRows("v2v_single")[0] : presetRows("multigen_monologue")[0]);
rows.push({ ...base, label: `${base.label || "shot"}_${rows.length + 1}` });
renderRows();
updatePreview();
});
const clearRows = button("Clear", () => {
rows.splice(0, rows.length);
renderRows();
updatePreview();
}, "danger");
toolbar.append(labelText("Preset"), presetSelect, loadPreset, addRow, clearRows);
const body = document.createElement("div");
body.className = "iamccs-cine-body";
const table = document.createElement("table");
table.className = "iamccs-cine-table";
body.appendChild(table);
const footer = document.createElement("div");
footer.className = "iamccs-cine-footer";
const status = document.createElement("div");
status.className = "iamccs-cine-status";
const preview = document.createElement("textarea");
preview.className = "iamccs-cine-textarea iamccs-cine-preview";
preview.readOnly = true;
const actions = document.createElement("div");
actions.style.display = "flex";
actions.style.gap = "8px";
actions.style.alignItems = "center";
actions.append(
button("Copy text", async () => {
try {
await navigator.clipboard?.writeText(preview.value);
status.textContent = "Copied generated lines.";
} catch {
status.textContent = "Copy failed; select preview text manually.";
}
}),
button("Cancel", () => close()),
button("Apply to node", () => {
setWidgetValue(node, config.widgetName, rowsToText(rows, config));
status.textContent = `Applied ${rows.length} rows to ${config.widgetName}.`;
close();
}, "primary"),
);
footer.append(preview, status, actions);
modal.append(header, toolbar, body, footer);
document.body.appendChild(overlay);
overlay.addEventListener("pointerdown", (event) => {
if (event.target === overlay) close();
});
document.addEventListener("keydown", onDocumentKeydown, { capture: true });
function onDocumentKeydown(event) {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
close();
}
}
function close() {
document.removeEventListener("keydown", onDocumentKeydown, { capture: true });
overlay.remove();
}
function renderRows() {
table.textContent = "";
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
const columns = config.kind === "v2v"
? ["#", "seconds", "cut", "source", "range", "ref", "audio", "label", "framing", "prompt", "dialogue", "voice", "mode", "actions"]
: ["#", "seconds", "cut", "ref", "audio", "label", "framing", "prompt", "dialogue", "voice", "actions"];
for (const col of columns) {
const th = document.createElement("th");
th.textContent = col;
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
rows.forEach((row, index) => {
const tr = document.createElement("tr");
addIndexCell(tr, index);
addCell(tr, makeInput(row.seconds, (v) => { row.seconds = v; updatePreview(); }, { type: "number", step: "0.1", min: "0.01" }), "70px");
addCell(tr, makeSelect(row.cut, CUT_MODES, (v) => { row.cut = v; updatePreview(); }), "135px");
if (config.kind === "v2v") {
addCell(tr, makeSelect(row.source, SOURCE_OPTIONS, (v) => { row.source = v; updatePreview(); }), "70px");
addCell(tr, makeInput(row.sourceRange, (v) => { row.sourceRange = v; updatePreview(); }, { list: "iamccs-cine-range-options" }), "115px");
}
addCell(tr, makeInput(row.ref, (v) => { row.ref = v; updatePreview(); }, { list: "iamccs-cine-ref-options" }), "90px");
addCell(tr, makeSelect(row.audio, AUDIO_OPTIONS, (v) => { row.audio = v; updatePreview(); }), "70px");
addCell(tr, makeInput(row.label, (v) => { row.label = v; updatePreview(); }), "140px");
addCell(tr, makeSelect(row.framing, FRAMING_OPTIONS, (v) => { row.framing = v; updatePreview(); }), "160px");
addCell(tr, makeTextarea(row.prompt, (v) => { row.prompt = v; updatePreview(); }), "260px");
addCell(tr, makeTextarea(row.dialogue, (v) => { row.dialogue = v; updatePreview(); }), "220px");
addCell(tr, makeTextarea(row.voice, (v) => { row.voice = v; updatePreview(); }), "170px");
if (config.kind === "v2v") {
addCell(tr, makeSelect(row.v2vMode, V2V_MODES, (v) => { row.v2vMode = v; updatePreview(); }), "190px");
}
addCell(tr, rowActions(index), "74px");
tbody.appendChild(tr);
});
table.appendChild(tbody);
updatePreview();
}
function addIndexCell(tr, index) {
const div = document.createElement("div");
div.className = "iamccs-cine-row-index";
div.textContent = String(index + 1);
addCell(tr, div, "34px");
}
function rowActions(index) {
const box = document.createElement("div");
box.className = "iamccs-cine-row-actions";
box.append(
button("up", () => {
if (index <= 0) return;
const [row] = rows.splice(index, 1);
rows.splice(index - 1, 0, row);
renderRows();
}),
button("down", () => {
if (index >= rows.length - 1) return;
const [row] = rows.splice(index, 1);
rows.splice(index + 1, 0, row);
renderRows();
}),
button("dup", () => {
rows.splice(index + 1, 0, { ...rows[index], label: `${rows[index].label || "shot"}_copy` });
renderRows();
}),
button("del", () => {
rows.splice(index, 1);
renderRows();
}, "danger"),
);
return box;
}
function updatePreview() {
const text = rowsToText(rows, config);
preview.value = text;
status.textContent = `${rows.length} row(s). ${config.widgetName} will receive ${text.length} characters.`;
}
renderRows();
}
function button(text, onClick, variant = "") {
const btn = document.createElement("button");
btn.className = `iamccs-cine-btn ${variant || ""}`.trim();
btn.type = "button";
btn.textContent = text;
btn.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
onClick();
});
return btn;
}
function labelText(text) {
const span = document.createElement("span");
span.className = "iamccs-cine-help";
span.textContent = text;
return span;
}
function installEditorButton(node, config) {
if (!node || node.__iamccsTimelineEditorInstalled) return;
node.__iamccsTimelineEditorInstalled = true;
node.properties = node.properties || {};
const rawWidget = getWidget(node, config.widgetName);
if (rawWidget) {
rawWidget.label = config.kind === "v2v" ? "timeline_lines raw text" : "shot_lines raw text";
}
const editorButton = node.addWidget("button", "Open Timeline Editor", null, () => {
openTimelineEditor(node, config);
}, { serialize: false });
editorButton.serialize = false;
editorButton.options = { ...(editorButton.options || {}), serialize: false };
const presetButton = node.addWidget("button", "Load Default Timeline", null, () => {
const key = config.kind === "v2v" ? "v2v_two_camera" : "multigen_dialogue_4";
const rows = presetRows(key);
setWidgetValue(node, config.widgetName, rowsToText(rows, config));
}, { serialize: false });
presetButton.serialize = false;
presetButton.options = { ...(presetButton.options || {}), serialize: false };
try {
const size = node.computeSize?.() || node.size;
node.setSize?.([Math.max(node.size?.[0] || 0, 560), Math.max(node.size?.[1] || 0, size?.[1] || 520)]);
node.setDirtyCanvas?.(true, true);
} catch {
// ignore
}
}
app.registerExtension({
name: "iamccs.cinematic.timeline_editor",
async beforeRegisterNodeDef(nodeType, nodeData) {
const config = getConfigForNodeData(nodeData);
if (!config) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
installEditorButton(this, config);
return result;
};
},
async loadedGraphNode(node) {
const config = getConfigForNode(node);
if (config) installEditorButton(node, config);
},
});
console.log(`[IAMCCS Cinematic] Timeline editor loaded v=${EDITOR_VERSION}`);
+249 -23
View File
@@ -12,9 +12,12 @@ const TIMEFRAME_TYPE = "IAMCCS_LTX2_TimeFrameCount";
const VALIDATOR_TYPE = "IAMCCS_LTX2_Validator";
const FRAMERATE_SYNC_TYPE = "IAMCCS_LTX2_FrameRateSync";
const SEGMENT_PLANNER_TYPE = "IAMCCS_SegmentPlanner";
const SEGMENT_PLANNER_LINKED_TYPE = "IAMCCS_SegmentPlannerLinked";
const SEGMENT_PLANNER_SETTINGS_TYPE = "IAMCCS_SegmentPlannerSettings";
const SEGMENT_PLANNER_PREVIEW_WIDGET = "iamccs_segmentplanner_live_preview";
const SEGMENT_PLANNER_DETAILS_WIDGET = "iamccs_segmentplanner_live_details";
const SEGMENT_PLANNER_COPY_BUTTON = "iamccs_segmentplanner_copy_button";
const SEGMENT_PLANNER_SETTINGS_REPORT_WIDGET = "iamccs_segmentplanner_settings_live_report";
console.log("[IAMCCS LTX2] Loading seconds/length sync...");
@@ -23,12 +26,77 @@ function getWidget(node, name) {
return node.widgets.find(w => w?.name === name || w?.label === name) || null;
}
function getInput(node, name) {
if (!node?.inputs?.length) return null;
return node.inputs.find(i => i?.name === name) || null;
}
function getNodeById(nodeId) {
if (nodeId == null) return null;
return app?.graph?._nodes_by_id?.[nodeId] || app?.graph?._nodes?.find(n => n?.id === nodeId) || null;
}
function getLinkedSource(node, inputName) {
const input = getInput(node, inputName);
const linkId = input?.link;
if (linkId == null) return null;
const link = app?.graph?.links?.[linkId];
if (!link) return null;
const originId = Array.isArray(link) ? link[1] : link.origin_id;
const originSlot = Array.isArray(link) ? link[2] : link.origin_slot;
const sourceNode = getNodeById(originId);
if (!sourceNode) return null;
// Defensive: outputs must exist and be an array, originSlot must be valid
let sourceOutput = null;
if (Array.isArray(sourceNode.outputs) && originSlot != null && originSlot >= 0 && originSlot < sourceNode.outputs.length) {
sourceOutput = sourceNode.outputs[originSlot];
}
return {
sourceNode,
sourceOutput,
};
}
function getLinkedPrimitiveValue(node, inputName) {
const source = getLinkedSource(node, inputName);
if (!source?.sourceNode) return null;
const sourceNode = source.sourceNode;
const outputName = String(source.sourceOutput?.name || "");
const candidateWidgetNames = [outputName, inputName, "value", "float", "int", "string", "text"];
// Defensive: try to get value from output if present
if (source.sourceOutput && typeof source.sourceOutput.value !== "undefined") {
return source.sourceOutput.value;
}
for (const widgetName of candidateWidgetNames) {
const widget = getWidget(sourceNode, widgetName);
if (widget?.value != null) return widget.value;
}
if (Array.isArray(sourceNode.widgets) && sourceNode.widgets.length > 0) {
const widgetValue = sourceNode.widgets[0]?.value;
if (widgetValue != null) return widgetValue;
}
return null;
}
function clampNumber(v, min, max) {
const n = Number(v);
if (!Number.isFinite(n)) return min;
return Math.max(min, Math.min(max, n));
}
function numbersClose(a, b, epsilon = 0.0001) {
return Math.abs(Number(a) - Number(b)) <= epsilon;
}
function getGraphFpsOrDefault(defaultFps = 25) {
try {
const nodes = app?.graph?._nodes || [];
@@ -118,44 +186,163 @@ function ensurePlannerCopyButton(node) {
return widget;
}
function ensurePlannerSettingsReportWidget(node) {
let widget = getWidget(node, SEGMENT_PLANNER_SETTINGS_REPORT_WIDGET);
if (widget) {
widget.label = "Live Preview";
return widget;
}
widget = node.addWidget("text", "Live Preview", "", () => {}, { multiline: true });
widget.name = SEGMENT_PLANNER_SETTINGS_REPORT_WIDGET;
widget.serialize = false;
try {
widget.inputEl?.setAttribute?.("readonly", true);
} catch {
// ignore
}
return widget;
}
function ensurePlannerNodeSize(node) {
try {
const width = Math.max(460, Number(node.size?.[0] || 0));
const height = Math.max(520, Number(node.size?.[1] || 0));
const height = Math.max(node?.type === SEGMENT_PLANNER_SETTINGS_TYPE ? 300 : 520, Number(node.size?.[1] || 0));
node.size = [width, height];
} catch {
// ignore
}
}
function refreshLinkedPlannerPreviews() {
try {
const nodes = app?.graph?._nodes || [];
for (const node of nodes) {
if (node?.type === SEGMENT_PLANNER_LINKED_TYPE) {
updateSegmentPlannerPreview(node);
}
}
} catch {
// ignore
}
}
function updateSegmentPlannerSettingsReport(node) {
const wSeg = getWidget(node, "segment_duration_s");
const wPlanning = getWidget(node, "planning_mode");
const wProfile = getWidget(node, "segment_preset") || getWidget(node, "content_profile");
const wOverlap = getWidget(node, "overlap_frames");
const wAutoSync = getWidget(node, "auto_sync_overlap");
if (!wSeg || !wPlanning || !wProfile || !wOverlap || !wAutoSync) return;
const segmentDuration = clampNumber(wSeg.value, 0.01, 3600.0);
const planningMode = String(wPlanning.value || "manual_segment_seconds");
const segmentPreset = String(wProfile.value || "10sec");
const autoSyncOverlap = Boolean(wAutoSync.value);
const rec = getPlannerProfile(segmentPreset);
const explicitPresetMode = planningMode === "explicit_preset_seconds" || planningMode === "auto_profile";
const autoDurationProfile = !explicitPresetMode ? getAutoOverlapProfileForDuration(segmentDuration) : null;
const effectiveSegmentDuration = explicitPresetMode ? rec.segmentSeconds : segmentDuration;
const effectiveOverlapFrames = clampNumber(wOverlap.value, 0, 4096);
if (explicitPresetMode && !numbersClose(wSeg.value, effectiveSegmentDuration)) {
wSeg.value = effectiveSegmentDuration;
}
const previewFps = getGraphFpsOrDefault(24);
const uniqueFrames = Math.max(1, Math.round(effectiveSegmentDuration * previewFps));
const firstRawFrames = snapLengthToLtx2Rule(uniqueFrames, "up");
const continuationRawFrames = snapLengthToLtx2Rule(uniqueFrames + effectiveOverlapFrames, "up");
const report = [
`segment_duration_s = ${effectiveSegmentDuration.toFixed(3)}`,
`planning_mode = ${planningMode}`,
`segment_preset = ${segmentPreset}`,
`overlap_frames = ${effectiveOverlapFrames}`,
`preview_fps = ${previewFps}`,
`unique_segment_frames = ${uniqueFrames}`,
`first_segment_raw_frames = ${firstRawFrames}`,
`continuation_raw_frames = ${continuationRawFrames}`,
`auto_sync_overlap = ${autoSyncOverlap}`,
`auto_duration_profile = ${autoDurationProfile?.profileLabel || "none"}`,
`overlap_policy = manual/default_9`,
`recommended_extension_preset = ${rec.preset}`,
].join("\n");
const widget = ensurePlannerSettingsReportWidget(node);
widget.value = report;
node.properties = node.properties || {};
node.properties.iamccs_segmentplanner_settings_live_report = report;
ensurePlannerNodeSize(node);
try {
node.setDirtyCanvas(true, true);
} catch {
// ignore
}
}
function getPlannerProfile(presetOrLegacyProfile) {
const value = String(presetOrLegacyProfile || "15sec");
if (value === "20sec") {
return { segmentSeconds: 20.0, overlap: 9, leftContext: 1.0, preset: "monologue_audio_24fps", profileLabel: "20sec" };
}
if (value === "15sec" || value === "monologue") {
return { segmentSeconds: 15.0, overlap: 9, leftContext: 0.75, preset: "monologue_audio_24fps", profileLabel: "15sec" };
}
if (value === "5sec") {
return { segmentSeconds: 5.0, overlap: 9, leftContext: 0.25, preset: "videoclip_audio_24fps", profileLabel: "5sec" };
}
return { segmentSeconds: 10.0, overlap: 9, leftContext: 0.5, preset: "videoclip_audio_24fps", profileLabel: "10sec" };
}
function getAutoOverlapProfileForDuration(segmentDuration) {
const duration = Number(segmentDuration);
if (!Number.isFinite(duration)) return null;
const profiles = [
getPlannerProfile("5sec"),
getPlannerProfile("10sec"),
getPlannerProfile("15sec"),
getPlannerProfile("20sec"),
];
return profiles.find(profile => Math.abs(duration - profile.segmentSeconds) <= 0.1) || null;
}
function updateSegmentPlannerPreview(node) {
const wSong = getWidget(node, "song_duration_s");
const wFps = getWidget(node, "fps");
const wSeg = getWidget(node, "segment_duration_s");
const wPlanning = getWidget(node, "planning_mode");
const wProfile = getWidget(node, "content_profile");
const wOverlap = getWidget(node, "overlap_frames");
const wPlanning = getWidget(node, "planning_mode") || getWidget(node, "planning_mode_in");
const wProfile = getWidget(node, "segment_preset") || getWidget(node, "segment_preset_in") || getWidget(node, "content_profile");
const wOverlap = getWidget(node, "overlap_frames") || getWidget(node, "overlap_frames_in");
const wRound = getWidget(node, "ltx_round_mode");
const wIndex = getWidget(node, "segment_index");
if (!wSong || !wFps || !wSeg || !wPlanning || !wProfile || !wOverlap || !wRound || !wIndex) return;
const songDuration = clampNumber(wSong.value, 0.01, 36000);
const fps = clampNumber(wFps.value, 0.001, 240.0);
const segmentDuration = clampNumber(wSeg.value, 0.01, 3600.0);
const planningMode = String(wPlanning.value || "manual_segment_seconds");
const contentProfile = String(wProfile.value || "videoclip");
const overlapFrames = clampNumber(wOverlap.value, 0, 4096);
const linkedSegmentDuration = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "segment_duration_s") : null;
const linkedPlanningMode = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "planning_mode_in") : null;
const linkedSegmentPreset = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "segment_preset_in") : null;
const linkedOverlapFrames = node?.type === SEGMENT_PLANNER_LINKED_TYPE ? getLinkedPrimitiveValue(node, "overlap_frames_in") : null;
const segmentDuration = clampNumber(linkedSegmentDuration ?? wSeg.value, 0.01, 3600.0);
const planningMode = String(linkedPlanningMode ?? wPlanning.value || "manual_segment_seconds");
const segmentPreset = String(linkedSegmentPreset ?? wProfile.value || "15sec");
const inputOverlapFrames = clampNumber(linkedOverlapFrames ?? wOverlap.value, 0, 4096);
const roundMode = String(wRound.value || "up");
const segmentIndex = Math.max(0, Math.trunc(Number(wIndex.value || 0)));
const rec = contentProfile === "monologue"
? { baseTargetS: 15.0, minS: 8.0, maxS: 15.0, overlap: 13, leftContext: 0.75, preset: "monologue_audio_24fps" }
: { baseTargetS: 10.0, minS: 5.0, maxS: 10.0, overlap: 9, leftContext: 0.5, preset: "videoclip_audio_24fps" };
const rec = getPlannerProfile(segmentPreset);
const explicitPresetMode = planningMode === "explicit_preset_seconds" || planningMode === "auto_profile";
const autoDurationProfile = !explicitPresetMode ? getAutoOverlapProfileForDuration(segmentDuration) : null;
const effectiveSegmentDuration = explicitPresetMode ? rec.segmentSeconds : segmentDuration;
const effectiveOverlapFrames = inputOverlapFrames;
const effectiveSegmentDuration = planningMode === "auto_profile"
? Math.max(rec.minS, Math.min(rec.maxS, songDuration / Math.max(1, Math.ceil(songDuration / rec.baseTargetS))))
: segmentDuration;
const effectiveOverlapFrames = planningMode === "auto_profile" ? rec.overlap : overlapFrames;
if (explicitPresetMode && node?.type !== SEGMENT_PLANNER_LINKED_TYPE && !numbersClose(wSeg.value, effectiveSegmentDuration)) {
wSeg.value = effectiveSegmentDuration;
}
const overlapFrames = clampNumber(wOverlap.value, 0, 4096);
const totalFrames = Math.max(1, Math.round(songDuration * fps));
const uniqueFrames = Math.max(1, Math.round(effectiveSegmentDuration * fps));
@@ -178,7 +365,7 @@ function updateSegmentPlannerPreview(node) {
`total=${totalFrames}f`,
`unique=${uniqueFrames}f`,
`mode=${planningMode}`,
`profile=${contentProfile}`,
`profile=${segmentPreset}`,
`first_raw=${firstRaw}f`,
`next_raw=${nextRaw}f`,
`segments=${segments}`,
@@ -197,9 +384,10 @@ function updateSegmentPlannerPreview(node) {
`segment_duration_s = ${segmentDuration.toFixed(3)}`,
`effective_segment_duration_s = ${effectiveSegmentDuration.toFixed(3)}`,
`planning_mode = ${planningMode}`,
`content_profile = ${contentProfile}`,
`segment_preset = ${segmentPreset}`,
`overlap_frames = ${overlapFrames}`,
`effective_overlap_frames = ${effectiveOverlapFrames}`,
`overlap_policy = manual/default_9`,
`ltx_round_mode = ${roundMode}`,
`segment_index = ${clampedIndex}`,
`total_frames = ${totalFrames}`,
@@ -218,6 +406,7 @@ function updateSegmentPlannerPreview(node) {
`current_segment_end_s = ${currentEndS.toFixed(3)}`,
`recommended_audio_left_context_s = ${rec.leftContext.toFixed(2)}`,
`recommended_extension_preset = ${rec.preset}`,
`auto_duration_profile = ${autoDurationProfile?.profileLabel || "none"}`,
].join("\n");
const widget = ensurePlannerPreviewWidget(node);
@@ -241,9 +430,9 @@ function installSegmentPlannerSync(node) {
getWidget(node, "song_duration_s"),
getWidget(node, "fps"),
getWidget(node, "segment_duration_s"),
getWidget(node, "planning_mode"),
getWidget(node, "content_profile"),
getWidget(node, "overlap_frames"),
getWidget(node, "planning_mode") || getWidget(node, "planning_mode_in"),
getWidget(node, "segment_preset") || getWidget(node, "segment_preset_in") || getWidget(node, "content_profile"),
getWidget(node, "overlap_frames") || getWidget(node, "overlap_frames_in"),
getWidget(node, "ltx_round_mode"),
getWidget(node, "segment_index"),
].filter(Boolean);
@@ -271,6 +460,38 @@ function installSegmentPlannerSync(node) {
updateSegmentPlannerPreview(node);
}
function installSegmentPlannerSettingsSync(node) {
const widgets = [
getWidget(node, "segment_duration_s"),
getWidget(node, "planning_mode"),
getWidget(node, "segment_preset"),
getWidget(node, "overlap_frames"),
getWidget(node, "auto_sync_overlap"),
].filter(Boolean);
if (!widgets.length || node._iamccsSegmentPlannerSettingsSyncInstalled) {
updateSegmentPlannerSettingsReport(node);
return;
}
node._iamccsSegmentPlannerSettingsSyncInstalled = true;
ensurePlannerSettingsReportWidget(node);
ensurePlannerNodeSize(node);
for (const widget of widgets) {
const prev = widget.callback;
widget.callback = function () {
const r = prev?.apply(this, arguments);
updateSegmentPlannerSettingsReport(node);
refreshLinkedPlannerPreviews();
return r;
};
}
updateSegmentPlannerSettingsReport(node);
refreshLinkedPlannerPreviews();
}
function installSecondsLengthSync(node, { snapRule = false } = {}) {
const wSeconds = getWidget(node, "seconds");
const wLength = getWidget(node, "length");
@@ -321,13 +542,15 @@ app.registerExtension({
if (!nodeData?.name) return;
const name = nodeData.name;
if (name !== TIMEFRAME_TYPE && name !== VALIDATOR_TYPE && name !== SEGMENT_PLANNER_TYPE) return;
if (name !== TIMEFRAME_TYPE && name !== VALIDATOR_TYPE && name !== SEGMENT_PLANNER_TYPE && name !== SEGMENT_PLANNER_LINKED_TYPE && name !== SEGMENT_PLANNER_SETTINGS_TYPE) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated?.apply(this, arguments);
if (name === SEGMENT_PLANNER_TYPE) {
if (name === SEGMENT_PLANNER_TYPE || name === SEGMENT_PLANNER_LINKED_TYPE) {
installSegmentPlannerSync(this);
} else if (name === SEGMENT_PLANNER_SETTINGS_TYPE) {
installSegmentPlannerSettingsSync(this);
} else {
// Snap to 8n+1 on both nodes, since LTX-2 VAE encode requires it.
installSecondsLengthSync(this, { snapRule: true });
@@ -338,9 +561,12 @@ app.registerExtension({
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const r = onConfigure?.apply(this, arguments);
if (name === SEGMENT_PLANNER_TYPE) {
if (name === SEGMENT_PLANNER_TYPE || name === SEGMENT_PLANNER_LINKED_TYPE) {
installSegmentPlannerSync(this);
updateSegmentPlannerPreview(this);
} else if (name === SEGMENT_PLANNER_SETTINGS_TYPE) {
installSegmentPlannerSettingsSync(this);
updateSegmentPlannerSettingsReport(this);
}
return r;
};
+808
View File
@@ -0,0 +1,808 @@
import { app } from "../../../scripts/app.js";
const PRESET_CONFIGS = {
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { modular_decode: "low_ram", steps: 16, image_compression: 40, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
balanced: { modular_decode: "normal", steps: 20, image_compression: 33, continuity_anchor_mode: "off", anchor_refresh_interval: 3, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
high_quality: { modular_decode: "high", steps: 24, image_compression: 28, continuity_anchor_mode: "off", anchor_refresh_interval: 1, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
fast_preview: { modular_decode: "low_ram", steps: 12, image_compression: 45, continuity_anchor_mode: "off", anchor_refresh_interval: 2, anti_drift_mode: "off", anti_drift_strength: 0.0, identity_persistence_strength: 0.0 },
},
visibility: {
low_ram_safe: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
balanced: { anchor_image_strength: false, anti_drift_mode: false, anti_drift_strength: false, identity_persistence_strength: false, output_root: true },
high_quality: { anchor_image_strength: false, anti_drift_mode: false, 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 },
},
},
"IAMCCS-SuperNodes AU+IMG2VID Exec VAE": {
presetWidget: "ui_preset",
defaultPreset: "balanced",
values: {
low_ram_safe: { decode_mode: "low_ram", jpg_quality: 92, crf: 21, tiled_tile_size: 512, tiled_overlap: 64 },
balanced: { decode_mode: "normal", jpg_quality: 95, crf: 19, tiled_tile_size: 512, tiled_overlap: 64 },
high_quality: { decode_mode: "high", jpg_quality: 100, crf: 16, tiled_tile_size: 768, tiled_overlap: 96 },
fast_preview: { decode_mode: "low_ram", jpg_quality: 88, crf: 24, tiled_tile_size: 384, tiled_overlap: 48 },
},
visibility: {
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 },
},
},
};
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"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Render": [
{ key: "preset", label: "Preset", color: "#557ea6", widgets: ["ui_preset"] },
{ 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", "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: ["modular_decode", "downstream_stage_mode", "output_root"] },
{ key: "debug", label: "Debug", color: "#8a8a36", widgets: ["segment_overlay_mode", "segment_overlay_text"] },
],
"IAMCCS-SuperNodes Second Stage": [
{ 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_GC_AudioConcatSupernode: [
{ 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"] },
],
"IAMCCS-SuperNodes AU+IMG2VID Exec Finalize": [
{ key: "output", label: "Output", color: "#6a85a0", widgets: ["frame_rate", "filename_prefix", "crf", "pix_fmt", "trim_to_audio"] },
],
};
const SECTION_BUTTON_COLLAPSED_BG = "#22303f";
const SECTION_BUTTON_TEXT = "#f8fbff";
const SECTION_BUTTON_HEIGHT = 30;
function findWidget(node, name) {
return node.widgets?.find((widget) => widget?.name === name);
}
function setWidgetVisibility(widget, visible) {
if (!widget || widget.type === "converted-widget") {
return;
}
widget.hidden = !visible;
widget.disabled = !visible;
if (widget.element) {
widget.element.style.display = visible ? "" : "none";
}
if (widget.inputEl) {
widget.inputEl.style.display = visible ? "" : "none";
}
if (visible) {
if (Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.computeSize = widget.__iamccsOrigComputeSize;
} else {
delete widget.computeSize;
}
} else {
if (!Object.prototype.hasOwnProperty.call(widget, "__iamccsOrigComputeSize")) {
widget.__iamccsOrigComputeSize = widget.computeSize;
}
widget.computeSize = () => [0, -4];
widget.y = undefined;
widget.last_y = undefined;
}
}
function fitNodeToWidgets(node) {
const size = node.computeSize?.() || node.size;
node.setSize([
Math.max(node.size?.[0] || 0, size[0]),
Math.max(size[1], size[1] + 8),
]);
}
function setWidgetLabel(node, widgetName, label) {
const widget = findWidget(node, widgetName);
if (!widget) {
return;
}
widget.label = label;
}
function sectionButtonCaption(group, isExpanded) {
return `${isExpanded ? "▼" : "▶"} ${group.label}`;
}
function insertWidget(node, widget, index) {
const widgets = node.widgets || [];
const currentIndex = widgets.indexOf(widget);
if (currentIndex >= 0) {
widgets.splice(currentIndex, 1);
}
widgets.splice(index, 0, widget);
}
function setWidgetValue(node, widgetName, value) {
const widget = findWidget(node, widgetName);
if (!widget || widget.value === value) {
return;
}
widget.value = value;
widget.callback?.(value);
}
const EXEC_PLANNER_PRESETS = {
"5sec": { seconds: 5.0, overlap: 9 },
"10sec": { seconds: 10.0, overlap: 9 },
"15sec": { seconds: 15.0, overlap: 9 },
"20sec": { seconds: 20.0, overlap: 9 },
videoclip: { seconds: 10.0, overlap: 9 },
monologue: { seconds: 15.0, overlap: 9 },
};
function getExecPlannerPreset(value) {
return EXEC_PLANNER_PRESETS[String(value || "15sec")] || EXEC_PLANNER_PRESETS["15sec"];
}
function getExecPlannerPresetForSeconds(seconds) {
const value = Number(seconds);
if (!Number.isFinite(value)) {
return null;
}
for (const presetName of ["5sec", "10sec", "15sec", "20sec"]) {
const rec = EXEC_PLANNER_PRESETS[presetName];
if (Math.abs(value - rec.seconds) <= 0.001) {
return rec;
}
}
return null;
}
function ltxSafeFramesAtLeast(frameCount) {
const frames = Math.max(1, Math.round(Number(frameCount) || 1));
const remainder = (frames - 1) % 8;
return remainder === 0 ? frames : frames + (8 - remainder);
}
function readPlannerNumber(node, widgetName, fallback) {
const value = Number(findWidget(node, widgetName)?.value);
return Number.isFinite(value) ? value : fallback;
}
function getExecPlannerLiveConfig(node) {
const fps = Math.max(0.001, readPlannerNumber(node, "fps", 24.0));
const planningMode = String(findWidget(node, "planning_mode")?.value || "manual_segment_seconds");
const segmentPreset = String(findWidget(node, "segment_preset")?.value || "15sec");
const presetRec = getExecPlannerPreset(segmentPreset);
let seconds = 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";
if (planningMode === "explicit_preset_seconds") {
seconds = presetRec.seconds;
} else {
const secondsRec = getExecPlannerPresetForSeconds(seconds);
if (secondsRec) {
overlapSource = `${secondsRec.seconds}s duration`;
}
}
return { fps, planningMode, segmentPreset, seconds, overlap, overlapSource };
}
function updateExecPlannerLivePreview(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
return;
}
node.properties = node.properties || {};
const config = getExecPlannerLiveConfig(node);
const uniqueFrames = Math.max(1, Math.round(config.seconds * config.fps));
const firstRaw = ltxSafeFramesAtLeast(uniqueFrames);
const continuationTarget = uniqueFrames + config.overlap;
const continuationRaw = ltxSafeFramesAtLeast(continuationTarget);
const lines = [
`live ${config.seconds.toFixed(2)}s @ ${config.fps.toFixed(2)}fps -> ${uniqueFrames}f unique`,
`overlap ${config.overlap}f (${config.overlapSource}) | first raw ${firstRaw}f | cont raw ${continuationRaw}f`,
];
const storedDuration = Number(node.properties.iamccsPlannerDuration);
const storedTotalFrames = Number(node.properties.iamccsPlannerTotalFrames);
const hasDuration = Number.isFinite(storedDuration) && storedDuration > 0;
const totalFrames = Number.isFinite(storedTotalFrames) && storedTotalFrames > 0
? Math.round(storedTotalFrames)
: hasDuration
? Math.round(storedDuration * config.fps)
: 0;
if (totalFrames > 0) {
const segmentCount = Math.max(1, Math.ceil(totalFrames / uniqueFrames));
lines.push(`audio ${hasDuration ? storedDuration.toFixed(2) : "cached"}s -> ${totalFrames}f | segments ${segmentCount}`);
const parts = [];
for (let index = 0; index < Math.min(segmentCount, 4); index += 1) {
const remaining = Math.max(0, totalFrames - index * uniqueFrames);
const effective = Math.max(1, Math.min(uniqueFrames, remaining));
const target = index === 0 ? effective : effective + config.overlap;
parts.push(`s${index + 1}:${effective}${index === 0 ? "" : `+${config.overlap}`}->${ltxSafeFramesAtLeast(target)}`);
}
lines.push(`${parts.join(" ")}${segmentCount > 4 ? " ..." : ""}`);
} else {
lines.push("audio duration unknown until planner runs once");
}
node.properties.iamccsPlannerLivePreview = lines.join("\n");
}
function syncExecPlannerExplicitPreset(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
return;
}
const planningMode = String(findWidget(node, "planning_mode")?.value || "manual_segment_seconds");
if (planningMode === "explicit_preset_seconds") {
const rec = getExecPlannerPreset(findWidget(node, "segment_preset")?.value);
setWidgetValue(node, "segment_seconds", rec.seconds);
}
updateExecPlannerLivePreview(node);
}
function installExecPlannerExplicitPresetSync(node) {
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"]) {
const widget = findWidget(node, widgetName);
if (!widget) {
continue;
}
const originalCallback = widget.callback;
widget.callback = (...args) => {
originalCallback?.apply(widget, args);
if (widgetName === "planning_mode") {
applyPlannerModeVisibility(node);
} else {
syncExecPlannerExplicitPreset(node);
}
updateExecPlannerLivePreview(node);
app.graph.setDirtyCanvas(true, true);
};
}
syncExecPlannerExplicitPreset(node);
updateExecPlannerLivePreview(node);
}
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) {
continue;
}
const targetNode = app.graph.getNodeById?.(link.target_id);
if (targetNode) {
results.push(targetNode);
}
}
}
return results;
}
function applyVaeDecodeModeVisibility(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
return;
}
const decodeMode = String(findWidget(node, "decode_mode")?.value || "low_ram");
const isLowRam = decodeMode === "low_ram";
const isNormal = decodeMode === "normal";
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);
fitNodeToWidgets(node);
}
function applyRenderAnchorLabels(node) {
if (node.comfyClass !== "IAMCCS-SuperNodes AU+IMG2VID Exec Render") {
return;
}
setWidgetLabel(node, "continuity_anchor_mode", "Anchor Refresh");
setWidgetLabel(node, "anchor_refresh_interval", "Refresh Interval");
setWidgetLabel(node, "anchor_image_strength", "Anchor Guidance Strength");
}
function syncDownstreamVaeDecodeModes(renderNode) {
const renderDecode = String(findWidget(renderNode, "modular_decode")?.value || "low_ram");
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") {
setWidgetValue(node, "decode_mode", renderDecode);
setWidgetValue(node, "ui_preset", "custom");
applyVaeDecodeModeVisibility(node);
app.graph.setDirtyCanvas(true, true);
continue;
}
if (node.comfyClass === "IAMCCS-SuperNodes Second Stage") {
queue.push(...getLinxTargets(node));
}
}
}
function applyPresetConfig(node, nodeName) {
const config = PRESET_CONFIGS[nodeName];
if (!config) {
return;
}
const presetWidget = findWidget(node, config.presetWidget);
if (!presetWidget) {
return;
}
const presetName = String(presetWidget.value || config.defaultPreset || "custom");
if (presetName !== "custom") {
const values = config.values?.[presetName] || {};
Object.entries(values).forEach(([widgetName, widgetValue]) => setWidgetValue(node, widgetName, widgetValue));
}
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Render") {
applyRenderAnchorLabels(node);
}
fitNodeToWidgets(node);
}
function getSerializableWidgets(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false);
}
function rehydrateSerializedWidgets(node, serializedValues) {
if (!Array.isArray(serializedValues) || !node.widgets?.length) {
return;
}
const widgets = getSerializableWidgets(node);
const count = Math.min(widgets.length, serializedValues.length);
for (let index = 0; index < count; index += 1) {
const widget = widgets[index];
if (!widget) {
continue;
}
widget.value = serializedValues[index];
}
}
function addSectionButton(node, group, index) {
// 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);
if (existing) {
return existing;
}
node.properties = node.properties || {};
const propKey = `iamccs_section_${group.key}`;
if (node.properties[propKey] === undefined) {
node.properties[propKey] = true;
}
const sectionButton = node.addWidget("button", "", "", () => {
node.properties[propKey] = !node.properties[propKey];
applyGroupVisibility(node, group, propKey, sectionButton);
});
// IMPORTANT: do NOT set serialize = false.
// Section buttons must occupy a slot in widgets_values so that ComfyUI's
// index-based configure() assignment keeps real widget values aligned.
// Their value (empty string / null) is harmless.
sectionButton._iamccsSectionKey = group.key;
sectionButton.computeSize = (width) => [width || 280, SECTION_BUTTON_HEIGHT];
sectionButton.label = group.label;
sectionButton.options = { bgcolor: group.color || "#4f6f8f", color: SECTION_BUTTON_TEXT };
sectionButton.value = "";
applyGroupVisibility(node, group, propKey, sectionButton);
insertWidget(node, sectionButton, index);
return sectionButton;
}
function applyGroupVisibility(node, group, propKey, button) {
const isExpanded = !!node.properties[propKey];
const caption = sectionButtonCaption(group, isExpanded);
button.name = caption;
button.label = caption;
button.value = caption;
button.options = {
bgcolor: isExpanded ? (group.color || "#4f6f8f") : SECTION_BUTTON_COLLAPSED_BG,
color: SECTION_BUTTON_TEXT,
};
for (const widgetName of group.widgets) {
setWidgetVisibility(findWidget(node, widgetName), isExpanded);
}
fitNodeToWidgets(node);
app.graph.setDirtyCanvas(true, true);
}
function applyPlannerModeVisibility(node) {
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);
fitNodeToWidgets(node);
}
function refreshNodeLayoutState(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
for (const group of groups) {
const button = node.widgets?.find((widget) => widget?._iamccsSectionKey === group.key);
if (!button) {
continue;
}
applyGroupVisibility(node, group, `iamccs_section_${group.key}`, button);
}
const config = PRESET_CONFIGS[nodeName];
if (config) {
const presetWidget = findWidget(node, config.presetWidget);
const presetName = String(presetWidget?.value || config.defaultPreset || "custom");
const visibilityMap = config.visibility?.[presetName] || {};
Object.entries(visibilityMap).forEach(([widgetName, isVisible]) => setWidgetVisibility(findWidget(node, widgetName), !!isVisible));
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
} else if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
applyPlannerModeVisibility(node);
} else {
fitNodeToWidgets(node);
}
}
function addPlannerChip(node) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === "planner_chip_preview")) {
return;
}
const chipWidget = {
type: "custom",
name: "planner_chip_preview",
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 30];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.iamccsPlannerChip || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 22;
const top = y + 4;
ctx.save();
ctx.fillStyle = "#18312b";
ctx.strokeStyle = "#6ea88d";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#d8efe2";
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(chipWidget);
}
function addStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 42];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 34;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "middle";
ctx.fillText(text, x + 10, top + h / 2 + 0.5);
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function addMultiLineStatusBox(node, propertyName, widgetName, fill, stroke, textColor) {
if (!node.widgets || node.widgets.some((widget) => widget?.name === widgetName)) {
return;
}
const statusWidget = {
type: "custom",
name: widgetName,
serialize: false,
computeSize(width) {
return [Math.max(220, (width || 260) - 20), 78];
},
draw(ctx, widget, nodeRef, widgetWidth, y) {
const text = nodeRef.properties?.[propertyName] || "";
if (!text) {
return;
}
const lines = String(text).split("\n").filter(Boolean).slice(0, 4);
const x = 12;
const w = Math.max(180, widgetWidth - 24);
const h = 70;
const top = y + 4;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, top, w, h, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = textColor;
ctx.font = "12px Segoe UI";
ctx.textBaseline = "top";
lines.forEach((line, index) => {
ctx.fillText(line, x + 10, top + 8 + index * 15);
});
ctx.restore();
},
};
node.widgets.push(statusWidget);
}
function enhanceNodeLayout(node, nodeName) {
const groups = NODE_GROUPS[nodeName] || [];
if (!groups.length || !node.widgets?.length) {
return;
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
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");
}
if (nodeName === "IAMCCS-SuperNodes Second Stage") {
addStatusBox(node, "iamccsSecondStageStatus", "second_stage_status_preview", "#30243d", "#9f77bf", "#f1e7f8");
}
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec VAE") {
applyVaeDecodeModeVisibility(node);
}
for (let index = groups.length - 1; index >= 0; index -= 1) {
const group = groups[index];
const firstWidgetIndex = node.widgets.findIndex((widget) => group.widgets.includes(widget?.name));
if (firstWidgetIndex >= 0) {
addSectionButton(node, group, firstWidgetIndex);
}
}
applyPresetConfig(node, nodeName);
fitNodeToWidgets(node);
}
app.registerExtension({
name: "IAMCCS.SuperNodesExecUI",
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name;
if (!NODE_GROUPS[nodeName]) {
return;
}
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);
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 modularDecodeWidget = findWidget(this, "modular_decode");
if (modularDecodeWidget) {
const originalCallback = modularDecodeWidget.callback;
modularDecodeWidget.callback = (...args) => {
originalCallback?.apply(modularDecodeWidget, args);
syncDownstreamVaeDecodeModes(this);
app.graph.setDirtyCanvas(true, true);
};
syncDownstreamVaeDecodeModes(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);
};
}
}
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);
rehydrateSerializedWidgets(this, info?.widgets_values);
if (nodeName === "IAMCCS-SuperNodes AU+IMG2VID Exec Planner") {
installExecPlannerExplicitPresetSync(this);
}
refreshNodeLayoutState(this, nodeName);
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);
}
};
}
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);
}
};
}
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);
}
};
}
},
});
+376 -60
View File
@@ -1,6 +1,12 @@
import { app } from "../../scripts/app.js";
import { app } from "../../scripts/app.js";
const NODE_NAME = "IAMCCS_WanLoRASchedule";
const NODE_NAMES = new Set([
"IAMCCS_WanLoRASchedule",
"IAMCCS_WanLoRAHookSchedule",
"IAMCCS_ApplyScheduledWanLoRAFromConditioning",
"IAMCCS_BuildScheduledWanModelBank",
]);
const MODE_WIDGET = "_iamccs_schedule_mode";
const LOAD_WIDGET = "_iamccs_schedule_load_preset";
const SAVE_WIDGET = "_iamccs_schedule_save_preset";
@@ -29,6 +35,45 @@ const SLOT_BLOCK = 5;
const SLOTS_START_IDX = 3;
const VISIBLE_PRESET_OPTIONS = ["custom range", "all generations", "gen 0 only", "gen 1 onwards"];
function getSerializedHeadWidgetNames(node) {
const names = [];
if (getWidget(node, "generation_count")) {
names.push("generation_count");
} else if (getWidget(node, "generation_index")) {
names.push("generation_index");
}
names.push("log_prefix", "model_type");
return names;
}
function expectedSerializedValueCount(node, slotCount = MAX_SLOTS) {
return getSerializedHeadWidgetNames(node).length + (slotCount * SLOT_BLOCK);
}
function getSerializedWidgetNames(node, slotCount = MAX_SLOTS) {
const names = [...getSerializedHeadWidgetNames(node)];
for (let slot = 1; slot <= slotCount; slot += 1) {
const prefix = `slot_${String(slot).padStart(2, "0")}`;
names.push(`${prefix}_lora_name`);
names.push(`${prefix}_strength`);
names.push(`${prefix}_preset`);
names.push(`${prefix}_start`);
names.push(`${prefix}_end`);
}
return names;
}
function reapplySerializedWidgetValues(node, values) {
if (!Array.isArray(values) || !node) return;
let index = 0;
for (const name of getSerializedWidgetNames(node)) {
if (index >= values.length) break;
const widget = getWidget(node, name);
if (!widget) continue;
widget.value = values[index++];
}
}
const SLOT_META = [
{ slot: 1, label: "Gen 0", preset: "gen 0 only", start: 0, end: 0 },
{ slot: 2, label: "Gen 1", preset: "custom range", start: 1, end: 1 },
@@ -62,6 +107,82 @@ function getWidget(node, name) {
return node?.widgets?.find((widget) => widget?.name === name || widget?.label === name) || null;
}
function setButtonText(widget, text) {
if (!widget) return;
widget.label = text;
widget.options = { ...(widget.options || {}), label: text };
widget._iamccsButtonText = text;
}
function getButtonText(widget, fallback = "") {
return String(widget?._iamccsButtonText || widget?.label || widget?.options?.label || fallback);
}
function waitForNextPaint() {
return new Promise((resolve) => {
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => setTimeout(resolve, 0));
return;
}
setTimeout(resolve, 0);
});
}
async function runButtonAction(node, widget, busyText, action) {
if (!widget || widget._iamccsBusy) return;
const originalText = getButtonText(widget);
widget._iamccsBusy = true;
setButtonText(widget, busyText);
node.setDirtyCanvas?.(true, true);
await waitForNextPaint();
try {
await action();
} finally {
widget._iamccsBusy = false;
setButtonText(widget, originalText);
node.setDirtyCanvas?.(true, true);
}
}
function beginBatchUpdate(node) {
node._iamccsBatchDepth = Number(node._iamccsBatchDepth || 0) + 1;
}
function endBatchUpdate(node) {
node._iamccsBatchDepth = Math.max(0, Number(node._iamccsBatchDepth || 0) - 1);
if (!node._iamccsBatchDepth && node._iamccsPendingSyncLowNodes) {
node._iamccsPendingSyncLowNodes = false;
requestSyncAllLinkedLowNodes(node);
}
}
function isBatchUpdating(node) {
return Number(node?._iamccsBatchDepth || 0) > 0;
}
function isRestoringNodeState(node) {
return Boolean(node?._iamccsRestoringState);
}
function requestSyncAllLinkedLowNodes(node) {
if (!node) return;
if (isBatchUpdating(node)) {
node._iamccsPendingSyncLowNodes = true;
return;
}
if (isRestoringNodeState(node)) {
node._iamccsPendingSyncLowNodes = true;
return;
}
if (node._iamccsSyncLowNodesTimer) return;
node._iamccsSyncLowNodesTimer = setTimeout(() => {
node._iamccsSyncLowNodesTimer = 0;
void syncAllLinkedLowNodes(node).finally(() => {
node.setDirtyCanvas?.(true, true);
});
}, 0);
}
function markUiWidgetNonSerializable(widget) {
if (!widget) return widget;
widget.options = { ...(widget.options || {}), serialize: false };
@@ -78,9 +199,9 @@ function isModelTypeValue(value) {
return value === "wan2x" || value === "flow" || value === "standard";
}
// Correct serialized count: generation_index(1) + log_prefix(1) + model_type(1) + 6 slots × 5 = 33
// Correct serialized count: generation_index(1) + log_prefix(1) + model_type(1) + 6 slots × 5 = 33
// (generation_index stays as a widget even when wired, in ComfyUI's serialization model)
const EXPECTED_SERIALIZED_VALUES_CONNECTED = EXPECTED_SERIALIZED_VALUES; // 33 – gen_idx present as wired widget
const EXPECTED_SERIALIZED_VALUES_CONNECTED = EXPECTED_SERIALIZED_VALUES; // 33 – gen_idx present as wired widget
function slotCountFromSerializedLength(length) {
if (typeof length !== "number" || length < 3) return 0;
@@ -103,7 +224,7 @@ function sanitizePresetValue(value) {
return aliases[raw] || raw;
}
function normalizeLegacyWidgetValues(config) {
function normalizeLegacyWidgetValues(config, node = null) {
const values = config?.widgets_values;
if (!Array.isArray(values)) return;
@@ -141,13 +262,67 @@ function normalizeLegacyWidgetValues(config) {
normalized = [generationIndex, logPrefix, modelType, ...slotValues];
}
const hasGenerationCount = Boolean(node && getWidget(node, "generation_count"));
const genericExpected = node ? expectedSerializedValueCount(node) : EXPECTED_SERIALIZED_VALUES;
const maxSlotValuesLength = MAX_SLOTS * SLOT_BLOCK;
// Pattern C (bank nodes): generation_count is serialized last, while log_prefix/model_type
// stay at the front and UI-only widgets are inserted before the slot payload.
// Example observed on disk:
// log_prefix, model_type, mode, null..., slot_01..., ..., slot_64..., generation_count
if (
!normalized
&& hasGenerationCount
&& values.length > genericExpected
&& maxSlotValuesLength > 0
&& typeof values[0] === "string"
&& isModelTypeValue(values[1])
) {
const slotStart = values.length - maxSlotValuesLength - 1;
if (slotStart > 2) {
const generationCount = values[values.length - 1] ?? 1;
const logPrefix = values[0] ?? "WAN LoRA schedule";
const modelType = values[1] ?? "flow";
const slotValues = values.slice(slotStart, values.length - 1);
const inferredMode = values.slice(2, slotStart).find((value) => isModeValue(value));
if (slotValues.length === maxSlotValuesLength) {
modeValue = isModeValue(inferredMode) ? inferredMode : modeValue;
normalized = [generationCount, logPrefix, modelType, ...slotValues];
}
}
}
// Pattern D (bank nodes): current head widgets are already first, but UI-only widgets
// were serialized before the slot payload. Example:
// generation_count, log_prefix, model_type, <ui widgets...>, slot_01..., ..., slot_64...
if (
!normalized
&& hasGenerationCount
&& values.length > genericExpected
&& typeof values[0] === "number"
&& typeof values[1] === "string"
&& isModelTypeValue(values[2])
) {
const extraCount = values.length - genericExpected;
const slotStart = 3 + extraCount;
const generationCount = values[0] ?? 1;
const logPrefix = values[1] ?? "WAN LoRA schedule";
const modelType = values[2] ?? "flow";
const uiValues = values.slice(3, slotStart);
const slotValues = values.slice(slotStart);
const inferredMode = uiValues.find((value) => isModeValue(value));
if (slotValues.length === maxSlotValuesLength) {
modeValue = isModeValue(inferredMode) ? inferredMode : modeValue;
normalized = [generationCount, logPrefix, modelType, ...slotValues];
}
}
// Determine what to work on: use normalized if we reordered, else work on original (in-place)
// for preset-migration-only case (already-correct 33-value arrays with legacy preset names).
const inferredSlots = slotCountFromSerializedLength(values.length);
const target = normalized ?? (inferredSlots > 0 ? values : null);
if (!target) return;
// Migrate legacy internal preset names → current _PRESET_OPTIONS names.
// Migrate legacy internal preset names → current _PRESET_OPTIONS names.
// "manual_range" was an internal alias for "custom range" in early builds.
// ComfyUI server REJECTS it as an invalid combo value because it is not in _PRESET_OPTIONS.
let presetPatched = false;
@@ -197,13 +372,16 @@ function showWidget(widget) {
}
}
function setWidgetValue(node, name, value) {
function setWidgetValue(node, name, value, options = {}) {
const { invokeCallback = true } = options;
const widget = getWidget(node, name);
if (!widget) return false;
widget.value = value;
try {
widget.callback?.(value, app.canvas, node);
} catch {}
if (invokeCallback && !isBatchUpdating(node)) {
try {
widget.callback?.(value, app.canvas, node);
} catch {}
}
return true;
}
@@ -261,26 +439,84 @@ function resetScheduleRules(node) {
node.properties[INIT_PROP] = {};
node.properties[AUTO_PROP] = {};
setVisibleSlots(node, 3);
beginBatchUpdate(node);
for (let slot = 1; slot <= MAX_SLOTS; slot += 1) {
const meta = slotMeta(slot);
const prefix = `slot_${String(slot).padStart(2, "0")}`;
setWidgetValue(node, `${prefix}_lora_name`, "no");
setWidgetValue(node, `${prefix}_strength`, 1);
setWidgetValue(node, `${prefix}_preset`, meta.preset);
setWidgetValue(node, `${prefix}_start`, meta.start);
setWidgetValue(node, `${prefix}_end`, meta.end);
setWidgetValue(node, `${prefix}_lora_name`, "no", { invokeCallback: false });
setWidgetValue(node, `${prefix}_strength`, 1, { invokeCallback: false });
setWidgetValue(node, `${prefix}_preset`, meta.preset, { invokeCallback: false });
setWidgetValue(node, `${prefix}_start`, meta.start, { invokeCallback: false });
setWidgetValue(node, `${prefix}_end`, meta.end, { invokeCallback: false });
node.properties[INIT_PROP][slot] = true;
}
endBatchUpdate(node);
clearDropHoverSlot(node);
}
function notifyScheduleUi(message, isError = false) {
function notifyScheduleUi(message, isError = false, node = null, options = {}) {
const prefix = "[IAMCCS_WanLoRASchedule UI]";
if (isError) {
console.error(prefix, message);
} else {
console.info(prefix, message);
}
if (node) {
showNodeToast(node, message, { isError, ...options });
}
}
function showNodeToast(node, message, options = {}) {
if (!node || !message) return;
const { isError = false, anchorWidgetName = null, durationMs = 1800 } = options;
node._iamccsToast = {
message: String(message),
isError: Boolean(isError),
anchorWidgetName,
until: Date.now() + Math.max(800, Number(durationMs) || 1800),
};
node.setDirtyCanvas?.(true, true);
}
function drawNodeToast(node, ctx) {
const toast = node?._iamccsToast;
if (!toast?.message) return;
if ((Number(toast.until) || 0) <= Date.now()) {
delete node._iamccsToast;
node.setDirtyCanvas?.(true, true);
return;
}
const message = toast.message.length > 72 ? `${toast.message.slice(0, 69)}...` : toast.message;
const anchorWidget = toast.anchorWidgetName ? getWidget(node, toast.anchorWidgetName) : null;
const fallbackWidth = Number(node?.size?.[0]) || SIMPLE_WIDTH;
const toastHeight = 24;
const paddingX = 10;
const textWidth = Math.ceil(ctx.measureText(message).width);
const toastWidth = Math.min(Math.max(textWidth + (paddingX * 2), 110), Math.max(120, fallbackWidth - 12));
let anchorY = 18;
if (anchorWidget) {
anchorY = getWidgetBodyY(node, anchorWidget) + (getWidgetHeight(anchorWidget) / 2);
}
const x = Math.max(6, fallbackWidth - toastWidth - 8);
const maxY = Math.max(8, (Number(node?.size?.[1]) || 200) - toastHeight - 8);
const y = Math.min(Math.max(8, anchorY - (toastHeight / 2)), maxY);
ctx.save();
ctx.fillStyle = toast.isError ? "rgba(120, 24, 24, 0.96)" : "rgba(24, 30, 36, 0.96)";
ctx.strokeStyle = toast.isError ? "rgba(255, 126, 126, 0.95)" : "rgba(110, 214, 156, 0.95)";
ctx.lineWidth = 1.25;
ctx.beginPath();
ctx.roundRect(x, y, toastWidth, toastHeight, 8);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#f5f7fa";
ctx.font = "12px sans-serif";
ctx.textBaseline = "middle";
ctx.fillText(message, x + paddingX, y + (toastHeight / 2) + 0.5);
ctx.restore();
}
function isAbortError(error) {
@@ -316,7 +552,7 @@ function buildSchedulePreset(node) {
return {
kind: PRESET_KIND,
version: PRESET_VERSION,
node_type: NODE_NAME,
node_type: node?.type || NODE_NAME,
exported_at: new Date().toISOString(),
mode,
visible_slots: getVisibleSlots(node),
@@ -362,11 +598,32 @@ async function applySchedulePreset(node, payload) {
}
}
if (hasConnectedInput(node, "linx")) {
const optionSet = getOptionSetForNode(node) ?? await getLoraSet();
for (const [slot, record] of slotMap.entries()) {
const sourceName = String(record?.lora_name || "no");
if (!sourceName || sourceName === "no") continue;
const candidates = candidateLowNames(sourceName);
let resolvedName = sourceName;
if (optionSet && optionSet.size > 0) {
resolvedName = candidates.find((candidate) => optionSet.has(candidate)) || sourceName;
} else {
resolvedName = candidates.find((candidate) => candidate !== sourceName) || sourceName;
}
if (resolvedName !== sourceName) {
slotMap.set(slot, { ...record, lora_name: resolvedName });
}
}
}
const nextMode = isModeValue(payload.mode) ? payload.mode : "simple";
const nextVisibleSlots = Math.max(3, Math.min(MAX_SLOTS, Math.trunc(Number(payload.visible_slots ?? payload.visibleSlots) || 3)));
const nextModelType = isModelTypeValue(payload.model_type || payload.modelType) ? String(payload.model_type || payload.modelType) : "flow";
const nextLogPrefix = String(payload.log_prefix || payload.logPrefix || "WAN LoRA schedule");
beginBatchUpdate(node);
resetScheduleRules(node);
node.properties = node.properties || {};
node.properties[MODE_PROP] = nextMode;
@@ -395,10 +652,11 @@ async function applySchedulePreset(node, payload) {
if (endWidget) endWidget.value = record.end;
node.properties[INIT_PROP][slot] = true;
}
endBatchUpdate(node);
clearDropHoverSlot(node);
applyLayout(node);
await syncAllLinkedLowNodes(node);
requestSyncAllLinkedLowNodes(node);
node.setDirtyCanvas?.(true, true);
}
@@ -441,7 +699,7 @@ async function saveSchedulePresetAs(node) {
downloadTextFile(suggestedName, text);
}
notifyScheduleUi(`Preset saved: ${suggestedName}`);
notifyScheduleUi(`Preset saved: ${suggestedName}`, false, node, { anchorWidgetName: SAVE_WIDGET });
}
async function pickPresetTextFile() {
@@ -485,7 +743,7 @@ async function loadSchedulePreset(node) {
const text = await pickPresetTextFile();
const payload = JSON.parse(text);
await applySchedulePreset(node, payload);
notifyScheduleUi("Preset loaded.");
notifyScheduleUi("Preset loaded.", false, node, { anchorWidgetName: LOAD_WIDGET });
}
function applyLabels(node) {
@@ -546,7 +804,7 @@ function ensureModeWidget(node) {
(value) => {
node.properties[MODE_PROP] = String(value || "simple");
applyLayout(node);
syncAllLinkedLowNodes(node);
requestSyncAllLinkedLowNodes(node);
node.setDirtyCanvas(true, true);
},
{ values: ["simple", "advanced"], serialize: false }
@@ -558,22 +816,28 @@ function ensureModeWidget(node) {
function ensureActionWidgets(node) {
if (!getWidget(node, LOAD_WIDGET)) {
const widget = node.addWidget("button", "Load Preset", null, () => {
void loadSchedulePreset(node).catch((error) => {
void runButtonAction(node, widget, "Loading...", async () => {
await loadSchedulePreset(node);
}).catch((error) => {
if (isAbortError(error)) return;
notifyScheduleUi(`Load preset failed: ${error?.message || error}`, true);
notifyScheduleUi(`Load preset failed: ${error?.message || error}`, true, node, { anchorWidgetName: LOAD_WIDGET, durationMs: 2600 });
});
});
widget.name = LOAD_WIDGET;
setButtonText(widget, "Load Preset");
markUiWidgetNonSerializable(widget);
}
if (!getWidget(node, SAVE_WIDGET)) {
const widget = node.addWidget("button", "Save As...", null, () => {
void saveSchedulePresetAs(node).catch((error) => {
void runButtonAction(node, widget, "Saving...", async () => {
await saveSchedulePresetAs(node);
}).catch((error) => {
if (isAbortError(error)) return;
notifyScheduleUi(`Save preset failed: ${error?.message || error}`, true);
notifyScheduleUi(`Save preset failed: ${error?.message || error}`, true, node, { anchorWidgetName: SAVE_WIDGET, durationMs: 2600 });
});
});
widget.name = SAVE_WIDGET;
setButtonText(widget, "Save As...");
markUiWidgetNonSerializable(widget);
}
if (!getWidget(node, ADD_WIDGET)) {
@@ -584,30 +848,36 @@ function ensureActionWidgets(node) {
setVisibleSlots(node, newSlot);
ensureVisibleDefaults(node);
applyLayout(node);
syncAllLinkedLowNodes(node);
requestSyncAllLinkedLowNodes(node);
node.setDirtyCanvas(true, true);
});
widget.name = ADD_WIDGET;
setButtonText(widget, "+ Add Slot");
markUiWidgetNonSerializable(widget);
}
if (!getWidget(node, DELETE_WIDGET)) {
const widget = node.addWidget("button", "- Delete Slot", null, () => {
setVisibleSlots(node, getVisibleSlots(node) - 1);
applyLayout(node);
syncAllLinkedLowNodes(node);
requestSyncAllLinkedLowNodes(node);
node.setDirtyCanvas(true, true);
});
widget.name = DELETE_WIDGET;
setButtonText(widget, "- Delete Slot");
markUiWidgetNonSerializable(widget);
}
if (!getWidget(node, RESET_WIDGET)) {
const widget = node.addWidget("button", "Reset Rules", null, () => {
resetScheduleRules(node);
applyLayout(node);
syncAllLinkedLowNodes(node);
node.setDirtyCanvas(true, true);
void runButtonAction(node, widget, "Resetting...", async () => {
resetScheduleRules(node);
applyLayout(node);
requestSyncAllLinkedLowNodes(node);
node.setDirtyCanvas(true, true);
notifyScheduleUi("Rules reset.", false, node, { anchorWidgetName: RESET_WIDGET, durationMs: 1400 });
});
});
widget.name = RESET_WIDGET;
setButtonText(widget, "Reset Rules");
markUiWidgetNonSerializable(widget);
}
}
@@ -620,6 +890,12 @@ function getInputIndexByName(node, name) {
return (node?.inputs || []).findIndex((input) => input?.name === name);
}
function hasConnectedInput(node, name) {
const inputIndex = getInputIndexByName(node, name);
if (inputIndex < 0) return false;
return node?.inputs?.[inputIndex]?.link != null;
}
function getLinkedLinxTargets(node) {
const graph = getGraph();
if (!graph) return [];
@@ -792,7 +1068,7 @@ function hasDroppedLoraCandidate(event) {
// The canvas is translated to node.pos before drawNode/onDrawForeground, so the coordinate
// system for drawing starts at (0,0) = top-left of node (including title).
// widget.y is set by _arrangeWidgets and starts at NODE_TITLE_HEIGHT (approximately),
// so no extra correction is needed — just subtract node.pos to get node-local coords.
// so no extra correction is needed — just subtract node.pos to get node-local coords.
function resolveLocalDropPoint(node, event) {
try {
const canvas = app?.canvas?.canvas;
@@ -854,7 +1130,7 @@ function getWidgetHeight(widget, nodeWidth) {
// widget.y is set by _arrangeWidgets(), widget.last_y is set during drawWidgets().
// Both are relative to the node body (not including the title bar).
function getWidgetBodyY(widget) {
// widget.y is set by _arrangeWidgets() — always > 0 for arranged widgets (title adds ~30px offset).
// widget.y is set by _arrangeWidgets() — always > 0 for arranged widgets (title adds ~30px offset).
// A value of 0 means the widget hasn't been arranged yet (default from addWidget), treat as unknown.
const y = widget?.y;
if (typeof y === "number" && Number.isFinite(y) && y > 0) return y;
@@ -1029,7 +1305,7 @@ function assignDroppedLora(node, event) {
try {
targetWidget.callback?.(loraName, app.canvas, node);
} catch {}
syncAllLinkedLowNodes(node);
requestSyncAllLinkedLowNodes(node);
clearDropHoverSlot(node);
node.setDirtyCanvas?.(true, true);
return true;
@@ -1057,7 +1333,7 @@ async function getLoraSet() {
const nodeType = Object.keys(app.graph?._nodes_by_id || {});
for (const id of nodeType) {
const n = app.graph.getNodeById(id);
if (n?.type !== NODE_NAME) continue;
if (!NODE_NAMES.has(n?.type)) continue;
const w = getWidget(n, "slot_01_lora_name");
const v = w?.options?.values ?? (Array.isArray(w?.options) ? w.options : null);
if (Array.isArray(v) && v.length > 1) {
@@ -1070,7 +1346,7 @@ async function getLoraSet() {
return _loraSetCache;
}
async function syncSlotToLowNode(highNode, lowNode, slot) {
async function syncSlotToLowNode(highNode, lowNode, slot, providedOptionSet = null) {
const prefix = `slot_${String(slot).padStart(2, "0")}`;
const highWidget = getWidget(highNode, `${prefix}_lora_name`);
const lowWidget = getWidget(lowNode, `${prefix}_lora_name`);
@@ -1097,7 +1373,7 @@ async function syncSlotToLowNode(highNode, lowNode, slot) {
if (!candidates.length) return; // no _HIGH_/_LOW_ pattern in name
// Build option set: prefer widget options, then server cache
let optionSet = null;
let optionSet = providedOptionSet;
const rawOpts = lowWidget?.options?.values ?? (Array.isArray(lowWidget?.options) ? lowWidget.options : null);
if (Array.isArray(rawOpts) && rawOpts.length > 1) {
optionSet = new Set(rawOpts);
@@ -1122,16 +1398,33 @@ async function syncSlotToLowNode(highNode, lowNode, slot) {
} catch {}
}
function syncSlotSettingsToLowNode(highNode, lowNode, slot) {
const prefix = `slot_${String(slot).padStart(2, "0")}`;
const fieldNames = ["strength", "preset", "start", "end"];
for (const fieldName of fieldNames) {
const highWidget = getWidget(highNode, `${prefix}_${fieldName}`);
const lowWidget = getWidget(lowNode, `${prefix}_${fieldName}`);
if (!highWidget || !lowWidget) continue;
lowWidget.value = highWidget.value;
try {
lowWidget.callback?.(highWidget.value, app.canvas, lowNode);
} catch {}
}
}
async function syncAllLinkedLowNodes(highNode) {
const targets = getLinkedLinxTargets(highNode);
if (!targets.length) return;
const optionSet = await getLoraSet();
for (const lowNode of targets) {
syncLowNodePresentation(highNode, lowNode);
ensureVisibleDefaults(lowNode);
applyLayout(lowNode);
for (let slot = 1; slot <= getVisibleSlots(highNode); slot += 1) {
await syncSlotToLowNode(highNode, lowNode, slot);
await syncSlotToLowNode(highNode, lowNode, slot, optionSet);
syncSlotSettingsToLowNode(highNode, lowNode, slot);
}
applyLayout(lowNode);
lowNode.setDirtyCanvas?.(true, true);
}
}
@@ -1140,28 +1433,43 @@ function wrapSlotCallbacks(node) {
if (node._iamccsScheduleWrappedCallbacks) return;
for (let slot = 1; slot <= MAX_SLOTS; slot += 1) {
const prefix = `slot_${String(slot).padStart(2, "0")}`;
// Wrap lora_name → triggers linx sync
// Wrap lora_name → triggers linx sync
const loraWidget = getWidget(node, `${prefix}_lora_name`);
if (loraWidget && !loraWidget._iamccsWrapped) {
const origLora = loraWidget.callback;
loraWidget.callback = (value, canvas, targetNode) => {
try { origLora?.(value, canvas, targetNode); } finally {
syncAllLinkedLowNodes(node);
requestSyncAllLinkedLowNodes(node);
}
};
loraWidget._iamccsWrapped = true;
}
// Wrap preset → refreshes start/end visibility
// Wrap preset → refreshes start/end visibility
const presetWidget = getWidget(node, `${prefix}_preset`);
if (presetWidget && !presetWidget._iamccsWrapped) {
const origPreset = presetWidget.callback;
presetWidget.callback = (value, canvas, targetNode) => {
try { origPreset?.(value, canvas, targetNode); } finally {
setTimeout(() => { applyLayout(node); node.setDirtyCanvas(true, true); }, 0);
setTimeout(() => {
applyLayout(node);
requestSyncAllLinkedLowNodes(node);
node.setDirtyCanvas(true, true);
}, 0);
}
};
presetWidget._iamccsWrapped = true;
}
for (const fieldName of ["strength", "start", "end"]) {
const widget = getWidget(node, `${prefix}_${fieldName}`);
if (!widget || widget._iamccsWrapped) continue;
const origCallback = widget.callback;
widget.callback = (value, canvas, targetNode) => {
try { origCallback?.(value, canvas, targetNode); } finally {
requestSyncAllLinkedLowNodes(node);
}
};
widget._iamccsWrapped = true;
}
}
node._iamccsScheduleWrappedCallbacks = true;
}
@@ -1207,9 +1515,7 @@ function setSlotVisibility(node, slot, visible, advanced) {
function reorderWidgets(node) {
const orderedNames = [
"generation_index",
"log_prefix",
"model_type",
...getSerializedHeadWidgetNames(node),
MODE_WIDGET,
LOAD_WIDGET,
SAVE_WIDGET,
@@ -1281,7 +1587,7 @@ app.registerExtension({
name: "iamccs.wan_lora_schedule_ui",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== NODE_NAME) return;
if (!NODE_NAMES.has(nodeData?.name)) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
@@ -1293,7 +1599,6 @@ app.registerExtension({
ensureActionWidgets(this);
setTimeout(() => {
applyLayout(this);
syncAllLinkedLowNodes(this);
this.setDirtyCanvas(true, true);
}, 0);
return result;
@@ -1344,26 +1649,23 @@ app.registerExtension({
nodeType.prototype.onDrawForeground = function (ctx) {
const result = onDrawForeground?.apply(this, arguments);
drawDropHover(this, ctx);
drawNodeToast(this, ctx);
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (config) {
this._iamccsRestoringState = true;
// 1. Normalize legacy corrupt widget-value arrays (modifies config in place)
normalizeLegacyWidgetValues(config);
normalizeLegacyWidgetValues(config, this);
const restoredValues = Array.isArray(config?.widgets_values) ? [...config.widgets_values] : null;
// 2. Re-apply normalized values to widgets explicitly.
// LiteGraph assigns widgets_values → widget.value BEFORE calling onConfigure,
// LiteGraph assigns widgets_values → widget.value BEFORE calling onConfigure,
// so if the values were corrupt, they are already set wrong. Re-assigning here
// (in the same iteration order, skipping non-serializable UI widgets) corrects them.
if (Array.isArray(config.widgets_values) && this.widgets) {
let j = 0;
for (const widget of this.widgets) {
if (!widget || widget.options?.serialize === false || widget._iamccsUiOnly) continue;
if (j < config.widgets_values.length) {
widget.value = config.widgets_values[j++];
}
}
// by explicit serialized-widget name order avoids bank-node slot shifts.
if (restoredValues && this.widgets) {
reapplySerializedWidgetValues(this, restoredValues);
}
const result = onConfigure?.apply(this, arguments);
@@ -1372,6 +1674,9 @@ app.registerExtension({
if (!this.properties[SLOTS_PROP]) this.properties[SLOTS_PROP] = 3;
ensureModeWidget(this);
ensureActionWidgets(this);
if (restoredValues && this.widgets) {
reapplySerializedWidgetValues(this, restoredValues);
}
setTimeout(() => {
// Mark ALL slots as already-initialized so ensureSlotDefaults won't clobber
// values that were restored from the saved workflow.
@@ -1379,8 +1684,15 @@ app.registerExtension({
for (let s = 1; s <= MAX_SLOTS; s++) {
this.properties[INIT_PROP][s] = true;
}
if (restoredValues && this.widgets) {
reapplySerializedWidgetValues(this, restoredValues);
}
applyLayout(this);
syncAllLinkedLowNodes(this);
this._iamccsRestoringState = false;
if (this._iamccsPendingSyncLowNodes) {
this._iamccsPendingSyncLowNodes = false;
requestSyncAllLinkedLowNodes(this);
}
this.setDirtyCanvas(true, true);
}, 0);
return result;
@@ -1391,7 +1703,9 @@ app.registerExtension({
const result = onConnectionsChange?.apply(this, arguments);
setTimeout(() => {
applyLayout(this);
syncAllLinkedLowNodes(this);
if (!isRestoringNodeState(this)) {
requestSyncAllLinkedLowNodes(this);
}
this.setDirtyCanvas(true, true);
}, 0);
return result;
@@ -1402,7 +1716,9 @@ app.registerExtension({
nodeType.prototype.collapse = function () {
const result = collapse?.apply(this, arguments);
setTimeout(() => {
syncAllLinkedLowNodes(this);
if (!isRestoringNodeState(this)) {
requestSyncAllLinkedLowNodes(this);
}
this.setDirtyCanvas(true, true);
}, 0);
return result;
+249
View File
@@ -20,6 +20,8 @@ const V2_PREFIX = {
const MAX_DRAFTS_PER_WORKSPACE = 4;
const MAX_DRAFT_CHARS_PER_WORKSPACE = 2_500_000;
const STORAGE_PATCH_FLAG = "__iamccsWorkflowDraftQuotaPatchApplied";
const STORAGE_PROTOTYPE_PATCH_FLAG = "__iamccsWorkflowDraftQuotaPrototypePatchApplied";
function safeParseJson(json) {
if (!json) return null;
@@ -56,6 +58,62 @@ function getPayloadKey(workspaceId, draftKey) {
return `${V2_PREFIX.draftPayload}${workspaceId}:${draftKey}`;
}
function extractWorkspaceIdFromKey(key) {
if (typeof key !== "string") return null;
if (key.startsWith(V2_PREFIX.draftIndex)) {
return key.slice(V2_PREFIX.draftIndex.length);
}
if (key.startsWith(V2_PREFIX.draftPayload)) {
const rest = key.slice(V2_PREFIX.draftPayload.length);
const separatorIndex = rest.indexOf(":");
return separatorIndex === -1 ? null : rest.slice(0, separatorIndex);
}
if (key.startsWith(V2_PREFIX.lastActivePath)) {
return key.slice(V2_PREFIX.lastActivePath.length);
}
if (key.startsWith(V2_PREFIX.lastOpenPaths)) {
return key.slice(V2_PREFIX.lastOpenPaths.length);
}
return null;
}
function isDraftStorageKey(key) {
return typeof key === "string" && (
key.startsWith(V2_PREFIX.draftIndex)
|| key.startsWith(V2_PREFIX.draftPayload)
|| key.startsWith(V2_PREFIX.lastActivePath)
|| key.startsWith(V2_PREFIX.lastOpenPaths)
|| LEGACY_LOCAL_KEYS.includes(key)
|| LEGACY_PREFIXES.some((prefix) => key.startsWith(prefix))
);
}
function isQuotaExceededError(error) {
if (!error) return false;
const name = String(error.name || "");
const message = String(error.message || "").toLowerCase();
return name === "QuotaExceededError"
|| name === "NS_ERROR_DOM_QUOTA_REACHED"
|| message.includes("quota")
|| message.includes("storage is full");
}
function cleanupWorkspaceTrackingStorage(localStorageRef, workspaceId = null) {
let removed = 0;
for (const key of listStorageKeys(localStorageRef)) {
const isTrackingKey = key.startsWith(V2_PREFIX.lastActivePath)
|| key.startsWith(V2_PREFIX.lastOpenPaths);
if (!isTrackingKey) continue;
if (workspaceId !== null && extractWorkspaceIdFromKey(key) !== workspaceId) {
continue;
}
if (removeKey(localStorageRef, key)) removed += 1;
}
return removed;
}
function cleanupLegacyWorkflowStorage(localStorageRef) {
let removed = 0;
for (const key of LEGACY_LOCAL_KEYS) {
@@ -154,6 +212,7 @@ function cleanupWorkflowPersistenceStorage() {
if (!localStorageRef) return;
let removed = cleanupLegacyWorkflowStorage(localStorageRef);
removed += cleanupWorkspaceTrackingStorage(localStorageRef);
let rewritten = 0;
const workspaceIds = new Set();
@@ -180,9 +239,199 @@ function cleanupWorkflowPersistenceStorage() {
}
}
function recoverDraftStorage(localStorageRef, key) {
let removed = cleanupLegacyWorkflowStorage(localStorageRef);
let rewritten = 0;
const workspaceIds = new Set();
const workspaceId = extractWorkspaceIdFromKey(key);
if (workspaceId) workspaceIds.add(workspaceId);
for (const storageKey of listStorageKeys(localStorageRef)) {
if (storageKey.startsWith(V2_PREFIX.draftIndex)) {
workspaceIds.add(storageKey.slice(V2_PREFIX.draftIndex.length));
}
}
for (const currentWorkspaceId of workspaceIds) {
removed += cleanupWorkspaceTrackingStorage(localStorageRef, currentWorkspaceId);
const result = cleanupWorkspaceDrafts(localStorageRef, currentWorkspaceId);
removed += result.removed;
if (result.rewritten) rewritten += 1;
}
return { removed, rewritten };
}
function aggressiveRecoverDraftStorage(localStorageRef) {
let removed = cleanupLegacyWorkflowStorage(localStorageRef);
removed += cleanupWorkspaceTrackingStorage(localStorageRef);
for (const storageKey of listStorageKeys(localStorageRef)) {
if (
storageKey.startsWith(V2_PREFIX.draftIndex)
|| storageKey.startsWith(V2_PREFIX.draftPayload)
) {
if (removeKey(localStorageRef, storageKey)) removed += 1;
}
}
return { removed, rewritten: 0 };
}
function isStorageLike(storageRef) {
return storageRef != null
&& typeof storageRef.getItem === "function"
&& typeof storageRef.setItem === "function"
&& typeof storageRef.removeItem === "function";
}
function isLocalStorageRef(storageRef) {
try {
return storageRef === window.localStorage;
} catch {
return false;
}
}
function shouldSuppressDraftQuotaFailure(key) {
if (!isDraftStorageKey(key)) return false;
return typeof key === "string" && (
key.startsWith(V2_PREFIX.draftPayload)
|| key.startsWith(V2_PREFIX.draftIndex)
|| key.startsWith(V2_PREFIX.lastActivePath)
|| key.startsWith(V2_PREFIX.lastOpenPaths)
|| LEGACY_LOCAL_KEYS.includes(key)
|| LEGACY_PREFIXES.some((prefix) => key.startsWith(prefix))
);
}
function handleDraftQuotaSetItem(originalSetItem, storageRef, key, value, error) {
if (!isDraftStorageKey(key) || !isQuotaExceededError(error) || !isStorageLike(storageRef)) {
throw error;
}
const canRecover = isLocalStorageRef(storageRef);
const recovery = canRecover ? recoverDraftStorage(storageRef, key) : { removed: 0, rewritten: 0 };
console.warn("[IAMCCS] Workflow draft quota reached, attempting recovery", {
key,
removedKeys: recovery.removed,
rewrittenIndexes: recovery.rewritten,
storage: canRecover ? "localStorage" : "other",
});
try {
const result = Reflect.apply(originalSetItem, storageRef, [key, value]);
console.info("[IAMCCS] Workflow draft save recovered after storage cleanup", {
key,
removedKeys: recovery.removed,
rewrittenIndexes: recovery.rewritten,
storage: canRecover ? "localStorage" : "other",
});
return result;
} catch (retryError) {
if (!isQuotaExceededError(retryError)) {
console.error("[IAMCCS] Workflow draft save failed after cleanup with non-quota error", {
key,
removedKeys: recovery.removed,
rewrittenIndexes: recovery.rewritten,
retryError,
});
throw retryError;
}
const aggressiveRecovery = canRecover
? aggressiveRecoverDraftStorage(storageRef)
: { removed: 0, rewritten: 0 };
if (aggressiveRecovery.removed) {
console.warn("[IAMCCS] Workflow draft quota still exceeded, applying aggressive recovery", {
key,
removedKeys: aggressiveRecovery.removed,
storage: canRecover ? "localStorage" : "other",
});
}
try {
const result = Reflect.apply(originalSetItem, storageRef, [key, value]);
console.info("[IAMCCS] Workflow draft save recovered after aggressive cleanup", {
key,
removedKeys: recovery.removed + aggressiveRecovery.removed,
rewrittenIndexes: recovery.rewritten,
storage: canRecover ? "localStorage" : "other",
});
return result;
} catch (finalRetryError) {
if (!isQuotaExceededError(finalRetryError) || !shouldSuppressDraftQuotaFailure(key)) {
console.error("[IAMCCS] Workflow draft save still exceeds storage quota after aggressive cleanup", {
key,
removedKeys: recovery.removed + aggressiveRecovery.removed,
rewrittenIndexes: recovery.rewritten,
retryError: finalRetryError,
});
throw finalRetryError;
}
console.warn("[IAMCCS] Workflow draft save skipped after quota recovery failed", {
key,
removedKeys: recovery.removed + aggressiveRecovery.removed,
rewrittenIndexes: recovery.rewritten,
storage: canRecover ? "localStorage" : "other",
});
return undefined;
}
}
}
function installDraftStorageQuotaGuard() {
try {
const localStorageRef = window.localStorage;
if (!localStorageRef || localStorageRef[STORAGE_PATCH_FLAG]) return;
const storagePrototype = Object.getPrototypeOf(localStorageRef);
if (!storagePrototype || storagePrototype[STORAGE_PROTOTYPE_PATCH_FLAG]) {
Object.defineProperty(localStorageRef, STORAGE_PATCH_FLAG, {
value: true,
configurable: false,
enumerable: false,
writable: false,
});
return;
}
const originalSetItem = storagePrototype.setItem;
if (typeof originalSetItem !== "function") return;
storagePrototype.setItem = function patchedSetItem(key, value) {
try {
return Reflect.apply(originalSetItem, this, [key, value]);
} catch (error) {
return handleDraftQuotaSetItem(originalSetItem, this, key, value, error);
}
};
Object.defineProperty(storagePrototype, STORAGE_PROTOTYPE_PATCH_FLAG, {
value: true,
configurable: false,
enumerable: false,
writable: false,
});
Object.defineProperty(localStorageRef, STORAGE_PATCH_FLAG, {
value: true,
configurable: false,
enumerable: false,
writable: false,
});
} catch (error) {
console.warn("[IAMCCS] Workflow draft quota guard skipped", error);
}
}
app.registerExtension({
name: "iamccs.workflow_persist_cleanup",
async setup() {
cleanupWorkflowPersistenceStorage();
installDraftStorageQuotaGuard();
},
});