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.
+
+](https://github.com/IAMCCS/IAMCCS-nodes/blob/main/assets/autolink.png)
+
+- LTX-2: Extension Module + helpers for iterative long video extension workflows.
+
+](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)
+
+](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:
](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"
+ }
+}