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.
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.
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.
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éthode
Endpoint
Fonction
POST
/prompt
Met 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
/view
Télécharge une image par filename, subfolder et type
POST
/upload/image
Place une image dans le dossier d’entrée de ComfyUI
GET
/queue
Liste les prompts en cours et en attente
POST
/interrupt
Arrête le prompt en cours d’exécution
GET
/object_info
Décrit chaque classe de nœud et ses entrées
GET
/system_stats
Affiche 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.
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 :
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.
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.
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 message
Signification
status
La taille de la file a changé
execution_start
Votre prompt a quitté la file et a commencé à s’exécuter
execution_cached
Liste les nœuds ignorés parce que leur résultat était en cache
executing
Le nœud en cours d’exécution ; node: null signifie que le graphe est terminé
progress
Étape value sur max de l’échantillonneur
executed
Un nœud a produit une sortie, comme des noms de fichiers enregistrés
execution_error
Un nœud a levé une exception
execution_success
Le 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"]
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)
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.
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 :
Rédigez votre prompt. Nommez le sujet, la lumière et l’objectif, comme vous le feriez dans un nœud texte de ComfyUI.
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.
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é.
Ajoutez jusqu’à 8 images d’entrée si vous voulez que le résultat suive un style, un visage ou une photo de produit.
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églage
Par défaut
Conseil pratique
Résolution
1 MP
Restez à 2 MP ou moins pour les meilleurs résultats
Qualité de sortie
80
Plage de 0 à 100, ignorée pour le PNG
Tolérance de sécurité
2
1 est le plus strict, 5 le plus permissif
Seed
Aléatoire
Fixez-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.