--listen で常駐させ、Pythonから POST /prompt + WS /ws で叩く"無人ジョブランナー"を1本作る。これが量産効率化の9割。本DRはCC1の既存資産(GOLDEN勝ちパターン・メモリ番人・品質ゲート)を「APIで縫い合わせる」前提で書いている。新しく覚えるのはエンドポイント12個とWebSocketのメッセージ6種だけ。コードは4本(①WSクライアント ②OOMリトライ付きジョブランナー ③自動採点リジェネループ ④夜間CSVバッチ)をそのまま貼れる形で全文掲載した。
「市場規模」をここでは 1台のGPUで月に何枚の合格画像を作れるか(=供給キャパシティ) と定義する。手動運用とAPI無人運用では、ボトルネックが「人間の操作待ち」から「GPUの素の生成速度」へ移るため、桁が変わる。
CC1のGOLDEN勝ちパターン(IPAdapter無し / cfg6.0 / dpmpp_2m karras / 1024px / steps30 / 超シンプルprompt)[17]を waiIllustriousSDXL_v160 で回す前提で逆算する。
| 項目 | 値(目安) | 根拠・備考 |
|---|---|---|
| 1枚の生成時間(1024/steps30/normalvram) | 20〜40秒 | WSのprogress(value/max)で実測[2] |
| 理論上限 | 90〜180枚/時 | 生成のみ・待ち時間ゼロの仮定 |
| 安定スループット | 60〜120枚/時 | /free[3]のメモリ回収・キュー監視のオーバーヘッド込み |
| 合格率(品質ゲート76点+審美閾値) | 70〜85% | r18_quality_gate[18]+LAION審美[11] |
| ネット有効スループット(合格枚数) | 40〜80枚/時 | リジェネ分のロス込み |
| 夜間バッチ1回(8〜10時間) | 320〜800枚 | 無人・就寝中 |
| 月間(1日2バッチ運用) | 2〜5万枚規模 | 合格ベース。CG集なら数百冊分 |
重要なのは「人間が張り付く必要がゼロになる」点だ。手動だと1人が同時に見られるのは1キューだけで、夜間と日中の差が出ない。API無人運用は GPUの稼働率を24時間に近づける のが本質的な"市場拡大"であり、追加の人件費なしに供給量が数倍になる。
「ComfyUIを無人で大量に回す」目的に絞って、制御手段・ノード・ホスティングを横並びにした。結論は 「ComfyUI公式API直叩き+自作WSクライアント」がもっとも低遅延・高信頼。GUI系ノードは100枚規模までの補助、serverlessは初期投資ゼロのスケール用と棲み分ける。
| # | 手段/ツール | 無人量産での強み | 弱み・注意 | 推奨度 |
|---|---|---|---|---|
| 1 | ComfyUI公式API直叩き(/prompt+/ws+/history) | prompt_id keyed履歴[8]・executedでoutputs直取得・最低遅延 | 自分でWS再接続/リトライを実装する必要 | ★★★★★ |
| 2 | comfyui-api-client (PyPI)[1] | WS+HTTPの薄いラッパ・導入が速い | OOM分類リトライは自前追加が必要 | ★★★★ |
| 3 | ai-dock/comfyui-api-wrapper[10] | WS無音時に/queue+/history cross-check・OOM分類リトライの設計思想が秀逸 | 環境想定がコンテナ寄り | ★★★★ |
| 4 | SwarmUI | マルチGPU分散・UI完備 | UI層が厚くAPIラップが間接的・WS cross-checkが煩雑 | ★★★ |
| 5 | RunPod等 serverless GPU | 初期投資ゼロ・スパイク対応・--lowvram柔軟 | /free相当の制御が外部依存・cold start・従量課金 | ★★★ |
| 6 | ComfyUI-Queue-Manager[4] | アーカイブ/個別/バッチ実行をGUIで | 1000+枚はAPI直叩きに劣る | ★★★ |
| 7 | ComfyUI-Simple-Prompt-Batcher[12] | 複数prompt一括+モデルロード最適化 | node_errors自動検知が弱くリトライ原則違反を招きやすい | ★★★ |
| 8 | 9elements式 自前ホスティング[7] | workflow-as-API化のお手本・nginx前段 | 運用は自分持ち | ★★★ |
| 9 | ComfyUI-Gallery[14] | 出力のリアルタイム監視+メタ検査 | 生成本体ではなく可視化補助 | ★★★ |
| 10 | OSタスクスケジューラ/cron[5] | ComfyUIに無い"自動起動ボタン"を代替・夜間バッチの起点 | ジョブ管理は自分で書く | ★★★★ |
| Method | Endpoint | 用途 |
|---|---|---|
| POST | /prompt | workflow(API形式JSON)投入 → prompt_id返却 |
| GET | /prompt | 軽量なキュー状態(queue_remaining)取得 |
| GET | /queue | running + pending の詳細 |
| POST | /queue | 指定アイテム削除 / 全pendingクリア |
| POST | /interrupt | 実行中ジョブの中断 |
| GET | /history | 完了履歴(prompt_id keyed) |
| GET | /history/{id} | 個別ジョブの結果・メタ |
| POST | /upload/image | inputディレクトリへ画像保存(LoadImage用) |
| GET | /view | filename指定で出力画像を取得 |
| GET | /object_info | 全ノードカタログ(入出力スキーマ) |
| GET | /system_stats | Python/CUDA/VRAM等のサーバー情報 |
| POST | /free | モデルunload + GPUメモリ回収 |
| WS | /ws | リアルタイム進捗・完了・エラー |
出典: ComfyUI公式ドキュメント / Runflow APIリファレンス2026[8]。
| type | 意味 | 使いどころ |
|---|---|---|
status | キュー状態の変化 | 残ジョブ数の把握 |
executing | ノード開始。node==null でそのprompt完了 | 完了検知のトリガー |
progress | sampler進捗(value/max) | 進捗バー・ハング検知 |
executed | ノード完了+outputs(画像filename等) | 画像回収 |
execution_error | 例外詳細(node_errors含む) | リトライ可否の分類 |
execution_interrupted | 中断通知 | /interrupt後のクリーンアップ |
import json, time, uuid, urllib.parse, urllib.request, threading
import websocket # pip install websocket-client
class ComfyClient:
def __init__(self, host="127.0.0.1:8188"):
self.host = host
self.cid = str(uuid.uuid4()) # client_id は固定で使い回す
self.http_timeout = 10 # /prompt 投入は短く [10]
# ---- HTTP ヘルパ ----
def _get(self, path):
with urllib.request.urlopen(f"http://{self.host}{path}", timeout=self.http_timeout) as r:
return json.loads(r.read())
def _post(self, path, obj):
data = json.dumps(obj).encode()
req = urllib.request.Request(f"http://{self.host}{path}", data=data,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=self.http_timeout) as r:
return json.loads(r.read())
def queue_prompt(self, workflow):
"""API形式workflowを投入し prompt_id を返す"""
res = self._post("/prompt", {"prompt": workflow, "client_id": self.cid})
if "error" in res or res.get("node_errors"):
# validationエラー → 二度と成功しない。即raiseしリトライさせない [10]
raise ValidationError(res.get("node_errors") or res.get("error"))
return res["prompt_id"]
def get_history(self, pid):
return self._get(f"/history/{pid}").get(pid)
def get_image(self, filename, subfolder, ftype):
q = urllib.parse.urlencode({"filename": filename,
"subfolder": subfolder, "type": ftype})
with urllib.request.urlopen(f"http://{self.host}/view?{q}", timeout=60) as r:
return r.read()
def free(self, unload_models=False):
# メモリ番人と同じbody形式 [3]
self._post("/free", {"unload_models": unload_models, "free_memory": True})
# ---- 1ジョブ完了まで待つ(WS監視 + 再接続 + /history cross-check)----
def run_and_wait(self, workflow, exec_timeout=900, max_reconnect=5):
pid = self.queue_prompt(workflow)
done = threading.Event()
last_seen = {"t": time.time()}
reconnects = {"n": 0}
def on_message(ws, msg):
if not isinstance(msg, str): # バイナリ(プレビュー)は無視 [1]
return
d = json.loads(msg)
last_seen["t"] = time.time()
t = d.get("type")
if t == "progress":
p = d["data"]
print(f"\r {p['value']}/{p['max']}", end="", flush=True)
elif t == "executing" and d["data"].get("node") is None \
and d["data"].get("prompt_id") == pid:
done.set(); ws.close() # node=null = 完了
elif t == "execution_error" and d["data"].get("prompt_id") == pid:
done.set(); ws.close()
def on_error(ws, err):
pass
deadline = time.time() + exec_timeout
while not done.is_set() and time.time() < deadline:
ws = websocket.WebSocketApp(
f"ws://{self.host}/ws?clientId={self.cid}",
on_message=on_message, on_error=on_error)
# ping_interval=30 で heartbeat、無音切断を検知 [10]
t = threading.Thread(target=ws.run_forever,
kwargs={"ping_interval": 30, "ping_timeout": 10},
daemon=True)
t.start()
t.join(timeout=exec_timeout)
if done.is_set():
break
# --- WSが切れた: ジョブが生きているか /history と /queue で確認 ---
hist = self.get_history(pid)
if hist and hist.get("status", {}).get("completed"):
done.set(); break # 実は完走していた → 失敗扱いしない [10]
reconnects["n"] += 1
if reconnects["n"] > max_reconnect:
raise WSLost(f"prompt {pid} WS再接続上限超過")
time.sleep(2) # 少し待って再接続
hist = self.get_history(pid)
if not hist:
raise JobTimeout(f"prompt {pid} 実行タイムアウト")
# outputs から画像filenameを集めて回収
imgs = []
for node_out in hist["outputs"].values():
for im in node_out.get("images", []):
imgs.append(self.get_image(im["filename"], im["subfolder"], im["type"]))
return pid, imgs
class ValidationError(Exception): pass
class WSLost(Exception): pass
class JobTimeout(Exception): pass/history で完走済みかを照合してから再接続する。これを入れるだけで夜間バッチの"謎の取りこぼし"がほぼ消える。| フラグ | 対象VRAM | 挙動 |
|---|---|---|
| --highvram | 16GB+ | モデルをVRAM常駐・最速・段取り少ない量産向き[2] |
| --normalvram(既定) | 8〜16GB | スマートにRAMへoffload。CC1の標準 |
| --lowvram | 4〜8GB | 部分ロード。低VRAM延命 |
| --novram | 極小 | GPU転送最小・最遅 |
| --fp16-intermediates | — | Dynamic VRAMの実験フラグ。中間tensorをfp16化しRAM footprint削減[6] |
| PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True | — | 環境変数。VMM活用で断片化解消。実測16.39→10.83GiB[9] |
GPU消費電力300W・1枚20〜40秒・夜間電力単価を仮に約27円/kWhとして1枚の電気代を出す。
| 項目 | 計算 | 結果 |
|---|---|---|
| 1枚の消費電力量 | 0.3kW × (20〜40秒 ÷ 3600) | 0.0017〜0.0033 kWh |
| 1枚の電気代 | ×27円/kWh | 約 0.05〜0.09円 |
| 1000枚(生成のみ) | 合格率は別計算 | 約 50〜90円 |
| 合格1000枚に必要な生成枚数 | ÷ 合格率80% | 約1250枚 → 電気代 約63〜113円 |
つまり電気代は誤差レベル。CG集量産の損益はGPU電気代ではなく「①GPU減価償却 ②プラットフォーム手数料 ③合格率(=リジェネのロス)」で決まる。電気代を削るより合格率を上げてリジェネ回数を減らすほうが圧倒的に効く。
CG集としての販売面の単価設計・価格帯の科学は別DR(価格設定の科学・LTV最大化)に詳しいので、本DRは「供給原価=GPU時間×合格率」までを担当範囲とする。
| リスク | 症状 | 対策 |
|---|---|---|
| CUDA OOM(断片化型) | 「空きはあるのに out of memory」[3] | expandable_segments:True[9]+Dynamic VRAM[6]+/freeでunload |
| RAM枯渇でPCフリーズ | VRAMは余裕なのに固まる[19] | メモリ番人を常駐(<9GBでfree、<6GBでunload+WSトリム) |
| プロセス死・ハング | WS無音のまま進まない | progress無音タイムアウト→/interrupt→/free→再起動を監視ループで |
| WS切断の誤判定 | 生きてるジョブを失敗扱い | /history+/queue cross-check[10] |
| 品質ばらつき | たまに崩壊画像が混ざる | LAION審美[11]+品質ゲート[18]で自動reject |
| 規制・年齢表現 | R18の地雷 | 品質ゲートのKillスイッチ(mature/adult封殺・若年表現固定)[18] |
| データ消失 | workflow/seedが分からなくなる | PNG埋め込みworkflow[13]+メタDB+出力即バックアップ |
| 週 | ゴール | やること | 完了判定 |
|---|---|---|---|
| 第1週 | API基盤 | ComfyUIを--listen --highvram常駐化/第4章のWSクライアントでGOLDEN workflowを1枚API投入→画像回収まで | 手動0クリックで1枚出る |
| 第2週 | 落ちない化 | expandable_segments環境変数化/メモリ番人をbg常駐/OOMリトライ付きジョブランナー(第10章②)導入 | 1000枚連続でOOM落ち0回 |
| 第3週 | 高品質化 | LAION審美モデル導入→不合格seedリジェネループ(第10章③)/品質ゲート76点と直結 | 合格率75%以上で安定 |
| 第4週 | 無人化 | CSV→キュー投入の夜間バッチ(第10章④)をタスクスケジューラ登録/メタDB保存/朝レポート自動化 | 就寝→起床で数百枚の合格画像+ログが揃う |
各週は「前週の成果物が壊れていない」ことを必ず確認してから次へ進む。特に第2週のOOMゼロ化が崩れたまま第3週に行くと、リジェネが増えて原因切り分けが地獄になる。
下記のいずれかを2夜連続で割ったら、量産を止めて構成を見直す(=これ以上回しても赤字/品質事故のサイン)。
撤退=中止ではなく「自動運転を止めて人間が原因を潰すモード」へ切替えること。合格率割れの主因はたいていモデル/LoRAの組み合わせ崩れかseedリジェネの更新漏れ。第9章の落とし穴を上から確認する。
workflow[seed_node]["inputs"]["seed"]=random.randint(...)。PYTORCH_CUDA_ALLOC_CONF=expandable_segments:Trueを起動前に環境変数で[9]。Dynamic VRAM環境ならさらに解消[6]。CC1の既存資産を、APIランナーで以下のようにつなぐ。GOLDEN workflowを核に、メモリ番人(RAM)・OOMリトライ(VRAM)・自動採点(品質)の3層で守る。
import time, random
from comfy_client import ComfyClient, ValidationError, WSLost, JobTimeout
cli = ComfyClient("127.0.0.1:8188")
def set_seed(workflow, seed_node_id):
s = random.randint(0, 2**31 - 1)
workflow[seed_node_id]["inputs"]["seed"] = s # 毎回必ず更新 [10]
return s
def run_one(workflow, seed_node, max_retry=3):
"""1ジョブを賢くリトライ。validationエラーはリトライしない。"""
attempt = 0
while True:
attempt += 1
try:
return cli.run_and_wait(workflow, exec_timeout=900)
except ValidationError as e:
# node_errors → 何度やっても同じ。即あきらめ人間に通知 [10]
raise RuntimeError(f"VALIDATION(リトライ不可): {e}")
except (WSLost, JobTimeout, OSError) as e:
# ネット/WS/タイムアウト → 再試行OK
if attempt > max_retry:
raise
print(f" retry {attempt} ({type(e).__name__})")
time.sleep(3)
except Exception as e:
# CUDA OOM等 → モデルをunloadしてから再試行 [3][9]
msg = str(e).lower()
if ("out of memory" in msg or "oom" in msg) and attempt <= max_retry:
print(" OOM → /free(unload) して再試行")
cli.free(unload_models=True) # 断片化解消 [3]
set_seed(workflow, seed_node) # seedも変えておく
time.sleep(5)
continue
raiseimport io, torch, clip # pip install git+https://github.com/openai/CLIP
from PIL import Image
import torch.nn as nn
# LAION aesthetic predictor: CLIP ViT-L/14 embedding 上の線形回帰 [11]
device = "cuda" if torch.cuda.is_available() else "cpu"
clip_model, preprocess = clip.load("ViT-L/14", device=device)
class AestheticHead(nn.Module): # sa_0_4_vit_l_14_linear.pth をロード [11]
def __init__(self):
super().__init__()
self.fc = nn.Linear(768, 1)
def forward(self, x): return self.fc(x)
head = AestheticHead().to(device)
head.load_state_dict(torch.load("sa_0_4_vit_l_14_linear.pth", map_location=device))
head.eval()
@torch.no_grad()
def aesthetic_score(png_bytes):
img = preprocess(Image.open(io.BytesIO(png_bytes)).convert("RGB")).unsqueeze(0).to(device)
feat = clip_model.encode_image(img).float()
feat = feat / feat.norm(dim=-1, keepdim=True) # 正規化が必須
return head(feat).item() # 1.0〜10.0
# ---- 合格するまでseedを変えてリジェネ(最悪N回で打ち切り=原価管理)----
def gen_until_pass(workflow, seed_node, min_aesthetic=5.5, max_tries=6):
from job_runner import run_one, set_seed
best = (-1.0, None)
for i in range(max_tries):
set_seed(workflow, seed_node) # 毎回seed変更 [10]
pid, imgs = run_one(workflow, seed_node)
if not imgs:
continue
score = aesthetic_score(imgs[0])
print(f" try{i+1} aesthetic={score:.2f}")
if score >= min_aesthetic:
return pid, imgs[0], score # 合格
if score > best[0]:
best = (score, imgs[0])
# 規定回数で合格せず → ベストを返す(無限ループ防止=原価上限) [#1]
return None, best[1], best[0]import csv, json, copy, sqlite3, datetime, pathlib
from aesthetic_gate import gen_until_pass
from comfy_client import ComfyClient
cli = ComfyClient("127.0.0.1:8188")
BASE = json.load(open("golden_api.json", encoding="utf-8")) # GOLDEN workflow [17]
SEED_NODE, POS_NODE, CKPT_NODE = "3", "6", "4" # 自環境のノードIDに合わせる
OUT = pathlib.Path(r"D:\ComfyUI\output\night") # 出力先 [17]
OUT.mkdir(parents=True, exist_ok=True)
db = sqlite3.connect("night.db")
db.execute("""CREATE TABLE IF NOT EXISTS gen(
ts TEXT, row INT, prompt TEXT, model TEXT, aesthetic REAL, file TEXT, passed INT)""")
def build(row):
wf = copy.deepcopy(BASE)
wf[POS_NODE]["inputs"]["text"] = row["prompt"]
wf[CKPT_NODE]["inputs"]["ckpt_name"] = row.get("model", "waiIllustriousSDXL_v160.safetensors")
return wf
with open("queue.csv", encoding="utf-8") as f:
rows = list(csv.DictReader(f)) # 列: prompt, model, count
n = 0
for i, row in enumerate(rows):
for _ in range(int(row.get("count", 1))):
wf = build(row)
pid, img, score = gen_until_pass(wf, SEED_NODE, min_aesthetic=5.5, max_tries=6)
passed = 1 if pid else 0
fname = OUT / f"{datetime.datetime.now():%Y%m%d_%H%M%S}_{i}_{score:.1f}.png"
if img:
fname.write_bytes(img)
db.execute("INSERT INTO gen VALUES(?,?,?,?,?,?,?)",
(datetime.datetime.now().isoformat(), i, row["prompt"],
row.get("model",""), score, str(fname), passed))
db.commit(); n += 1
# 適宜メモリ回収(メモリ番人と二重でも害なし)[3]
if n % 20 == 0:
cli.free(unload_models=False)
print(f"夜間バッチ完了: {n}枚処理 / DB=night.db")schtasks /Create /TN "ComfyNightBatch" /SC DAILY /ST 02:00 ^ /TR "cmd /c set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True&& python D:\proj\night_batch.py" ^ /RL HIGHEST /F
この4本(WSクライアント・ジョブランナー・審美リジェネ・夜間バッチ)で、CC1の量産は「就寝→起床で合格画像とメタDBが揃う」無人ループになる。あとはCSVに作りたいテーマと枚数を書くだけ。メモリ番人はこれと並行してbg常駐させ続ける。
本DRは「API無人運用インフラ」に特化。重複を避け、隣接領域は以下の既存DRに委譲する。
本DR内のComfyUIエンドポイント・WSメッセージ型・VRAMフラグ・OOM対策・リトライ原則・審美モデル仕様・メタノードは全て上記実在ソースで裏取り済み。GOLDEN/品質ゲート/メモリ番人はCC1の実在内部資産(パス明記)。