はじめに
個人的に購入したDGX Sparkは統合メモリが128GBもあるので、ローカルLLMを動かしつつComfyUIで画像生成することだって可能。すごい!
しばらく、ローカルLLMにお願いして、プロンプトを考えてもらって、そのプロンプトをComfyUIにコピペして、画像生成させる、っていうことを繰り返していたんだけど、めんどくさくなってきた。
ローカルLLMからComfyUIまでを一気通貫に自動連携させたい!
この記事では、以下の構成で OpenWebUI と ComfyUI を連携させる仕組み を構築した記録をまとめる。
- OpenWebUI + Ollama(ローカルLLM環境)
- ComfyUI(画像生成エンジン)
- 今回自作した中継API(bridge.py)
※ bridge.py は今回の記事のために自作したスクリプトであり、既存ツールではない。 - OpenWebUIのPipe(Function)
これにより、
OpenWebUIに日本語で指示を書くと、
ローカルLLMがプロンプトを生成し、
ComfyUIにAPI経由で画像生成が投げられる
というパイプラインを構築できた。
できること(デモのイメージ)
たとえば、OpenWebUI上で次のように日本語を入力する。
真夜中に債務者たちと鉄骨渡りをする女子高生
すると裏側では、
- Pipeがユーザー入力を取得
- ローカルLLM(例:dolphin3:8b)が英語プロンプトを生成
- 自作API bridge.py にPOST
- bridge.pyがComfyUI APIにリクエスト
- ComfyUIが画像生成を実行
- outputフォルダに画像が出力される
という処理がすべて自動で走り、画像が出来上がる。
全体構成
コンポーネントは以下の3つ。
- OpenWebUI(UI + Pipe)
- 自作の中継API(bridge.py)
- ComfyUI(画像生成API)
前提環境
この記事では、以下の環境がすでにセットアップ済みであることを前提とする。
- 実行環境:DGX Spark
- OpenWebUI + Ollama が動作している
- 未導入の場合は公式手順を参照
https://build.nvidia.com/spark/open-webui
- 未導入の場合は公式手順を参照
- ローカルLLMとして
dolphin3:8bモデルを導入済み - ComfyUI が動作している
- 未導入の場合は公式手順を参照
https://build.nvidia.com/spark/comfy-ui
- 未導入の場合は公式手順を参照
- ComfyUIには以下のワークフローを導入済み
- NetaYume Lumina
- ComfyUI API が http://localhost:8188 でアクセスできる状態
ComfyUI側の準備(重要)
1. workflowをAPI用JSONとしてエクスポートする
ComfyUIには「APIとしてそのまま使えるworkflow JSON」をエクスポートする機能がある。
手順:
- 設定から「開発者モード」をON
- NetaYume Luminaのワークフローを開いた状態で左上の「C」メニュー
- ファイル → エクスポート (API)
-
workflow_api.jsonとして保存する
このJSONが、bridge.pyからComfyUIに投げるテンプレートになる。

