Cambiar tema

Automatizar la generación de imágenes con la API de ComfyUI

Easton editorial illustration: large dark node-graph workspace with orange connected nodes

"La documentación oficial describe la validación y cola mediante /prompt, además de /ws, /history, /view, /queue y /interrupt."

Ya tienes un workflow estable de ComfyUI y necesitas cuatro imágenes principales para cada uno de 200 productos. Cambiar prompt y seed manualmente en cada ejecución no es viable. El primer POST a /prompt devuelve node_errors porque el archivo no está en API format. Al terminar, /history solo muestra filename y subfolder; el archivo real se descarga mediante /view.

Pasar de la GUI a una producción con scripts exige exportar el workflow correcto, esperar por WebSocket sin sondeos ciegos y controlar concurrencia y VRAM para evitar OOM. A continuación se incluyen un script mínimo, parámetros, estrategias de cola y una lista de ingeniería backend.

Acceso a la API local de ComfyUI Server

Inicio por CLI y puerto predeterminado

ComfyUI escucha en 127.0.0.1:8188 de forma predeterminada. El inicio normal basta para procesos locales. Especifica una dirección de escucha solo si otro equipo de la LAN debe conectarse:

# Local access only
python main.py --port 8188

# Allow LAN access; also configure a firewall, authentication, or a reverse proxy
python main.py --listen 0.0.0.0 --port 8188

Cuando la terminal muestre Starting server, abre http://127.0.0.1:8188. Si aparece la interfaz de ComfyUI, el servicio está listo.

Si falta VRAM, prueba --lowvram, --novram o, como último recurso lento, --cpu, según la versión. Con margen amplio también puedes evaluar --highvram. No asignes estas opciones a una capacidad fija: modelo, precisión, VAE y workflow cambian el pico. Revisa python main.py --help y la documentación oficial actual.

Rutas principales de la API

Las rutas locales se definen en server.py. Un script suele necesitar estos endpoints:

RutaFunciónParámetros/respuesta
/promptValidar un workflow y añadirlo a la colaPOST {"prompt": workflow_dict, "client_id": "..."}; devuelve prompt_id o node_errors
/history/{prompt_id}Consultar historial y metadataGET; outputs incluye filename, subfolder y type
/view?filename=...&subfolder=...&type=...Descargar un archivoGET; devuelve datos binarios
/wsRecibir estado por WebSocketws://127.0.0.1:8188/ws?clientId=...
/queueConsultar la colaGET; devuelve tareas pendientes
/interruptInterrumpir la ejecución actualPOST; útil para timeouts
/upload/imageSubir una imagen de entradaPOST multipart/form-data
/object_infoConsultar tipos de nodos y parámetrosGET; permite verificar un nodo

Las rutas pueden cambiar entre versiones. Consulta la documentación actual o server.py si el comportamiento difiere.

Tipos de mensajes WebSocket

Al esperar una tarea, observa estos mensajes:

  • status: estado de la cola, incluido queue_remaining
  • execution_start: inicio con prompt_id
  • execution_cached: nodos reutilizados desde caché
  • executing: nodo actual; node is None con el prompt_id correcto indica finalización
  • progress: pasos actual y total
  • executed: nodo finalizado con metadata de salida

Para detectar el final, recibe type == "executing", comprueba que data.node sea None y que data.prompt_id coincida con la tarea enviada.

Exportar un workflow en API format

Pasos de Export Workflow (API)

El JSON guardado por la interfaz no es la representación que espera la API. Un workflow normal puede fallar con node_errors, por lo que primero debes exportar el API format.

Sigue estos pasos:

  1. Carga en ComfyUI un workflow que ya genere una imagen válida
  2. Elige File -> Export Workflow (API); algunas versiones muestran Save (API Format)
  3. Guarda un .json, por ejemplo workflow_api.json
  4. Comprueba ID numéricos como "3" y "6", además de class_type e inputs en cada nodo

El nombre del menú puede cambiar; usa la función equivalente para exportar a API de tu versión.

API format frente a Save format

El Save format incluye datos de diseño de la interfaz. El API format conserva solo lo necesario para ejecutar:

