From 6da42759754ff1bc0035ef493be2ac2d666ed040 Mon Sep 17 00:00:00 2001 From: Meisoftcoltd <38667978+Meisoftcoltd@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:12:43 +0000 Subject: [PATCH] feat: implement recursive self-queuing nodes for autonomous execution - Add `SequentialLoopStart` and `SequentialLoopTrigger` nodes in `loop.py`. - Manage global loop index for memory persistence across cycles. - Implement auto-queue HTTP POST trigger using local ComfyUI API `/prompt`. - Add non-blocking timeout of 5s on urllib requests. - Register new nodes in `__init__.py` under NODE_DISPLAY_NAME_MAPPINGS. --- __init__.py | 2 ++ loop.py | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/__init__.py b/__init__.py index b1bd376..6e094ae 100644 --- a/__init__.py +++ b/__init__.py @@ -67,6 +67,8 @@ NODE_DISPLAY_NAME_MAPPINGS.update({ # Loop "LoopIndex": "🔁 Sequential Loop Index", "Repeat": "🔁 Repeat", + "SequentialLoopStart": "🏁 Loop Start (Index)", + "SequentialLoopTrigger": "🚀 Loop Trigger (Auto-Queue)", }) # Backward compatibility for "Job" naming diff --git a/loop.py b/loop.py index 14c43c2..3743cac 100644 --- a/loop.py +++ b/loop.py @@ -1,5 +1,10 @@ +import urllib.request +import json from . import register_node +# Variable global para mantener el estado del bucle entre ejecuciones +global_loop_index = 0 + class AnyType(str): def __ne__(self, __value: object) -> bool: return False @@ -36,3 +41,99 @@ class Repeat: CATEGORY = "🔁 Sequential Batcher/Loop" def go(self, input, count): return ([input] * count,) + +@register_node +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": { + "reset_loop": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("current_loop_index",) + FUNCTION = "get_index" + CATEGORY = "🔁 Sequential Batcher/Loop" + + @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 ()