2. JSONのどこを書き換えるか(今回のケース)
今回使用している workflow(netayume_lumina)では、
以下のノードを書き換えればプロンプトを差し替えられる構造になっていた。
- ポジティブプロンプト本体
- ノードID "26:24" の inputs.value
- ネガティブプロンプト本体
- ノードID "25:24" の inputs.value
後述する bridge.py は、JSON内の次の箇所を書き換えることでプロンプトを差し替える。
"26:24": {
"inputs": {
"value": "ここを書き換える"
}
}
自作中継API(bridge.py)の作成とセットアップ
今回の連携のために作成した中継API(bridge.py)を用意する。
bridge.py は FastAPIを利用して以下を実装したスクリプト。
- OpenWebUIからプロンプトを受け取る
- workflow JSONを書き換える
- ComfyUIの /prompt API に投げる
1. ディレクトリ作成とFastAPIのインストール(仮想環境)
mkdir ~/comfy-bridge
cd ~/comfy-bridge
python3 -m venv venv
source venv/bin/activate
pip install fastapi uvicorn requests websocket-client
2. workflow_api.json を配置
先ほどComfyUIからエクスポートしたJSONを配置する。
cp ~/ComfyUI/.../workflow_api.json ~/comfy-bridge/
3. bridge.py を作成
~/comfy-bridge/bridge.py を作成して、中身に以下のコードを貼り付け:
コードが長いので折りたたんである。
クリックしてコードを展開
import json, uuid, time
import requests
import websocket
from fastapi import FastAPI
from pydantic import BaseModel
import hashlib
def sha10(s: str | None) -> str:
s = s or ""
return hashlib.sha1(s.encode("utf-8")).hexdigest()[:10]
COMFY = "http://localhost:8188"
WORKFLOW_PATH = "./workflow_api.json"
POS_VALUE_NODE = "26:24" # positive本体
NEG_VALUE_NODE = "25:24" # negative本体
app = FastAPI(title="ComfyUI Bridge")
class GenReq(BaseModel):
prompt: str
negative: str | None = None
def load_workflow():
with open(WORKFLOW_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def patch_prompt(workflow: dict, positive: str, negative: str | None):
workflow[POS_VALUE_NODE]["inputs"]["value"] = positive
if negative is not None:
workflow[NEG_VALUE_NODE]["inputs"]["value"] = negative
def queue_prompt(workflow: dict, client_id: str):
r = requests.post(f"{COMFY}/prompt", json={"prompt": workflow, "client_id": client_id}, timeout=30)
r.raise_for_status()
return r.json()["prompt_id"]
def wait_done_ws(client_id: str, timeout_s: int = 300):
ws = websocket.WebSocket()
ws.connect(f"{COMFY.replace('http://','ws://')}/ws?clientId={client_id}")
t0 = time.time()
while time.time() - t0 < timeout_s:
msg = ws.recv()
if not msg:
continue
data = json.loads(msg)
if data.get("type") == "executing" and data.get("data", {}).get("node") is None:
break
ws.close()
def get_first_image_url(prompt_id: str):
h = requests.get(f"{COMFY}/history/{prompt_id}", timeout=30)
h.raise_for_status()
hist = h.json().get(prompt_id, {})
outputs = hist.get("outputs", {})
for node_out in outputs.values():
images = node_out.get("images")
if images:
img = images[0]
filename = img["filename"]
subfolder = img.get("subfolder", "")
img_type = img.get("type", "output")
return f"{COMFY}/view?filename={filename}&subfolder={subfolder}&type={img_type}"
raise RuntimeError("historyに画像が見つからない。SaveImageノードがあるか確認。")
@app.post("/generate")
def generate(req: GenReq):
wf = load_workflow()
patch_prompt(wf, req.prompt, req.negative)
client_id = str(uuid.uuid4())
prompt_id = queue_prompt(wf, client_id)
wait_done_ws(client_id)
url = get_first_image_url(prompt_id)
return {"prompt_id": prompt_id, "image_url": url}
@app.post("/enqueue")
def enqueue(req: GenReq):
if not (req.prompt or "").strip():
print("[ENQUEUE-SKIP] empty prompt")
return {"prompt_id": "(skipped-empty)"}
print(
"[ENQUEUE-IN ] "
f"pos_sha={sha10(req.prompt)} neg_sha={sha10(req.negative)} "
f"pos_head={req.prompt[:60]!r}"
)
wf = load_workflow()
patch_prompt(wf, req.prompt, req.negative)
client_id = str(uuid.uuid4())
prompt_id = queue_prompt(wf, client_id)
print(
"[ENQUEUE-OUT] "
f"prompt_id={prompt_id} client_id={client_id} "
f"pos_sha={sha10(req.prompt)}"
)
return {"prompt_id": prompt_id}
bridge.py 内で、今回は以下の通り設定。
-
COMFY- 例:
http://localhost:8188
- 例:
-
WORKFLOW_PATH- 例:
./workflow_api.json
- 例:
-
POS_VALUE_NODE- 例:
"26:24"
- 例:
-
NEG_VALUE_NODE- 例:
"25:24"
- 例:
4. 起動
bridge API を立ち上げて http://localhost:8000 で待ち受ける。
uvicorn bridge:app --host 0.0.0.0 --port 8000
OpenWebUIにPipeを追加する
OpenWebUIの管理画面から Functions → 新しいFunction を追加する。
Functionの中身を以下のコードで置き換えて、名称と説明をComfyUI enqueueにして、カスタムPipeを作る。
コードは長いので折りたたんである。
クリックしてコード展開
from pydantic import BaseModel, Field
from fastapi import Request
import json, re
import requests
from open_webui.models.users import Users
from open_webui.utils.chat import generate_chat_completion
class Pipe:
class Valves(BaseModel):
BASE_MODEL: str = Field(
default="dolphin3:8b", description="日本語→英語プロンプト生成に使うモデル"
)
BRIDGE_ENQUEUE_URL: str = Field(
default="http://localhost:8000/enqueue", description="bridge /enqueue"
)
ALWAYS_NEGATIVE: str = Field(default="", description="常に足すネガティブ(任意)")
TIMEOUT_S: int = Field(default=120, description="HTTPタイムアウト")
def __init__(self):
self.valves = self.Valves()
def pipes(self):
return [{"id": "jp2comfy_enqueue", "name": "JP→ComfyUI enqueue (no image)"}]
async def pipe(self, body: dict, __user__: dict, __request__: Request):
user = Users.get_user_by_id(__user__["id"])
# 最新のユーザー発話を取得
user_text = ""
for m in reversed(body.get("messages", [])):
if m.get("role") == "user":
user_text = m.get("content", "")
break
user_text = (user_text or "").strip()
if not user_text:
return "(skip) empty user message"
# 1) ベースLLMに「strict JSON」だけを返させる(tool不要)
sys = (
"You are a prompt engineer for ComfyUI/Stable Diffusion.\n"
"Return STRICT JSON only (no markdown, no commentary).\n"
'Schema: {"positive":"...","negative":"..."}\n'
"positive: English prompt. negative: optional English negative prompt."
)
usr = f"Japanese instruction:\n{user_text}\n\nReturn JSON only:"
sub_body = {
"model": self.valves.BASE_MODEL,
"messages": [
{"role": "system", "content": sys},
{"role": "user", "content": usr},
],
"stream": False,
"temperature": 0.7,
"max_tokens": 300,
}
resp = await generate_chat_completion(__request__, sub_body, user)
content = resp.get("choices", [{}])[0].get("message", {}).get("content", "")
# JSON抽出(モデルが余計な文字を混ぜても耐える)
m = re.search(r"\{.*\}", content, re.S)
if not m:
return f"[ERROR] prompt JSON parse failed. Raw:\n{content}"
j = json.loads(m.group(0))
positive = (j.get("positive") or "").strip()
negative = (j.get("negative") or "").strip()
if self.valves.ALWAYS_NEGATIVE:
negative = (self.valves.ALWAYS_NEGATIVE + " " + negative).strip()
# 2) bridgeへ投げる(待たない)
r = requests.post(
self.valves.BRIDGE_ENQUEUE_URL,
json={"prompt": positive, "negative": negative or None},
timeout=self.valves.TIMEOUT_S,
)
r.raise_for_status()
data = r.json()
prompt_id = data.get("prompt_id", "")
# 3) チャットには「投入した」ことだけ返す(画像表示しない)
return (
f"Queued to ComfyUI.\n"
f"prompt_id: {prompt_id}\n"
f"positive: {positive}\n"
f"negative: {negative}"
)
コードを保存したら、トグルスイッチをONにしておく。
このPipeは、次の処理を行う。
- 最新のユーザー発話を取得
- ローカルLLMに「strict JSON形式でプロンプトを返せ」と指示
- JSONから positive / negative を抽出
- bridge.py にPOST
Pipe内では generate_chat_completion を使って内部的に別モデル(例:dolphin3:8b)を呼び出している。
結果として、
- OpenWebUIの「モデル選択UI」にPipeが表示されるため、ユーザー体験としては「新しいモデルが追加された」ように使える。
- 実体はPipeが処理をルーティングしているだけ
という構成になっている。
実際に使ってみる
あとは OpenWebUI 上で、今回作ったPipeJP→ComfyUI enqueue(no image)を選択して普通にチャット入力するだけ。
- 日本語で指示を書く
- ComfyUIのキューが増える
- outputフォルダに画像が出力される
ちなみに、bridge API を立ち上げておくこと。
cd ~/comfy-bridge
source venv/bin/activate
uvicorn bridge:app --host 0.0.0.0 --port 8000
感想
- こちらが指定する通りのプロンプトを書いてくれないLLMがほとんど。Qwenとかgpt-ossとかはいろんな面で厳しい印象。dolphinは、比較的言うことを聞いてくれるモデルだったので採用。
- LLMは、negativeプロンプトに不適切と判断された単語を入れてくることがあるので、Pipeでその部分を切り離しておいたほうがいいかも?
- 画像生成モデルとして、NetaYume Luminaは言うことを結構聞いてくれる。ここでもQwenは厳しくて、言うことを聞いてくれない。
- 最初はOpenWebUIのToolを使おうと悪戦苦闘したが、だめだった。使い方が全然わからない。結果として、Pipe + 自作API連携で実現できた。
- 画像生成が終わる前でも、チャット画面でどんどんお願いしていけば、ComfyUI側でキューが溜まっていく。実は、チャット画面で生成した画像を表示させることを考えていたけど、そうすると、チャット画面での連続お願いができなくなっちゃうのでやめた。
まとめ
- OpenWebUIとComfyUIをAPIで連携できた。
- 日本語指示から画像生成までを一気通貫で自動化できたので色々捗る。
ソースコード
本記事で紹介したコード一式は以下に公開してる。
https://github.com/hoshibayasushi/comfyui-bridge