FormatoContenidoUso
Save formatPosiciones, colores, grupos, tamaños y enlaces visualesReabrir y editar el diseño en la interfaz
API formatID numéricos, class_type, inputs y _meta opcionalEnviar el workflow mediante script o API

Enviar un Save format a /prompt puede devolver node_errors o error. Cárgalo en la interfaz y vuelve a exportarlo.

Gestionar los ID de nodos

La parametrización necesita los ID de prompt, seed, dimensiones y salida. Localiza:

  • Nodo de prompt: CLIPTextEncode, a menudo "6" en ejemplos simples
  • Nodo de seed: KSampler, a menudo "3"
  • Nodo de width/height: una entrada de EmptyLatentImage u otro nodo específico
  • Nodo de salida: SaveImage o SaveImageWebsocket

Notes y Groups pueden documentar la función, pero el script debe leer el JSON exportado. Los ID pertenecen al grafo concreto y pueden cambiar al volver a exportar.

Script mínimo: enviar, esperar y descargar

El flujo tiene tres pasos: enviar a /prompt, esperar y leer metadata en /history para descargar mediante /view.

Envío HTTP sin espera

El cliente mínimo hace POST a /prompt y no espera. Un worker separado puede consultar las tareas.

import json
import requests

# Load an API-format workflow
with open("workflow_api.json", "r") as f:
    workflow = json.load(f)

# Build the request payload
payload = {
    "prompt": workflow,
    "client_id": "my-script-client"  # Optional; associates the job with WebSocket events
}

# Add the job to the queue
response = requests.post("http://127.0.0.1:8188/prompt", json=payload)

if response.status_code == 200:
    result = response.json()
    prompt_id = result["prompt_id"]
    print(f"Submitted: {prompt_id}")
else:
    error = response.json()
    print(f"Submission failed: {error}")
    # node_errors contains node-level validation details

Una respuesta correcta incluye {"prompt_id": "...", "number": ...}; un error incluye {"error": {...}, "node_errors": {...}}. El envío solo añade la tarea a la cola.

Esperar por WebSocket

Los eventos WebSocket evitan un polling agresivo. Conéctate, envía la tarea y espera el evento terminal executing.

import json
import uuid
import requests
import websocket

# Load the workflow
with open("workflow_api.json", "r") as f:
    workflow = json.load(f)

# Generate a client ID
client_id = str(uuid.uuid4())

# Connect to WebSocket
ws = websocket.create_connection(f"ws://127.0.0.1:8188/ws?clientId={client_id}")

# Submit the job
payload = {"prompt": workflow, "client_id": client_id}
response = requests.post("http://127.0.0.1:8188/prompt", json=payload)
prompt_id = response.json()["prompt_id"]

# Wait for completion
while True:
    message = ws.recv()
    data = json.loads(message)

    if data["type"] == "executing":
        # node is None with the same prompt_id when the job is complete
        if data["data"]["node"] is None and data["data"]["prompt_id"] == prompt_id:
            print("Job complete")
            break

ws.close()

Añade una espera máxima, por ejemplo 300 segundos. /interrupt detiene la ejecución actual, así que úsalo con cuidado.

Descargar con History y View

Tras finalizar, solicita /history/{prompt_id} para obtener metadata y descarga cada archivo mediante /view.

import requests

# Retrieve job history
history_url = f"http://127.0.0.1:8188/history/{prompt_id}"
history = requests.get(history_url).json()

# Traverse output nodes
outputs = history[prompt_id]["outputs"]
for node_id, node_output in outputs.items():
    if "images" in node_output:
        for image in node_output["images"]:
            filename = image["filename"]
            subfolder = image.get("subfolder", "")
            type = image.get("type", "output")

            # Build the download URL
            view_url = f"http://127.0.0.1:8188/view?filename={filename}&subfolder={subfolder}&type={type}"

            # Download the binary image
            img_data = requests.get(view_url).content
            with open(f"output_{filename}", "wb") as f:
                f.write(img_data)
            print(f"Saved: output_{filename}")

outputs sigue {node_id: {"images": [{"filename": "...", "subfolder": "...", "type": "..."}]}}. History entrega metadata y /view el binario.

