API ComfyUI avec Python : exécuter des flux de travail, endpoints et exemples

ComfyUI fait déjà tourner un serveur HTTP, donc Python peut le piloter de bout en bout. Exportez un flux de travail au format API, mettez-le en file d’attente, suivez la progression via WebSocket, téléchargez les images et évitez les erreurs qui font échouer les scripts sans surveillance. Avec une classe client réutilisable et une boucle de traitement par lots.

API ComfyUI avec Python : exécuter des flux de travail, endpoints et exemples
Cristian Da Conceicao
Fondateur de Picasso IA

ComfyUI ressemble à une planche à dessin pour graphes de nœuds, mais sous le canevas, c’est un serveur HTTP ordinaire. Chaque bouton sur lequel vous cliquez dans le navigateur appelle un endpoint, et un script Python peut appeler exactement les mêmes endpoints. C’est toute l’idée derrière l’API ComfyUI avec Python : exporter un flux de travail en JSON, modifier deux ou trois valeurs, l’envoyer en POST sur /prompt, puis récupérer les images terminées. Pas d’onglet de navigateur, pas de clics, pas de surveillance. Cet article présente les vrais endpoints, un écouteur WebSocket pour la progression en direct et des exemples fonctionnels que vous pouvez coller dans un fichier et exécuter sur votre propre machine.

Mains tapant du code Python sur un ordinateur portable à côté d’une tasse de café

Pourquoi scripter ComfyUI

Faire une image à la main, c’est très bien. Produire deux cents visuels de produits, lancer chaque nuit une tâche de miniatures ou laisser vos clients appuyer sur un bouton dans votre propre application, c’est une autre histoire, et le canevas ne peut rien faire de tout cela. Un serveur ComfyUI headless le peut, et Python est le chemin le plus court pour y arriver. En prime, vos prompts, seeds et réglages finissent dans un dépôt Git au lieu d’un dossier de captures d’écran.

L’API se justifie dans trois situations :

  • Traitement par lots : des centaines de prompts, un seul gabarit, zéro clic manuel.
  • Produits : votre propre application envoie une requête et récupère une image.
  • Automatisation : une tâche cron, un chatbot ou une étape de CI qui génère des ressources selon un planning.

Boîtier de PC ouvert avec une grande carte graphique sur un établi en bois

Ce que le serveur expose

Lancez ComfyUI de la manière habituelle (python main.py) et il écoute sur 127.0.0.1:8188. Ajoutez --listen 0.0.0.0 pour accepter les connexions d’autres machines et --port pour changer de port. Voici les endpoints que vous utiliserez le plus souvent :

MéthodeEndpointFonction
POST/promptMet en file d’attente un flux de travail et renvoie un prompt_id
GET/history/{prompt_id}Renvoie les sorties et le statut une fois l’exécution terminée
GET/viewTélécharge une image par filename, subfolder et type
POST/upload/imagePlace une image dans le dossier d’entrée de ComfyUI
GET/queueListe les prompts en cours et en attente
POST/interruptArrête le prompt en cours d’exécution
GET/object_infoDécrit chaque classe de nœud et ses entrées
GET/system_statsAffiche les détails de la VRAM, de la RAM et du périphérique
WebSocket/ws?clientId=...Diffuse les événements en direct vers votre client

Le rythme ne change jamais : mettre en file, attendre, récupérer. Vous envoyez un graphe en POST, vous attendez (par interrogation ou en écoutant le WebSocket), puis vous téléchargez ce que le graphe a produit.

Exporter votre flux de travail au format API

Construisez et testez d’abord le graphe sur le canevas. Quand il produit l’image voulue, exportez-le. Votre script ne peut pas exécuter le fichier de flux de travail habituel, car ce format enregistre la position des nœuds sur le canevas, les couleurs et la disposition des widgets. Le script a besoin de la version allégée, où chaque nœud est réduit à sa classe et à ses entrées.

Ingénieur étudiant un schéma de nœuds sur un grand écran de bureau

Format API ou JSON classique

Dans les interfaces actuelles, ouvrez le menu Workflow et choisissez Export (API). Sur les versions plus anciennes, activez Dev mode options dans les paramètres et utilisez le bouton Save (API Format). Enregistrez le résultat sous workflow_api.json à côté de votre script.

💡 Conseil : Gardez les deux fichiers. Le JSON classique se rouvre sur le canevas pour être modifié, tandis que le JSON API est ce que votre code envoie.

