ComfyUI API 教學:用 Python 批次產圖,無須操作介面

摘要

ComfyUI 內建的 API 可以讓程式無須操作介面就能產圖。當我們要替同一份 workflow 換一整批文字,或想讓自己的工具在背後產圖時,只要匯出 API 格式的 workflow,再用 Python 替換節點的設定值並送出,就能批次產圖並取回檔案。

文章目錄

ComfyUI server 內建 API,程式無須操作介面,也能把 workflow 送進佇列並取回圖片。當我們要替同一份 workflow 換一整批文字時,在介面裡每張圖都要重複修改 Prompt、按下「執行」,再去尋找檔案,自己的工具想在背後產圖時也無法這樣操作。只要匯出 API 格式的 workflow,再用一支 Python 腳本替換每張圖的文字與隨機種子,就能一次產出整批圖片。

測試環境是 Windows 11、RTX 3070 8 GB 與 ComfyUI 0.43.2。呼叫本機的 ComfyUI 不需要 API key,ComfyUI 帳號的 API key 是呼叫雲端付費的 Partner Nodes 時才會用到。本文用 MOSS-Image 的文字生圖範本,替貓咪咖啡廳產出三張店員介紹貼文:

三張貓咪咖啡廳的店員介紹貼文,上方都寫「本週店員」。左邊是戴圍裙的橘貓「麻糬」,職稱店長,中間是戴圍裙的黑貓「墨墨」,職稱咖啡師,右邊是戴紅色領結的賓士貓「豆豆」,職稱保全

匯出 API 格式的 workflow

API 要收的是 API 格式的 workflow,介面「儲存」或「匯出」的檔案不能直接送出。在 ComfyUI 打開已經能正常產圖的 workflow,先調整好每張圖都相同的設定,我們把「解析度選擇器」改成 3:4。接著按左上角的圖示,選「檔案」的「匯出(API)」,這個項目不用開啟開發者模式就會顯示:

ComfyUI 左上角選單展開「檔案」,子選單最下方是「匯出(API)」

輸入檔名 cat-post-api 並按「確認」後,瀏覽器會下載 cat-post-api.json。把這個檔案放到一個新的資料夾,接下來的腳本也放在這裡。

找出要替換的節點

用文字編輯器打開 cat-post-api.json,最外層的每個 key 是節點編號,節點的設定值放在 inputs 裡。搜尋 workflow 裡原本的 Prompt、KSampler 和 SaveImage,就能找到要替換的節點。本文的檔案節錄如下,這次要換的是 Prompt 的 value、KSampler 的 seed,以及決定存檔名稱的 filename_prefix:

1{
2  "9": {
3    "inputs": { "filename_prefix": "moss_image_t2i", "images": ["18:8", 0] },
4    "class_type": "SaveImage"
5  },
6  "18:7": {
7    "inputs": { "seed": 438829726369282, "steps": 25, "cfg": 2.5 },
8    "class_type": "KSampler"
9  },
10  "18:17": {
11    "inputs": { "value": "Cinematic night street photograph: ..." },
12    "class_type": "PrimitiveStringMultiline"
13  }
14}

節點編號與欄位名稱每份 workflow 都不同,例如有些 workflow 的 Prompt 放在 CLIPTextEncode 的 text,因此下一節的腳本要換成自己的編號與欄位。18:7 這種帶冒號的編號代表這個節點在子圖裡。

批次產圖的腳本

腳本會對每張圖重複下面的流程,等待完成的方式和官方 API 範例的 WebSocket 做法相同。開始前要先啟動 ComfyUI,並裝好 uv:

Python 腳本與 ComfyUI server 之間的四個步驟:POST /prompt 送出 workflow 與 client_id 並收到 prompt_id,WebSocket /ws 等到 executing 的 node 變成 null,GET /history 查詢圖片檔名,GET /view 下載圖片

在同一個資料夾新增 generate_cat_posts.py,內容如下。POSTS 是每張圖的名稱、隨機種子與 Prompt,SERVER 要和 ComfyUI 的網址相同:

1import json
2import pathlib
3import urllib.error
4import urllib.parse
5import urllib.request
6import uuid
7
8import websocket
9
10SERVER = "127.0.0.1:8188"
11OUTPUT_DIR = pathlib.Path("output")
12
13POSTS = [
14    {"name": "mochi", "seed": 20261103, "prompt": "貓咪咖啡廳的 Instagram 店員介紹貼文,3:4 直式。上方寫「本週店員」,中間是一隻戴小圍裙的橘貓照片,下方大字寫名字「麻糬」,名字下面寫職稱「店長」。暖色、溫馨的咖啡廳氛圍,版面乾淨。"},
15    {"name": "inky", "seed": 20261104, "prompt": "貓咪咖啡廳的 Instagram 店員介紹貼文,3:4 直式。上方寫「本週店員」,中間是一隻戴小圍裙的黑貓照片,下方大字寫名字「墨墨」,名字下面寫職稱「咖啡師」。暖色、溫馨的咖啡廳氛圍,版面乾淨。"},
16    {"name": "bean", "seed": 20261105, "prompt": "貓咪咖啡廳的 Instagram 店員介紹貼文,3:4 直式。上方寫「本週店員」,中間是一隻戴小領結的賓士貓照片,下方大字寫名字「豆豆」,名字下面寫職稱「保全」。暖色、溫馨的咖啡廳氛圍,版面乾淨。"},
17]
18
19
20def queue_workflow(workflow, client_id):
21    body = json.dumps({"prompt": workflow, "client_id": client_id}).encode()
22    request = urllib.request.Request(
23        f"http://{SERVER}/prompt",
24        data=body,
25        headers={"Content-Type": "application/json"},
26    )
27    try:
28        with urllib.request.urlopen(request) as response:
29            result = json.load(response)
30    except urllib.error.HTTPError as error:
31        raise RuntimeError(error.read().decode()) from error
32    if result["node_errors"]:
33        raise RuntimeError(result["node_errors"])
34    return result["prompt_id"]
35
36
37def wait_until_done(ws, prompt_id):
38    while True:
39        message = ws.recv()
40        if isinstance(message, bytes):
41            continue  # 開啟預覽時,取樣中的預覽圖會以二進位傳來
42        event = json.loads(message)
43        data = event.get("data", {})
44        if data.get("prompt_id") != prompt_id:
45            continue
46        if event["type"] == "execution_error":
47            raise RuntimeError(data.get("exception_message"))
48        if event["type"] == "executing" and data.get("node") is None:
49            return
50
51
52def download_images(prompt_id, name):
53    with urllib.request.urlopen(f"http://{SERVER}/history/{prompt_id}") as response:
54        outputs = json.load(response)[prompt_id]["outputs"]
55    for node_output in outputs.values():
56        for index, image in enumerate(node_output.get("images", [])):
57            if image["type"] != "output":
58                continue
59            query = urllib.parse.urlencode(
60                {"filename": image["filename"], "subfolder": image["subfolder"], "type": image["type"]}
61            )
62            with urllib.request.urlopen(f"http://{SERVER}/view?{query}") as response:
63                path = OUTPUT_DIR / f"{name}_{index}.png"
64                path.write_bytes(response.read())
65                print(f"{name}: {path}")
66
67
68workflow = json.loads(pathlib.Path("cat-post-api.json").read_text(encoding="utf-8"))
69OUTPUT_DIR.mkdir(exist_ok=True)
70client_id = str(uuid.uuid4())
71ws = websocket.create_connection(f"ws://{SERVER}/ws?clientId={client_id}")
72
73for post in POSTS:
74    workflow["18:17"]["inputs"]["value"] = post["prompt"]
75    workflow["18:7"]["inputs"]["seed"] = post["seed"]
76    workflow["9"]["inputs"]["filename_prefix"] = f"cats/{post['name']}"
77    prompt_id = queue_workflow(workflow, client_id)
78    wait_until_done(ws, prompt_id)
79    download_images(prompt_id, post["name"])
80
81ws.close()

腳本只在記憶體裡替換內容,不會修改 cat-post-api.json。接著在這個資料夾執行腳本,websocket-client 套件會由 uv 一起準備,ComfyUI 的介面也無須開啟:

1uv run --with websocket-client generate_cat_posts.py

每完成一張,終端機就會印出檔案路徑。全部跑完後,output 資料夾會有三張 880 × 1184 的 PNG,就是本文開頭的貼文,ComfyUI 自己的 output\cats 資料夾也會留一份。在 RTX 3070 8 GB 上,第一張要載入模型,約 150 秒,之後每張約 40 秒。

呼叫失敗的情況

呼叫失敗時會看到下面這些情況,腳本會停下並印出錯誤訊息:

情況 原因 處理方式
腳本出現 KeyError,或自己送出時 /prompt 回傳 HTTP 500 檔案是介面存檔格式的 workflow 改用「匯出(API)」的檔案
/prompt 回傳 HTTP 400,或回傳 HTTP 200 但 node_errors 有內容 節點的設定值有錯,例如模型名稱不在清單裡,這時不會產出圖片 依 node_errors 的 details 修正該欄位
收到 execution_error 節點執行到一半發生錯誤,例如 VAE 和模型不相符 依 exception_message 與 node_id 檢查該節點

下面兩種情況沒有錯誤訊息,要從現象判斷:

  • 腳本一直等待,ComfyUI 卻已經產完圖:/prompt 沒有帶 client_id 時,server 不會把這個工作的事件傳給 WebSocket 連線。client_id 要和連線網址的 clientId 相同。
  • 終端機立刻印出路徑,ComfyUI 的 output\cats 卻沒有新檔案:這次送出的內容和上一次完全相同,ComfyUI 直接回傳上一次的結果。要產出新的圖,就換一個隨機種子。

總結

把 ComfyUI 接進腳本或自己的工具時,先從介面匯出 API 格式的 workflow,再由程式替換節點的設定值、送進佇列並取回圖片。實測用同一支腳本連續產出三張貼文,圖中的文字都正確。

接著可以做什麼

上一篇Minecraft gamerule 新舊名稱對照:keepInventory 等指令新版改名Games下一篇Claude Code 壓縮後我的規則還在嗎?讓 Agent 在長對話中持續遵守規則AI
Ted Liou

Ted Liou

現役軟體工程師,Unity、AI 技術開發經驗分享與諮詢。