diff --git a/AUTOLINK_PAPER.md b/AUTOLINK_PAPER.md new file mode 100644 index 0000000..d71f4b9 --- /dev/null +++ b/AUTOLINK_PAPER.md @@ -0,0 +1,185 @@ +# IAMCCS AutoLink — Paper & Usage Instructions (EN/IT) + +## English + +### 1) What is AutoLink? +AutoLink is a **Set/Get** workflow tool designed to keep ComfyUI graphs clean and maintainable. + +Instead of long cables across the canvas, AutoLink lets you: +- Convert direct connections into **Set** (source) + **Get** (destination) pairs +- Restore the original direct connections when needed +- Apply repeatable filters (groups/blacklist), layout rules, and colors + +Everything is controlled by a dedicated “tool” node that operates on the canvas. + +### 2) Components +AutoLink is made of four logical elements: + +1. **AutoLink Converter** + - Buttons to convert/restore links. +2. **AutoLink Arguments** + - Central configuration: group filters, alignment/layout, packing/anti-overlap, colors, blacklist. +3. **AutoLink Set** + - Created near the source node: captures an output and exposes it under a key. +4. **AutoLink Get** + - Created near the destination node: retrieves the key and feeds the target input. + +### 3) Quickstart +1. Add to the canvas: + - **AutoLink Arguments** + - **AutoLink Converter** +2. Connect **AutoLink Arguments** output to the Converter `arg` input. +3. Adjust options (or keep defaults). +4. Click **Convert All Links**. + +To revert: +- Click **Restore Direct Links**. + +### 4) v1.3.3 reliability updates (important) +AutoLink Set/Get nodes are **UI tools** and are treated as **virtual** nodes. To prevent “missing required input” prompt errors, the extension automatically: +- Materializes direct links **only during prompt serialization/queue**, then restores the AutoLink wiring +- Supports nested graphs/subgraphs +- Truncates long AutoLink titles with an ellipsis (`…`) so they stay inside the node header + +--- + +## Italiano + +### 1) Cos’è AutoLink +AutoLink è un sistema **Set/Get** pensato per rendere i workflow ComfyUI più ordinati, leggibili e facili da mantenere. + +Invece di avere cavi lunghi che attraversano la canvas, AutoLink permette di: +- Convertire automaticamente collegamenti diretti in coppie **Set** (sorgente) + **Get** (destinazione) +- Ripristinare i collegamenti originali quando serve +- Gestire filtri, gruppi, layout e colori in modo ripetibile + +Il tutto è controllato da un nodo “tool” che opera sulla canvas. + +### 2) I nodi coinvolti +AutoLink è composto da quattro elementi logici: + +1. **AutoLink Converter** + - Contiene i pulsanti per convertire/ripristinare i collegamenti. +2. **AutoLink Arguments** + - Contiene tutte le opzioni: filtri per gruppi, layout, packing/anti-overlap, colori, blacklist. +3. **AutoLink Set** + - Viene creato vicino al nodo sorgente: cattura un output e lo espone con una chiave. +4. **AutoLink Get** + - Viene creato vicino al nodo destinazione: recupera la chiave del Set e alimenta l’input. + +### 3) Quickstart (workflow consigliato) +1. Aggiungi in canvas: + - **AutoLink Arguments** + - **AutoLink Converter** +2. Collega l’output di **AutoLink Arguments** all’input `arg` di **AutoLink Converter**. +3. Imposta le opzioni nel nodo **AutoLink Arguments** (anche lasciando i default). +4. Premi **Convert All Links** nel nodo **AutoLink Converter**. + +Per tornare indietro: +- Premi **Restore Direct Links** nel Converter. + +### 4) Aggiornamenti affidabilità v1.3.3 (importante) +I nodi Set/Get di AutoLink sono strumenti **lato UI** e vengono trattati come nodi **virtuali**. Per evitare errori di prompt del tipo “required input missing”, l’estensione: +- Materializza i link diretti **solo durante la queue/serializzazione del prompt**, poi ripristina il wiring AutoLink +- Supporta grafi annidati/subgraph +- Tronca i titoli AutoLink troppo lunghi con ellissi (`…`) per non farli uscire dal nodo + +--- + +## 5) Opzioni principali (Arguments) + +### 4.1 GroupExclude +- Se abilitato, **non converte** i collegamenti tra due nodi che stanno **dentro lo stesso group**. +- I collegamenti che **entrano** o **escono** dal group possono comunque essere convertiti (dipende anche da GroupInOutExclude). + +Quando usarlo: +- Se un group rappresenta un “blocco logico” che vuoi tenere cablato internamente. + +### 4.2 GroupInOutExclude +Gestisce i link che attraversano un confine di group: +- `None`: nessuna esclusione. +- `ExcludeEnter`: non converte i link che **entrano** in un group. +- `ExcludeExit`: non converte i link che **escono** da un group. +- `ExcludeBoth`: combina entrambe. + +### 4.3 Align mode +Determina come vengono posizionati Set/Get dopo la conversione e quando fai relayout. + +Opzioni principali: +- `TopToDown`, `BottomToTop`, `CenterUpDown`, `CenterDownUp` +- `AlignX_Right`, `AlignX_Left` +- `Columns_Down`, `Columns_Up` +- `Rake_Down`, `Rake_Up` +- **`Proportional`** (consigliato per layout “come i cavi”) + +#### Align = Proportional (come nell’immagine) +Con `Proportional`, Set e Get vengono agganciati alla **stessa altezza (Y)** del relativo connettore (slot) del nodo: +- Set: si allinea alla Y dello **slot di output** sorgente +- Get: si allinea alla Y dello **slot di input** destinazione + +In caso di collisioni, mantiene la Y e cerca spazio spostandosi orizzontalmente. + +### 4.4 Packing mode +Controlla l’anti-overlap durante posizionamento e relayout: +- `AvoidAll`: evita sovrapposizioni con tutti i nodi. +- `AvoidNonAutoLink`: evita solo i nodi non-AutoLink (Set/Get possono compattarsi fra loro). + +### 4.5 SeparateCol + colori +- `SeparateCol`: se attivo, permette di usare colori diversi per Set e Get. +- `AutoLinkColor`: colore base (Set). +- `AutoLinkColorGet`: colore dei Get (solo se SeparateCol è attivo). + +### 4.6 ColorTitles +Cambia il colore del testo del titolo dei nodi AutoLink: +- `White` +- `Black` +- `Auto` + +### 4.7 Blacklist (ID e Types) +AutoLink permette di escludere nodi dalla conversione: + +- `all_nodes_sel`: + - OFF: la blacklist lavora per **tipo** (`[TYPE] ...`) + - ON: la blacklist lavora per **ID singolo nodo** + +- `add_to_blacklist`: + - Scegli un nodo (ID) o un tipo. + +- `blacklist_mode` (solo per nodi singoli): + - `both`: esclude link dove il nodo è sorgente o destinazione + - `only_output`: esclude solo quando il nodo è sorgente (output) + - `only_input`: esclude solo quando il nodo è destinazione (input) + +- `EXECUTE`: + - Applica davvero l’inserimento (o l’update della modalità) e poi pulisce i widget. + +- `blacklist_view`: + - Elenco leggibile: `id - nome nodo - (modalità)` e `[TYPE] ...`. + - Selezionare una voce **non rimuove nulla**. + +- `remove_blacklist`: + - Rimuove la voce attualmente selezionata in `blacklist_view`. + +--- + +## 6) Best practices +- Prima di convertire “tutto”, imposta la blacklist per escludere nodi che vuoi lasciare cablati. +- Usa `GroupExclude` per mantenere “blocchi” interni puliti. +- Usa `Proportional` quando vuoi un layout che segua visivamente l’ordine degli slot (come routing naturale dei cavi). +- Se la canvas è molto piena, prova `PackingMode = AvoidAll`. + +--- + +## 7) Troubleshooting +- **Convert All Links non sembra fare nulla**: + - Verifica che `AutoLink Arguments` sia collegato all’input `arg` del Converter. + - Controlla blacklist e filtri group. +- **Nodi sovrapposti**: + - Prova `PackingMode = AvoidAll`. + - Cambia align mode o usa relayout cambiando `align_mode`. + +--- + +## 8) Documentazione correlata +- AUTOLINK_README.md +- AUTOLINK_TECHNICAL_PAPER.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9da74..c22007e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +# IAMCCS Nodes - Changelog + +## 🆕 Version 1.3.3 — AutoLink + LTX-2 Extension Module (Stability Update) + +Date: 2026-01-26 + +### AutoLink (frontend) +- AutoLink Set/Get + Converter for compact “wireless” graphs +- Convert/Restore tools: + - `Convert All Links` + - `Restore Direct Links` +- Group-aware filters: `GroupExclude`, `GroupInOutExclude` +- Layout controls: multiple align modes (including `Proportional`) + packing/anti-overlap +- Styling controls: color presets, optional separate Set/Get colors, title text color +- Blacklist improvements: per-node (directional) and per-type entries + +Stability fixes: +- AutoLink links are now materialized automatically during queue/prompt serialization (then restored), preventing “missing required input” prompt errors +- Works with nested graphs/subgraphs +- Long AutoLink titles are truncated with an ellipsis (`…`) to prevent overflow + +### LTX-2 Extension (backend nodes) +- Added **LTX-2 Extension Module** (`IAMCCS_LTX2_ExtensionModule`): + - Extends/merges image batches with overlap management + - Built-in math operations for overlap/start-frames logic + - AutoLink integration for overlap sharing between iterations (`autolink_overlap_in/out`) + - Multiple blending modes: cut, linear_blend, ease_in_out, filmic_crossfade, perceptual_crossfade + - Automatic `start_images` extraction for the next pass + - `total_frames` / `validate_ltx2` moved out to dedicated validation utilities + +- Added **LTX-2 Get Images From Batch** (`IAMCCS_LTX2_GetImageFromBatch`): + - Extract frames from start/end or by explicit range + +- Added **LTX-2 Frame Count Validator** (`IAMCCS_LTX2_FrameCountValidator`): + - Validates/corrects counts to the LTX-2 `8n+1` rule + - Intended to be placed before the LTX Sampler + +### LTX-2 frame-count robustness +- `IAMCCS_LTX2_TimeFrameCount` snaps computed `length` to the next valid `8n+1` +- UI seconds↔length sync snaps to valid `8n+1` lengths +- Optional VAE encode auto-padding to valid `8n+1` (defensive safeguard) + +--- + ## 🆕 Version 1.3.2 — LTX-2 Nodes Pack Date: 2026-01-15 diff --git a/IAMCCS_WAN22_SVI_PRO_v-2_FIXED2.json b/IAMCCS_WAN22_SVI_PRO_v-2_FIXED2.json new file mode 100644 index 0000000..a8135df --- /dev/null +++ b/IAMCCS_WAN22_SVI_PRO_v-2_FIXED2.json @@ -0,0 +1,7855 @@ +{ + "id": "ec7da562-7e21-4dac-a0d2-f4441e1efd3b", + "revision": 0, + "last_node_id": 406, + "last_link_id": 637, + "nodes": [ + { + "id": 90, + "type": "VAELoader", + "pos": [ + 355.83329161693285, + -299.32503868581756 + ], + "size": [ + 344.731689453125, + 59.98149108886719 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 284 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.45", + "Node name for S&R": "VAELoader", + "models": [ + { + "name": "wan_2.1_vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors", + "directory": "vae" + } + ], + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.1", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "wan_2.1_vae.safetensors" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 127, + "type": "KSamplerSelect", + "pos": [ + 345.08992579346346, + -1123.9621547462161 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 253 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "KSamplerSelect", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "euler" + ] + }, + { + "id": 128, + "type": "SplitSigmas", + "pos": [ + 654.4312613872978, + -995.7349316352739 + ], + "size": [ + 270, + 78 + ], + "flags": {}, + "order": 88, + "mode": 0, + "inputs": [ + { + "name": "sigmas", + "type": "SIGMAS", + "link": 250 + } + ], + "outputs": [ + { + "name": "high_sigmas", + "type": "SIGMAS", + "links": [ + 254 + ] + }, + { + "name": "low_sigmas", + "type": "SIGMAS", + "links": [ + 255 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "SplitSigmas", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 2 + ] + }, + { + "id": 129, + "type": "SetNode", + "pos": [ + 662.1608990780405, + -1095.7303654898724 + ], + "size": [ + 210, + 61.01185634578792 + ], + "flags": { + "collapsed": true + }, + "order": 53, + "mode": 0, + "inputs": [ + { + "name": "SAMPLER", + "type": "SAMPLER", + "link": 253 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_sampler", + "properties": { + "previousName": "sampler", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sampler" + ], + "color": "#614a4a", + "bgcolor": "#3b2c2c" + }, + { + "id": 130, + "type": "SetNode", + "pos": [ + 977.1417459346173, + -982.3415261171592 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 95, + "mode": 0, + "inputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "link": 254 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_sigmas_high", + "properties": { + "previousName": "sigmas_high", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sigmas_high" + ], + "color": "#485248", + "bgcolor": "#272e27" + }, + { + "id": 131, + "type": "SetNode", + "pos": [ + 976.7971196007186, + -921.6081690337912 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 96, + "mode": 0, + "inputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "link": 255 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_sigmas_low", + "properties": { + "previousName": "sigmas_low", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sigmas_low" + ], + "color": "#485248", + "bgcolor": "#272e27" + }, + { + "id": 143, + "type": "SetNode", + "pos": [ + 741.7428782352613, + -416.88225262179515 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 62, + "mode": 0, + "inputs": [ + { + "name": "CLIP", + "type": "CLIP", + "link": 283 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_clip_model", + "properties": { + "previousName": "clip_model", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "clip_model" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 144, + "type": "SetNode", + "pos": [ + 748.4211301174612, + -318.9330297956516 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 52, + "mode": 0, + "inputs": [ + { + "name": "VAE", + "type": "VAE", + "link": 284 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": [] + } + ], + "title": "Set_vae", + "properties": { + "previousName": "vae", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "vae" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 301, + "type": "GetNode", + "pos": [ + 3087.7162093215893, + -1427.513264257069 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 510 + ] + } + ], + "title": "Get_clip_model", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "clip_model" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 302, + "type": "GetNode", + "pos": [ + 3087.7162093215893, + -1332.115190772293 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 512 + ] + } + ], + "title": "Get_model_low", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "model_low" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 303, + "type": "GetNode", + "pos": [ + 3087.7162093215893, + -1379.814227514681 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 511 + ] + } + ], + "title": "Get_model_high", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "model_high" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 304, + "type": "GetNode", + "pos": [ + 3087.7162093215893, + -1284.4161540299058 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 513 + ] + } + ], + "title": "Get_vae", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "vae" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 307, + "type": "GetNode", + "pos": [ + 3087.7162093215893, + -1474.0112283999702 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 6, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 509 + ] + } + ], + "title": "Get_sigmas_low", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sigmas_low" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 308, + "type": "GetNode", + "pos": [ + 3087.7162093215893, + -1523.2692797219427 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 7, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 508 + ] + } + ], + "title": "Get_sigmas_high", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sigmas_high" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 317, + "type": "SetNode", + "pos": [ + 681.7975573430999, + -658.9482118523099 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 54, + "mode": 0, + "inputs": [ + { + "name": "INT", + "type": "INT", + "link": 521 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_total_seconds", + "properties": { + "previousName": "total_seconds", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "total_seconds" + ], + "color": "#1b4669", + "bgcolor": "#29699c" + }, + { + "id": 320, + "type": "GetNode", + "pos": [ + 1675.0856338721744, + -1560.5379786555038 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 8, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 528 + ] + } + ], + "title": "Get_sampler", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sampler" + ], + "color": "#614a4a", + "bgcolor": "#3b2c2c" + }, + { + "id": 322, + "type": "GetNode", + "pos": [ + 1675.0856338721744, + -1369.7418316859557 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 9, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 532 + ] + } + ], + "title": "Get_model_high", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "model_high" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 323, + "type": "RandomNoise", + "pos": [ + 1894.7623606534441, + -1599.584724707526 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "NOISE", + "type": "NOISE", + "links": [ + 527 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "RandomNoise", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 88888, + "fixed" + ] + }, + { + "id": 324, + "type": "GetNode", + "pos": [ + 1675.0856338721744, + -1463.9388325712432 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 11, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 530 + ] + } + ], + "title": "Get_sigmas_low", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sigmas_low" + ], + "color": "#485248", + "bgcolor": "#272e27" + }, + { + "id": 325, + "type": "GetNode", + "pos": [ + 1675.0856338721744, + -1513.1968838932153 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 12, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 529 + ] + } + ], + "title": "Get_sigmas_high", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sigmas_high" + ], + "color": "#485248", + "bgcolor": "#272e27" + }, + { + "id": 326, + "type": "GetNode", + "pos": [ + 1675.0856338721744, + -1322.0427949435682 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 13, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 533 + ] + } + ], + "title": "Get_model_low", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "model_low" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 300, + "type": "GetNode", + "pos": [ + 3087.7162093215893, + -1570.6103744842308 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 14, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 507 + ] + } + ], + "title": "Get_sampler", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sampler" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 122, + "type": "BasicScheduler", + "pos": [ + 351.695490116646, + -1011.961093236818 + ], + "size": [ + 270, + 106 + ], + "flags": {}, + "order": 74, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 227 + } + ], + "outputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 250 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "BasicScheduler", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "simple", + 4, + 1 + ] + }, + { + "id": 316, + "type": "PrimitiveInt", + "pos": [ + 352.3842164378712, + -714.101590154819 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 521 + ] + } + ], + "title": "TIME (sec)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PrimitiveInt", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 15, + "fixed" + ] + }, + { + "id": 135, + "type": "VAEEncode", + "pos": [ + 1202.9242449349158, + -1809.7829258316697 + ], + "size": [ + 140, + 46 + ], + "flags": {}, + "order": 77, + "mode": 0, + "inputs": [ + { + "name": "pixels", + "type": "IMAGE", + "link": 269 + }, + { + "name": "vae", + "type": "VAE", + "link": 402 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 323 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "VAEEncode", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 188, + "type": "GetNode", + "pos": [ + 1039.5145597877395, + -1731.9407714607933 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 16, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 402 + ] + } + ], + "title": "Get_vae", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "vae" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 169, + "type": "SetNode", + "pos": [ + 1218.3615041051412, + -1704.8600039082394 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 91, + "mode": 0, + "inputs": [ + { + "name": "LATENT", + "type": "LATENT", + "link": 323 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": [] + } + ], + "title": "Set_anchor_samples", + "properties": { + "previousName": "anchor_samples", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "anchor_samples" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 343, + "type": "PreviewImage", + "pos": [ + 1060.9773166853965, + -1627.2267382312257 + ], + "size": [ + 317.8207483037961, + 278.42035414580585 + ], + "flags": {}, + "order": 78, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 558 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PreviewImage", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [] + }, + { + "id": 136, + "type": "ImageResizeKJv2", + "pos": [ + 745.003267826397, + -1719.2812777506963 + ], + "size": [ + 270, + 336 + ], + "flags": {}, + "order": 63, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 581 + }, + { + "name": "mask", + "shape": 7, + "type": "MASK", + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 269, + 558 + ] + }, + { + "name": "width", + "type": "INT", + "links": null + }, + { + "name": "height", + "type": "INT", + "links": null + }, + { + "name": "mask", + "type": "MASK", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "79f529a84a8c20fe5dcdfa984c6be7a94102c014", + "Node name for S&R": "ImageResizeKJv2", + "aux_id": "kijai/ComfyUI-KJNodes", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 832, + 480, + "lanczos", + "crop", + "0, 0, 0", + "center", + 2, + "cpu" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 328, + "type": "GetNode", + "pos": [ + 1708.642311291139, + -1273.1434062573542 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 17, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 534 + ] + } + ], + "title": "Get_vae", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "vae" + ], + "color": "#332922", + "bgcolor": "#593930" + }, + { + "id": 321, + "type": "GetNode", + "pos": [ + 1675.0856338721744, + -1417.4408684283426 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 18, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 531 + ] + } + ], + "title": "Get_clip_model", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "clip_model" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 329, + "type": "GetNode", + "pos": [ + 1644.9799352504144, + -1220.4091252064923 + ], + "size": [ + 210, + 50 + ], + "flags": { + "collapsed": true + }, + "order": 19, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 535 + ] + } + ], + "title": "Get_anchor_samples", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "anchor_samples" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 344, + "type": "easy showAnything", + "pos": [ + 3754.2235861310023, + -186.94428116436305 + ], + "size": [ + 210, + 88 + ], + "flags": {}, + "order": 93, + "mode": 0, + "inputs": [ + { + "name": "anything", + "shape": 7, + "type": "*", + "link": 559 + } + ], + "outputs": [ + { + "name": "output", + "type": "*", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "1.3.4", + "Node name for S&R": "easy showAnything", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "2" + ], + "color": "#222", + "bgcolor": "#000" + }, + { + "id": 354, + "type": "SetNode", + "pos": [ + 3807.3395545707976, + -282.2032626406764 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 94, + "mode": 0, + "inputs": [ + { + "name": "INT", + "type": "INT", + "link": 584 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_INDEX", + "properties": { + "previousName": "INDEX", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "INDEX" + ], + "color": "#1b4669", + "bgcolor": "#29699c" + }, + { + "id": 360, + "type": "SetNode", + "pos": [ + 2573.9742090241366, + -1589.25148277492 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 80, + "mode": 0, + "inputs": [ + { + "name": "LATENT", + "type": "LATENT", + "link": 587 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_SAMPLES", + "properties": { + "previousName": "SAMPLES", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "SAMPLES" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 327, + "type": "08be7d9e-8766-4c78-98c9-50fc97389ca0", + "pos": [ + 1897.741881805851, + -1451.5549362688987 + ], + "size": [ + 276.7490534966155, + 288 + ], + "flags": { + "collapsed": false + }, + "order": 69, + "mode": 0, + "inputs": [ + { + "name": "noise", + "type": "NOISE", + "link": 527 + }, + { + "name": "sampler", + "type": "SAMPLER", + "link": 528 + }, + { + "label": "sigmas_high", + "name": "sigmas", + "type": "SIGMAS", + "link": 529 + }, + { + "label": "sigmas_low", + "name": "sigmas_1", + "type": "SIGMAS", + "link": 530 + }, + { + "name": "clip", + "type": "CLIP", + "link": 531 + }, + { + "label": "model_high", + "name": "model", + "type": "MODEL", + "link": 532 + }, + { + "label": "model_low", + "name": "model_1", + "type": "MODEL", + "link": 533 + }, + { + "name": "vae", + "type": "VAE", + "link": 534 + }, + { + "name": "anchor_samples", + "type": "LATENT", + "link": 535 + }, + { + "name": "prev_samples", + "type": "LATENT", + "link": null + }, + { + "label": "prev_images", + "name": "source_images", + "type": "IMAGE", + "link": null + }, + { + "label": "text_prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 542 + } + ], + "outputs": [ + { + "label": "samples", + "name": "output", + "type": "LATENT", + "links": [ + 587 + ] + }, + { + "label": "decoded_images", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 536, + 588 + ] + } + ], + "properties": { + "proxyWidgets": [ + [ + "-1", + "text" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.5.1", + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ] + }, + { + "id": 361, + "type": "SetNode", + "pos": [ + 2553.6333766071825, + -1410.5432995019082 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 82, + "mode": 0, + "inputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "link": 588 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_DECODED_IMG", + "properties": { + "previousName": "DECODED_IMG", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "DECODED_IMG" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 362, + "type": "GetNode", + "pos": [ + 2710.82910510753, + -439.5695377237848 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 20, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 589 + ] + } + ], + "title": "Get_SAMPLES", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "SAMPLES" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 364, + "type": "RandomNoise //Inspire", + "pos": [ + 3356.8486032823566, + -1841.294453367152 + ], + "size": [ + 270, + 226 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "NOISE", + "type": "NOISE", + "links": [ + 591 + ] + } + ], + "properties": { + "cnr_id": "comfyui-inspire-pack", + "ver": "1.23.0", + "Node name for S&R": "RandomNoise //Inspire", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + 605056063716529, + "randomize", + "GPU(=A1111)", + "incremental", + 0, + 0, + "linear", + 0 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 370, + "type": "SetNode", + "pos": [ + 1890.1400062875023, + -414.548054104644 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 79, + "mode": 0, + "inputs": [ + { + "name": "*", + "type": "*", + "link": 596 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_1", + "properties": { + "previousName": "1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "1" + ] + }, + { + "id": 373, + "type": "SetNode", + "pos": [ + 1812.2931506091395, + 353.60665480222355 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 67, + "mode": 0, + "inputs": [ + { + "name": "STRING", + "type": "STRING", + "link": 599 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_4", + "properties": { + "previousName": "4", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "4" + ] + }, + { + "id": 375, + "type": "SetNode", + "pos": [ + 1825.1500409212122, + 611.5563821128518 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 68, + "mode": 0, + "inputs": [ + { + "name": "STRING", + "type": "STRING", + "link": 600 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_5", + "properties": { + "previousName": "5", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "5" + ] + }, + { + "id": 371, + "type": "SetNode", + "pos": [ + 1813.8187046602716, + -156.75784369809392 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 65, + "mode": 0, + "inputs": [ + { + "name": "STRING", + "type": "STRING", + "link": 597 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_2", + "properties": { + "previousName": "2", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "2" + ] + }, + { + "id": 372, + "type": "SetNode", + "pos": [ + 1812.0605307161154, + 149.6614023477076 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 66, + "mode": 0, + "inputs": [ + { + "name": "STRING", + "type": "STRING", + "link": 598 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_3", + "properties": { + "previousName": "3", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "3" + ] + }, + { + "id": 319, + "type": "SetNode", + "pos": [ + 1832.833492770389, + -354.07545083810413 + ], + "size": [ + 285.9129641428478, + 71.01161137141946 + ], + "flags": { + "collapsed": true + }, + "order": 64, + "mode": 0, + "inputs": [ + { + "name": "STRING", + "type": "STRING", + "link": 525 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 596 + ] + } + ], + "title": "Set_PRO_SCE", + "properties": { + "previousName": "PRO_SCE", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "PRO_SCE" + ] + }, + { + "id": 376, + "type": "GetNode", + "pos": [ + 2141.9401855391334, + -447.938295263549 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 22, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 601 + ] + } + ], + "title": "Get_1", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "1" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 381, + "type": "GetNode", + "pos": [ + 2137.9775768846725, + -385.8593127819167 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 23, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 602 + ] + } + ], + "title": "Get_2", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "2" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 378, + "type": "GetNode", + "pos": [ + 2140.619315987647, + -317.1762002088025 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 24, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 603 + ] + } + ], + "title": "Get_3", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "3" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 379, + "type": "GetNode", + "pos": [ + 2139.2985189914766, + -244.53055153654546 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 25, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 604 + ] + } + ], + "title": "Get_4", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "4" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 380, + "type": "GetNode", + "pos": [ + 2143.260982535303, + -169.24330887194787 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 26, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 605 + ] + } + ], + "title": "Get_5", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "5" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 355, + "type": "GetNode", + "pos": [ + 2133.335438565198, + -90.52530295530687 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 27, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 606 + ] + } + ], + "title": "Get_INDEX", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "INDEX" + ], + "color": "#1b4669", + "bgcolor": "#29699c" + }, + { + "id": 356, + "type": "easy anythingIndexSwitch", + "pos": [ + 2299.2861542452824, + -408.0690692741314 + ], + "size": [ + 270, + 158 + ], + "flags": {}, + "order": 55, + "mode": 0, + "inputs": [ + { + "name": "value0", + "shape": 7, + "type": "*", + "link": 601 + }, + { + "name": "value1", + "shape": 7, + "type": "*", + "link": 602 + }, + { + "name": "index", + "type": "INT", + "widget": { + "name": "index" + }, + "link": 606 + }, + { + "name": "value2", + "type": "*", + "link": 603 + }, + { + "name": "value3", + "type": "*", + "link": 604 + }, + { + "name": "value4", + "type": "*", + "link": 605 + }, + { + "name": "value5", + "type": "*", + "link": null + } + ], + "outputs": [ + { + "name": "value", + "type": "*", + "links": [ + 610 + ] + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "e94405e610bce3416b6c887d6b70601ab7a32dd7", + "Node name for S&R": "easy anythingIndexSwitch", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + 0 + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 306, + "type": "GetNode", + "pos": [ + 3073.1870795871378, + -1230.1128420853995 + ], + "size": [ + 210, + 34 + ], + "flags": { + "collapsed": true + }, + "order": 28, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 514 + ] + } + ], + "title": "Get_anchor_samples", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "anchor_samples" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 387, + "type": "GetNode", + "pos": [ + 3120.203717692286, + -1124.0959115059263 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 29, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "*", + "type": "*", + "links": [ + 614 + ] + } + ], + "title": "GetNode", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "VAL_2_OUT" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 386, + "type": "GetNode", + "pos": [ + 3095.257824214445, + -1177.63464820337 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 30, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "*", + "type": "*", + "links": [ + 612 + ] + } + ], + "title": "Get_VAL_1_OUT", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "VAL_1_OUT" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 383, + "type": "GetNode", + "pos": [ + 3093.6280275156714, + -1078.941688326561 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 31, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "*", + "type": "*", + "links": [ + 609 + ] + } + ], + "title": "Get_FINAL_PROMPT", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "FINAL_PROMPT" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 390, + "type": "GetNode", + "pos": [ + 3410.258687770349, + -454.94745298440506 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 32, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 617 + ] + } + ], + "title": "Get_SAMPLES_EXT", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "SAMPLES_EXT" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 391, + "type": "GetNode", + "pos": [ + 3453.84614952902, + -358.52695049387427 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 33, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 618 + ] + } + ], + "title": "Get_IMG_EXT", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "IMG_EXT" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 333, + "type": "easy forLoopEnd", + "pos": [ + 3747.5955818749308, + -439.828366748678 + ], + "size": [ + 164.203125, + 86 + ], + "flags": {}, + "order": 84, + "mode": 0, + "inputs": [ + { + "name": "flow", + "shape": 5, + "type": "FLOW_CONTROL", + "link": 544 + }, + { + "name": "initial_value1", + "shape": 7, + "type": "*", + "link": 617 + }, + { + "name": "initial_value2", + "type": "*", + "link": 618 + }, + { + "name": "initial_value3", + "type": "*", + "link": null + } + ], + "outputs": [ + { + "name": "value1", + "type": "*", + "links": null + }, + { + "name": "value2", + "type": "*", + "links": [ + 547 + ] + }, + { + "name": "value3", + "type": "*", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "1.3.4", + "Node name for S&R": "easy forLoopEnd", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#1f1f48", + "bgcolor": "rgba(24,24,27,.9)" + }, + { + "id": 384, + "type": "SetNode", + "pos": [ + 3430.1483400212774, + -308.4147809874838 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 86, + "mode": 0, + "inputs": [ + { + "name": "*", + "type": "*", + "link": 611 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_VAL_1_OUT", + "properties": { + "previousName": "VAL_1_OUT", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "VAL_1_OUT" + ] + }, + { + "id": 294, + "type": "easy forLoopStart", + "pos": [ + 3041.4010551205247, + -453.7040243730285 + ], + "size": [ + 293.4524540345526, + 189.57603486092376 + ], + "flags": {}, + "order": 73, + "mode": 0, + "inputs": [ + { + "name": "initial_value1", + "shape": 7, + "type": "*", + "link": 589 + }, + { + "name": "total", + "type": "INT", + "widget": { + "name": "total" + }, + "link": 501 + }, + { + "name": "initial_value2", + "type": "*", + "link": 590 + }, + { + "name": "initial_value3", + "type": "*", + "link": null + } + ], + "outputs": [ + { + "name": "flow", + "shape": 5, + "type": "FLOW_CONTROL", + "links": [ + 544 + ] + }, + { + "name": "index", + "type": "INT", + "links": [ + 523 + ] + }, + { + "name": "value1", + "type": "*", + "links": [ + 611 + ] + }, + { + "name": "value2", + "type": "*", + "links": [ + 613 + ] + }, + { + "name": "value3", + "type": "*", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "1.3.4", + "Node name for S&R": "easy forLoopStart", + "ue_properties": { + "widget_ue_connectable": { + "total": true + }, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1 + ], + "color": "#1f1f48", + "bgcolor": "rgba(24,24,27,.9)" + }, + { + "id": 363, + "type": "GetNode", + "pos": [ + 2692.2057223509055, + -392.9442465016886 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 34, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 590 + ] + } + ], + "title": "Get_DECODED_IMG", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "DECODED_IMG" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 296, + "type": "GetNode", + "pos": [ + 2790.2328263897875, + -128.01915919011503 + ], + "size": [ + 210, + 58 + ], + "flags": { + "collapsed": true + }, + "order": 35, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 502 + ] + } + ], + "title": "Get_total_seconds", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "total_seconds" + ], + "color": "#1b4669", + "bgcolor": "#29699c" + }, + { + "id": 385, + "type": "SetNode", + "pos": [ + 3428.8275430251097, + -408.7978377159869 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 87, + "mode": 0, + "inputs": [ + { + "name": "*", + "type": "*", + "link": 613 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_VAL_2_OUT", + "properties": { + "previousName": "VAL_2_OUT", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "VAL_2_OUT" + ] + }, + { + "id": 318, + "type": "SimpleMath+", + "pos": [ + 3467.662012462077, + -237.02688120748343 + ], + "size": [ + 210, + 98 + ], + "flags": { + "collapsed": false + }, + "order": 85, + "mode": 0, + "inputs": [ + { + "name": "a", + "shape": 7, + "type": "INT,FLOAT", + "link": 523 + }, + { + "name": "b", + "shape": 7, + "type": "INT,FLOAT", + "link": null + }, + { + "name": "c", + "shape": 7, + "type": "*", + "link": null + } + ], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 559, + 584 + ] + }, + { + "name": "FLOAT", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui_essentials", + "ver": "9d9f4bedfc9f0321c19faf71855e228c93bd0dc9", + "Node name for S&R": "SimpleMath+", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "a+1" + ], + "color": "#222", + "bgcolor": "#000" + }, + { + "id": 388, + "type": "SetNode", + "pos": [ + 3741.175351303154, + -1609.108207074329 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 71, + "mode": 0, + "inputs": [ + { + "name": "LATENT", + "type": "LATENT", + "link": 615 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_SAMPLES_EXT", + "properties": { + "previousName": "SAMPLES_EXT", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "SAMPLES_EXT" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 389, + "type": "SetNode", + "pos": [ + 3746.75323260296, + -1564.63635194269 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 72, + "mode": 0, + "inputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "link": 616 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_IMG_EXT", + "properties": { + "previousName": "IMG_EXT", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "IMG_EXT" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 392, + "type": "GetNode", + "pos": [ + 3764.1785236785868, + -1502.5088633661057 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 36, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 619 + ] + } + ], + "title": "Get_IMG_EXT", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "IMG_EXT" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 382, + "type": "SetNode", + "pos": [ + 2559.6084771059286, + 26.033590896645308 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 83, + "mode": 0, + "inputs": [ + { + "name": "*", + "type": "*", + "link": 608 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_FINAL_PROMPT", + "properties": { + "previousName": "FINAL_PROMPT", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "FINAL_PROMPT" + ] + }, + { + "id": 293, + "type": "easy showAnything", + "pos": [ + 2213.0617412416673, + 3.6766057328171695 + ], + "size": [ + 314.7015167467025, + 240.5347855297273 + ], + "flags": {}, + "order": 70, + "mode": 0, + "inputs": [ + { + "name": "anything", + "shape": 7, + "type": "*", + "link": 610 + } + ], + "outputs": [ + { + "name": "output", + "type": "*", + "links": [ + 608 + ] + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "1.3.4", + "Node name for S&R": "easy showAnything", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "Subject: Her hair lifts slightly as breeze stirs — she turns her head, listening.\nScene: Dark trees line the shore; moonlight breaks through clouds, casting long shadows.\nAction: She pauses, tilts her face upward — a single drop of water rolls down her neck. Camera circles gently around her shoulders." + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 352, + "type": "UnetLoaderGGUF", + "pos": [ + -1238.8688609733217, + -1176.9012180793875 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 37, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 629 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-GGUF", + "ver": "c2e3b0adc3f805d401d745f69c88cc7e3eb5db75", + "Node name for S&R": "UnetLoaderGGUF", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + "smoothMixWan22I2VT2V_i2vHigh-Q5_K_S.gguf" + ] + }, + { + "id": 393, + "type": "Any Switch (rgthree)", + "pos": [ + -826.1487818347597, + -1068.58490983122 + ], + "size": [ + 263.72841796875, + 106 + ], + "flags": {}, + "order": 60, + "mode": 0, + "inputs": [ + { + "dir": 3, + "name": "any_01", + "type": "MODEL", + "link": 629 + }, + { + "dir": 3, + "name": "any_02", + "type": "MODEL", + "link": 621 + }, + { + "dir": 3, + "name": "any_03", + "type": "MODEL", + "link": null + }, + { + "dir": 3, + "name": "any_04", + "type": "MODEL", + "link": null + }, + { + "dir": 3, + "name": "any_05", + "type": "MODEL", + "link": null + } + ], + "outputs": [ + { + "dir": 4, + "label": "MODEL", + "name": "*", + "shape": 3, + "type": "MODEL", + "links": [ + 630 + ] + } + ], + "properties": { + "cnr_id": "rgthree-comfy", + "ver": "8ff50e4521881eca1fe26aec9615fc9362474931", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [] + }, + { + "id": 137, + "type": "SetNode", + "pos": [ + -113.34490841255587, + -1040.637788378767 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 89, + "mode": 0, + "inputs": [ + { + "name": "MODEL", + "type": "MODEL", + "link": 631 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_model_high", + "properties": { + "previousName": "model_high", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "model_high" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 104, + "type": "ModelSamplingSD3", + "pos": [ + 553.2262655678377, + -846.5806579969146 + ], + "size": [ + 210, + 60 + ], + "flags": {}, + "order": 59, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 632 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 227 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.45", + "Node name for S&R": "ModelSamplingSD3", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.1", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 5.000000000000001 + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 399, + "type": "GetNode", + "pos": [ + 362.68875235022864, + -834.008595837459 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 38, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 632 + ] + } + ], + "title": "Get_model_high", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "model_high" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 394, + "type": "UNETLoader", + "pos": [ + -1241.2882522654688, + -922.1960497026663 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 39, + "mode": 2, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 621 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.8.2", + "Node name for S&R": "UNETLoader", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + "Wan2_2-I2V-A14B-HIGH_fp8_e5m2_scaled_KJ.safetensors", + "default" + ] + }, + { + "id": 398, + "type": "IAMCCS_WanLoRAStackModelIO", + "pos": [ + -498.2439244668632, + -1127.660968837162 + ], + "size": [ + 321.9859375, + 270 + ], + "flags": {}, + "order": 75, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 630 + }, + { + "name": "lora", + "shape": 7, + "type": "LORA", + "link": null + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 631 + ] + } + ], + "properties": { + "aux_id": "IAMCCS/IAMCCS-nodes", + "ver": "87665e54a4b06a50fc631609f53d3bacdd10fc3e", + "Node name for S&R": "IAMCCS_WanLoRAStackModelIO", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + "WAN22\\wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors", + 1, + "SVI_v2_PRO_Wan2.2-I2V-A14B_HIGH_lora_rank_128_fp16.safetensors", + 1, + "no", + 0, + "no", + 0, + "flow" + ] + }, + { + "id": 401, + "type": "UnetLoaderGGUF", + "pos": [ + -1237.3403049907386, + -636.5259029825223 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 40, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 633 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-GGUF", + "ver": "c2e3b0adc3f805d401d745f69c88cc7e3eb5db75", + "Node name for S&R": "UnetLoaderGGUF", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + "smoothMixWan22I2VT2V_i2vLow-Q5_K_S.gguf" + ] + }, + { + "id": 403, + "type": "UNETLoader", + "pos": [ + -1239.7596962828857, + -381.8207346058019 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 41, + "mode": 2, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 634 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.8.2", + "Node name for S&R": "UNETLoader", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + "Wan2_2-I2V-A14B-LOW_fp8_e5m2_scaled_KJ.safetensors", + "default" + ] + }, + { + "id": 405, + "type": "IAMCCS_WanLoRAStackModelIO", + "pos": [ + -496.71536848427945, + -587.2856537402969 + ], + "size": [ + 321.9859375, + 270 + ], + "flags": {}, + "order": 76, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 635 + }, + { + "name": "lora", + "shape": 7, + "type": "LORA", + "link": null + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 636 + ] + } + ], + "properties": { + "aux_id": "IAMCCS/IAMCCS-nodes", + "ver": "87665e54a4b06a50fc631609f53d3bacdd10fc3e", + "Node name for S&R": "IAMCCS_WanLoRAStackModelIO", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + "WAN22\\wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors", + 1, + "SVI_v2_PRO_Wan2.2-I2V-A14B_LOW_lora_rank_128_fp16.safetensors", + 1, + "no", + 0, + "no", + 0, + "flow" + ] + }, + { + "id": 138, + "type": "SetNode", + "pos": [ + -102.50868968170452, + -541.4089488487232 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 90, + "mode": 0, + "inputs": [ + { + "name": "MODEL", + "type": "MODEL", + "link": 636 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": null + } + ], + "title": "Set_model_low", + "properties": { + "previousName": "model_low", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "model_low" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 402, + "type": "Any Switch (rgthree)", + "pos": [ + -824.620225852176, + -528.2095947343555 + ], + "size": [ + 263.72841796875, + 106 + ], + "flags": {}, + "order": 61, + "mode": 0, + "inputs": [ + { + "dir": 3, + "name": "any_01", + "type": "MODEL", + "link": 633 + }, + { + "dir": 3, + "name": "any_02", + "type": "MODEL", + "link": 634 + }, + { + "dir": 3, + "name": "any_03", + "type": "MODEL", + "link": null + }, + { + "dir": 3, + "name": "any_04", + "type": "MODEL", + "link": null + }, + { + "dir": 3, + "name": "any_05", + "type": "MODEL", + "link": null + } + ], + "outputs": [ + { + "dir": 4, + "label": "MODEL", + "name": "*", + "shape": 3, + "type": "MODEL", + "links": [ + 635 + ] + } + ], + "properties": { + "cnr_id": "rgthree-comfy", + "ver": "8ff50e4521881eca1fe26aec9615fc9362474931", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [] + }, + { + "id": 400, + "type": "Fast Groups Muter (rgthree)", + "pos": [ + -1677.3790186204328, + -1080.4718119335234 + ], + "size": [ + 308.4172663125313, + 106 + ], + "flags": {}, + "order": 42, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "OPT_CONNECTION", + "type": "*", + "links": null + } + ], + "title": "HIGH MODELS", + "properties": { + "matchColors": "", + "matchTitle": "HIGH", + "showNav": true, + "showAllGraphs": true, + "sort": "position", + "customSortAlphabet": "", + "toggleRestriction": "default", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + } + }, + { + "id": 406, + "type": "Fast Groups Muter (rgthree)", + "pos": [ + -1678.581175874653, + -521.4907400194472 + ], + "size": [ + 297.7202816630951, + 96.73454637652048 + ], + "flags": {}, + "order": 43, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "OPT_CONNECTION", + "type": "*", + "links": null + } + ], + "properties": { + "matchColors": "", + "matchTitle": "LOW", + "showNav": true, + "showAllGraphs": true, + "sort": "position", + "customSortAlphabet": "", + "toggleRestriction": "default", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + } + }, + { + "id": 84, + "type": "CLIPLoader", + "pos": [ + 355.83329161693285, + -459.3250386858174 + ], + "size": [ + 346.391845703125, + 106 + ], + "flags": {}, + "order": 44, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 0, + "links": [ + 283 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.45", + "Node name for S&R": "CLIPLoader", + "models": [ + { + "name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "directory": "text_encoders" + } + ], + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.1", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "umt5_xxl_fp16.safetensors", + "wan", + "default" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 351, + "type": "LoadImage", + "pos": [ + 390.01329440687584, + -1740.0305411550864 + ], + "size": [ + 282.798828125, + 314.0000000000001 + ], + "flags": {}, + "order": 45, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 581 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.8.2", + "Node name for S&R": "LoadImage", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + "QWEN_00005_.png", + "image" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 331, + "type": "VHS_VideoCombine", + "pos": [ + 2244.8226198473562, + -1619.3363502998589 + ], + "size": [ + 273.9599911039079, + 334 + ], + "flags": {}, + "order": 81, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 536 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "8550981384301e9bc5bfea83e5c2c75258102593", + "Node name for S&R": "VHS_VideoCombine", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": { + "frame_rate": 24, + "loop_count": 0, + "filename_prefix": "Wan22_SVI_Pro_progress", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": false, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "Wan22_SVI_Pro_progress_00001.mp4", + "subfolder": "", + "type": "temp", + "format": "video/h264-mp4", + "frame_rate": 16, + "workflow": "Wan22_SVI_Pro_progress_00001.png", + "fullpath": "E:\\ComfyUI-Easy-Install\\ComfyUI-Easy-Install\\ComfyUI\\temp\\Wan22_SVI_Pro_progress_00001.mp4" + } + } + } + }, + { + "id": 312, + "type": "VHS_VideoCombine", + "pos": [ + 4022.0653168767003, + -503.7850316115217 + ], + "size": [ + 797.5640031222306, + 1695.7776054118665 + ], + "flags": {}, + "order": 92, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 547 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "8550981384301e9bc5bfea83e5c2c75258102593", + "Node name for S&R": "VHS_VideoCombine", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": { + "frame_rate": 24, + "loop_count": 0, + "filename_prefix": "Wan22_SVI_Pro_final", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "Wan22_SVI_Pro_final_00001.mp4", + "subfolder": "", + "type": "output", + "format": "video/h264-mp4", + "frame_rate": 16, + "workflow": "Wan22_SVI_Pro_final_00001.png", + "fullpath": "E:\\ComfyUI-Easy-Install\\ComfyUI-Easy-Install\\ComfyUI\\output\\Wan22_SVI_Pro_final_00001.mp4" + } + } + }, + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 310, + "type": "VHS_VideoCombine", + "pos": [ + 4014.2040273560083, + -1596.1503951468137 + ], + "size": [ + 296.5255405114517, + 334 + ], + "flags": {}, + "order": 58, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 619 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "8550981384301e9bc5bfea83e5c2c75258102593", + "Node name for S&R": "VHS_VideoCombine", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": { + "frame_rate": 24, + "loop_count": 0, + "filename_prefix": "Wan22_SVI_Pro_progress", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": false, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "Wan22_SVI_Pro_progress_00003.mp4", + "subfolder": "", + "type": "temp", + "format": "video/h264-mp4", + "frame_rate": 16, + "workflow": "Wan22_SVI_Pro_progress_00003.png", + "fullpath": "E:\\ComfyUI-Easy-Install\\ComfyUI-Easy-Install\\ComfyUI\\temp\\Wan22_SVI_Pro_progress_00003.mp4" + } + } + } + }, + { + "id": 298, + "type": "MathExpression|pysssss", + "pos": [ + 2780.213484401525, + -334.5103596947303 + ], + "size": [ + 210, + 128 + ], + "flags": { + "collapsed": false + }, + "order": 57, + "mode": 0, + "inputs": [ + { + "name": "a", + "shape": 7, + "type": "INT,FLOAT,IMAGE,LATENT", + "link": 502 + }, + { + "name": "b", + "shape": 7, + "type": "INT,FLOAT,IMAGE,LATENT", + "link": null + }, + { + "name": "c", + "shape": 7, + "type": "INT,FLOAT,IMAGE,LATENT", + "link": null + } + ], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 501 + ] + }, + { + "name": "FLOAT", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-custom-scripts", + "ver": "1.2.5", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ceil(((a*24)/81)-1)" + ], + "color": "#222", + "bgcolor": "#000" + }, + { + "id": 309, + "type": "c2a1c2eb-20cf-4b0b-aa1a-ebed356dec34", + "pos": [ + 3340.415516199842, + -1548.9524693507599 + ], + "size": [ + 326.62850054315595, + 288 + ], + "flags": { + "collapsed": false + }, + "order": 56, + "mode": 0, + "inputs": [ + { + "name": "noise", + "type": "NOISE", + "link": 591 + }, + { + "name": "sampler", + "type": "SAMPLER", + "link": 507 + }, + { + "label": "sigmas_high", + "name": "sigmas", + "type": "SIGMAS", + "link": 508 + }, + { + "label": "sigmas_low", + "name": "sigmas_1", + "type": "SIGMAS", + "link": 509 + }, + { + "name": "clip", + "type": "CLIP", + "link": 510 + }, + { + "label": "model_high", + "name": "model", + "type": "MODEL", + "link": 511 + }, + { + "label": "model_low", + "name": "model_1", + "type": "MODEL", + "link": 512 + }, + { + "name": "vae", + "type": "VAE", + "link": 513 + }, + { + "name": "anchor_samples", + "type": "LATENT", + "link": 514 + }, + { + "name": "prev_samples", + "type": "LATENT", + "link": 612 + }, + { + "label": "prev_images", + "name": "source_images", + "type": "IMAGE", + "link": 614 + }, + { + "label": "text_prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 609 + } + ], + "outputs": [ + { + "label": "samples", + "name": "output", + "type": "LATENT", + "links": [ + 615 + ] + }, + { + "name": "extended_images", + "type": "IMAGE", + "links": [ + 616 + ] + } + ], + "properties": { + "proxyWidgets": [ + [ + "-1", + "text" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.5.1", + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 288, + "type": "PrimitiveStringMultiline", + "pos": [ + 1390.7468186339183, + -451.802917546974 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 46, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 525 + ] + } + ], + "title": "Prompt", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PrimitiveStringMultiline", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "A goblin walks towards camera" + ], + "color": "#232", + "bgcolor": "#353", + "shape": 1 + }, + { + "id": 366, + "type": "PrimitiveStringMultiline", + "pos": [ + 1391.5730127508996, + -192.65684439498244 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 47, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 597 + ] + } + ], + "title": "Prompt", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PrimitiveStringMultiline", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "a goblin walks towards camera looking to the right and to the left" + ], + "color": "#232", + "bgcolor": "#353", + "shape": 1 + }, + { + "id": 367, + "type": "PrimitiveStringMultiline", + "pos": [ + 1389.1714317481963, + 64.3045726672012 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 48, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 598 + ] + } + ], + "title": "Prompt", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PrimitiveStringMultiline", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "a goblin walks towards camera and suddenly sotps and look at the camera smiling" + ], + "color": "#232", + "bgcolor": "#353", + "shape": 1 + }, + { + "id": 368, + "type": "PrimitiveStringMultiline", + "pos": [ + 1387.9706412468447, + 316.46272878490896 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 49, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 599 + ] + } + ], + "title": "Prompt", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PrimitiveStringMultiline", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "camera slowly doll in to the face of the goblin while smiling looking at the camera" + ], + "color": "#232", + "bgcolor": "#353", + "shape": 1 + }, + { + "id": 369, + "type": "PrimitiveStringMultiline", + "pos": [ + 1387.9707072062238, + 578.2272089134299 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 50, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 600 + ] + } + ], + "title": "Prompt", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PrimitiveStringMultiline", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "camera zoom to the face of the goblin while the goblin screams scared" + ], + "color": "#232", + "bgcolor": "#353", + "shape": 1 + }, + { + "id": 332, + "type": "GetNode", + "pos": [ + 1645.5110994766185, + -1167.594634960315 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 51, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 542 + ] + } + ], + "title": "Get_PRO_SCE", + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "PRO_SCE" + ] + } + ], + "links": [ + [ + 227, + 104, + 0, + 122, + 0, + "MODEL" + ], + [ + 250, + 122, + 0, + 128, + 0, + "SIGMAS" + ], + [ + 253, + 127, + 0, + 129, + 0, + "SAMPLER" + ], + [ + 254, + 128, + 0, + 130, + 0, + "SIGMAS" + ], + [ + 255, + 128, + 1, + 131, + 0, + "SIGMAS" + ], + [ + 269, + 136, + 0, + 135, + 0, + "IMAGE" + ], + [ + 283, + 84, + 0, + 143, + 0, + "CLIP" + ], + [ + 284, + 90, + 0, + 144, + 0, + "VAE" + ], + [ + 323, + 135, + 0, + 169, + 0, + "LATENT" + ], + [ + 402, + 188, + 0, + 135, + 1, + "VAE" + ], + [ + 501, + 298, + 0, + 294, + 1, + "INT" + ], + [ + 502, + 296, + 0, + 298, + 0, + "INT" + ], + [ + 507, + 300, + 0, + 309, + 1, + "SAMPLER" + ], + [ + 508, + 308, + 0, + 309, + 2, + "SIGMAS" + ], + [ + 509, + 307, + 0, + 309, + 3, + "SIGMAS" + ], + [ + 510, + 301, + 0, + 309, + 4, + "CLIP" + ], + [ + 511, + 303, + 0, + 309, + 5, + "MODEL" + ], + [ + 512, + 302, + 0, + 309, + 6, + "MODEL" + ], + [ + 513, + 304, + 0, + 309, + 7, + "VAE" + ], + [ + 514, + 306, + 0, + 309, + 8, + "LATENT" + ], + [ + 521, + 316, + 0, + 317, + 0, + "INT" + ], + [ + 523, + 294, + 1, + 318, + 0, + "INT" + ], + [ + 525, + 288, + 0, + 319, + 0, + "STRING" + ], + [ + 527, + 323, + 0, + 327, + 0, + "NOISE" + ], + [ + 528, + 320, + 0, + 327, + 1, + "SAMPLER" + ], + [ + 529, + 325, + 0, + 327, + 2, + "SIGMAS" + ], + [ + 530, + 324, + 0, + 327, + 3, + "SIGMAS" + ], + [ + 531, + 321, + 0, + 327, + 4, + "CLIP" + ], + [ + 532, + 322, + 0, + 327, + 5, + "MODEL" + ], + [ + 533, + 326, + 0, + 327, + 6, + "MODEL" + ], + [ + 534, + 328, + 0, + 327, + 7, + "VAE" + ], + [ + 535, + 329, + 0, + 327, + 8, + "LATENT" + ], + [ + 536, + 327, + 1, + 331, + 0, + "IMAGE" + ], + [ + 542, + 332, + 0, + 327, + 11, + "STRING" + ], + [ + 544, + 294, + 0, + 333, + 0, + "FLOW_CONTROL" + ], + [ + 547, + 333, + 1, + 312, + 0, + "IMAGE" + ], + [ + 558, + 136, + 0, + 343, + 0, + "IMAGE" + ], + [ + 559, + 318, + 0, + 344, + 0, + "INT" + ], + [ + 581, + 351, + 0, + 136, + 0, + "IMAGE" + ], + [ + 584, + 318, + 0, + 354, + 0, + "INT" + ], + [ + 587, + 327, + 0, + 360, + 0, + "LATENT" + ], + [ + 588, + 327, + 1, + 361, + 0, + "IMAGE" + ], + [ + 589, + 362, + 0, + 294, + 0, + "LATENT" + ], + [ + 590, + 363, + 0, + 294, + 2, + "IMAGE" + ], + [ + 591, + 364, + 0, + 309, + 0, + "NOISE" + ], + [ + 596, + 319, + 0, + 370, + 0, + "*" + ], + [ + 597, + 366, + 0, + 371, + 0, + "STRING" + ], + [ + 598, + 367, + 0, + 372, + 0, + "STRING" + ], + [ + 599, + 368, + 0, + 373, + 0, + "STRING" + ], + [ + 600, + 369, + 0, + 375, + 0, + "STRING" + ], + [ + 601, + 376, + 0, + 356, + 0, + "STRING" + ], + [ + 602, + 381, + 0, + 356, + 1, + "STRING" + ], + [ + 603, + 378, + 0, + 356, + 3, + "*" + ], + [ + 604, + 379, + 0, + 356, + 4, + "STRING" + ], + [ + 605, + 380, + 0, + 356, + 5, + "STRING" + ], + [ + 606, + 355, + 0, + 356, + 2, + "INT" + ], + [ + 608, + 293, + 0, + 382, + 0, + "*" + ], + [ + 609, + 383, + 0, + 309, + 11, + "STRING" + ], + [ + 610, + 356, + 0, + 293, + 0, + "*" + ], + [ + 611, + 294, + 2, + 384, + 0, + "*" + ], + [ + 612, + 386, + 0, + 309, + 9, + "LATENT" + ], + [ + 613, + 294, + 3, + 385, + 0, + "*" + ], + [ + 614, + 387, + 0, + 309, + 10, + "IMAGE" + ], + [ + 615, + 309, + 0, + 388, + 0, + "LATENT" + ], + [ + 616, + 309, + 1, + 389, + 0, + "IMAGE" + ], + [ + 617, + 390, + 0, + 333, + 1, + "LATENT" + ], + [ + 618, + 391, + 0, + 333, + 2, + "IMAGE" + ], + [ + 619, + 392, + 0, + 310, + 0, + "IMAGE" + ], + [ + 621, + 394, + 0, + 393, + 1, + "MODEL" + ], + [ + 629, + 352, + 0, + 393, + 0, + "MODEL" + ], + [ + 630, + 393, + 0, + 398, + 0, + "MODEL" + ], + [ + 631, + 398, + 0, + 137, + 0, + "MODEL" + ], + [ + 632, + 399, + 0, + 104, + 0, + "MODEL" + ], + [ + 633, + 401, + 0, + 402, + 0, + "MODEL" + ], + [ + 634, + 403, + 0, + 402, + 1, + "MODEL" + ], + [ + 635, + 402, + 0, + 405, + 0, + "MODEL" + ], + [ + 636, + 405, + 0, + 138, + 0, + "MODEL" + ] + ], + "groups": [ + { + "id": 2, + "title": "VIDEO SEGMENT 1", + "bounding": [ + 1627.7879936848228, + -1692.9363502998588, + 1064.2991298092993, + 763.8100906453444 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Settting", + "bounding": [ + 325.894876776323, + -1239.6985783450068, + 835.4165891719979, + 687.4493307537022 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 5, + "title": "IMAGE", + "bounding": [ + 314.7837694552321, + -1892.3713006966948, + 1128.6676930763565, + 575.0895040708374 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 6, + "title": "LOOP_MODULE", + "bounding": [ + 2634.680788596277, + -535.0273371340219, + 1349.7319792372286, + 475.0123258172445 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 7, + "title": "PROMPT INDEX", + "bounding": [ + 2123.335438565198, + -521.5382952635489, + 455.95071568008416, + 441.0129923082427 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 8, + "title": "PROMPTS", + "bounding": [ + 1377.9706412468445, + -525.4029175469739, + 609.8490525406571, + 1313.6301264604044 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 9, + "title": "Group", + "bounding": [ + 3063.1870795871378, + -1914.894453367152, + 1257.5424882803222, + 1156.0549951068547 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 10, + "title": "HIGH - GGUF MODEL", + "bounding": [ + -1248.8688609733217, + -1250.5012180793872, + 290, + 141.6 + ], + "color": "#b58b2a", + "font_size": 24, + "flags": {} + }, + { + "id": 11, + "title": "HIGH DIFF MODEL", + "bounding": [ + -1251.2882522654688, + -995.7960497026663, + 290, + 165.6 + ], + "color": "#b58b2a", + "font_size": 24, + "flags": {} + }, + { + "id": 12, + "title": "LOW - GGUF MODEL", + "bounding": [ + -1247.3403049907386, + -710.1259029825222, + 290, + 141.6 + ], + "color": "#a1309b", + "font_size": 24, + "flags": {} + }, + { + "id": 13, + "title": "LOW - DIFF MODEL", + "bounding": [ + -1249.7596962828857, + -455.42073460580184, + 290, + 165.6 + ], + "color": "#a1309b", + "font_size": 24, + "flags": {} + }, + { + "id": 14, + "title": "MODELS A", + "bounding": [ + -1687.3790186204328, + -1294.101218079387, + 1733.823172707877, + 473.9051683767208 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 15, + "title": "MODELS B", + "bounding": [ + -1688.581175874653, + -753.7259029825221, + 1741.1095955679482, + 473.9051683767208 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "definitions": { + "subgraphs": [ + { + "id": "c2a1c2eb-20cf-4b0b-aa1a-ebed356dec34", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 186, + "lastLinkId": 395, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "VIDEO_EXTENSION_MODULE", + "inputNode": { + "id": -10, + "bounding": [ + 4115.81544227034, + -400.92473247969093, + 132.1328125, + 280 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 6400.849747062121, + -230.5874192045676, + 139.8203125, + 80 + ] + }, + "inputs": [ + { + "id": "8b4a7fa4-f3be-4c26-b0dd-88848c9ab797", + "name": "noise", + "type": "NOISE", + "linkIds": [ + 363, + 378 + ], + "localized_name": "noise", + "pos": [ + 4227.94825477034, + -380.92473247969093 + ] + }, + { + "id": "969b9107-4ce1-4202-9957-acd32444ba4c", + "name": "sampler", + "type": "SAMPLER", + "linkIds": [ + 370, + 379 + ], + "localized_name": "sampler", + "pos": [ + 4227.94825477034, + -360.92473247969093 + ] + }, + { + "id": "33e3cb1c-f033-4042-a118-733adb12f19d", + "name": "sigmas", + "type": "SIGMAS", + "linkIds": [ + 381 + ], + "localized_name": "sigmas", + "label": "sigmas_high", + "pos": [ + 4227.94825477034, + -340.92473247969093 + ] + }, + { + "id": "693f230c-fe4f-4a3f-a154-39f6f0aee5cf", + "name": "sigmas_1", + "type": "SIGMAS", + "linkIds": [ + 376 + ], + "localized_name": "sigmas_1", + "label": "sigmas_low", + "pos": [ + 4227.94825477034, + -320.92473247969093 + ] + }, + { + "id": "8a165693-1f38-4451-8a8f-626af3cbc814", + "name": "clip", + "type": "CLIP", + "linkIds": [ + 306, + 359 + ], + "localized_name": "clip", + "pos": [ + 4227.94825477034, + -300.92473247969093 + ] + }, + { + "id": "5f2cf9c8-9abe-4676-ab8a-06bacf33cea1", + "name": "model", + "type": "MODEL", + "linkIds": [ + 308 + ], + "localized_name": "model", + "label": "model_high", + "pos": [ + 4227.94825477034, + -280.92473247969093 + ] + }, + { + "id": "3fcc850b-b642-4e2e-b44f-8d524513470d", + "name": "model_1", + "type": "MODEL", + "linkIds": [ + 311 + ], + "localized_name": "model_1", + "label": "model_low", + "pos": [ + 4227.94825477034, + -260.92473247969093 + ] + }, + { + "id": "8c2de8a0-efbf-441b-b32d-313c571e81a5", + "name": "vae", + "type": "VAE", + "linkIds": [ + 317 + ], + "localized_name": "vae", + "pos": [ + 4227.94825477034, + -240.92473247969093 + ] + }, + { + "id": "61713356-47df-4089-a6f1-fa9e39a19ef3", + "name": "anchor_samples", + "type": "LATENT", + "linkIds": [ + 389 + ], + "localized_name": "anchor_samples", + "pos": [ + 4227.94825477034, + -220.92473247969093 + ] + }, + { + "id": "4f4ab288-e9b4-4fff-8501-6556415ea12d", + "name": "prev_samples", + "type": "LATENT", + "linkIds": [ + 390 + ], + "localized_name": "prev_samples", + "shape": 7, + "pos": [ + 4227.94825477034, + -200.92473247969093 + ] + }, + { + "id": "f4a82cb8-05fb-47fc-a580-a3fb3c32e99c", + "name": "source_images", + "type": "IMAGE", + "linkIds": [ + 319 + ], + "localized_name": "source_images", + "label": "prev_images", + "pos": [ + 4227.94825477034, + -180.92473247969093 + ] + }, + { + "id": "4e92d0f3-27fe-41cf-962c-bcce804c4c07", + "name": "text", + "type": "STRING", + "linkIds": [ + 386 + ], + "label": "text_prompt", + "pos": [ + 4227.94825477034, + -160.92473247969093 + ] + } + ], + "outputs": [ + { + "id": "ba13141d-868a-42a1-ac48-e51d5bd57c48", + "name": "output", + "type": "LATENT", + "linkIds": [ + 384 + ], + "label": "samples", + "pos": [ + 6420.849747062121, + -210.5874192045676 + ] + }, + { + "id": "c16acdef-88c8-49fd-a2dc-4aca876df35f", + "name": "extended_images", + "type": "IMAGE", + "linkIds": [ + 385 + ], + "pos": [ + 6420.849747062121, + -190.5874192045676 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 182, + "type": "CLIPTextEncode", + "pos": [ + 4557.246296160685, + 88.09894919868954 + ], + "size": [ + 400, + 200 + ], + "flags": { + "collapsed": true + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 359 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 388 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "CLIPTextEncode", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" + ], + "color": "#332922", + "bgcolor": "#593930" + }, + { + "id": 158, + "type": "ScheduledCFGGuidance", + "pos": [ + 4979.369448463057, + -577.2273949478679 + ], + "size": [ + 270.0875, + 146 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 308 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 391 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 392 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 380 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "79f529a84a8c20fe5dcdfa984c6be7a94102c014", + "Node name for S&R": "ScheduledCFGGuidance", + "aux_id": "kijai/ComfyUI-KJNodes", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1, + 0, + 1 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 159, + "type": "ScheduledCFGGuidance", + "pos": [ + 4892.021207244808, + -378.6041455925486 + ], + "size": [ + 270.0875, + 146 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 311 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 394 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 395 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 369 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "79f529a84a8c20fe5dcdfa984c6be7a94102c014", + "Node name for S&R": "ScheduledCFGGuidance", + "aux_id": "kijai/ComfyUI-KJNodes", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1, + 0, + 1 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 185, + "type": "SamplerCustomAdvanced", + "pos": [ + 5199.957283942452, + -362.71893743481655 + ], + "size": [ + 257.5849944250107, + 365.06318566771085 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 378 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 380 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 379 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 381 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 393 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "links": [ + 383 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "SamplerCustomAdvanced", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 184, + "type": "SamplerCustomAdvanced", + "pos": [ + 5492.177651225263, + -359.40395912698784 + ], + "size": [ + 289.26606114292565, + 397.26606114292565 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 368 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 369 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 370 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 376 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 383 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "links": [ + 373, + 384 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "SamplerCustomAdvanced", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 183, + "type": "DisableNoise", + "pos": [ + 5396.93830547369, + -449.90225361537813 + ], + "size": [ + 140, + 26 + ], + "flags": { + "collapsed": true + }, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 368 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "DisableNoise", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 168, + "type": "ImageBatchExtendWithOverlap", + "pos": [ + 5933.047625581195, + -167.66778635634307 + ], + "size": [ + 321.6236328125, + 146 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "source_images", + "name": "source_images", + "type": "IMAGE", + "link": 319 + }, + { + "localized_name": "new_images", + "name": "new_images", + "shape": 7, + "type": "IMAGE", + "link": 321 + } + ], + "outputs": [ + { + "localized_name": "source_images", + "name": "source_images", + "type": "IMAGE", + "links": null + }, + { + "localized_name": "start_images", + "name": "start_images", + "type": "IMAGE", + "links": null + }, + { + "localized_name": "extended_images", + "name": "extended_images", + "type": "IMAGE", + "links": [ + 385 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "79f529a84a8c20fe5dcdfa984c6be7a94102c014", + "Node name for S&R": "ImageBatchExtendWithOverlap", + "aux_id": "kijai/ComfyUI-KJNodes", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 5, + "source", + "linear_blend" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 162, + "type": "VAEDecode", + "pos": [ + 5894.565863157888, + -333.7746725024751 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 373 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 317 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 321 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.45", + "Node name for S&R": "VAEDecode", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.1", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 152, + "type": "CLIPTextEncode", + "pos": [ + 4545.284023464112, + 17.65102760535699 + ], + "size": [ + 479.7835317198833, + 88 + ], + "flags": { + "collapsed": true + }, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 306 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 386 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 387 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.45", + "Node name for S&R": "CLIPTextEncode", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.1", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 186, + "type": "IAMCCS_WanImageMotion", + "pos": [ + 4502.750755505038, + -414.8038352702417 + ], + "size": [ + 317.90234375, + 286 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 387 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 388 + }, + { + "localized_name": "anchor_samples", + "name": "anchor_samples", + "type": "LATENT", + "link": 389 + }, + { + "localized_name": "prev_samples", + "name": "prev_samples", + "shape": 7, + "type": "LATENT", + "link": 390 + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 391, + 394 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 392, + 395 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 393 + ] + } + ], + "properties": { + "aux_id": "IAMCCS/IAMCCS-nodes", + "ver": "87665e54a4b06a50fc631609f53d3bacdd10fc3e", + "Node name for S&R": "IAMCCS_WanImageMotion", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + 81, + 1, + 1, + "motion_only (prev_samples)", + false, + "auto", + "normal", + false + ], + "color": "#233", + "bgcolor": "#355" + } + ], + "groups": [], + "links": [ + { + "id": 321, + "origin_id": 162, + "origin_slot": 0, + "target_id": 168, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 306, + "origin_id": -10, + "origin_slot": 4, + "target_id": 152, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 308, + "origin_id": -10, + "origin_slot": 5, + "target_id": 158, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 311, + "origin_id": -10, + "origin_slot": 6, + "target_id": 159, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 317, + "origin_id": -10, + "origin_slot": 7, + "target_id": 162, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 319, + "origin_id": -10, + "origin_slot": 10, + "target_id": 168, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 359, + "origin_id": -10, + "origin_slot": 4, + "target_id": 182, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 363, + "origin_id": -10, + "origin_slot": 0, + "target_id": 150, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 368, + "origin_id": 183, + "origin_slot": 0, + "target_id": 184, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 369, + "origin_id": 159, + "origin_slot": 0, + "target_id": 184, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 370, + "origin_id": -10, + "origin_slot": 1, + "target_id": 184, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 373, + "origin_id": 184, + "origin_slot": 0, + "target_id": 162, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 376, + "origin_id": -10, + "origin_slot": 3, + "target_id": 184, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 378, + "origin_id": -10, + "origin_slot": 0, + "target_id": 185, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 379, + "origin_id": -10, + "origin_slot": 1, + "target_id": 185, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 380, + "origin_id": 158, + "origin_slot": 0, + "target_id": 185, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 381, + "origin_id": -10, + "origin_slot": 2, + "target_id": 185, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 383, + "origin_id": 185, + "origin_slot": 0, + "target_id": 184, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 384, + "origin_id": 184, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 385, + "origin_id": 168, + "origin_slot": 2, + "target_id": -20, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 386, + "origin_id": -10, + "origin_slot": 11, + "target_id": 152, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 387, + "origin_id": 152, + "origin_slot": 0, + "target_id": 186, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 388, + "origin_id": 182, + "origin_slot": 0, + "target_id": 186, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 389, + "origin_id": -10, + "origin_slot": 8, + "target_id": 186, + "target_slot": 2, + "type": "LATENT" + }, + { + "id": 390, + "origin_id": -10, + "origin_slot": 9, + "target_id": 186, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 391, + "origin_id": 186, + "origin_slot": 0, + "target_id": 158, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 392, + "origin_id": 186, + "origin_slot": 1, + "target_id": 158, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 393, + "origin_id": 186, + "origin_slot": 2, + "target_id": 185, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 394, + "origin_id": 186, + "origin_slot": 0, + "target_id": 159, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 395, + "origin_id": 186, + "origin_slot": 1, + "target_id": 159, + "target_slot": 2, + "type": "CONDITIONING" + } + ], + "extra": { + "workflowRendererVersion": "LG", + "ue_links": [], + "links_added_by_ue": [] + } + }, + { + "id": "08be7d9e-8766-4c78-98c9-50fc97389ca0", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 186, + "lastLinkId": 398, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Initial", + "inputNode": { + "id": -10, + "bounding": [ + 4115.81544227034, + -400.92473247969093, + 132.1328125, + 280 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 6095.627231727035, + -219.44839331354646, + 136.80859375, + 80 + ] + }, + "inputs": [ + { + "id": "8b4a7fa4-f3be-4c26-b0dd-88848c9ab797", + "name": "noise", + "type": "NOISE", + "linkIds": [ + 363, + 378 + ], + "localized_name": "noise", + "pos": [ + 4227.94825477034, + -380.92473247969093 + ] + }, + { + "id": "969b9107-4ce1-4202-9957-acd32444ba4c", + "name": "sampler", + "type": "SAMPLER", + "linkIds": [ + 370, + 379 + ], + "localized_name": "sampler", + "pos": [ + 4227.94825477034, + -360.92473247969093 + ] + }, + { + "id": "33e3cb1c-f033-4042-a118-733adb12f19d", + "name": "sigmas", + "type": "SIGMAS", + "linkIds": [ + 381 + ], + "localized_name": "sigmas", + "label": "sigmas_high", + "pos": [ + 4227.94825477034, + -340.92473247969093 + ] + }, + { + "id": "693f230c-fe4f-4a3f-a154-39f6f0aee5cf", + "name": "sigmas_1", + "type": "SIGMAS", + "linkIds": [ + 376 + ], + "localized_name": "sigmas_1", + "label": "sigmas_low", + "pos": [ + 4227.94825477034, + -320.92473247969093 + ] + }, + { + "id": "8a165693-1f38-4451-8a8f-626af3cbc814", + "name": "clip", + "type": "CLIP", + "linkIds": [ + 306, + 359 + ], + "localized_name": "clip", + "pos": [ + 4227.94825477034, + -300.92473247969093 + ] + }, + { + "id": "5f2cf9c8-9abe-4676-ab8a-06bacf33cea1", + "name": "model", + "type": "MODEL", + "linkIds": [ + 308 + ], + "localized_name": "model", + "label": "model_high", + "pos": [ + 4227.94825477034, + -280.92473247969093 + ] + }, + { + "id": "3fcc850b-b642-4e2e-b44f-8d524513470d", + "name": "model_1", + "type": "MODEL", + "linkIds": [ + 311 + ], + "localized_name": "model_1", + "label": "model_low", + "pos": [ + 4227.94825477034, + -260.92473247969093 + ] + }, + { + "id": "8c2de8a0-efbf-441b-b32d-313c571e81a5", + "name": "vae", + "type": "VAE", + "linkIds": [ + 317 + ], + "localized_name": "vae", + "pos": [ + 4227.94825477034, + -240.92473247969093 + ] + }, + { + "id": "61713356-47df-4089-a6f1-fa9e39a19ef3", + "name": "anchor_samples", + "type": "LATENT", + "linkIds": [ + 390 + ], + "localized_name": "anchor_samples", + "pos": [ + 4227.94825477034, + -220.92473247969093 + ] + }, + { + "id": "4f4ab288-e9b4-4fff-8501-6556415ea12d", + "name": "prev_samples", + "type": "LATENT", + "linkIds": [ + 391 + ], + "localized_name": "prev_samples", + "shape": 7, + "pos": [ + 4227.94825477034, + -200.92473247969093 + ] + }, + { + "id": "f4a82cb8-05fb-47fc-a580-a3fb3c32e99c", + "name": "source_images", + "type": "IMAGE", + "linkIds": [], + "localized_name": "source_images", + "label": "prev_images", + "pos": [ + 4227.94825477034, + -180.92473247969093 + ] + }, + { + "id": "9c7cacd9-109f-46d3-8644-536e43f8ab3e", + "name": "text", + "type": "STRING", + "linkIds": [ + 389 + ], + "label": "text_prompt", + "pos": [ + 4227.94825477034, + -160.92473247969093 + ] + } + ], + "outputs": [ + { + "id": "309763cc-bd21-44e1-83ab-0d95e80c8ec3", + "name": "output", + "type": "LATENT", + "linkIds": [ + 386 + ], + "label": "samples", + "pos": [ + 6115.627231727035, + -199.44839331354646 + ] + }, + { + "id": "274b2726-e09c-433d-b072-c23df311f391", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 387 + ], + "label": "decoded_images", + "pos": [ + 6115.627231727035, + -179.44839331354646 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 152, + "type": "CLIPTextEncode", + "pos": [ + 4384.025453293967, + -27.480377907595074 + ], + "size": [ + 510, + 160 + ], + "flags": { + "collapsed": true + }, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 306 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 389 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 394 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.45", + "Node name for S&R": "CLIPTextEncode", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.1", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 182, + "type": "CLIPTextEncode", + "pos": [ + 4368.148994310642, + 41.02177929190853 + ], + "size": [ + 400, + 200 + ], + "flags": { + "collapsed": true + }, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 359 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 395 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "CLIPTextEncode", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" + ], + "color": "#332922", + "bgcolor": "#593930" + }, + { + "id": 158, + "type": "ScheduledCFGGuidance", + "pos": [ + 5148.9073437359175, + -688.5339493273569 + ], + "size": [ + 270.0875, + 146 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 308 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 392 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 393 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 380 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "79f529a84a8c20fe5dcdfa984c6be7a94102c014", + "Node name for S&R": "ScheduledCFGGuidance", + "aux_id": "kijai/ComfyUI-KJNodes", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1, + 0, + 1 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 185, + "type": "SamplerCustomAdvanced", + "pos": [ + 4934.701702975717, + -293.2393678373789 + ], + "size": [ + 257.5849944250107, + 365.06318566771085 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 378 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 380 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 379 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 381 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 396 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "links": [ + 383 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "SamplerCustomAdvanced", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 183, + "type": "DisableNoise", + "pos": [ + 5208.455762821815, + -482.50762168567115 + ], + "size": [ + 140, + 26 + ], + "flags": { + "collapsed": true + }, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 368 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "DisableNoise", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 159, + "type": "ScheduledCFGGuidance", + "pos": [ + 5443.28682439448, + -550.9843234134333 + ], + "size": [ + 270.0875, + 146 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 311 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 397 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 398 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 369 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "79f529a84a8c20fe5dcdfa984c6be7a94102c014", + "Node name for S&R": "ScheduledCFGGuidance", + "aux_id": "kijai/ComfyUI-KJNodes", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1, + 0, + 1 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 162, + "type": "VAEDecode", + "pos": [ + 5685.934273792993, + -270.08803856524406 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 373 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 317 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 387 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.45", + "Node name for S&R": "VAEDecode", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.1", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 184, + "type": "SamplerCustomAdvanced", + "pos": [ + 5247.316600641524, + -320.48695240078024 + ], + "size": [ + 289.26606114292565, + 397.26606114292565 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 368 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 369 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 370 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 376 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 383 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "links": [ + 373, + 386 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "Node name for S&R": "SamplerCustomAdvanced", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 186, + "type": "IAMCCS_WanImageMotion", + "pos": [ + 4542.307651881915, + -702.5206901237116 + ], + "size": [ + 317.90234375, + 286 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 394 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 395 + }, + { + "localized_name": "anchor_samples", + "name": "anchor_samples", + "type": "LATENT", + "link": 390 + }, + { + "localized_name": "prev_samples", + "name": "prev_samples", + "shape": 7, + "type": "LATENT", + "link": 391 + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 392, + 397 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 393, + 398 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 396 + ] + } + ], + "properties": { + "aux_id": "IAMCCS/IAMCCS-nodes", + "ver": "87665e54a4b06a50fc631609f53d3bacdd10fc3e", + "Node name for S&R": "IAMCCS_WanImageMotion", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + } + }, + "widgets_values": [ + 81, + 0, + 1, + "motion_only (prev_samples)", + false, + "auto", + "normal", + false + ], + "color": "#233", + "bgcolor": "#355" + } + ], + "groups": [], + "links": [ + { + "id": 306, + "origin_id": -10, + "origin_slot": 4, + "target_id": 152, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 308, + "origin_id": -10, + "origin_slot": 5, + "target_id": 158, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 311, + "origin_id": -10, + "origin_slot": 6, + "target_id": 159, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 317, + "origin_id": -10, + "origin_slot": 7, + "target_id": 162, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 359, + "origin_id": -10, + "origin_slot": 4, + "target_id": 182, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 363, + "origin_id": -10, + "origin_slot": 0, + "target_id": 150, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 368, + "origin_id": 183, + "origin_slot": 0, + "target_id": 184, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 369, + "origin_id": 159, + "origin_slot": 0, + "target_id": 184, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 370, + "origin_id": -10, + "origin_slot": 1, + "target_id": 184, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 373, + "origin_id": 184, + "origin_slot": 0, + "target_id": 162, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 376, + "origin_id": -10, + "origin_slot": 3, + "target_id": 184, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 378, + "origin_id": -10, + "origin_slot": 0, + "target_id": 185, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 379, + "origin_id": -10, + "origin_slot": 1, + "target_id": 185, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 380, + "origin_id": 158, + "origin_slot": 0, + "target_id": 185, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 381, + "origin_id": -10, + "origin_slot": 2, + "target_id": 185, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 383, + "origin_id": 185, + "origin_slot": 0, + "target_id": 184, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 386, + "origin_id": 184, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 387, + "origin_id": 162, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 389, + "origin_id": -10, + "origin_slot": 11, + "target_id": 152, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 390, + "origin_id": -10, + "origin_slot": 8, + "target_id": 186, + "target_slot": 2, + "type": "LATENT" + }, + { + "id": 391, + "origin_id": -10, + "origin_slot": 9, + "target_id": 186, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 392, + "origin_id": 186, + "origin_slot": 0, + "target_id": 158, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 393, + "origin_id": 186, + "origin_slot": 1, + "target_id": 158, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 394, + "origin_id": 152, + "origin_slot": 0, + "target_id": 186, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 395, + "origin_id": 182, + "origin_slot": 0, + "target_id": 186, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 396, + "origin_id": 186, + "origin_slot": 2, + "target_id": 185, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 397, + "origin_id": 186, + "origin_slot": 0, + "target_id": 159, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 398, + "origin_id": 186, + "origin_slot": 1, + "target_id": 159, + "target_slot": 2, + "type": "CONDITIONING" + } + ], + "extra": { + "workflowRendererVersion": "LG", + "ue_links": [], + "links_added_by_ue": [] + } + } + ] + }, + "config": {}, + "extra": { + "ds": { + "scale": 0.35676040523396557, + "offset": [ + -452.99173270133963, + 1915.1583210546055 + ] + }, + "frontendVersion": "1.35.9", + "workflowRendererVersion": "LG", + "VHS_latentpreview": true, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true, + "ue_links": [], + "iamccs_annotations": { + "version": 2, + "enabled": false, + "color": "#ff4444", + "width": 7, + "opacity": 1, + "eraser": false, + "constantScreen": false, + "dashed": false, + "hidden": false, + "penOnly": false, + "hiDPIx2": false, + "selOverride": false, + "pinMode": false, + "tool": "draw", + "selectMode": "rect", + "transformMode": "fixed", + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1, + "textFontFamily": "Arial", + "textFontSize": 28, + "textColor": "#111111", + "textFontWeight": "normal", + "textFontStyle": "normal", + "textUnderline": false, + "paths": [], + "layers": [ + { + "name": "Layer 1", + "kind": "draw", + "visible": true, + "locked": false, + "paths": [], + "style": { + "color": "#ff4444", + "dashed": false, + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1 + } + } + ], + "currentLayerIdx": 0, + "stickers": [], + "stickerStyle": { + "frameColor": "#ffffff", + "paddingPx": 10, + "borderWidthPx": 2, + "shadow": true, + "shadowStrength": 12 + } + }, + "iamccs_annotations_multi": { + "version": 2, + "graphs": { + "ec7da562-7e21-4dac-a0d2-f4441e1efd3b": { + "version": 2, + "enabled": false, + "color": "#ff4444", + "width": 7, + "opacity": 1, + "eraser": false, + "constantScreen": false, + "dashed": false, + "hidden": false, + "penOnly": false, + "hiDPIx2": false, + "selOverride": false, + "pinMode": false, + "tool": "draw", + "selectMode": "rect", + "transformMode": "fixed", + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1, + "textFontFamily": "Arial", + "textFontSize": 28, + "textColor": "#111111", + "textFontWeight": "normal", + "textFontStyle": "normal", + "textUnderline": false, + "paths": [], + "layers": [ + { + "name": "Layer 1", + "kind": "draw", + "visible": true, + "locked": false, + "paths": [], + "style": { + "color": "#ff4444", + "dashed": false, + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1 + } + } + ], + "currentLayerIdx": 0, + "stickers": [], + "stickerStyle": { + "frameColor": "#ffffff", + "paddingPx": 10, + "borderWidthPx": 2, + "shadow": true, + "shadowStrength": 12 + } + }, + "c2a1c2eb-20cf-4b0b-aa1a-ebed356dec34": { + "version": 2, + "enabled": false, + "color": "#ff4444", + "width": 7, + "opacity": 1, + "eraser": false, + "constantScreen": false, + "dashed": false, + "hidden": false, + "penOnly": false, + "hiDPIx2": false, + "selOverride": false, + "pinMode": false, + "tool": "draw", + "selectMode": "rect", + "transformMode": "fixed", + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1, + "textFontFamily": "Arial", + "textFontSize": 28, + "textColor": "#111111", + "textFontWeight": "normal", + "textFontStyle": "normal", + "textUnderline": false, + "paths": [], + "layers": [ + { + "name": "Layer 1", + "kind": "draw", + "visible": true, + "locked": false, + "paths": [], + "style": { + "color": "#ff4444", + "dashed": false, + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1 + } + } + ], + "currentLayerIdx": 0, + "stickers": [], + "stickerStyle": { + "frameColor": "#ffffff", + "paddingPx": 10, + "borderWidthPx": 2, + "shadow": true, + "shadowStrength": 12 + } + }, + "08be7d9e-8766-4c78-98c9-50fc97389ca0": { + "version": 2, + "enabled": false, + "color": "#ff4444", + "width": 7, + "opacity": 1, + "eraser": false, + "constantScreen": false, + "dashed": false, + "hidden": false, + "penOnly": false, + "hiDPIx2": false, + "selOverride": false, + "pinMode": false, + "tool": "draw", + "selectMode": "rect", + "transformMode": "fixed", + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1, + "textFontFamily": "Arial", + "textFontSize": 28, + "textColor": "#111111", + "textFontWeight": "normal", + "textFontStyle": "normal", + "textUnderline": false, + "paths": [], + "layers": [ + { + "name": "Layer 1", + "kind": "draw", + "visible": true, + "locked": false, + "paths": [], + "style": { + "color": "#ff4444", + "dashed": false, + "widthDraw": 7, + "widthErase": 48, + "opacityDraw": 1, + "opacityErase": 1 + } + } + ], + "currentLayerIdx": 0, + "stickers": [], + "stickerStyle": { + "frameColor": "#ffffff", + "paddingPx": 10, + "borderWidthPx": 2, + "shadow": true, + "shadowStrength": 12 + } + } + } + }, + "links_added_by_ue": [] + }, + "version": 0.4 +} \ No newline at end of file diff --git a/README.md b/README.md index 214b1fc..6ed25c8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,59 @@ ### Category: ComfyUI Custom Nodes ### Main Feature: Fix for LoRA loading in native WANAnimate workflows -Version: 1.3.2 +Version: 1.3.3 + +# UPDATE VERSION 1-3-3 + +## 🆕 Version 1.3.3 — AutoLink + LTX-2 Extension Module + +Date: 2026-01-26 + +Highlights (EN): +- AutoLink (frontend): convert direct links into compact Set/Get nodes + restore when needed. + +![[Node piece](assets/autolink.png)](https://github.com/IAMCCS/IAMCCS-nodes/blob/main/assets/autolink.png) + +- LTX-2: Extension Module + helpers for iterative long video extension workflows. + +![[Node piece](assets/extension.png)](https://github.com/IAMCCS/IAMCCS-nodes/blob/main/assets/extension.png) + +Docs: + +- LTX-2 Extension Module (EN/IT): `LTX2_EXTENSION_MODULE_README.md` +- LTX-2 Nodes Guide: `LTX2_EXTENSION_NODES_GUIDE_EN.md` + +GGUF / OOM tips: +- If you use `IAMCCS_GGUF_accelerator` and you are close to the VRAM limit, consider PyTorch allocator tuning to reduce fragmentation (must be set **before** launching ComfyUI). + - Example: `PYTORCH_ALLOC_CONF=backend:cudaMallocAsync` + - Example (native allocator): `PYTORCH_ALLOC_CONF=max_split_size_mb:128,garbage_collection_threshold:0.8` + - Example (experimental, native allocator): `PYTORCH_ALLOC_CONF=expandable_segments:True` + +### IAMCCS_GGUF_accelerator (how to use) + +![[Node piece](assets/gguf.png)](https://github.com/IAMCCS/IAMCCS-nodes/blob/main/assets/gguf.png) + +This node modifies a GGUF `MODEL` so ComfyUI-GGUF can avoid expensive per-step CPU↔GPU patch movement. + +Recommended usage: +- Place it **after** your GGUF model loader and **before** LoRA application / sampling. +- Default: `mode = auto_oom_safe`. + - If free VRAM is low, it automatically disables `patch_on_device` and avoids pre-moving patches. + - If a CUDA OOM happens while moving patches, it falls back to CPU/offload (when `oom_fallback = true`). + +Suggested starting values on 12GB GPUs: +- `mode = auto_oom_safe` +- `min_free_vram_mb = 1500` (raise to 2000–3000 if you still get OOMs) +- Keep `move_patches_now = true` only if you have headroom; set to `false` if you want the safest VRAM behavior. + +PyTorch allocator tuning (set before start): +- You can use `PYTORCH_ALLOC_CONF` (or the legacy alias `PYTORCH_CUDA_ALLOC_CONF`) to reduce fragmentation. +- Windows example (PowerShell, current session): + - `$env:PYTORCH_ALLOC_CONF = "backend:cudaMallocAsync"` +- Windows example (CMD / .bat): + - `set PYTORCH_ALLOC_CONF=backend:cudaMallocAsync` + +--- # UPDATE VERSION 1-3-2 @@ -48,7 +100,7 @@ Highlights: ![[Node piece](assets/wanmotion.png)](https://github.com/IAMCCS/IAMCCS-nodes/blob/main/assets/wanmotion.png) Highlights: -- Added `IAMCCS WanImageMotion` node: drop-in replacement for KJNodes `WanImageToVideoSVIPro` with motion amplitude control to fix slow-motion issues in WAN SVI Pro workflows. +- Added `IAMCCS WanImageMotion` node: drop-in replacement for common WAN SVI Pro image-to-video nodes, with motion amplitude control to fix slow-motion issues in WAN SVI Pro workflows. - Motion modes: apply boost to `prev_samples` only or all non-first latents. - VRAM profiles: normal / chunked / per-frame loop / CPU offload for memory-constrained systems. - `include_padding_in_motion` toggle: enables motion boost on padded frames when anchor has single frame (T=1). diff --git a/__init__.py b/__init__.py index ddd529f..f6a1c1f 100644 --- a/__init__.py +++ b/__init__.py @@ -2,6 +2,9 @@ # __init__.py — Registro nodi IAMCCS # ========================================================== +import logging +import os + from .iamccs_wan_lora_stack import ( IAMCCS_WanLoRAStack, IAMCCS_ModelWithLoRA, @@ -18,17 +21,45 @@ from .iamccs_ltx2_lora_stack import ( 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_ControlPreprocess, + IAMCCS_LTX2_ImageBatchPadReflect, + IAMCCS_LTX2_ImageBatchCropByPad, +) + +from .iamccs_ltx2_extension_module import ( + IAMCCS_LTX2_ExtensionModule, + IAMCCS_LTX2_ExtensionModule_simple, + IAMCCS_LTX2_GetImageFromBatch, + IAMCCS_LTX2_ReferenceImageSwitch, + IAMCCS_LTX2_ReferenceStartFramesInjector, + IAMCCS_LTX2_FrameCountValidator, ) from .iamccs_wan_svipro_motion import ( IAMCCS_WanImageMotion, ) +from .iamccs_autolink import ( + IAMCCS_SetAutoLink, + IAMCCS_GetAutoLink, + IAMCCS_AutoLinkConverter, + IAMCCS_AutoLinkArguments, +) + +from .iamccs_gguf_accelerator import ( + IAMCCS_GGUF_accelerator, +) + # Nodi principali NODE_CLASS_MAPPINGS = { "IAMCCS_WanLoRAStack": IAMCCS_WanLoRAStack, @@ -42,12 +73,30 @@ NODE_CLASS_MAPPINGS = { "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_ControlPreprocess": IAMCCS_LTX2_ControlPreprocess, + "IAMCCS_LTX2_ImageBatchPadReflect": IAMCCS_LTX2_ImageBatchPadReflect, + "IAMCCS_LTX2_ImageBatchCropByPad": IAMCCS_LTX2_ImageBatchCropByPad, + "IAMCCS_LTX2_ExtensionModule": IAMCCS_LTX2_ExtensionModule, + "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_WanImageMotion": IAMCCS_WanImageMotion, + + "IAMCCS_SetAutoLink": IAMCCS_SetAutoLink, + "IAMCCS_GetAutoLink": IAMCCS_GetAutoLink, + "IAMCCS_AutoLinkConverter": IAMCCS_AutoLinkConverter, + "IAMCCS_AutoLinkArguments": IAMCCS_AutoLinkArguments, + + "IAMCCS_GGUF_accelerator": IAMCCS_GGUF_accelerator, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -60,15 +109,177 @@ NODE_DISPLAY_NAME_MAPPINGS = { "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 (16px, 8n +1)", "IAMCCS_LTX2_TimeFrameCount": "LTX-2 TimeFrameCount", + "IAMCCS_LTX2_EnsureFrames8nPlus1": "LTX-2 Ensure Frames (8n + 1)", "IAMCCS_LTX2_ControlPreprocess": "LTX-2 Control Preprocess (aux)", - "IAMCCS_WanImageMotion": "IAMCCS WanImageMotion", + "IAMCCS_LTX2_ImageBatchPadReflect": "LTX-2 Pad Reflect (IMAGE batch)", + "IAMCCS_LTX2_ImageBatchCropByPad": "LTX-2 Crop By Pad (IMAGE batch)", + "IAMCCS_LTX2_ExtensionModule": "LTX-2 Extension Module 🎬", + "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_WanImageMotion": "WanImageMotion", + + "IAMCCS_SetAutoLink": "Set AutoLink", + "IAMCCS_GetAutoLink": "Get AutoLink", + "IAMCCS_AutoLinkConverter": "AutoLink Converter", + "IAMCCS_AutoLinkArguments": "AutoLink Arguments", + + "IAMCCS_GGUF_accelerator": "GGUF Accelerator (patch_on_device)", } -# Web directory for JavaScript extensions WEB_DIRECTORY = "./web" __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] + + +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() diff --git a/assets/autolink.png b/assets/autolink.png new file mode 100644 index 0000000..a1b61ef Binary files /dev/null and b/assets/autolink.png differ diff --git a/assets/extension.png b/assets/extension.png new file mode 100644 index 0000000..7965fe6 Binary files /dev/null and b/assets/extension.png differ diff --git a/assets/gguf.png b/assets/gguf.png new file mode 100644 index 0000000..ef0454c Binary files /dev/null and b/assets/gguf.png differ diff --git a/docs/LTX2_EXTENSION_MODULE_COMPLETE_GUIDE.md b/docs/LTX2_EXTENSION_MODULE_COMPLETE_GUIDE.md new file mode 100644 index 0000000..6ec34cc --- /dev/null +++ b/docs/LTX2_EXTENSION_MODULE_COMPLETE_GUIDE.md @@ -0,0 +1,851 @@ +# LTX-2 Extension Module - Complete Technical Guide + +## Table of Contents +1. [Overview](#overview) +2. [Architecture & Workflow](#architecture--workflow) +3. [Parameters Reference](#parameters-reference) +4. [Usage Scenarios](#usage-scenarios) +5. [Advanced Features](#advanced-features) +6. [Troubleshooting](#troubleshooting) +7. [Best Practices](#best-practices) + +--- + +## Overview + +The **IAMCCS LTX-2 Extension Module** is an all-in-one node designed for iterative video extension workflows with the LTX-2 model. It combines multiple operations into a single, efficient node: + +- **Image batch merging** with configurable overlap +- **Multiple blending modes** for smooth transitions +- **Automatic frame calculations** with built-in math operations +- **LTX-2 8n+1 conformance** for start_images +- **Advanced quality features** (color matching, seam search) + +### Key Benefits +- ✅ Eliminates need for multiple separate nodes (GetImageRange, ImageBatchExtend, SimpleMath, etc.) +- ✅ Automatic 8n+1 validation prevents encoding errors +- ✅ Seamless video segment concatenation with no visible cuts +- ✅ Flexible overlap strategies for different content types +- ✅ Built-in quality enhancement features + +--- + +## Architecture & Workflow + +### Basic Extension Flow + +```mermaid +graph TB + A[Generation 1
121 frames] --> B[Extension Module] + C[Generation 2
121 frames] --> B + B --> D[extended_images
217 frames] + B --> E[start_images
17 frames 8n+1] + E --> F[Next Generation Input] + + style B fill:#2a363b,stroke:#3f5159,color:#fff + style E fill:#233,stroke:#355,color:#fff +``` + +### Complete Multi-Segment Workflow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ITERATIVE EXTENSION LOOP │ +└─────────────────────────────────────────────────────────────────┘ + +Iteration 1: Initial Generation +┌──────────────────┐ +│ Initial Image │ 1 frame +└────────┬─────────┘ + │ + v +┌──────────────────┐ +│ LTX Sampler │ Generate 121 frames +│ (8×15 + 1) │ +└────────┬─────────┘ + │ + v +┌──────────────────┐ +│ VAE Decode │ Latent → Images +└────────┬─────────┘ + │ + v + source_images (121 frames) + │ + └──────────────────────────────────┐ + │ +Iteration 2: First Extension │ +┌──────────────────┐ │ +│ Extension │◄───────────────────────┘ +│ Module │◄── new_images (121 frames from Gen 2) +│ overlap=25 │ +│ mode=linear │ +└────────┬─────────┘ + │ + ├──► extended_images (217 frames) + │ 121 - 25 + 121 = 217 + │ + └──► start_images (17 frames) + 25 → 24 (math: a-1) → 17 (8n+1 conform) + │ + v + ┌──────────────────┐ + │ LTX Sampler │ Gen 3 (121 frames) + │ uses 17 frames │ + └────────┬─────────┘ + │ + v + new_images + │ + └──► Loop continues... + +Final Output: +┌──────────────────┐ +│ Video Segments │ +│ 217 + 217 + ... │ +│ Seamless Concat │ +└──────────────────┘ +``` + +### Internal Processing Flow + +``` +INPUT IMAGES + │ + ├─── source_images (previous generation) + │ │ + │ └─── Last 25 frames ──┐ + │ │ + └─── new_images (current generation) + │ │ + └─── First 25 frames ───┤ + │ + ┌────────v────────┐ + │ OVERLAP ZONE │ + │ 25 frames │ + └────────┬────────┘ + │ + ┌────────v────────┐ + │ BLENDING │ + │ linear_blend │ + │ Alpha: 0→1 │ + └────────┬────────┘ + │ + ┌──────────────────┴──────────────────┐ + │ │ + ┌─────────v─────────┐ ┌──────────v──────────┐ + │ extended_images │ │ start_images │ + │ Full merged batch │ │ For next iteration │ + │ (source-25+new) │ │ With 8n+1 conform │ + └────────────────────┘ └─────────────────────┘ +``` + +--- + +## Parameters Reference + +### Core Parameters + +#### `overlap_frames` (INT) +- **Default**: 10 +- **Range**: 1-256 +- **Recommended**: 25-40 for smooth transitions +- **Purpose**: Number of frames to overlap and blend between segments + +**Impact**: +- **Low (8-15)**: Fast processing, visible seams possible +- **Medium (20-30)**: ✅ **Recommended** - Good balance +- **High (40-60)**: Very smooth, but higher computational cost + +**Formula**: `extended_length = source_count - overlap + new_count` + +Example with overlap=25: +``` +source: [1...121] +new: [1...121] +overlap: 25 frames +extended: 121 - 25 + 121 = 217 frames +``` + +--- + +#### `overlap_side` (DROPDOWN) +- **Options**: `source` | `new_images` +- **Default**: `source` +- **Purpose**: Which batch to take overlap frames from + +``` +overlap_side = "source": + Take last 25 from source + Take first 25 from new + Blend source→new (recommended) + +overlap_side = "new_images": + Take first 25 from new + Take last 25 from source + Blend new→source (reverse) +``` + +**Use Cases**: +- `source`: ✅ **Standard** - Smooth forward progression +- `new_images`: Experimental - reverse blending effect + +--- + +#### `overlap_mode` (DROPDOWN) +- **Options**: `cut` | `linear_blend` | `ease_in_out` | `filmic_crossfade` | `perceptual_crossfade` +- **Default**: `linear_blend` + +### Blending Modes Comparison + +| Mode | Speed | Quality | Use Case | Formula | +|------|-------|---------|----------|---------| +| **cut** | ⚡⚡⚡ | ⭐ | Testing, no blend needed | Direct concatenation | +| **linear_blend** | ⚡⚡ | ⭐⭐⭐⭐ | ✅ **General use** | `(1-t)×src + t×dst` | +| **ease_in_out** | ⚡⚡ | ⭐⭐⭐⭐⭐ | Smooth artistic transitions | `3t² - 2t³` | +| **filmic_crossfade** | ⚡ | ⭐⭐⭐⭐⭐ | Color-accurate blending | Gamma 2.2 correction | +| **perceptual_crossfade** | ⚡ | ⭐⭐⭐⭐⭐ | Best quality (needs Kornia) | LAB color space blend | + +**Visual Comparison**: +``` +Alpha progression over 25 frames: + +linear_blend: +0.0 ████░░░░░░░░░░░░░░░░░░░░ 1.0 + │ │ + Linear interpolation + +ease_in_out: +0.0 ██▓▓▒▒░░░░░░░░░░▒▒▓▓████ 1.0 + │ Slow→Fast→Slow │ + Smooth S-curve + +filmic_crossfade: +0.0 ███▓▓▒▒░░░░░░░░░░▒▓▓███ 1.0 + │ Gamma-corrected │ + Perceptually uniform +``` + +**Recommendations**: +- **General video**: `linear_blend` (fast, reliable) +- **High quality**: `ease_in_out` (smooth, cinematic) +- **Color-critical**: `filmic_crossfade` or `perceptual_crossfade` +- **Testing/Debug**: `cut` (no blending overhead) + +--- + +#### `enable_math` (BOOLEAN) +- **Default**: `true` +- **Purpose**: Enable mathematical operations on overlap value for start_images calculation + +When enabled, applies `math_operation` to calculate the number of frames for `start_images`. + +--- + +#### `math_operation` (DROPDOWN) +- **Options**: `none` | `a-b` | `a-1` | `a+b` | `a*b` | `a/b` | `min(a,b)` | `max(a,b)` +- **Default**: `a-b` +- **Variables**: + - `a` = overlap_frames + - `b` = math_value_b (optional input) + +**Common Use Cases**: + +| Operation | Example | Result | Use Case | +|-----------|---------|--------|----------| +| `none` | overlap=25 | 25 | Direct use of overlap | +| `a-1` | 25-1 | 24 | ✅ **Standard** - LTX-2 workflow | +| `a-b` | 25-15 | 10 | Custom frame count | +| `a/b` | 25/2.5 | 10 | Proportional reduction | + +**Recommended Configuration**: +```json +{ + "overlap_frames": 25, + "enable_math": true, + "math_operation": "a-1" +} +``` +Result: 25 - 1 = 24 frames → 17 frames (after 8n+1 conform) + +--- + +#### `start_frames_rule` (DROPDOWN) +- **Options**: `none` | `ltx2_round_down` | `ltx2_nearest` +- **Default**: `none` +- **Purpose**: Enforce LTX-2 8n+1 rule for VideoVAE encoding + +### LTX-2 Frame Count Rule + +LTX-2 VideoVAE requires frame counts following the formula: **`frames = 8n + 1`** + +Valid frame counts: `1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, 97, 105, 113, 121...` + +**Examples**: + +| Input | ltx2_round_down | ltx2_nearest | none | +|-------|----------------|--------------|------| +| 24 | 17 (8×2+1) | 17 (closer) | 24 ❌ | +| 26 | 25 (8×3+1) | 25 (closer) | 26 ❌ | +| 30 | 25 (8×3+1) | 33 (closer) | 30 ❌ | +| 17 | 17 ✅ | 17 ✅ | 17 ✅ | + +**When to Use**: +- ✅ **Always use** `ltx2_round_down` or `ltx2_nearest` when start_images feeds into a sampler +- ❌ **Never use** when output is only for preview/saving (not encoding) + +**Critical**: Without this, you'll get errors like: +``` +Error: Expected frame count 8n+1, got 24 +``` + +--- + +### Advanced Quality Parameters + +#### `color_match_mode` (DROPDOWN) +- **Options**: `none` | `luma_only` | `per_channel` +- **Default**: `none` +- **Purpose**: Match color/exposure of new_images to source_images tail + +**Use Cases**: +- **Lighting changes**: Different segments with varying brightness +- **Color shifts**: Camera auto-balance between shots +- **Consistency**: Maintain uniform look across segments + +``` +none: + source: █████████▓▓▓▓▓ (bright end) + new: ▒▒▒▒▒░░░░░░░░ (dark start) + → Visible seam + +luma_only: + Match overall brightness only + → Quick, preserves color tone + +per_channel: + Match R, G, B independently + → Best quality, may shift colors +``` + +--- + +#### `color_match_strength` (FLOAT) +- **Range**: 0.0-1.0 +- **Default**: 1.0 +- **Purpose**: Blend factor for color matching + +``` +strength = 0.0: No correction +strength = 0.5: Partial correction +strength = 1.0: Full correction +``` + +--- + +#### `seam_search_mode` (DROPDOWN) +- **Options**: `none` | `best_of_k` +- **Default**: `none` +- **Purpose**: Search for optimal seam position within overlap zone + +**How It Works**: +``` +Standard overlap (offset=0): +source: ████████████████████▓▓▓▓▓ +new: ░░░░░░░░░░░░░░░░░ + ↑ Potential seam + +Best-of-k search (k=8): +Tries offsets 0-8: +offset=0: ▓▓▓▓▓ vs ░░░░░ → score: 0.85 +offset=1: ▓▓▓▓▓ vs ░░░░░ → score: 0.72 +offset=2: ▓▓▓▓▓ vs ░░░░░ → score: 0.65 ✅ Best! +... +Chooses offset=2 (lowest discontinuity) +``` + +**Scoring Metrics**: +- Color/luma continuity (weighted by `metric_weight_color`) +- Edge continuity (weighted by `metric_weight_edges`) + +**Trade-offs**: +- ✅ Reduces visible seams +- ✅ Handles motion/camera cuts better +- ❌ Slower (tests k candidates) +- ❌ May "skip" frames from new_images + +--- + +## Usage Scenarios + +### Scenario 1: Standard Video Extension (Recommended) + +**Goal**: Extend a video smoothly without visible seams + +**Configuration**: +```json +{ + "overlap_frames": 25, + "overlap_side": "source", + "overlap_mode": "linear_blend", + "enable_math": true, + "math_operation": "a-1", + "start_frames_rule": "ltx2_round_down", + "color_match_mode": "none", + "seam_search_mode": "none" +} +``` + +**Workflow**: +1. Generate segment 1 (121 frames) +2. Extract last 17 frames (8×2+1) +3. Generate segment 2 with those 17 frames as reference +4. Extension Module merges with 25-frame overlap +5. Repeat + +**Output**: Seamless 217-frame video (then 313, 409, etc.) + +--- + +### Scenario 2: High-Quality Cinematic Extension + +**Goal**: Maximum quality with perceptual blending + +**Configuration**: +```json +{ + "overlap_frames": 40, + "overlap_side": "source", + "overlap_mode": "perceptual_crossfade", + "enable_math": true, + "math_operation": "a-1", + "start_frames_rule": "ltx2_nearest", + "color_match_mode": "per_channel", + "color_match_strength": 0.8, + "seam_search_mode": "best_of_k", + "k_search": 16 +} +``` + +**Best For**: +- Film production +- High-resolution output +- Color-critical content +- Complex lighting scenarios + +--- + +### Scenario 3: Fast Preview / Testing + +**Goal**: Quick iteration, minimal processing + +**Configuration**: +```json +{ + "overlap_frames": 10, + "overlap_side": "source", + "overlap_mode": "cut", + "enable_math": true, + "math_operation": "a-1", + "start_frames_rule": "ltx2_round_down", + "color_match_mode": "none", + "seam_search_mode": "none" +} +``` + +**Best For**: +- Testing prompts +- Workflow debugging +- Quick previews + +--- + +### Scenario 4: Lighting-Corrected Extension + +**Goal**: Handle varying lighting between segments + +**Configuration**: +```json +{ + "overlap_frames": 30, + "overlap_side": "source", + "overlap_mode": "ease_in_out", + "enable_math": true, + "math_operation": "a-1", + "start_frames_rule": "ltx2_round_down", + "color_match_mode": "luma_only", + "color_match_strength": 1.0, + "color_reference_window": 12 +} +``` + +**Best For**: +- Outdoor scenes (sun changes) +- Mixed lighting conditions +- Auto-exposure variations + +--- + +## Advanced Features + +### Two-Stage Overlap Strategy + +Replicating the "early version" workflow behavior with separate overlap values: + +```python +# Early version used: +# - overlap=10 for frame extraction +# - overlap=25 for blending + +# Extension Module equivalent: +{ + "overlap_frames": 25, # For blending + "math_operation": "a/b", # Calculate extraction + "math_value_b": 2.5, # 25/2.5 = 10 + "start_frames_rule": "ltx2_round_down" +} + +# Result: +# - Blending uses 25 frames (smooth) +# - start_images calculated from 10 → 9 → 9 frames (8×1+1) +``` + +--- + +### Custom Frame Count Calculation + +**Example**: Generate 33 frames for next iteration (8×4+1) + +```json +{ + "overlap_frames": 25, + "math_operation": "a+b", + "math_value_b": 9, // 25 + 9 = 34 + "start_frames_rule": "ltx2_round_down" // 34 → 33 +} +``` + +--- + +### Adaptive Overlap with AutoLink + +When using AutoLink for iterative loops: + +```json +{ + "overlap_frames": 25, + "autolink_overlap_in": 0, // Override if > 0 from AutoLink + // ... other params ... +} + +// Extension Module outputs: +// autolink_overlap_out → feeds next iteration's autolink_overlap_in +``` + +--- + +## Troubleshooting + +### Problem: Visible seams between segments + +**Symptoms**: Hard cuts, color shifts, motion jumps + +**Solutions**: +1. ✅ Increase `overlap_frames` to 25-40 +2. ✅ Change to `ease_in_out` or `filmic_crossfade` +3. ✅ Enable `color_match_mode = "luma_only"` +4. ✅ Try `seam_search_mode = "best_of_k"` with `k_search = 8` + +--- + +### Problem: Error "Expected 8n+1 frames" + +**Symptoms**: Workflow fails at sampler/encoder + +**Solutions**: +1. ✅ Set `start_frames_rule = "ltx2_round_down"` +2. ✅ Verify `enable_math = true` +3. ✅ Check math formula produces reasonable values +4. ❌ Don't use `start_frames_rule` if output is for preview only + +--- + +### Problem: Videos too long / memory issues + +**Symptoms**: Out of memory, slow processing + +**Solutions**: +1. ✅ Reduce `overlap_frames` to 15-20 +2. ✅ Use `overlap_mode = "linear_blend"` (faster) +3. ✅ Disable `seam_search_mode` +4. ✅ Process in smaller batches + +--- + +### Problem: Color mismatch at seams + +**Symptoms**: Brightness/hue shifts visible + +**Solutions**: +1. ✅ Enable `color_match_mode = "per_channel"` +2. ✅ Set `color_match_strength = 0.8-1.0` +3. ✅ Increase `color_reference_window` to 16-24 +4. ✅ Use `filmic_crossfade` for gamma-correct blending + +--- + +## Best Practices + +### 1. Start with Recommended Defaults + +```json +{ + "overlap_frames": 25, + "overlap_side": "source", + "overlap_mode": "linear_blend", + "enable_math": true, + "math_operation": "a-1", + "start_frames_rule": "ltx2_round_down", + "color_match_mode": "none", + "seam_search_mode": "none" +} +``` + +Then optimize based on your specific needs. + +--- + +### 2. Overlap Guidelines by Content Type + +| Content Type | Overlap | Blend Mode | Reason | +|--------------|---------|------------|--------| +| **Static scenes** | 15-20 | linear_blend | Less motion, simpler blend | +| **Camera movement** | 25-40 | ease_in_out | Smooth motion transition | +| **Fast action** | 30-50 | filmic_crossfade | Avoid motion artifacts | +| **Talking heads** | 20-30 | linear_blend | Consistent framing | +| **Nature/landscape** | 25-35 | perceptual_crossfade | Color accuracy | + +--- + +### 3. Processing Order + +Always follow this order in your workflow: + +``` +1. Initial Image + ↓ +2. LTX Sampler (8n+1 frames) + ↓ +3. VAE Decode + ↓ +4. Extension Module + ├─→ extended_images (for final output) + └─→ start_images (for next iteration) + ↓ +5. Loop back to step 2 +``` + +**Critical**: Never feed `extended_images` back into the sampler directly - always use `start_images` (conformant to 8n+1). + +--- + +### 4. Testing Workflow + +Before full production: + +1. Test with `overlap=10`, `mode=cut` (fast preview) +2. Verify no errors with `start_frames_rule = "ltx2_round_down"` +3. Increase overlap to 25, switch to `linear_blend` +4. Fine-tune with quality features if needed + +--- + +### 5. Output Validation + +Check the `report` output for each iteration: + +``` +Source: 121 frames | +Overlap (effective): 25 frames | +Start range: start_index=96, num_frames=17 | +Math: a-1 | +Start frames rule: ltx2_round_down | +Extended: 217 frames | +Extension delta: +96 frames | +Blend mode: linear_blend +``` + +Verify: +- ✅ `num_frames` is 8n+1 (9, 17, 25, 33, etc.) +- ✅ `Extension delta` is positive +- ✅ No warnings in console + +--- + +## Performance Optimization + +### Memory Usage + +| Configuration | Memory Impact | Speed | +|---------------|---------------|-------| +| overlap=10, cut | Low | ⚡⚡⚡ | +| overlap=25, linear | Medium | ⚡⚡ | +| overlap=40, ease_in_out | Medium-High | ⚡⚡ | +| overlap=40, perceptual + seam search | High | ⚡ | + +--- + +### Batch Processing Tips + +For very long videos (10+ segments): + +1. **Save intermediate results**: + ``` + Segment 1 → Save + Segment 2 → Save + ... + Final concatenation separately + ``` + +2. **Use progressive overlap**: + ``` + Segments 1-3: overlap=25 (quality) + Segments 4+: overlap=15 (speed) + ``` + +3. **Monitor VRAM**: + - Each 121-frame batch ≈ 4-8GB VRAM + - Reduce resolution if needed + +--- + +## Workflow Diagrams + +### Complete Extension Pipeline + +``` +┌────────────────────────────────────────────────────────────────┐ +│ INITIALIZATION │ +└────────────────────────────────────────────────────────────────┘ + +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Load Model │────>│ Load VAE │────>│ Load CLIP │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + └───────────────────┴───────────────────┘ + │ + v +┌────────────────────────────────────────────────────────────────┐ +│ GENERATION LOOP START │ +└────────────────────────────────────────────────────────────────┘ + +Iteration N: +┌─────────────┐ +│ start_images│ (17 frames, 8×2+1) +│ from prev │ +└──────┬──────┘ + │ + v +┌─────────────────────────────────────────────────────────────┐ +│ SUBGRAPH: Samplers │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ VAE Encode │────>│ LTX Sampler │────>│ VAE Decode │ │ +│ │ (to latent) │ │ (121 frames)│ │ (to images) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + v + new_images (121 frames) + │ + └──────────────────────────┐ + │ +┌─────────────────────────────────v──────────────────────────┐ +│ Extension Module │ +│ │ +│ source_images (121) + new_images (121) │ +│ │ │ +│ v │ +│ ┌────────────────────────┐ │ +│ │ Overlap Extraction │ │ +│ │ Last 25 from source │ │ +│ │ First 25 from new │ │ +│ └────────┬───────────────┘ │ +│ │ │ +│ v │ +│ ┌────────────────────────┐ │ +│ │ Blending │ │ +│ │ Mode: linear_blend │ │ +│ │ Alpha: 0→1 over 25 │ │ +│ └────────┬───────────────┘ │ +│ │ │ +│ v │ +│ ┌────────────────────────┐ │ +│ │ Concatenation │ │ +│ │ [prefix][blend][suffix]│ │ +│ └────────┬───────────────┘ │ +│ │ │ +│ ├─────────────────────────────┐ │ +│ │ │ │ +│ v v │ +│ extended_images (217) start_images (17, 8n+1) │ +│ │ │ │ +└───────────┼─────────────────────────────┼─────────────────┘ + │ │ + v └─> Next Iteration + ┌───────────────┐ + │ CreateVideo │ + │ Concatenate │ + │ with Audio │ + └───────┬───────┘ + │ + v + ┌───────────────┐ + │ SaveVideo │ + │ Final Output │ + └───────────────┘ +``` + +--- + +### Overlap Blending Visualization + +``` +Source Batch (121 frames): +[████████████████████████████████████████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] + └─ Last 25 frames ─┘ + +New Batch (121 frames): + [░░░░░░░░░░░░░░░░░░░░░░░░░████████████████████████████████████████████████████] + └─ First 25 frames ─┘ + +Blending Zone (25 frames with linear alpha): +Frame: 1 2 3 4 5 ... 23 24 25 +Alpha: 0.00 0.04 0.08 0.12 0.16 ... 0.92 0.96 1.00 + ████ ███▓ ███▒ ██▒░ ██░░ ... ░▒██ ░▓███ ░███ + +Blended: (1-α)×source + α×new + +Extended Result (217 frames): +[████████████████████████████████████████████████████▓▓▒▒░░████████████████████████████████████████████████████████████████████] + └─ Smooth transition ─┘ +``` + +--- + +## Conclusion + +The Extension Module provides a powerful, flexible solution for iterative video generation with LTX-2. Key takeaways: + +1. **Always use 8n+1 conformance** (`ltx2_round_down`) when feeding samplers +2. **Start with overlap=25** and `linear_blend` for best results +3. **Enable quality features** (color match, seam search) only when needed +4. **Monitor the report output** to verify correct operation +5. **Test with simple configs first**, then optimize + +For support and updates, see the [IAMCCS-nodes repository](https://github.com/IAMCCS/IAMCCS-nodes). + +--- + +*Document Version: 1.0* +*Last Updated: January 2026* +*Extension Module Version: 87665e5* diff --git a/docs/LTX2_EXTENSION_NODES_GUIDE_EN.md b/docs/LTX2_EXTENSION_NODES_GUIDE_EN.md new file mode 100644 index 0000000..795ee80 --- /dev/null +++ b/docs/LTX2_EXTENSION_NODES_GUIDE_EN.md @@ -0,0 +1,394 @@ +# IAMCCS LTX-2 Extension Nodes — Final Guide (EN) + +This document explains how to use the IAMCCS LTX-2 nodes for **long-length / multi-segment video generation and extension** in ComfyUI, including the purpose of each widget and recommended usage patterns. + +## What problems these nodes solve + +1. **Seam artifacts between segments** (visible cut, flicker, exposure shift) +2. **Bad seam position** (the extension starts at an awkward frame) +3. **LTX VideoVAE frame-count constraint**: some encode paths require the number of guide frames to be of the form: + +$$N = 1 + 8k$$ + +4. **Workflow simplification**: reduce reliance on multiple helper nodes for overlap math, ranges, etc. + +--- + +## Quick decision guide (what to touch first) + +### If you get the LTX VideoVAE error: “Encode input must have 1 + 8 * x frames” + +This is the **`8n+1`** rule: the number of frames going into certain LTX/LTXV encode paths must be: + +$$N = 1 + 8k$$ + +In iterative extension workflows, this usually affects **the guide/start frames** you feed into the next segment. + +Use these fixes in this order: + +1) **Set `safe_mode = native_workflow_safe`** in `IAMCCS_LTX2_ExtensionModule` + - This extracts the start frames exactly like the original stable workflow: + - `start_images = extended_images[-overlap_frames:-1]` + +2) If you still need a strict `8n+1` count, set **`start_frames_rule`**: + - `ltx2_round_down`: most predictable and “never increases” the frame count. + - `ltx2_nearest`: useful if you want the closest valid count (may go up or down). + +3) If the frame rule is needed elsewhere (not on the extension module), use `IAMCCS_LTX2_FrameCountValidator` on the integer driving that node. + +### If the seam is visible (hard cut / flicker) + +- Start with: + - `overlap_mode = ease_in_out` (or `linear_blend` if you want the simplest behavior) + - keep overlap modest (common range: ~8–24 frames; larger overlap can help but costs compute/time) + +### If exposure/white balance shifts at the seam + +- Enable color matching: + - `color_match_mode = luma_only` (usually the safest) + - `color_match_strength = 0.3..0.7` + - `color_reference_window = 6..12` + +### If the seam “restarts weirdly” (bad timing / rewind) + +- Use seam search (try only after overlap/blend): + - `seam_search_mode = best_of_k` + - `k_search = 8..24` + +### If you are using AutoLink overlap loops + +- Prefer wiring `autolink_overlap_in` / `autolink_overlap_out` so each iteration can override overlap cleanly. + +### About `IAMCCS_LTX2_ExtensionModule_simple` + +- `IAMCCS_LTX2_ExtensionModule_simple` is the **minimal** variant of the Extension Module. +- It exposes only the core overlap/blend/math widgets (no color match, seam search, metrics). +- It does **not** expose `safe_mode` or `start_frames_rule` as widgets. +- It **always enforces** the LTX-2 start-frame rule $N = 1 + 8k$ automatically (round-down), to avoid VideoVAE encode frame-count errors. + +## Nodes overview + +- `IAMCCS_LTX2_ExtensionModule` + - Merges the previous segment (`source_images`) with the new segment (`new_images`) using overlap/blend. + - Outputs `extended_images` (merged batch) and `start_images` (frames used to guide the next segment). + - Optional seam improvements: exposure/color matching and best-of-k seam selection. + - Optional “native safe” extraction that matches the original stable workflow behavior. + - Optional AutoLink overlap loop I/O: + - `autolink_overlap_in` (override overlap when > 0) + - `autolink_overlap_out` (feed the next iteration) + +- `IAMCCS_LTX2_GetImageFromBatch` + - Extracts frames from the start/end of an image batch, or by an explicit range. + - Adds optional auto-count and diagnostics outputs. + - Optional “native safe” mode matching `images[-count:-1]` in from-end mode. + +- `IAMCCS_LTX2_ReferenceImageSwitch` + - Safe way to inject a **reference image** to improve identity/style consistency **without breaking overlap continuity**. + - Default is `none`, so existing workflows are unchanged. + +- `IAMCCS_LTX2_ReferenceStartFramesInjector` + - (New) Injects/blends the reference directly into the **guide/conditioning frames** (`start_images` / segment `images`). + - Useful when feeding the reference into `image_1` (empty latent image) has **weak or no identity effect**. + - Can be applied to **only one segment** (e.g. segment 3 only). + +- `IAMCCS_LTX2_FrameCountValidator` + - Helper to validate/correct an integer frame count to the `1 + 8*k` rule. + +--- + +## 1) IAMCCS_LTX2_ExtensionModule + +### Inputs + +**Required** + +- `source_images` (IMAGE) + - The current accumulated batch (previous segment output). +- `overlap_frames` (INT) + - How many frames overlap between segments. +- `overlap_side` (dropdown) + - `source`: overlap uses the tail of `source_images` against the head of `new_images`. + - `new_images`: swaps which side is treated as source/destination for blending. +- `overlap_mode` (dropdown) + - `cut`: hard cut (fastest, most visible seam). + - `linear_blend`: linear crossfade. + - `ease_in_out`: smoother crossfade. + - `filmic_crossfade`: gamma-aware blend (often smoother in highlights). + - `perceptual_crossfade`: LAB blend via Kornia (falls back if Kornia not installed). +- `enable_math` (BOOLEAN) + - Enables the built-in “how many start frames to output” calculation. +- `math_operation` (dropdown) + - Applies to `overlap_frames` (as `a`) and `math_value_b` (as `b`) when computing how many frames to output as `start_images`. + - Typical: `a-b` or `a-1`. + +**Safety / LTX rule** + +- `safe_mode` (dropdown) + - `none`: uses the node’s normal start-images logic. + - `native_workflow_safe`: extracts start images exactly like the proven stable graph: + - `start_images = extended_images[-overlap_frames:-1]` + - Use this if you are hitting the LTX VideoVAE error “Encode input must have 1 + 8 * x frames”. + +- `start_frames_rule` (dropdown) + - `none`: do not modify the calculated number of start frames. + - `ltx2_round_down`: force the count down to the nearest valid `1 + 8*k`. + - `ltx2_nearest`: choose the nearest valid `1 + 8*k` within bounds. + - Use this when a downstream node (VideoVAE encode/guide) requires `1 + 8*k` frame counts. + +**Quality upgrades (defaults are safe/off)** + +- `color_match_mode` (dropdown) + - `none`: no change (original behavior). + - `luma_only`: match exposure/contrast on luma. + - `per_channel`: match mean/std per RGB channel. +- `color_match_strength` (FLOAT 0..1) + - Blend between original and matched. +- `color_reference_window` (INT) + - Number of frames used from tail/head for statistics. + +- `seam_search_mode` (dropdown) + - `none`: no seam search. + - `best_of_k`: search for a better seam by testing candidate offsets. +- `k_search` (INT) + - How many candidate offsets to test (0 disables). +- `metric_weight_color` (FLOAT) + - Weight of luma continuity in the seam score. +- `metric_weight_edges` (FLOAT) + - Weight of edge continuity in the seam score. + +**Optional** + +- `new_images` (IMAGE) + - The newly generated segment. + - If omitted, the node can be used as a “prep” node (it will still output `start_images` from the current batch). +- `math_value_b` (INT) + - Used by `math_operation`. + +### Outputs + +- `source_images` (IMAGE) — passthrough +- `start_images` (IMAGE) — frames to feed as guide for the next segment +- `extended_images` (IMAGE) — merged batch +- `overlap_frames` (INT) +- `calculated_frames` (INT) — actual number of frames output in `start_images` +- `extension_frames` (INT) — how many frames were added +- `report` (STRING) + +### Recommended settings + +- Most stable: `safe_mode = native_workflow_safe`, `overlap_mode = ease_in_out` (or `linear_blend`) +- If you see exposure shift: `color_match_mode = luma_only`, `strength = 0.3..0.7` +- If you see weird seam timing: `seam_search_mode = best_of_k`, `k_search = 8..24` + +--- + +## 2) IAMCCS_LTX2_GetImageFromBatch + +### Purpose +A small helper to extract frames for the next segment or for debugging. + +### Inputs + +- `images` (IMAGE) +- `mode` (dropdown) + - `from_start`: take the first `count` frames + - `from_end`: take the last `count` frames + - `range`: take `[start_index:end_index)` +- `count` (INT) + +**Upgrades** + +- `auto_count_mode` (dropdown) + - `none`: use `count` widget. + - `prefer_input`: use `count_in` if connected. + - `use_widget`: explicitly use the widget value. +- `diagnostics` (dropdown) + - `none`: normal behavior. + - `basic`: exposes `start_index` and `end_index` outputs. + +**Safety / LTX rule** + +- `count_rule` (dropdown) + - `none` / `ltx2_round_down` / `ltx2_nearest` for `1 + 8*k`. +- `safe_mode` (dropdown) + - `none`: normal extraction. + - `native_workflow_safe`: for `from_end` uses `images[-count:-1]`. + +**Optional** + +- `count_in` (INT) +- `start_index` / `end_index` (INT) for `range` mode. + +### Outputs + +- `images` (IMAGE) +- `count` (INT) +- `report` (STRING) +- `start_index`, `end_index` (INT) + +--- + +## 3) IAMCCS_LTX2_ReferenceImageSwitch + +### Why this node exists +In long-length generation, you typically want: +- **Continuity** driven by overlap/start frames +- **Identity/style consistency** reinforced by a stable reference image + +This node lets you add a reference image **without replacing** the overlap continuity input. + +### Inputs + +- `default_image` (IMAGE) + - What the workflow already used before (pass-through by default). +- `mode` (dropdown) + - `none`: output `default_image` (fully backward-compatible). + - `use_reference`: output `reference_image`. + - `blend`: output mix of `default_image` and `reference_image`. +- `blend_strength` (FLOAT) + - Only for `blend` mode. +- `reference_image` (optional IMAGE) + - If not connected, the node behaves like `none`. + +### Output + +- `image` (IMAGE) +- `report` (STRING) + +### Practical usage + +- Insert it on the **auxiliary** image input of your segment sampler (often called `image_1`). +- Keep overlap/start frames connected exactly as before. +- If you enable `use_reference`/`blend`, the reference is **automatically resized** to match `default_image` (more stable for downstream nodes). + +Note: in many LTX/LTXV workflows, feeding the reference into `image_1` (empty latent image) may not be enough to “lock” identity when a face is revealed later in the segment. In that case, use the node below. + +--- + +## 3b) IAMCCS_LTX2_ReferenceStartFramesInjector + +### Why it exists +If identity drifts even with a reference, it often means the reference is connected to an input that the model barely uses. This node modifies the actual guide/conditioning frames. + +### Inputs + +- `start_images` (IMAGE) + - The guide frames that feed the segment (typically `start_images` from the extension module, or the sampler’s `images` input). +- `mode` + - `none`: passthrough. + - `inject`: replaces the selected frames with the reference. + - `blend`: mixes reference and original frames. +- `blend_strength` (0..1) + - Only used for `blend` (0 = no effect, 1 = full reference). In `inject` it behaves like 1. +- `frames_to_inject` (INT) + - How many guide frames to modify. +- `ramp` (BOOLEAN) + - If `true`, applies a gradual ramp across the injected frames. +- `position` + - `tail`: last K frames (usually best, closest to the seam). + - `head`: first K frames. +- `reference_image` (optional IMAGE) + - Usually the output of `IAMCCS_LTX2_ReferenceImageSwitch`. + +### Outputs + +- `start_images` (IMAGE) +- `report` (STRING) + +### Recommended starter settings + +- If identity is not sticking but you want to preserve continuity: + - `mode = blend` + - `frames_to_inject = 3..6` + - `blend_strength = 0.5..0.85` + - `ramp = true` + - `position = tail` + +If you see seam discontinuity, lower `blend_strength` and/or reduce `frames_to_inject`. + +--- + +## How to decide when/where to use a reference + +Quick checklist: + +1. **Is the face/identity visible in the first frames of the segment?** + - Yes → a reference can work well. + - No (reveal happens mid/late segment) → the reference may have little leverage: consider cutting segments so the reveal starts at the segment boundary, or use `ReferenceStartFramesInjector` (and/or dedicated tools like FaceID/IPAdapter if compatible). + +2. **What are you stabilizing?** + - Style / global look → `ReferenceImageSwitch` (or `color_match_mode` in ExtensionModule) is often enough. + - Identity (specific face) → `ReferenceStartFramesInjector` is more likely required. + +3. **Where to wire it?** + - `image_1` / empty latent image: can be a hint, not guaranteed. + - `images` / start frames (conditioning): highest impact. + +4. **How to limit it to one segment (e.g. segment 3 only)** + - Place `ReferenceStartFramesInjector` only in the path feeding that segment’s `images` / `start_images`. + - Leave other segments untouched (no injector). + +## 4) IAMCCS_LTX2_FrameCountValidator + +### Inputs + +- `frame_count` (INT) +- `auto_correct` (BOOLEAN) +- `correction_mode` (`nearest` / `round_up` / `round_down`) + +### Outputs + +- `validated_count` (INT) +- `is_valid` (BOOLEAN) +- `nearest_valid` (INT) +- `report` (STRING) + +--- + +## Common workflows / use cases + +### A) Long-length extension (multi segment) +1. Generate segment 1. +2. Use `IAMCCS_LTX2_ExtensionModule` to compute `start_images` and merge segments. +3. Feed `start_images` into the next segment guide/conditioning. +4. Repeat. + +Recommended: enable `safe_mode = native_workflow_safe` if you see LTX frame-count errors. + +### B) Reduce seams +- Prefer `ease_in_out` or `filmic_crossfade`. +- Use `color_match_mode` if you see exposure shifts. +- Use `best_of_k` seam search if the seam starts at a bad moment. + +### C) Improve identity consistency +- Add `IAMCCS_LTX2_ReferenceImageSwitch` to `image_1`. +- Connect a single reference image and set mode to `blend` (start at 0.2..0.4). + +--- + +## Troubleshooting + +- **“IAMCCS_LTX2_ReferenceImageSwitch not found”** + - Ensure you updated the IAMCCS nodes and restart ComfyUI. + - The node must be exported in the package registry (`__init__.py`). + +- **“Encode input must have 1 + 8 * x frames”** + - Use `safe_mode = native_workflow_safe` or set `start_frames_rule/count_rule` to enforce `1 + 8*k`. + +- **Border motion artifacts (edge warping / flicker)** + - Note: `metric_weight_edges` and `best_of_k` improve seam selection *inside the overlap* between segments; they do not automatically “fix” frame borders. + - Common improvements: + - Avoid changing resize/crop between segments; keep one resolution end-to-end. + - Prefer “clean” resolutions (multiples of 64 where possible) to reduce VAE boundary artifacts. + - Quick workaround: apply a small crop (e.g., 8–16 px per side) then resize back. + - Helpful nodes (IAMCCS): + - `IAMCCS_LTX2_ImageBatchPadReflect`: adds a reflect border (increases resolution). + - `IAMCCS_LTX2_ImageBatchCropByPad`: removes that border (back to target resolution). + - Recommended usage (when you want the model to have more border context): + - Pick `pad_x/pad_y` (e.g., 16). + - Generate at a higher resolution: `W_pad = W + 2*pad_x`, `H_pad = H + 2*pad_y` (including `EmptyImage`). + - If you have “initial”/reference images at the old resolution, run them through `PadReflect` to reach `W_pad x H_pad`. + - At the end (before `CreateVideo`), run `CropByPad` with the same `pad_x/pad_y` to return to `W x H`. + +- **Reference image causes a resolution error** + - Resize/crop the reference to match your workflow resolution before feeding it. diff --git a/WanImageMotion.md b/docs/WanImageMotion.md similarity index 100% rename from WanImageMotion.md rename to docs/WanImageMotion.md diff --git a/iamccs_autolink.py b/iamccs_autolink.py new file mode 100644 index 0000000..0a39693 --- /dev/null +++ b/iamccs_autolink.py @@ -0,0 +1,76 @@ +# IAMCCS AutoLink - Wireless node connections +# 1:1 copy of KJ nodes functionality (Set/Get/Converter) + +# Questi nodi sono PURAMENTE FRONTEND - non fanno nulla in Python +# Tutta la logica è in JavaScript + +class IAMCCS_SetAutoLink: + """Set AutoLink - Virtual node (frontend only)""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + } + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "IAMCCS/AutoLink" + + def noop(self): + # Questo non viene mai eseguito - il nodo è virtuale + return () + + +class IAMCCS_GetAutoLink: + """Get AutoLink - Virtual node (frontend only)""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + } + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "IAMCCS/AutoLink" + + def noop(self): + # Questo non viene mai eseguito - il nodo è virtuale + return () + + +class IAMCCS_AutoLinkConverter: + """AutoLink Converter - UI tool""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "optional": { + "arg": ("AUTOLINK_ARG",), + } + } + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "IAMCCS/AutoLink" + + def noop(self, arg=None): + return () + + +class IAMCCS_AutoLinkArguments: + """AutoLink Arguments - Configuration node""" + + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = ("AUTOLINK_ARG",) + FUNCTION = "noop" + CATEGORY = "IAMCCS/AutoLink" + + def noop(self): + return (None,) + diff --git a/iamccs_gguf_accelerator.py b/iamccs_gguf_accelerator.py new file mode 100644 index 0000000..bc2539a --- /dev/null +++ b/iamccs_gguf_accelerator.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import logging +from typing import Any, Tuple + +import torch + + +_log = logging.getLogger("IAMCCS.GGUF.Accelerator") + + +def _move_to_device_recursive(obj: Any, device: torch.device) -> Tuple[Any, int, int]: + """Recursively move torch.Tensor objects inside obj to device. + + Returns (new_obj, tensor_count_moved, bytes_moved). + """ + if isinstance(obj, torch.Tensor): + try: + # If already on target device, do nothing. + if obj.device == device: + return obj, 0, 0 + moved = obj.to(device, non_blocking=True) + bytes_moved = moved.element_size() * moved.numel() + return moved, 1, bytes_moved + except Exception: + # If move fails (rare), keep original. + return obj, 0, 0 + + if isinstance(obj, tuple): + out_items: list[Any] = [] + moved_count = 0 + moved_bytes = 0 + changed = False + for item in obj: + new_item, c, b = _move_to_device_recursive(item, device) + out_items.append(new_item) + moved_count += c + moved_bytes += b + changed = changed or (new_item is not item) + return (tuple(out_items) if changed else obj), moved_count, moved_bytes + + if isinstance(obj, list): + out_list: list[Any] = [] + moved_count = 0 + moved_bytes = 0 + changed = False + for item in obj: + new_item, c, b = _move_to_device_recursive(item, device) + out_list.append(new_item) + moved_count += c + moved_bytes += b + changed = changed or (new_item is not item) + return (out_list if changed else obj), moved_count, moved_bytes + + if isinstance(obj, dict): + out_dict: dict[Any, Any] = {} + moved_count = 0 + moved_bytes = 0 + changed = False + for k, v in obj.items(): + new_v, c, b = _move_to_device_recursive(v, device) + out_dict[k] = new_v + moved_count += c + moved_bytes += b + changed = changed or (new_v is not v) + return (out_dict if changed else obj), moved_count, moved_bytes + + return obj, 0, 0 + + +def _normalize_device(value: Any, fallback: torch.device) -> torch.device: + if value is None: + return fallback + try: + return torch.device(value) + except Exception: + return fallback + + +def _cuda_device_index(device: torch.device) -> int | None: + if device.type != "cuda": + return None + if device.index is not None: + return int(device.index) + try: + return int(torch.cuda.current_device()) + except Exception: + return 0 + + +def _cuda_mem_info_mb(device: torch.device) -> tuple[float, float] | None: + if not torch.cuda.is_available() or device.type != "cuda": + return None + try: + idx = _cuda_device_index(device) + if idx is None: + return None + free_b, total_b = torch.cuda.mem_get_info(idx) + return (float(free_b) / (1024 * 1024), float(total_b) / (1024 * 1024)) + except Exception: + return None + + +def _cuda_gc() -> None: + if not torch.cuda.is_available(): + return + try: + torch.cuda.empty_cache() + except Exception: + pass + try: + torch.cuda.ipc_collect() + except Exception: + pass + + +class IAMCCS_GGUF_accelerator: + """GGUF accelerator: forces patch_on_device to reduce per-step CPU↔GPU patch movement. + + Intended for GGUF UNet models with LoRA patches where repeatedly moving patch tensors to GPU + can dominate runtime on low VRAM setups. + + Notes: + - This node does not change sampling parameters. + - It can increase VRAM usage depending on how many/large LoRA patches are present. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "mode": (["auto_oom_safe", "manual"], { + "default": "auto_oom_safe", + "tooltip": "auto_oom_safe: tries patch_on_device+eager move, falls back to offload on OOM | manual: use toggles below" + }), + "patch_on_device": ("BOOLEAN", {"default": True}), + "move_patches_now": ("BOOLEAN", { + "default": True, + "tooltip": "If enabled, attempts to pre-move patch tensors to the model load_device to reduce runtime transfers. Can increase VRAM usage." + }), + "min_free_vram_mb": ("INT", { + "default": 1500, + "min": 0, + "max": 65536, + "step": 64, + "tooltip": "(auto_oom_safe) If free VRAM is below this, we disable patch_on_device to reduce OOM risk. 0 disables the check." + }), + "oom_fallback": ("BOOLEAN", { + "default": True, + "tooltip": "If a CUDA OOM happens while moving patches, automatically switches to offload and continues." + }), + } + } + + RETURN_TYPES = ("MODEL", "STRING") + RETURN_NAMES = ("model", "report") + FUNCTION = "accelerate" + CATEGORY = "IAMCCS/Optimize" + + def accelerate(self, model, mode: str, patch_on_device: bool, move_patches_now: bool, min_free_vram_mb: int, oom_fallback: bool): + # Clone to avoid mutating upstream graph state. + try: + model_out = model.clone() + except Exception: + model_out = model + + applied = [] + + mode = str(mode or "auto_oom_safe") + + # Decide devices. + load_device = _normalize_device( + getattr(model_out, "load_device", None), + torch.device("cuda" if torch.cuda.is_available() else "cpu"), + ) + offload_device = _normalize_device(getattr(model_out, "offload_device", None), torch.device("cpu")) + + # auto mode chooses patch strategy based on VRAM headroom. + chosen_patch_on_device = bool(patch_on_device) + chosen_move_now = bool(move_patches_now) + + if mode == "auto_oom_safe" and load_device.type == "cuda": + info = _cuda_mem_info_mb(load_device) + if info is not None and int(min_free_vram_mb) > 0: + free_mb, total_mb = info + if free_mb < float(min_free_vram_mb): + chosen_patch_on_device = False + chosen_move_now = False + applied.append(f"auto:disable_patch_on_device(free≈{free_mb:.0f}MiB None: + nonlocal moved_tensors, moved_bytes + patches = getattr(model_out, "patches") + new_patches, moved_tensors, moved_bytes = _move_to_device_recursive(patches, target_device) + if new_patches is not patches: + setattr(model_out, "patches", new_patches) + + # If patch_on_device is disabled, try to ensure patches live on offload_device (CPU) + # to reduce persistent VRAM usage. + if not bool(chosen_patch_on_device) and hasattr(model_out, "patches"): + try: + target_device = offload_device + _try_move_patches() + applied.append("model.patches(move_to_offload_device)") + except Exception: + pass + + # 3) Optional: eagerly move patch tensors to load_device. + # This mirrors what GGUF's `move_patch_to_device` would do later, but doing it once + # can eliminate huge per-layer overhead. + if bool(chosen_patch_on_device) and bool(chosen_move_now) and hasattr(model_out, "patches"): + try: + target_device = load_device + _cuda_gc() + _try_move_patches() + applied.append("model.patches(move_to_load_device)") + except RuntimeError as e: + msg = str(e).lower() + is_oom = ("out of memory" in msg) or ("cuda" in msg and "memory" in msg) + if bool(oom_fallback) and is_oom: + _log.warning("OOM while moving patches to CUDA; falling back to offload. Error: %s", e) + _cuda_gc() + try: + setattr(model_out, "patch_on_device", False) + applied.append("fallback:disable_patch_on_device") + except Exception: + pass + # Also attempt to move patches back to CPU/offload_device. + try: + target_device = offload_device + _try_move_patches() + applied.append("fallback:move_patches_to_offload_device") + except Exception: + pass + else: + _log.warning("Failed to move patches to device: %s", e) + except Exception as e: + _log.warning("Failed to move patches to device: %s", e) + + mb = moved_bytes / (1024 * 1024) if moved_bytes else 0.0 + mem_info = _cuda_mem_info_mb(load_device) if load_device.type == "cuda" else None + mem_str = "" + if mem_info is not None: + free_mb, total_mb = mem_info + mem_str = f" | cuda_free≈{free_mb:.0f}/{total_mb:.0f} MiB" + report = ( + f"mode={mode} | patch_on_device={bool(getattr(model_out, 'patch_on_device', chosen_patch_on_device))} | " + f"move_patches_now={bool(chosen_move_now)} | load_device={load_device} | offload_device={offload_device}{mem_str} | " + f"applied={applied or ['(none)']} | moved_tensors={moved_tensors} | moved≈{mb:.1f} MiB" + ) + + return (model_out, report) diff --git a/iamccs_ltx2_extension_module.py b/iamccs_ltx2_extension_module.py new file mode 100644 index 0000000..e3fabf1 --- /dev/null +++ b/iamccs_ltx2_extension_module.py @@ -0,0 +1,1161 @@ +# iamccs_ltx2_extension_module.py +# =============================================================== +# IAMCCS LTX-2 Extension Module +# All-in-one node for LTX-2 video extension workflows +# Combines: Image batch extension, overlap management, math operations +# =============================================================== + +from __future__ import annotations + +import logging +import math +from typing import Optional, Tuple + +import torch +import torch.nn.functional as F + + +_log = logging.getLogger("IAMCCS.LTX2.ExtensionModule") + + +class IAMCCS_LTX2_ExtensionModule: + """ + All-in-one extension module for LTX-2 video generation workflows. + Combines image batch extension with overlap, math operations, and frame calculations. + + Features: + - Automatic overlap frame calculation with configurable modes + - Multiple blending modes (linear, ease_in_out, filmic, perceptual) + - Built-in math operations for frame calculations + - Start images extraction for next generation pass + - Compatible with iterative video extension workflows + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # Source images (from previous generation or initial frames) + "source_images": ("IMAGE", { + "tooltip": "The source images to extend (from previous generation)" + }), + + # Overlap configuration + "overlap_frames": ("INT", { + "default": 10, + "min": 1, + "max": 256, + "step": 1, + "tooltip": "Number of overlapping frames between batches" + }), + + "overlap_side": (["source", "new_images"], { + "default": "source", + "tooltip": "Which side to take overlap frames from" + }), + + "overlap_mode": ([ + "cut", + "linear_blend", + "ease_in_out", + "filmic_crossfade", + "perceptual_crossfade" + ], { + "default": "linear_blend", + "tooltip": "Blending method for overlapping frames" + }), + + # Math operations for frame calculations + "enable_math": ("BOOLEAN", { + "default": True, + "tooltip": "Enable math calculations for frame adjustments" + }), + + "math_operation": (["none", "a-b", "a-1", "a+b", "a*b", "a/b", "min(a,b)", "max(a,b)"], { + "default": "a-b", + "tooltip": "Math operation to perform on overlap value" + }), + + "safe_mode": (["none", "native_workflow_safe"], { + "default": "none", + "tooltip": "Compatibility: mimic the original workflow behavior (start_images extracted as images[-overlap_frames:-1])" + }), + + "start_frames_rule": (["none", "ltx2_round_down", "ltx2_nearest"], { + "default": "none", + "tooltip": "Optional: force start_images frame count to LTX rule (1 + 8*x) for VideoVAE encode" + }), + + # Quality upgrades (default: none = keep current behavior) + "color_match_mode": (["none", "luma_only", "per_channel"], { + "default": "none", + "tooltip": "Optional: match color/exposure of new_images to the tail of source_images before merging" + }), + "color_match_strength": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.05, + "tooltip": "0=no effect, 1=full match (only used if color_match_mode != none)" + }), + "color_reference_window": ("INT", { + "default": 8, + "min": 1, + "max": 256, + "step": 1, + "tooltip": "How many frames from the tail/head to use for stats matching" + }), + + "seam_search_mode": (["none", "best_of_k"], { + "default": "none", + "tooltip": "Optional: search inside new_images for a better seam start (reduces rewind/odd restarts)" + }), + "k_search": ("INT", { + "default": 0, + "min": 0, + "max": 64, + "step": 1, + "tooltip": "How many candidate offsets to test (0 disables). Used only if seam_search_mode=best_of_k" + }), + "metric_weight_color": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 5.0, + "step": 0.1, + "tooltip": "Weight for color/luma continuity metric" + }), + "metric_weight_edges": ("FLOAT", { + "default": 0.5, + "min": 0.0, + "max": 5.0, + "step": 0.1, + "tooltip": "Weight for edge continuity metric" + }), + }, + "optional": { + # New images (from current generation pass) + "new_images": ("IMAGE", { + "tooltip": "The newly generated images to extend with" + }), + + # Optional math operands + "math_value_b": ("INT", { + "default": 1, + "min": 0, + "max": 256, + "step": 1, + "tooltip": "Second operand for math operations (b)" + }), + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "INT", "INT", "INT", "STRING") + RETURN_NAMES = ( + "source_images", + "start_images", + "extended_images", + "overlap_frames", + "calculated_frames", + "extension_frames", + "report" + ) + FUNCTION = "process_extension" + CATEGORY = "IAMCCS/LTX-2" + + def _validate_ltx2_frames(self, frames: int) -> Tuple[bool, int]: + """ + Validate if frame count follows LTX-2 rule (8n+1). + Returns (is_valid, nearest_valid) + """ + if frames < 1: + return False, 1 + + remainder = (frames - 1) % 8 + if remainder == 0: + return True, frames + + # Find nearest valid value + down = frames - remainder + up = frames + (8 - remainder) + nearest = up if (up - frames) <= (frames - down) else max(1, down) + + return False, nearest + + def _execute_math(self, operation: str, a: int, b: int) -> int: + """Execute simple math operation safely""" + try: + if operation == "none" or operation == "": + return a + elif operation == "a-b": + return max(0, a - b) + elif operation == "a-1": + return max(0, a - 1) + elif operation == "a+b": + return a + b + elif operation == "a*b": + return a * b + elif operation == "a/b": + return int(a / b) if b != 0 else a + elif operation == "min(a,b)": + return min(a, b) + elif operation == "max(a,b)": + return max(a, b) + else: + return a + except Exception as e: + _log.warning(f"Math operation failed: {e}, returning a={a}") + return a + + def _blend_images( + self, + blend_src: torch.Tensor, + blend_dst: torch.Tensor, + mode: str + ) -> torch.Tensor: + """ + Blend two image batches using specified mode. + Both inputs should have same shape: [N, H, W, C] + """ + overlap = blend_src.shape[0] + device = blend_src.device + dtype = blend_src.dtype + + if mode == "cut": + # No blending, just return destination + return blend_dst + + elif mode == "linear_blend": + # Simple linear interpolation + alpha = torch.linspace(0, 1, overlap + 2, device=device, dtype=dtype)[1:-1] + alpha = alpha.view(-1, 1, 1, 1) + return (1 - alpha) * blend_src + alpha * blend_dst + + elif mode == "ease_in_out": + # Smooth easing curve + t = torch.linspace(0, 1, overlap + 2, device=device, dtype=dtype)[1:-1] + eased_t = 3 * t * t - 2 * t * t * t + eased_t = eased_t.view(-1, 1, 1, 1) + return (1 - eased_t) * blend_src + eased_t * blend_dst + + elif mode == "filmic_crossfade": + # Gamma-corrected blend for more natural transitions + gamma = 2.2 + alpha = torch.linspace(0, 1, overlap + 2, device=device, dtype=dtype)[1:-1] + alpha = alpha.view(-1, 1, 1, 1) + + linear_src = torch.pow(blend_src.clamp(0, 1), gamma) + linear_dst = torch.pow(blend_dst.clamp(0, 1), gamma) + blended = (1 - alpha) * linear_src + alpha * linear_dst + return torch.pow(blended, 1.0 / gamma) + + elif mode == "perceptual_crossfade": + # Blend in LAB color space for perceptually uniform transitions + try: + import kornia + alpha = torch.linspace(0, 1, overlap + 2, device=device, dtype=dtype)[1:-1] + alpha = alpha.view(-1, 1, 1, 1) + + # Convert to LAB space + src_nchw = blend_src.movedim(-1, 1) + dst_nchw = blend_dst.movedim(-1, 1) + lab_src = kornia.color.rgb_to_lab(src_nchw) + lab_dst = kornia.color.rgb_to_lab(dst_nchw) + + # Blend in LAB + blended_lab = (1 - alpha) * lab_src + alpha * lab_dst + + # Convert back to RGB + blended_rgb = kornia.color.lab_to_rgb(blended_lab) + return blended_rgb.movedim(1, -1) + except ImportError: + _log.warning("Kornia not available, falling back to linear blend") + return self._blend_images(blend_src, blend_dst, "linear_blend") + + else: + # Fallback to linear + return self._blend_images(blend_src, blend_dst, "linear_blend") + + def _apply_ltx2_frame_rule(self, frames: int, rule: str, max_allowed: int) -> int: + """Apply LTX (1 + 8*x) rule to a frame count. rule='none' keeps value.""" + frames = int(frames) + max_allowed = max(1, int(max_allowed)) + frames = max(1, min(frames, max_allowed)) + + if rule == "none" or rule == "": + return frames + + remainder = (frames - 1) % 8 + if remainder == 0: + return frames + + down = max(1, frames - remainder) + up = frames + (8 - remainder) + + if rule == "ltx2_round_down": + return max(1, min(down, max_allowed)) + + # nearest + candidates = [] + if down <= max_allowed: + candidates.append(down) + if up <= max_allowed: + candidates.append(up) + if not candidates: + return max(1, min(down, max_allowed)) + # choose min |delta|, prefer down on tie + candidates.sort(key=lambda v: (abs(v - frames), v > frames)) + return int(candidates[0]) + + def _compute_mean_std_per_channel(self, images: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """Compute per-channel mean/std across batch+spatial dims for NHWC images.""" + if images.numel() == 0: + raise ValueError("Empty image tensor") + mean = images.mean(dim=(0, 1, 2)) + var = images.var(dim=(0, 1, 2), unbiased=False) + std = torch.sqrt(var.clamp_min(1e-8)) + return mean, std + + def _match_color_exposure( + self, + new_images: torch.Tensor, + source_images: torch.Tensor, + mode: str, + strength: float, + reference_window: int, + ) -> torch.Tensor: + """Match new_images color/exposure to the tail of source_images. Images are NHWC in [0,1].""" + if mode == "none" or strength <= 0.0: + return new_images + + src_count = int(source_images.shape[0]) + new_count = int(new_images.shape[0]) + w = max(1, int(reference_window)) + src_ref = source_images[max(0, src_count - w):src_count] + new_ref = new_images[: min(w, new_count)] + + if mode == "per_channel": + src_mean, src_std = self._compute_mean_std_per_channel(src_ref) + new_mean, new_std = self._compute_mean_std_per_channel(new_ref) + scale = (src_std / new_std).view(1, 1, 1, -1) + shift = (src_mean - (src_std / new_std) * new_mean).view(1, 1, 1, -1) + matched = (new_images * scale + shift).clamp(0, 1) + elif mode == "luma_only": + # Match exposure/contrast on luma, apply same affine to all channels + weights = torch.tensor([0.2126, 0.7152, 0.0722], device=new_images.device, dtype=new_images.dtype) + src_y = (src_ref * weights.view(1, 1, 1, -1)).sum(dim=-1) + new_y = (new_ref * weights.view(1, 1, 1, -1)).sum(dim=-1) + src_mean = src_y.mean() + src_std = src_y.std(unbiased=False).clamp_min(1e-6) + new_mean = new_y.mean() + new_std = new_y.std(unbiased=False).clamp_min(1e-6) + scale = (src_std / new_std) + shift = (src_mean - scale * new_mean) + matched = (new_images * scale + shift).clamp(0, 1) + else: + return new_images + + s = float(max(0.0, min(1.0, strength))) + return ((1.0 - s) * new_images + s * matched).clamp(0, 1) + + def _downsample_nhwc(self, images: torch.Tensor, size: int = 64) -> torch.Tensor: + """Downsample NHWC images to size x size in NCHW.""" + nchw = images.movedim(-1, 1) + return F.interpolate(nchw, size=(size, size), mode="bilinear", align_corners=False) + + def _sobel_mag(self, gray_nchw: torch.Tensor) -> torch.Tensor: + """Sobel magnitude for NCHW grayscale tensor.""" + device = gray_nchw.device + dtype = gray_nchw.dtype + kx = torch.tensor([[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]], device=device, dtype=dtype).view(1, 1, 3, 3) + ky = torch.tensor([[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]], device=device, dtype=dtype).view(1, 1, 3, 3) + gx = F.conv2d(gray_nchw, kx, padding=1) + gy = F.conv2d(gray_nchw, ky, padding=1) + return torch.sqrt(gx * gx + gy * gy + 1e-8) + + def _best_of_k_offset( + self, + source_tail: torch.Tensor, + new_images: torch.Tensor, + blend_overlap: int, + k_search: int, + w_color: float, + w_edges: float, + ) -> int: + """Pick an offset into new_images that best matches source_tail over the overlap window.""" + if k_search <= 0: + return 0 + + new_count = int(new_images.shape[0]) + max_offset = min(int(k_search), max(0, new_count - blend_overlap)) + if max_offset <= 0: + return 0 + + # Prepare features (downsample + luma + edges) + weights = torch.tensor([0.2126, 0.7152, 0.0722], device=new_images.device, dtype=new_images.dtype) + src_win = source_tail[-blend_overlap:] + src_ds = self._downsample_nhwc(src_win, size=64) + src_luma = (src_ds * weights.view(1, 3, 1, 1)).sum(dim=1, keepdim=True) + src_edges = self._sobel_mag(src_luma) + + best_offset = 0 + best_score = None + + for off in range(0, max_offset + 1): + cand = new_images[off: off + blend_overlap] + if int(cand.shape[0]) != blend_overlap: + continue + cand_ds = self._downsample_nhwc(cand, size=64) + cand_luma = (cand_ds * weights.view(1, 3, 1, 1)).sum(dim=1, keepdim=True) + cand_edges = self._sobel_mag(cand_luma) + + color_mse = (src_luma - cand_luma).pow(2).mean() + edge_mse = (src_edges - cand_edges).pow(2).mean() + score = float(w_color) * color_mse + float(w_edges) * edge_mse + + if best_score is None or score < best_score: + best_score = score + best_offset = off + + return int(best_offset) + + def process_extension( + self, + source_images: torch.Tensor, + overlap_frames: int, + overlap_side: str, + overlap_mode: str, + enable_math: bool, + math_operation: str, + safe_mode: str, + start_frames_rule: str, + color_match_mode: str, + color_match_strength: float, + color_reference_window: int, + seam_search_mode: str, + k_search: int, + metric_weight_color: float, + metric_weight_edges: float, + new_images: Optional[torch.Tensor] = None, + math_value_b: int = 1, + ): + # Initialize + source_count = int(source_images.shape[0]) + overlap_frames_in = int(overlap_frames) + + # Validate inputs (match KJNodes semantics: if overlap is too large, just passthrough) + if source_count < 1: + raise ValueError("source_images batch is empty") + + if overlap_frames_in < 1: + overlap_frames_in = 1 + + if overlap_frames_in >= source_count: + report = ( + f"Source: {source_count} frames | " + f"Overlap (effective): {overlap_frames_in} frames | " + f"Start images: {source_count} frames | " + f"Extended: {source_count} frames | " + f"Extension delta: +0 frames | " + f"Blend mode: {overlap_mode} | " + f"Overlap side: {overlap_side}" + ) + _log.info(f"[LTX2_ExtensionModule] {report}") + return ( + source_images, + source_images, + source_images, + overlap_frames_in, + source_count, + 0, + report, + ) + + # Initialize output + # If no new_images are provided, behave as a "prep" node: + # - extended_images == source_images + # - start_images extracted from the (current) batch + extended_images = source_images + extension_frames_count = 0 + + # Process extension if new images are provided + if new_images is not None: + new_count = int(new_images.shape[0]) + + # Validate shapes + if source_images.shape[1:3] != new_images.shape[1:3]: + raise ValueError( + f"Source and new images must have same resolution: " + f"{tuple(source_images.shape[1:3])} vs {tuple(new_images.shape[1:3])}" + ) + + # Overlap used for blending (matches ImageBatchExtendWithOverlap) + blend_overlap = min(overlap_frames_in, source_count, new_count) + + # Option 5: Best-of-K seam search (choose a better start inside new_images) + chosen_offset = 0 + if seam_search_mode == "best_of_k" and int(k_search) > 0 and blend_overlap > 0: + chosen_offset = self._best_of_k_offset( + source_images[-blend_overlap:], + new_images, + blend_overlap=blend_overlap, + k_search=int(k_search), + w_color=float(metric_weight_color), + w_edges=float(metric_weight_edges), + ) + if chosen_offset > 0: + new_images = new_images[chosen_offset:] + new_count = int(new_images.shape[0]) + blend_overlap = min(overlap_frames_in, source_count, new_count) + + # Option 3: Color/Exposure match (apply before blending) + if color_match_mode != "none" and float(color_match_strength) > 0.0: + new_images = self._match_color_exposure( + new_images=new_images, + source_images=source_images, + mode=str(color_match_mode), + strength=float(color_match_strength), + reference_window=int(color_reference_window), + ) + + prefix = source_images[:-blend_overlap] + if overlap_side == "source": + blend_src = source_images[-blend_overlap:] + blend_dst = new_images[:blend_overlap] + else: # new_images + blend_src = new_images[:blend_overlap] + blend_dst = source_images[-blend_overlap:] + + suffix = new_images[blend_overlap:] + + if overlap_mode == "cut": + # Match KJNodes cut semantics + if overlap_side == "new_images": + extended_images = torch.cat((source_images, new_images[blend_overlap:]), dim=0) + else: + extended_images = torch.cat((source_images[:-blend_overlap], new_images), dim=0) + else: + blended = self._blend_images(blend_src, blend_dst, overlap_mode) + extended_images = torch.cat((prefix, blended, suffix), dim=0) + + extension_frames_count = int(extended_images.shape[0] - source_count) + + # Compute start_images from the CURRENT batch for the NEXT iteration. + # When chaining multiple generations, using the post-merge batch (extended_images) + # avoids graph cycles and removes the need for external math/range nodes. + base_count = int(extended_images.shape[0]) + safe_mode = str(safe_mode or "none") + if safe_mode == "native_workflow_safe": + # Match the original graph: start_images is computed with + # start = total - overlap_frames, end = total - 1 (exclusive) + if base_count <= 1: + start_images = extended_images[:1] + start_index = 0 + start_end = int(start_images.shape[0]) + else: + start_end = base_count - 1 + start_index = max(0, start_end - overlap_frames_in) + start_images = extended_images[start_index:start_end] + else: + start_index = max(0, base_count - overlap_frames_in) + + calculated_frames = overlap_frames_in + if enable_math and math_operation != "none": + calculated_frames = self._execute_math(math_operation, overlap_frames_in, math_value_b) + + max_start_frames = max(1, base_count - start_index) + calculated_frames = max(1, min(int(calculated_frames), max_start_frames)) + + # Optional: enforce LTX (1+8*x) rule for VideoVAE encode + calculated_frames = self._apply_ltx2_frame_rule(calculated_frames, str(start_frames_rule), max_start_frames) + + start_end = min(base_count, start_index + calculated_frames) + start_images = extended_images[start_index:start_end] + + # Generate report + report = ( + f"Source: {source_count} frames | " + f"Overlap (effective): {overlap_frames_in} frames | " + f"Start range (from current batch): start_index={start_index}, num_frames={start_images.shape[0]} | " + f"Math: {math_operation if enable_math else 'disabled'} | " + f"Start frames rule: {start_frames_rule} | " + f"Safe mode: {safe_mode} | " + f"Extended: {int(extended_images.shape[0]) if extended_images is not None else 0} frames | " + f"Extension delta: +{extension_frames_count} frames | " + f"Blend mode: {overlap_mode} | " + f"Overlap side: {overlap_side} | " + f"Color match: {color_match_mode} | " + f"Seam search: {seam_search_mode}" + ) + + _log.info(f"[LTX2_ExtensionModule] {report}") + + return ( + source_images, # Pass through source + start_images, # Start images for next pass + extended_images, # Extended result + overlap_frames_in, # Original overlap value + int(start_images.shape[0]), # Actual start-frame count + extension_frames_count, # How many frames were added + report # Detailed report + ) + + +class IAMCCS_LTX2_GetImageFromBatch: + """ + Extracts a specific range of images from a batch. + Useful for: + - Getting start images for next iteration + - Extracting specific frames from generation + - Creating sub-batches from large batches + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE", { + "tooltip": "Input image batch" + }), + "mode": (["from_start", "from_end", "range"], { + "default": "from_end", + "tooltip": "Extraction mode" + }), + "count": ("INT", { + "default": 10, + "min": 1, + "max": 10000, + "step": 1, + "tooltip": "Number of frames to extract (for from_start/from_end)" + }), + + # Upgrade options (default: none = keep current behavior) + "auto_count_mode": (["none", "prefer_input", "use_widget"], { + "default": "none", + "tooltip": "Optional: auto-drive count from an INT input (e.g. overlap_frames)" + }), + "diagnostics": (["none", "basic"], { + "default": "none", + "tooltip": "Optional: expose start/end indices as extra outputs" + }), + + "count_rule": (["none", "ltx2_round_down", "ltx2_nearest"], { + "default": "none", + "tooltip": "Optional: force count to LTX rule (1 + 8*x) for VideoVAE encode" + }), + + "safe_mode": (["none", "native_workflow_safe"], { + "default": "none", + "tooltip": "Compatibility: mimic original GetImageRangeFromBatch behavior for from_end (images[-count:-1])" + }), + }, + "optional": { + "count_in": ("INT", { + "default": 10, + "min": 1, + "max": 10000, + "step": 1, + "tooltip": "Optional count input (used if auto_count_mode=prefer_input)" + }), + "start_index": ("INT", { + "default": 0, + "min": 0, + "max": 10000, + "step": 1, + "tooltip": "Start index for range mode" + }), + "end_index": ("INT", { + "default": 10, + "min": 0, + "max": 10000, + "step": 1, + "tooltip": "End index for range mode (exclusive)" + }), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "STRING", "INT", "INT") + RETURN_NAMES = ("images", "count", "report", "start_index", "end_index") + FUNCTION = "extract" + CATEGORY = "IAMCCS/LTX-2" + + def extract(self, images, mode, count, auto_count_mode, diagnostics, count_rule, safe_mode, count_in=None, start_index=0, end_index=10): + """Extract images from batch""" + total = images.shape[0] + + def apply_ltx_rule(n: int, rule: str, max_allowed: int) -> int: + n = int(n) + max_allowed = max(1, int(max_allowed)) + n = max(1, min(n, max_allowed)) + if rule == "none" or rule == "": + return n + remainder = (n - 1) % 8 + if remainder == 0: + return n + down = max(1, n - remainder) + up = n + (8 - remainder) + if rule == "ltx2_round_down": + return max(1, min(down, max_allowed)) + candidates = [] + if down <= max_allowed: + candidates.append(down) + if up <= max_allowed: + candidates.append(up) + if not candidates: + return max(1, min(down, max_allowed)) + candidates.sort(key=lambda v: (abs(v - n), v > n)) + return int(candidates[0]) + + # Option C: Auto-Count + effective_count = int(count) + if auto_count_mode != "none" and count_in is not None: + if auto_count_mode == "prefer_input": + effective_count = int(count_in) + elif auto_count_mode == "use_widget": + effective_count = int(count) + + effective_count = max(1, min(effective_count, int(total))) + + safe_mode = str(safe_mode or "none") + if safe_mode == "native_workflow_safe" and mode == "from_end": + # Match: start = total - count, end = total - 1 (exclusive) + if int(total) <= 1: + result = images[:1] + used_start = 0 + used_end = int(result.shape[0]) + report = f"Extracted (safe) {result.shape[0]} frames from batch of {total}" + else: + used_start = max(0, int(total) - int(effective_count)) + used_end = max(0, int(total) - 1) + result = images[used_start:used_end] + report = f"Extracted (safe) frames {used_start} to {used_end} ({result.shape[0]} frames) from batch of {total}" + return (result, result.shape[0], report, used_start, used_end) + + # Optional: enforce LTX (1+8*x) rule for VideoVAE encode + if mode in ("from_start", "from_end"): + effective_count = apply_ltx_rule(effective_count, str(count_rule), int(total)) + + if mode == "from_start": + result = images[:effective_count] + used_start = 0 + used_end = effective_count + report = f"Extracted first {effective_count} frames from batch of {total}" + + elif mode == "from_end": + result = images[-effective_count:] + used_start = int(total) - int(effective_count) + used_end = int(total) + report = f"Extracted last {effective_count} frames from batch of {total}" + + else: # range + start_index = max(0, min(start_index, total)) + end_index = max(start_index, min(end_index, total)) + result = images[start_index:end_index] + actual_count = result.shape[0] + used_start = int(start_index) + used_end = int(end_index) + report = f"Extracted frames {start_index} to {end_index} ({actual_count} frames) from batch of {total}" + + return (result, result.shape[0], report, used_start, used_end) + + +class IAMCCS_LTX2_ReferenceImageSwitch: + """Selects a reference image (or keeps the default). + + Intended use: feed the output into a segment node's optional/secondary image input + (e.g. `image_1`) to reinforce identity/style consistency WITHOUT touching the + overlap/start-image continuity input. + + Default behavior is `none` which preserves old workflows. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "default_image": ("IMAGE", {"tooltip": "Fallback image (e.g. EmptyImage)"}), + "mode": (["none", "use_reference", "blend"], { + "default": "none", + "tooltip": "none: pass default_image | use_reference: output reference_image | blend: mix both" + }), + "blend_strength": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.05, + "tooltip": "Only used if mode=blend (0=default, 1=reference)" + }), + }, + "optional": { + "reference_image": ("IMAGE", {"tooltip": "Optional reference image (usually batch size 1)"}), + }, + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "report") + FUNCTION = "select" + CATEGORY = "IAMCCS/LTX-2" + + def _match_batch(self, base: torch.Tensor, other: torch.Tensor) -> torch.Tensor: + """Broadcast/crop `other` batch to match `base` batch length when possible.""" + bn = int(base.shape[0]) + on = int(other.shape[0]) + if bn == on: + return other + if on == 1 and bn > 1: + return other.repeat(bn, 1, 1, 1) + if bn == 1 and on > 1: + return other[:1] + # Both >1 but mismatch: crop to min + m = min(bn, on) + return other[:m] + + def _resize_to(self, image: torch.Tensor, target_h: int, target_w: int) -> torch.Tensor: + if int(image.shape[1]) == int(target_h) and int(image.shape[2]) == int(target_w): + return image + # IMAGE tensors in ComfyUI are [N, H, W, C] + x = image.permute(0, 3, 1, 2) + x = F.interpolate(x, size=(int(target_h), int(target_w)), mode="bilinear", align_corners=False) + x = x.permute(0, 2, 3, 1) + return x.clamp(0, 1) + + def select(self, default_image: torch.Tensor, mode: str, blend_strength: float, reference_image: Optional[torch.Tensor] = None): + mode = str(mode or "none") + if mode == "none" or reference_image is None: + return (default_image, f"Reference switch: {mode} (using default_image)") + + ref = self._match_batch(default_image, reference_image) + base = default_image + # If we cropped the ref, crop base too to keep alignment. + if int(ref.shape[0]) != int(base.shape[0]): + base = base[: int(ref.shape[0])] + + target_h, target_w = int(base.shape[1]), int(base.shape[2]) + resized = False + if int(ref.shape[1]) != target_h or int(ref.shape[2]) != target_w: + ref = self._resize_to(ref, target_h, target_w) + resized = True + + if mode == "use_reference": + return (ref, f"Reference switch: use_reference{' (resized)' if resized else ''}") + + # blend + s = float(max(0.0, min(1.0, blend_strength))) + out = ((1.0 - s) * base + s * ref).clamp(0, 1) + return (out, f"Reference switch: blend (strength={s:.2f}){' (resized)' if resized else ''}") + + +class IAMCCS_LTX2_ReferenceStartFramesInjector: + """Inject a reference image into the conditioning frames (start_images). + + Why: in LTX extension workflows, the model mostly follows `images` (conditioning frames). + Feeding a reference into an auxiliary/empty-latent image slot often has little/no effect on identity. + This node lets you (optionally) blend the reference into the last (or first) K conditioning frames. + + Default mode is `none` to preserve old workflows. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "start_images": ("IMAGE", {"tooltip": "Conditioning frames (e.g. start_images from ExtensionModule)"}), + "mode": (["none", "inject", "blend"], { + "default": "none", + "tooltip": "none: passthrough | inject: replace frames with reference | blend: mix with existing" + }), + "blend_strength": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.05, + "tooltip": "Used for mode=blend (0=no change, 1=full reference). For mode=inject it's treated as 1.0" + }), + "frames_to_inject": ("INT", { + "default": 1, + "min": 1, + "max": 64, + "step": 1, + "tooltip": "How many conditioning frames to modify" + }), + "ramp": ("BOOLEAN", { + "default": True, + "tooltip": "If true, gradually increases strength across the injected frames" + }), + "position": (["tail", "head"], { + "default": "tail", + "tooltip": "Where to inject (tail=last K frames, head=first K frames)" + }), + }, + "optional": { + "reference_image": ("IMAGE", {"tooltip": "Reference image (batch 1 is ok; will be resized to match start_images)"}), + }, + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("start_images", "report") + FUNCTION = "inject" + CATEGORY = "IAMCCS/LTX-2" + + def _resize_to(self, image: torch.Tensor, target_h: int, target_w: int) -> torch.Tensor: + if int(image.shape[1]) == int(target_h) and int(image.shape[2]) == int(target_w): + return image + x = image.permute(0, 3, 1, 2) + x = F.interpolate(x, size=(int(target_h), int(target_w)), mode="bilinear", align_corners=False) + x = x.permute(0, 2, 3, 1) + return x.clamp(0, 1) + + def _repeat_or_crop_batch(self, desired_n: int, image: torch.Tensor) -> torch.Tensor: + n = int(image.shape[0]) + if n == desired_n: + return image + if n == 1 and desired_n > 1: + return image.repeat(desired_n, 1, 1, 1) + return image[:desired_n] + + def inject( + self, + start_images: torch.Tensor, + mode: str, + blend_strength: float, + frames_to_inject: int, + ramp: bool, + position: str, + reference_image: Optional[torch.Tensor] = None, + ): + mode = str(mode or "none") + if mode == "none" or reference_image is None: + return (start_images, f"StartFrames injector: {mode} (passthrough)") + + base = start_images + total = int(base.shape[0]) + k = int(max(1, min(int(frames_to_inject), total))) + pos = str(position or "tail") + + target_h, target_w = int(base.shape[1]), int(base.shape[2]) + ref = self._resize_to(reference_image, target_h, target_w) + ref = self._repeat_or_crop_batch(k, ref) + + out = base.clone() + if pos == "head": + idxs = list(range(0, k)) + else: # tail + idxs = list(range(total - k, total)) + + # Strength handling + if mode == "inject": + max_s = 1.0 + else: + max_s = float(max(0.0, min(1.0, blend_strength))) + + used = 0 + for j, i in enumerate(idxs): + if ramp and k > 1: + s = max_s * float(j + 1) / float(k) + else: + s = max_s + out[i] = ((1.0 - s) * out[i] + s * ref[j]).clamp(0, 1) + used += 1 + + return (out, f"StartFrames injector: {mode} ({pos}, frames={used}, strength={max_s:.2f}, resized)") + + +class IAMCCS_LTX2_FrameCountValidator: + """ + Validates and corrects frame counts for LTX-2 (8n+1 rule). + Outputs: validated count, is_valid flag, nearest valid count. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "frame_count": ("INT", { + "default": 81, + "min": 1, + "max": 10000, + "step": 1, + "tooltip": "Frame count to validate" + }), + "auto_correct": ("BOOLEAN", { + "default": True, + "tooltip": "Automatically correct to nearest valid value" + }), + "correction_mode": (["nearest", "round_up", "round_down"], { + "default": "nearest", + "tooltip": "How to correct invalid values" + }), + } + } + + RETURN_TYPES = ("INT", "BOOLEAN", "INT", "STRING") + RETURN_NAMES = ("validated_count", "is_valid", "nearest_valid", "report") + FUNCTION = "validate" + CATEGORY = "IAMCCS/LTX-2" + + def validate(self, frame_count, auto_correct, correction_mode): + """Validate LTX-2 frame count (8n+1 rule)""" + + # Check if valid + remainder = (frame_count - 1) % 8 + is_valid = remainder == 0 + + if is_valid: + report = f"✅ {frame_count} is valid (8n+1 rule)" + return (frame_count, True, frame_count, report) + + # Calculate corrections + down = frame_count - remainder + up = frame_count + (8 - remainder) + + if correction_mode == "round_up": + nearest = up + elif correction_mode == "round_down": + nearest = max(1, down) + else: # nearest + nearest = up if (up - frame_count) <= (frame_count - down) else max(1, down) + + # Output + output_count = nearest if auto_correct else frame_count + + report = ( + f"❌ {frame_count} is NOT valid (8n+1 rule)\n" + f"Remainder: {remainder}\n" + f"Nearest valid: {nearest} (n={(nearest-1)//8})\n" + f"Output: {output_count} ({'corrected' if auto_correct else 'uncorrected'})" + ) + + if auto_correct: + _log.info(f"[LTX2_Validator] Corrected {frame_count} → {nearest}") + + return (output_count, False, nearest, report) + + +class IAMCCS_LTX2_ExtensionModule_simple(IAMCCS_LTX2_ExtensionModule): + """A truly minimal Extension Module. + + Goals: + - Keep ONLY the core widgets (overlap + blend + math) + - No additional "quality" options (color match / seam search / metrics) + - No user-facing safe_mode/start_frames_rule widgets + - Always enforce LTX-2 start-frames rule (1 + 8*k) automatically + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "source_images": ("IMAGE", { + "tooltip": "The source images to extend (from previous generation)" + }), + "overlap_frames": ("INT", { + "default": 10, + "min": 1, + "max": 256, + "step": 1, + "tooltip": "Number of overlapping frames between batches" + }), + "overlap_side": (["source", "new_images"], { + "default": "source", + "tooltip": "Which side to take overlap frames from" + }), + "overlap_mode": ([ + "cut", + "linear_blend", + "ease_in_out", + "filmic_crossfade", + "perceptual_crossfade" + ], { + "default": "linear_blend", + "tooltip": "Blending method for overlapping frames" + }), + "enable_math": ("BOOLEAN", { + "default": True, + "tooltip": "Enable math calculations for frame adjustments" + }), + "math_operation": (["none", "a-b", "a-1", "a+b", "a*b", "a/b", "min(a,b)", "max(a,b)"], { + "default": "a-b", + "tooltip": "Math operation to perform on overlap value" + }), + "math_value_b": ("INT", { + "default": 1, + "min": 0, + "max": 256, + "step": 1, + "tooltip": "Second operand for math operations (b)" + }), + }, + "optional": { + "new_images": ("IMAGE", { + "tooltip": "The newly generated images to extend with" + }), + }, + } + + RETURN_TYPES = IAMCCS_LTX2_ExtensionModule.RETURN_TYPES + RETURN_NAMES = IAMCCS_LTX2_ExtensionModule.RETURN_NAMES + FUNCTION = IAMCCS_LTX2_ExtensionModule.FUNCTION + CATEGORY = "IAMCCS/LTX-2" + + def process_extension( + self, + source_images: torch.Tensor, + overlap_frames: int, + overlap_side: str, + overlap_mode: str, + enable_math: bool, + math_operation: str, + math_value_b: int, + new_images: Optional[torch.Tensor] = None, + ): + # Fixed behavior knobs (not user-exposed in the simple node) + safe_mode = "none" + start_frames_rule = "ltx2_round_down" # always enforce 8n+1 + + color_match_mode = "none" + color_match_strength = 0.0 + color_reference_window = 8 + + seam_search_mode = "none" + k_search = 0 + metric_weight_color = 1.0 + metric_weight_edges = 0.5 + + return super().process_extension( + source_images=source_images, + overlap_frames=overlap_frames, + overlap_side=overlap_side, + overlap_mode=overlap_mode, + enable_math=enable_math, + math_operation=math_operation, + safe_mode=safe_mode, + start_frames_rule=start_frames_rule, + color_match_mode=color_match_mode, + color_match_strength=color_match_strength, + color_reference_window=color_reference_window, + seam_search_mode=seam_search_mode, + k_search=k_search, + metric_weight_color=metric_weight_color, + metric_weight_edges=metric_weight_edges, + new_images=new_images, + math_value_b=int(math_value_b), + ) + + +# Node registration +NODE_CLASS_MAPPINGS = { + "IAMCCS_LTX2_ExtensionModule": IAMCCS_LTX2_ExtensionModule, + "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, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "IAMCCS_LTX2_ExtensionModule": "LTX-2 Extension Module 🎬", + "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)", +} diff --git a/iamccs_ltx2_lora_stack_segmented6.py b/iamccs_ltx2_lora_stack_segmented6.py new file mode 100644 index 0000000..c16d511 --- /dev/null +++ b/iamccs_ltx2_lora_stack_segmented6.py @@ -0,0 +1,288 @@ +# iamccs_ltx2_lora_stack_segmented6.py +# =============================================================== +# Segmented LoRA stacks for workflows with 3 segments × 2 stages. +# Outputs either 6 LoRA stacks or 6 MODELs (MODEL only, no CLIP). +# =============================================================== + +import logging +from typing import Any, Dict, Optional + +import comfy.sd +import comfy.utils +import folder_paths + +from .iamccs_ltx2_lora_stack import SuppressLTX2MissingKeysFilter, standardize_ltx2_lora_keys + + +def _load_lora_state_dict(name: str, cache: Dict[str, Any]) -> Optional[dict]: + if not name or name == "no": + return None + if name in cache: + return cache[name] + + path = folder_paths.get_full_path_or_raise("loras", name) + sd = comfy.utils.load_torch_file(path, safe_load=True) + sd = standardize_ltx2_lora_keys(sd) + cache[name] = sd + return sd + + +def _append_lora(stack: list, name: str, strength: float, cache: Dict[str, Any]) -> None: + if not name or name == "no": + return + s = float(strength) + if s == 0.0: + return + + sd = _load_lora_state_dict(name, cache) + if not sd: + return + + stack.append({"name": name, "strength": s, "state_dict": sd}) + + +def _build_segment_stage_stack( + *, + fixed_lora: str, + fixed_strength: float, + var_lora1: str, + var1_strength: float, + var_lora2: str, + var2_strength: float, + cache: Dict[str, Any], +) -> list: + stack: list = [] + _append_lora(stack, fixed_lora, fixed_strength, cache) + _append_lora(stack, var_lora1, var1_strength, cache) + _append_lora(stack, var_lora2, var2_strength, cache) + return stack + + +def _apply_lora_stack_to_model(model, lora_stack: list): + if not lora_stack: + return model + + model_out = model + for entry in lora_stack: + sd = entry["state_dict"] + strength = float(entry["strength"]) + model_out, _ = comfy.sd.load_lora_for_models(model_out, None, sd, strength, 0) + return model_out + + +class IAMCCS_LTX2_LoRAStackSegmented6: + """Builds 6 LORA stacks: 3 segments × 2 stages (MODEL-only workflows).""" + + @classmethod + def INPUT_TYPES(cls): + lora_list = folder_paths.get_filename_list("loras") + ["no"] + + required: Dict[str, Any] = { + "fixed_lora": (lora_list, {"default": "no"}), + } + + # 3 segments (0..2), each has 2 stages and 2 variable loras + for seg in range(3): + required[f"seg{seg}_var_lora1"] = (lora_list, {"default": "no"}) + required[f"seg{seg}_var_lora2"] = (lora_list, {"default": "no"}) + + # fixed strength per stage + required[f"seg{seg}_fixed_strength_stage1"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + required[f"seg{seg}_fixed_strength_stage2"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + + # var strengths per stage + for i in (1, 2): + required[f"seg{seg}_var{i}_strength_stage1"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + required[f"seg{seg}_var{i}_strength_stage2"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + + return {"required": required} + + RETURN_TYPES = ("LORA", "LORA", "LORA", "LORA", "LORA", "LORA") + RETURN_NAMES = ( + "seg0_stage1_lora", + "seg0_stage2_lora", + "seg1_stage1_lora", + "seg1_stage2_lora", + "seg2_stage1_lora", + "seg2_stage2_lora", + ) + FUNCTION = "build" + CATEGORY = "IAMCCS/LoRA" + + def build(self, fixed_lora: str, **kwargs): + cache: Dict[str, Any] = {} + + out: list[list] = [] + for seg in range(3): + var1 = str(kwargs.get(f"seg{seg}_var_lora1") or "no") + var2 = str(kwargs.get(f"seg{seg}_var_lora2") or "no") + + fixed_s1 = kwargs.get(f"seg{seg}_fixed_strength_stage1", 0.0) + fixed_s2 = kwargs.get(f"seg{seg}_fixed_strength_stage2", 0.0) + + v1s1 = kwargs.get(f"seg{seg}_var1_strength_stage1", 0.0) + v1s2 = kwargs.get(f"seg{seg}_var1_strength_stage2", 0.0) + v2s1 = kwargs.get(f"seg{seg}_var2_strength_stage1", 0.0) + v2s2 = kwargs.get(f"seg{seg}_var2_strength_stage2", 0.0) + + out.append( + _build_segment_stage_stack( + fixed_lora=fixed_lora, + fixed_strength=fixed_s1, + var_lora1=var1, + var1_strength=v1s1, + var_lora2=var2, + var2_strength=v2s1, + cache=cache, + ) + ) + out.append( + _build_segment_stage_stack( + fixed_lora=fixed_lora, + fixed_strength=fixed_s2, + var_lora1=var1, + var1_strength=v1s2, + var_lora2=var2, + var2_strength=v2s2, + cache=cache, + ) + ) + + # Logging summary (compact) + total = sum(len(s) for s in out) + if total == 0: + logging.warning("[IAMCCS_LTX2_LoRAStackSegmented6] ⚠ No LoRA selected") + else: + logging.info(f"[IAMCCS_LTX2_LoRAStackSegmented6] ✅ Built 6 stacks ({total} active entries)") + + return tuple(out) + + +class IAMCCS_LTX2_ModelWithLoRA_Segmented6: + """Applies 6 stacks (3 segments × 2 stages) to a base MODEL and outputs 6 MODELs.""" + + @classmethod + def INPUT_TYPES(cls): + # Mirror config of stack node, but include base model + lora_list = folder_paths.get_filename_list("loras") + ["no"] + + required: Dict[str, Any] = { + "model": ("MODEL",), + "fixed_lora": (lora_list, {"default": "no"}), + } + + for seg in range(3): + required[f"seg{seg}_var_lora1"] = (lora_list, {"default": "no"}) + required[f"seg{seg}_var_lora2"] = (lora_list, {"default": "no"}) + + required[f"seg{seg}_fixed_strength_stage1"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + required[f"seg{seg}_fixed_strength_stage2"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + + for i in (1, 2): + required[f"seg{seg}_var{i}_strength_stage1"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + required[f"seg{seg}_var{i}_strength_stage2"] = ( + "FLOAT", + {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ) + + return {"required": required} + + RETURN_TYPES = ("MODEL", "MODEL", "MODEL", "MODEL", "MODEL", "MODEL") + RETURN_NAMES = ( + "seg0_stage1_model", + "seg0_stage2_model", + "seg1_stage1_model", + "seg1_stage2_model", + "seg2_stage1_model", + "seg2_stage2_model", + ) + FUNCTION = "apply_segmented" + CATEGORY = "IAMCCS/LoRA" + + def apply_segmented(self, model, fixed_lora: str, **kwargs): + cache: Dict[str, Any] = {} + + # Build all 6 stacks + stacks: list[list] = [] + for seg in range(3): + var1 = str(kwargs.get(f"seg{seg}_var_lora1") or "no") + var2 = str(kwargs.get(f"seg{seg}_var_lora2") or "no") + + fixed_s1 = kwargs.get(f"seg{seg}_fixed_strength_stage1", 0.0) + fixed_s2 = kwargs.get(f"seg{seg}_fixed_strength_stage2", 0.0) + + v1s1 = kwargs.get(f"seg{seg}_var1_strength_stage1", 0.0) + v1s2 = kwargs.get(f"seg{seg}_var1_strength_stage2", 0.0) + v2s1 = kwargs.get(f"seg{seg}_var2_strength_stage1", 0.0) + v2s2 = kwargs.get(f"seg{seg}_var2_strength_stage2", 0.0) + + stacks.append( + _build_segment_stage_stack( + fixed_lora=fixed_lora, + fixed_strength=fixed_s1, + var_lora1=var1, + var1_strength=v1s1, + var_lora2=var2, + var2_strength=v2s1, + cache=cache, + ) + ) + stacks.append( + _build_segment_stage_stack( + fixed_lora=fixed_lora, + fixed_strength=fixed_s2, + var_lora1=var1, + var1_strength=v1s2, + var_lora2=var2, + var2_strength=v2s2, + cache=cache, + ) + ) + + # Apply with log suppression + logger = logging.getLogger() + missing_keys_filter = SuppressLTX2MissingKeysFilter() + logger.addFilter(missing_keys_filter) + try: + models = [] + for idx, stack in enumerate(stacks): + out_model = _apply_lora_stack_to_model(model, stack) + models.append(out_model) + if stack: + names = ", ".join(f"{e['name']}({e['strength']})" for e in stack) + logging.info(f"[IAMCCS_LTX2_ModelWithLoRA_Segmented6] segStage[{idx}] -> {names}") + return tuple(models) + finally: + logger.removeFilter(missing_keys_filter) + + +NODE_CLASS_MAPPINGS = { + "IAMCCS_LTX2_LoRAStackSegmented6": IAMCCS_LTX2_LoRAStackSegmented6, + "IAMCCS_LTX2_ModelWithLoRA_Segmented6": IAMCCS_LTX2_ModelWithLoRA_Segmented6, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "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)", +} diff --git a/iamccs_ltx2_tools.py b/iamccs_ltx2_tools.py index 5584e6b..930ea2a 100644 --- a/iamccs_ltx2_tools.py +++ b/iamccs_ltx2_tools.py @@ -15,6 +15,9 @@ from typing import Tuple import torch +_F = torch.nn.functional + + _log = logging.getLogger("IAMCCS.LTX2.Tools") @@ -101,6 +104,13 @@ class IAMCCS_LTX2_Validator: "autofix": ("BOOLEAN", {"default": True}), "length_fix": (["up", "down", "nearest"], {"default": "up"}), }, + # Optional pass-through: if provided, we will make the IMAGE batch frame-count + # match the validated length (8n+1) by padding/cropping. This is the workflow-safe + # way to guarantee the VAE encode constraint without adding extra nodes. + "optional": { + "images": ("IMAGE", {}), + "images_mode": (["pad_repeat_last", "crop_end"], {"default": "pad_repeat_last"}), + }, } RETURN_TYPES = ("IMAGE", "INT", "STRING") @@ -150,6 +160,8 @@ class IAMCCS_LTX2_Validator: length: int, autofix: bool, length_fix: str, + images: torch.Tensor | None = None, + images_mode: str = "pad_repeat_last", ): width_in = int(width) height_in = int(height) @@ -178,20 +190,64 @@ class IAMCCS_LTX2_Validator: batch_fixed = max(1, batch_in) color_fixed = max(0, min(255, color_in)) - # EmptyImage-compatible IMAGE tensor: [B,H,W,3] float in [0,1] - fill = float(color_fixed) / 255.0 - img = torch.full( - (batch_fixed, int(height_fixed), int(width_fixed), 3), - fill, - dtype=torch.float32, - device="cpu", - ) + def _pad_repeat_last_frames(x: torch.Tensor, pad: int) -> torch.Tensor: + if pad <= 0: + return x + last = x[-1:, ...].repeat(int(pad), 1, 1, 1) + return torch.cat([x, last], dim=0) + + # If an IMAGE batch is provided, enforce that its frames match the validated length. + # This is the actual guarantee needed by LTX VAE encode. + if images is not None: + img_in = images + if not torch.is_floating_point(img_in): + img_in = img_in.float() / 255.0 + # Expect ComfyUI IMAGE: [frames,H,W,C] + if img_in.ndim != 4: + raise ValueError("images must be a ComfyUI IMAGE tensor with shape [frames,H,W,C]") + + frames_in = int(img_in.shape[0]) + frames_out = int(length_fixed) + if not autofix: + frames_out = frames_in + + mode = str(images_mode or "pad_repeat_last") + if mode not in ("pad_repeat_last", "crop_end"): + mode = "pad_repeat_last" + + if frames_out == frames_in: + img = img_in + elif frames_out < frames_in: + # Crop end to match target length + img = img_in[:frames_out, ...] + else: + # Pad by repeating last frame + img = _pad_repeat_last_frames(img_in, frames_out - frames_in) + + # Note: we do NOT resize spatially here; this node's width/height validation is + # meant for parameter sanity and EmptyImage-like generation. Spatial resizing remains + # the responsibility of the workflow. + else: + # EmptyImage-compatible IMAGE tensor: [B,H,W,3] float in [0,1] + fill = float(color_fixed) / 255.0 + img = torch.full( + (batch_fixed, int(height_fixed), int(width_fixed), 3), + fill, + dtype=torch.float32, + device="cpu", + ) modified = (width_fixed != width_in) or (height_fixed != height_in) or (length_fixed != length_in) or (batch_fixed != batch_in) or (color_fixed != color_in) + if images is not None and isinstance(images, torch.Tensor) and images.ndim == 4: + modified = modified or (int(images.shape[0]) != int(img.shape[0])) implied_fps_str = "n/a" if seconds_in > 0: implied_fps = (float(length_fixed) - 1.0) / seconds_in implied_fps_str = f"{implied_fps:.3f}" + images_note = "" + if images is not None and isinstance(images, torch.Tensor) and images.ndim == 4: + images_note = f" | images_frames: in={int(images.shape[0])} -> out={int(img.shape[0])} (mode={images_mode}, autofix={autofix})" + report = ( f"input: {width_in}x{height_in}, batch={batch_in}, color={color_in} | " f"seconds_input={seconds_in:.3f}, length_input={length_in} | " @@ -200,6 +256,7 @@ class IAMCCS_LTX2_Validator: f"implied_fps={implied_fps_str} | " f"autofix={autofix} (len_fix={length_fix}) | modified={modified} | " f"delta: +{pad_w}w, +{pad_h}h, {pad_len:+d} length" + f"{images_note}" ) if not (w_ok and h_ok and l_ok): @@ -229,6 +286,13 @@ class IAMCCS_LTX2_TimeFrameCount: seconds_in = float(seconds) length_in = int(length) length_fixed = max(1, length_in) + # LTX-2 encode constraint: frames must be 8n + 1. + # Round UP to avoid shortening the requested duration. + pad = 0 + rem = (length_fixed - 1) % 8 + if rem != 0: + pad = 8 - rem + length_fixed = length_fixed + pad seconds_fixed = max(0.01, seconds_in) implied_fps_str = "n/a" @@ -236,14 +300,176 @@ class IAMCCS_LTX2_TimeFrameCount: implied_fps = (float(length_fixed) - 1.0) / seconds_fixed implied_fps_str = f"{implied_fps:.3f}" + snap = "ok" if pad == 0 else f"up(+{pad})" report = ( f"seconds_input={seconds_in:.3f}, length_input={length_in} -> " - f"seconds={seconds_fixed:.3f}, length={length_fixed} | implied_fps={implied_fps_str}" + f"seconds={seconds_fixed:.3f}, length={length_fixed} | implied_fps={implied_fps_str} | ltx2_8n+1={snap}" ) return (int(length_fixed), float(seconds_fixed), report) +class IAMCCS_LTX2_ImageBatchPadReflect: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE", {}), + "pad_x": ("INT", {"default": 16, "min": 0, "max": 512, "step": 1}), + "pad_y": ("INT", {"default": 16, "min": 0, "max": 512, "step": 1}), + "pad_mode": (["reflect", "replicate"], {"default": "reflect"}), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "INT", "STRING") + RETURN_NAMES = ("images", "pad_x", "pad_y", "report") + FUNCTION = "pad" + CATEGORY = "IAMCCS/LTX-2" + + def pad(self, images: torch.Tensor, pad_x: int, pad_y: int, pad_mode: str): + # images: [B,H,W,C] float + if images is None: + raise ValueError("images is required") + + b, h, w, c = images.shape + px = max(0, int(pad_x)) + py = max(0, int(pad_y)) + + # reflect requires pad < dim; clamp to avoid runtime errors + px_eff = min(px, max(0, w - 1)) + py_eff = min(py, max(0, h - 1)) + + if px_eff == 0 and py_eff == 0: + return (images, 0, 0, f"PadReflect: no-op (input {w}x{h})") + + mode = str(pad_mode or "reflect") + if mode not in ("reflect", "replicate"): + mode = "reflect" + + x = images.permute(0, 3, 1, 2) # [B,C,H,W] + # pad format: (left, right, top, bottom) + x = _F.pad(x, (px_eff, px_eff, py_eff, py_eff), mode=mode) + out = x.permute(0, 2, 3, 1).contiguous() + + report = ( + f"PadReflect: mode={mode}, requested=({px},{py}), used=({px_eff},{py_eff}) | " + f"{w}x{h} -> {int(out.shape[2])}x{int(out.shape[1])}" + ) + return (out, int(px_eff), int(py_eff), report) + + +class IAMCCS_LTX2_ImageBatchCropByPad: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE", {}), + "pad_x": ("INT", {"default": 16, "min": 0, "max": 512, "step": 1}), + "pad_y": ("INT", {"default": 16, "min": 0, "max": 512, "step": 1}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("images", "report") + FUNCTION = "crop" + CATEGORY = "IAMCCS/LTX-2" + + def crop(self, images: torch.Tensor, pad_x: int, pad_y: int): + if images is None: + raise ValueError("images is required") + + b, h, w, c = images.shape + px = max(0, int(pad_x)) + py = max(0, int(pad_y)) + + if px == 0 and py == 0: + return (images, f"CropByPad: no-op (input {w}x{h})") + + # Clamp so we never invert the crop + px_eff = min(px, max(0, (w - 1) // 2)) + py_eff = min(py, max(0, (h - 1) // 2)) + + out = images[:, py_eff : h - py_eff, px_eff : w - px_eff, :] + report = ( + f"CropByPad: requested=({px},{py}), used=({px_eff},{py_eff}) | " + f"{w}x{h} -> {int(out.shape[2])}x{int(out.shape[1])}" + ) + return (out, report) + + +class IAMCCS_LTX2_EnsureFrames8nPlus1: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE", {}), + # LTX-2 encode constraint: frames must be 1 + 8*k. + # "pad" is workflow-safe (never shortens), "crop" is deterministic. + "mode": (["pad_repeat_last", "crop_end"], {"default": "pad_repeat_last"}), + "fix": (["up", "down", "nearest"], {"default": "up"}), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "STRING") + RETURN_NAMES = ("images", "frames", "report") + FUNCTION = "ensure" + CATEGORY = "IAMCCS/LTX-2" + + def _frames_rule_fix(self, frames: int, mode: str) -> tuple[int, int]: + frames = int(frames) + if frames < 1: + frames = 1 + + rem = (frames - 1) % 8 + if rem == 0: + return frames, 0 + + down = frames - rem + up = frames + (8 - rem) + + if mode == "down": + fixed = max(1, down) + elif mode == "nearest": + fixed = up if (up - frames) <= (frames - down) else max(1, down) + else: + fixed = up + + return fixed, fixed - frames + + def ensure(self, images: torch.Tensor, mode: str, fix: str): + if images is None: + raise ValueError("images is required") + + if not isinstance(images, torch.Tensor) or images.ndim != 4: + raise ValueError("images must be a ComfyUI IMAGE tensor with shape [frames,H,W,C]") + + frames_in = int(images.shape[0]) + frames_fixed, delta = self._frames_rule_fix(frames_in, str(fix or "up")) + + if frames_fixed == frames_in: + return (images, frames_in, f"EnsureFrames8n+1: ok ({frames_in})") + + mode = str(mode or "pad_repeat_last") + if mode == "crop_end": + # For crop mode, prefer shortening, regardless of requested 'fix'. + rem = (frames_in - 1) % 8 + frames_fixed = frames_in if rem == 0 else max(1, frames_in - rem) + out = images[:frames_fixed, ...] + return (out, frames_fixed, f"EnsureFrames8n+1: crop_end {frames_in} -> {frames_fixed}") + + # pad_repeat_last (workflow-safe) + if frames_fixed < frames_in: + # If user selected fix=down/nearest and it resulted in fewer frames, + # still keep behavior consistent with padding node: do a crop. + out = images[:frames_fixed, ...] + return (out, frames_fixed, f"EnsureFrames8n+1: crop_end {frames_in} -> {frames_fixed} (fix={fix})") + + pad = int(frames_fixed - frames_in) + last = images[-1:, ...].repeat(pad, 1, 1, 1) + out = torch.cat([images, last], dim=0) + return (out, frames_fixed, f"EnsureFrames8n+1: pad_repeat_last {frames_in} -> {frames_fixed} (pad={pad}, fix={fix})") + + class IAMCCS_LTX2_ControlPreprocess: @classmethod def INPUT_TYPES(cls): @@ -341,6 +567,7 @@ NODE_CLASS_MAPPINGS = { "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_ControlPreprocess": IAMCCS_LTX2_ControlPreprocess, } @@ -348,5 +575,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "IAMCCS_LTX2_FrameRateSync": "LTX-2 FrameRate Sync (int+float)", "IAMCCS_LTX2_Validator": "LTX-2 Validator (32px, 8n +1)", "IAMCCS_LTX2_TimeFrameCount": "LTX-2 TimeFrameCount", + "IAMCCS_LTX2_EnsureFrames8nPlus1": "LTX-2 Ensure Frames (8n + 1)", "IAMCCS_LTX2_ControlPreprocess": "LTX-2 Control Preprocess (aux)", } diff --git a/pyproject.toml b/pyproject.toml index a518d9e..0a15e6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "iamccs-wan-lora-fixer-stack" -version = "1.3.2" -description = "IAMCCS multi-LoRA stack & MODEL IO WAN remap + LTX-2 LoRA/tools nodes (FrameRate Sync, Validator, Control Preprocess, LTX-2 LoRA stacks)." +version = "1.3.3" +description = "IAMCCS nodes for ComfyUI: multi-LoRA stack + MODEL IO WAN/Flow remap + LTX-2 LoRA/tools + LTX-2 Extension Module + AutoLink Set/Get + Converter (frontend)." license = { text = "MIT" } authors = [ { name = "Carmine Cristallo Scalzi (IAMCCS)", email = "info@carminecristalloscalzi.com" } diff --git a/version.json b/version.json index 44bc625..e3be690 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "name": "iamccs-wan-lora-fixer-stack", - "version": "1.3.2", + "version": "1.3.3", "author": "Carmine Cristallo Scalzi (IAMCCS)", - "description": "IAMCCS nodes for ComfyUI: multi-LoRA stack + direct MODEL IO WAN/Flow remap + LTX-2 LoRA & utility nodes (FrameRate Sync, Validator, Control Preprocess, LTX-2 LoRA stacks). WAN 2.1 + 2.2 compatible." + "description": "IAMCCS nodes for ComfyUI: multi-LoRA stack + direct MODEL IO WAN/Flow remap + LTX-2 LoRA & utility nodes + LTX-2 Extension Module + AutoLink (Set/Get + Converter, frontend). WAN 2.1 + 2.2 compatible." } diff --git a/web/iamccs_autolink_converter.js b/web/iamccs_autolink_converter.js new file mode 100644 index 0000000..bb6b601 --- /dev/null +++ b/web/iamccs_autolink_converter.js @@ -0,0 +1,2795 @@ +// IAMCCS AutoLink - Based on KJ nodes SetNode/GetNode +// Nodi puramente frontend - non eseguono niente lato Python + +import { app } from "../../scripts/app.js"; + +const SET_TYPE = "IAMCCS_SetAutoLink"; +const GET_TYPE = "IAMCCS_GetAutoLink"; +const CONVERTER_TYPE = "IAMCCS_AutoLinkConverter"; +const ARGUMENTS_TYPE = "IAMCCS_AutoLinkArguments"; +const KJ_SET_TYPE = "SetNode"; +const KJ_GET_TYPE = "GetNode"; + +console.log("[IAMCCS AutoLink] Loading extension..."); + +function alphaSort(values) { + return [...values].sort((a, b) => String(a).localeCompare(String(b), undefined, { sensitivity: 'base' })); +} + +function getWidget(node, widgetName) { + if (!node?.widgets?.length) return null; + return node.widgets.find(w => w?.name === widgetName || w?.label === widgetName) || null; +} + +function getWidgetValue(node, widgetName) { + return getWidget(node, widgetName)?.value; +} + +function isValidAutolinkKey(value) { + const s = String(value ?? "").trim(); + return !!s && s !== "*"; +} + +function getAutolinkKey(node) { + const v = getWidgetValue(node, "name"); + if (isValidAutolinkKey(v)) return String(v).trim(); + const outName = node?.outputs?.[0]?.name; + if (isValidAutolinkKey(outName)) return String(outName).trim(); + const inName = node?.inputs?.[0]?.name; + if (isValidAutolinkKey(inName)) return String(inName).trim(); + return ""; +} + +function setAutolinkKeyAndTitle(node, key) { + const safeKey = String(key ?? "").trim(); + if (!node || !isValidAutolinkKey(safeKey)) return; + + setWidgetValue(node, "name", safeKey); + node.title = safeKey; + + if (node.type === SET_TYPE) { + if (node.inputs?.[0]) node.inputs[0].name = safeKey; + if (node.outputs?.[0]) node.outputs[0].name = safeKey; + } + + const nameWidget = getWidget(node, "name"); + if (nameWidget) nameWidget.lastValue = safeKey; +} + +function normalizeAutolinkIOSlots(graph, node, { wantInputs = 0, wantOutputs = 0 } = {}) { + if (!node) return; + + // Ensure arrays exist + if (!node.inputs) node.inputs = []; + if (!node.outputs) node.outputs = []; + + // Ensure at least one slot exists when requested + if (wantInputs > 0 && node.inputs.length === 0 && typeof node.addInput === "function") { + node.addInput("*", "*"); + } + if (wantOutputs > 0 && node.outputs.length === 0 && typeof node.addOutput === "function") { + node.addOutput("*", "*"); + } + + // If multiple inputs got serialized (buggy old workflows), try to move any link onto slot 0 + if (wantInputs > 0 && node.inputs.length > 1) { + try { + const in0HasLink = !!node.inputs?.[0]?.link; + if (!in0HasLink) { + const idx = node.inputs.findIndex((inp, i) => i > 0 && inp?.link != null); + if (idx > 0) { + const linkId = node.inputs[idx].link; + const link = graph?.links?.[linkId]; + if (link) { + try { node.disconnectInput?.(idx); } catch {} + const src = graph?.getNodeById?.(link.origin_id); + if (src) { + try { + // Reconnect to slot 0 + src.connect(link.origin_slot, node, 0); + } catch {} + } + } + } + } + } catch {} + + // Remove extra input slots (keep only slot 0) + try { + while (node.inputs.length > 1) { + if (typeof node.removeInput === "function") node.removeInput(1); + else node.inputs.splice(1, 1); + } + } catch {} + } + + // Same for outputs: if multiple outputs exist, try to migrate links to slot 0 + if (wantOutputs > 0 && node.outputs.length > 1) { + try { + const out0Links = node.outputs?.[0]?.links; + const out0HasLinks = Array.isArray(out0Links) && out0Links.length > 0; + if (!out0HasLinks) { + for (let i = 1; i < node.outputs.length; i++) { + const links = node.outputs?.[i]?.links; + if (!Array.isArray(links) || links.length === 0) continue; + for (const linkId of [...links]) { + const link = graph?.links?.[linkId]; + if (!link) continue; + const dst = graph?.getNodeById?.(link.target_id); + if (!dst) continue; + const ts = link.target_slot; + try { dst.disconnectInput?.(ts); } catch {} + try { node.connect(0, dst, ts); } catch {} + } + } + } + } catch {} + + // Remove extra output slots (keep only slot 0) + try { + while (node.outputs.length > 1) { + if (typeof node.removeOutput === "function") node.removeOutput(1); + else node.outputs.splice(1, 1); + } + } catch {} + } +} + +function makeUniqueAutolinkSetName(graph, desired) { + const raw = String(desired ?? "").trim(); + let base = raw; + + // sanitize + base = base.replace(/\s+/g, "_"); + base = base.replace(/^\*+|\*+$/g, ""); + base = base.trim(); + if (!isValidAutolinkKey(base)) base = "output"; + + // prefer lower-case keys for readability + base = String(base).trim(); + const baseLower = base.toLowerCase(); + + const used = new Set( + (graph?._nodes || []) + .filter(n => n?.type === SET_TYPE) + .map(n => getAutolinkKey(n)) + .filter(v => isValidAutolinkKey(v)) + ); + + // If already has _N suffix, keep it unless it collides. + const m = baseLower.match(/^(.+?)_(\d+)$/); + if (m) { + const stem = m[1]; + let n = parseInt(m[2], 10); + let candidate = `${stem}_${n}`; + while (used.has(candidate)) { + n++; + candidate = `${stem}_${n}`; + } + return candidate; + } + + // Always add _0, _1, ... (matches convertAllLinks behavior) + let n = 0; + let candidate = `${baseLower}_${n}`; + while (used.has(candidate)) { + n++; + candidate = `${baseLower}_${n}`; + } + return candidate; +} + +function parseHexColor(hex) { + if (!hex || typeof hex !== 'string') return null; + const s = hex.trim(); + if (!s.startsWith('#')) return null; + const h = s.slice(1); + if (h.length === 3) { + const r = parseInt(h[0] + h[0], 16); + const g = parseInt(h[1] + h[1], 16); + const b = parseInt(h[2] + h[2], 16); + if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null; + return { r, g, b }; + } + if (h.length === 6) { + const r = parseInt(h.slice(0, 2), 16); + const g = parseInt(h.slice(2, 4), 16); + const b = parseInt(h.slice(4, 6), 16); + if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null; + return { r, g, b }; + } + return null; +} + +function getTitleTextColor(mode, bgcolorHex) { + const m = String(mode || 'White'); + if (m === 'Black') return '#000000'; + if (m === 'White') return '#ffffff'; + // Auto + const rgb = parseHexColor(bgcolorHex); + if (!rgb) return '#ffffff'; + // relative luminance-ish (sufficient for UI contrast) + const lum = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255; + return lum > 0.55 ? '#000000' : '#ffffff'; +} + +function applyNodeTitleTextColor(node, mode) { + if (!node) return; + const color = getTitleTextColor(mode, node.bgcolor); + // LiteGraph variants (best-effort) + node.title_text_color = color; + node.titleTextColor = color; + // Some variants read textcolor for title text too + node.textcolor = color; + node.textColor = color; + node.properties = node.properties || {}; + node.properties.autolink_title_color = color; +} + +let AUTOLINK_TITLE_COLOR_HOOK_INSTALLED = false; +function installAutolinkTitleColorCanvasHook() { + if (AUTOLINK_TITLE_COLOR_HOOK_INSTALLED) return; + try { + const LGraphCanvas = window?.LGraphCanvas; + const LiteGraph = window?.LiteGraph; + if (!LGraphCanvas?.prototype?.drawNode || !LiteGraph) return; + + const originalDrawNode = LGraphCanvas.prototype.drawNode; + if (originalDrawNode?.__iamccs_autolink_title_hook) { + AUTOLINK_TITLE_COLOR_HOOK_INSTALLED = true; + return; + } + + function wrappedDrawNode(node, ctx) { + const isAutolink = node && (node.type === SET_TYPE || node.type === GET_TYPE); + if (!isAutolink) return originalDrawNode.apply(this, arguments); + + const desired = node?.properties?.autolink_title_color || node?.title_text_color || node?.textcolor; + if (!desired) return originalDrawNode.apply(this, arguments); + + // Some ComfyUI/LiteGraph builds force title text to white on "dark" nodes. + // To respect ColorTitles (White/Black/Auto) without drawing a second title, + // we temporarily wrap ctx.fillText and override fillStyle only for the title draw call. + const prevFillText = ctx?.fillText; + let titleA = ""; + let titleB = ""; + try { + const rawTitle = (typeof node?.getTitle === "function") ? node.getTitle() : node?.title; + titleA = String(rawTitle ?? ""); + titleB = String(node?.title ?? ""); + if (node?.pinned) { + if (titleA) titleA += "📌"; + if (titleB) titleB += "📌"; + } + } catch { + // ignore + } + + if (typeof prevFillText === "function") { + ctx.fillText = function(text, x, y, maxWidth) { + try { + const t = String(text ?? ""); + const isTitle = (t && (t === titleA || t === titleB || (titleA && titleA.startsWith(t)) || (titleB && titleB.startsWith(t)))); + // title is drawn in the title bar area (usually negative y) + if (isTitle && typeof y === "number" && y < 0) { + const prevStyle = ctx.fillStyle; + ctx.fillStyle = desired; + try { + return prevFillText.apply(this, arguments); + } finally { + ctx.fillStyle = prevStyle; + } + } + } catch { + // ignore + } + return prevFillText.apply(this, arguments); + }; + } + + const prevNodeTitleColor = LiteGraph.NODE_TITLE_COLOR; + const prevSelectedTitleColor = LiteGraph.NODE_SELECTED_TITLE_COLOR; + const prevNodeTextColor = LiteGraph.NODE_TEXT_COLOR; + const prevSelectedTextColor = LiteGraph.NODE_SELECTED_TEXT_COLOR; + if (typeof LiteGraph.NODE_TITLE_COLOR !== "undefined") LiteGraph.NODE_TITLE_COLOR = desired; + if (typeof LiteGraph.NODE_SELECTED_TITLE_COLOR !== "undefined") LiteGraph.NODE_SELECTED_TITLE_COLOR = desired; + if (typeof LiteGraph.NODE_TEXT_COLOR !== "undefined") LiteGraph.NODE_TEXT_COLOR = desired; + if (typeof LiteGraph.NODE_SELECTED_TEXT_COLOR !== "undefined") LiteGraph.NODE_SELECTED_TEXT_COLOR = desired; + + try { + return originalDrawNode.apply(this, arguments); + } finally { + if (typeof prevFillText === "function") ctx.fillText = prevFillText; + if (typeof prevNodeTitleColor !== "undefined") LiteGraph.NODE_TITLE_COLOR = prevNodeTitleColor; + if (typeof prevSelectedTitleColor !== "undefined") LiteGraph.NODE_SELECTED_TITLE_COLOR = prevSelectedTitleColor; + if (typeof prevNodeTextColor !== "undefined") LiteGraph.NODE_TEXT_COLOR = prevNodeTextColor; + if (typeof prevSelectedTextColor !== "undefined") LiteGraph.NODE_SELECTED_TEXT_COLOR = prevSelectedTextColor; + } + } + wrappedDrawNode.__iamccs_autolink_title_hook = true; + LGraphCanvas.prototype.drawNode = wrappedDrawNode; + AUTOLINK_TITLE_COLOR_HOOK_INSTALLED = true; + } catch (e) { + console.warn("[IAMCCS AutoLink] Failed to install title color hook", e); + } +} + +function getCurrentArgumentsNode() { + try { + return app?.graph?._nodes?.find(n => n?.type === ARGUMENTS_TYPE) || null; + } catch { + return null; + } +} + +function getCurrentColorTitlesMode() { + const argNode = getCurrentArgumentsNode(); + return argNode?.properties?.color_titles || "White"; +} + +const AUTOLINK_COLORS = { + Gray: { set: { color: "#1f1f1f", bgcolor: "#3a3a3a" }, get: { color: "#2a2a2a", bgcolor: "#555555" } }, + Blue: { set: { color: "#1b4669", bgcolor: "#29699c" }, get: { color: "#234f73", bgcolor: "#347cb8" } }, + Green: { set: { color: "#1f5a3a", bgcolor: "#2d7d52" }, get: { color: "#2a6b46", bgcolor: "#3aa66c" } }, + Red: { set: { color: "#6a1b1b", bgcolor: "#9c2929" }, get: { color: "#7a2323", bgcolor: "#b83434" } }, + Orange: { set: { color: "#6b3e1a", bgcolor: "#9c5b29" }, get: { color: "#7a4a22", bgcolor: "#b86c34" } }, + Purple: { set: { color: "#3f1b69", bgcolor: "#5e299c" }, get: { color: "#4a237a", bgcolor: "#7034b8" } }, + Yellow: { set: { color: "#6b651a", bgcolor: "#9c9229" }, get: { color: "#7a7422", bgcolor: "#b8aa34" } }, + Teal: { set: { color: "#1b6961", bgcolor: "#299c90" }, get: { color: "#237a71", bgcolor: "#34b8aa" } }, + Pink: { set: { color: "#691b46", bgcolor: "#9c2969" }, get: { color: "#7a2351", bgcolor: "#b8347c" } }, +}; + +function getAutolinkColorPreset(colorName, role, separateCol, colorGetName) { + const safeRole = role === 'get' ? 'get' : 'set'; + const base = AUTOLINK_COLORS[colorName] || AUTOLINK_COLORS.Gray; + if (!separateCol) return base.set; // stessa identità colore per set/get + + if (safeRole === 'get' && colorGetName && AUTOLINK_COLORS[colorGetName]) { + return AUTOLINK_COLORS[colorGetName].get; + } + + return base[safeRole]; +} + +function applyNodeColors(node, preset) { + if (!node || !preset) return; + node.color = preset.color; + node.bgcolor = preset.bgcolor; +} + +function recolorExistingAutoLinks(graph, colorSetName = "Gray", separateCol = false, colorGetName = "Gray", colorTitles = "White") { + if (!graph) return; + const sets = graph._nodes.filter(n => n?.type === SET_TYPE); + const gets = graph._nodes.filter(n => n?.type === GET_TYPE); + + for (const setNode of sets) { + // Migrazione: vecchi workflow (prima del widget colore) possono aver scritto il nome nel widget colore. + const maybeName = getWidgetValue(setNode, "name"); + const maybeColor = getWidgetValue(setNode, "AutoLinkColor"); + if ((!maybeName || !String(maybeName).trim()) && maybeColor && !AUTOLINK_COLORS[maybeColor]) { + const migrated = String(maybeColor).trim(); + setAutolinkKeyAndTitle(setNode, migrated); + setWidgetValue(setNode, "AutoLinkColor", "Gray"); + setNode.properties = setNode.properties || {}; + setNode.properties.autolink_color_name = "Gray"; + } + + const key = getAutolinkKey(setNode); + + setNode.properties = setNode.properties || {}; + if (setNode.properties.autolink_color_locked === undefined) { + setNode.properties.autolink_color_locked = false; + } + + const locked = !!setNode.properties.autolink_color_locked; + const chosen = locked + ? (setNode.properties.autolink_color_name || colorSetName) + : colorSetName; + + // mantieni la sorgente di verità sempre in autolink_color_name + setNode.properties.autolink_color_name = chosen; + applyNodeColors(setNode, getAutolinkColorPreset(chosen, 'set', separateCol, colorGetName)); + applyNodeTitleTextColor(setNode, colorTitles); + } + + for (const getNode of gets) { + const key = getAutolinkKey(getNode); + const chosen = key + ? (graph._nodes.find(n => n?.type === SET_TYPE && getAutolinkKey(n) === key)?.properties?.autolink_color_name || colorSetName) + : colorSetName; + applyNodeColors(getNode, getAutolinkColorPreset(chosen, 'get', separateCol, colorGetName)); + getNode.properties = getNode.properties || {}; + getNode.properties.autolink_color_name = chosen; + applyNodeTitleTextColor(getNode, colorTitles); + } + + graph.setDirtyCanvas(true, true); +} + +function applyColorToAutolinkKey(graph, key, colorName, separateCol = false, colorGetName = "Gray", colorTitles = "White") { + if (!graph || !key) return; + const safeKey = String(key).trim(); + const sets = graph._nodes.filter(n => n?.type === SET_TYPE && getAutolinkKey(n) === safeKey); + const gets = graph._nodes.filter(n => n?.type === GET_TYPE && getAutolinkKey(n) === safeKey); + + for (const setNode of sets) { + setNode.properties = setNode.properties || {}; + setNode.properties.autolink_color_name = colorName; + setNode.properties.autolink_color_locked = true; + applyNodeColors(setNode, getAutolinkColorPreset(colorName, 'set', separateCol, colorGetName)); + applyNodeTitleTextColor(setNode, colorTitles); + } + for (const getNode of gets) { + getNode.properties = getNode.properties || {}; + getNode.properties.autolink_color_name = colorName; + applyNodeColors(getNode, getAutolinkColorPreset(colorName, 'get', separateCol, colorGetName)); + applyNodeTitleTextColor(getNode, colorTitles); + } + + graph.setDirtyCanvas(true, true); +} + +// === VISUAL FLOW TRACER === +let flowTracerEnabled = false; +let flowCanvas = null; +let animationFrameId = null; +let activeFlows = []; + +function toggleFlowTracer(enabled) { + flowTracerEnabled = enabled; + + if (enabled && !flowCanvas) { + flowCanvas = document.createElement('canvas'); + flowCanvas.id = 'autolink-flow-overlay'; + flowCanvas.style.position = 'absolute'; + flowCanvas.style.top = '0'; + flowCanvas.style.left = '0'; + flowCanvas.style.pointerEvents = 'none'; + flowCanvas.style.zIndex = '999'; + document.body.appendChild(flowCanvas); + + const resizeCanvas = () => { + flowCanvas.width = window.innerWidth; + flowCanvas.height = window.innerHeight; + }; + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + startFlowAnimation(); + console.log("[IAMCCS AutoLink] ✨ Flow Tracer enabled"); + } else if (!enabled && flowCanvas) { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + animationFrameId = null; + } + flowCanvas.remove(); + flowCanvas = null; + activeFlows = []; + console.log("[IAMCCS AutoLink] Flow Tracer disabled"); + } +} + +function startFlowAnimation() { + if (!flowTracerEnabled || !flowCanvas) return; + + const ctx = flowCanvas.getContext('2d'); + ctx.clearRect(0, 0, flowCanvas.width, flowCanvas.height); + + // Trova tutte le coppie Set-Get attive + activeFlows = []; + const setNodes = app.graph._nodes.filter(n => n.type === SET_TYPE); + const getNodes = app.graph._nodes.filter(n => n.type === GET_TYPE); + + setNodes.forEach(setNode => { + const setName = getAutolinkKey(setNode); + if (!setName) return; + + const matchingGets = getNodes.filter(g => getAutolinkKey(g) === setName); + matchingGets.forEach(getNode => { + const dataType = setNode.outputs[0]?.type || '*'; + activeFlows.push({ + setNode: setNode, + getNode: getNode, + dataType: dataType, + particles: initParticles(5) + }); + }); + }); + + animateFlows(ctx); +} + +function initParticles(count) { + const particles = []; + for (let i = 0; i < count; i++) { + particles.push({ + progress: Math.random(), + speed: 0.005 + Math.random() * 0.01, + size: 3 + Math.random() * 3 + }); + } + return particles; +} + +function animateFlows(ctx) { + if (!flowTracerEnabled || !flowCanvas) return; + + ctx.clearRect(0, 0, flowCanvas.width, flowCanvas.height); + + activeFlows.forEach(flow => { + const setPos = getNodeScreenPosition(flow.setNode); + const getPos = getNodeScreenPosition(flow.getNode); + const color = getTypeColor(flow.dataType); + + // Disegna linea di connessione + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.globalAlpha = 0.3; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(setPos.x, setPos.y); + ctx.lineTo(getPos.x, getPos.y); + ctx.stroke(); + ctx.setLineDash([]); + + // Anima particelle + flow.particles.forEach(particle => { + particle.progress += particle.speed; + if (particle.progress > 1) particle.progress = 0; + + const x = setPos.x + (getPos.x - setPos.x) * particle.progress; + const y = setPos.y + (getPos.y - setPos.y) * particle.progress; + + ctx.globalAlpha = 0.8; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(x, y, particle.size, 0, Math.PI * 2); + ctx.fill(); + }); + }); + + ctx.globalAlpha = 1.0; + animationFrameId = requestAnimationFrame(() => animateFlows(ctx)); +} + +function getNodeScreenPosition(node) { + const canvasElement = document.querySelector('.litegraph'); + if (!canvasElement || !app.canvas) { + return { x: 0, y: 0 }; + } + + const rect = canvasElement.getBoundingClientRect(); + const scale = app.canvas.ds.scale; + const offset = app.canvas.ds.offset; + + const x = rect.left + (node.pos[0] + node.size[0] / 2) * scale + offset[0] * scale; + const y = rect.top + (node.pos[1] + node.size[1] / 2) * scale + offset[1] * scale; + + return { x, y }; +} + +function getTypeColor(dataType) { + const colors = { + 'MODEL': '#4a9eff', + 'VAE': '#ff4a9e', + 'CLIP': '#4aff9e', + 'IMAGE': '#ffff4a', + 'LATENT': '#ff9e4a', + 'CONDITIONING': '#9e4aff', + 'MASK': '#ff6b6b', + 'FLOAT': '#6bffa3', + 'INT': '#a3c6ff' + }; + return colors[dataType] || '#ffffff'; +} + +function _iamccsFixLinkIntegrity(graph) { + try { + if (!graph || !graph.links) return; + + // 1) Ensure each graph.links entry is reflected in origin.outputs[*].links and target.inputs[*].link + for (const [idStr, link] of Object.entries(graph.links)) { + const linkId = Number(idStr); + if (!link || !Number.isFinite(linkId)) continue; + + const origin = graph.getNodeById?.(link.origin_id); + const target = graph.getNodeById?.(link.target_id); + const os = Number(link.origin_slot); + const ts = Number(link.target_slot); + if (!origin || !target || !Number.isFinite(os) || !Number.isFinite(ts)) continue; + + // Origin + try { + origin.outputs = origin.outputs || []; + const out = origin.outputs[os]; + if (out) { + if (!Array.isArray(out.links)) out.links = []; + if (!out.links.includes(linkId)) out.links.push(linkId); + } + } catch {} + + // Target + try { + target.inputs = target.inputs || []; + const inp = target.inputs[ts]; + if (inp) { + // Never overwrite an existing target link: if this link isn't the one + // referenced by the input, it's an orphan/duplicate and should be removed. + if (inp.link == null) { + inp.link = linkId; + } else if (Number(inp.link) !== linkId) { + try { + // Remove orphan link from origin output list + const o2 = graph.getNodeById?.(link.origin_id); + const os2 = Number(link.origin_slot); + if (o2?.outputs?.[os2]?.links && Array.isArray(o2.outputs[os2].links)) { + o2.outputs[os2].links = o2.outputs[os2].links.filter(x => Number(x) !== linkId); + } + } catch {} + try { delete graph.links[linkId]; } catch {} + continue; + } + } + } catch {} + } + + // 2) Remove stale link IDs from outputs[*].links that don't exist in graph.links + const existing = new Set(Object.keys(graph.links).map(k => Number(k)).filter(Number.isFinite)); + for (const node of graph._nodes || []) { + if (!node?.outputs?.length) continue; + for (const out of node.outputs) { + if (!out || !Array.isArray(out.links) || out.links.length === 0) continue; + out.links = out.links.filter(id => existing.has(Number(id))); + } + } + } catch (e) { + console.warn("[IAMCCS AutoLink] Link integrity fix failed", e); + } +} + +function _iamccsForceRemoveLink(graph, linkId) { + if (!graph || !graph.links || linkId == null) return; + const id = Number(linkId); + if (!Number.isFinite(id)) return; + const link = graph.links?.[id]; + if (!link) return; + + // Remove from origin output links array if possible + try { + const origin = graph.getNodeById?.(link.origin_id); + const os = Number(link.origin_slot); + if (origin?.outputs?.[os]?.links && Array.isArray(origin.outputs[os].links)) { + origin.outputs[os].links = origin.outputs[os].links.filter(x => Number(x) !== id); + } + } catch {} + + // Remove from target input pointer if it points to this link + try { + const target = graph.getNodeById?.(link.target_id); + const ts = Number(link.target_slot); + if (target?.inputs?.[ts] && Number(target.inputs[ts].link) === id) { + target.inputs[ts].link = null; + } + } catch {} + + try { + // Prefer native removal if present + if (typeof graph.removeLink === "function") { + graph.removeLink(id); + return; + } + } catch {} + + try { delete graph.links[id]; } catch {} +} + +function _iamccsDisconnectTargetInput(graph, dstNode, ts) { + try { + const slot = Number(ts); + if (!Number.isFinite(slot) || !dstNode) return; + const prevId = dstNode?.inputs?.[slot]?.link; + + try { dstNode.disconnectInput?.(slot); } catch {} + + const still = dstNode?.inputs?.[slot]?.link; + if (still != null) { + // If disconnect failed, force-remove whatever link the input points to + _iamccsForceRemoveLink(graph, still); + try { + if (dstNode?.inputs?.[slot]) dstNode.inputs[slot].link = null; + } catch {} + } else if (prevId != null) { + // Some implementations clear input.link but keep graph.links/origin.outputs stale + _iamccsForceRemoveLink(graph, prevId); + } + } catch {} +} + +function _iamccsRemoveOtherLinksToTarget(graph, targetId, targetSlot, keepLinkId) { + try { + const tid = Number(targetId); + const ts = Number(targetSlot); + const keep = keepLinkId != null ? Number(keepLinkId) : null; + if (!graph?.links || !Number.isFinite(tid) || !Number.isFinite(ts)) return; + + for (const [idStr, link] of Object.entries(graph.links)) { + const id = Number(idStr); + if (!Number.isFinite(id) || !link) continue; + if (keep != null && id === keep) continue; + if (Number(link.target_id) === tid && Number(link.target_slot) === ts) { + _iamccsForceRemoveLink(graph, id); + } + } + } catch {} +} + +// === CSS FIXES === +// Fix per titoli che escono dai nodi contratti +const style = document.createElement('style'); +style.textContent = ` + .litegraph .node.collapsed .title { + max-width: calc(100% - 40px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + } +`; +document.head.appendChild(style); + +// === CONVERTER NODE === +app.registerExtension({ + name: "iamccs.autolink.converter", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // Ensure native title coloring works without overlay text + installAutolinkTitleColorCanvasHook(); + + if (nodeData?.name === CONVERTER_TYPE) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const result = onNodeCreated?.apply(this, arguments); + + this.addWidget("button", "🔗 Convert All Links", null, () => { + const config = getBlacklistFromInput(this); + convertAllLinks( + app.graph, + config.blacklist, + config.blacklistTypes, + config.blacklistNodeModes, + config.includeKijNodes, + config.groupExclude, + config.groupInOutExclude, + config.alignMode, + config.packingMode, + config.colorSet, + config.colorGet, + config.separateCol, + config.colorTitles + ); + }); + + this.addWidget("button", "↩️ Restore Direct Links", null, () => { + restoreDirectLinks(app.graph); + }); + + return result; + }; + } + + if (nodeData?.name === ARGUMENTS_TYPE) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const result = onNodeCreated?.apply(this, arguments); + + if (!this.properties) this.properties = {}; + if (!this.properties.blacklist) this.properties.blacklist = []; + if (!this.properties.blacklist_types) this.properties.blacklist_types = []; + if (!this.properties.blacklist_node_modes) this.properties.blacklist_node_modes = {}; + if (this.properties.blacklist_add_mode === undefined) this.properties.blacklist_add_mode = "both"; + if (this.properties.blacklist_pending_value === undefined) this.properties.blacklist_pending_value = ""; + if (this.properties.blacklist_view_selected === undefined) this.properties.blacklist_view_selected = ""; + if (this.properties.include_kijnodes === undefined) this.properties.include_kijnodes = false; + if (this.properties.flow_tracer === undefined) this.properties.flow_tracer = false; + if (this.properties.all_nodes_sel === undefined) this.properties.all_nodes_sel = false; + if (this.properties.align_mode === undefined) this.properties.align_mode = "TopToDown"; + if (this.properties.packing_mode === undefined) this.properties.packing_mode = "AvoidAll"; + if (this.properties.autolink_color_set === undefined) this.properties.autolink_color_set = "Gray"; + if (this.properties.autolink_color_get === undefined) this.properties.autolink_color_get = "Gray"; + if (this.properties.separate_col === undefined) this.properties.separate_col = false; + if (this.properties.color_titles === undefined) this.properties.color_titles = "White"; + if (this.properties.group_inout_exclude === undefined) this.properties.group_inout_exclude = "None"; + // Backward compat: old typo key was group_exlude + if (this.properties.group_exclude === undefined) { + if (this.properties.group_exlude !== undefined) { + this.properties.group_exclude = this.properties.group_exlude; + } else { + this.properties.group_exclude = false; + } + } + + const addDivider = () => { + // Inert divider (non-interactive, non-serializzato). Serve solo come separazione visiva. + const divider = { + type: "iamccs_divider", + name: "", + value: "", + options: { serialize: false }, + computeSize: function (width) { + const w = Number.isFinite(width) ? width : 200; + return [Math.max(40, w), 10]; + }, + draw: function (ctx, node, widget_width, y, H) { + try { + ctx.save(); + const margin = 10; + const w = Number.isFinite(widget_width) ? widget_width : (node?.size?.[0] || 200); + const x1 = margin; + const x2 = Math.max(margin + 10, w - margin); + const yy = y + Math.floor((H || 10) / 2); + + ctx.globalAlpha = 0.35; + ctx.strokeStyle = "rgba(128,128,128,0.85)"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x1, yy); + ctx.lineTo(x2, yy); + ctx.stroke(); + ctx.restore(); + } catch (e) { + // mute + } + }, + mouse: function () { + // Non cattura i click: resta "muto" e permette drag del nodo. + return false; + }, + }; + + if (typeof this.addCustomWidget === "function") { + this.addCustomWidget(divider); + } + }; + + // Normalize per-node modes: every blacklisted node must have a mode, default = both + try { + this.properties.blacklist_node_modes = this.properties.blacklist_node_modes || {}; + const ids = new Set((this.properties.blacklist || []).map(v => Number(v)).filter(v => Number.isFinite(v))); + for (const id of ids) { + const k = String(id); + if (!this.properties.blacklist_node_modes[k]) this.properties.blacklist_node_modes[k] = "both"; + } + for (const k of Object.keys(this.properties.blacklist_node_modes)) { + const id = Number(k); + if (!ids.has(id)) delete this.properties.blacklist_node_modes[k]; + } + } catch (e) { + console.warn("[IAMCCS AutoLink] blacklist_node_modes normalize failed", e); + } + + // Toggle per Visual Flow Tracer + this.addWidget( + "toggle", + "visual_flow_tracer", + this.properties.flow_tracer, + (value) => { + this.properties.flow_tracer = value; + toggleFlowTracer(value); + console.log("[IAMCCS AutoLink] Flow Tracer:", value); + } + ); + + // Toggle per mostrare tutti i nodi singolarmente nella blacklist + this.addWidget( + "toggle", + "all_nodes_sel", + this.properties.all_nodes_sel, + (value) => { + this.properties.all_nodes_sel = value; + console.log("[IAMCCS AutoLink] Show all nodes individually:", value); + } + ); + + // Toggle per includere KijNodes nella conversione + this.addWidget( + "toggle", + "include_kijnodes", + this.properties.include_kijnodes, + (value) => { + this.properties.include_kijnodes = value; + console.log("[IAMCCS AutoLink] Include KijNodes:", value); + } + ); + + // Toggle: esclude i link interni allo stesso Group (ma non quelli che attraversano il confine) + this.addWidget( + "toggle", + "GroupExclude", + this.properties.group_exclude, + (value) => { + this.properties.group_exclude = value; + // keep legacy key in sync + this.properties.group_exlude = value; + console.log("[IAMCCS AutoLink] GroupExclude:", value); + } + ); + + // Dropdown: filtra i link che entrano/escono dai group + this.addWidget( + "combo", + "GroupInOutExclude", + this.properties.group_inout_exclude, + (value) => { + this.properties.group_inout_exclude = value; + console.log("[IAMCCS AutoLink] GroupInOutExclude:", value); + }, + { + values: () => ["None", "ExcludeEnter", "ExcludeExit", "ExcludeBoth"] + } + ); + + // Divider: GroupInOutExclude -> Align + addDivider(); + + // Dropdown: modalità di allineamento/packing per Set/Get creati automaticamente + this.addWidget( + "combo", + "align_mode", + this.properties.align_mode, + (value) => { + this.properties.align_mode = value; + console.log("[IAMCCS AutoLink] Align mode:", value); + + // Re-layout esistente: permette di riallineare dopo che gli AutoLink sono stati creati + try { + relayoutExistingAutoLinks(app.graph, value, this.properties.packing_mode); + } catch (e) { + console.error("[IAMCCS AutoLink] Relayout error:", e); + } + }, + { + values: () => [ + "TopToDown", + "BottomToTop", + "CenterUpDown", + "CenterDownUp", + "Proportional", + "AlignX_Right", + "AlignX_Left", + "Columns_Down", + "Columns_Up", + "Rake_Down", + "Rake_Up", + ] + } + ); + + // Dropdown: regole overlap/packing (operativo anche post conversione) + this.addWidget( + "combo", + "packing_mode", + this.properties.packing_mode, + (value) => { + this.properties.packing_mode = value; + console.log("[IAMCCS AutoLink] Packing mode:", value); + + try { + relayoutExistingAutoLinks(app.graph, this.properties.align_mode, value); + } catch (e) { + console.error("[IAMCCS AutoLink] Relayout error:", e); + } + }, + { + values: () => [ + "AvoidAll", + "AvoidNonAutoLink", + ] + } + ); + + // Divider: Packing -> SeparateCol + addDivider(); + + // Toggle + dropdown colori AutoLink (operativo anche post conversione) + this.addWidget( + "toggle", + "SeparateCol", + this.properties.separate_col, + (value) => { + this.properties.separate_col = value; + console.log("[IAMCCS AutoLink] SeparateCol:", value); + recolorExistingAutoLinks(app.graph, this.properties.autolink_color_set, value, this.properties.autolink_color_get, this.properties.color_titles); + } + ); + + this.addWidget( + "combo", + "AutoLinkColor", + this.properties.autolink_color_set, + (value) => { + this.properties.autolink_color_set = value; + console.log("[IAMCCS AutoLink] AutoLinkColor (set/base):", value); + recolorExistingAutoLinks(app.graph, value, this.properties.separate_col, this.properties.autolink_color_get, this.properties.color_titles); + }, + { + values: () => alphaSort(Object.keys(AUTOLINK_COLORS)) + } + ); + + this.addWidget( + "combo", + "AutoLinkColorGet", + this.properties.autolink_color_get, + (value) => { + this.properties.autolink_color_get = value; + console.log("[IAMCCS AutoLink] AutoLinkColorGet:", value); + recolorExistingAutoLinks(app.graph, this.properties.autolink_color_set, this.properties.separate_col, value, this.properties.color_titles); + }, + { + values: () => alphaSort(Object.keys(AUTOLINK_COLORS)) + } + ); + + // Dropdown: colore testo titoli (AutoLink) + this.addWidget( + "combo", + "ColorTitles", + this.properties.color_titles, + (value) => { + this.properties.color_titles = value; + console.log("[IAMCCS AutoLink] ColorTitles:", value); + recolorExistingAutoLinks(app.graph, this.properties.autolink_color_set, this.properties.separate_col, this.properties.autolink_color_get, value); + }, + { + values: () => ["White", "Black", "Auto"] + } + ); + + // Divider: ColorTitles -> Blacklist + addDivider(); + + // Dropdown per aggiungere nodi/tipi alla blacklist + this.addWidget( + "combo", + "add_to_blacklist", + this.properties.blacklist_pending_value, + (value) => { + // Keep selection "loaded"; actual add happens via EXECUTE + this.properties.blacklist_pending_value = value || ""; + console.log("[IAMCCS AutoLink] Pending blacklist selection:", this.properties.blacklist_pending_value); + }, + { + values: () => { + if (this.properties.all_nodes_sel) { + // Mostra tutti i nodi singolarmente + const nodes = app.graph._nodes.filter(n => + n.type !== SET_TYPE && + n.type !== GET_TYPE && + n.type !== CONVERTER_TYPE && + n.type !== ARGUMENTS_TYPE && + true + ); + // Solo ID (no title/type) + const sorted = alphaSort(nodes.map(n => String(n.id))); + return ["", ...sorted]; + } else { + // Mostra solo tipi di nodi + const nodeTypes = new Set(); + app.graph._nodes.forEach(n => { + if (n.type !== SET_TYPE && + n.type !== GET_TYPE && + n.type !== CONVERTER_TYPE && + n.type !== ARGUMENTS_TYPE && + !this.properties.blacklist_types.includes(n.type)) { + nodeTypes.add(n.type); + } + }); + const sorted = alphaSort(Array.from(nodeTypes)).map(t => `[TYPE] ${t}`); + return ["", ...sorted]; + } + } + } + ); + + // Sposta blacklist mode sotto add_to_blacklist + this.addWidget( + "combo", + "blacklist_mode", + this.properties.blacklist_add_mode, + (value) => { + this.properties.blacklist_add_mode = value; + console.log("[IAMCCS AutoLink] Blacklist mode:", value); + }, + { + values: () => ["", "both", "only_output", "only_input"] + } + ); + + // EXECUTE: applica la modalità al nodo selezionato (o aggiunge tipo) + this.addWidget( + "button", + "EXECUTE", + null, + () => { + const value = (this.properties.blacklist_pending_value || "").trim(); + if (!value) return; + + const pickedMode = (this.properties.blacklist_add_mode || "").trim() || "both"; + + if (value.startsWith("[TYPE] ")) { + const nodeType = value.replace(/^\[TYPE\] /, ""); + this.properties.blacklist_types = this.properties.blacklist_types || []; + if (nodeType && !this.properties.blacklist_types.includes(nodeType)) { + this.properties.blacklist_types.push(nodeType); + console.log("[IAMCCS AutoLink] Added node type to blacklist:", nodeType); + } + } else { + const nodeId = parseInt(value); + if (!Number.isFinite(nodeId)) return; + this.properties.blacklist = this.properties.blacklist || []; + this.properties.blacklist_node_modes = this.properties.blacklist_node_modes || {}; + + if (!this.properties.blacklist.includes(nodeId)) this.properties.blacklist.push(nodeId); + this.properties.blacklist_node_modes[String(nodeId)] = pickedMode; + console.log("[IAMCCS AutoLink] Added/Updated node blacklist mode:", nodeId, this.properties.blacklist_node_modes[String(nodeId)]); + } + + // Clear pending selection + mode after executing (so they "disappear" from UI) + this.properties.blacklist_pending_value = ""; + this.properties.blacklist_add_mode = ""; + const widget = this.widgets?.find(w => w?.name === "add_to_blacklist" || w?.label === "add_to_blacklist"); + if (widget) widget.value = ""; + + const modeWidget = this.widgets?.find(w => w?.name === "blacklist_mode" || w?.label === "blacklist_mode"); + if (modeWidget) modeWidget.value = ""; + } + ); + + // Dropdown che mostra la blacklist (selezione non distruttiva) + this.addWidget( + "combo", + "blacklist_view", + this.properties.blacklist_view_selected, + (value) => { + this.properties.blacklist_view_selected = value || ""; + console.log("[IAMCCS AutoLink] Blacklist selection:", this.properties.blacklist_view_selected); + }, + { + values: () => { + const items = []; + + // Aggiungi tipi + (this.properties.blacklist_types || []).forEach(type => { + items.push(`[TYPE] ${type}`); + }); + + // Aggiungi singoli nodi + (this.properties.blacklist || []).forEach(id => { + const node = app.graph.getNodeById(id); + const mode = this.properties.blacklist_node_modes?.[String(id)] || "both"; + const name = node ? (node.title || node.type) : "(deleted)"; + items.push(`${id} - ${name} - (${mode})`); + }); + + return ["", ...alphaSort(items)]; + } + } + ); + + // Remove button: rimuove l'elemento selezionato nella blacklist_view + this.addWidget( + "button", + "remove_blacklist", + null, + () => { + const value = (this.properties.blacklist_view_selected || "").trim(); + if (!value) return; + + if (value.startsWith("[TYPE] ")) { + const nodeType = value.replace(/^\[TYPE\] /, ""); + const index = (this.properties.blacklist_types || []).indexOf(nodeType); + if (index !== -1) { + this.properties.blacklist_types.splice(index, 1); + console.log("[IAMCCS AutoLink] Removed node type from blacklist:", nodeType); + } + } else { + const [nodeIdStr] = value.split(" - "); + const nodeId = parseInt(nodeIdStr); + const index = (this.properties.blacklist || []).indexOf(nodeId); + if (index !== -1) { + this.properties.blacklist.splice(index, 1); + if (this.properties.blacklist_node_modes) delete this.properties.blacklist_node_modes[String(nodeId)]; + console.log("[IAMCCS AutoLink] Removed node from blacklist:", nodeId); + } + } + + // Clear selection after removal + this.properties.blacklist_view_selected = ""; + const widget = this.widgets?.find(w => w?.name === "blacklist_view" || w?.label === "blacklist_view"); + if (widget) widget.value = ""; + } + ); + + // Dropdown per navigare tra Set/Get + this.addWidget( + "combo", + "jump_to_autolink", + "", + (value) => { + if (!value) return; + + const [nodeIdStr] = value.split(" - "); + const nodeId = parseInt(nodeIdStr); + const node = app.graph.getNodeById(nodeId); + + if (node) { + app.canvas.centerOnNode(node); + app.canvas.selectNode(node); + } + }, + { + values: () => { + const autoLinkNodes = app.graph._nodes.filter(n => + n.type === SET_TYPE || n.type === GET_TYPE + ); + return ["", ...alphaSort(autoLinkNodes.map(n => `${n.id} - ${n.title || n.type}`))]; + } + } + ); + + // Auto-resize so newly added widgets are not hidden + try { + const s = this.computeSize?.(); + if (Array.isArray(s) && s.length >= 2) { + const w = Math.max(this.size?.[0] || 0, s[0] || 0); + const h = Math.max(this.size?.[1] || 0, s[1] || 0); + this.size = [w, h]; + } + } catch (e) { + console.warn("[IAMCCS AutoLink] Arguments auto-resize failed", e); + } + try { app?.graph?.setDirtyCanvas?.(true, true); } catch {} + + this.isVirtualNode = true; + + return result; + }; + } + + // Set node - come KJ SetNode + if (nodeData?.name === SET_TYPE) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const result = onNodeCreated?.apply(this, arguments); + const node = this; + + // Dropdown colore (se cambi un Set, cambia anche i Get corrispondenti) + node.properties = node.properties || {}; + if (!node.properties.autolink_color_name) node.properties.autolink_color_name = "Gray"; + this.addWidget( + "combo", + "AutoLinkColor", + node.properties.autolink_color_name, + (value) => { + const key = getAutolinkKey(node); + + // prendi lo stato SeparateCol + colore get dal primo Arguments disponibile (se presente) + const argNode = app.graph._nodes.find(n => n?.type === ARGUMENTS_TYPE); + const separateCol = !!argNode?.properties?.separate_col; + const colorGetName = argNode?.properties?.autolink_color_get || "Gray"; + const colorTitles = argNode?.properties?.color_titles || "White"; + + node.properties.autolink_color_name = value; + node.properties.autolink_color_locked = true; + applyColorToAutolinkKey(app.graph, key, value, separateCol, colorGetName, colorTitles); + }, + { + values: () => alphaSort(Object.keys(AUTOLINK_COLORS)) + } + ); + + // Aggiungi widget per il nome + const nameWidget = this.addWidget("text", "name", "", (value) => { + if (isValidAutolinkKey(value)) { + const safe = String(value).trim(); + node.title = safe; + + // Mantieni i port name coerenti con la chiave + if (node.inputs && node.inputs[0]) node.inputs[0].name = safe; + if (node.outputs && node.outputs[0]) node.outputs[0].name = safe; + + // Salva il valore corrente per il prossimo cambiamento (tracking locale) + nameWidget.lastValue = safe; + + // Applica colore testo titolo (se configurato) + applyNodeTitleTextColor(node, getCurrentColorTitlesMode()); + } else { + // evita titoli '*' o vuoti + if (String(value ?? "").trim() === "*") { + try { setWidgetValue(node, "name", ""); } catch {} + } + if (String(node.title ?? "").trim() === "*" || !String(node.title ?? "").trim()) { + node.title = "Set AutoLink"; + } + } + }); + + // Inizializza lastValue quando il nodo viene caricato + if (!nameWidget.lastValue && nameWidget.value) { + nameWidget.lastValue = nameWidget.value; + } + + // Input/output: normalizza workflow vecchi (che possono avere input duplicati) + normalizeAutolinkIOSlots(app.graph, node, { wantInputs: 1, wantOutputs: 1 }); + + // Callback quando si collega + this.onConnectionsChange = function(slotType, slot, isConnect, link_info) { + if (slotType === 1 && isConnect && link_info) { + const fromNode = app.graph.getNodeById(link_info.origin_id); + if (fromNode && fromNode.outputs && fromNode.outputs[link_info.origin_slot]) { + const outputType = fromNode.outputs[link_info.origin_slot].type; + + // Usa sempre un nome leggibile e stabile (slot-name), non il tipo puro. + // Questo produce base tipo "model"/"image" e poi lo rendiamo unico: model_0, image_2, ... + let suggestedBase = getSlotName(fromNode, link_info.origin_slot, true); + if (!isValidAutolinkKey(suggestedBase)) { + if (outputType && outputType !== "*") suggestedBase = String(outputType).trim().toLowerCase(); + else suggestedBase = `output_${link_info.origin_slot}`; + } + + // Imposta tipo + node.inputs[0].type = outputType; + node.outputs[0].type = outputType; + + // Se il converter ha già impostato un nome unico, NON sovrascriverlo qui. + // Auto-fill solo quando il widget è vuoto. + const currentKey = getAutolinkKey(node); + const desiredKey = isValidAutolinkKey(currentKey) + ? currentKey + : makeUniqueAutolinkSetName(app.graph, suggestedBase); + + // Imposta chiave + UI + porta coerenti + setAutolinkKeyAndTitle(node, desiredKey); + + // Applica colore testo titolo (se configurato) + applyNodeTitleTextColor(node, getCurrentColorTitlesMode()); + } + } + }; + + // Overlay draw: rende visibile ColorTitles anche se LiteGraph ignora title_text_color + // (removed) per-node overlay title drawing; native title is colored via canvas hook + + // Migrazione: vecchi workflow (prima del widget colore) possono aver scritto il nome nel widget colore. + const colorWidget = getWidget(node, "AutoLinkColor"); + const colorVal = colorWidget?.value; + const nameVal = nameWidget?.value; + if (colorVal && !AUTOLINK_COLORS[colorVal] && (!nameVal || !String(nameVal).trim())) { + const migrated = String(colorVal).trim(); + setAutolinkKeyAndTitle(node, migrated); + if (colorWidget) colorWidget.value = "Gray"; + node.properties = node.properties || {}; + node.properties.autolink_color_name = "Gray"; + node.properties.autolink_color_locked = false; + } + + // Fix workflow vecchi: titolo o name a '*' + try { + const rawName = getWidgetValue(node, "name"); + if (String(rawName ?? "").trim() === "*") setWidgetValue(node, "name", ""); + if (String(node.title ?? "").trim() === "*") node.title = "Set AutoLink"; + } catch {} + + // Nodo virtuale - non serializza per il prompt + this.isVirtualNode = true; + + return result; + }; + } + + // Get node - come KJ GetNode + if (nodeData?.name === GET_TYPE) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const result = onNodeCreated?.apply(this, arguments); + const node = this; + + // Combo dinamico con lista Set disponibili + this.addWidget("combo", "name", "", (value) => { + node.onRename(); + }, { + values: () => { + const setNodes = app.graph._nodes.filter(n => n.type === SET_TYPE); + return alphaSort(setNodes.map(n => getAutolinkKey(n)).filter(v => v)); + } + }); + + // Normalizza output duplicati su workflow vecchi + normalizeAutolinkIOSlots(app.graph, node, { wantInputs: 0, wantOutputs: 1 }); + + this.onRename = function() { + const setterName = getAutolinkKey(node); + const setter = app.graph._nodes.find(n => + n.type === SET_TYPE && getAutolinkKey(n) === setterName + ); + + if (setter) { + const linkType = setter.outputs[0].type; + node.outputs[0].type = linkType; + node.outputs[0].name = linkType; + node.title = setterName; + + applyNodeTitleTextColor(node, getCurrentColorTitlesMode()); + } + }; + + // Fix workflow vecchi: titolo a '*' + try { + if (String(node.title ?? "").trim() === "*") node.title = "Get AutoLink"; + } catch {} + // (removed) per-node overlay title drawing; native title is colored via canvas hook + + // Override getInputLink per prendere da Set + this.getInputLink = function(slot) { + const setterName = getAutolinkKey(node); + const setter = app.graph._nodes.find(n => + n.type === SET_TYPE && getAutolinkKey(n) === setterName + ); + + if (setter) { + const slotInfo = setter.inputs[slot]; + if (slotInfo) { + const link = app.graph.links[slotInfo.link]; + return link; + } + } + return null; + }; + + // Nodo virtuale - non serializza per il prompt + this.isVirtualNode = true; + + return result; + }; + } + } +}); + +// === FUNZIONI CONVERSIONE === + +function getNodeById(graph, id) { + return graph.getNodeById ? graph.getNodeById(id) : graph._nodes.find(n => n.id === id); +} + +function createNode(graph, type, x, y) { + const node = LiteGraph.createNode(type); + if (!node) return null; + node.pos = [x, y]; + graph.add(node); + return node; +} + +function setWidgetValue(node, name, value) { + if (!node || !node.widgets || node.widgets.length === 0) return false; + + // 1) Exact match (ideal case) + const exact = node.widgets.find(w => w?.name === name); + if (exact) { + exact.value = value; + return true; + } + + // 2) Case-insensitive / substring match (some custom nodes don't use "name" literally) + const lowered = String(name).toLowerCase(); + const fuzzy = node.widgets.find(w => typeof w?.name === 'string' && w.name.toLowerCase().includes(lowered)); + if (fuzzy) { + fuzzy.value = value; + return true; + } + + // 3) Fallback: if there's a single widget, assume it's the name + if (node.widgets.length === 1) { + node.widgets[0].value = value; + return true; + } + + return false; +} + +function getSlotName(node, slot, isOutput) { + if (!node) return `slot_${slot}`; + const slots = isOutput ? node.outputs : node.inputs; + + const info = slots?.[slot]; + const name = info?.name; + if (name === "*") { + const t = info?.type; + if (t && t !== "*") return String(t).trim().toLowerCase(); + return isOutput ? `output_${slot}` : `input_${slot}`; + } + + return name || (isOutput ? `output_${slot}` : `input_${slot}`); +} + +function getBlacklistFromInput(converterNode) { + // Cerca il nodo Arguments collegato all'input arg + if (!converterNode.inputs || converterNode.inputs.length === 0) { + return { + blacklist: [], + blacklistTypes: [], + blacklistNodeModes: {}, + includeKijNodes: false, + groupExclude: false, + groupInOutExclude: "None", + alignMode: "TopToDown", + packingMode: "AvoidAll", + colorSet: "Gray", + colorGet: "Gray", + separateCol: false, + colorTitles: "White", + }; + } + + const argInput = converterNode.inputs.find(i => i.name === "arg"); + if (!argInput || !argInput.link) { + return { + blacklist: [], + blacklistTypes: [], + blacklistNodeModes: {}, + includeKijNodes: false, + groupExclude: false, + groupInOutExclude: "None", + alignMode: "TopToDown", + packingMode: "AvoidAll", + colorSet: "Gray", + colorGet: "Gray", + separateCol: false, + colorTitles: "White", + }; + } + + const link = app.graph.links[argInput.link]; + if (!link) { + return { + blacklist: [], + blacklistTypes: [], + blacklistNodeModes: {}, + includeKijNodes: false, + groupExclude: false, + groupInOutExclude: "None", + alignMode: "TopToDown", + packingMode: "AvoidAll", + colorSet: "Gray", + colorGet: "Gray", + separateCol: false, + colorTitles: "White", + }; + } + + const argNode = app.graph.getNodeById(link.origin_id); + if (!argNode || argNode.type !== ARGUMENTS_TYPE) { + return { + blacklist: [], + blacklistTypes: [], + blacklistNodeModes: {}, + includeKijNodes: false, + groupExclude: false, + groupInOutExclude: "None", + alignMode: "TopToDown", + packingMode: "AvoidAll", + colorSet: "Gray", + colorGet: "Gray", + separateCol: false, + colorTitles: "White", + }; + } + + return { + blacklist: argNode.properties?.blacklist || [], + blacklistTypes: argNode.properties?.blacklist_types || [], + blacklistNodeModes: argNode.properties?.blacklist_node_modes || {}, + includeKijNodes: argNode.properties?.include_kijnodes || false, + groupExclude: (argNode.properties?.group_exclude ?? argNode.properties?.group_exlude) || false, + groupInOutExclude: argNode.properties?.group_inout_exclude || "None", + alignMode: argNode.properties?.align_mode || "TopToDown", + packingMode: argNode.properties?.packing_mode || "AvoidAll", + colorSet: argNode.properties?.autolink_color_set || "Gray", + colorGet: argNode.properties?.autolink_color_get || "Gray", + separateCol: !!argNode.properties?.separate_col, + colorTitles: argNode.properties?.color_titles || "White", + }; +} + +function relayoutExistingAutoLinks(graph, alignMode = "TopToDown", packingMode = "AvoidAll") { + if (!graph) return; + + const GRID_SIZE = 80; + const GET_OFFSET_X = -220; + + const isArrayLike = (v, minLen) => v != null && typeof v.length === 'number' && v.length >= minLen; + const getNodePos = (n) => { + const x = (isArrayLike(n?.pos, 2) ? n.pos[0] : n?.pos?.[0]) ?? 0; + const y = (isArrayLike(n?.pos, 2) ? n.pos[1] : n?.pos?.[1]) ?? 0; + return [x, y]; + }; + const getNodeSize = (n) => { + const w = (isArrayLike(n?.size, 2) ? n.size[0] : n?.size?.[0]) ?? 200; + const h = (isArrayLike(n?.size, 2) ? n.size[1] : n?.size?.[1]) ?? 100; + return [w, h]; + }; + + const isAutoLinkNode = (node) => node?.type === SET_TYPE || node?.type === GET_TYPE; + const shouldTreatAsObstacle = (node) => { + if (!node) return false; + if (packingMode === "AvoidAll") return true; + if (packingMode === "AvoidNonAutoLink") return !isAutoLinkNode(node); + return true; + }; + + const rectsOverlap = (ax, ay, aw, ah, bx, by, bw, bh) => + ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by; + + function seriesCentered(firstUp = true) { + return (i) => { + if (i === 0) return 0; + const k = Math.ceil(i / 2); + const sign = (i % 2 === 1) ? (firstUp ? -1 : 1) : (firstUp ? 1 : -1); + return sign * k; + }; + } + + function layoutDelta(i, mode) { + switch (mode) { + case "TopToDown": + case "stack_down": + return { dx: 0, dy: i }; + case "BottomToTop": + case "stack_up": + return { dx: 0, dy: -i }; + case "CenterUpDown": + case "stack_center_up": + return { dx: 0, dy: seriesCentered(true)(i) }; + case "CenterDownUp": + case "stack_center_down": + return { dx: 0, dy: seriesCentered(false)(i) }; + case "AlignX_Right": + case "row_right": + return { dx: i, dy: 0 }; + case "AlignX_Left": + case "row_left": + return { dx: -i, dy: 0 }; + case "Columns_Down": + case "columns_down": { + const rows = 10; + const col = Math.floor(i / rows); + const row = i % rows; + return { dx: col, dy: row }; + } + case "Columns_Up": + case "columns_up": { + const rows = 10; + const col = Math.floor(i / rows); + const row = i % rows; + return { dx: col, dy: -row }; + } + case "Rake_Down": + case "rake_down": + return { dx: i, dy: i }; + case "Rake_Up": + case "rake_up": + return { dx: i, dy: -i }; + case "Proportional": + // Keep Y anchored; move horizontally if collisions + return { dx: seriesCentered(true)(i), dy: 0 }; + default: + return { dx: 0, dy: seriesCentered(true)(i) }; + } + } + + function findFreePosition(baseX, baseY, offsetX, occupied, mode, ignoreNode, extraObstacles = []) { + const maxAttempts = 250; + for (let attempts = 0; attempts < maxAttempts; attempts++) { + const { dx, dy } = layoutDelta(attempts, mode); + const testX = baseX + offsetX + dx * GRID_SIZE; + const testY = baseY + dy * GRID_SIZE; + const posKey = `${Math.round(testX / GRID_SIZE)}_${Math.round(testY / GRID_SIZE)}`; + if (occupied.has(posKey)) continue; + + let overlaps = false; + for (const node of graph._nodes) { + if (!shouldTreatAsObstacle(node)) continue; + if (ignoreNode && node === ignoreNode) continue; + const [nx, ny] = getNodePos(node); + const [nw, nh] = getNodeSize(node); + if (rectsOverlap(testX, testY, 150, 26, nx, ny, nw, nh)) { + overlaps = true; + break; + } + } + + if (!overlaps && extraObstacles && extraObstacles.length) { + for (const r of extraObstacles) { + if (rectsOverlap(testX, testY, 150, 26, r.x, r.y, r.w, r.h)) { + overlaps = true; + break; + } + } + } + + if (!overlaps) { + occupied.add(posKey); + return [testX, testY]; + } + } + return [baseX + offsetX, baseY]; + } + + const sets = graph._nodes.filter(n => n?.type === SET_TYPE); + const gets = graph._nodes.filter(n => n?.type === GET_TYPE); + + // 1) Re-layout Sets: ancorati al nodo origine che li alimenta + const occupiedSet = new Set(); + const setRecords = []; + for (const setNode of sets) { + const inLinkId = setNode?.inputs?.[0]?.link; + const inLink = inLinkId != null ? graph.links?.[inLinkId] : null; + if (!inLink) continue; + + const originNode = getNodeById(graph, inLink.origin_id); + if (!originNode) continue; + + const originSlot = inLink.origin_slot ?? 0; + const name = getAutolinkKey(setNode); + setRecords.push({ setNode, originNode, originSlot, name }); + } + + setRecords.sort((a, b) => { + if (a.originNode.id !== b.originNode.id) return a.originNode.id - b.originNode.id; + if (a.originSlot !== b.originSlot) return a.originSlot - b.originSlot; + return String(a.name).localeCompare(String(b.name)); + }); + + const placedAutoLinkRects = []; + + for (const rec of setRecords) { + const [ox, oy] = getNodePos(rec.originNode); + const [ow] = getNodeSize(rec.originNode); + const baseX = ox + ow; + let baseY = oy; + if (alignMode === "Proportional" && typeof rec.originNode?.getConnectionPos === 'function') { + const p = rec.originNode.getConnectionPos(false, rec.originSlot); + if (Array.isArray(p) && p.length >= 2) baseY = p[1] ?? baseY; + } + const [nx, ny] = findFreePosition(baseX, baseY, 20, occupiedSet, alignMode, rec.setNode, placedAutoLinkRects); + rec.setNode.pos = [nx, ny]; + placedAutoLinkRects.push({ x: nx, y: ny, w: 150, h: 26 }); + } + + // 2) Re-layout Gets: ancorati al nodo target che alimentano + const occupiedGet = new Set(); + const getRecords = []; + for (const getNode of gets) { + // preferisci link reale in uscita + const outLinks = getNode?.outputs?.[0]?.links; + const linkId = Array.isArray(outLinks) && outLinks.length > 0 ? outLinks[0] : null; + const outLink = linkId != null ? graph.links?.[linkId] : null; + const targetId = outLink?.target_id ?? getNode?.properties?.metadata?.target?.id; + if (targetId == null) continue; + const targetNode = getNodeById(graph, targetId); + if (!targetNode) continue; + const name = getAutolinkKey(getNode); + const targetSlot = outLink?.target_slot; + getRecords.push({ getNode, targetNode, targetSlot, name }); + } + + getRecords.sort((a, b) => { + if (a.targetNode.id !== b.targetNode.id) return a.targetNode.id - b.targetNode.id; + if (alignMode === "Proportional") { + const as = Number.isFinite(a.targetSlot) ? a.targetSlot : 0; + const bs = Number.isFinite(b.targetSlot) ? b.targetSlot : 0; + if (as !== bs) return as - bs; + } + return String(a.name).localeCompare(String(b.name)); + }); + + for (const rec of getRecords) { + const [tx, ty] = getNodePos(rec.targetNode); + const baseX = tx; + let baseY = ty; + if (alignMode === "Proportional" && Number.isFinite(rec.targetSlot) && typeof rec.targetNode?.getConnectionPos === 'function') { + const p = rec.targetNode.getConnectionPos(true, rec.targetSlot); + if (Array.isArray(p) && p.length >= 2) baseY = p[1] ?? baseY; + } + const [nx, ny] = findFreePosition(baseX, baseY, GET_OFFSET_X, occupiedGet, alignMode, rec.getNode, placedAutoLinkRects); + rec.getNode.pos = [nx, ny]; + placedAutoLinkRects.push({ x: nx, y: ny, w: 150, h: 26 }); + } + + graph.setDirtyCanvas(true, true); +} + +function getGroupForNode(graph, node) { + const rawGroups = graph?._groups ?? graph?.groups ?? []; + const groups = Array.isArray(rawGroups) + ? rawGroups + : (rawGroups && typeof rawGroups === 'object' ? Object.values(rawGroups) : []); + + if (!node || !groups || groups.length === 0) return null; + + const isArrayLike = (v, minLen) => v != null && typeof v.length === 'number' && v.length >= minLen; + + const nodeW = (isArrayLike(node.size, 2) ? node.size[0] : node.size?.[0]) ?? 0; + const nodeH = (isArrayLike(node.size, 2) ? node.size[1] : node.size?.[1]) ?? 0; + const nodeX = (isArrayLike(node.pos, 2) ? node.pos[0] : node.pos?.[0]) ?? 0; + const nodeY = (isArrayLike(node.pos, 2) ? node.pos[1] : node.pos?.[1]) ?? 0; + const cx = nodeX + nodeW / 2; + const cy = nodeY + nodeH / 2; + + let bestGroup = null; + let bestArea = Infinity; + + for (let i = 0; i < groups.length; i++) { + const g = groups[i]; + if (!g) continue; + + let bounding = null; + if (isArrayLike(g.bounding, 4)) { + bounding = [g.bounding[0], g.bounding[1], g.bounding[2], g.bounding[3]]; + } else if (typeof g.getBounding === 'function') { + const b = g.getBounding(); + if (isArrayLike(b, 4)) bounding = [b[0], b[1], b[2], b[3]]; + } + + if (!bounding && isArrayLike(g.pos, 2) && isArrayLike(g.size, 2)) { + bounding = [g.pos[0], g.pos[1], g.size[0], g.size[1]]; + } + + if (!bounding) continue; + + const [gx, gy, gw, gh] = bounding; + if (cx >= gx && cx <= gx + gw && cy >= gy && cy <= gy + gh) { + const area = Math.abs(gw * gh); + if (area < bestArea) { + bestArea = area; + bestGroup = g; + } + } + } + + return bestGroup; +} + +function convertAllLinks( + graph, + blacklist = [], + blacklistTypes = [], + blacklistNodeModes = {}, + includeKijNodes = false, + groupExclude = false, + groupInOutExclude = "None", + alignMode = "TopToDown", + packingMode = "AvoidAll", + colorSet = "Gray", + colorGet = "Gray", + separateCol = false, + colorTitles = "White" +) { + // Backward compat for older call signature that didn't include blacklistNodeModes / colorTitles + if (typeof blacklistNodeModes === 'boolean') { + const oldIncludeKijNodes = blacklistNodeModes; + const oldGroupExclude = includeKijNodes; + const oldAlignMode = groupExclude; + const oldPackingMode = alignMode; + const oldColorSet = packingMode; + const oldColorGet = colorSet; + const oldSeparateCol = colorGet; + + blacklistNodeModes = {}; + includeKijNodes = !!oldIncludeKijNodes; + groupExclude = !!oldGroupExclude; + groupInOutExclude = "None"; + alignMode = oldAlignMode || "TopToDown"; + packingMode = oldPackingMode || "AvoidAll"; + colorSet = oldColorSet || "Gray"; + colorGet = oldColorGet || "Gray"; + separateCol = !!oldSeparateCol; + colorTitles = "White"; + } + + console.log("[IAMCCS AutoLink] Converting links..."); + console.log("[IAMCCS AutoLink] Blacklist IDs:", blacklist); + console.log("[IAMCCS AutoLink] Blacklist Types:", blacklistTypes); + console.log("[IAMCCS AutoLink] Blacklist Node Modes:", blacklistNodeModes); + console.log("[IAMCCS AutoLink] Include KijNodes:", includeKijNodes); + console.log("[IAMCCS AutoLink] GroupExclude:", groupExclude); + console.log("[IAMCCS AutoLink] GroupInOutExclude:", groupInOutExclude); + console.log("[IAMCCS AutoLink] Align mode:", alignMode); + console.log("[IAMCCS AutoLink] Packing mode:", packingMode); + console.log("[IAMCCS AutoLink] ColorSet:", colorSet); + console.log("[IAMCCS AutoLink] ColorGet:", colorGet); + console.log("[IAMCCS AutoLink] SeparateCol:", separateCol); + console.log("[IAMCCS AutoLink] ColorTitles:", colorTitles); + + const linksData = graph.links || {}; + const linksToConvert = []; + const kijNodesToConvert = []; + + for (const linkId in linksData) { + const link = linksData[linkId]; + if (!link) continue; + + const srcNode = getNodeById(graph, link.origin_id); + const dstNode = getNodeById(graph, link.target_id); + + if (!srcNode || !dstNode) continue; + + // Skip già AutoLink + if (srcNode.type === SET_TYPE || srcNode.type === GET_TYPE) continue; + if (dstNode.type === SET_TYPE || dstNode.type === GET_TYPE) continue; + + // Blacklist permanente: nodi Arguments e Converter non vengono mai convertiti + if (srcNode.type === ARGUMENTS_TYPE || srcNode.type === CONVERTER_TYPE || + dstNode.type === ARGUMENTS_TYPE || dstNode.type === CONVERTER_TYPE) { + console.log(`[IAMCCS AutoLink] Skipping AutoLink system node: ${srcNode.type} -> ${dstNode.type}`); + continue; + } + + // Gestione KijNodes + const srcIsKij = (srcNode.type === KJ_SET_TYPE || srcNode.type === KJ_GET_TYPE); + const dstIsKij = (dstNode.type === KJ_SET_TYPE || dstNode.type === KJ_GET_TYPE); + + if (srcIsKij || dstIsKij) { + if (includeKijNodes) { + // Converte KijNodes in AutoLink + kijNodesToConvert.push({ link, srcNode, dstNode, srcIsKij, dstIsKij }); + } else { + // Esclude permanentemente KijNodes e i loro collegamenti + console.log(`[IAMCCS AutoLink] Skipping KijNode: ${srcNode.type} -> ${dstNode.type}`); + } + continue; + } + + // Salta i nodi in blacklist utente (per ID o per tipo) + { + const srcMode = blacklistNodeModes?.[String(srcNode.id)] || (blacklist.includes(srcNode.id) ? "both" : null); + const dstMode = blacklistNodeModes?.[String(dstNode.id)] || (blacklist.includes(dstNode.id) ? "both" : null); + + const skipBecauseSrc = !!srcMode && (srcMode === "both" || srcMode === "only_output"); + const skipBecauseDst = !!dstMode && (dstMode === "both" || dstMode === "only_input"); + + if (skipBecauseSrc || skipBecauseDst) { + console.log( + `[IAMCCS AutoLink] Skipping blacklisted node by mode: src=${srcNode.id}(${srcMode || "-"}) dst=${dstNode.id}(${dstMode || "-"})` + ); + continue; + } + } + + if (blacklistTypes.includes(srcNode.type) || blacklistTypes.includes(dstNode.type)) { + console.log(`[IAMCCS AutoLink] Skipping blacklisted node type: ${srcNode.type} or ${dstNode.type}`); + continue; + } + + // GroupExlude: se entrambi i nodi sono dentro lo stesso Group, NON convertire quel collegamento. + // (I collegamenti in entrata/uscita dal group restano convertibili.) + if (groupExclude) { + const srcGroup = getGroupForNode(graph, srcNode); + const dstGroup = getGroupForNode(graph, dstNode); + if (srcGroup && srcGroup === dstGroup) continue; + } + + // GroupInOutExclude: se il link attraversa il confine di un group, puoi escludere le entrate/uscite + if (groupInOutExclude && groupInOutExclude !== "None") { + const srcGroup = getGroupForNode(graph, srcNode); + const dstGroup = getGroupForNode(graph, dstNode); + const crossesBoundary = (srcGroup !== dstGroup) && (srcGroup || dstGroup); + + if (crossesBoundary) { + const entersDstGroup = !!dstGroup && srcGroup !== dstGroup; + const exitsSrcGroup = !!srcGroup && srcGroup !== dstGroup; + + if ( + (groupInOutExclude === "ExcludeEnter" && entersDstGroup) || + (groupInOutExclude === "ExcludeExit" && exitsSrcGroup) || + (groupInOutExclude === "ExcludeBoth" && (entersDstGroup || exitsSrcGroup)) + ) { + continue; + } + } + } + + linksToConvert.push({ link, srcNode, dstNode }); + } + + // Raggruppa i link per origine (stesso nodo + stesso slot) per creare un solo Set per output multipli + const linksByOrigin = new Map(); + for (const linkData of linksToConvert) { + const key = `${linkData.srcNode.id}_${linkData.link.origin_slot}`; + if (!linksByOrigin.has(key)) { + linksByOrigin.set(key, { + srcNode: linkData.srcNode, + originSlot: linkData.link.origin_slot, + outputName: getSlotName(linkData.srcNode, linkData.link.origin_slot, true), + destinations: [] + }); + } + linksByOrigin.get(key).destinations.push({ + dstNode: linkData.dstNode, + targetSlot: linkData.link.target_slot, + linkId: linkData.link.id + }); + } + + // Sistema di griglia per posizionamento non sovrapposto + const GRID_SIZE = 80; + const SET_OFFSET_X = 250; // Distanza dal nodo sorgente + const GET_OFFSET_X = -220; // Distanza dal nodo destinazione + + const isAutoLinkNode = (node) => node?.type === SET_TYPE || node?.type === GET_TYPE; + function shouldTreatAsObstacle(node) { + if (!node) return false; + if (packingMode === "AvoidAll") return true; + if (packingMode === "AvoidNonAutoLink") return !isAutoLinkNode(node); + return true; + } + + function seriesCentered(firstUp = true) { + // 0, -1, +1, -2, +2 ... (firstUp=true) oppure 0, +1, -1, +2, -2 + return (i) => { + if (i === 0) return 0; + const k = Math.ceil(i / 2); + const sign = (i % 2 === 1) ? (firstUp ? -1 : 1) : (firstUp ? 1 : -1); + return sign * k; + }; + } + + function layoutDelta(i, mode) { + // Backward compat: accetta anche i vecchi nomi interni usati durante lo sviluppo + switch (mode) { + case "TopToDown": + case "stack_down": + return { dx: 0, dy: i }; + case "BottomToTop": + case "stack_up": + return { dx: 0, dy: -i }; + case "CenterUpDown": + case "stack_center_up": + return { dx: 0, dy: seriesCentered(true)(i) }; + case "CenterDownUp": + case "stack_center_down": + return { dx: 0, dy: seriesCentered(false)(i) }; + case "AlignX_Right": + case "row_right": + return { dx: i, dy: 0 }; + case "AlignX_Left": + case "row_left": + return { dx: -i, dy: 0 }; + case "Columns_Down": + case "columns_down": { + const rows = 10; + const col = Math.floor(i / rows); + const row = i % rows; + return { dx: col, dy: row }; + } + case "Columns_Up": + case "columns_up": { + const rows = 10; + const col = Math.floor(i / rows); + const row = i % rows; + return { dx: col, dy: -row }; + } + case "Rake_Down": + case "rake_down": + return { dx: i, dy: i }; + case "Rake_Up": + case "rake_up": + return { dx: i, dy: -i }; + case "Proportional": + // Keep Y anchored; move horizontally if collisions + return { dx: seriesCentered(true)(i), dy: 0 }; + default: + return { dx: 0, dy: seriesCentered(true)(i) }; + } + } + + function getAnchorY(node, slot, isOutput) { + if (!node) return 0; + // Prefer LiteGraph connector Y (best match with what you see on screen) + if (typeof node.getConnectionPos === 'function') { + const isInput = !isOutput; + const p = node.getConnectionPos(isInput, slot); + if (Array.isArray(p) && p.length >= 2 && Number.isFinite(p[1])) return p[1]; + } + // Fallback: proportional inside node bounding box + const y = node.pos?.[1] ?? 0; + const h = node.size?.[1] ?? 100; + const count = isOutput ? (node.outputs?.length || 1) : (node.inputs?.length || 1); + const idx = Number.isFinite(slot) ? slot : 0; + const t = (idx + 0.5) / Math.max(1, count); + return y + t * h; + } + + function findFreePosition(graph, baseX, baseY, offsetX, occupiedPositions, mode) { + const maxAttempts = 250; + + for (let attempts = 0; attempts < maxAttempts; attempts++) { + const { dx, dy } = layoutDelta(attempts, mode); + const testX = baseX + offsetX + dx * GRID_SIZE; + const testY = baseY + dy * GRID_SIZE; + const posKey = `${Math.round(testX / GRID_SIZE)}_${Math.round(testY / GRID_SIZE)}`; + + if (occupiedPositions.has(posKey)) continue; + + // Controlla sovrapposizione con nodi esistenti + let overlaps = false; + for (const node of graph._nodes) { + if (!shouldTreatAsObstacle(node)) continue; + const nodeRight = node.pos[0] + (node.size?.[0] || 200); + const nodeBottom = node.pos[1] + (node.size?.[1] || 100); + + if (testX < nodeRight && testX + 150 > node.pos[0] && + testY < nodeBottom && testY + 26 > node.pos[1]) { + overlaps = true; + break; + } + } + + if (!overlaps) { + occupiedPositions.add(posKey); + return [testX, testY]; + } + } + + // Fallback + return [baseX + offsetX, baseY]; + } + + const occupiedSetPositions = new Set(); + const occupiedGetPositions = new Set(); + const createdSets = new Map(); + + // Traccia i nomi dei Set già esistenti/creati per evitare duplicati + // - Preserva nomi numerati come "model_0" (non li riduce a "model") + const usedExactSetNames = new Set(); + const existingSets = graph._nodes.filter(n => n.type === SET_TYPE); + for (const existingSet of existingSets) { + const name = getAutolinkKey(existingSet); + if (name && String(name).trim()) usedExactSetNames.add(String(name).trim()); + } + + function makeUniqueSetName(desiredName) { + const desired = String(desiredName ?? "").trim(); + if (!desired) { + let i = 0; + while (usedExactSetNames.has(`output_${i}`)) i++; + const fallback = `output_${i}`; + usedExactSetNames.add(fallback); + return fallback; + } + + if (!usedExactSetNames.has(desired)) { + usedExactSetNames.add(desired); + return desired; + } + + const m = desired.match(/^(.+?)_(\d+)$/); + if (m) { + const base = m[1]; + let n = parseInt(m[2], 10) + 1; + while (usedExactSetNames.has(`${base}_${n}`)) n++; + const candidate = `${base}_${n}`; + usedExactSetNames.add(candidate); + return candidate; + } + + let n = 0; + while (usedExactSetNames.has(`${desired}_${n}`)) n++; + const candidate = `${desired}_${n}`; + usedExactSetNames.add(candidate); + return candidate; + } + + console.log(`[IAMCCS AutoLink] Creating ${linksByOrigin.size} Set nodes...`); + + // Crea Set nodes (uno per origine) + for (const [key, originData] of linksByOrigin) { + const { srcNode, originSlot, outputName, destinations } = originData; + + const setPos = findFreePosition( + graph, + srcNode.pos[0] + (srcNode.size?.[0] || 200), + (alignMode === "Proportional" ? getAnchorY(srcNode, originSlot, true) : srcNode.pos[1]), + 20, + occupiedSetPositions, + alignMode + ); + + const setNode = createNode(graph, SET_TYPE, setPos[0], setPos[1]); + if (!setNode) continue; + + setNode.properties = setNode.properties || {}; + setNode.properties.autolink_color_name = colorSet; + if (setNode.properties.autolink_color_locked === undefined) setNode.properties.autolink_color_locked = false; + applyNodeColors(setNode, getAutolinkColorPreset(colorSet, 'set', separateCol, colorGet)); + applyNodeTitleTextColor(setNode, colorTitles); + + // Ottieni il tipo dall'output del nodo sorgente + const outputType = srcNode.outputs?.[originSlot]?.type || "*"; + let outputSlotName = getSlotName(srcNode, originSlot, true); + + // Ulteriore fix: non permettere mai "*" come chiave + if (!outputSlotName || String(outputSlotName).trim() === "*") { + if (outputType && outputType !== "*") outputSlotName = String(outputType).trim().toLowerCase(); + else outputSlotName = `output_${originSlot}`; + } + + console.log(`[IAMCCS AutoLink] Processing: ${srcNode.title || srcNode.type}[${originSlot}] with name "${outputSlotName}"`); + + // Genera nome unico se esiste già un Set con questo nome. + // Importante: NON ridurre mai "model_0" a "model". + const uniqueName = makeUniqueSetName(outputSlotName); + + // Imposta tipo e nome correttamente + if (setNode.inputs && setNode.inputs[0]) { + setNode.inputs[0].type = outputType; + setNode.inputs[0].name = uniqueName; + } + if (setNode.outputs && setNode.outputs[0]) { + setNode.outputs[0].type = outputType; + setNode.outputs[0].name = uniqueName; + } + + setWidgetValue(setNode, "name", uniqueName); + setNode.title = `${uniqueName}`; + + console.log(`[IAMCCS AutoLink] ✓ Created Set node: "${uniqueName}" (from ${srcNode.title || srcNode.type})`); + + // Inizializza lastValue per il tracking delle modifiche + const nameWidget = getWidget(setNode, "name"); + if (nameWidget) nameWidget.lastValue = uniqueName; + + // Collassa + setTimeout(() => { + if (setNode.collapse) setNode.collapse(); + setNode.size = [150, 26]; + }, 0); + + // Collega il Set al nodo sorgente + srcNode.connect(originSlot, setNode, 0); + + // Salva il Set creato per creare i Get dopo + createdSets.set(key, { + setNode, + outputName: uniqueName, + outputType, + srcNode, + originSlot, + destinations + }); + } + + // Crea Get nodes (uno per destinazione) + for (const [key, setData] of createdSets) { + const { setNode, outputName, outputType, srcNode, originSlot, destinations } = setData; + + const sortedDest = [...destinations].sort((a, b) => { + const ay = a.dstNode?.pos?.[1] ?? 0; + const by = b.dstNode?.pos?.[1] ?? 0; + const ax = a.dstNode?.pos?.[0] ?? 0; + const bx = b.dstNode?.pos?.[0] ?? 0; + if (alignMode === "Proportional") { + const as = Number.isFinite(a.targetSlot) ? a.targetSlot : 0; + const bs = Number.isFinite(b.targetSlot) ? b.targetSlot : 0; + if (as !== bs) return as - bs; + } + if (alignMode === "AlignX_Right" || alignMode === "AlignX_Left" || alignMode === "row_right" || alignMode === "row_left") { + return ax - bx; + } + return ay - by; + }); + + for (const dest of sortedDest) { + const { dstNode, targetSlot, linkId } = dest; + + const getPos = findFreePosition( + graph, + dstNode.pos[0], + (alignMode === "Proportional" ? getAnchorY(dstNode, targetSlot, false) : dstNode.pos[1]), + GET_OFFSET_X, + occupiedGetPositions, + alignMode + ); + + const getNode = createNode(graph, GET_TYPE, getPos[0], getPos[1]); + if (!getNode) continue; + + getNode.properties = getNode.properties || {}; + // il get segue sempre il colore della sua chiave (quindi del set) + getNode.properties.autolink_color_name = setNode.properties?.autolink_color_name || colorSet; + applyNodeColors(getNode, getAutolinkColorPreset(getNode.properties.autolink_color_name, 'get', separateCol, colorGet)); + applyNodeTitleTextColor(getNode, colorTitles); + + // Imposta tipo e nome correttamente + if (getNode.outputs && getNode.outputs[0]) { + getNode.outputs[0].type = outputType; + getNode.outputs[0].name = outputName; + } + + setWidgetValue(getNode, "name", outputName); + getNode.title = `${outputName}`; + + // Collassa + setTimeout(() => { + if (getNode.collapse) getNode.collapse(); + getNode.size = [150, 26]; + }, 0); + + // Salva metadata per restore + const metadata = { + iamccs_autolink: true, + output_name: outputName, + origin: { id: srcNode.id, slot: originSlot }, + target: { id: dstNode.id, slot: targetSlot } + }; + + setNode.properties = setNode.properties || {}; + getNode.properties = getNode.properties || {}; + if (!setNode.properties.metadata) { + setNode.properties.metadata = metadata; + } + getNode.properties.metadata = metadata; + + // Disconnetti link originale + _iamccsDisconnectTargetInput(graph, dstNode, targetSlot); + + // Collega Get al nodo destinazione + getNode.connect(0, dstNode, targetSlot); + } + } + + // Converti KijNodes in AutoLink se richiesto + if (includeKijNodes && kijNodesToConvert.length > 0) { + console.log(`[IAMCCS AutoLink] Converting ${kijNodesToConvert.length} KijNode connections...`); + + for (const { link, srcNode, dstNode, srcIsKij, dstIsKij } of kijNodesToConvert) { + // Se src è KijNode (SetNode o GetNode), lo sostituiamo con AutoLink + if (srcIsKij && srcNode.type === KJ_SET_TYPE) { + // Converti KJ SetNode in IAMCCS SetNode + const kijName = srcNode.widgets?.[0]?.value || "output"; + const newSetNode = createNode(graph, SET_TYPE, srcNode.pos[0], srcNode.pos[1]); + + if (newSetNode) { + setWidgetValue(newSetNode, "name", kijName); + newSetNode.title = `${kijName}`; + + // Ricollega input del KJ SetNode + if (srcNode.inputs?.[0]?.link) { + const inputLink = graph.links[srcNode.inputs[0].link]; + if (inputLink) { + const inputSrcNode = getNodeById(graph, inputLink.origin_id); + if (inputSrcNode) { + inputSrcNode.connect(inputLink.origin_slot, newSetNode, 0); + } + } + } + + // Rimuovi il KJ SetNode + graph.remove(srcNode); + } + } + + if (srcIsKij && srcNode.type === KJ_GET_TYPE) { + // Converti KJ GetNode in IAMCCS GetNode + const kijName = srcNode.widgets?.[0]?.value || "output"; + const newGetNode = createNode(graph, GET_TYPE, srcNode.pos[0], srcNode.pos[1]); + + if (newGetNode) { + setWidgetValue(newGetNode, "name", kijName); + newGetNode.title = `${kijName}`; + + // Ricollega output del KJ GetNode + if (srcNode.outputs?.[0]?.links) { + for (const linkId of srcNode.outputs[0].links) { + const outputLink = graph.links[linkId]; + if (outputLink) { + const outputDstNode = getNodeById(graph, outputLink.target_id); + if (outputDstNode) { + newGetNode.connect(0, outputDstNode, outputLink.target_slot); + } + } + } + } + + // Rimuovi il KJ GetNode + graph.remove(srcNode); + } + } + } + } + + console.log(`[IAMCCS AutoLink] ✓ Converted ${linksToConvert.length} links`); + // Normalizza colori (utile se ci sono già Set/Get in scena) + recolorExistingAutoLinks(graph, colorSet, separateCol, colorGet, colorTitles); + _iamccsFixLinkIntegrity(graph); + graph.setDirtyCanvas(true, true); +} + +function restoreDirectLinks(graph) { + console.log("[IAMCCS AutoLink] Restoring links..."); + + const nodes = graph._nodes || []; + const setNodes = []; + const getNodes = []; + + // Raccogli tutti i nodi Set e Get + for (const node of nodes) { + if (node.type === SET_TYPE) { + setNodes.push(node); + } else if (node.type === GET_TYPE) { + getNodes.push(node); + } + } + + console.log(`[IAMCCS AutoLink] Found ${setNodes.length} Set nodes, ${getNodes.length} Get nodes`); + + // Raggruppa per nome + const byName = new Map(); + + for (const setNode of setNodes) { + const name = getAutolinkKey(setNode); + if (!name) continue; + + if (!byName.has(name)) { + byName.set(name, { set: null, gets: [] }); + } + byName.get(name).set = setNode; + } + + for (const getNode of getNodes) { + const name = getAutolinkKey(getNode); + if (!name) continue; + + if (!byName.has(name)) { + byName.set(name, { set: null, gets: [] }); + } + byName.get(name).gets.push(getNode); + } + + const _iamccsDidConnect = (srcNode, originSlot, dstNode, targetSlot) => { + try { + const ts = Number(targetSlot); + const os = Number(originSlot); + const linkId = dstNode?.inputs?.[ts]?.link; + if (linkId == null) return null; + const link = graph.links?.[linkId]; + if (!link) return null; + if (link.origin_id !== srcNode.id) return null; + if (link.target_id !== dstNode.id) return null; + if (Number(link.origin_slot) !== os) return null; + if (Number(link.target_slot) !== ts) return null; + return linkId; + } catch (e) { + return null; + } + }; + + const safeConnect = (srcNode, originSlot, dstNode, targetSlot) => { + const os = Number(originSlot); + const ts = Number(targetSlot); + if (!Number.isFinite(os) || !Number.isFinite(ts)) return null; + + try { + srcNode.connect(os, dstNode, ts); + const ok = _iamccsDidConnect(srcNode, os, dstNode, ts); + if (ok != null) return ok; + } catch (e) { + // fallthrough + } + + try { + if (typeof graph.connect === "function") { + graph.connect(srcNode.id, os, dstNode.id, ts); + const ok2 = _iamccsDidConnect(srcNode, os, dstNode, ts); + if (ok2 != null) return ok2; + } + } catch (e) { + // fallthrough + } + + try { + if (typeof graph.addLink === "function") { + graph.addLink(srcNode, os, dstNode, ts); + const ok3 = _iamccsDidConnect(srcNode, os, dstNode, ts); + if (ok3 != null) return ok3; + } + } catch (e) { + // fallthrough + } + + return null; + }; + + let restored = 0; + let failed = 0; + + const keyToSet = new Map(); + for (const setNode of setNodes) { + const key = getAutolinkKey(setNode); + if (key) keyToSet.set(key, setNode); + } + const keyToGets = new Map(); + for (const getNode of getNodes) { + const key = getAutolinkKey(getNode); + if (!key) continue; + if (!keyToGets.has(key)) keyToGets.set(key, []); + keyToGets.get(key).push(getNode); + } + + const restoredGetIds = new Set(); + const keysWithFailures = new Set(); + + for (const getNode of getNodes) { + const key = getAutolinkKey(getNode) || ""; + const md = getNode?.properties?.metadata; + const origin = md?.origin; + const target = md?.target; + + if (!origin?.id || origin?.slot === undefined || !target?.id || target?.slot === undefined) { + continue; + } + + const srcNode = getNodeById(graph, origin.id); + const dstNode = getNodeById(graph, target.id); + const originSlot = origin.slot; + const targetSlot = target.slot; + + if (!srcNode || !dstNode) { + failed++; + if (key) keysWithFailures.add(key); + continue; + } + + const ts = Number(targetSlot); + if (!Number.isFinite(ts)) { + failed++; + if (key) keysWithFailures.add(key); + continue; + } + + // Preserve old link in case we fail to restore + let prev = null; + try { + const prevId = dstNode?.inputs?.[ts]?.link; + const prevLink = prevId != null ? graph.links?.[prevId] : null; + if (prevLink) prev = { origin_id: prevLink.origin_id, origin_slot: prevLink.origin_slot }; + } catch (e) { + prev = null; + } + + let hadGetLink = false; + try { + const out = getNode.outputs?.[0]; + hadGetLink = !!(out?.links && out.links.length); + } catch (e) { + hadGetLink = false; + } + + _iamccsDisconnectTargetInput(graph, dstNode, ts); + + const ok = safeConnect(srcNode, originSlot, dstNode, ts); + if (ok === null) { + failed++; + if (key) keysWithFailures.add(key); + + // rollback previous direct link if present + if (prev) { + try { + const prevSrc = getNodeById(graph, prev.origin_id); + if (prevSrc) safeConnect(prevSrc, prev.origin_slot, dstNode, ts); + } catch (e) { + // ignore + } + } + + if (hadGetLink) { + try { getNode.connect(0, dstNode, ts); } catch (e) {} + } + continue; + } + + // Ensure we don't leave duplicate/orphan links pointing to the same target input + _iamccsRemoveOtherLinksToTarget(graph, dstNode.id, ts, ok); + + restored++; + restoredGetIds.add(getNode.id); + } + + if (restored === 0) { + console.warn("[IAMCCS AutoLink] No metadata restores performed; falling back to legacy restore"); + + for (const [name, { set, gets }] of byName) { + if (!set || !gets.length) continue; + + const setInputLink = set.inputs?.[0]?.link; + if (!setInputLink) continue; + const link = graph.links?.[setInputLink]; + if (!link) continue; + + const srcNode = getNodeById(graph, link.origin_id); + if (!srcNode) continue; + const originSlot = link.origin_slot; + + for (const getNode of gets) { + const getOutput = getNode.outputs?.[0]; + if (!getOutput?.links?.length) continue; + + const outLinks = [...getOutput.links]; + for (const linkId of outLinks) { + const outLink = graph.links?.[linkId]; + if (!outLink) continue; + + const dstNode = getNodeById(graph, outLink.target_id); + if (!dstNode) continue; + const targetSlot = outLink.target_slot; + const ts = Number(targetSlot); + if (!Number.isFinite(ts)) continue; + + // Preserve old link in case we fail to restore + let prev = null; + try { + const prevId = dstNode?.inputs?.[ts]?.link; + const prevLink = prevId != null ? graph.links?.[prevId] : null; + if (prevLink) prev = { origin_id: prevLink.origin_id, origin_slot: prevLink.origin_slot }; + } catch (e) { + prev = null; + } + + _iamccsDisconnectTargetInput(graph, dstNode, ts); + + const ok = safeConnect(srcNode, originSlot, dstNode, ts); + if (ok === null) { + failed++; + keysWithFailures.add(name); + + // rollback previous direct link if present + if (prev) { + try { + const prevSrc = getNodeById(graph, prev.origin_id); + if (prevSrc) safeConnect(prevSrc, prev.origin_slot, dstNode, ts); + } catch (e) { + // ignore + } + } + continue; + } + + // Ensure we don't leave duplicate/orphan links pointing to the same target input + _iamccsRemoveOtherLinksToTarget(graph, dstNode.id, ts, ok); + + restored++; + restoredGetIds.add(getNode.id); + } + } + } + } + + const nodesToRemove = new Set(); + for (const getNode of getNodes) { + if (restoredGetIds.has(getNode.id)) nodesToRemove.add(getNode); + } + for (const [key, setNode] of keyToSet) { + const gets = keyToGets.get(key) || []; + const allGetsRestored = gets.length > 0 && gets.every(g => restoredGetIds.has(g.id)); + if (allGetsRestored && !keysWithFailures.has(key)) { + nodesToRemove.add(setNode); + } + } + + console.log(`[IAMCCS AutoLink] Removing ${nodesToRemove.size} AutoLink nodes (safe mode)...`); + + // Make sure UI updates immediately after link rewiring + try { _iamccsFixLinkIntegrity(graph); } catch (e) {} + try { graph.setDirtyCanvas(true, true); } catch (e) {} + + // Rimuovi i nodi in modo asincrono per permettere agli eventi di stabilizzarsi + if (nodesToRemove.size > 0) { + setTimeout(() => { + for (const node of nodesToRemove) { + try { + // Disconnetti tutti i collegamenti prima di rimuovere + if (node.inputs) { + for (let i = node.inputs.length - 1; i >= 0; i--) { + try { + if (node.disconnectInput) node.disconnectInput(i); + } catch (e) { + // ignore + } + } + } + if (node.outputs) { + for (let i = node.outputs.length - 1; i >= 0; i--) { + try { + if (node.disconnectOutput) node.disconnectOutput(i); + } catch (e) { + // ignore + } + } + } + + graph.remove(node); + } catch (e) { + console.error(`[IAMCCS AutoLink] Error removing node ${node.id}:`, e); + } + } + try { _iamccsFixLinkIntegrity(graph); } catch (e) {} + try { graph.setDirtyCanvas(true, true); } catch (e) {} + }, 100); + } + + console.log(`[IAMCCS AutoLink] ✓ Restored ${restored} links`); + if (failed > 0) console.log(`[IAMCCS AutoLink] ⚠ Restore failures: ${failed}`); + console.log(`[IAMCCS AutoLink] ✓ Removed ${nodesToRemove.size} AutoLink nodes`); +} + +console.log("[IAMCCS AutoLink] Extension loaded"); + +// ---- Runtime patch: ensure AutoLink graphs can execute ---- +// AutoLink Set/Get nodes are frontend helpers; the backend nodes are no-op. +// To run a workflow, we temporarily restore direct links before queueing, +// then reload the original graph so the user keeps AutoLink nodes. +function _iamccsPatchQueuePromptForAutolink() { + try { + if (app.__iamccs_autolink_queue_patch_installed) return; + if (typeof app?.queuePrompt !== "function") return; + + const originalQueuePrompt = app.queuePrompt; + app.queuePrompt = async function (...args) { + const graph = app?.graph; + const hasAutoLink = !!graph?._nodes?.some(n => n?.type === SET_TYPE || n?.type === GET_TYPE); + if (!hasAutoLink) { + return await originalQueuePrompt.apply(this, args); + } + + let snapshot = null; + try { + snapshot = typeof graph.serialize === "function" ? graph.serialize() : null; + } catch (e) { + snapshot = null; + } + + try { + // Convert AutoLink nodes into direct links (and remove them) for execution. + // This mutates the live graph, so we restore from snapshot in finally. + try { restoreDirectLinks(graph); } catch (e) { + console.warn("[IAMCCS AutoLink] restoreDirectLinks failed before queuePrompt", e); + } + + try { _iamccsFixLinkIntegrity(graph); } catch {} + + return await originalQueuePrompt.apply(this, args); + } finally { + // Restore the original AutoLink graph for the UI. + if (snapshot) { + try { + if (typeof app.loadGraphData === "function") { + await app.loadGraphData(snapshot); + } else if (typeof graph?.configure === "function") { + graph.configure(snapshot); + graph.setDirtyCanvas?.(true, true); + } + } catch (e) { + console.warn("[IAMCCS AutoLink] Failed to restore graph snapshot after queuePrompt", e); + } + } + } + }; + + app.__iamccs_autolink_queue_patch_installed = true; + console.log("[IAMCCS AutoLink] Patched app.queuePrompt (temporary restore for execution)"); + } catch (e) { + console.warn("[IAMCCS AutoLink] Failed to patch queuePrompt", e); + } +} + +_iamccsPatchQueuePromptForAutolink(); + diff --git a/web/iamccs_ltx2_time_length_sync.js b/web/iamccs_ltx2_time_length_sync.js new file mode 100644 index 0000000..56639ed --- /dev/null +++ b/web/iamccs_ltx2_time_length_sync.js @@ -0,0 +1,117 @@ +// 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"; + +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 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 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) return; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated?.apply(this, arguments); + // Snap to 8n+1 on both nodes, since LTX-2 VAE encode requires it. + installSecondsLengthSync(this, { snapRule: true }); + return r; + }; + }, +}); diff --git a/web/js/iamccs_ltx2_validator_sync.js b/web/js/iamccs_ltx2_validator_sync.js deleted file mode 100644 index c2b692e..0000000 --- a/web/js/iamccs_ltx2_validator_sync.js +++ /dev/null @@ -1,190 +0,0 @@ -import { app } from "/scripts/app.js"; - -function findWidget(node, name) { - return node?.widgets?.find((w) => w?.name === name); -} - -function clampNumber(value, min, max) { - const v = Number(value); - if (!Number.isFinite(v)) return min; - return Math.min(max, Math.max(min, v)); -} - -function getNearestFrameRateSyncFpsOrFallback(node, fallback = 24.0) { - try { - const graph = node?.graph; - const nodes = graph?._nodes || graph?.nodes || []; - const frNodes = nodes.filter((n) => n?.comfyClass === "IAMCCS_LTX2_FrameRateSync"); - if (!frNodes.length) return fallback; - - const pos = node?.pos || [0, 0]; - let best = frNodes[0]; - let bestD2 = Infinity; - - for (const n of frNodes) { - const p = n?.pos || [0, 0]; - const dx = Number(p[0]) - Number(pos[0]); - const dy = Number(p[1]) - Number(pos[1]); - const d2 = dx * dx + dy * dy; - if (d2 < bestD2) { - bestD2 = d2; - best = n; - } - } - - const fpsWidget = findWidget(best, "fps") || findWidget(best, "value"); - const fps = Number(fpsWidget?.value); - if (!Number.isFinite(fps) || fps <= 0) return fallback; - return fps; - } catch (e) { - return fallback; - } -} - -function framesTo8n1(frames, mode) { - let f = Math.max(1, Math.round(Number(frames) || 1)); - const rem = (f - 1) % 8; - if (rem === 0) return f; - - const down = Math.max(1, f - rem); - const up = f + (8 - rem); - - if (mode === "down") return down; - if (mode === "nearest") return (up - f) <= (f - down) ? up : down; - return up; -} - -function shouldEnable8n1Autofix(node) { - const autofixWidget = findWidget(node, "autofix"); - const lengthFixWidget = findWidget(node, "length_fix"); - - return { - hasAutofixWidgets: Boolean(autofixWidget && lengthFixWidget), - getAutofix: () => Boolean(autofixWidget?.value), - getLengthFix: () => String(lengthFixWidget?.value || "up"), - autofixWidget, - lengthFixWidget, - }; -} - -function syncLtx2SecondsLengthNode(node) { - const secondsWidget = findWidget(node, "seconds"); - const lengthWidget = findWidget(node, "length"); - - if (!secondsWidget || !lengthWidget) return; - - const { - hasAutofixWidgets, - getAutofix, - getLengthFix, - autofixWidget, - lengthFixWidget, - } = shouldEnable8n1Autofix(node); - - let isUpdating = false; - - const applyAutofix = (len) => { - const safeLen = Math.max(1, Math.round(Number(len) || 1)); - if (!hasAutofixWidgets) return safeLen; - if (!getAutofix()) return safeLen; - return framesTo8n1(safeLen, getLengthFix()); - }; - - const setWidgetValue = (widget, value) => { - widget.value = value; - // Ensure UI refresh - app.graph?.setDirtyCanvas(true, false); - }; - - const syncFromSeconds = () => { - const fps = getNearestFrameRateSyncFpsOrFallback(node, 24.0); - const seconds = clampNumber(secondsWidget.value, 0.0, 3600.0); - - const rawLength = Math.round(seconds * fps) + 1; - const fixedLength = applyAutofix(rawLength); - const fixedSeconds = (fixedLength - 1) / fps; - - setWidgetValue(lengthWidget, fixedLength); - // Keep both consistent with what the node will actually output. - setWidgetValue(secondsWidget, Number(fixedSeconds.toFixed(2))); - }; - - const syncFromLength = () => { - const fps = getNearestFrameRateSyncFpsOrFallback(node, 24.0); - const len = Math.max(1, Math.round(Number(lengthWidget.value) || 1)); - const fixedLength = applyAutofix(len); - const fixedSeconds = (fixedLength - 1) / fps; - - setWidgetValue(lengthWidget, fixedLength); - setWidgetValue(secondsWidget, Number(fixedSeconds.toFixed(2))); - }; - - const withGuard = (fn) => { - if (isUpdating) return; - isUpdating = true; - try { - fn(); - } finally { - isUpdating = false; - } - }; - - const originalSecondsCb = secondsWidget.callback; - secondsWidget.callback = function (value) { - originalSecondsCb?.call(this, value); - withGuard(syncFromSeconds); - }; - - const originalLengthCb = lengthWidget.callback; - lengthWidget.callback = function (value) { - originalLengthCb?.call(this, value); - withGuard(syncFromLength); - }; - - const originalAutofixCb = autofixWidget?.callback; - if (autofixWidget) { - autofixWidget.callback = function (value) { - originalAutofixCb?.call(this, value); - withGuard(syncFromLength); - }; - } - - const originalLengthFixCb = lengthFixWidget?.callback; - if (lengthFixWidget) { - lengthFixWidget.callback = function (value) { - originalLengthFixCb?.call(this, value); - withGuard(syncFromLength); - }; - } - - // Initial sync after creation/configure. - withGuard(syncFromLength); -} - -app.registerExtension({ - name: "IAMCCS.LTX2Validator.SecondsLengthSync", - - async beforeRegisterNodeDef(nodeType, nodeData, app) { - const supported = new Set([ - "IAMCCS_LTX2_Validator", - "IAMCCS_LTX2_TimeFrameCount", - ]); - if (!supported.has(nodeData?.name)) return; - - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated?.apply(this, arguments); - const node = this; - setTimeout(() => syncLtx2SecondsLengthNode(node), 10); - return r; - }; - - const configure = nodeType.prototype.configure; - nodeType.prototype.configure = function () { - const r = configure?.apply(this, arguments); - const node = this; - setTimeout(() => syncLtx2SecondsLengthNode(node), 10); - return r; - }; - }, -}); diff --git a/workflow_examples/IAMCCS_LTX2_ExtensionModule_Example.json b/workflow_examples/IAMCCS_LTX2_ExtensionModule_Example.json new file mode 100644 index 0000000..20e3f28 --- /dev/null +++ b/workflow_examples/IAMCCS_LTX2_ExtensionModule_Example.json @@ -0,0 +1,247 @@ +{ + "last_node_id": 8, + "last_link_id": 12, + "nodes": [ + { + "id": 1, + "type": "LoadImage", + "pos": [100, 100], + "size": [315, 314], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [1], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "example_frames.png" + ] + }, + { + "id": 2, + "type": "IAMCCS_GetAutoLink", + "pos": [100, 450], + "size": [210, 58], + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [2], + "slot_index": 0, + "label": "OVERLAP_IMAGE_COUNT" + } + ], + "properties": { + "Node name for S&R": "IAMCCS_GetAutoLink" + }, + "widgets_values": [] + }, + { + "id": 3, + "type": "IAMCCS_LTX2_ExtensionModule", + "pos": [450, 100], + "size": [400, 600], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "source_images", + "type": "IMAGE", + "link": 1 + }, + { + "name": "new_images", + "type": "IMAGE", + "link": null + }, + { + "name": "math_value_b", + "type": "INT", + "link": null + }, + { + "name": "autolink_overlap_in", + "type": "INT", + "link": 2 + } + ], + "outputs": [ + { + "name": "source_images", + "type": "IMAGE", + "links": [], + "slot_index": 0 + }, + { + "name": "start_images", + "type": "IMAGE", + "links": [3], + "slot_index": 1 + }, + { + "name": "extended_images", + "type": "IMAGE", + "links": [4], + "slot_index": 2 + }, + { + "name": "overlap_frames", + "type": "INT", + "links": [], + "slot_index": 3 + }, + { + "name": "calculated_frames", + "type": "INT", + "links": [], + "slot_index": 4 + }, + { + "name": "extension_frames", + "type": "INT", + "links": [], + "slot_index": 5 + }, + { + "name": "autolink_overlap_out", + "type": "INT", + "links": [5], + "slot_index": 6 + }, + { + "name": "report", + "type": "STRING", + "links": [6], + "slot_index": 7 + } + ], + "properties": { + "Node name for S&R": "IAMCCS_LTX2_ExtensionModule" + }, + "widgets_values": [ + 10, + "source", + "linear_blend", + true, + "a-1", + 121, + true + ] + }, + { + "id": 4, + "type": "IAMCCS_SetAutoLink", + "pos": [900, 100], + "size": [210, 58], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "value", + "type": "INT", + "link": 5, + "label": "OVERLAP_IMAGE_COUNT" + } + ], + "properties": { + "Node name for S&R": "IAMCCS_SetAutoLink" + }, + "widgets_values": [] + }, + { + "id": 5, + "type": "ShowText", + "pos": [900, 200], + "size": [400, 200], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText" + }, + "widgets_values": [] + }, + { + "id": 6, + "type": "PreviewImage", + "pos": [900, 450], + "size": [315, 246], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 3 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 7, + "type": "PreviewImage", + "pos": [900, 750], + "size": [315, 246], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 4 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + } + ], + "links": [ + [1, 1, 0, 3, 0, "IMAGE"], + [2, 2, 0, 3, 3, "INT"], + [3, 3, 1, 6, 0, "IMAGE"], + [4, 3, 2, 7, 0, "IMAGE"], + [5, 3, 6, 4, 0, "INT"], + [6, 3, 7, 5, 0, "STRING"] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1, + "offset": [0, 0] + } + }, + "version": 0.4, + "workflow_info": { + "name": "IAMCCS LTX-2 Extension Module - Example Workflow", + "description": "Demonstrates the LTX-2 Extension Module with AutoLink integration.\n\nFeatures:\n- GetAutoLink for receiving overlap count from previous iteration\n- LTX2_ExtensionModule for batch extension with blending\n- SetAutoLink for passing overlap count to next iteration\n- Math operation (a-1) to reduce overlap each iteration\n- Preview of start_images and extended_images\n\nUsage:\n1. Load initial frames\n2. First iteration uses static overlap_frames=10\n3. Subsequent iterations use AutoLink value (decremented by 1)\n4. Start images become source for next pass\n5. Loop continues until desired length reached", + "author": "IAMCCS", + "version": "1.0", + "created": "2026-01-24" + } +}