Anatomie du JSON exporté

Ouvrez le fichier et vous verrez un dictionnaire plat. Chaque entrée porte le nom d’un ID de nœud (une chaîne), et sa valeur contient une class_type ainsi que les inputs du nœud :

{
  "3": {
    "class_type": "KSampler",
    "inputs": {
      "seed": 421337,
      "steps": 20,
      "cfg": 1.0,
      "sampler_name": "euler",
      "scheduler": "simple",
      "denoise": 1.0,
      "model": ["4", 0],
      "positive": ["6", 0],
      "negative": ["7", 0],
      "latent_image": ["5", 0]
    }
  },
  "4": {
    "class_type": "CheckpointLoaderSimple",
    "inputs": { "ckpt_name": "flux1-dev-fp8.safetensors" }
  },
  "6": {
    "class_type": "CLIPTextEncode",
    "inputs": { "text": "a lighthouse at dawn", "clip": ["4", 1] },
    "_meta": { "title": "Positive Prompt" }
  }
}

Cet exemple suppose un checkpoint Flux Dev, ce qui explique que cfg soit à 1.0. Les checkpoints de type Stable Diffusion 3.5 Large demandent généralement une valeur plus élevée, souvent entre 4 et 8. Recopiez donc les valeurs de votre propre export plutôt que celles d’un tutoriel.

Deux détails comptent. Les valeurs simples, comme seed et steps, sont les réglages que vous modifiez depuis Python. Les valeurs comme ["4", 0] sont des liens : le premier élément est l’ID du nœud source, le second est l’emplacement de sortie à lire. Ne touchez pas aux liens, sauf si vous recâblez volontairement le graphe.

💡 Conseil : Renommez vos nœuds de prompt sur le canevas (« Positive Prompt », « Negative Prompt ») avant d’exporter. Le nom apparaît dans _meta.title, et votre code peut retrouver les nœuds par leur titre au lieu d’un numéro fragile.

Votre premier appel Python

Deux paquets suffisent pour tout ce qui est décrit dans cet article : pip install requests websocket-client. Enregistrez l’export sous workflow_api.json, lancez ComfyUI et exécutez les extraits dans l’ordre.

Vue de dessus d’un bureau avec un ordinateur portable, un schéma sur carnet et un café

Installer et mettre en file un prompt

import json
import requests

SERVER = "http://127.0.0.1:8188"

with open("workflow_api.json", "r", encoding="utf-8") as f:
    workflow = json.load(f)

# "6" is the positive CLIPTextEncode, "3" is the KSampler
workflow["6"]["inputs"]["text"] = "a lighthouse at dawn, 35mm photo, film grain"
workflow["3"]["inputs"]["seed"] = 421337

response = requests.post(f"{SERVER}/prompt", json={"prompt": workflow})
response.raise_for_status()
prompt_id = response.json()["prompt_id"]
print("Queued:", prompt_id)

ComfyUI répond avec un corps JSON contenant un prompt_id et un number, c’est-à-dire la position dans la file d’attente. Rien n’a encore été généré à ce stade. Le prompt a seulement été accepté. Conservez l’ID, car chaque appel suivant en aura besoin.

Interroger l’endpoint d’historique

Le moyen le plus simple de savoir qu’un prompt est terminé consiste à interroger l’endpoint d’historique jusqu’à obtenir une réponse. Tant que l’exécution continue, /history/{prompt_id} renvoie un objet vide.

import time

def wait_for_outputs(prompt_id, timeout=300):
    started = time.time()
    while time.time() - started < timeout:
        history = requests.get(f"{SERVER}/history/{prompt_id}").json()
        if prompt_id in history:
            return history[prompt_id]["outputs"]
        time.sleep(1)
    raise TimeoutError(f"Prompt {prompt_id} took longer than {timeout}s")

Le dictionnaire outputs est indexé par ID de nœud. Chaque nœud SaveImage signale une liste appelée images, et chaque image est un petit dictionnaire avec un filename, un subfolder et un type.

Télécharger l’image terminée

import os

def download_images(outputs, folder="renders"):
    os.makedirs(folder, exist_ok=True)
    saved = []
    for node_id, node_output in outputs.items():
        for image in node_output.get("images", []):
            if image["type"] != "output":
                continue  # skip PreviewImage temp files
            data = requests.get(f"{SERVER}/view", params=image).content
            path = os.path.join(folder, image["filename"])
            with open(path, "wb") as f:
                f.write(data)
            saved.append(path)
    return saved

print(download_images(wait_for_outputs(prompt_id)))

Le dictionnaire d’image contient déjà les trois paramètres attendus par /view, il peut donc être transmis directement comme chaîne de requête. Les nœuds SaveImage signalent type: "output", tandis que les nœuds PreviewImage signalent temp, raison pour laquelle la boucle filtre sur ce champ.

Progression en direct via WebSocket

L’interrogation fonctionne, mais elle multiplie les appels et ne dit rien avant la toute fin. ComfyUI parle aussi WebSocket, ce qui donne un flux en direct : changements dans la file, nœud en cours d’exécution et compteur pour chaque étape de l’échantillonneur. Dans une application web, c’est ce qui pilote la barre de progression.

Technicien vérifiant des câbles dans une salle serveur étroite

Se connecter avec un ID client

Générez un UUID une seule fois et utilisez-le à deux endroits : la chaîne de requête clientId de la socket et le champ client_id de votre requête /prompt. ComfyUI n’envoie les événements d’un prompt qu’au client qui l’a mis en file. Si les ID ne correspondent pas, votre socket reste muette.

import json
import uuid
import requests
import websocket  # pip install websocket-client

HOST = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())

def run_with_progress(workflow):
    ws = websocket.WebSocket()
    ws.connect(f"ws://{HOST}/ws?clientId={CLIENT_ID}")

    payload = {"prompt": workflow, "client_id": CLIENT_ID}
    r = requests.post(f"http://{HOST}/prompt", json=payload)
    r.raise_for_status()
    prompt_id = r.json()["prompt_id"]

    while True:
        message = ws.recv()
        if isinstance(message, bytes):
            continue  # binary frames are preview thumbnails
        event = json.loads(message)
        kind, data = event["type"], event["data"]

        if kind == "progress":
            print(f"step {data['value']}/{data['max']}")
        elif kind == "execution_error":
            raise RuntimeError(data.get("exception_message", "node failed"))
        elif data.get("prompt_id") == prompt_id and (
            kind == "execution_success"
            or (kind == "executing" and data["node"] is None)
        ):
            break

    ws.close()
    history = requests.get(f"http://{HOST}/history/{prompt_id}").json()
    return history[prompt_id]["outputs"]

Les trames binaires transportent les miniatures de prévisualisation pendant que l’échantillonneur travaille, la boucle ignore donc tout ce qui n’est pas du texte. Si vous voulez afficher des aperçus en direct dans votre propre interface, décodez ces trames au lieu de les ignorer.

Messages que vous recevrez

Type de messageSignification
statusLa taille de la file a changé
execution_startVotre prompt a quitté la file et a commencé à s’exécuter
execution_cachedListe les nœuds ignorés parce que leur résultat était en cache
executingLe nœud en cours d’exécution ; node: null signifie que le graphe est terminé
progressÉtape value sur max de l’échantillonneur
executedUn nœud a produit une sortie, comme des noms de fichiers enregistrés
execution_errorUn nœud a levé une exception
execution_successLe prompt entier a réussi (versions récentes)

Le message executing avec node égal à null est le signal classique de fin d’exécution. Les versions récentes ajoutent execution_success, et gérer les deux permet à votre script de fonctionner d’une version à l’autre.

Encapsuler le tout dans une classe client

Des fonctions isolées suffisent pour un premier test. Tout ce qui s’exécute plus d’une fois mérite une petite classe qui stocke l’hôte, l’ID client et les opérations que vous répétez.

import time
import uuid
import requests

class ComfyClient:
    def __init__(self, host="127.0.0.1:8188"):
        self.host = host
        self.client_id = str(uuid.uuid4())

    def queue(self, workflow):
        r = requests.post(
            f"http://{self.host}/prompt",
            json={"prompt": workflow, "client_id": self.client_id},
        )
        if r.status_code != 200:
            raise RuntimeError(r.text)  # includes node_errors
        return r.json()["prompt_id"]

    def result(self, prompt_id, timeout=300):
        deadline = time.time() + timeout
        while time.time() < deadline:
            history = requests.get(f"http://{self.host}/history/{prompt_id}").json()
            if prompt_id in history:
                return history[prompt_id]
            time.sleep(1)
        raise TimeoutError(prompt_id)

    def fetch(self, image):
        r = requests.get(f"http://{self.host}/view", params=image)
        r.raise_for_status()
        return r.content

    def upload(self, path):
        with open(path, "rb") as f:
            r = requests.post(
                f"http://{self.host}/upload/image",
                files={"image": f},
                data={"overwrite": "true"},
            )
        r.raise_for_status()
        return r.json()["name"]

