cat posts/comfyui-api-erstes-bild.md
ComfyUI per API: das erste Bild mit Python erzeugen
Einen ComfyUI-Workflow im API-Format exportieren, Prompt und Seed in Python setzen, den Auftrag per /prompt starten und das fertige Bild über /history und /view herunterladen.
ComfyUI ist mehr als der Node-Editor im Browser. Hinter der Oberfläche läuft ein Server, der Workflows entgegennimmt, validiert, in eine Queue stellt und ausführt. Diesen Server können wir auch direkt aus Python ansprechen.
Der Ablauf ist überschaubar:
Python-Skript
|
| POST /prompt
v
ComfyUI-Queue
|
| Workflow ausführen
v
History
|
| GET /history/{prompt_id}
v
Bildinformationen
|
| GET /view
v
Lokale Bilddatei
Wir verwenden dafür bewusst nur HTTP-Requests. ComfyUI bietet auch WebSockets für Live-Fortschritt, aber zum Einstieg lässt sich Polling über die History leichter nachvollziehen.
Voraussetzungen
Bevor Python ins Spiel kommt, sollte Dein Workflow direkt in ComfyUI funktionieren. Du brauchst:
- eine laufende lokale ComfyUI-Instanz;
- einen funktionierenden Text-to-Image-Workflow;
- ein lokal verfügbares Checkpoint-Modell;
- einen
Save Image-Node am Ende des Workflows; - Python und das Paket
requests.
Standardmäßig ist ComfyUI unter dieser Adresse erreichbar:
http://127.0.0.1:8188
Wenn sich dort die Oberfläche öffnet und Dein Workflow ein Bild erzeugt, können wir ihn aus Python starten.
Den Workflow für die API exportieren
Das Skript baut den Workflow nicht selbst zusammen. Wir erstellen und testen ihn in ComfyUI und exportieren anschließend die ausführbare Beschreibung als JSON.
Für das Beispiel reicht ein klassischer Text-to-Image-Workflow:
Load Checkpoint
├──> CLIP Text Encode (positiver Prompt)
├──> CLIP Text Encode (negativer Prompt)
└──> KSampler <── Empty Latent Image
|
v
VAE Decode
|
v
Save Image
Wichtig ist, dass Du nicht das normale Workflow-JSON verwendest. POST /prompt
erwartet das API-Format. Exportiere den funktionierenden Workflow über:
File → Export Workflow (API)
und speichere ihn als:
workflow_api.json
Das normale Speicherformat enthält zusätzliche Daten für den Editor, etwa Positionen, Größen und Gruppen. Das API-Format besteht im Wesentlichen aus den Nodes, ihren Inputs und den Verbindungen untereinander.
Ein Ausschnitt sieht beispielsweise so aus:
{
"3": {
"inputs": {
"seed": 42,
"steps": 20,
"cfg": 7.0,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
},
"class_type": "KSampler"
},
"6": {
"inputs": {
"text": "a small robot repairing a computer",
"clip": ["4", 1]
},
"class_type": "CLIPTextEncode"
},
"7": {
"inputs": {
"text": "blurry, text, watermark",
"clip": ["4", 1]
},
"class_type": "CLIPTextEncode"
}
}
Je nach ComfyUI-Version kann zusätzlich ein _meta-Block an einem Node hängen. Für
unser Skript ist er uninteressant und kann einfach im JSON bleiben.
Die Schlüssel "3", "6" und "7" sind die Node-IDs. Eine Verbindung wie:
"positive": ["6", 0]
heißt: Verwende Ausgang 0 von Node 6 als positiven Input des Samplers.
Damit können wir die drei IDs ermitteln, die das Skript später verändert. Suche in
workflow_api.json nach:
"class_type": "KSampler"
In unserem Beispiel ist das Node "3". Dort stehen auch:
"positive": ["6", 0],
"negative": ["7", 0]
Also:
KSampler: 3
Positiver Prompt: 6
Negativer Prompt: 7
Die IDs sind nicht universell. In Deinem Workflow können sie anders lauten.
Python-Projekt vorbereiten
Lege ein kleines Projekt an und installiere requests:
mkdir comfy-api
cd comfy-api
uv init
uv add requests
Kopiere workflow_api.json in das Verzeichnis und lege daneben generate.py an:
comfy-api/
├── generate.py
└── workflow_api.json
Pillow oder eine andere Bildbibliothek brauchen wir nicht. ComfyUI liefert die fertige Datei als Bytes, die Python direkt speichern kann.
Das vollständige Skript
from __future__ import annotations
import argparse
import json
import random
import time
from pathlib import Path
from typing import Any
import requests
SERVER_URL = "http://127.0.0.1:8188"
WORKFLOW_FILE = Path("workflow_api.json")
OUTPUT_DIR = Path("output")
# Diese IDs musst Du an Deinen Workflow anpassen.
SAMPLER_NODE_ID = "3"
POSITIVE_PROMPT_NODE_ID = "6"
NEGATIVE_PROMPT_NODE_ID = "7"
HTTP_TIMEOUT = 30
GENERATION_TIMEOUT = 600
POLL_INTERVAL = 0.5
MAX_SEED = 2**64
def load_workflow() -> dict[str, Any]:
with WORKFLOW_FILE.open(encoding="utf-8") as file:
workflow = json.load(file)
if not isinstance(workflow, dict):
raise ValueError("Der Workflow muss ein JSON-Objekt sein.")
return workflow
def check_response(response: requests.Response) -> None:
if response.ok:
return
try:
details = json.dumps(response.json(), ensure_ascii=False, indent=2)
except ValueError:
details = response.text
raise RuntimeError(
f"ComfyUI antwortete mit HTTP {response.status_code}:\n{details}"
)
def queue_prompt(workflow: dict[str, Any]) -> str:
response = requests.post(
f"{SERVER_URL}/prompt",
json={"prompt": workflow},
timeout=HTTP_TIMEOUT,
)
check_response(response)
data = response.json()
return str(data["prompt_id"])
def wait_for_result(prompt_id: str) -> dict[str, Any]:
deadline = time.monotonic() + GENERATION_TIMEOUT
while time.monotonic() < deadline:
response = requests.get(
f"{SERVER_URL}/history/{prompt_id}",
timeout=HTTP_TIMEOUT,
)
check_response(response)
history = response.json()
if prompt_id in history:
return history[prompt_id]
time.sleep(POLL_INTERVAL)
raise TimeoutError(
f"Nach {GENERATION_TIMEOUT} Sekunden liegt noch kein Ergebnis vor."
)
def collect_images(result: dict[str, Any]) -> list[dict[str, str]]:
status = result.get("status", {})
if status.get("status_str") == "error":
messages = status.get("messages", [])
raise RuntimeError(
"ComfyUI konnte den Workflow nicht ausführen:\n"
f"{json.dumps(messages, ensure_ascii=False, indent=2)}"
)
images: list[dict[str, str]] = []
for node_output in result.get("outputs", {}).values():
images.extend(node_output.get("images", []))
if not images:
raise RuntimeError(
"Der Workflow wurde beendet, enthält aber keine abrufbaren Bilder."
)
return images
def download_image(image: dict[str, str], index: int) -> Path:
response = requests.get(
f"{SERVER_URL}/view",
params={
"filename": image["filename"],
"subfolder": image["subfolder"],
"type": image["type"],
},
timeout=HTTP_TIMEOUT,
)
check_response(response)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
suffix = Path(image["filename"]).suffix or ".png"
target = OUTPUT_DIR / f"bild_{index:02d}{suffix}"
target.write_bytes(response.content)
return target
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Erzeugt über die lokale ComfyUI-API Bilder."
)
parser.add_argument(
"--prompt",
default=(
"a small friendly robot repairing a computer, "
"detailed illustration"
),
)
parser.add_argument(
"--negative",
default="blurry, text, watermark, low quality",
)
parser.add_argument("--seed", type=int)
args = parser.parse_args()
if args.seed is not None and not 0 <= args.seed < MAX_SEED:
parser.error(f"--seed muss zwischen 0 und {MAX_SEED - 1} liegen")
return args
def main() -> None:
args = parse_arguments()
seed = args.seed if args.seed is not None else random.randrange(MAX_SEED)
workflow = load_workflow()
workflow[POSITIVE_PROMPT_NODE_ID]["inputs"]["text"] = args.prompt
workflow[NEGATIVE_PROMPT_NODE_ID]["inputs"]["text"] = args.negative
workflow[SAMPLER_NODE_ID]["inputs"]["seed"] = seed
print(f"Seed: {seed}")
prompt_id = queue_prompt(workflow)
print(f"Auftrag gestartet: {prompt_id}")
result = wait_for_result(prompt_id)
images = collect_images(result)
for index, image in enumerate(images, start=1):
target = download_image(image, index)
print(f"Gespeichert: {target}")
if __name__ == "__main__":
main()
Prüfe vor dem ersten Start diese drei Werte:
SAMPLER_NODE_ID = "3"
POSITIVE_PROMPT_NODE_ID = "6"
NEGATIVE_PROMPT_NODE_ID = "7"
Sie müssen zu den IDs in Deiner workflow_api.json passen.
Das erste Bild erzeugen
Starte das Skript ohne Argumente:
uv run python generate.py
Eine erfolgreiche Ausgabe sieht ungefähr so aus:
Seed: 15727431580152408306
Auftrag gestartet: 9d9c6bd9-2caf-48ad-b988-f48b928ca88a
Gespeichert: output/bild_01.png
Danach liegt die heruntergeladene Datei unter output/bild_01.png. Der
Save Image-Node hat das Bild außerdem bereits im Output-Verzeichnis Deiner
ComfyUI-Instanz gespeichert. Unser Skript holt diese Datei über die API ab und
legt eine eigene Kopie im Projekt an.
Einen anderen Prompt übergibst Du so:
uv run python generate.py \
--prompt "a corgi astronaut on the moon, cinematic lighting"
Der negative Prompt lässt sich ebenfalls ersetzen:
uv run python generate.py \
--prompt "an old library inside a giant tree, fantasy illustration" \
--negative "blurry, text, watermark, distorted architecture"
Für einen festen Seed kommt --seed dazu:
uv run python generate.py \
--prompt "a corgi astronaut on the moon, cinematic lighting" \
--seed 42
Der KSampler akzeptiert Seeds von 0 bis 2^64 - 1. Ohne --seed wählt das
Skript bei jedem Aufruf einen zufälligen Wert aus diesem Bereich.
Bleiben Workflow, Modell, Prompt und Einstellungen gleich, ist das Ergebnis unter gleichen Bedingungen normalerweise reproduzierbar. Bitgenau identische Ergebnisse über andere ComfyUI-, PyTorch-, Treiber- oder Hardware-Versionen hinweg sind damit aber nicht garantiert.
Was die API-Aufrufe machen
Im Skript stecken nur drei ComfyUI-Endpunkte.
POST /prompt
Mit:
requests.post(
f"{SERVER_URL}/prompt",
json={"prompt": workflow},
)
schicken wir den Workflow an ComfyUI. Etwas verwirrend ist der Name prompt: Hier
ist damit nicht der Text-Prompt gemeint, sondern der komplette Workflow im
API-Format.
ComfyUI validiert ihn und stellt ihn in die Queue. Bei Erfolg kommt unter anderem
eine prompt_id zurück:
{
"prompt_id": "9d9c6bd9-2caf-48ad-b988-f48b928ca88a",
"number": 4,
"node_errors": {}
}
Ist der Workflow ungültig, liefert der Server einen HTTP-Fehler. Unser Skript gibt
die Antwort mit aus, damit Du zum Beispiel node_errors sehen kannst.
GET /history/{prompt_id}
POST /prompt wartet nicht, bis das Bild fertig ist. Die Ausführung läuft
asynchron weiter. Deshalb fragt das Skript alle 0,5 Sekunden:
GET /history/9d9c6bd9-2caf-48ad-b988-f48b928ca88a
Sobald der Auftrag in der History steht, finden wir unter outputs die Ausgaben der
Output-Nodes. Ein Save Image-Node liefert beispielsweise:
{
"outputs": {
"9": {
"images": [
{
"filename": "ComfyUI_00001_.png",
"subfolder": "",
"type": "output"
}
]
}
}
}
Das Bild selbst steckt noch nicht in dieser Antwort. Wir bekommen nur die Daten, mit denen es sich abrufen lässt.
GET /view
Aus filename, subfolder und type entsteht der letzte Request:
GET /view?filename=ComfyUI_00001_.png&subfolder=&type=output
Die Antwort enthält die Binärdaten des Bildes. write_bytes() schreibt sie direkt
in unser output-Verzeichnis.
Warum keine client_id?
In vielen ComfyUI-Beispielen taucht zusätzlich eine client_id auf. Für unser
Polling über HTTP ist sie nicht nötig: Die von /prompt zurückgegebene prompt_id
reicht aus, um das fertige Ergebnis aus der History zu holen.
Eine client_id wird interessant, sobald Du WebSockets verwendest:
ws://127.0.0.1:8188/ws?clientId=<client_id>
Darüber meldet ComfyUI unter anderem gestartete und ausgeführte Nodes, Sampler-Fortschritt, Cache-Treffer und Fehler. Für interaktive Anwendungen ist die Kombination aus WebSocket und History deshalb besser geeignet. Die offizielle Dokumentation empfiehlt diesen Weg für die meisten Anwendungen, die auf die Fertigstellung eines Jobs warten müssen.
Für unser erstes Skript ist Polling absichtlich simpler.
Mehrere Bilder
Das Skript sammelt alle images-Einträge aus den Outputs. Dadurch funktioniert es
auch bei einem batch_size größer als 1 oder bei mehreren Nodes, die Bilder
zurückgeben.
Die lokalen Dateien werden einfach durchnummeriert:
bild_01.png
bild_02.png
bild_03.png
Häufige Fehler
Keine Verbindung zu ComfyUI
Eine Meldung wie:
Failed to establish a new connection
bedeutet meistens, dass ComfyUI nicht läuft oder unter einer anderen Adresse lauscht. Prüfe zuerst:
http://127.0.0.1:8188
Bei einem anderen Port passt Du SERVER_URL an:
SERVER_URL = "http://127.0.0.1:8288"
Falsche Node-ID
Ein Fehler wie:
KeyError: '6'
bedeutet meist, dass die IDs im Skript nicht zu Deinem exportierten Workflow
passen. Ermittle KSampler, positive und negative erneut in
workflow_api.json.
HTTP 400 und node_errors
Dann konnte ComfyUI den Workflow nicht validieren. Häufige Ursachen sind ein fehlendes Modell, ein nicht installierter Custom Node, ein ungültiger Input oder ein Workflow, der nicht im API-Format exportiert wurde.
Keine Bilder in der History
Wenn der Auftrag fertig ist, aber keine images-Einträge liefert, prüfe den
Output des Workflows. Für dieses Beispiel sollte VAE Decode mit einem
Save Image-Node verbunden sein.
Nach Änderungen am Workflow musst Du die API-Datei erneut exportieren.
Timeout
Das Skript wartet standardmäßig höchstens zehn Minuten:
GENERATION_TIMEOUT = 600
Bei großen Modellen, hohen Auflösungen oder einer vollen Queue kannst Du den Wert
erhöhen. HTTP_TIMEOUT = 30 gilt dagegen nur für einen einzelnen HTTP-Request.
ComfyUI auf einem anderen Rechner
Standardmäßig bindet ComfyUI den Server an 127.0.0.1. Andere Rechner können ihn
so nicht erreichen.
Für Zugriff aus dem lokalen Netz kannst Du ComfyUI beispielsweise so starten:
python main.py --listen 0.0.0.0
oder einfach:
python main.py --listen
Ohne Wert lauscht --listen auf allen IPv4- und IPv6-Interfaces. Im Skript trägst
Du anschließend die Adresse des ComfyUI-Rechners ein:
SERVER_URL = "http://192.168.1.50:8188"
Damit ist die API allerdings auch für andere Geräte erreichbar, die Zugriff auf
diesen Port haben. Einen direkt aus dem Internet erreichbaren ComfyUI-Server würde
ich nicht einfach mit --listen freigeben. Für Remote-Zugriff gehören
Zugriffskontrolle und Verschlüsselung davor, etwa über einen Reverse Proxy oder ein
VPN.
Von hier aus weiter
Das Grundprinzip bleibt auch für größere Anwendungen dasselbe: Workflow laden,
Inputs verändern, mit /prompt starten, auf das Ergebnis warten und die Dateien
über /view abholen.
Darauf lassen sich Batch-Generierungen aus Prompt-Listen, Telegram- oder Discord-Bots und eigene Web-UIs aufbauen. Sobald Live-Fortschritt, Abbruch oder viele parallele Jobs ins Spiel kommen, ist der WebSocket-Endpunkt der nächste sinnvolle Schritt.
Weiterlesen
- uv: pip und venv in schnell – Python-Projekte und Abhängigkeiten mit uv verwalten.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.