diff --git a/README.md b/README.md index 04c6557..d9f2bb0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ComfyUI Sequential Batcher (v1.1.0) +# ComfyUI Sequential Batcher (v1.3.0) 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. @@ -8,7 +8,7 @@ Una suite altamente especializada de nodos personalizados para ComfyUI diseñada 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. -En la **v1.1.0**, hemos introducido capacidades de guardado a disco progresivo, soporte para multiplexado de audio y un hackeo profundo a nivel de JSON para vencer la agresiva caché de ComfyUI. +En la **v1.3.0**, hemos implementado el **"Cerebro Proporcional"**. Hemos eliminado los cables de dependencias circulares (como `total_loops`) usando variables fantasma globales, y añadido un calculador matemático para repartir lotes de vídeo de forma perfecta sin caída de frames, soportando control de framerate (`select_every_nth`). ## Los 6 Nodos Principales @@ -26,7 +26,7 @@ El sistema se construye alrededor de tres categorías principales: - **💾 Guardado de Keyframes (¡Nuevo en v1.1.0!):** Ahora recibe el índice actual y realiza un volcado de seguridad en el disco duro, guardando progresivamente `keyframe_XXX.png` en cada ciclo para prevenir pérdidas de datos. ### 🎞️ 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). +5. **📊 Auto Loop Calculator (`AutoLoopCalculator`)**: Es el "Cerebro" obligatorio de la máquina. Calcula y reparte proporcionalmente los lotes de frames (incluso cuando usas salto de frames con `select_every_nth`), evitando caídas de VRAM en el último ciclo, y guarda el total de bucles en una memoria global invisible para orquestar al resto de los nodos automáticamente. 6. **🎞️ Incremental Auto-Stitcher (`IncrementalVideoStitcher`)**: Archiva progresivamente los tensores generados en el disco duro y los ensambla de forma segura al final de todos los ciclos. - **🧠 Cero OOM (¡Nuevo!):** Sustituye las acumulaciones en memoria por guardados temporales en disco (`.pt`), borrando la RAM de inmediato para poder procesar vídeos infinitos sin colapsar el sistema. Al desactivar `INPUT_IS_LIST`, maneja tensores puros eficientemente. - **🎵 Passthrough de Audio (¡Nuevo!):** Alimenta directamente el audio original hacia el archivo ensamblado en el último ciclo (devolviendo `None` en los ciclos intermedios para ahorrar recursos). @@ -45,12 +45,14 @@ El sistema se construye alrededor de tres categorías principales: 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`, `SessionImageSender`, y tu `Incremental Auto-Stitcher`. *¡No olvides conectar el Sender para el guardado de los keyframes!* - - Asegúrate de que su interruptor `reset_loop` está en `False`. +1. **El Inicio:** Añade el nodo `🏁 Loop Start (Index)` y el nodo `📊 Auto Loop Calculator`. + - Conecta el total de frames de tu vídeo a la entrada `source_frame_count` del calculador (o usa un nodo numérico Primitive si es Texto-a-Vídeo). + - Conecta la salida `current_loop_index` del `Loop Start` al calculador y a los nodos de Imagen y Video (Receiver, Sender, Stitcher). *¡No olvides conectar el Sender para el guardado de los keyframes!* + - Asegúrate de que su interruptor `reset_loop` del Loop Start está en `False`. + - Lleva las salidas `chunk_frames`, `skip_frames` y `select_every_nth` del calculador hacia tu cargador/generador de vídeo. 2. **Conectando el Audio (Opcional):** Si tu flujo tiene sonido, saca un cable de la salida de audio de tu nodo inicial (ej. `VHS_LoadVideo`) y conéctalo al puerto `audio` azul de tu `Incremental Auto-Stitcher`. -3. **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`. -4. **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. Gracias a la Inyección Anti-Caché, la caché se destruye en cada iteración y el progreso fluye hasta completar tu vídeo. +3. **El Final:** Añade el nodo `🚀 Loop Trigger (Auto-Queue)`. Crucial: Conecta la salida de imagen o audio 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 en disco temporal antes de disparar. (Nota: gracias a la memoria global fantasma, el Trigger ya sabe cuántos bucles hacer sin necesitar cables extras). +4. **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. Gracias a la Inyección Anti-Caché, la caché se destruye en cada iteración y el progreso fluye hasta completar tu vídeo perfectamente repartido. --- *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/README_EN.md b/README_EN.md index f06bb2c..0a2c04e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,4 +1,4 @@ -# ComfyUI Sequential Batcher (v1.1.0) +# ComfyUI Sequential Batcher (v1.3.0) 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. @@ -8,7 +8,7 @@ A highly specialized suite of custom nodes for ComfyUI designed for **Recursive 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. -In **v1.1.0**, we have introduced progressive disk saving capabilities, audio multiplexing support, and a deep JSON hack to defeat ComfyUI's aggressive cache. +In **v1.3.0**, we implemented the **"Proportional Brain"**. We've eliminated circular dependency cables (like `total_loops`) using global ghost variables, and added a mathematical calculator to perfectly distribute video batches without dropping frames, supporting framerate striding via `select_every_nth`. ## The 6 Core Nodes @@ -26,7 +26,7 @@ The system is built around three major categories: - **💾 Keyframe Dumping (New in v1.1.0!):** Now receives the current index and performs a safety dump to the hard drive, progressively saving `keyframe_XXX.png` every cycle to prevent data loss. ### 🎞️ 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). +5. **📊 Auto Loop Calculator (`AutoLoopCalculator`)**: The mandatory "Brain" of the machine. It proportionally calculates and distributes batches of frames (even when using frame skipping with `select_every_nth`), preventing VRAM spikes in the final cycle. It saves the total loops into an invisible global memory to orchestrate the rest of the nodes automatically. 6. **🎞️ Incremental Auto-Stitcher (`IncrementalVideoStitcher`)**: Progressively archives generated tensors directly to the hard drive and safely assembles them at the end of all cycles. - **🧠 Zero OOM (New!):** Replaces RAM accumulation with progressive temporary disk saves (`.pt`), clearing system memory immediately to enable infinite video processing without crashing the system. By disabling `INPUT_IS_LIST`, it handles raw tensors efficiently. - **🎵 Audio Passthrough (New!):** Feeds the original pure audio straight to the assembled output during the final loop iteration (returning `None` during intermediate loops to save resources). @@ -45,12 +45,14 @@ The system is built around three major categories: 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 `SessionImageReceiver`, `SessionImageSender`, and `Incremental Auto-Stitcher`. *Don't forget the Sender for keyframe saving!* - - Ensure `reset_loop` is set to `False`. +1. **The Start:** Add the `🏁 Loop Start (Index)` and the `📊 Auto Loop Calculator` nodes. + - Connect your video's total frames to the `source_frame_count` input of the calculator (or use a numeric Primitive node if it's Text-to-Video). + - Connect the `current_loop_index` output of `Loop Start` to the calculator, and to your Image and Video nodes (Receiver, Sender, Stitcher). *Don't forget the Sender for keyframe saving!* + - Ensure the `reset_loop` toggle on the Loop Start is set to `False`. + - Wire the `chunk_frames`, `skip_frames`, and `select_every_nth` outputs from the calculator into your video loader/generator. 2. **Connecting Audio (Optional):** If your workflow has sound, pull a cable from your initial node's audio output (e.g., `VHS_LoadVideo`) and connect it to the blue `audio` port on your `Incremental Auto-Stitcher`. -3. **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`. -4. **Execution:** **You no longer need to check "Auto Queue".** Just press "Queue Prompt" **once**. Batch 0 starts, and upon finishing, the trigger invisibly signals the server. Thanks to Anti-Cache Injection, cache is destroyed on every iteration, and progress flows until your video is complete. +3. **The End:** Add the `🚀 Loop Trigger (Auto-Queue)` node. Crucially, connect the image or audio output from your `Incremental Auto-Stitcher` into the `trigger_dependency` input. This forces the trigger to wait until the video is physically saved to the temporary disk before firing. (Note: thanks to the global ghost memory, the Trigger already knows how many loops to execute without needing extra cables). +4. **Execution:** **You no longer need to check "Auto Queue".** Just press "Queue Prompt" **once**. Batch 0 starts, and upon finishing, the trigger invisibly signals the server. Thanks to Anti-Cache Injection, cache is destroyed on every iteration, and progress flows until your video is perfectly distributed and complete. --- *Created to push the boundaries of ComfyUI automation.* \ No newline at end of file diff --git a/__init__.py b/__init__.py index 1d6fe62..7f056ae 100644 --- a/__init__.py +++ b/__init__.py @@ -21,7 +21,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update({ # Video (Ensamblaje y Validación) "IncrementalVideoStitcher": "🎞️ Incremental Auto-Stitcher", "LoadVideoWithSourceAudio": "🎥 Load Video + Source Audio", - "WanFrameValidator": "🛡️ Wan Frame Validator", + "AutoLoopCalculator": "📊 Auto Loop Calculator", # Loop (Orquestación Autónoma) "SequentialLoopStart": "🏁 Loop Start (Index)", diff --git a/image.py b/image.py index 86f42cc..32ac2f1 100644 --- a/image.py +++ b/image.py @@ -26,7 +26,6 @@ class SessionImageReceiver: "required": { "initial_image": ("IMAGE",), "current_loop_index": ("INT", {"default": 0, "min": 0, "max": 10000}), - "total_loops": ("INT", {"default": 1, "min": 1, "max": 10000}), }, } @@ -40,7 +39,10 @@ class SessionImageReceiver: def IS_CHANGED(cls, **kwargs): return time.time() - def get_image(self, initial_image, current_loop_index, total_loops): + def get_image(self, initial_image, current_loop_index): + from . import loop + total_loops = loop.global_total_loops + global global_session_image loop_idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index is_first = (loop_idx == 0) @@ -71,7 +73,6 @@ class SessionImageSender: "required": { "generated_images": ("IMAGE",), "current_loop_index": ("INT", {"default": 0, "min": 0, "max": 10000}), - "total_loops": ("INT", {"default": 1, "min": 1, "max": 10000}), }, } @@ -85,7 +86,10 @@ class SessionImageSender: def IS_CHANGED(cls, **kwargs): return time.time() - def set_image(self, generated_images, current_loop_index, total_loops): + def set_image(self, generated_images, current_loop_index): + from . import loop + total_loops = loop.global_total_loops + if generated_images is None: raise ValueError("❌ ERROR CRÍTICO: El nodo 'Session Image Sender' no está recibiendo imágenes. Conecta la salida de tu VAE Decode o Sampler.") diff --git a/loop.py b/loop.py index 0311fe0..8e2b53d 100644 --- a/loop.py +++ b/loop.py @@ -5,6 +5,7 @@ import json from . import register_node global_loop_index = 0 +global_total_loops = 1 # 🌍 NUEVA MEMORIA GLOBAL PARA LOS CICLOS @register_node class SequentialLoopStart: @@ -14,7 +15,6 @@ class SequentialLoopStart: "required": { "reset_loop": ("BOOLEAN", {"default": False}), "loop_idx": ("INT", {"default": 0, "min": 0, "max": 10000}), - "total_loops": ("INT", {"default": 1, "min": 1, "max": 10000}), } } @@ -27,17 +27,19 @@ class SequentialLoopStart: def IS_CHANGED(cls, **kwargs): return time.time() - def get_index(self, reset_loop, loop_idx, total_loops): + def get_index(self, reset_loop, loop_idx): global global_loop_index + global global_total_loops print(f"\n{'='*50}") print(f"🚀 [DEBUG] NODO: Loop Start") - print(f" -> Input loop_idx (UI/Trigger): {loop_idx} / {total_loops - 1} (Total: {total_loops})") + print(f" -> Input loop_idx: {loop_idx} (Total Esperado: {global_total_loops})") print(f" -> Input reset_loop: {reset_loop}") is_reset = str(reset_loop).lower() in ['true', '1', 't', 'y'] if is_reset: global_loop_index = 0 + global_total_loops = 1 # Reinicio de seguridad limpia la memoria fantasma print(" -> 🔄 Bucle reiniciado a 0 manualmente.") else: global_loop_index = loop_idx @@ -54,7 +56,6 @@ class SequentialLoopTrigger: return { "required": { "trigger_dependency": ("*", ), - "target_loops": ("INT", {"default": 4, "min": 1, "max": 1000}), "port": ("INT", {"default": 8188, "min": 1000, "max": 9999}), }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"} @@ -70,11 +71,14 @@ class SequentialLoopTrigger: def IS_CHANGED(cls, **kwargs): return time.time() - def trigger_next(self, trigger_dependency, target_loops, port, prompt=None, extra_pnginfo=None): + def trigger_next(self, trigger_dependency, port, prompt=None, extra_pnginfo=None): if trigger_dependency is None: raise ValueError("❌ ERROR CRÍTICO: El nodo 'Loop Trigger' no tiene nada conectado en 'trigger_dependency'. Debes conectar la salida del Stitcher para que el bucle pueda continuar.") global global_loop_index + global global_total_loops + + target_loops = global_total_loops next_loop = global_loop_index + 1 print(f"\n{'='*50}") diff --git a/video.py b/video.py index aaf9293..f7e2d82 100644 --- a/video.py +++ b/video.py @@ -7,27 +7,61 @@ import time from . import register_node @register_node -class WanFrameValidator: +class AutoLoopCalculator: @classmethod def INPUT_TYPES(s): return { "required": { - "target_frames": ("INT", {"default": 49, "min": 1, "max": 10000}), - "current_loop_index": ("INT", {"default": 0, "forceInput": True}), + "source_frame_count": ("INT", {"forceInput": True}), + "target_frames_per_loop": ("INT", {"default": 50, "min": 1, "max": 10000}), + "select_every_nth": ("INT", {"default": 1, "min": 1, "max": 100}), # NUEVO PARÁMETRO + "current_loop_index": ("INT", {"forceInput": True}), } } - RETURN_TYPES = ("INT", "INT") - RETURN_NAMES = ("valid_frames", "skip_frames") - FUNCTION = "validate" + # Ahora devolvemos 3 valores para el cargador de vídeo + RETURN_TYPES = ("INT", "INT", "INT") + RETURN_NAMES = ("chunk_frames", "skip_frames", "select_every_nth") + FUNCTION = "calculate" CATEGORY = "🔁 Sequential Batcher/Video" - def validate(self, target_frames, current_loop_index): - k = (target_frames - 1) // 4 - corrected_frames = max(1, (4 * k) + 1) - skip_frames = current_loop_index * corrected_frames - print(f"🛡️ [Wan Validator] Lote: {corrected_frames} frames | Saltar: {skip_frames} frames") - return (corrected_frames, skip_frames) + def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index): + import math + from . import loop + + if source_frame_count <= 0 or target_frames_per_loop <= 0: + loop.global_total_loops = 1 + return (max(1, source_frame_count), 0, select_every_nth) + + # 1. Calcular los frames efectivos reales que vamos a procesar + effective_source_frames = math.ceil(source_frame_count / select_every_nth) + + # 2. La matemática proporcional del usuario SOBRE LOS FRAMES EFECTIVOS + total_loops = math.ceil(effective_source_frames / target_frames_per_loop) + base_frames = effective_source_frames // total_loops + remainder = effective_source_frames % total_loops + + plan = [] + for i in range(total_loops): + plan.append(base_frames + (1 if i < remainder else 0)) + + safe_index = min(current_loop_index, total_loops - 1) + chunk_frames = plan[safe_index] + + # 3. Calcular el salto de frames originales + # Sumamos los frames efectivos de ciclos anteriores y los multiplicamos por el salto + effective_skip = sum(plan[:safe_index]) + skip_frames = effective_skip * select_every_nth + + # Guardamos en la memoria fantasma + loop.global_total_loops = total_loops + + print(f"\n📊 [Auto Calculator] Video Original: {source_frame_count} frames | Stride: {select_every_nth}") + print(f" -> Frames efectivos a procesar: {effective_source_frames}") + print(f" -> Planificando {total_loops} ciclos: {plan}") + print(f" -> 🚀 Ciclo {current_loop_index}: Cargando {chunk_frames} frames (Saltando {skip_frames} frames de origen)") + + return (chunk_frames, skip_frames, select_every_nth) @register_node class LoadVideoWithSourceAudio: @@ -108,7 +142,6 @@ class IncrementalVideoStitcher: "images": ("IMAGE",), "audio": ("AUDIO",), "current_loop_index": ("INT", {"default": 0}), - "total_loops": ("INT", {"default": 1}), } } @@ -117,7 +150,10 @@ class IncrementalVideoStitcher: FUNCTION = "stitch" CATEGORY = "🔁 Sequential Batcher/Video" - def stitch(self, images, audio, current_loop_index, total_loops): + def stitch(self, images, audio, current_loop_index): + from . import loop + total_loops = loop.global_total_loops # Leemos la variable global + cache_dir = os.path.join(folder_paths.get_temp_directory(), "wan_stitcher_cache") os.makedirs(cache_dir, exist_ok=True)