Updated IAMCCS-nodes to version 1.4.2
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
+560
@@ -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;
|
||||
};
|
||||
},
|
||||
});
|
||||
+577
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
+602
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
+640
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
+1607
File diff suppressed because it is too large
Load Diff
+188
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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()}")
|
||||
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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'}"
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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}`);
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user