西川和久の不定期コラム

ComfyUIのWebUIを作りました!普段使うWorkflowが固定ならこの方が見やすいかも!?

 8月に「ComfyUI」を使って話題の「FLUX.1」を生成できるWorkflowをjson付きでご紹介した。しかし、実際操作すると「AUTOMATIC1111」のようなUIの方が使いやすいという場面もある。そこで今回は、ComfyUIが搭載しているAPIを使ってHTMLとJavaScriptだけで簡単なWebUIを作ってみたい。

ComfyUIは便利なのだが……。

 ComfyUIは、最新モデル(Model)などにいち早く対応し、とにかくこれがないと始まらない状況が続いている。また普段生成するWorkflowが決まっていれば、Nodeを使いMy Workflowを構築できるなど、AUTOMATIC1111にはない魅力がある。

 ただ実際触り出すと、プロンプト(Prompt)をや数値などを入力する時には移動/ズームして表示、すると生成した画像がはみ出すのでまた移動してズームアウト……といった操作が結構頻繁に発生する。このため、プロンプトだけ変えて結果を得たり、少し数値を変えて様子を見るといった、作り込むような使い方にはあまり向いていないように思う。

AUTOMATIC1111から派生したFLUX.1対応Forge
ComfyUI。筆者が普段使っているWorkflow

 このようなケースでは、AUTOMATIC1111的に、入力する項目がWebUI上でガッチリ固定されている方がやりやすい。

 そこでComfyUIのAPIを使って超簡易(笑)AUTOMATIC1111っぽいWebUIを作ってみたい。一般的にはPythonのライブラリ、Gradioを使うのだろうが、それではハードルが高いので、ちょっとサイトを作ったことがある人なら誰でも分かるHTML/JavaScriptで動くようにした。

 ファイルはindex.htmlの1本。これならWebブラウザでindex.htmlを開けば動作するので手間もかからない。コードも公開するので、好みにカスタマイズして使って頂ければと思う。

用意するもの


    1. 起動しているComfyUI
    2. Google Chrome もしくは Edge
    3. 拡張機能のCORS Unblock
    4. 筆者が作ったindex.html(zip圧縮、5,034Byte、リンクを右クリックで「リンク先をファイルに保存」でブロックを解除して保存してください)。

以上4つ。1は当たり前として(笑)、2でChromeとEdgeに限定しているのには理由がある。これはセキュリティ上の問題で、ローカルのindex.htmlからComfyUIへPOSTしようとすると、CORSエラーが発生するためだ。これ自体は仕方がないのだが、この状態ではComfyUIのAPIが使えない(GETはできる)。そこで、対策として3の拡張機能「CORS Unblock」を予めブラウザにインストールする必要があり、FirefoxやSafariで該当するものがあるのか不明なので、ChromeとEdge限定とした。

 ただ、Webブラウザ側で禁止しているのを態々CORS Unblockを使って穴を開けるので、本index.htmlを使わない時は、常時オフをお勧めする。

【8時30分追記】ComfyUIのオプションに --enable-cors-header を付けて起動すればCORS Unblockは必要なく、Safari、Firefoxでも作動する。

内部の説明の前にまず使ってみよう!

 内部の話はプログラム的な要素が多く、分からない人もいらっしゃると思うので、まず実際動かしてみたい。