Mur de studio couvert de tirages de photos produits épinglés

Modifier prompts et seeds en toute sécurité

Ne codez jamais en dur des ID de nœuds comme "6" dans un vrai projet. Si vous réexportez le graphe, les numéros peuvent changer. Recherchez plutôt les nœuds par classe et titre, et modifiez toujours une copie du gabarit, pour qu’une tâche ne contamine pas la suivante.

import copy
import json
import random

def find_node(workflow, class_type, title=None):
    for node_id, node in workflow.items():
        if node["class_type"] != class_type:
            continue
        if title is None or node.get("_meta", {}).get("title") == title:
            return node_id
    raise LookupError(f"{class_type} {title or ''} not found")

def build(template, prompt, seed=None):
    wf = copy.deepcopy(template)
    wf[find_node(wf, "CLIPTextEncode", "Positive Prompt")]["inputs"]["text"] = prompt
    wf[find_node(wf, "KSampler")]["inputs"]["seed"] = (
        seed if seed is not None else random.randint(0, 2**32 - 1)
    )
    return wf

template = json.load(open("workflow_api.json", encoding="utf-8"))
client = ComfyClient()

prompts = [
    "ceramic teapot on a linen cloth, soft window light",
    "walnut desk with a fountain pen, low morning sun",
    "leather boots on wet cobblestones, overcast sky",
]

ids = [client.queue(build(template, p)) for p in prompts]  # queue everything first
for pid in ids:
    entry = client.result(pid)
    for out in entry["outputs"].values():
        for image in out.get("images", []):
            with open(image["filename"], "wb") as f:
                f.write(client.fetch(image))

Mettez tout en file d’abord, puis récupérez. ComfyUI exécute les prompts un par un, dans l’ordre d’arrivée, donc le GPU ne reste jamais inactif pendant que votre script télécharge un fichier.

💡 Conseil : Vous avez besoin de 200 prompts et non de trois ? Demandez à un grand modèle de langage comme Claude Sonnet 5 ou Gemini 3.5 Flash de les écrire sous forme de liste JSON, puis injectez cette liste directement dans la boucle ci-dessus.

Importer des images pour les retouches

Les graphes d’image vers image, d’inpainting et ControlNet commencent par un nœud LoadImage. Ce nœud lit depuis le dossier d’entrée de ComfyUI : importez donc d’abord le fichier, puis faites pointer le nœud vers le nom renvoyé.

name = client.upload("portrait.png")
wf = copy.deepcopy(template)
wf[find_node(wf, "LoadImage")]["inputs"]["image"] = name
pid = client.queue(wf)

Retoucheur repeignant une partie d’un portrait sur une tablette à stylet

C’est là que le script se rapproche du travail d’effets visuels. La suppression d’objets, le remplacement d’arrière-plan et le changement d’éclairage suivent tous la même boucle : importer une image source, définir un masque et un prompt, mettre en file, récupérer. Encapsulée dans une fonction, un dossier de 500 photos devient une seule commande.

Erreurs de production à éviter

Les scripts qui fonctionnent sur votre ordinateur tombent en panne de façons prévisibles dès qu’ils tournent sans surveillance. Trois problèmes expliquent la plupart des questions d’assistance.

Développeur travaillant seul la nuit sous une lampe de bureau chaude

Les prompts en cache renvoient instantanément

ComfyUI met en cache les résultats des nœuds selon leurs entrées. Si vous mettez en file exactement le même graphe deux fois, la seconde exécution ne calcule rien, et vous récupérez donc la même image en quelques millisecondes. L’option « randomize seed after each run » n’existe que dans l’interface du navigateur. Le JSON API contient un nombre fixe : votre code doit donc choisir un nouveau seed chaque fois qu’il veut une nouvelle image.

Lire correctement node_errors

Quand la validation échoue, /prompt répond avec un HTTP 400 et un corps contenant error et node_errors. Le second champ indique l’ID de nœud et l’entrée exacts en cause, par exemple un nom de fichier de checkpoint non installé sur cette machine. Affichez le corps complet, pas seulement le code d’état. Gardez aussi à l’esprit qu’un prompt peut passer la validation et échouer tout de même pendant l’exécution ; dans ce cas, l’entrée d’historique affiche status_str: "error".

