概要
Stable diffusion Automatic 1111 とは、画像生成モデルの Stable Diffusion をローカルで動かす web GUI を提供するツール・ライブラリのことです。
ComfyUI など、Stable diffusion の GUI ツールは他にもありますが、私は automatic 1111 webui を愛用しており、SD 1.5 時代から、今に至るまでローカル画像生成を遊んでいます。
そんな Automatic 1111 webui (以下 webui) では、ユーザーがかなり自由度高く拡張できる、拡張機能の仕組みを提供しています。有名なものでいえば、controlnet や tagcomplete などでしょうか。そんな拡張機能の仕組みに少し興味を持ったので、本記事では拡張機能の開発の流れを紹介します。習作として、プロンプトをノードエディタで組み立てる拡張機能を作り、その中で拡張機能のエントリーポイントや、よく見る機能の実現方法について検討します。
前提
- Stable diffusion automatic 1111 webui 自体の導入は済んでいる
- Python / JavaScript の解説はしません
今回作るもの
タブを新規に追加して、https://github.com/jagenjo/litegraph.js のノードエディタを表示して、プロンプトを出力します。
Stable Difusion automatic 1111 拡張機能開発
エントリーポイントについて
python と javascript を動的に読み込む手段を提供しています。なお、筆者も詳しい仕組みは完全理解できていませんので、誤った説明があれば教えてください。
- UIなどで拡張機能をインストールをすると、extensions フォルダーの中に clone されます
- セットアップスクリプトなどが動いて、必要な依存モジュールなどをインストールしています
- webui リロード / 起動時に、拡張機能のスクリプトを動的に読み込みます
- ここの裏取りができてないですが、挙動的に多分そう
- 拡張機能のスクリプトは、webui 起動の各種タイミングで呼ばれるコールバックを追加しています
- UI が gradio の仕組みに乗って構成され、ブラウザで表示されます
- javascript も拡張機能毎にあれば読み込まれます
- タイミングは理解できていません
コールバックの種類は多岐に渡るようです。下記の関数定義辺りがコールバックの種類になっていて、 script_callbacks.on_app_started(callback)の形で呼び出します。
コールバックなしで、modules.scripts.Script を継承した class を定義をすることで読み込ませる流儀もあるみたいですが、あまりわかっていないので、ここではスコープ外とします。なんかこの辺で読み込んでいる気配:
ということで、extensions/ 配下に自分の拡張機能の名前で新規フォルダを作り、その中に scripts (pythonコード) と javascript (JSコード) のディレクトリを切れば、開発準備完了です。
今回作るものの概要
画像生成のプロンプトは、多くの場合次のように分類されるタグの結合で構成されるので、それをグラフィカルに扱えたら楽しいかな?と思いついたのでそれを形にします。
元ネタは sdweb-easy-prompt-selector です。
Python 側
次の二つの処理のみです:
- UI 定義
- API 定義
API は何の変哲もない fastapi の定義なので解説を省略。
UI 定義では gradio で script_callbacks.on_ui_tabs つまりタブを追加して新しいページを作る処理をしています。
今回、生成したプロンプトを txt2img / img2img タブにそのまま送りたかったので、webui 本体が提供している「送る」ボタンの仕組みをそのまま借用しています。これが modules.infotext_utils(コード中では parameters_copypaste という別名でimportしています)です。中身を見ると、以下のような処理になっています。
buttons = parameters_copypaste.create_buttons(["txt2img", "img2img"])
parameters_copypaste.bind_buttons(buttons, None, prompt_output)
create_buttons は渡したタブ名それぞれに対して Send to txt2img / Send to img2img という gr.Button を作るだけの単純な関数です。
bind_buttons(buttons, send_image, send_generate_info) の方は少し込み入っていて、内部では register_paste_params_button に ParamBinding(どのボタンが押されたらどのコンポーネントの値をどのタブに転送するか、を表す設定オブジェクト)を登録しているだけです。実際にボタンとタブ側のコンポーネントを繋ぐ処理(connect_paste_params_buttons)は webui 起動時に別途まとめて走るので、拡張機能側は「登録するだけ」でよい、という設計になっています。
今回は画像は送らないので第2引数 send_image は None、生成したプロンプトを積んだテキストボックス(prompt_output)を send_generate_info に渡すことで、ボタンを押すと prompt_output の中身が txt2img / img2img の該当欄にコピーされるようになります。
なお bind_buttons はコメントに "old function for backwards compatibility; do not use this, use register_paste_params_button" とある通り後方互換のための関数のようで、本来は register_paste_params_button を直接使うのが推奨のようです。今回は手短に済ませたかったのと、他の拡張機能でもよく使われている書き方だったのでそのまま bind_buttons を使っています。
もう一つ拝借しているのが modules.ui_common の create_refresh_button です。タグ定義ファイルを選ぶ Dropdown(file_selector)の横に、よくある🔄アイコンのリフレッシュボタンを置きたかったので使いました。
create_refresh_button(
file_selector,
self.load_tag_files,
lambda: {"choices": self.files},
"pne-file-selector-refresh",
)
引数は「更新対象のコンポーネント」「クリック時に呼ぶ更新処理(ここでは self.files を再読込する load_tag_files)」「更新後に対象コンポーネントへ反映する gradio の update 用引数を返す callable」「ボタンの elem_id」の4つです。内部の実装はシンプルで、ボタンがクリックされたら refresh_method() を呼んでから refreshed_args() の中身(今回なら {"choices": self.files})を gr.update(**args) として返す、というだけです。この統一されたインターフェースのおかげで、model や VAE の選択欄などあちこちにある「選択肢を再読込するリフレッシュボタン」と全く同じ見た目・挙動のものを自分の拡張機能にも簡単に生やせます。
このように、少し hack ですが modules モジュールを利用すると、他の部分で利用されている UI をパクることができます。(まぁ突然壊れるリスクは上がりますが...)
また、フロントエンドで利用したい canvas 要素を予め定義しています。ついでに css も読み込みます。/file=extensions/sd-webui-prompt-node-editor/* のようにすることで、自分の拡張機能側のディレクトリにアクセスできます。
javascript 側
onUiLoaded にコールバックを渡すことで、他の UI の処理が終わった後に拡張機能の読み込みをしてくれます。
また、パーツは基本的には python 側で定義して、フロントエンド独自の処理を id 経由で渡す形で実装してみました。
分けるとなると、フロントエンド側で動的に要素を生成する必要がありますが、見た目を gradio に寄せたいのでこの方式にしています。
後は今回独自のノードを litegraph.js の流儀で実装します。
拡張機能独自なといか、hackなのですが、 gradio で実装されたコンポーネントにイベントを伝播させるために、dispatchEvent などが使われていることでしょうか。
まとめ
今更 Stable diffusion automatic 1111 webui の拡張機能を開発してみました。
地味にネット世界に解説がなさそうだったので、今回結構雑ですが投稿しておきました。ちなみに内部のコンポーネント周りを利用する実装は、Claude 君にほとんどやってもらました。本当に賢いね。
本記事を読んで、I/F だけ書けば後は Claude 君が全部あなたがやりたいことを実現してくれるでしょう。
余談:他の web UI に乗り換えてもいいんですが、推しが結構マイナーで LoRA 等は非常に少なく、安定して生成できる現状から、最新モデルに対応したものに乗り換えるモチベがないんですよねw
