Updated IAMCCS-nodes to version 1.3.3
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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).
|
||||
|
||||
+213
-2
@@ -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()
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -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<br/>121 frames] --> B[Extension Module]
|
||||
C[Generation 2<br/>121 frames] --> B
|
||||
B --> D[extended_images<br/>217 frames]
|
||||
B --> E[start_images<br/>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*
|
||||
@@ -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.
|
||||
@@ -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,)
|
||||
|
||||
@@ -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<min{int(min_free_vram_mb)}MiB)")
|
||||
|
||||
# 1) Primary knob used by ComfyUI-GGUF's GGUFModelPatcher.
|
||||
if hasattr(model_out, "patch_on_device"):
|
||||
try:
|
||||
setattr(model_out, "patch_on_device", bool(chosen_patch_on_device))
|
||||
applied.append("model.patch_on_device")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2) Defensive: some wrappers might store a nested patcher.
|
||||
for attr in ("patcher", "model_patcher", "_patcher"):
|
||||
inner = getattr(model_out, attr, None)
|
||||
if inner is not None and hasattr(inner, "patch_on_device"):
|
||||
try:
|
||||
setattr(inner, "patch_on_device", bool(chosen_patch_on_device))
|
||||
applied.append(f"model.{attr}.patch_on_device")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Decide target device for patches.
|
||||
target_device = load_device if bool(chosen_patch_on_device) else offload_device
|
||||
|
||||
moved_tensors = 0
|
||||
moved_bytes = 0
|
||||
|
||||
def _try_move_patches() -> 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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)",
|
||||
}
|
||||
+237
-9
@@ -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)",
|
||||
}
|
||||
|
||||
+2
-2
@@ -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" }
|
||||
|
||||
+2
-2
@@ -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."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user