API ComfyUI

API ComfyUI

Pourquoi utiliser l'API ?

L'interface graphique de ComfyUI est idéale pour la création interactive, mais l'API ouvre la porte à l'automatisation et à l'intégration dans vos applications :

  • Génération d'images en batch automatisé
  • Intégration dans une application web ou mobile
  • Pipeline de production automatisé
  • Bot Discord/Telegram de génération d'images
  • Service SaaS de génération d'images
  • Tests et benchmarks automatisés

Architecture de l'API

ComfyUI expose une API REST et une API WebSocket sur le même port (8188 par défaut).

Endpoints principaux

Méthode Endpoint Description
POST /prompt Envoie un workflow pour exécution
GET /history Historique des générations
GET /history/{prompt_id} Résultat d'une génération spécifique
GET /view?filename=... Récupère une image générée
GET /queue État de la file d'attente
POST /queue Gère la file (supprimer, interrompre)
GET /object_info Liste tous les nœuds disponibles et leurs paramètres
POST /upload/image Upload une image d'entrée
GET /system_stats Statistiques système (VRAM, GPU)

WebSocket

L'API WebSocket (ws://127.0.0.1:8188/ws) fournit des mises à jour en temps réel :

  • Progression du sampling (pourcentage par step)
  • Notifications de complétion
  • Prévisualisations intermédiaires

Le format du Prompt API

Le cœur de l'API est le prompt : un objet JSON qui décrit le workflow complet sous forme de graphe de nœuds.

Structure

{
  "prompt": {
    "1": {
      "class_type": "CheckpointLoaderSimple",
      "inputs": {
        "ckpt_name": "juggernautXL.safetensors"
      }
    },
    "2": {
      "class_type": "CLIPTextEncode",
      "inputs": {
        "text": "a beautiful sunset over mountains",
        "clip": ["1", 1]
      }
    },
    "3": {
      "class_type": "CLIPTextEncode",
      "inputs": {
        "text": "blurry, low quality",
        "clip": ["1", 1]
      }
    },
    "4": {
      "class_type": "EmptyLatentImage",
      "inputs": {
        "width": 1024,
        "height": 1024,
        "batch_size": 1
      }
    },
    "5": {
      "class_type": "KSampler",
      "inputs": {
        "model": ["1", 0],
        "positive": ["2", 0],
        "negative": ["3", 0],
        "latent_image": ["4", 0],
        "seed": 42,
        "steps": 25,
        "cfg": 7.0,
        "sampler_name": "dpmpp_2m",
        "scheduler": "karras",
        "denoise": 1.0
      }
    },
    "6": {
      "class_type": "VAEDecode",
      "inputs": {
        "samples": ["5", 0],
        "vae": ["1", 2]
      }
    },
    "7": {
      "class_type": "SaveImage",
      "inputs": {
        "images": ["6", 0],
        "filename_prefix": "api_output"
      }
    }
  }
}

Comprendre les connexions

Les connexions entre nœuds sont représentées par des tuples ["node_id", output_index] :

  • ["1", 0] → sortie 0 du nœud 1 (MODEL pour un CheckpointLoader)
  • ["1", 1] → sortie 1 du nœud 1 (CLIP)
  • ["1", 2] → sortie 2 du nœud 1 (VAE)

Récupérer le JSON d'un workflow

Astuce : créez votre workflow dans l'interface graphique, puis :

  1. Activez Enable Dev Mode dans les paramètres
  2. Cliquez sur Save (API Format) → génère le JSON au format API
  3. Utilisez ce JSON comme base pour vos appels API

Intégration Python

Client basique

import json
import urllib.request
import urllib.parse

COMFYUI_URL = "http://127.0.0.1:8188"

def queue_prompt(prompt):
    """Envoie un workflow pour exécution."""
    data = json.dumps({"prompt": prompt}).encode('utf-8')
    req = urllib.request.Request(
        f"{COMFYUI_URL}/prompt",
        data=data,
        headers={'Content-Type': 'application/json'}
    )
    response = urllib.request.urlopen(req)
    return json.loads(response.read())

def get_history(prompt_id):
    """Récupère le résultat d'une génération."""
    response = urllib.request.urlopen(
        f"{COMFYUI_URL}/history/{prompt_id}"
    )
    return json.loads(response.read())

def get_image(filename, subfolder="", folder_type="output"):
    """Télécharge une image générée."""
    params = urllib.parse.urlencode({
        "filename": filename,
        "subfolder": subfolder,
        "type": folder_type
    })
    response = urllib.request.urlopen(
        f"{COMFYUI_URL}/view?{params}"
    )
    return response.read()

Client avec WebSocket (temps réel)

import websocket
import uuid
import json

class ComfyUIClient:
    def __init__(self, server_address="127.0.0.1:8188"):
        self.server_address = server_address
        self.client_id = str(uuid.uuid4())
        self.ws = websocket.WebSocket()
        self.ws.connect(
            f"ws://{server_address}/ws?clientId={self.client_id}"
        )

    def queue_prompt(self, prompt):
        """Envoie un prompt et retourne le prompt_id."""
        payload = {
            "prompt": prompt,
            "client_id": self.client_id
        }
        data = json.dumps(payload).encode('utf-8')
        req = urllib.request.Request(
            f"http://{self.server_address}/prompt",
            data=data,
            headers={'Content-Type': 'application/json'}
        )
        response = urllib.request.urlopen(req)
        return json.loads(response.read())['prompt_id']

    def wait_for_completion(self, prompt_id):
        """Attend la fin de la génération via WebSocket."""
        while True:
            msg = self.ws.recv()
            if isinstance(msg, str):
                data = json.loads(msg)
                if data['type'] == 'executing':
                    node = data['data'].get('node')
                    if node is None:
                        # Génération terminée
                        break
                elif data['type'] == 'progress':
                    step = data['data']['value']
                    total = data['data']['max']
                    print(f"  Progression : {step}/{total}")

    def generate(self, prompt):
        """Génère une image et retourne les fichiers résultants."""
        prompt_id = self.queue_prompt(prompt)
        print(f"Prompt envoyé : {prompt_id}")
        self.wait_for_completion(prompt_id)
        history = get_history(prompt_id)
        outputs = history[prompt_id]['outputs']
        images = []
        for node_id, node_output in outputs.items():
            if 'images' in node_output:
                for img_info in node_output['images']:
                    img_data = get_image(
                        img_info['filename'],
                        img_info.get('subfolder', ''),
                        img_info.get('type', 'output')
                    )
                    images.append(img_data)
        return images

    def close(self):
        self.ws.close()

