diff --git a/README.md b/README.md index aff808d..803c138 100644 --- a/README.md +++ b/README.md @@ -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.* \ No newline at end of file diff --git a/README_ES.md b/README_ES.md index ba88f10..ad8c138 100644 --- a/README_ES.md +++ b/README_ES.md @@ -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á.* \ No newline at end of file diff --git a/__init__.py b/__init__.py index b1bd376..261e9cb 100644 --- a/__init__.py +++ b/__init__.py @@ -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'] \ No newline at end of file diff --git a/batch.py b/batch.py deleted file mode 100644 index afd4e48..0000000 --- a/batch.py +++ /dev/null @@ -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,) diff --git a/debug.py b/debug.py deleted file mode 100644 index a20e157..0000000 --- a/debug.py +++ /dev/null @@ -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 () diff --git a/image.py b/image.py index 268d069..22b5f3b 100644 --- a/image.py +++ b/image.py @@ -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 diff --git a/loop.py b/loop.py index 14c43c2..9ec7ade 100644 --- a/loop.py +++ b/loop.py @@ -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 () diff --git a/paths.py b/paths.py deleted file mode 100644 index e2d171b..0000000 --- a/paths.py +++ /dev/null @@ -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]) diff --git a/sequence.py b/sequence.py deleted file mode 100644 index ea434a1..0000000 --- a/sequence.py +++ /dev/null @@ -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], ) diff --git a/video.py b/video.py index 89976df..8f6d7ec 100644 --- a/video.py +++ b/video.py @@ -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: