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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
Reference in New Issue
Block a user