Parametrizar tareas por lotes

Un workflow estable se convierte en plantilla. Cada iteración cambia solo prompt, seed, dimensiones u otra entrada elegida.

Parametrizar prompt, seed y dimensiones

Modifica los valores bajo inputs:

import json
import random

# Load the workflow
with open("workflow_api.json", "r") as f:
    workflow = json.load(f)

# Change the prompt; use the IDs from your own workflow
workflow["6"]["inputs"]["text"] = "a beautiful landscape, sunset, mountains"

# Generate a random seed
workflow["3"]["inputs"]["seed"] = random.randint(0, 1000000)

# Change the dimensions
workflow["5"]["inputs"]["width"] = 1024
workflow["5"]["inputs"]["height"] = 768

# Submit the job...

Confirma los ID "6", "3" y "5" en tu exportación. Después, un bucle puede cambiar prompt y seed antes de cada envío:

prompts = [
    "product photo, white background",
    "product photo, outdoor scene",
    "product photo, studio lighting"
]

for i, prompt_text in enumerate(prompts):
    workflow["6"]["inputs"]["text"] = prompt_text
    workflow["3"]["inputs"]["seed"] = random.randint(0, 1000000)

    # Submit the job
    payload = {"prompt": workflow, "client_id": client_id}
    response = requests.post("http://127.0.0.1:8188/prompt", json=payload)
    prompt_id = response.json()["prompt_id"]

    # Wait over WebSocket...
    # Download outputs...

Los ejemplos oficiales también modifican KSampler.seed y CLIPTextEncode.text, útiles para localizar parámetros.

Estrategias de cola para lotes

Para 200 imágenes puedes aumentar batch size o enviar muchos prompts. La opción segura depende del modelo y del margen de VRAM medido.

EstrategiaCaracterísticasCasos adecuados
Una imagen por envíoVRAM más fácil de controlar por tareaPoco margen, modelos grandes o workflows complejos
Batch sizeVarias imágenes en un prompt y, normalmente, mayor pico de VRAMMargen medido, modelos pequeños y workflows simples
Solicitudes concurrentesVarios prompts con número activo limitadoCapacidad medida y workers controlados

Enviar 20 prompts a la vez puede saturar la cola y causar OOM o un cierre del servicio.

El inicio conservador combina envío secuencial, finalización WebSocket y monitorización de VRAM. Envía la siguiente tarea después de la anterior. Si falta memoria, reduce la carga o prueba opciones actuales.

Ejemplo de control de concurrencia

En un servidor con varias GPU o workers aislados, un semáforo limita las tareas activas:

import copy
import threading

# Allow at most two active jobs
semaphore = threading.Semaphore(2)

def submit_and_wait(prompt_text, seed):
    with semaphore:
        # Give each task its own copy instead of mutating shared state
        job_workflow = copy.deepcopy(workflow)
        job_workflow["6"]["inputs"]["text"] = prompt_text
        job_workflow["3"]["inputs"]["seed"] = seed

        # Submit and wait...
        # WebSocket loop...

    # The permit is returned for the next task

# Submit the batch
threads = []
for i in range(50):
    t = threading.Thread(target=submit_and_wait, args=(prompts[i], seeds[i]))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

Empieza con concurrencia 1. Aumenta solo después de medir pico, latencia y fallos con el workflow real. Precisión, VAE, posprocesado y aislamiento cambian el límite.

Monitorizar la VRAM

Durante el lote, consulta estadísticas y pausa los nuevos envíos por encima de un umbral:

import time

def check_vram(threshold=0.8):
    stats = requests.get("http://127.0.0.1:8188/system_stats").json()
    vram_used = stats["system_stats"]["devices"][0]["vram_used"]
    vram_total = stats["system_stats"]["devices"][0]["vram_total"]
    return (vram_used / vram_total) > threshold

# Check memory before each submission
for prompt_text in prompts:
    while check_vram(0.85):
        print("VRAM pressure is high; waiting 30 seconds...")
        time.sleep(30)

    # Submit the next job...
    # Wait over WebSocket...

El pico suele aparecer durante KSampler y puede bajar al terminar. Un intervalo de 10 a 30 segundos evita saturar el servicio.