拡張機能CORS Unblock
index.htmlをWebブラウザへドラッグ&ドロップ。UIが表示される。この後、CORS Unblockをオン
Checkpoint、LoRA、各種パラメータを設定し画像生成。使用したCheckpointはclip/t5/vae全部入りのflux1-dev-fp8.safetensors
SDXLも生成できる。久々に触ると速いが、やっぱり絵っぽい

 機能は少ないものの、AUTOMATI1111っぽい雰囲気で操作できるのがお分かり頂けるだろうか?CheckpointはSD 1.5、SDXL、そして一応FLUX.1 [dev][schnell](clip/t5/vae全部入りタイプのみ)にも対応する。左下のファイルアップロードは、後半の説明用でこれを使った機能はない。

 またNegative Promptの項目はないが、主にFLUX.1での使用を考え、あえて付けていない。index.html中のWorkflowに、Negative Promptを入れる部分があるので(以下参照)、必要な人は直接書き換えるか、後半のコード説明を理解して<textarea></textarea>を1つ追加すればWebUI上からも操作可能となる。

 "7": {
    "inputs": {
      "text": "text, watermark",
※ Negative Prompt部分。2箇所ある
Workflow / LoRAなし
Workflow / LoRAあり(青のNode)

 使用したWorkflowは2つ。違いはLoRAの有無となる。なしの方はComfyUIデフォルトとほぼ同じ。最後のSave ImageをPreview Imageに変えたものだ。これは前者だと毎回画像を自動的に保存するのだが、そんなに打率がいい分けでもないため(笑)、OKのもののみ手動で保存する関係から後者としている。LoRAありは先のWorkflowに1つLoRAのNodeを加えたものとなる。

 “一応”FLUX.1 [dev][schnell]対応と書いたのは、以前の記事でも少し触れたが、このWorkflowではFLUX.1 [dev][schnell]に必要なModelSamplingFlux(1.15/0.5)、FluxGuidance(3.5)などが抜けている。従って正式対応Workflowではない。

 しかし、なぜか不都合なく生成するので、今回はSD 1.5/SDXL対応も含めるため、あえてこのままの状態で使っている。FLUX.1専用で使いたい人は上記の記事を参考にして、Nodeを追加して欲しい(正式なWorkflow参照)。

中身の話 / HTML編

 ここから先は少し?マニアックな内容だ。

 まずComfyUI APIを使い画像生成する流れは以下のようになる。

0. endpointは http://127.0.0.1:8188 (WebUIと同一PCの場合)
1. CheckpointとLoRA一覧を取得。endpoint/object_info(GET)
2. Workflowの各項目に値をセット
3. endpoint/promptにWorkflowをPOST。正常終了するとpromptIdが戻る
4. endpoint/history/promptId(GET)で画像を生成。生成時間がかかるので1秒毎にポーリングする。正常終了すると画像ファイル名が入った配列が戻る
5. 配列の数だけループを回し endpoint/view?filename=${image['filename']}で画像を得る(Preview Imageの場合は&type=tempを付ける)

 1はPATHまで含めてCheckpoint/LoRA名が一致しないとWorkflowが動かないため、ComfyUI側の環境に合っている一覧を取得し、設定する必要がある。

 2は主にHTMLの担当部分。以降の説明で必要項目のフォームを作り、それをWorkflowへ渡す。3〜5は後述するJavaScript編の処理だ。

 まず添付したindex.htmlをVSCodeなどエディタで開いてみてほしい。前半は、Bootstrapを使った簡単な2カラム構成となっている。

 左にパラメータ系、右に生成画像を配置。inlineのCSSは、画像生成中の待ち時間に表示するアニメーションと、select項目を検索できるselect2/ダークテーマ用だ。

HTML最初の部分、CSSの定義などがある

 Workflowに必要なパラメータに関連するHTML部分だけ抜粋すると以下のようになる。よく見かける日本的で複雑なお問合せフォームより数が少ないので簡単だ(笑)。

<select class="form-select" id="checkpoint-select"></select>
<select class="form-select" id="lora-select"></select>
※ ComfyUI APIを使って取得したCheckpointもしくはLoRA一覧

<input type="text" aria-label="lora" class="form-control" id="p_lora" value="0.8">
※ LoRA使用時の重み

<textarea class="form-control" id="prompt" rows="4">photo of a japanese woman 20 years old wearing camisole and shorts.</textarea>
※ Prompt。デフォルトでありがちなのを設定

<input type="text" aria-label="width" class="form-control" id="p_width" value="832">
<input type="text" aria-label="height" class="form-control" id="p_height" value="1216">
<input type="text" aria-label="steps" class="form-control" id="p_steps" value="20">
<input type="text" aria-label="cfg" class="form-control" id="p_cfg" value="1">
<input type="text" aria-label="seed" class="form-control" id="p_seed" value="-1">
※ 上から順に幅、高さ、Steps、CFG、Seed

 ほかは見栄えを少しマシにするため、Bootstrapの作法に基づき書いたもので特になくてもJavaScriptのプログラム自体は動作する。重要なのはここだけだ。

中身の話 / JavaScript編

 筆者がサイトを作っていた頃はjQuery全盛期?で、素のDOM操作などほとんどしたことがなかった。ところが近年では少しでもパフォーマンスを上げるため使わないのが主流となっている。調べながらも面倒なのでローカルで動作しているLLM(Meta-Llama-3.1-8B-Instruct-Q8_0.gguf)にいろいろ聞きながらのコーディングとなった(笑)。

 なお、コードの行数を減らすため、エラー処理はほとんど入っていない。予めご了承いただきたい。

 実はGradio(Python)版でさらに高機能のものは既に作っており、JavaScriptへの書き換えはどうすれば?的なもの聞いている。LLMからの回答は、当たるも八卦当たらぬも八卦。最終的には筆者の判断となる。

 と、頑張ってjQueryなしで書いたものの、最後の最後、「selectは件数も多いし検索できた方がいいよね!」的に使ったselect2がjQuery。$('.form-select').select2()この1行のためだけにjQueryを読み込むことに……。jQueryを使わないChoices.jsもあるのだが、こちらはダークテーマにうまく適合できず、また今回見栄えや操作性は本論ではないため詳しくは調べていない。

 さて前置きが長くなってしまったが、JavaScript、冒頭にあるのがWorkflow 2種類のjson。違いは先に書いた通りLoRA Nodeの有無となる。このjsonそのものの作り方は、

1. ComfyUI / 設定(左下にある歯車アイコン) で Dev ModeをON
2. Workflowの保存時、Save/Save As/Exportの3択だったのがExport(API Format)が増えており、これを使う

 という手順となる。ただしそのままではAPIに渡す形式になっておらず、

const workflow = {
  "prompt": {
.
. ここにはめ込む
.
  }
 }

 こんな感じで保存したjsonファイルの中身を上記の構造へはめ込む必要がある。もちろんプログラム的にはめ込んでもいいし、今回のようにそのままコードに書いてもよい。

 次にComfyUIのendpoint。ComfyUIもこのWebUIも同じPCなら下記の通り。もし筆者のように別のPCでComfyUIが動いている時は、IPアドレス部分を書き換える。

// ComfyUI endpoint
function endpoint() {
    return 'http://127.0.0.1:8188'
}

 Checkpoint一覧とLoRA一覧は結構複雑な構造になっているので、どちらもこのファンクションを“おまじない”として使ってほしい。

// Checkpoint一覧取得
async function getCheckpointList()
// LoRA一覧取得
async function getLoraList()

 正常終了すると、先のHTMLのこの部分、option valueに一覧が入っている。

<select class="form-select" id="checkpoint-select"></select>
<select class="form-select" id="lora-select"></select>

 次はSeed -1の時の乱数生成。AUTOMATIC1111などはSeedに-1を指定すると乱数になるが、これは-1の時、Samplerが自動的に乱数を生成しているのでなく、プログラム側で生成する必要がある。

// seed -1時の乱数発生
function generateRandomSeed()

 LLMに「ComfyUIのSeed -1的な乱数生成」と指示して出てきた答えなので、桁数や最大値などが正しいかは不明。とは言え、遊ぶ程度なら大丈夫そうだ。

LLMを使ってSeedが-1の時の乱数生成方法
LLMを使って既にPythonで動いているgetImages()の一部をJavaScriptへ変換(endpoint/history/data.prompt_idのループの部分)

 function isLoRA()は、LoRAを使用するしないでWorkflowを切り替える必要があるため、先のselectがNoneの時はfalse、None以外=LoRA使用の時はtrueを返す。

 そして本丸のfunction getImages(workflow)。まず引数のworkflowにLoRAなし版かあり版かどちらかのworkflowが入っている。そのworkflowに各パラメータをセット。たとえばこの部分、

workflow["prompt"]["3"]["inputs"]["steps"] = document.getElementById('p_steps').value
workflow["prompt"]["3"]["inputs"]["cfg"] = document.getElementById('p_cfg').value

 workflowの実態は、

"prompt": {
  "3": {
    "inputs": {
      "seed": 125441219168538,
      "steps": 20,
      "cfg": 1,
      "sampler_name": "euler",
      "scheduler": "simple",
      "denoise": 1,

 こうなっている。従って、stepsやcfgを変える時は上記のような表記となる。seedも同様だが、値が-1か固定値がによって動きが変わるので、この後に条件文と共に入っている。

// LoRA有り
if (isLoRA()) {
    workflow["prompt"]["11"]["inputs"]["lora_name"] = document.getElementById("lora-select").value
    workflow["prompt"]["11"]["inputs"]["strength_model"] = document.getElementById('p_lora').value
    workflow["prompt"]["11"]["inputs"]["strength_clip"] = document.getElementById('p_lora').value
}

 ここはLoRA指定があった時に追加のパラメータ設定。LoRA未対応のWorkflowだとそもそも"11": がないためエラーが発生する。

 また今回WorkflowをLoRAの有無で2つ用意したが、実はLoRAありの1つのみで済ます方法もある。LoRA使用時はそのまま使い、LoRA未使用時は、この"11":の要素を削除、参照しているほかのNodeの番号を正常な参照先に書き換える。

 当初はLoRA関連のNodeをBypassしたWorkflowを使えば……っと思っていたが(Bypass属性だけオン/オフすれば済むかも!?と)、API用のWorkflowで保存すると、BypassしたNodeは全部消えしまい、あっさりこの手法はボツとなった(笑)。

 注意点としては、それ(LoRA Nodeの有無)以外は全く同じ必要があること。そうしないと、たとえばworkflow["prompt"]["3"]["inputs"]["cfg"]の"3"が"4"だったりすると、毎回どのWorkflowか調べ、セットする場所を変えなければならなくなる。

 Workflowの準備ができたらendpoint/promptへPOSTする。正常終了すると戻ってきた data.prompt_idを元にendpoint/history/data.prompt_idを呼び出し(GET)、画像生成完了するまで(つまりなかなか戻ってこない)1秒間隔でループを回す。戻ってきたら historyData[promptId]['outputs']['10']['images'] を元にdisplayImages()を呼び出す。

 この"10":はWorkflowを見るとPreview Imageになっている。この出力を使うという分けだ。

  "10": {
    "inputs": {
      "images": [
        "8",
        0
      ]
    },
    "class_type": "PreviewImage",
    "_meta": {
      "title": "Preview Image"
    }
  }

 出力部分は、batch_size=2以上もあるため、ComfyUI側ファイル名の配列になっている。画像URLはSave ImageとPreview Imageで、

outputs.forEach(image => {
// Save Imageで自動保存した場合の画像URL
endpoint/view?filename=${image['filename']}`)

// Preview Imageの場合の画像URL
endpoint/view?filename=${image['filename']}&type=temp`)

 少しURLが変わるので要注意。これで実際の画像を取り出せれば、あとはHTML側に描画すれば良い。

 さて最後のuploadImage()は、上記で普通の生成は可能だが、ControlNetやimg2img、Inpaintなどを使うWorkflowの場合、参照画像をComfyUI側にアップロードする必要があり、そのテスト用だ。ここでアップロードすると、ComfyUIが管理するファイル名が戻ってくる(多くのケースは同じファイル名)。実態はComfyUI/inputに入る。

 たとえばLoad Imageを使う時、ローカルのファイル名ではなく、このファイル名をセットする。以降は普通にControlNetでもimg2imgでもInpaintでも好きなWorkflowを組めば良い。

Load Imageを使ってControlNet / Depthで深度画像を得るWorkflow

 Mainの部分は、CheckpointとLoRA一覧の取得、select2の初期化、そして[画像生成]と[ファイルを選択]のイベントリスナー。それぞれの機能実行となる。

 全部で560行ほどあるが、その半分はWorkflow定義。そしてその半分はHTML系と、JavaScript自体は大した量ではない。

 それより今年(2024年)はPythonばかり触っていて、HTML/JavaScriptをすっかり忘れており、そっちの方が大変だった(笑)。コードは公開したものの、こんな状態でかけた時間は数時間。妙なところは多めに見て頂きたい。


 以上、ComfyUIのAPIをHTML/JavaScriptだけで使う方法をご紹介した。それほど難しくないので、PHPやnode.jsなどほかへの移植も簡単だろう。「HTML/JavaScriptなら行ける!」という方は、ぜひ素敵なWebUIに改造して使ってほしい。