Ne jamais exposer le port 8188

ComfyUI est livré sans authentification. Quiconque peut atteindre le port peut mettre des tâches en file, lire votre dossier de sorties et appeler /object_info. Les nœuds personnalisés sont du Python ordinaire et s’exécutent avec les droits de votre utilisateur. Liez le serveur à 127.0.0.1, ou placez-le derrière un reverse proxy avec authentification ou un VPN. Si une application web sur une autre origine doit l’appeler, transmettez --enable-cors-header avec cette seule origine plutôt qu’un joker.

💡 Conseil : Vous manquez de VRAM après avoir utilisé beaucoup de checkpoints différents ? Envoyez en POST {"unload_models": true, "free_memory": true} vers /free entre les lots pour libérer la mémoire sans redémarrer le serveur.

Se passer du serveur avec Picasso IA

Tous les projets n’ont pas besoin d’une machine GPU, d’un environnement Python et d’une file à surveiller. Si votre objectif est simplement d’obtenir de bonnes images à partir de textes ou de photos de référence, Picasso IA fait tourner des modèles comparables dans le navigateur. Voici comment les modèles se positionnent par rapport aux tâches courantes de ComfyUI :

TâcheModèlePourquoi le choisir
Texte vers image au quotidienFlux Dev12 milliards de paramètres, 11 formats jusqu’à 21:9, mode img2img
Images basées sur des références et retouchesFlux 2 ProJusqu’à 8 images de référence, sorties jusqu’à 4 MP
Brouillons rapidesFlux SchnellAperçus rapides avant un rendu final
Inpainting et suppression d’objetsFlux Fill ProNe repeint que la zone masquée
Contrôle des contours et de la profondeurFlux Canny Pro et Flux Depth ProConserve la mise en page d’une image source
Une autre famille de modèlesStable Diffusion 3.5 LargeRendu différent, même flux de travail

Générer en six étapes

Designer tenant une photo imprimée d’un lac de montagne à côté d’un écran

Voici tout le processus avec Flux 2 Pro, le modèle qui se rapproche le plus d’un flux de travail ComfyUI à images de référence :

  1. Ouvrez la page Flux 2 Pro sur Picasso IA.
  2. Rédigez votre prompt. Nommez le sujet, la lumière et l’objectif, comme vous le feriez dans un nœud texte de ComfyUI.
  3. Choisissez un format. Le format par défaut est 1:1, 16:9 convient aux bannières, et match_input_image conserve la forme d’une photo importée.
  4. Choisissez une résolution. La valeur par défaut est 1 MP, et le modèle accepte jusqu’à 4 MP, même si 2 MP ou moins est recommandé.
  5. Ajoutez jusqu’à 8 images d’entrée si vous voulez que le résultat suive un style, un visage ou une photo de produit.
  6. Réglez le format de sortie (WebP, JPG ou PNG), puis lancez la génération. Réutilisez plus tard le seed pour recréer exactement le même résultat.
RéglagePar défautConseil pratique
Résolution1 MPRestez à 2 MP ou moins pour les meilleurs résultats
Qualité de sortie80Plage de 0 à 100, ignorée pour le PNG
Tolérance de sécurité21 est le plus strict, 5 le plus permissif
SeedAléatoireFixez-le pour reproduire une image à l’identique

💡 Conseil : Les habitudes acquises ci-dessus se transposent directement. Seeds fixes pour des résultats reproductibles, un seul changement par exécution, et des prompts courts qui nomment la lumière et l’objectif fonctionnent de la même façon sur les deux plateformes.

Créez vos propres images dès aujourd’hui

Vous avez maintenant la boucle complète : exporter le graphe, le mettre en file, écouter la socket, récupérer les fichiers. Lancez le premier extrait ce soir et vous aurez une image sur disque avant que votre café ait refroidi. Ensuite, affinez la classe client, ajoutez la logique de seed et laissez un lot tourner pendant que vous faites autre chose.

Et si vous préférez vous passer de l’installation, ouvrez Picasso IA, choisissez Flux Dev ou Flux 2 Pro, et tapez le premier prompt qui vous vient à l’esprit. Modifiez un réglage, relancez la génération et comparez. Cinq minutes d’essais vous en apprendront plus sur les prompts, les seeds et les formats que n’importe quelle lecture.

Partager cet article

Choisissez votre langue