Archivar las salidas

Las salidas necesitan una estructura predecible. {prompt_id}_{seed}_{timestamp}.png conserva ID, seed y hora.

Registra como mínimo:

  • prompt_id: ID de tarea de ComfyUI
  • seed: seed aleatorio
  • prompt_text: prompt usado
  • width/height: dimensiones
  • timestamp: hora de generación
  • business_id: identificador de negocio como SKU o pedido

Organiza por fecha o lote, por ejemplo outputs/20260624/batch_001/. SQLite o PostgreSQL puede relacionar metadata y rutas.

Lista de ingeniería backend

Un backend necesita request ID, timeouts, límites de cola, monitorización de memoria y tratamiento explícito de errores. Un script funcional no basta para producción.

Request ID e idempotencia

Genera un request_id único, como UUID o pedido, y relaciónalo con prompt_id. Si vuelve un ID ya terminado, devuelve el resultado existente.

import uuid

# Business request ID
request_id = str(uuid.uuid4())

# Store the mapping in a database or cache
request_prompt_map[request_id] = prompt_id

# Return an existing result for duplicate requests
if request_id in completed_requests:
    return get_cached_result(request_id)

ComfyUI crea prompt_id, distinto del request_id de la aplicación. Guarda la relación.

Timeouts y cancelación

Define una duración máxima, por ejemplo 300 segundos. Al superarla, /interrupt detiene la ejecución actual.

import time

timeout = 300  # Five minutes
start_time = time.time()

# Wait for WebSocket messages...
while True:
    elapsed = time.time() - start_time
    if elapsed > timeout:
        # Interrupt the current execution
        requests.post("http://127.0.0.1:8188/interrupt")
        print("Job timed out and was interrupted")
        break

    # Process normal messages...

Si WebSocket se desconecta o queda en silencio, vuelve a conectar o consulta history. Tras interrumpir, revisa /queue y decide sobre las tareas pendientes.

Límites de cola y monitorización de VRAM

Establece un máximo explícito, como cinco tareas pendientes, y rechaza o aplaza el exceso. Limita también los workers activos.

Consulta /system_stats para revisar la VRAM:

stats = requests.get("http://127.0.0.1:8188/system_stats").json()
vram_used = stats["system_stats"]["devices"][0]["vram_used"]
vram_total = stats["system_stats"]["devices"][0]["vram_total"]
vram_percent = vram_used / vram_total

if vram_percent > 0.8:
    print("VRAM pressure is high; rejecting a new request")

Con presión alta, rechaza trabajo nuevo o espera a que baje la cola. Reduce batch size y complejidad antes de repetir.

Clasificación de errores y reintentos

Cada fallo requiere una respuesta distinta:

ErrorCausa probableTratamiento
node_errorsModelo/nodo ausente, parámetro inválido o entrada faltanteCorregir workflow y entorno; no reintentar
OOMVRAM insuficienteReducir batch/carga y cambiar una opción verificada; no repetir igual
Desconexión WebSocketProblema de redReconectar y consultar /history/{prompt_id}
Timeout de tareaCarga lenta o workflow complejoAumentar timeout o simplificar; reintentar una vez
Cierre del servicioMemoria agotada o fallo de GPURevisar logs, reiniciar y reintentar con cautela

node_errors identifica clases ausentes y tipos incompatibles. Corrige workflow, custom nodes o modelos. Un OOM también exige cambiar la carga.

Comparar API Cloud y local

Comfy Cloud ejecuta workflows alojados, pero autenticación, estado, WebSocket y concurrencia difieren. El API format se reutiliza; los endpoints deben seguir la referencia actual.

Diferencias de rutas

Las diferencias principales son:

FunciónAPI localAPI Cloud
Enviar tarea/prompt/api/prompt
Estado/resultados/history/{prompt_id}/api/job/{prompt_id}/status y /api/jobs/{job_id}
Descargar salida/view/api/view
WebSocket/ws?clientId=.../ws?clientId=...&token=...

Cloud requiere X-API-Key y una suscripción. La API local solo escucha 127.0.0.1 por defecto; con --listen, proxy o redirección debes añadir autenticación y control de acceso. Cloud sigue siendo experimental. /api/history_v2/{prompt_id} está deprecated en favor de /api/jobs/{job_id}.

