fix(ml): comfyui_wait_result no sale prematuro en jobs de video/3D

Exige outputs no vacios (no solo status terminal) para dar por completado
un prompt: en jobs pesados ComfyUI marca la entry de /history como
terminada antes de poblar outputs, lo que devolvia un dict vacio mientras
el job seguia en GPU. Ahora sigue sondeando hasta que los outputs aparecen
o hasta agotar el timeout. Timeout default 180s -> 600s (cubre video/3D) y
timeout HTTP por-request acotado a 30s. Firma y contrato de retorno intactos.

Tests nuevos (mock urllib CI-safe + live opcional contra /history real):
golden, regresion del bug, edge imagen corta, timeout y error. v1.0.0 -> 1.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 12:39:58 +02:00
parent 2fe36e314e
commit 898502a321
3 changed files with 240 additions and 24 deletions
+34 -11
View File
@@ -17,37 +17,55 @@ import urllib.request
def comfyui_wait_result(
prompt_id: str,
server: str = "127.0.0.1:8188",
timeout: float = 180.0,
timeout: float = 600.0,
poll_interval: float = 1.0,
) -> dict:
"""Espera a que ComfyUI termine de ejecutar un prompt y devuelve sus outputs.
Sondea GET /history/{prompt_id} cada poll_interval segundos hasta que
status.completed es True o status.status_str es "success"/"error", o hasta
agotar el timeout.
Sondea GET /history/{prompt_id} cada poll_interval segundos hasta que el
prompt aparece en /history con un estado terminal de exito (status.completed
True o status.status_str "success") **y** outputs ya poblados, o hasta que
falla (status_str "error"), o hasta agotar el timeout.
El requisito de outputs no vacios es deliberado: en jobs pesados (video, 3D)
ComfyUI puede exponer la entry de /history con el estado marcado como
terminado antes de haber poblado los outputs. Devolver en ese instante daba
un dict vacio mientras el job seguia en GPU (salida prematura). Por eso, si
el estado dice "terminado" pero los outputs aun no estan, se sigue sondeando
hasta que aparezcan o hasta agotar el timeout, que actua como red de
seguridad.
Args:
prompt_id: id devuelto por comfyui_submit_workflow.
server: host:port del servidor ComfyUI (sin esquema).
timeout: maximo de segundos a esperar antes de fallar.
timeout: maximo de segundos a esperar antes de fallar. El default amplio
(600s) cubre workflows largos de video/3D; para imagenes cortas el
retorno sigue siendo inmediato en cuanto los outputs estan listos.
poll_interval: segundos entre sondeos.
Returns:
dict de outputs {node_id: {"images": [{filename, subfolder, type}, ...]}}
tal como ComfyUI los expone en history[prompt_id]["outputs"]. Puede
contener otros tipos de output (gifs, texto) segun los nodos del
workflow.
contener otros tipos de output (gifs, texto, video bajo "images" con
"animated") segun los nodos del workflow. Siempre no vacio en caso de
exito (un workflow sin nodo de guardado agotaria el timeout).
Raises:
TimeoutError: si se agota el timeout sin que el prompt complete.
TimeoutError: si se agota el timeout sin que el prompt complete con
outputs (incluye el caso de un prompt_id que nunca aparece en
/history porque sigue en cola, no existe, o el workflow no produce
outputs).
RuntimeError: si la ejecucion termina con status_str "error", si no se
puede conectar, o si la respuesta no es JSON valido.
"""
url = f"http://{server}/history/{prompt_id}"
# Timeout por-request acotado: una conexion colgada no debe bloquear todo el
# presupuesto global (que ahora puede ser de varios minutos).
req_timeout = min(timeout, 30.0) if timeout > 0 else 30.0
deadline = time.time() + timeout
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=timeout) as resp:
with urllib.request.urlopen(url, timeout=req_timeout) as resp:
hist = json.loads(resp.read())
except urllib.error.URLError as exc:
raise RuntimeError(
@@ -67,8 +85,13 @@ def comfyui_wait_result(
f"comfyui_wait_result: ejecucion fallo para {prompt_id}: "
f"{json.dumps(status)[:500]}"
)
if status.get("completed") or status_str == "success":
return entry.get("outputs", {})
done = bool(status.get("completed")) or status_str == "success"
outputs = entry.get("outputs") or {}
# Solo se considera terminado cuando hay estado de exito Y outputs.
# "done" con outputs vacios = entry creada pero aun sin resultados:
# se sigue esperando (no salir prematuro).
if done and outputs:
return outputs
time.sleep(poll_interval)
raise TimeoutError(