Exemple d'utilisation

client = ComfyUIClient()

# Charger un workflow depuis un fichier JSON
with open("mon_workflow_api.json") as f:
    workflow = json.load(f)

# Modifier dynamiquement le prompt
workflow["2"]["inputs"]["text"] = "a cyberpunk city at night, neon lights"
workflow["5"]["inputs"]["seed"] = 12345

# Générer
images = client.generate(workflow)

# Sauvegarder
for i, img_data in enumerate(images):
    with open(f"result_{i}.png", "wb") as f:
        f.write(img_data)

client.close()

Intégration JavaScript/TypeScript

Client Node.js

import WebSocket from 'ws';

interface ComfyUIPromptResponse {
  prompt_id: string;
}

class ComfyUIClient {
  private serverUrl: string;
  private clientId: string;

  constructor(serverAddress = '127.0.0.1:8188') {
    this.serverUrl = `http://${serverAddress}`;
    this.clientId = crypto.randomUUID();
  }

  async queuePrompt(prompt: Record<string, unknown>): Promise<string> {
    const response = await fetch(`${this.serverUrl}/prompt`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        prompt,
        client_id: this.clientId,
      }),
    });
    const data: ComfyUIPromptResponse = await response.json();
    return data.prompt_id;
  }

  async waitForCompletion(promptId: string): Promise<void> {
    return new Promise((resolve) => {
      const ws = new WebSocket(
        `ws://${this.serverUrl.replace('http://', '')}/ws?clientId=${this.clientId}`
      );

      ws.on('message', (data: string) => {
        const msg = JSON.parse(data);
        if (msg.type === 'executing' && msg.data.node === null) {
          ws.close();
          resolve();
        }
      });
    });
  }

  async getImage(filename: string): Promise<Buffer> {
    const params = new URLSearchParams({ filename, type: 'output' });
    const response = await fetch(`${this.serverUrl}/view?${params}`);
    return Buffer.from(await response.arrayBuffer());
  }
}

Upload d'images

Pour les workflows nécessitant une image d'entrée (img2img, ControlNet, IP-Adapter) :

import requests

def upload_image(filepath, filename=None):
    """Upload une image vers ComfyUI."""
    if filename is None:
        filename = os.path.basename(filepath)

    with open(filepath, 'rb') as f:
        files = {'image': (filename, f, 'image/png')}
        data = {'overwrite': 'true'}
        response = requests.post(
            f"{COMFYUI_URL}/upload/image",
            files=files,
            data=data
        )
    return response.json()

# Utilisation
result = upload_image("photo_reference.png")
# result: {"name": "photo_reference.png", "subfolder": "", "type": "input"}

# Puis dans le workflow, référencer l'image :
workflow["load_image_node"]["inputs"]["image"] = "photo_reference.png"

Déploiement en production

Docker avec API exposée

version: '3'
services:
  comfyui:
    image: comfyanonymous/comfyui
    ports:
      - "8188:8188"
    volumes:
      - ./models:/app/models
      - ./output:/app/output
      - ./input:/app/input
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]
    command: >
      python main.py
      --listen 0.0.0.0
      --port 8188
      --enable-cors-header

Arguments utiles pour la production

python main.py \
  --listen 0.0.0.0 \        # Écouter sur toutes les interfaces
  --port 8188 \              # Port
  --enable-cors-header \     # Autoriser les requêtes cross-origin
  --preview-method auto \    # Prévisualisations pendant la génération
  --max-upload-size 100 \    # Taille max d'upload en Mo
  --disable-auto-launch      # Ne pas ouvrir le navigateur

Proxy avec Nginx

upstream comfyui {
    server 127.0.0.1:8188;
}

server {
    listen 443 ssl;
    server_name comfyui.example.com;

    location / {
        proxy_pass http://comfyui;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 300s;
    }
}

Sécurité

  • Ne jamais exposer ComfyUI directement sur Internet sans authentification
  • Ajoutez un reverse proxy avec authentification (Basic Auth, OAuth)
  • Limitez le rate limiting pour éviter les abus
  • Surveillez l'utilisation GPU/VRAM
  • Mettez en place des limites de taille et de résolution

Cas d'usage : Bot Discord

import discord
from comfyui_client import ComfyUIClient

bot = discord.Bot()
comfy = ComfyUIClient()

@bot.slash_command(name="generate")
async def generate(ctx, prompt: str):
    await ctx.defer()

    # Préparer le workflow
    workflow = load_base_workflow()
    workflow["2"]["inputs"]["text"] = prompt
    workflow["5"]["inputs"]["seed"] = random.randint(0, 2**32)

    # Générer
    images = comfy.generate(workflow)

    # Envoyer le résultat
    file = discord.File(
        io.BytesIO(images[0]),
        filename="generated.png"
    )
    await ctx.followup.send(
        f"Prompt : *{prompt}*",
        file=file
    )

bot.run(TOKEN)