Concurrencia y límites de suscripción

La concurrencia Cloud depende del plan; el exceso espera en cola. Las salidas viven en cloud storage y /api/view devuelve una URL firmada temporal.

Planes, límites, tiempo y precio pueden cambiar, por lo que no se fijan cifras. Cloud evita gestionar la GPU local; en local administras cola, VRAM, seguridad y estabilidad.

Próximos pasos

Dentro de la serie

Esta página lleva de un workflow funcional a una producción por lotes programable:

  • Reutilizar workflows de ComfyUI: importación, nodos ausentes y rutas de modelos
  • Optimizar ComfyUI con poca VRAM: memoria, batch, OOM y Tiled VAE
  • Vídeo con ComfyUI: límite entre lotes de imágenes y workflows de vídeo
  • Mantenimiento de ComfyUI: nodos ausentes, inicio y conflictos de versiones

Temas relacionados

Para patrones más amplios de automatización:

  • Crear workflows de IA con n8n: conectar ComfyUI con varias herramientas
  • API de Ollama: llamadas programáticas, colas y salida estructurada
  • Salida LLM estructurada: extracción fiable e integración API

Conclusión

El paso a producción con scripts consiste en exportar el workflow API, esperar por WebSocket, descargar con /history y /view, parametrizar entradas y añadir request ID, timeout, límites y monitorización VRAM.

Empieza con un workflow y una imagen. Genera después diez imágenes parametrizadas y observa memoria y cola. Añade idempotencia, cancelación, monitorización y registros cuando el lote sea estable.

Ejecutar el primer lote con la API de ComfyUI

Valida el recorrido local desde la exportación hasta el archivo de resultados.

  1. 1

    Step 1: Validar el workflow GUI

    Genera una imagen fiable y revisa modelos, custom nodes, entradas y nodos de salida.
  2. 2

    Step 2: Exportar API format

    Usa Export Workflow (API) y comprueba class_type e inputs en cada nodo.
  3. 3

    Step 3: Enviar una tarea

    Haz POST del workflow a /prompt, conserva prompt_id y revisa node_errors si falla.
  4. 4

    Step 4: Esperar y descargar

    Espera por /ws o consulta /history/{prompt_id}; descarga cada salida mediante /view.
  5. 5

    Step 5: Parametrizar entradas

    Copia la plantilla, cambia prompt, seed, width, height y filename_prefix y valida node ID y class_type.
  6. 6

    Step 6: Añadir protecciones

    Empieza en serie y añade job_id, idempotencia, límite de cola, timeout, VRAM, errores y archivo.

FAQ

¿Qué workflow JSON usa la API de ComfyUI?
Usa API format. El Save format contiene diseño de la interfaz; cárgalo en ComfyUI y expórtalo con Export Workflow (API).
¿Cuál es el flujo mínimo de la API local?
Envía el workflow a /prompt, guarda prompt_id, espera por WebSocket o /history/{prompt_id} y llama a /view con filename, subfolder y type.
¿Qué significa node_errors?
Contiene errores por nodo: modelo o nodo ausente, tipo incorrecto o entrada faltante. Corrige workflow o entorno antes de reintentar.
¿Puedo recuperar el resultado si WebSocket se corta?
Sí. Conserva prompt_id, reconecta o consulta /history/{prompt_id} y /queue. WebSocket no debe ser el único registro.
¿Conviene aumentar batch size o enviar varios prompts?
En una GPU local, batch 1 y envíos secuenciales son el inicio más seguro. La cola facilita fallos, reintentos y archivo.
¿Se pueden enviar varios prompts en paralelo?
Sí, pero empieza con concurrencia 1, copia el workflow por tarea y aumenta tras medir VRAM, cola y fallos.
¿Comfy Cloud API y la API local son iguales?
No. Cloud requiere X-API-Key y suscripción; sus endpoints de estado, WebSocket y resultados difieren y sigue siendo experimental.

12 min de lectura · Publicado el: 24 jul 2026 · Actualizado el: 24 jul 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog