ComfyUI APIの落とし穴 — GUI形式とAPI形式はなぜ違うのか
はじめに
ComfyUIでワークフローを作り込んでいくと、ある時点で必ずこう思う。
「これ、毎回GUIでポチポチ実行するの面倒だな。APIで自動化したい」
n8nやDifyなどのワークフローツールと連携したい。バッチ処理で100枚回したい。AITuberのパイプラインに組み込みたい。理由は様々だが、行き着く先は同じ — ComfyUIのREST APIを叩くことだ。
ところが、GUIで「Save」したワークフローJSONを/promptエンドポイントにそのまま投げると、何も起きないか、エラーが返ってくる。
筆者はWindows環境でComfyUI CLIツールを自作する過程で、この「GUI形式とAPI形式の乖離」に何度もハマった。SetNode/GetNodeがmissing_node_typeエラーで弾かれ、widget_valuesの配列が名前付きパラメータに変換されずrequired_input_missingになり、フロントエンド専用ノードがAPIサーバーに存在しないことを知った。
本記事では、ComfyUIのGUI保存形式とAPI実行形式の構造的な違いと、自動化する際に踏む落とし穴を、実際のワークフローJSONを使って解説する。
検証環境
| 項目 | 詳細 |
|---|---|
| GPU | NVIDIA GeForce RTX 5090 (32GB VRAM) |
| OS | Windows 11 (26200.7840) |
| ComfyUI | v0.3.40 |
| Python | 3.13.12 |
| PyTorch | 2.9.1+cu130 |
| CUDA | 13.0 |
2つのJSON形式 — 何が違うのか
ComfyUIには「ワークフローJSON」が2種類ある。これが混乱の元凶だ。
GUI形式(Save で出力される)
GUIの「Save」ボタンで保存される形式。ノードの位置情報、色、リンクの配線情報など、GUIの表示に必要な全情報を含む。
{
"last_node_id": 291,
"last_link_id": 291,
"nodes": [
{
"id": 6,
"type": "EmptyLatentImage",
"pos": [-463.11, 1127.65],
"size": [421.85, 106],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [22]
}
],
"widgets_values": [720, 1280, 1]
},
{
"id": 38,
"type": "SetNode",
"title": "Set_Global_SIZE",
"inputs": [
{
"name": "LATENT",
"type": "LATENT",
"link": 22
}
],
"widgets_values": ["Global_SIZE"]
}
],
"links": [
[22, 6, 0, 38, 0, "LATENT"]
]
}
特徴:
-
nodes配列にノード一覧(位置、サイズ、色を含む) -
links配列にノード間の接続情報([link_id, from_node, from_slot, to_node, to_slot, type]) -
widgets_valuesにウィジェットの値が順番で格納された配列(名前なし) - フロントエンド専用ノード(SetNode, GetNode, Reroute等)を含む
API形式(/prompt エンドポイントが受け付ける)
ComfyUIのAPIが実際に実行できる形式。GUIの「Save (API Format)」で出力できる(後述)。
{
"6": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
}
}
}
特徴:
- キーがノードID(文字列)、値が
{class_type, inputs} -
inputsは名前付きパラメータ("width": 720) - ノード間のリンクは
[ノードID, スロット番号]で表現 - 位置情報、色、サイズなどGUI情報は一切含まない
- フロントエンド専用ノードは存在しない
一目でわかる違い
| 項目 | GUI形式 | API形式 |
|---|---|---|
| ノード格納 |
nodes配列(オブジェクト) |
フラットなdict(キーがノードID) |
| リンク情報 |
links配列(別テーブル) |
inputs内にインライン (["6", 0]) |
| ウィジェット値 |
widgets_values(順序配列、名前なし) |
inputs(名前付きdict) |
| 位置・見た目 | 含む(pos, size, color) | 含まない |
| フロントエンド専用ノード | 含む(SetNode, Reroute等) | 含まない(サーバーが知らない) |
| 用途 | GUIの保存・復元 | APIでの実行 |
この表の太字部分が、自動化しようとした時にハマるポイントだ。
落とし穴 1: widgets_values — 名前のない配列
最初にハマるのがここ。
GUI形式のノードを見てみよう:
{
"id": 139,
"type": "KSampler Adv. (Efficient)",
"widgets_values": [
"enable", 12345, "fixed", 8, 1, "euler", "simple",
0, 10000, "disable", "auto", "true"
]
}
12個の値が入っている。だがどの値がどのパラメータなのか、この情報だけではわからない。
同じノードのAPI形式:
{
"139": {
"class_type": "KSampler Adv. (Efficient)",
"inputs": {
"add_noise": "enable",
"noise_seed": 12345,
"control_after_generate": "fixed",
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 10000,
"return_with_leftover_noise": "disable",
"preview_method": "auto",
"vae_decode": "true"
}
}
}
widgets_valuesの配列を名前付きパラメータに変換する必要がある。この対応関係はノードの種類ごとに異なり、公式ドキュメントには書いていない。
解決策: /object_info エンドポイント
ComfyUIサーバーには /object_info というエンドポイントがあり、全ノードタイプの入力定義を返す。
curl http://127.0.0.1:8188/object_info/KSampler
レスポンス(抜粋):
{
"KSampler": {
"input": {
"required": {
"model": ["MODEL"],
"positive": ["CONDITIONING"],
"negative": ["CONDITIONING"],
"latent_image": ["LATENT"],
"seed": ["INT", {"default": 0, "min": 0, "max": 18446744073709551615}],
"control_after_generate": [["fixed", "increment", "decrement", "randomize"]],
"steps": ["INT", {"default": 20, "min": 1, "max": 10000}],
"cfg": ["FLOAT", {"default": 8.0}],
"sampler_name": [["euler", "euler_ancestral", "heun", ...]],
"scheduler": [["normal", "karras", "exponential", "simple", ...]],
"denoise": ["FLOAT", {"default": 1.0}]
}
}
}
}
requiredのキーの順序がwidgets_values配列の順序に対応する。ただしリンクで接続されている入力(model, positive, negative, latent_image)はwidgets_valuesには含まれない。
なお、optionalも同様に"input"."optional"キーの下に定義されている。requiredの後にoptionalの入力が続く順序だ。
つまり変換ロジックは:
-
/object_info/{ノードタイプ名}からノードの入力定義を取得 -
required→optionalの順で入力名リストを作成 - リンク接続済みの入力名を除外(値の先頭が
["MODEL"]や["CONDITIONING"]のように大文字の型名なら、それはリンク入力) - 残った入力名に
widgets_valuesを順番にマッピング
注意: カスタムノードの中にはobject_infoのキー順とwidgets_valuesの順序が一致しないものがある。筆者の経験ではDF_Text_Boxの入力名が Text(大文字T)であることに気づかずrequired_input_missingエラーに悩まされた。
GUIから「Save (API Format)」する方法
実はComfyUI GUIにはAPI形式で保存する機能がある。ただしデフォルトでは非表示だ。
- ComfyUI設定画面を開く(右上の歯車アイコン)
- 「Enable Dev mode Options」をONにする
- メニューに「Save (API Format)」が表示される
これを使えば変換を自分でやる必要はない。ただし、GUIで保存したワークフローをプログラムから動的に書き換えたい場合(プロンプトの差し替え、シード変更、バッチ実行など)は、GUI形式→API形式の変換ロジックが必要になる。
落とし穴 2: フロントエンド専用ノード
GUI形式のワークフローには、ComfyUIサーバーが認識しないノードが含まれている。API形式に含めるとmissing_node_typeエラーになる。
SetNode / GetNode(KJNodes)
これが最も厄介だった。
SetNode/GetNodeはComfyUI-KJNodesが提供するフロントエンド専用のJavaScriptノードだ。Pythonのサーバーサイドには存在しない。
用途: ワークフロー内で値を「名前付き変数」のように使い回す。
EmptyLatentImage → SetNode("Global_SIZE")
↓
GetNode("Global_SIZE") → KSampler A
GetNode("Global_SIZE") → KSampler B
GetNode("Global_SIZE") → KSampler C
GUIでは便利だが、APIサーバーはSetNodeを知らないので、そのまま送ると:
{
"error": {
"type": "missing_node_type",
"message": "Missing node type: SetNode",
"details": "node '38'"
}
}
解決策: SetNode/GetNodeのペアを解決して、直接リンクに置き換える。
# 変換前(GUI形式の関係性)
EmptyLatentImage [6] → SetNode [38] ("Global_SIZE")
GetNode [40] ("Global_SIZE") → KSampler [139]
# 変換後(API形式の直接リンク)
KSampler [139] の latent_image = ["6", 0] # EmptyLatentImageを直接参照
実装上の罠として、同じ名前のSetNodeが複数存在するパターンがある。筆者が遭遇した6モデルT2Iワークフロー(145ノード)では、Global_POSという名前のSetNodeが3つあり、そのうち2つは入力リンクを持たない「出力専用パススルー」だった。名前ベースのレジストリで逆引きする必要がある。
その他のフロントエンド専用ノード
| ノード | 提供元 | 役割 | API形式での扱い |
|---|---|---|---|
| SetNode / GetNode | KJNodes | 値の名前付きルーティング | 直接リンクに解決 |
| Reroute | ComfyUI Core | 配線の見た目整理 | スキップしてリンクを繋ぎ直す |
| Note | ComfyUI Core | メモ書き | 完全に無視 |
| PrimitiveNode | ComfyUI Core | 定数値の提供 | 値を下流ノードに埋め込む |
| Fast Groups Muter | rgthree | ノードグループの有効/無効切替 | 完全に無視 |
| Fast Groups Bypasser | rgthree | ノードグループのバイパス切替 | 完全に無視 |
これらを含んだままAPIに投げると全てmissing_node_typeエラーになる。自動変換する場合は全てハンドリングが必要だ。
落とし穴 3: リンク参照の形式が違う
GUI形式ではリンクが別テーブル(links配列)に集約されているが、API形式ではノードのinputs内にインラインで記述される。
GUI形式のリンク
{
"links": [
[22, 6, 0, 38, 0, "LATENT"]
]
}
[link_id, from_node_id, from_slot, to_node_id, to_slot, type]
ノード側ではlinkプロパティでリンクIDを参照する:
{
"inputs": [{"name": "LATENT", "type": "LATENT", "link": 22}]
}
API形式のリンク
{
"139": {
"inputs": {
"latent_image": ["6", 0],
"model": ["130", 0]
}
}
}
[接続元ノードID(文字列), 出力スロット番号]
変換時にはlinks配列をルックアップテーブル化して、各ノードの入力リンクを解決する必要がある。
# リンクテーブルを構築
link_map = {}
for link in workflow["links"]:
link_id, from_node, from_slot, to_node, to_slot, link_type = link[:6]
link_map[link_id] = (from_node, from_slot)
# ノードの入力リンクを解決
for inp in node["inputs"]:
if inp.get("link") is not None:
from_node_id, from_slot = link_map[inp["link"]]
api_inputs[inp["name"]] = [str(from_node_id), from_slot]
実践: curlでComfyUI APIを叩いてみる
ここまでの知識を踏まえて、実際にAPIでワークフローを実行してみよう。
Step 1: サーバーの死活確認
curl http://127.0.0.1:8188/system_stats
{
"system": {
"os": "nt",
"ram_total": 137138364416,
"ram_free": 104812175360,
"comfyui_version": "0.3.40"
},
"devices": [
{
"name": "NVIDIA GeForce RTX 5090",
"type": "cuda",
"vram_total": 34089730048,
"vram_free": 28741345280
}
]
}
Step 2: API形式のプロンプトを送信
curl -X POST http://127.0.0.1:8188/prompt \
-H "Content-Type: application/json" \
-d '{
"prompt": {
"1": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "your_model.safetensors"}
},
"2": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "1girl, cherry blossoms, anime", "clip": ["1", 1]}
},
"3": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "worst quality, blurry", "clip": ["1", 1]}
},
"4": {
"class_type": "EmptyLatentImage",
"inputs": {"width": 512, "height": 768, "batch_size": 1}
},
"5": {
"class_type": "KSampler",
"inputs": {
"model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0],
"latent_image": ["4", 0],
"seed": 42, "steps": 20, "cfg": 7, "sampler_name": "euler",
"scheduler": "normal", "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_test"}
}
}
}'
成功レスポンス:
{"prompt_id": "da8ef027-1753-4ab9-b7f0-08621c80a489", "number": 6}
Step 3: 実行結果を取得
curl http://127.0.0.1:8188/history/da8ef027-1753-4ab9-b7f0-08621c80a489
Step 4: WebSocketで進捗を監視(オプション)
リアルタイムで実行進捗を見たい場合はWebSocket接続を使う:
ws://127.0.0.1:8188/ws?clientId=my-client-001
受信するメッセージ:
{"type": "execution_start", "data": {"prompt_id": "..."}}
{"type": "execution_cached", "data": {"nodes": ["1", "2"]}}
{"type": "executing", "data": {"node": "5"}}
{"type": "progress", "data": {"value": 5, "max": 20}}
{"type": "executed", "data": {"node": "7", "output": {"images": [...]}}}
よくあるエラーと対処
| エラー | 原因 | 対処 |
|---|---|---|
missing_node_type: SetNode |
フロントエンド専用ノードをAPI形式に含めた | SetNode/GetNodeを解決して直接リンクに置換 |
required_input_missing |
widgets_valuesが名前付きに変換されていない |
/object_infoで入力定義を取得し正しくマッピング |
prompt is not valid JSON |
GUI形式のJSONをそのまま送った | API形式に変換してから送信 |
400 Bad Request(node_errors) |
リンク先のノードIDが存在しない | フロントエンドノード除去時にリンクが切れている |
| 黒い画像が出力される | ノード設定は正しいがパラメータの組み合わせが不適切 | KSamplerの設定(steps, cfg, sampler)を見直す |
まとめ
ComfyUI APIを使う上で知っておくべきポイント:
- GUI保存形式とAPI実行形式は別物。GUIのJSONをそのままAPIに投げても動かない
-
widgets_valuesは名前のない配列。/object_infoエンドポイントで入力定義を取得して名前付きパラメータに変換する - フロントエンド専用ノードはサーバーが知らない。SetNode/GetNode, Reroute, Note等はAPI形式から除去し、リンクを解決する必要がある
- API形式を手軽に得るなら「Dev mode」を有効化して「Save (API Format)」を使う。ただしプログラムから動的に操作したい場合は変換ロジックが必要
これらの知見は、筆者がComfyUI CLIツールを自作する過程で得たものだ。次回の記事では、これらの問題を吸収するCLIツールをPython + Typer + httpxで実装した話を書く。
📝 本記事の内容は RTX 5090 (32GB VRAM) / Windows 11 環境で筆者が実際に検証しています。執筆にはClaude Codeを補助利用しました。
シリーズ記事:
- ComfyUI APIの落とし穴 — GUI形式とAPI形式はなぜ違うのか(本記事)
- ComfyUI CLIを自作してワークフロー実行を自動化する(予定)
- Reddit発6モデルT2Iワークフローを解析・最適化した話(予定)
- RTX 5090でNVFP4量子化を試した — T2VとI2Vで品質が全然違う件(予定)
Discussion