Merge pull request #20 from Meisoftcoltd/feature/recursive-self-queue-1733009829692853919

feat: recursive self-queuing system for sequential execution
This commit is contained in:
Meisoft
2026-03-22 20:40:06 +01:00
committed by GitHub
10 changed files with 174 additions and 1263 deletions
+37 -100
View File
@@ -1,106 +1,43 @@
# 🔁 ComfyUI Sequential Batcher & Video Loop Master (Beta v0.9.3)
# ComfyUI Sequential Batcher (v1.0.0)
> [!IMPORTANT]
> This version is currently in **BETA**. We have fully transitioned the terminology from "Job" to **Batch** to align with ComfyUI standards and the project name.
A highly specialized suite of custom nodes for ComfyUI designed for **Recursive Self-Queuing** and autonomous sequential processing. This architecture minimizes VRAM usage by processing heavy tasks (like video generation) sequentially, batch-by-batch, orchestrated entirely from within the graph.
The ultimate tool for creating complex iterative workflows and frame-by-frame video processing in ComfyUI. Designed to handle huge tasks (like high-res video generation with Wan2.2 or LTX Video) without crashing your GPU by using intelligent sequential looping instead of VRAM-heavy batching.
> **Read in Spanish:** [README_ES.md](README_ES.md)
## 🚀 Why use this?
## The "Formula 1" Engine Architecture
Standard ComfyUI batching processes everything at once (4D tensors). For video or large batches, this leads to **Out of Memory (OOM)** errors.
**Sequential Batcher** allows you to "split" these tasks and process them **one-by-one** (sequentially) within a single "Queue Prompt" run, then "gather" the results back into a single batch or video file.
Starting with v1.0.0, this repository has pivoted exclusively to the autonomous sequential loops and global memory architecture. All legacy technical debt (deprecated batch/sequence/debug nodes) has been pruned, leaving a clean, highly maintainable codebase focused on 6 core nodes.
## The 6 Core Nodes
The system is built around three major categories:
### 🔁 Loop (Autonomous Orchestration)
1. **🏁 Loop Start (Index) (`SequentialLoopStart`)**: Initiates the loop, manages the global loop index, and provides the current iteration index to downstream nodes.
2. **🚀 Loop Trigger (Auto-Queue) (`SequentialLoopTrigger`)**: Placed at the very end of your workflow. It increments the loop counter and autonomously triggers an HTTP POST request to the ComfyUI API (`/prompt`) to queue the next batch cycle.
### 🖼️ Image (Session Memory)
3. **📥 Session Image Receiver (`SessionImageReceiver`)**: Retrieves the initial image or the last generated frame from the previous cycle, intelligently detecting the start of a session.
4. **📤 Session Image Sender (`SessionImageSender`)**: Extracts, saves to global memory, and displays the last frame of a video batch for the next cycle to use.
### 🎞️ Video (Assembly and Validation)
5. **🛡️ Wan Frame Validator (`WanFrameValidator`)**: Validates and corrects the target number of frames to ensure they fit the `4k+1` formula required by specific models (e.g., Wan).
6. **🎞️ Incremental Auto-Stitcher (`IncrementalVideoStitcher`)**: Assembles the generated video chunks of the current session sequentially using FFmpeg, parsing outputs natively from the `VHS_FILENAMES` format.
## Setup & Usage
### Prerequisites
- **FFmpeg**: Must be installed and available in your system's PATH for the `Incremental Auto-Stitcher` to function correctly.
### Installation
1. Navigate to your ComfyUI `custom_nodes` folder.
2. Clone this repository: `git clone https://github.com/your-repo/ComfyUI-Sequential-Batcher.git`
3. Restart ComfyUI.
### How to use the Autonomous Machine
1. **The Start:** Add the `🏁 Loop Start (Index)` node. Connect its `current_loop_index` to the index inputs of your receiver and stitcher nodes. Ensure `reset_loop` is set to `False`.
2. **The End:** Add the `🚀 Loop Trigger (Auto-Queue)` node. Crucially, connect the text output (`final_video_path`) from your `Incremental Auto-Stitcher` to the `trigger_dependency` input. This forces the trigger to wait until the video is physically saved. Set your desired `target_loops`.
3. **Execution:** You no longer need to check "Auto Queue". Just press "Queue Prompt" **once**. The workflow will run the first batch, and upon finishing, the trigger will invisibly signal the server to queue the next batch until the target is reached, at which point it stitches everything together and finishes.
---
## 🛠️ Installation
1. Clone this repo into `custom_nodes/comfyui-sequential-batcher`.
2. Restart ComfyUI.
3. Dependencies (`torch`, `numpy`) will be handled automatically if using ComfyUI Manager.
---
## 📖 Key Concepts
- **SEQUENCE**: A simple list of values (numbers, strings, etc.).
- **BATCH**: A structured collection of "steps". Each step has named **Attributes**.
- **Iteration**: The magic happens in nodes like `Batch To List`, `Image Batch To List`, or `Latent Batch To List`. When ComfyUI sees a "List" output from these nodes, it executes all downstream nodes once for each item in that list.
---
## 🎞️ Video Workflow (Wan2.2 / LTX-Video / Future Models)
Video models produce many frames that can easily exceed 24GB VRAM.
1. **Split**: Use `Latent Batch To List` to turn your video latent into a list of single frames.
2. **Process**: Connect to your KSampler/VAE Decoder. ComfyUI will process Frame 1, then Frame 2, then Frame 3...
3. **Gather**: Use `Latent List To Batch` (or `Image List To Batch` if you decoded first) to reconstruct the full video batch for saving.
4. **Stitch (VHS Combine)**: To stitch heavy video chunks sequentially:
- *Option A (Math)*: `Range` -> `MakeBatch` (attribute: "skip_frames") -> `BatchToList` -> `GetAttributeInt` (name: "skip_frames") connected to `VHS_LoadVideo`.
- *Option B (CSV)*: `LoadCSV` (with a `skip_frames` column) -> `BatchToList` -> `GetAttributeInt`.
- Connect the output of `VHS_VideoCombine` (specifically the `VHS_FILENAMES` output) to the **FFmpeg Video Stitcher** node (`video_paths`). The Stitcher will wait for all chunks to finish and combine them automatically!
> [!WARNING]
> **OOM Prevention (Memory Warning):** While `BatchToList` solves the data passing issue, you must still manage PyTorch VRAM. It is highly recommended to use the **easy cleanGpuUsed** node (or similar VRAM clearing nodes) after the VAE Decoder in each cycle of the Batch. Without manual VRAM clearing in each iteration, the sequential loop will accumulate garbage in your graphics card and eventually cause an Out of Memory error.
---
## 🔢 Detailed Node Reference
### 🔄 Loop Category (`🔁 Sequential Batcher/Loop`)
- **🔁 Sequential Loop Index**: The simplest way to start a loop.
- *Input*: `count` (How many times to run).
- *Output*: `index` (0, 1, 2...). Useful for seeding or selecting specific items.
- **🔁 Repeat**: Takes any input and repeats it N times.
- *Input*: `input` (Any), `count` (INT).
- *Output*: `output` (List of the same input).
### 🛠️ Batch Category (`🔁 Sequential Batcher/Batch`)
- **📂 Load CSV**: Loads a CSV file as a Batch. Now includes a table preview in the workflow.
- *Input*: `path` (File location), `delimiter`, `quotechar`.
- *Optional Input*: `index` (To pick a specific row).
- *Output*: `batch` (The full list), `current_attributes` (Dict of the selected row), `count` (Total rows).
- **📊 Preview Batch**: Displays a table of the batch content in the workflow.
- *Input*: `batch`, `index` (Highlight specific row), `max_rows`.
- **🛠️ Make Batch**: Turns a sequence into a "Batch" object.
- *Input*: `sequence` (The data), `name` (The attribute name, e.g., "cfg_scale").
- **🖇️ Combine Batches**: Merges multiple batches.
- *Modes*: `zip` (paired) or `product` (every combination).
- **🔄 Batch To List**: **CRITICAL**. Converts a Batch into a stream of attributes that triggers the sequential loop.
- **📥 Get Attribute**: Extracts a specific value from the current batch step by its name.
### 🖼️ Image & Latent Category (`🔁 Sequential Batcher/Image` & `/Latent`)
- **🖼️ Image Batch To List**: Splits a [N,H,W,C] tensor into N separate images.
- **🖼️ Image List To Batch**: Reconstructs a batch from iterated images.
- **🎞️ Latent Batch To List**: Splits video latents frame-by-frame for VRAM-safe processing.
- **🎞️ Latent List To Batch**: Merges individual frames back into a video latent batch.
- **⏳ Progress Bar**: Generates a visual progress indicator.
- **📤 Session Image Sender** & **📥 Session Image Receiver**: A powerful Sender/Receiver pattern designed to pass the *last frame* of an image batch into the *first frame* of the next Auto Queue cycle. This avoids ComfyUI's cyclic dependency errors, allowing for infinite, continuous video generation loops. The Receiver now accepts the `current_loop_index` and automatically clears its session memory when the loop resets to `0`.
### 🎞️ Video Category (`🔁 Sequential Batcher/Video`)
- **🛡️ Wan Frame Validator**: Ensures video frame counts comply with Wan's strict 4k+1 architecture to prevent skipped or duplicated keyframes.
- **🎞️ FFmpeg Video Stitcher**: Final node in a video loop. Waits for the entire sequential batch to finish and stitches the video chunks together using FFmpeg without re-encoding.
- *Input*: `video_paths` (List of VHS_FILENAMES), `output_filename` (String).
- *Output*: `final_video_path` (Path to the stitched video).
- **🎞️ Incremental Auto-Stitcher**: Incremental stitching node designed specifically for Auto Queue cycles. It runs immediately after each chunk is saved and extracts the `.mp4` absolute paths strictly from the `VHS_FILENAMES` output JSON. It maintains an active session memory to stitch *only* the videos generated during the current loop cycle, guaranteeing immunity to old/garbage files in the output directory.
- *Input*: `trigger` (VHS_FILENAMES list/JSON), `output_filename` (String), `current_loop_index` (Integer: connects to the loop index to automatically flush the session memory on the first cycle).
- *Output*: `final_video_path` (Path to the incrementally stitched final video).
---
## 💡 Pro Tips & Use Cases
### 📝 Using CSV for Prompts and Scenes
You can create a CSV with columns like `prompt`, `negative_prompt`, and `seed`.
1. Use **📂 Load CSV** to load your file.
2. Connect `batch` to **🔄 Batch To List**.
3. Use **📥 Get Attribute** to pull the `prompt` into your CLIP Text Encode.
4. Each row in your CSV will be processed as one "frame" or "job" in the sequence.
### 🎬 Scene Timings for Video
If you have a CSV with `frame_start` and `prompt`, you can use it to change prompts at specific points in a video generation loop.
### 🧪 XY Plots
Use **🖇️ Combine Batches** in `product` mode to create XY Plots (e.g., test every Prompt against every CFG scale).
### 🔍 Automatic Model Iteration
Use **🔍 Model Finder** to automatically iterate through a folder of LoRAs or Checkpoints.
*Created to push the boundaries of ComfyUI automation.*
+37 -100
View File
@@ -1,106 +1,43 @@
# 🔁 ComfyUI Sequential Batcher & Video Loop Master (Beta v0.9.3)
# ComfyUI Sequential Batcher (v1.0.0)
> [!IMPORTANT]
> Esta versión se encuentra actualmente en fase **BETA**. Hemos completado la transición de la terminología de "Job" a **Batch** (Lote) para alinearnos con los estándares de ComfyUI y el nombre del proyecto.
Una suite altamente especializada de nodos personalizados para ComfyUI diseñada para el **Auto-Encolado Recursivo (Recursive Self-Queuing)** y el procesamiento secuencial autónomo. Esta arquitectura minimiza el uso de VRAM procesando tareas pesadas (como la generación de vídeo) de forma secuencial, lote por lote, orquestadas completamente desde dentro del propio grafo.
La herramienta definitiva para crear flujos de trabajo iterativos complejos y procesamiento de vídeo fotograma a fotograma en ComfyUI. Diseñada para manejar tareas pesadas (como generación de vídeo de alta resolución con Wan2.2 o LTX Video) sin colapsar tu GPU, utilizando bucles secuenciales inteligentes en lugar de procesamiento por lotes masivo que agota la VRAM.
> **Read in English:** [README.md](README.md)
## 🚀 ¿Por qué usar esto?
## La Arquitectura "Motor de Fórmula 1"
El procesamiento por lotes (batch) estándar de ComfyUI procesa todo a la vez (tensores 4D). Para vídeo o lotes grandes, esto provoca errores de **Falta de Memoria (OOM)**.
**Sequential Batcher** te permite "dividir" estas tareas y procesarlas **una a una** (secuencialmente) dentro de una sola ejecución de "Queue Prompt", para luego "reunir" los resultados en un solo lote o archivo de vídeo.
A partir de la versión 1.0.0, este repositorio ha pivotado exclusivamente hacia la arquitectura de bucles secuenciales autónomos y memoria global. Toda la deuda técnica de los antiguos nodos (lotes, secuencias, depuración) ha sido eliminada, dejando un código base limpio y fácil de mantener enfocado en los 6 fantásticos.
## Los 6 Nodos Principales
El sistema se construye alrededor de tres categorías principales:
### 🔁 Loop (Orquestación Autónoma)
1. **🏁 Loop Start (Index) (`SequentialLoopStart`)**: Inicia el bucle, gestiona el índice global y provee el índice actual a los nodos de imagen y vídeo del flujo.
2. **🚀 Loop Trigger (Auto-Queue) (`SequentialLoopTrigger`)**: Se coloca al final del flujo de trabajo. Incrementa el contador y, si es necesario, auto-encola el siguiente ciclo mediante un POST a la propia API de ComfyUI (`/prompt`), inyectando el lienzo actual en los parámetros ocultos.
### 🖼️ Image (Memoria de Sesión)
3. **📥 Session Image Receiver (`SessionImageReceiver`)**: Proporciona la imagen inicial o la última generada del ciclo anterior, detectando inteligentemente el inicio de una sesión.
4. **📤 Session Image Sender (`SessionImageSender`)**: Extrae, guarda en la memoria global y muestra en la interfaz el último fotograma de un lote de vídeo, para que el receptor del siguiente ciclo lo utilice.
### 🎞️ Video (Ensamblaje y Validación)
5. **🛡️ Wan Frame Validator (`WanFrameValidator`)**: Valida y corrige el número objetivo de fotogramas para asegurar que encajen en la fórmula `4k+1` requerida por modelos específicos (ej. Wan).
6. **🎞️ Incremental Auto-Stitcher (`IncrementalVideoStitcher`)**: Ensambla secuencialmente los fragmentos de vídeo generados en la sesión actual utilizando FFmpeg, leyendo directamente las rutas desde las entradas nativas `VHS_FILENAMES`.
## Configuración y Uso
### Prerrequisitos
- **FFmpeg**: Debe estar instalado y disponible en el PATH del sistema para que el `Incremental Auto-Stitcher` funcione correctamente.
### Instalación
1. Ve a la carpeta `custom_nodes` de ComfyUI.
2. Clona este repositorio: `git clone https://github.com/your-repo/ComfyUI-Sequential-Batcher.git`
3. Reinicia ComfyUI.
### Cómo Conectar tu Nueva Máquina Autónoma
1. **El Inicio:** Añade el nodo `🏁 Loop Start (Index)`. Conecta su salida `current_loop_index` a las entradas de índice de tu `SessionImageReceiver` y tu `Incremental Auto-Stitcher`. Asegúrate de que su interruptor `reset_loop` está en `False`.
2. **El Final:** Añade el nodo `🚀 Loop Trigger (Auto-Queue)`. Crucial: Conecta la salida de texto (`final_video_path`) de tu `Incremental Auto-Stitcher` en la entrada `trigger_dependency`. Esto obliga al trigger a esperar a que el vídeo se haya guardado físicamente antes de disparar. Configura la cantidad de lotes que quieres en `target_loops`.
3. **Ejecución:** **Ya no tienes que marcar la casilla "Auto Queue" nunca más.** Pulsa "Queue Prompt" **1 sola vez**. El lote 0 arranca y, al llegar al final, el nodo `Trigger` envía una señal invisible al servidor. Verás que en el menú de ComfyUI aparece mágicamente el lote pendiente 1. Arranca el lote 1, lee la memoria perfecta y repite el proceso hasta llegar al target, ensambla el vídeo y se apaga automáticamente.
---
## 🛠️ Instalación
1. Clona este repositorio en `custom_nodes/comfyui-sequential-batcher`.
2. Reinicia ComfyUI.
3. Las dependencias (`torch`, `numpy`) se gestionarán automáticamente si usas ComfyUI Manager gracias al archivo `requirements.txt`.
---
## 📖 Conceptos Clave
- **SEQUENCE (Secuencia)**: Una lista simple de valores (números, textos, etc.).
- **BATCH (Lote)**: Una colección estructurada de "pasos". Cada paso tiene **Atributos** con nombre.
- **Iteración**: La magia ocurre en nodos como `Batch To List`, `Image Batch To List` o `Latent Batch To List`. Cuando ComfyUI detecta una salida tipo "Lista" de estos nodos, ejecuta todos los nodos conectados a continuación una vez por cada elemento de la lista.
---
## 🎞️ Flujo de Vídeo (Wan2.2 / LTX-Video / Modelos Futuros)
Los modelos de vídeo generan muchos fotogramas que pueden superar fácilmente los 24GB de VRAM.
1. **Split (Dividir)**: Usa `Latent Batch To List` para convertir tu latente de vídeo en una lista de fotogramas individuales.
2. **Process (Procesar)**: Conecta a tu KSampler o VAE Decoder. ComfyUI procesará el Fotograma 1, luego el 2, luego el 3... ahorrando memoria.
3. **Gather (Reunir)**: Usa `Latent List To Batch` (o `Image List To Batch` si decodificaste primero) para reconstruir el lote completo de vídeo para guardarlo.
4. **Stitch (Unir con VHS)**: Para unir chunks pesados de vídeo secuencialmente:
- *Opción A (Matemática)*: `Range` -> `MakeBatch` (atributo: "skip_frames") -> `BatchToList` -> `GetAttributeInt` (nombre: "skip_frames") conectado a `VHS_LoadVideo`.
- *Opción B (Por CSV)*: `LoadCSV` (con una columna `skip_frames`) -> `BatchToList` -> `GetAttributeInt`.
- Conecta la salida de `VHS_VideoCombine` (específicamente la salida `VHS_FILENAMES`) al nodo **FFmpeg Video Stitcher** (`video_paths`). ¡El Stitcher esperará a que terminen todos los chunks y los unirá automáticamente!
> [!WARNING]
> **Prevención OOM (Aviso de Memoria):** Aunque `BatchToList` soluciona el paso de datos, a nivel de VRAM de PyTorch, se debe seguir gestionando la memoria. Es altamente recomendable usar el nodo **easy cleanGpuUsed** (o similar) después del decodificador (VAE) en cada ciclo del Batch. Sin la liberación manual de VRAM en cada iteración, el bucle secuencial acumulará basura en la tarjeta gráfica y eventualmente provocará un error de "Out of Memory".
---
## 🔢 Referencia Detallada de Nodos
### 🔄 Categoría Bucles (`🔁 Sequential Batcher/Loop`)
- **🔁 Sequential Loop Index**: La forma más sencilla de iniciar un bucle.
- *Entrada*: `count` (Cuántas veces ejecutar).
- *Salida*: `index` (0, 1, 2...). Útil para semillas (seeds) o selección de elementos.
- **🔁 Repeat**: Toma cualquier entrada y la repite N veces.
- *Entrada*: `input` (Cualquiera), `count` (INT).
- *Salida*: `output` (Lista de la misma entrada repetida).
### 🛠️ Categoría Lote (`🔁 Sequential Batcher/Batch`)
- **📂 Load CSV**: Carga un archivo CSV como un Lote (Batch). Ahora incluye una vista previa de la tabla en el flujo de trabajo.
- *Entrada*: `path` (Ubicación del archivo), `delimiter`, `quotechar`.
- *Entrada Opcional*: `index` (Para elegir una fila específica).
- *Salida*: `batch` (La lista completa), `current_attributes` (Diccionario de la fila seleccionada), `count` (Total de filas).
- **📊 Preview Batch**: Muestra una tabla del contenido del lote en el flujo de trabajo.
- *Entrada*: `batch`, `index` (Resaltar fila específica), `max_rows`.
- **🛠️ Make Batch**: Convierte una secuencia en un objeto "Batch".
- *Entrada*: `sequence` (Los datos), `name` (El nombre del atributo, ej: "cfg_scale").
- **🖇️ Combine Batches**: Fusiona varios lotes.
- *Modos*: `zip` (por parejas) o `product` (todas las combinaciones posibles).
- **🔄 Batch To List**: **CRÍTICO**. Convierte un Lote en un flujo de atributos que activa el bucle secuencial.
- **📥 Get Attribute**: Extrae un valor específico del paso actual del lote por su nombre.
### 🖼️ Categoría Imagen y Latente (`🔁 Sequential Batcher/Image` & `/Latent`)
- **🖼️ Image Batch To List**: Divide un tensor [N,H,W,C] en N imágenes separadas.
- **🖼️ Image List To Batch**: Reconstruye un lote a partir de imágenes iteradas.
- **🎞️ Latent Batch To List**: Divide latentes de vídeo fotograma a fotograma para un procesamiento seguro en VRAM.
- **🎞️ Latent List To Batch**: Une fotogramas individuales de nuevo en un lote latente de vídeo.
- **⏳ Progress Bar**: Genera un indicador visual de progreso.
- **📤 Session Image Sender** y **📥 Session Image Receiver**: Un patrón Emisor/Receptor diseñado para enviar el *último fotograma* de un lote como el *primer fotograma* del siguiente ciclo de Auto Queue. Esto evita los errores de dependencias cíclicas en ComfyUI y permite bucles de generación de vídeo continuos e infinitos. El Receiver ahora acepta el `current_loop_index` y vacía automáticamente su memoria de sesión cuando el bucle vuelve a `0`.
### 🎞️ Categoría Vídeo (`🔁 Sequential Batcher/Video`)
- **🛡️ Wan Frame Validator**: Garantiza que el recuento de fotogramas de vídeo cumpla estrictamente con la arquitectura 4k+1 de Wan, evitando fotogramas clave omitidos o duplicados.
- **🎞️ FFmpeg Video Stitcher**: Nodo final en un bucle de vídeo. Espera a que termine todo el lote secuencial y une los fragmentos de vídeo utilizando FFmpeg sin recodificar.
- *Entrada*: `video_paths` (Lista de VHS_FILENAMES), `output_filename` (Cadena de texto).
- *Salida*: `final_video_path` (Ruta del vídeo ensamblado).
- **🎞️ Incremental Auto-Stitcher**: Nodo de ensamblaje incremental diseñado específicamente para ciclos de Auto Queue. Se ejecuta inmediatamente después de que cada fragmento es guardado y extrae de manera estricta las rutas `.mp4` del JSON de salida que genera `VHS_FILENAMES`. Mantiene una lista en la memoria de la sesión activa para ensamblar *únicamente* los vídeos generados durante el ciclo de bucle actual, garantizando inmunidad a archivos viejos o "basura" en el directorio de salida.
- *Entrada*: `trigger` (JSON/lista de VHS_FILENAMES), `output_filename` (Cadena de texto), `current_loop_index` (Entero: conéctalo al índice del bucle para vaciar automáticamente la memoria de la sesión en el primer ciclo).
- *Salida*: `final_video_path` (Ruta del vídeo ensamblado incremental).
---
## 💡 Consejos Pro y Casos de Uso
### 📝 Uso de CSV para Prompts y Escenas
Puedes crear un CSV con columnas como `prompt`, `negative_prompt` y `seed`.
1. Usa **📂 Load CSV** para cargar tu archivo.
2. Conecta `batch` a **🔄 Batch To List**.
3. Usa **📥 Get Attribute** para pasar el `prompt` a tu CLIP Text Encode.
4. Cada fila de tu CSV se procesará como un "fotograma" o "trabajo" en la secuencia.
### 🎬 Tiempos de Escena para Vídeo
Si tienes un CSV con `frame_start` y `prompt`, puedes usarlo para cambiar los prompts en puntos específicos de un bucle de generación de vídeo.
### 🧪 XY Plots
Usa **🖇️ Combine Batches** en modo `product` para crear "XY Plots" (ej: probar cada Prompt contra cada valor de CFG).
### 🔍 Iteración Automática de Modelos
Usa **🔍 Model Finder** para iterar automáticamente a través de una carpeta de LoRAs o Checkpoints.
*Creado para llevar los límites de la automatización en ComfyUI un paso más allá.*
+8 -57
View File
@@ -1,9 +1,8 @@
__version__ = "0.9.3-beta"
__version__ = "1.0.0" # Subimos a versión 1.0 ya que es una arquitectura nueva
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
def register_node(c, display_name=None):
assert not isinstance(c.RETURN_TYPES, str), "Error: string found instead of tuple."
assert not isinstance(c.RETURN_NAMES, str), "Error: string found instead of tuple."
@@ -11,69 +10,21 @@ def register_node(c, display_name=None):
NODE_DISPLAY_NAME_MAPPINGS[c.__name__] = display_name or c.__name__
return c
from . import sequence, paths, batch, image, debug, loop, video
from . import image, loop, video
# Display name mappings with emojis
NODE_DISPLAY_NAME_MAPPINGS.update({
# Sequence
"Range": "🔢 Range",
"Literal": "📝 Literal",
"Reorder": "🔄 Reorder",
"Combinations": "🧬 Combinations",
"Permutations": "🔀 Permutations",
"Slice": "✂️ Slice",
"Join": "🔗 Join",
"ModelFinder": "🔍 Model Finder",
# Batch
"MakeBatch": "🛠️ Make Batch",
"CombineBatches": "🖇️ Combine Batches",
"EnumerateBatch": "🔢 Enumerate Batch",
"GetBatchStep": "📍 Get Batch Step",
"FormatAttributes": "📝 Format Attributes",
"GetAttribute": "📥 Get Attribute",
"GetAttributeInt": "📥 Get Attribute (Int)",
"GetAttributeFloat": "📥 Get Attribute (Float)",
"GetAttributeString": "📥 Get Attribute (String)",
"BatchToList": "🔄 Batch To List",
"LoadCSV": "📂 Load CSV",
"PreviewBatch": "📊 Preview Batch",
# Image
"JoinImageBatch": "🖼️ Join Image Batch",
"JoinImages": "🖼️ Join Images",
"SelectImageBatch": "🎯 Select Image Batch",
"SelectImageList": "🎯 Select Image List",
"GetImageSize": "📏 Get Image Size",
"StringToImage": "🔤 String To Image",
"ProgressBar": "⏳ Progress Bar",
"ImageBatchToList": "🖼️ Image Batch To List",
"ImageListToBatch": "🖼️ Image List To Batch",
"LatentBatchToList": "🎞️ Latent Batch To List",
"LatentListToBatch": "🎞️ Latent List To Batch",
# Image (Memoria de Sesión)
"SessionImageReceiver": "📥 Session Image Receiver",
"SessionImageSender": "📤 Session Image Sender",
# Video
"FFmpegVideoStitcher": "🎞️ FFmpeg Video Stitcher",
# Video (Ensamblaje y Validación)
"IncrementalVideoStitcher": "🎞️ Incremental Auto-Stitcher",
"WanFrameValidator": "🛡️ Wan Frame Validator",
# Debug
"Stringify": "🧵 Stringify",
"Interact": "⌨️ Interact",
# Loop
"LoopIndex": "🔁 Sequential Loop Index",
"Repeat": "🔁 Repeat",
# Loop (Orquestación Autónoma)
"SequentialLoopStart": "🏁 Loop Start (Index)",
"SequentialLoopTrigger": "🚀 Loop Trigger (Auto-Queue)",
})
# Backward compatibility for "Job" naming
NODE_CLASS_MAPPINGS.update({
"MakeJob": batch.MakeBatch,
"CombineJobs": batch.CombineBatches,
"EnumerateJob": batch.EnumerateBatch,
"GetJobStep": batch.GetBatchStep,
"JobToList": batch.BatchToList,
})
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
-305
View File
@@ -1,305 +0,0 @@
import collections
import itertools
import csv
import os
import folder_paths
from . import register_node
def format_batch_as_table(batch, current_index=-1, max_rows=20):
if not batch:
return "(Empty Batch)"
# Get all unique keys across all rows
keys = list(dict.fromkeys(itertools.chain.from_iterable(row.keys() for row in batch)))
if not keys:
return "(Empty rows)"
# Truncate values for display
def trunc(v, l=20):
s = str(v).replace('\n', ' ')
return (s[:l-3] + "...") if len(s) > l else s
# Calculate widths
widths = {k: len(str(k)) for k in keys}
rows_to_show = batch[:max_rows]
for row in rows_to_show:
for k in keys:
widths[k] = max(widths[k], len(trunc(row.get(k, ""))))
# Build header
header = "| " + " | ".join(str(k).ljust(widths[k]) for k in keys) + " |"
sep = "|-" + "-|-".join("-" * widths[k] for k in keys) + "-|"
idx_to_highlight = (current_index % len(batch)) if (len(batch) > 0 and current_index != -1) else (0 if len(batch) > 0 else -1)
lines = [header, sep]
for i, row in enumerate(rows_to_show):
prefix = ">" if i == idx_to_highlight else " "
line = f"{prefix}| " + " | ".join(trunc(row.get(k, "")).ljust(widths[k]) for k in keys) + " |"
lines.append(line)
if len(batch) > max_rows:
lines.append(f"... and {len(batch) - max_rows} more rows")
return "\n".join(lines)
@register_node
class MakeBatch:
"""Turns a sequence into a batch with one attribute."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sequence": ("SEQUENCE", ),
"name": ("STRING", {"default": ''}),
},
}
RETURN_TYPES = ("BATCH", "INT")
RETURN_NAMES = ("batch", "count")
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def merge_dicts(self, *dicts):
#return collections.ChainMap(*reversed(dicts))
return dict(itertools.chain.from_iterable(d.items() for d in dicts))
def go(self, sequence, name):
result = [{name: value} for value in sequence]
return (result, len(result))
@register_node
class CombineBatches(MakeBatch):
"""Combines multiple batches."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"a": ("BATCH", ),
"method": (("zip", "product"), {"default": "zip"}),
},
"optional": {
x: ("BATCH", ) for x in ('b', 'c', 'd', 'e')
}
}
def go(self, method, **kwargs):
method = {'product': itertools.product, 'zip': zip}[method]
result = [self.merge_dicts(*steps) for steps in method(*kwargs.values())]
return (result, len(result))
@register_node
class EnumerateBatch(MakeBatch):
"""Combines multiple batches."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch": ("BATCH", ),
"name": ("STRING", {"default": ''}),
},
}
def go(self, batch, name):
result = [self.merge_dicts(step, {name: n}) for n, step in enumerate(batch)]
return (result, len(result))
@register_node
class GetBatchStep:
"""Gets the batch step by number."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch": ("BATCH", ),
"step": ("INT", {"default": 0}),
"wrap": (("repeat", "clamp"), {"default": "repeat"}),
},
}
RETURN_TYPES = ("ATTRIBUTES", )
RETURN_NAMES = ("attributes", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, batch, step, wrap):
if wrap == 'repeat':
while step < 0:
step += len(batch)
step = step % len(batch)
elif wrap == 'clamp':
step = max(min(step, len(batch)-1), 0)
return (batch[step], )
@register_node
class FormatAttributes:
"""Applies attributes to a format string."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"attributes": ("ATTRIBUTES",),
"format": ("STRING", {'default': '', 'multiline': True, "dynamicPrompts": False})
},
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("string", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, attributes, format):
return (format.format(**attributes), )
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
@register_node
class GetAttribute:
"""Gets a named attribute from a step."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"attributes": ("ATTRIBUTES", ),
"name": ("STRING", {"default": ''}),
},
}
RETURN_TYPES = (AnyType("*"), )
RETURN_NAMES = ("value", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, attributes, name):
return (attributes[name], )
# Dynamically register typed attribute getters to avoid wildcard bug
# https://github.com/comfyanonymous/ComfyUI/pull/770
for t in ('INT', 'FLOAT', 'STRING'):
register_node(type(
'GetAttribute'+t.title(),
(GetAttribute, ),
{
'RETURN_TYPES': (t, ),
}
))
@register_node
class LoadCSV:
"""Loads a CSV file into a batch."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"path": ("STRING", {"default": "prompt.csv"}),
"delimiter": (["comma", "tab", "semicolon"], {"default": "comma"}),
"quotechar": (["\"", "'"], {"default": "\""}),
},
"optional": {
"index": ("INT", {"default": -1, "min": -1, "max": 999999}),
}
}
RETURN_TYPES = ("BATCH", "ATTRIBUTES", "INT")
RETURN_NAMES = ("batch", "current_attributes", "count")
OUTPUT_NODE = True
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, path, delimiter, quotechar, index=-1):
input_dir = folder_paths.get_input_directory()
if input_dir is None:
raise ValueError("Input directory not found in ComfyUI configuration.")
if os.path.isabs(path):
full_path = os.path.normpath(path)
else:
full_path = os.path.normpath(os.path.join(input_dir, path))
if os.path.commonpath([input_dir, full_path]) != os.path.normpath(input_dir):
raise ValueError(f"Access denied: Path '{path}' is outside the input directory.")
if not os.path.exists(full_path):
raise FileNotFoundError(f"CSV file not found: {path}")
delim = {
"comma": ",",
"tab": "\t",
"semicolon": ";",
}[delimiter]
batch = []
with open(full_path, mode='r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f, delimiter=delim, quotechar=quotechar)
for row in reader:
# Clean up keys and values
clean_row = {k.strip() if k else k: v.strip() if v else v for k, v in row.items()}
batch.append(clean_row)
count = len(batch)
current_attributes = batch[index % count] if count > 0 and index != -1 else (batch[0] if count > 0 else {})
table = format_batch_as_table(batch, index)
return {"ui": {"text": [table]}, "result": (batch, current_attributes, count)}
@register_node
class PreviewBatch:
"""Shows a table of the batch."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch": ("BATCH",),
},
"optional": {
"index": ("INT", {"default": -1, "min": -1, "max": 999999}),
"max_rows": ("INT", {"default": 20, "min": 1, "max": 1000}),
}
}
RETURN_TYPES = ("BATCH",)
RETURN_NAMES = ("batch",)
OUTPUT_NODE = True
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, batch, index=-1, max_rows=20):
table = format_batch_as_table(batch, index, max_rows)
return {"ui": {"text": [table]}, "result": (batch,)}
@register_node
class BatchToList:
"""Converts a batch into a list."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch": ("BATCH",),
},
}
RETURN_TYPES = ("ATTRIBUTES",)
RETURN_NAMES = ("attributes",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Batch"
def go(self, batch):
return (batch,)
-106
View File
@@ -1,106 +0,0 @@
import sys
import os
import textwrap
import code
from . import register_node
@register_node
class Stringify:
"""Convert any input to str/repr."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"x": ("*", ),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("str", "repr")
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Debug"
def go(self, x):
return (str(x), repr(x))
class RestoreStdStreams(object):
# ComfyUI-Manager patches sys.stdout and sys.stder
# which breaks GNU Readline support and makes the
# REPL annoying to use. This context manager temporarily
# puts back the originals.
def __enter__(self):
self._stdout, self._stderr = sys.stdout, sys.stderr
sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__
self._stdout.flush(); self._stderr.flush()
def __exit__(self, exc_type, exc_value, traceback):
_stdout, _stderr = sys.stdout, sys.stderr
sys.stdout, sys.stderr = self._stdout, self._stderr
_stdout.flush(); _stderr.flush()
EOF = 'Ctrl-Z plus Return' if os.sep == '\\' else 'Ctrl-D (i.e. EOF)'
MESSAGE = f"Press {EOF} to continue processing."
class Quitter:
# Replace exit() and quit() to make sure the user exits with EOF
# otherwise the whole server gets terminated.
def __repr__(self):
return MESSAGE
def __call__(self):
print(MESSAGE)
@register_node
class Interact:
"""
Opens an interactive REPL whenever the node is evaluated.
SECURITY WARNING: This node allows arbitrary code execution.
To enable it, you must set the environment variable:
COMFYUI_SB_ENABLE_INTERACTIVE_DEBUG=1
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {},
"optional": {x: "*" for x in ('a', 'b', 'c', 'd')},
}
RETURN_TYPES = ()
RETURN_NAMES = ()
FUNCTION = "interact"
CATEGORY = "🔁 Sequential Batcher/Debug"
OUTPUT_NODE = True
def interact(self, **kwargs):
if os.environ.get("COMFYUI_SB_ENABLE_INTERACTIVE_DEBUG", "").lower() not in ("true", "1", "yes"):
print(f"[{self.__class__.__name__}] Interactive debugging is disabled. Set COMFYUI_SB_ENABLE_INTERACTIVE_DEBUG=1 to enable.")
return ()
if sys.__stdout__.isatty():
with RestoreStdStreams():
code.interact(
banner=textwrap.dedent(f"""
Interactive debugging started.
Try `print(a)`.
{MESSAGE}
"""),
exitmsg="Resuming workflow...",
local={
**kwargs,
'quit': Quitter(),
'exit': Quitter()
}
)
else:
# Don't block the server if there is no tty.
print("Skipping interactive prompt because there is no tty.")
return ()
+1 -253
View File
@@ -2,264 +2,12 @@ import os
import random
import numpy as np
import torch
from PIL import Image, ImageDraw, ImageFont
from PIL import Image
import folder_paths
from . import register_node
@register_node
class JoinImageBatch:
"""Turns an image batch into one big image."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"mode": (("horizontal", "vertical"), {"default": "horizontal"}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
INPUT_IS_LIST = True
FUNCTION = "join"
CATEGORY = "🔁 Sequential Batcher/Image"
def join(self, images, mode):
# mode will be a list of strings, e.g. ["horizontal", "horizontal", ...]
mode = mode[0]
if isinstance(images, list):
# images is a list of [1, H, W, C] tensors or similar
images = torch.cat(images, dim=0)
n, h, w, c = images.shape
image = None
if mode == "vertical":
# for vertical we can just reshape
image = images.reshape(1, n * h, w, c)
elif mode == "horizontal":
# for horizontal we have to swap axes
image = torch.transpose(torch.transpose(images, 1, 2).reshape(1, n * w, h, c), 1, 2)
return (image,)
@register_node
class JoinImages:
"""Turns joins two images into one big image."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image_a": ("IMAGE",),
"image_b": ("IMAGE",),
"mode": (("horizontal", "vertical"), {"default": "horizontal"}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "join"
CATEGORY = "🔁 Sequential Batcher/Image"
def join(self, image_a, image_b, mode):
dim = {'horizontal': 2, 'vertical': 1}[mode]
return (torch.concat((image_a, image_b), dim), )
@register_node
class SelectImageBatch:
"""Selects one image from an image batch."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"select": ("INT", {"default": 0, "min": 0, "max": 99999, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "select"
CATEGORY = "🔁 Sequential Batcher/Image"
def select(self, images, select):
n, h, w, c = images.shape
if select >= n:
select = n - 1
return (images[select].reshape(1, h, w, c),)
@register_node
class SelectImageList:
"""Selects one image from an image list."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"select": ("INT", {"default": 0, "min": 0, "max": 99999, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
INPUT_IS_LIST = True
FUNCTION = "select"
CATEGORY = "🔁 Sequential Batcher/Image"
def select(self, images, select):
select = select[0]
n = len(images)
if select >= n:
select = n - 1
return (images[select],)
@register_node
class GetImageSize:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
},
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("width", "height", "batch_size")
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Image"
def go(self, images):
return (images.shape[2], images.shape[1], images.shape[0])
@register_node
class StringToImage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": "Hello world!"}),
"width": ("INT", {"default": 384}),
"height": ("INT", {"default": 16}),
"colour": ("COLOR", {"default": "white"}),
"background": ("COLOR", {"default": "black"}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "render"
CATEGORY = "🔁 Sequential Batcher/Image"
def render(self, text, width, height, colour, background):
font = ImageFont.load_default()
img = Image.new("RGB", (width, height), background)
draw = ImageDraw.Draw(img)
_, _, w, h = draw.textbbox((0, 0), text, font=font)
draw.text(((width - w) / 2, ((height - h) / 2) - 1), text, font=font, fill=colour)
tensor = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0)
return (tensor,)
@register_node
class ProgressBar:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"progress": ("FLOAT", {'default': 0, 'forceInput': True}),
"padding": ("INT", {'default': 3}),
"width": ("INT", {"default": 384}),
"height": ("INT", {"default": 16}),
"colour": ("COLOR", {"default": "white"}),
"background": ("COLOR", {"default": "black"}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "render"
CATEGORY = "🔁 Sequential Batcher/Image"
def render(self, progress, padding, width, height, colour, background):
img = Image.new("RGB", (width, height), background)
draw = ImageDraw.Draw(img)
draw.rectangle((padding, padding, width - padding - 1, height - padding - 1), outline=colour)
if progress > 0:
ip = padding + 2
draw.rectangle((ip, ip, max(ip+1, (width - ip - 1) * progress), height - ip - 1), outline=colour, fill=colour)
tensor = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0)
return (tensor,)
@register_node
class ImageBatchToList:
"""Splits an image batch into a list for iteration."""
@classmethod
def INPUT_TYPES(s):
return {"required": {"images": ("IMAGE",)}}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Image"
def go(self, images):
return (list(torch.split(images, 1, dim=0)),)
@register_node
class ImageListToBatch:
"""Gathers an iterated list of images into a single batch."""
@classmethod
def INPUT_TYPES(s):
return {"required": {"images": ("IMAGE",)}}
INPUT_IS_LIST = True
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Image"
def go(self, images):
return (torch.cat(images, dim=0),)
@register_node
class LatentBatchToList:
"""Splits a latent batch into a list for iteration."""
@classmethod
def INPUT_TYPES(s):
return {"required": {"latents": ("LATENT",)}}
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("latent",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Latent"
def go(self, latents):
samples = latents["samples"]
return ([{"samples": samples[i:i+1]} for i in range(samples.shape[0])],)
@register_node
class LatentListToBatch:
"""Gathers an iterated list of latents into a single batch."""
@classmethod
def INPUT_TYPES(s):
return {"required": {"latents": ("LATENT",)}}
INPUT_IS_LIST = True
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("latents",)
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Latent"
def go(self, latents):
return ({"samples": torch.cat([l["samples"] for l in latents], dim=0)},)
# Variable global para almacenar el último fotograma de la sesión
global_session_image = None
+91 -27
View File
@@ -1,38 +1,102 @@
import urllib.request
import json
from . import register_node
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
# Variable global para mantener el estado del bucle entre ejecuciones
global_loop_index = 0
@register_node
class LoopIndex:
"""Iterates a number of times."""
@classmethod
def INPUT_TYPES(s):
return {"required": {"count": ("INT", {"default": 2, "min": 1, "max": 1000000})}}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("index",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Loop"
def go(self, count):
return (list(range(count)),)
@register_node
class Repeat:
"""Repeats an input N times."""
class SequentialLoopStart:
"""Inicia el bucle y provee el índice actual a los nodos de imagen y vídeo."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input": (AnyType("*"),),
"count": ("INT", {"default": 2, "min": 1, "max": 1000000}),
"reset_loop": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = (AnyType("*"),)
RETURN_NAMES = ("output",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "go"
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("current_loop_index",)
FUNCTION = "get_index"
CATEGORY = "🔁 Sequential Batcher/Loop"
def go(self, input, count):
return ([input] * count,)
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN") # Forzamos ejecución siempre
def get_index(self, reset_loop):
global global_loop_index
# Saneamiento de booleano por si viene de un text input
is_reset = False
if isinstance(reset_loop, str):
is_reset = reset_loop.strip().lower() in ['true', '1', 't', 'y', 'yes']
else:
is_reset = bool(reset_loop)
if is_reset:
global_loop_index = 0
print("🔄 [Sequential Batcher] Loop Start: Bucle reiniciado a 0 manualmente.")
print(f"🔄 [Sequential Batcher] Loop Start: Ejecutando ciclo {global_loop_index}.")
return (global_loop_index, )
@register_node
class SequentialLoopTrigger:
"""Se ejecuta al final del flujo. Incrementa el contador y auto-encola el siguiente ciclo si es necesario."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"trigger_dependency": ("*", ), # Cable obligatorio para forzar que espere al final
"target_loops": ("INT", {"default": 4, "min": 1, "max": 1000, "step": 1}),
"port": ("INT", {"default": 8188, "min": 1000, "max": 9999, "step": 1}), # Puerto de ComfyUI (estándar 8188)
},
# MAGIA OCULTA: Pedimos a ComfyUI que nos pase el lienzo actual
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
}
}
RETURN_TYPES = ()
RETURN_NAMES = ()
OUTPUT_NODE = True
FUNCTION = "trigger_next"
CATEGORY = "🔁 Sequential Batcher/Loop"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
def trigger_next(self, trigger_dependency, target_loops, port, prompt=None, extra_pnginfo=None):
global global_loop_index
# 1. Incrementamos el contador
global_loop_index += 1
# 2. Comprobamos si debemos continuar
if global_loop_index < target_loops:
print(f"🚀 [Sequential Batcher] Loop Trigger: Ciclo {global_loop_index}/{target_loops}. Encolando automáticamente el siguiente lote...")
# 3. Preparamos la carga útil (payload) exactamente como la espera ComfyUI
p = {"prompt": prompt}
if extra_pnginfo:
p["extra_data"] = {"extra_pnginfo": extra_pnginfo}
data = json.dumps(p).encode('utf-8')
req = urllib.request.Request(f"http://127.0.0.1:{port}/prompt", data=data, headers={'Content-Type': 'application/json'})
# 4. Disparamos la petición a nuestro propio servidor con timeout de 5s para evitar cuelgues
try:
urllib.request.urlopen(req, timeout=5)
print("✅ [Sequential Batcher] Loop Trigger: Siguiente lote inyectado en la cola con éxito.")
except Exception as e:
print(f"❌ [Sequential Batcher] Error al auto-encolar: {e}")
else:
print(f"🏁 [Sequential Batcher] Loop Trigger: ¡Generación finalizada! Se han completado los {target_loops} ciclos.")
# Reseteamos automáticamente para la próxima vez que el usuario pulse Queue
global_loop_index = 0
return ()
-70
View File
@@ -1,70 +0,0 @@
import pathlib
import glob
from folder_paths import folder_names_and_paths
from . import register_node
folder_types = tuple(sorted(folder_names_and_paths.keys()))
folder_default = "checkpoint" if "checkpoint" in folder_types else folder_types[0]
@register_node
class ModelFinder:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"filenames": ("SEQUENCE", ),
"model_type": (folder_types, {"default": folder_default}),
"recursive": ("BOOLEAN", {"default": True}),
"skip_missing": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("SEQUENCE", "SEQUENCE")
RETURN_NAMES = ("paths", "stems")
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def find_models(self, fn, paths, recursive):
for p in paths:
for f in (p.rglob(fn) if recursive else p.glob(fn)):
yield f.relative_to(p)
def go(self, filenames, model_type, recursive, skip_missing):
paths = [pathlib.Path(folder) for folder in folder_names_and_paths[model_type][0]]
available_files = []
file_index = {}
for p in paths:
for f in (p.rglob('*') if recursive else p.glob('*')):
try:
rel = f.relative_to(p)
available_files.append(rel)
if rel.name not in file_index:
file_index[rel.name] = []
file_index[rel.name].append(rel)
except ValueError:
pass
result = []
for fn in filenames:
if '..' in fn:
raise Exception(f'".." is not allowed: {fn}.')
candidates = []
if glob.has_magic(fn):
for f in available_files:
if f.match(fn):
candidates.append(f)
else:
potential = file_index.get(pathlib.Path(fn).name, [])
for f in potential:
if f.match(fn):
candidates.append(f)
if candidates:
result.extend(candidates)
elif not skip_missing:
raise Exception(f'Could not find file: {fn}')
return ([str(f) for f in result], [f.stem for f in result])
-159
View File
@@ -1,159 +0,0 @@
import ast
import functools
import itertools
from . import register_node
# Sequence creation.
@register_node
class Range:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"start": ("INT", {"default": 0, "min": -9999999, "max": 9999999, "step": 1}),
"stop": ("INT", {"default": 10, "min": -9999999, "max": 9999999, "step": 1}),
"step": ("INT", {"default": 1, "min": -9999999, "max": 9999999, "step": 1}),
},
}
RETURN_TYPES = ("SEQUENCE", )
RETURN_NAMES = ("sequence", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def go(self, start, stop, step):
return (range(start, stop, step), )
@register_node
class Literal:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"literal": ("STRING", {'default': '', 'multiline': True})
},
}
RETURN_TYPES = ("SEQUENCE", )
RETURN_NAMES = ("sequence", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def go(self, literal):
return (ast.literal_eval(literal), )
# Processing of existing sequences.
@register_node
class Reorder:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sequence": ("SEQUENCE", ),
"method": (("reverse", "sort", "reverse sort"), {"default": "sort"}),
},
}
RETURN_TYPES = ("SEQUENCE", )
RETURN_NAMES = ("sequence", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def go(self, sequence, method):
f = {"reverse": reversed, "sort": sorted, "reverse_sort": lambda x: sorted(x, reverse=True)}[method]
return (f(sequence), )
@register_node
class Combinations:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sequence": ("SEQUENCE", ),
"count": ("INT", {"default": 0, "min": 0, "max": 9999999, "step": 1}),
"replacement": ("BOOLEAN", {"default": False, "label_on": "Yes", "label_off": "No"}),
},
}
RETURN_TYPES = ("SEQUENCE", )
RETURN_NAMES = ("sequence", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def go(self, sequence, count, replacement):
if replacement:
return (itertools.combinations_with_replacement(sequence, count), )
else:
return (itertools.combinations(sequence, count), )
@register_node
class Permutations:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sequence": ("SEQUENCE", ),
"count": ("INT", {"default": 0, "min": 0, "max": 9999999, "step": 1}),
},
}
RETURN_TYPES = ("SEQUENCE", )
RETURN_NAMES = ("sequence", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def go(self, sequence, count):
return (itertools.permutations(sequence, count), )
@register_node
class Slice:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sequence": ("SEQUENCE",),
"start": ("INT", {"default": 0, "min": -9999999, "max": 9999999, "step": 1}),
"stop": ("INT", {"default": 9999999, "min": -9999999, "max": 9999999, "step": 1}),
"step": ("INT", {"default": 1, "min": -9999999, "max": 9999999, "step": 1}),
},
}
RETURN_TYPES = ("SEQUENCE", )
RETURN_NAMES = ("sequence", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def go(self, sequence, start, stop, step):
return (sequence[start:stop:step], )
@register_node
class Join:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sequence": ("SEQUENCE", ),
"join_str": ("STRING", {'default': ', '}),
},
}
RETURN_TYPES = ("SEQUENCE", )
RETURN_NAMES = ("sequence", )
FUNCTION = "go"
CATEGORY = "🔁 Sequential Batcher/Sequence"
def go(self, sequence, join_str):
return ([join_str.join(x) for x in sequence], )
-86
View File
@@ -5,92 +5,6 @@ import subprocess
import folder_paths
from . import register_node
@register_node
class FFmpegVideoStitcher:
"""Nodo final: Espera a que termine todo el lote secuencial y une los vídeos con FFmpeg."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"video_paths": ("VHS_FILENAMES", ),
"output_filename": ("STRING", {"default": "SCAIL_Final_Completo.mp4"}),
},
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("final_video_path", )
# CRÍTICO: Obliga a ComfyUI a recolectar todas las ejecuciones del loop antes de actuar.
INPUT_IS_LIST = True
FUNCTION = "stitch_videos"
CATEGORY = "🔁 Sequential Batcher/Video"
def stitch_videos(self, video_paths, output_filename):
# 1. Comprobación estricta de dependencias
if not shutil.which("ffmpeg"):
raise Exception("FFmpeg no está instalado o no se encuentra en el PATH del sistema. Por favor, instálalo para unir los vídeos.")
# Ensure video_paths is a valid list before iteration
if not video_paths or not isinstance(video_paths, list):
raise Exception("Error: 'video_paths' inválido. Se esperaba una lista.")
# 2. Aplanar la lista de rutas generadas (VHS_FILENAMES genera listas anidadas)
flat_paths = []
for path_group in video_paths:
if not isinstance(path_group, list):
# Sometimes path_group might be a string directly depending on how it's passed
if isinstance(path_group, str) and path_group.endswith(".mp4"):
flat_paths.append(path_group)
continue
for p in path_group:
if isinstance(p, str) and p.endswith(".mp4"):
flat_paths.append(p)
# Ensure output_filename is safe to read
if isinstance(output_filename, list):
if not output_filename:
out_name = "SCAIL_Final_Completo.mp4"
else:
out_name = output_filename[0]
else:
out_name = output_filename
if not isinstance(out_name, str) or not out_name.strip():
out_name = "SCAIL_Final_Completo.mp4"
# Add .mp4 extension if not present
if not out_name.lower().endswith('.mp4'):
out_name += '.mp4'
if not flat_paths:
return ("Error: No se encontraron vídeos .mp4 para unir.", )
# 3. Preparar directorio de salida estándar
out_dir = folder_paths.get_output_directory()
list_file_path = os.path.join(out_dir, "batch_concat_list.txt")
# 4. Crear archivo de concatenación seguro para FFmpeg
with open(list_file_path, 'w', encoding='utf-8') as f:
for video in sorted(flat_paths):
# Usar rutas absolutas aseguradas
f.write(f"file '{os.path.abspath(video)}'\n")
# 5. Ejecutar unión sin recodificación (Copy codec)
final_output = os.path.join(out_dir, out_name)
ffmpeg_cmd = [
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", list_file_path, "-c", "copy", final_output
]
try:
subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"[Sequential Batcher] Vídeo ensamblado exitosamente en: {final_output}")
except subprocess.CalledProcessError as e:
raise Exception(f"Error crítico de FFmpeg al unir los fragmentos: {e.stderr.decode()}")
return (final_output, )
@register_node
class WanFrameValidator: