Compare commits

...
5 Commits
Author SHA1 Message Date
ketle-manandClaude Sonnet 5 78d9bba904 release: v0.3.76 — Comic Creator I2I execution bridge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 13:29:37 +09:00
ketle-manandClaude Sonnet 5 f8435ed770 release: v0.3.75 — Lemonade Server backend support, Bypass/Mute node fix
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 10:04:09 +09:00
ketle-manandClaude Sonnet 5 b2ef333dfe release: v0.3.74 — Chat image attachment (TOOLS drop zone) + I2I generation
Shares the TOOLS pane's existing image drop zone as the Chat pane's
attachment mechanism: images are sent to the LLM as vision input and,
when generate_image is called, used as the base for image-to-image
generation via a new Chat I2I Generation dedicated workflow setting.
Also adjusts chat bubble image sizing and documents verified models
(Gemma 4:e4b, Qwen 3.5:9b, Ministral 3:3b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:25:53 +09:00
ketle-manandClaude Sonnet 5 dfb33e831d release: v0.3.73 — Chat image generation via Tool Calling, Default Checkpoint safeguard
Adds LLM Tool Calling-driven image generation to the AI TOOL Chat pane
(generate_image tool, GenerateUI pipeline reuse, dedicated workflow option,
allow-toggle), plus a Settings > Default Checkpoint safeguard and a fix for
convertUiToApi() silently substituting missing checkpoints without warning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 13:03:06 +09:00
ketle-man 64c21b1224 release: v0.3.72 — Comic Creator Inpaint integration, cross-realm Blob fix 2026-07-24 10:54:17 +09:00
17 changed files with 1171 additions and 132 deletions
+170
View File
@@ -2,6 +2,176 @@
---
## v0.3.76
### 追加機能: Comic Creator向けI2I実行ブリッジ`_wfmReceiveI2IRunRequest`
Comic Creator(`comfyui-comic-creator`、本アプリをiframe埋め込みで連携する別アプリ)から、「マスク不要のI2I実行をその場で行い結果を受け取りたい」という依頼を受けて追加。既存のInpaint連携ブリッジ(`gallery-tab.js`の`_wfmReceiveInpaintRequest` → `image-edit-tab.js`の`runInpaintExternal`/`_runInpaintWithImages`)と対称的な、マスク版の無いI2I専用ルートを新設した。
- `image-edit-tab.js`: `_runI2IWithImage(imageBlob, {positive, negative, denoise, overrideOpts, onStatus})`を新規実装。`comfyEditor.applyImageToSlot(file, 0)`(マスクなし版アップロード)→`comfyEditor.setPromptText("positive"/"negative", ...)`→`comfyEditor.setInpaintParams({denoise, ...overrideOpts})`→`window._wfmGenerateTab.generate()`→`#wfm-gen-result-img`のsrcを返す、という`_runInpaintWithImages()`と同型の流れ。`setInpaintParams()`は`growMaskBy`未指定時に自動でスキップする既存実装だったため、denoise単体用の新規セッターは追加せずそのまま流用できた。外部エントリポイント`runI2IExternal(imageBlob, {positive, negative, denoise})`(多重実行ガード`_i2iExternalRunning`付き)を追加。
- `gallery-tab.js`: `window._wfmReceiveI2IRunRequest = async (imageBlob, params, workflowData, workflowFilename) => {...}`を追加。`_wfmReceiveInpaintRequest`と同じく、`workflowData`が渡されれば実行前に`loadWorkflowIntoEditor()`でロードしてから`window._wfmImageEditTab.runI2IExternal(imageBlob, params)`を呼び、結果`{ok, url}`をそのまま返す。既存の`_wfmReceiveImageForI2I`(画像をI2Iスロットへセットするだけ、Comic Creatorのレイアウトタブが使用)は無改造。
**検証**: 変更した2ファイルを`node --check`で構文チェック。Comic Creator側(Imageタブの新規「Select I2I」パネル)からKapture経由で実際に呼び出し、Positive/Negative Prompt・Denoiseがワークフローへ反映された上でキュー実行され、結果画像URLが返ることを確認(Comic Creator側DEVLOG参照)。
**How to apply**: 「画像をComfyUIのワークフローに自動セットしてキュー実行し、結果を返す」という外部連携パターンを新設する際は、`comfyEditor.applyImageToSlot`/`setPromptText`/`setInpaintParams`/`window._wfmGenerateTab.generate`が既にマスクなし用途にも流用できる設計になっている(オプション引数は未指定なら書き込みをスキップする)。Inpaint版と重複する新規プリミティブをWorkflow Studio側に増やす必要は薄く、`_runInpaintWithImages`と同型の薄いラッパーを追加するだけで足りる。
## v0.3.75
### Lemonadeバックエンド追加(3つ目のAIバックエンド)
「LLMのバックエンドにlemonadeも追加したい」という依頼を受け、lemonade-server.ai のAPIドキュメント(OpenAI互換API・Ollama互換API等)を確認した上で実装。既存のバックエンド切替は`ai-tab.js`(SPA AI TOOLタブ)・`prompt-tab.js`/`settings-tab.js`(Promptタブ設定)・`node_sets_menu.js`(サイドパネル)の3箇所に独立実装されており、いずれも`if (backend === "ollama") {...} else {...}`という2値決め打ちで、elseがLM Studio(OpenAI互換API: `/v1/models`, `/v1/chat/completions`)を指す構造だった。Lemonadeのデフォルト(OpenAI互換モード)は`http://localhost:13305`でLM Studioと全く同じエンドポイント形状のため、fetch系関数(`fetchModels`/`callLLM`/`callChat`/`callVLM`等)は変更不要で、ラジオボタンの選択肢追加とデフォルトURL解決部分の3値対応のみで済んだ。
**対象範囲・接続方式は事前にユーザーに確認:** 3箇所すべてに追加するか/SPA AI TOOLタブのみ先行導入するか、接続方式はOpenAI互換モード既定かOllama互換モードも選択肢に含めるかを質問し、「3箇所すべて」「OpenAI互換モード既定」で合意して実装した。
**デフォルトURL解決の共通化:**
- `static/js/util.js`に`AI_BACKEND_DEFAULT_URLS`(ollama/lmstudio/lemonade)と`getAiBackendDefaultUrl(backend)`を新設し、`ai-tab.js`・`prompt-tab.js`・`settings-tab.js`(いずれもESモジュールで`util.js`をimport済み)の計8箇所の三項演算子を置き換え
- `web/comfyui/node_sets_menu.js`(ComfyUI拡張側、`static/js`とは別スコープのESモジュールでimport不可)は同名のヘルパーをローカルに複製(既存の重複実装パターンを踏襲)
**UI・ヘルプ更新:**
- ラジオボタン`value="lemonade"`を`templates/index.html`(SPA)・`static/js/settings-tab.js`(Promptタブ設定)・`web/comfyui/node_sets_menu.js`(サイドパネル)の3箇所に追加
- ヘルプ文言3点セット(`index.html`/`i18n.js`のEN・JA・ZH/`app.js`の`helpIdMap`)のうち、`helpIdMap`は既存キーへの追記のみで変更不要。「Ollama or/または/或 LM Studio」を列挙している9キー(`helpPrompt1`, `helpSettings5`, `helpSidepanel16`, `helpAi2`, `helpAi5`, `helpAi6`, `helpTrouble2` の各ロケール)すべてにLemonadeを追記
- README.mdの現行機能セクション(Changelogではなくpresent-tense説明部分)とRequirementsセクションにLemonade関連の記載を追加。過去バージョンのChangelogエントリ(v0.3.40〜v0.3.74)は当時の実装を正しく記述しているため変更していない
**検証:** 変更した5つのJSファイル(`util.js`, `ai-tab.js`, `prompt-tab.js`, `settings-tab.js`, `node_sets_menu.js`)を`node --check`で構文チェック(全て成功)。実行中のComfyUIインスタンス(Kapture経由)で3箇所すべてを確認 — SPA AI TOOLタブSettings・SPA 設定タブ(Prompt tab AI Assistant)・サイドパネルAIタブのBackendラジオがいずれも`["ollama","lmstudio","lemonade"]`の3値で表示されることを確認、`getAiBackendDefaultUrl()`をブラウザ上で直接呼び出しollama/lmstudio/lemonade/未知の値それぞれで正しいデフォルトURL(`lemonade`→`http://localhost:13305`)を返すことを確認した。
### 追加修正: バックエンド切替時にURLを常にデフォルト値へ切り替え
上記の初回実装では、URL欄は「未保存の場合のみ」デフォルト値を補完する仕様(ラジオ切替時に`if (!saved.backendUrl) urlInput.value = defaultUrl`)だったため、一度どれかのURLを保存済みのユーザーがバックエンドを切り替えても欄が変わらなかった。「LMStudio、Lemonadeに切り替えた際それぞれデフォルトのURLに変更したい」というフィードバックを受け、3箇所すべてで無条件にデフォルトURLへ切り替える仕様に変更:
- `ai-tab.js`: `change`ハンドラの`if (!saved.backendUrl)`ガードを削除し、常に`urlInput.value = getAiBackendDefaultUrl(r.value)`
- `settings-tab.js`: 従来`change`時に`placeholder`のみ更新していた(欄に既存の値があると効果が見えない実質バグ)のを、`value`を直接書き換えるよう修正
- `node_sets_menu.js`: `change`リスナー自体が存在しなかった(バックエンド切替時にURLを更新する仕組みがそもそも未実装)ため新規追加
いずれもDOM上の表示値を書き換えるのみで、実際の永続化は各画面の「Save」ボタン押下時のまま変更なし。Kaptureで3箇所(SPA AI TOOLタブ・SPA 設定タブ・サイドパネル)すべて、ollama⇄lmstudio⇄lemonadeの切替でURL欄がそれぞれ`http://localhost:11434`/`http://localhost:1234`/`http://localhost:13305`に変わることを確認。テスト中に実際の保存設定(`wfm_ai_settings`)を上書きしないよう、Saveボタンは押さずページリロードで検証用の一時入力を破棄した。
### 検討事項: Lemonadeの画像生成・音声機能への対応可否(質問への回答のみ、実装保留)
ユーザーから「lemonadeでは画像生成や音声出力、テキスト読み上げなどに対応しているが、これに対応させることは可能か」と質問(実装依頼ではなく質問のみと明言)された。Lemonadeは`/v1/images/generations`(画像生成)・`/v1/audio/speech`(読み上げ)・`/v1/audio/transcriptions`(音声認識)をOpenAI互換の独立エンドポイントとして提供しており技術的には対応可能だが、以下の理由で「既存バックエンド切替の自然な延長」ではなく別枠の機能追加になる旨を回答した:
- **画像生成**: 本プラグインの画像生成はChatのTool Calling(`generate_image`)からComfyUIワークフロー自体を実行する設計であり、Lemonade側の画像生成APIを追加すると生成経路が2系統になり紛らわしい
- **音声読み上げ/文字起こし**: AI TOOLタブに該当UI(再生プレイヤー、マイク入力等)が現状一切なく、既存キーの拡張ではなく新規機能追加になる
ユーザーは「検討したいと思います」として今回は実装せず保留。README.mdの「Backend support」直後に「Lemonade's other endpoints (not integrated)」として上記を明記し、将来検討時の起点として残した。
### バグ修正: Bypass(バイパス)ノードを含むワークフローの生成失敗
ユーザーがミュートしているノードを含むワークフロー(`chat_i2i_anima.json`、ComfyUI保存済み)をGenerateUIタブで読み込んで生成したところ、以下のエラーで失敗した:
```
[ERROR] Failed to validate prompt for output 46:
[ERROR] * CLIPTextEncode 73:
[ERROR] - Exception when validating inner node: '74'
[ERROR] * KSampler 66:
[ERROR] - Exception when validating inner node: '74'
```
**原因調査:** 実際のワークフローjsonを確認したところ、ノード74(LoraLoader)は`mode: 4`(ComfyUI独自のBypassモード。litegraph標準の`mode: 2`=Never/Muteとは別物)だった。`static/js/comfyui-workflow.js`の`convertUiToApi()`は`if (node.mode === 4) continue;`でBypassノードをAPI出力から単純に除外するだけで、そのノードを経由していた配線(CLIPLoader→LoraLoader.clip→CLIPTextEncode(73).clip、UNETLoader→LoraLoader.model→KSampler(66).model)を繋ぎ直しておらず、73番・66番が存在しないノード74を参照したまま出力されてバリデーションエラーになっていた。ComfyUI本体はBypassノードを「入力と出力を同じ型のスロット同士でパススルー接続してから除去」する仕様のため、この動作の再現が必要と判断。
**実装:**
- `_resolveBypassSource(nodeById, linkMap, nodeId, slot, visited)`(新規ヘルパー)を追加。リンクの参照先がBypassノードの場合、その出力スロットと同じ型の入力スロットを探し(同インデックス優先、なければ型一致する最初の入力)、その入力の配線元を再帰的に辿る(連鎖バイパスにも対応、循環ガード付き)。マッチする入力が無ければ`null`を返しリンク無し扱いにする
- `convertUiToApi()`内の全リンク解決箇所をこの関数経由に変更。バイパスされていないノードが参照先の場合は即座にそのまま返るため、既存の非バイパス経路への影響はない
- ついでに`mode: 2`(真のMute)も除外対象に追加(従来は一切スキップされておらず、ミュートしても実行時は普通に動いていた)。ただしMuteはパススルーせず単純除外 — ComfyUI本体でも「必須の下流依存があるノードをMuteすると壊れる」のが仕様通りの挙動のため、あえて救済しない
**検証:** `chat_i2i_anima.json`を実際のComfyUIサーバー(`/object_info`)に接続したNodeスクリプトで`convertUiToApi()`に直接通し、KSampler(66).model→`["68",0]`(UNETLoader直結)、CLIPTextEncode(73).clip→`["61",0]`(CLIPLoader直結)、ノード74自体は出力に存在しない、ことを確認。さらに変換結果を実際に`/prompt`へPOSTし`node_errors: {}`(バリデーション成功)を確認。キューに入った時点で`/interrupt`により即座に中断し、無駄なGPU生成は行っていない。
### 追加機能: GenerateUIのRaw JSON上部にBypass/Muteノード表示
ユーザーから「生成UIタブのInput、Model、SettingsのRAW JSONペインの上部にバイパスノードの存在を示す表示を行いたい」と依頼。API形式のJSONには`mode`情報が残らないため、Raw JSON表示だけではBypass/Muteの有無が分からなくなる点への対応。
- `comfyui-workflow.js`に`_lastBypassedNodes`/`_lastMutedNodes`(モジュールスコープ、`_lastCheckpointSubstitutions`と同じパターン)を追加し、`convertUiToApi()`実行中にBypass/Muteで除外したノードの`{nodeId, title}`を記録。`getLastBypassedNodes()`/`getLastMutedNodes()`で呼び出し元から取得可能にした
- `templates/index.html`の共有Raw JSONウィジェット(`#wfm-gen-rawjson-widget`、Input/Model/Settingsタブ間をJSで移動する仕組み — 既存のsearch barと同じ場所)に`#wfm-gen-rawjson-bypass-note`を追加。3タブ共通のヘッダーに置くことで「Input/Model/Settingsそれぞれの上部」という要望を1箇所の実装で満たせる
- `generate-tab.js`の`loadWorkflowIntoEditor()`で、UI形式からの変換時(`format === "ui"`)のみ上記2つのgetterを読み、該当ノードがあれば`t("rawJsonBypassNote", list)`/`t("rawJsonMutedNote", list)`(新規i18nキー、EN/JA/ZH追加)を`escapeHtml`経由でnote要素に表示、無ければ`display:none`
- CSS: `--wfm-warning`色のアンバー背景バーとして`main.css`に追加
- ヘルプ: `helpGen7`(index.html + i18n.js全ロケール、既存キーへの追記のため`app.js`のhelpIdMapは変更不要)にBypass/Mute注記の説明を追加
**検証**(Kapture、実機ブラウザ): Workflowタブから`chat_i2i_anima`を検索→「生成UIに読み込み」ボタンで読み込み、Input/Model/Settingsいずれのサブタブでも`#wfm-gen-rawjson-bypass-note`に「バイパス中(除外・配線は再接続済み): LoraLoader (74)」が表示されることを確認。Bypass/Muteノードを含まない別ワークフロー(`04031-lora.json`)を読み込んだ場合は`display: none`で非表示になることも確認。
## v0.3.74
### AI TOOL Chatペインの画像添付対応 + I2I連携
v0.3.73でChatペインのT2I画像生成(Tool Calling)を実装した直後の依頼。「TOOLSのファイルドロップエリアを使ってChatに画像添付対応させたい、I2Iワークフローとの連携(画像の差し替え)も行いたい」という要望を受けて着手。実装前に設計上の分岐点(添付画像をLLMにも見せるか/I2I用ワークフローをT2Iと共用するか/添付の持続性)をユーザーに確認し、それぞれ「LLMにも見せる(マルチモーダル)」「T2I専用とは別にI2I専用ワークフローを新設」「明示的にクリアするまで保持」で合意して実装した。
**対象ファイル:** `static/js/ai-tab.js`, `static/js/comfyui-editor.js`, `static/css/main.css`, `static/js/i18n.js`, `static/js/app.js`, `templates/index.html`
**TOOLS↔Chat 共有の添付画像:**
- 新しいドロップゾーンを作らず、TOOLSペインの既存ドロップエリア(`wfm-ai-vlm-drop`、VLMタスク用)をChatの添付機構としても共用する設計。モジュールレベルの共有state `_toolsImage = { file, base64, mimeType }` を導入し、`initVlmTab()`のローカル変数`vlmImage`を置き換えた
- `_syncAttachmentUI()`(新規): TOOLS側プレビューの表示/非表示とChat側の添付インジケーター(サムネイル+ラベル+✕)を1箇所で同期。`document.getElementById`による都度参照のため、`initVlmTab`/`initChatTab`どちらが先に呼ばれても安全
- クリアは共通の`_clearToolsImage()`。TOOLSプレビュー上の✕ボタン、Chat添付インジケーターの✕ボタンのどちらからでも同じ状態を更新する
- 添付は合意事項通り、送信後も自動クリアされず明示的にクリアするまで保持される
**マルチモーダルチャット送信:**
- `chatHistory`のエントリを内部共通形式 `{role, content, images?: [{base64, mimeType}]}` に拡張
- `callChat()`に`_formatMessagesForBackend(messages, backend)`を追加し、送信直前にバックエンド別のワイヤーフォーマットへ変換(Ollama: `images:[base64...]`、LM Studio: `content`をテキスト+`image_url`のコンテンツブロック配列に変換)。`callVLM()`で既に使われている規約を踏襲
- `generate_image`ツールの説明文を更新し、画像添付時はT2IでなくI2Iとして動作することをLLMに伝えるようにした(引数スキーマ自体は`prompt`/`negative_prompt`のまま変更なし)
**I2I生成ブリッジ:**
- `comfyui-editor.js`の`applyImageToSlot(file, slotIndex, opts)`に`opts.workflow`/`opts.analysis`対応を追加(`applyImageAndMaskToSlot`と同一パターン)。渡された場合はDOM更新を行わずアップロード後のファイル名のみ返す非破壊的変更で、既存呼び出し元(GenerateUI Imageタブ)への影響なし
- `generateImageFromChat(prompt, negativePrompt)`を拡張し、`_toolsImage`がセットされていればI2I分岐へ:対象ワークフロー(GenerateUIの読み込み中ワークフロー、またはI2I専用ワークフロー)に`load_image_nodes`が無ければ明確なエラーを投げ、あればslot 0のLoadImageノードへ添付画像を差し替えてから通常のプロンプト設定・生成へ進む
**SETTINGSペイン — Chat I2I Generation:**
- 既存の「Chat Image Generation」(T2I専用ワークフロー)セクションと全く同じ構造で「Chat I2I Generation」セクションを新設。`chatGenI2IDedicatedEnabled`/`chatGenI2IDedicatedFilename`として`wfm_ai_settings`に独立して永続化。ワークフロー一覧の取得(`/api/wfm/workflows`)はT2I側のfetchを流用し2つのselectへ同時反映
**表示調整(ユーザーフィードバックにより追加修正):**
- Chatバブル内の添付画像エコー(送信したユーザーメッセージに表示される添付プレビュー)が大きすぎたため、`appendChatBubble`に`opts.attachThumb`(新設、max 120px)を追加し、生成結果画像用の`opts.imageUrl`(`.wfm-ai-chat-img`)とスタイルを分離
- 生成結果画像自体も表示が大きすぎるとのフィードバックを受け、`.wfm-ai-chat-img`の`max-width`を`100%`→`20%`に変更(約80%サイズダウン)
**推奨モデル:** 動作確認の結果、Tool CallingとVision機能の両方が良好に動作したモデルとして Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)を確認。ヘルプ(`helpAi3`)とREADMEに記載
**ヘルプ更新(3点セット):** `templates/index.html`/`static/js/i18n.js`(EN/JA/ZH)/`static/js/app.js`の`helpIdMap` — `helpAi3`(添付画像・I2I・推奨モデルの追記)、`helpAi4`(TOOLSドロップ画像がChatとも共有される旨)、`helpAi6`(Chat I2I Generation設定セクションの追記)。いずれも既存キーへの追記のため`helpIdMap`自体の変更は不要
**検証**(Kapture、実機ブラウザ、Ollama): TOOLSへの画像ドロップ→Chat添付インジケーター表示、Chatでの画像編集依頼→I2I生成の実行、TOOLS/Chat双方の✕による添付クリア、Chatバブルの画像サイズ調整(添付エコー120px、生成結果`max-width:20%`)を確認。ブラウザ接続(Kapture)が一時的に切断される場面があり、再接続後に検証を継続した。
## v0.3.73
### AI TOOL Chatペインからの対話式画像生成(Tool Calling)+ Checkpoint関連の安全策
ユーザーからの「OpenWebUIのような対話式画像生成をこのプラグインで実現できないか」という相談から着手。既存のOllama/LM Studioチャット機能(`static/js/ai-tab.js`)とGenerateUIタブの生成機能(`static/js/generate-tab.js`)は別々に存在していたが、両者をLLM Tool Calling経由で接続した。実装・検証の過程で、GenerateUIのModelタブに以前から存在した「保存済みワークフローのCheckpointが実在しない場合、無警告で別モデルに差し替わる」という表示バグも発見・修正した。
**対象ファイル:** `static/js/ai-tab.js`, `static/js/generate-tab.js`, `static/js/settings-tab.js`, `static/js/comfyui-editor.js`, `static/js/comfyui-workflow.js`, `static/css/main.css`, `static/js/i18n.js`, `static/js/app.js`, `templates/index.html`
**Chatペイン画像生成(Tool Calling):**
- `generate_image`ツール(`prompt`/`negative_prompt`引数)をOllama/LM Studio双方のchat APIに渡し、呼び出すかどうかはLLM自身が判断する設計(合意事項: 画像生成後、結果はLLMに送り返さず会話は一旦区切る/SPA版のみ対応、サイドパネル版は対象外)
- `callChat()`の戻り値を`string`から`{content, toolCalls}`へ変更。Ollamaの`tool_calls[].function.arguments`はオブジェクト、LM Studio(OpenAI互換)はJSON文字列という差異は`_parseToolArgs()`で吸収
- 生成処理は既存の`window._wfmGenerateTab.generate(workflowOverride)`(Image Edit「Inpaint」の外部呼び出しと同じ橋渡しパターン)をそのまま再利用。ワークフローはディープクローンしてから`comfyEditor.setPromptText()`で書き込むため、GenerateUIタブの表示状態は一切汚染しない
- **既存コードの欠落を修正:** `_coreGenerate`/`handleGenerate`(`generate-tab.js`)が生成結果`{images, seed}`を計算していながら`return`していなかったため、戻り値を追加(既存呼び出し元は戻り値を使っていないため影響なし)
- Chatペイン下部(Send/Clearボタンの隣)に「画像生成を許可する」チェックボックス(デフォルトON、`wfm_ai_settings`に永続化)。OFF時は`callChat()`に`tools`を渡さずLLMには一切提示しない
**SETTINGSペイン — Chat画像生成の専用ワークフロー:**
- Image Edit「Inpaint」の「Use dedicated workflow」と同一パターンを踏襲。OFF(デフォルト)はGenerateUIの読み込み中ワークフロー、ONは選択した保存済みワークフローを`/api/wfm/workflows/raw`経由でその都度ロード・変換(`comfyWorkflow.detectFormat`/`convertUiToApi`/`analyzeWorkflow`)して使用
**バグ修正: Checkpoint未一致時の無警告差し替え(`comfyui-workflow.js`):**
- `convertUiToApi()`のCOMBO値フォールバック処理(Impact Packのワイルドカード等、動的な選択肢がインスタンス間でズレて生成エラーになるのを防ぐため既存)が、`ckpt_name`にも無差別に適用されており、保存されたCheckpointパスが現在の環境に存在しない場合、無警告でCombo選択肢の先頭(たまたまアルファベット順で最初のFluxモデル)に差し替えられていた。この製品の「なぜかいつもFluxが表示される」という混乱の直接的な原因だった
- `getLastCheckpointSubstitutions()`を追加し、`ckpt_name`の差し替えが発生した場合のみ元の値・ノードIDを記録。`loadWorkflowIntoEditor`(`generate-tab.js`)で検知し、通常の「読み込み完了」トーストの代わりに、差し替え前のモデル名を含む警告トースト(`error`スタイル)を表示する
- GenerateUI Modelタブ(`comfyui-editor.js`の`renderModelTab`)にも、保存値が現在のモデル一覧に存在しない場合に警告付きの選択肢を先頭表示する対応を追加(API形式で保存され`convertUiToApi`を経由しないワークフローに対して有効。UI形式ワークフローは上記の差し替えにより「一致している」ように見えるため、この表示だけでは検知できないケースがある)
**設定タブ — デフォルトCheckpoint:**
- チェックボックスで有効化すると、GenerateUIへのワークフロー読み込み時に全Checkpointローダー系ノード(`CheckpointLoaderSimple`/`CheckpointLoader`/`ImageMetadataPromptLoader`)の`ckpt_name`を指定モデルへ強制上書き。サーバー側`/api/wfm/settings`に永続化(複数ブラウザ間で共有)
- **実装中に判明した順序依存の不具合:** 上書き処理を`analyzeWorkflow()`実行後に行っていたため、Raw JSON上のデータは正しく上書きされてもModelタブの表示(`analysis.checkpoint_nodes`由来)が古い値のまま残る不具合があった。`analyzeWorkflow()`実行前に上書きする順序に修正
**ヘルプ更新(3点セット):** `templates/index.html`/`static/js/i18n.js`(EN/JA/ZH)/`static/js/app.js`の`helpIdMap` — `helpAi3`(Chat画像生成機能+許可トグルの説明を追記)、`helpAi6`(SETTINGSペインの専用ワークフロー設定を追記)、`helpTrouble8`(新規: 「ModelタブのCheckpointが想定と違う」症状の原因と対処法)
**検証**(Kapture、実機ブラウザ + Ollama qwen3.5:9b):
- Chatペインから「〜の画像を生成して」で`generate_image`が呼ばれ、実際に画像が生成されチャット内に表示されることを確認(初回は無関係のワークフロー設定に起因するVAEチャンネル数不一致エラーが発生したが、ワークフロー側の修正後に正常動作を確認)
- SETTINGSペインの専用ワークフロー機能: 選択したワークフロー(UI形式、`ws_sdxl_t2i_basic.json`)が正しくロード・変換され、Chat経由の生成に使われることを確認
- 「画像生成を許可する」チェックボックスの位置変更(ヘッダー→下部の操作行)を反映
- デフォルトCheckpointのON/OFF切り替えでModelタブの表示が正しく変わることを確認(ON: 指定モデル、OFF: 元の壊れた表示に復帰)
- 実際に不一致のあるワークフロー(`op_sdxl_ill_lora.json`、保存値`anime\animozalyca_v10.safetensors`が未インストール)で、差し替え警告トーストが元のモデル名を含めて正しく表示されることを確認
## v0.3.72
### Comic Creator連携用の外部Inpaintエントリポイントを追加
Comic Creator(`comfyui-comic-creator`)のImageタブから、既存のI2I連携(`window._wfmReceiveImageForI2I`、iframe越しの直接関数呼び出し)と同じ方式でInpaintも呼び出せるようにする依頼を受けて対応。
**対象ファイル:** `static/js/image-edit-tab.js`, `static/js/gallery-tab.js`, `static/js/comfyui-editor.js`
- `image-edit-tab.js`: `_runInpaint()`(UI駆動)から共通処理(LoadImage/MaskEditorOneノードへの反映〜Generate実行〜結果URL取得)を`_runInpaintWithImages()`として切り出し、外部呼び出し専用の`runInpaintExternal(imageBlob, maskBlob, params)`を新設(専用ワークフロー選択は扱わず、常にGenerateUIの現在ロード中ワークフローが対象)。マスクレイヤー選択に`.visible`チェックを追加し、非表示のマスクレイヤーはUI駆動・外部呼び出し双方で対象から除外されるよう修正。
- `gallery-tab.js`: `window._wfmReceiveImageForI2I`と同じ場所に`window._wfmReceiveInpaintRequest(imageBlob, maskBlob, params, workflowData?, workflowFilename?)`を新設。`workflowData`が渡されればI2Iと同じ手順(接続確認→モデルリスト確保→`loadWorkflowIntoEditor`)でロードしてから`runInpaintExternal`を呼ぶ。
- **バグ修正(`comfyui-editor.js`):** `_loadImageElement()`内の`src instanceof Blob`判定が、別ウィンドウ(Comic Creator)で生成されたBlob/Fileに対しては別レルムのコンストラクタ参照になるため常に`false`を返し、`img.src`にBlobオブジェクトがそのまま代入されて`[object Blob]`という文字列がURLとして解釈され404になるバグを発見・修正(`typeof src !== "string"`による実体判定に変更)。Blob自体は`URL.createObjectURL`等でレルムをまたいで問題なく使えるため、チェック方法のみが原因だった。同一オリジンiframe越しに他ウィンドウ生成のBlob/Fileを直接受け渡す連携を実装する際は、受け取り側で`instanceof`ベースの判定を避けること。
**検証**(Kapture、Comic Creator実機 `http://127.0.0.1:8189/ccc`): Comic Creator側からInpaint実行→本ノードのInpaintが正常に動作し結果が返ることを確認。非表示マスクレイヤーの除外もComic Creator側UIから確認。
## v0.3.71
### Inpaint機能の追加(GenerateUI Imageタブ + Image Edit「Inpaint」タブ)
+51 -6
View File
@@ -21,7 +21,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.3.71-green)
![Version](https://img.shields.io/badge/version-0.3.76-green)
## Screenshots
@@ -89,6 +89,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- **Model tab** — Checkpoint, VAE, LoRA, ControlNet, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — single clip: type + device; dual clip: two clip selectors + type + device), **Hypernetwork** (with Strength field) selectors with filter; Raw JSON on the right
- **Settings tab** — KSampler and Latent Image side by side at 50% width each; Raw JSON on the right
- **Always-visible Raw JSON** — edit the API-format JSON directly from any tab with syntax highlighting; Apply button reloads the workflow; built-in **search bar** (always shown) finds all matches as you type with count display (`3/12`); navigate with ↑/↓ buttons or Enter / Shift+Enter; Escape or ✕ clears; current match highlighted in orange, other matches in yellow
- **Bypass/Mute node handling** (v0.3.75) — loading a UI-format workflow with Bypass-mode nodes now reroutes their wires to the upstream source (matching same-type input/output slots, recursively through chained bypasses) instead of leaving a dangling reference that failed validation; Mute-mode nodes are excluded without rerouting, same as ComfyUI itself; when either is present, a note listing the affected nodes appears above the Raw JSON search bar in the Input/Model/Settings tabs
- **One-click generation** — queue prompts to ComfyUI without leaving the studio
- **Seed control** — randomize, lock, or manually set seeds; seed input and mode selector stacked vertically for readability
- **Style selector** — checkbox and dropdown next to the Reset Workflow button; enable to apply a Fooocus-style JSON to positive and negative prompts at generation time; style files (`*.json`) are loaded from `user/default/Workflow-Studio/style/`; the style's `{prompt}` placeholder is replaced with the original prompt text, or the style is appended if no placeholder exists; `negative_prompt` is appended to the existing negative prompt; styles are applied to a per-generation copy and do not modify the loaded workflow
@@ -164,6 +165,7 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
- **CivitAI Host** — choose which site opens when clicking a model link: `civitai.com` (SFW only) or `civitai.red` (unrestricted); saved to `settings.json` and synced to `localStorage` for the Models tab to use without extra fetches
- **CivitAI API Key** — optional Bearer token for authenticated CivitAI access; stored in `settings.json` (excluded from data exports); environment variable `CIVITAI_API_KEY` takes priority if set
- **Default workflow** — set a workflow to auto-load on startup
- **Default Checkpoint** (v0.3.73) — optional checkbox to force every checkpoint loader node's model to a chosen default whenever a workflow is loaded into GenerateUI; a safeguard against generation errors or garbled images from a stale/mismatched checkpoint saved in the workflow file
- **Data Management** — export all plugin data (settings, metadata, prompts, etc.) to a single JSON file; import to restore data (useful when migrating or reinstalling); API keys are excluded from exports for security
- **Text Size** — one slider (10–28 px) adjusts font size for all prompt and chat textareas at once: Generate UI positive/negative prompts, AI Assistant chat input, Preset prompts, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings
- **RAW JSON Colors** — customize the 6 syntax highlight colors for the Raw JSON editor in Generate UI: Default Text (base), Name/Scheduler (yellow), Title (pink), Width/Height (green), Prompt/Text (cyan), Image/File (red); changes apply immediately on color pick; Reset Defaults restores the original scheme; saved to `localStorage` under `wfm_settings.jsonColors` and applied on startup
@@ -271,14 +273,20 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
### AI TOOL Tab (v0.3.14)
- **4-pane layout** — Translation | Chat | TOOLS | Settings; all panes always visible simultaneously; no sub-tab switching required
- **Translation pane** — translate text between Japanese, English, Chinese, or a custom Free language using Ollama or LM Studio; language selectors with ⇄ swap button (swaps both language selectors and text content); selections saved automatically
- **Chat pane** (v0.3.40) — multi-turn conversation with the LLM; full conversation history sent each turn for context; Enter to send, Shift+Enter for a newline; Clear button resets history; Ollama uses `/api/chat`, LM Studio uses `/v1/chat/completions`
- **Translation pane** — translate text between Japanese, English, Chinese, or a custom Free language using Ollama, LM Studio, or Lemonade; language selectors with ⇄ swap button (swaps both language selectors and text content); selections saved automatically
- **Chat pane** (v0.3.40) — multi-turn conversation with the LLM; full conversation history sent each turn for context; Enter to send, Shift+Enter for a newline; Clear button resets history; Ollama uses `/api/chat`, LM Studio and Lemonade use `/v1/chat/completions`
- **Chat pane — image generation via Tool Calling** (v0.3.73) — if the selected model supports tool calling and the user asks for an image, a `generate_image` tool is invoked and the request runs through the same generation pipeline as the GenerateUI Generate button, with the result shown inline in the chat; an "Allow image generation" checkbox in the input row toggles this on/off (default on); the workflow used is the one currently loaded in GenerateUI, or a dedicated saved workflow configured in the Settings pane; models verified to work well for tool calling and vision: **Gemma 4:e4b**, **Qwen 3.5:9b**, **Ministral 3:3b** (Ollama)
- **TOOLS pane (VLM)** — drop an image into the 110px drop zone, select a task (Describe Image / Create Prompt / Create Tags), and click Run to analyze with a vision model; result shown in the output area with a Copy button
- **TOOLS pane — shared Chat attachment** (v0.3.74) — an image dropped in the TOOLS drop zone is also usable as the Chat pane's attachment: it's sent to the LLM as vision input, and used as the base image when `generate_image` runs image-to-image; a ✕ button clears it (from either the TOOLS preview or the Chat attachment indicator); it stays attached across turns until explicitly cleared
- **TOOLS pane (Wildcards)** (v0.3.40) — select "Create wildcards" from the task dropdown; enter a category name and count; click Run to generate plain-text wildcard entries one per line (no markdown, no numbering); result can be copied directly into wildcard `.txt` files
- **Settings pane** — choose backend (Ollama / LM Studio), set the API URL, test connection, select a model (with refresh button), and configure Free language names for translation source and destination
- **Chat pane — image-to-image (I2I)** (v0.3.74) — when an image is attached and `generate_image` is invoked, the attachment is uploaded and swapped into the target workflow's LoadImage node instead of running text-to-image; the target workflow is the one currently loaded in GenerateUI, or a dedicated I2I workflow configured in the Settings pane
- **Settings pane** — choose backend (Ollama / LM Studio / Lemonade), set the API URL, test connection, select a model (with refresh button), and configure Free language names for translation source and destination
- **Settings pane — Chat Image Generation** (v0.3.73) — "Use dedicated workflow" checkbox lets Chat-triggered image generation use a saved workflow of your choice instead of the one currently loaded in GenerateUI, without disturbing what's shown there (same pattern as Image Edit's Inpaint dedicated workflow)
- **Settings pane — Chat I2I Generation** (v0.3.74) — same "Use dedicated workflow" pattern as above, but for image-to-image generation when an image is attached; independent of the text-to-image dedicated workflow setting
- **Settings shared** — settings saved to `localStorage` under `wfm_ai_settings`; shared with the Library panel's AI TOOL tab so configuration is consistent across both interfaces
- **Backend support** — Ollama (`/api/generate` for text, `/api/chat` for conversations, `/api/tags` for model list); LM Studio OpenAI-compatible API (`/v1/chat/completions`, `/v1/models`); VLM images sent as base64 (`images:[]` for Ollama, `image_url` content block for LM Studio)
- **Backend support** — Ollama (`/api/generate` for text, `/api/chat` for conversations, `/api/tags` for model list); LM Studio and Lemonade via the OpenAI-compatible API (`/v1/chat/completions`, `/v1/models`); VLM images sent as base64 (`images:[]` for Ollama, `image_url` content block for LM Studio/Lemonade)
- **URL security** — backend URL validated via `new URL()` to enforce `http://` or `https://` scheme
- **Lemonade's other endpoints (not integrated)** — Lemonade Server also exposes OpenAI-compatible image generation (`/v1/images/generations`), text-to-speech (`/v1/audio/speech`), and transcription (`/v1/audio/transcriptions`) endpoints; these are not wired up — image generation would duplicate the existing ComfyUI-workflow-based `generate_image` tool call, and audio/TTS has no corresponding UI yet, so both are future considerations rather than a natural extension of the current backend switch
### Workflow Studio Library (ComfyUI Side Panel) (v0.3.9)
@@ -290,7 +298,7 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
- **M — Models tab** — browse installed models (All / ★ Favorites / Groups / By Type sub-tabs); LoRA groups show an **All N LoRAs** item — drag to canvas to place a `Lora Loader (LoraManager)` node with all LoRAs pre-loaded
- **P — Prompts tab** — browse prompt presets with All / ★ Favorites / Categories sub-tabs; **Groups sub-tab** (row 2) — view presets by group (shared with the Batch tab's `wfm_prompt_preset_groups`)
- **I — Information tab** — drop a ComfyUI-generated PNG/WebP or workflow JSON in the side panel to view its metadata; detects LoRAs from `LoraLoader`, `LoraLoaderModelOnly`, and `Lora Loader (LoraManager)` nodes (API format supported); supports `UnetLoaderGGUF` and `QuadrupleCLIPLoader` node types; preview area fixed at 110px
- **A — AI TOOL tab** — Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama or LM Studio; Chat supports multi-turn conversations (full history sent each turn); TOOLS includes VLM image analysis and wildcard generation; settings (backend, URL, model) shared with the SPA AI TOOL tab via `localStorage`
- **A — AI TOOL tab** — Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama, LM Studio, or Lemonade; Chat supports multi-turn conversations (full history sent each turn); TOOLS includes VLM image analysis and wildcard generation; settings (backend, URL, model) shared with the SPA AI TOOL tab via `localStorage`
- **model sub-tab** — Checkpoint, VAE, Diffusion Model, and Text Encoder; drag items to canvas to place the corresponding loader node (Checkpoint → `CheckpointLoaderSimple`, VAE → `VAELoader`, Diffusion Model → `UNETLoader`, Text Encoder → `CLIPLoader`); double-click also places at canvas center
- **lora sub-tab** — detects LoRAs from `LoraLoader`, `LoraLoaderModelOnly`, and `Lora Loader (LoraManager)` nodes (API format `inputs.loras.__value__` supported); shows `strength_model / strength_clip` values; drag individual LoRA to place `LoraLoader`; **Multiple LORA** section (appears for 1+ LoRAs) drags all LoRAs into a single `Lora Loader (LoraManager)` node with LoRA syntax pre-filled
- **Prompts sub-tab** — POS / NEG badge list; drag a prompt to place `CLIPTextEncode` with text pre-filled; click any entry to view full text + Copy button
@@ -381,6 +389,7 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features
- **[LM Studio](https://lmstudio.ai/)** — alternative backend for translation and VLM (OpenAI-compatible API)
- **[Lemonade Server](https://lemonade-server.ai/)** — alternative backend for translation, chat, and VLM (OpenAI-compatible API)
- **[Eagle](https://eagle.cool/)** — for auto-saving generated images with metadata
- **[comfyui-mask-editor-one](https://github.com/ketle-man/comfyui-mask-editor-one) (v0.1.9+)** — enables BiRefNet background removal, SAM3 text-prompt segmentation, and ABR stamp-brush library in the Image Edit Mask tool; `birefnet.safetensors` must be placed in `ComfyUI/models/background_removal/` for BiRefNet
@@ -398,6 +407,42 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
## Changelog
### v0.3.76
- **Comic Creator — I2I execution bridge** — added `window._wfmReceiveI2IRunRequest` (`gallery-tab.js`) and `runI2IExternal()`/`_runI2IWithImage()` (`image-edit-tab.js`), a mask-free counterpart to the existing Inpaint bridge (`_wfmReceiveInpaintRequest`/`runInpaintExternal`); lets Comic Creator's Image tab send an image with Positive/Negative prompts and Denoise, run generation in place, and get the result image URL back without opening the Workflow Studio UI
### v0.3.75
- **Lemonade Server support — third AI backend** — added [Lemonade Server](https://lemonade-server.ai/) alongside Ollama and LM Studio as a selectable backend for Translation, Chat, TOOLS (VLM/Wildcards), and the Prompt tab's AI Assistant, in both the SPA and the Library side panel; Lemonade's OpenAI-compatible API (`/v1/models`, `/v1/chat/completions`) reuses the same request/response handling already used for LM Studio, so no new wire-format code was needed
- **Shared default-URL lookup** — `static/js/util.js` gains `getAiBackendDefaultUrl(backend)` (`AI_BACKEND_DEFAULT_URLS`: Ollama `http://localhost:11434`, LM Studio `http://localhost:1234`, Lemonade `http://localhost:13305`), replacing the previous two-way ternary in `ai-tab.js`, `prompt-tab.js`, and `settings-tab.js`; the ComfyUI side panel (`node_sets_menu.js`) keeps its own local copy since it can't import from the SPA's `static/js` modules
- **Backend switch now always resets the URL field** — switching the backend radio (Ollama / LM Studio / Lemonade) in any of the three UIs now unconditionally fills the URL field with that backend's default, instead of only when the field was empty; the side panel previously had no such handler at all
- **Bug fix — Bypass-mode nodes broke generation with a dangling-reference validation error** — `convertUiToApi()` used to drop Bypass-mode (`mode: 4`) nodes without reconnecting the links that ran through them, so any node downstream of a bypassed node's output referenced a node ID that no longer existed in the API graph, and ComfyUI rejected the prompt (`Exception when validating inner node: 'N'`). Bypassed nodes' wires are now traced through to their upstream source (matching same-type input/output slots, recursively for chained bypasses) before being dropped, exactly like ComfyUI's own Bypass behavior. Mute-mode nodes (`mode: 2`) are now also excluded from the API graph (previously they were sent as if fully active); unlike Bypass they are not rerouted, matching ComfyUI's own behavior where muting a node with required downstream dependents is expected to error.
- **GenerateUI — Bypass/Mute indicator above Raw JSON** — when a loaded workflow has Bypass or Mute nodes, a note listing them (title + node ID) now appears above the Raw JSON search bar in the Input/Model/Settings tabs, since the API-format JSON shown there has no `mode` field and can no longer reveal this on its own
### v0.3.74
- **AI TOOL Chat — image attachment shared with TOOLS drop zone** — an image dropped in the TOOLS pane's existing drop zone is now also usable as the Chat pane's attachment (no separate drop zone); it is sent to the LLM as vision input (Ollama `images:[]`, LM Studio `image_url` content block) and shown as an indicator (thumbnail + label + ✕) above the Chat input; it persists across turns until explicitly cleared from either pane
- **AI TOOL Chat — image-to-image (I2I) generation** — when an image is attached and the `generate_image` tool is invoked, the attachment is uploaded and swapped into the target workflow's `LoadImage` node instead of running text-to-image; the `generate_image` tool description was updated so the LLM knows attaching an image switches it to I2I
- **AI TOOL Settings — Chat I2I Generation dedicated workflow** — same "Use dedicated workflow" pattern as the existing T2I setting, but independent and specific to I2I generation
- **`comfyui-editor.js` — `applyImageToSlot()` now supports `opts.workflow`/`opts.analysis`** — mirrors the existing pattern in `applyImageAndMaskToSlot()`; lets callers upload+swap an image into a cloned workflow without touching GenerateUI's displayed state (used by the new Chat I2I bridge)
- **Chat bubble image sizing** — the attached-image echo shown in the user's own message is now a small thumbnail (max 120px) instead of filling the bubble; generated result images are now capped at 20% width instead of 100%
- **Recommended models** — Gemma 4:e4b, Qwen 3.5:9b, and Ministral 3:3b (Ollama) verified to work well for both tool calling and vision in the Chat pane
### v0.3.73
- **AI TOOL Chat — image generation via Tool Calling** — if the selected Ollama/LM Studio model supports tool calling, asking Chat for an image (e.g. "generate a picture of a cat") invokes a `generate_image` tool and runs the request through the same generation pipeline as the GenerateUI Generate button (`window._wfmGenerateTab.generate()`); the result is shown inline in the chat as an image bubble; the conversation is not sent back to the LLM after generation
- **AI TOOL Chat — "Allow image generation" toggle** — checkbox next to the Send/Clear buttons (default on, persisted to `wfm_ai_settings`); when off, the `generate_image` tool is not offered to the LLM at all
- **AI TOOL Settings — Chat Image Generation dedicated workflow** — "Use dedicated workflow" checkbox + saved-workflow picker; when off (default), Chat-triggered generation uses the workflow currently loaded in GenerateUI; when on, it loads the selected saved workflow fresh each time via `/api/wfm/workflows/raw`, without touching what's shown in GenerateUI (same pattern as Image Edit's Inpaint dedicated workflow)
- **Settings — Default Checkpoint** — optional checkbox to force every checkpoint loader node (`CheckpointLoaderSimple` / `CheckpointLoader` / `ImageMetadataPromptLoader`) to a chosen model whenever a workflow is loaded into GenerateUI; a safeguard for environments with many saved workflows referencing models that were later moved/renamed
- **Bug fix — silent checkpoint substitution now surfaced** — `convertUiToApi()`'s COMBO-value fallback (originally added to tolerate drifting Impact Pack wildcard options) was silently replacing *any* unrecognized `ckpt_name` with the first entry in the installed-checkpoints list, making the Model tab show an unrelated model with no indication anything was wrong; workflow loading now detects this specific case and shows a warning toast naming the missing checkpoint instead of the usual "workflow loaded" toast
- **GenerateUI — return value for programmatic generation** — `_coreGenerate`/`handleGenerate` now return `{images, seed}` on success (previously void); existing callers are unaffected, and this enables features like the new Chat image bridge to retrieve the generated image without scraping the DOM
### v0.3.72
- **Comic Creator integration — Inpaint via external entry point** — the Image tab in Comic Creator (`comfyui-comic-creator`) can now trigger Inpaint the same way it already triggers I2I, via `window._wfmReceiveInpaintRequest(imageBlob, maskBlob, params, workflowData?, workflowFilename?)` called across the same-origin iframe boundary; common Inpaint logic was factored out into `_runInpaintWithImages()` and shared by the UI-driven flow and this new external entry point
- **Bug fix — cross-realm Blob detection** — images/masks generated in a different window (Comic Creator) failed to load (`[object Blob]` was interpreted as a URL string, causing a 404) because `instanceof Blob` checks fail across window realms; switched to a `typeof src !== "string"` check
- Hidden mask layers are now excluded from Inpaint in both the UI-driven and external-call paths
### v0.3.71
- **Image Edit — Inpaint tool (🩹)** — new tool composites the canvas (all non-mask layers) plus the active mask layer, uploads to an inpaint-capable LoadImage node (or a Mask Editor One node) in the GenerateUI workflow, applies Positive/Negative prompts, Grow Mask By, and Denoise, then triggers generation — all without leaving the Image Edit tab; the result is shown in the Properties pane
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-workflow-studio"
description = "A comprehensive workflow, asset management, and generation UI plugin for ComfyUI. Manage workflows, models, images, and prompts; batch generation and Image Feeder in the GenerateUI tab; library side panel with canvas drag & drop and AI tools."
version = "0.3.71"
version = "0.3.76"
license = "MIT"
requires-python = ">= 3.10"
dependencies = ["jinja2"]
+118
View File
@@ -511,6 +511,18 @@ body {
transition: border-color 0.2s ease;
}
.wfm-select-missing {
border-color: var(--wfm-warning);
color: var(--wfm-warning);
}
.wfm-model-missing-hint {
font-size: 11px;
color: var(--wfm-warning);
margin-top: 2px;
margin-bottom: 4px;
}
.wfm-input:focus,
.wfm-textarea:focus,
.wfm-select:focus {
@@ -1044,6 +1056,17 @@ body {
height: auto !important;
}
/* Bypass/Mute node indicator (shown above the Raw JSON search bar when present) */
.wfm-gen-rawjson-bypass-note {
padding: 6px 10px;
background: rgba(255, 165, 2, 0.12);
border-bottom: 1px solid var(--wfm-border);
color: var(--wfm-warning);
font-size: 11px;
line-height: 1.5;
flex-shrink: 0;
}
/* RAW JSON search bar */
.wfm-rawjson-search {
display: flex;
@@ -5242,6 +5265,9 @@ body {
.wfm-ai-pane-settings { flex: 2; }
.wfm-ai-pane-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
font-weight: 600;
color: var(--wfm-text-secondary);
@@ -5253,6 +5279,19 @@ body {
background: var(--wfm-surface);
}
.wfm-ai-chat-toggle-label {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
font-size: 11px;
font-weight: 400;
color: var(--wfm-text-secondary);
text-transform: none;
letter-spacing: normal;
cursor: pointer;
}
.wfm-ai-pane-divider {
width: 1px;
background: var(--wfm-border);
@@ -5470,6 +5509,29 @@ body {
pointer-events: none;
}
.wfm-ai-vlm-clear {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 11px;
line-height: 1;
cursor: pointer;
padding: 0;
}
.wfm-ai-vlm-clear:hover {
background: var(--wfm-danger);
}
.wfm-ai-vlm-task-row {
display: flex;
align-items: center;
@@ -5546,6 +5608,62 @@ body {
color: var(--wfm-text);
}
.wfm-ai-chat-img {
max-width: 20%;
border-radius: 6px;
margin-top: 6px;
display: block;
}
.wfm-ai-chat-attach-thumb {
max-width: 120px;
max-height: 120px;
border-radius: 6px;
margin-top: 6px;
display: block;
object-fit: cover;
}
.wfm-ai-chat-attachment {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border: 1px solid var(--wfm-border);
border-radius: var(--wfm-radius-sm);
background: var(--wfm-surface);
font-size: 11px;
color: var(--wfm-text-secondary);
}
.wfm-ai-chat-attachment-thumb {
width: 28px;
height: 28px;
object-fit: cover;
border-radius: 4px;
}
.wfm-ai-chat-attachment-clear {
margin-left: auto;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: transparent;
color: var(--wfm-text-secondary);
font-size: 11px;
cursor: pointer;
padding: 0;
}
.wfm-ai-chat-attachment-clear:hover {
background: var(--wfm-danger);
color: #fff;
}
.wfm-ai-chat-input-area {
display: flex;
flex-direction: column;
+261 -31
View File
@@ -1,11 +1,14 @@
/**
* AI Tab - Translation, VLM, Settings
* Supports Ollama and LM Studio as backends
* Supports Ollama, LM Studio, and Lemonade as backends
*/
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { readJsonStorage } from "./util.js";
import { readJsonStorage, getAiBackendDefaultUrl } from "./util.js";
import { comfyUI } from "./comfyui-client.js";
import { comfyEditor } from "./comfyui-editor.js";
import { comfyWorkflow } from "./comfyui-workflow.js";
const SETTINGS_KEY = "wfm_ai_settings";
@@ -196,7 +199,7 @@ function initTranslateTab() {
const settings = loadAiSettings();
const { backend = "ollama", backendUrl, model } = settings;
const url = backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
const url = backendUrl || getAiBackendDefaultUrl(backend);
if (!isValidBackendUrl(url)) {
showToast(t("aiToastInvalidUrl"), "error");
@@ -285,12 +288,11 @@ function initSettingsTab() {
const urlInput = document.getElementById("wfm-ai-backend-url");
if (urlInput && saved.backendUrl) urlInput.value = saved.backendUrl;
// Update URL placeholder when backend changes
// Switch URL to the new backend's default when the user changes backend
backendRadios.forEach((r) => {
r.addEventListener("change", () => {
if (!urlInput) return;
const defaultUrl = r.value === "ollama" ? "http://localhost:11434" : "http://localhost:1234";
if (!saved.backendUrl) urlInput.value = defaultUrl;
urlInput.value = getAiBackendDefaultUrl(r.value);
});
});
@@ -341,6 +343,47 @@ function initSettingsTab() {
if (freeSrcInput && saved.freeSrcLang) freeSrcInput.value = saved.freeSrcLang;
if (freeDstInput && saved.freeDstLang) freeDstInput.value = saved.freeDstLang;
// Chat image generation — dedicated workflow selection (T2I)
const chatGenCheckbox = document.getElementById("wfm-ai-chatgen-dedicated-checkbox");
const chatGenRow = document.getElementById("wfm-ai-chatgen-dedicated-row");
const chatGenSelect = document.getElementById("wfm-ai-chatgen-dedicated-select");
if (chatGenCheckbox) chatGenCheckbox.checked = !!saved.chatGenDedicatedEnabled;
if (chatGenRow) chatGenRow.style.display = saved.chatGenDedicatedEnabled ? "" : "none";
chatGenCheckbox?.addEventListener("change", () => {
if (chatGenRow) chatGenRow.style.display = chatGenCheckbox.checked ? "" : "none";
});
// Chat I2I generation — dedicated workflow selection
const chatGenI2ICheckbox = document.getElementById("wfm-ai-chatgen-i2i-dedicated-checkbox");
const chatGenI2IRow = document.getElementById("wfm-ai-chatgen-i2i-dedicated-row");
const chatGenI2ISelect = document.getElementById("wfm-ai-chatgen-i2i-dedicated-select");
if (chatGenI2ICheckbox) chatGenI2ICheckbox.checked = !!saved.chatGenI2IDedicatedEnabled;
if (chatGenI2IRow) chatGenI2IRow.style.display = saved.chatGenI2IDedicatedEnabled ? "" : "none";
chatGenI2ICheckbox?.addEventListener("change", () => {
if (chatGenI2IRow) chatGenI2IRow.style.display = chatGenI2ICheckbox.checked ? "" : "none";
});
if (chatGenSelect) {
fetch(`${comfyUI.baseUrl}/api/wfm/workflows`)
.then((r) => (r.ok ? r.json() : []))
.then((list) => {
const filenames = (list || []).map((w) => w.filename).filter(Boolean);
const optionsHtml = `<option value="">-- select workflow --</option>` +
filenames.map((f) => `<option value="${f}">${f}</option>`).join("");
chatGenSelect.innerHTML = optionsHtml;
if (saved.chatGenDedicatedFilename) chatGenSelect.value = saved.chatGenDedicatedFilename;
if (chatGenI2ISelect) {
chatGenI2ISelect.innerHTML = optionsHtml;
if (saved.chatGenI2IDedicatedFilename) chatGenI2ISelect.value = saved.chatGenI2IDedicatedFilename;
}
})
.catch(() => {});
}
// Save settings
document.getElementById("wfm-ai-settings-save-btn")?.addEventListener("click", () => {
const backend = document.querySelector("input[name='wfm-ai-backend']:checked")?.value || "ollama";
@@ -348,13 +391,21 @@ function initSettingsTab() {
const model = document.getElementById("wfm-ai-model-select")?.value || "";
const freeSrcLang = freeSrcInput?.value?.trim() || "";
const freeDstLang = freeDstInput?.value?.trim() || "";
const chatGenDedicatedEnabled = !!chatGenCheckbox?.checked;
const chatGenDedicatedFilename = chatGenSelect?.value || "";
const chatGenI2IDedicatedEnabled = !!chatGenI2ICheckbox?.checked;
const chatGenI2IDedicatedFilename = chatGenI2ISelect?.value || "";
if (url && !isValidBackendUrl(url)) {
showToast(t("aiToastInvalidUrl"), "error");
return;
}
saveAiSettings({ backend, backendUrl: url, model, freeSrcLang, freeDstLang });
saveAiSettings({
backend, backendUrl: url, model, freeSrcLang, freeDstLang,
chatGenDedicatedEnabled, chatGenDedicatedFilename,
chatGenI2IDedicatedEnabled, chatGenI2IDedicatedFilename,
});
showToast(t("aiToastSettingsSaved"), "success");
});
@@ -368,30 +419,137 @@ function initSettingsTab() {
// Chat tab
// ============================================
async function callChat(url, backend, model, messages) {
const IMAGE_GEN_TOOLS = [{
type: "function",
function: {
name: "generate_image",
description: "Generate an image using the currently loaded ComfyUI workflow. If an image is currently attached to the conversation, this performs image-to-image generation using that image as the base; otherwise it performs text-to-image. Call this when the user asks to create, draw, paint, edit, or transform an image.",
parameters: {
type: "object",
properties: {
prompt: { type: "string", description: "Positive prompt describing the desired image." },
negative_prompt: { type: "string", description: "What to avoid in the image. Optional." },
},
required: ["prompt"],
},
},
}];
// chatHistory の内部共通形式 {role, content, images?:[{base64,mimeType}]} を
// バックエンドごとのワイヤーフォーマットへ変換する(callVLM() と同じ規約)
function _formatMessagesForBackend(messages, backend) {
return messages.map((m) => {
if (!m.images?.length) return { role: m.role, content: m.content };
if (backend === "ollama") {
return { role: m.role, content: m.content, images: m.images.map((i) => i.base64) };
}
return {
role: m.role,
content: [
{ type: "text", text: m.content },
...m.images.map((i) => ({ type: "image_url", image_url: { url: `data:${i.mimeType};base64,${i.base64}` } })),
],
};
});
}
async function callChat(url, backend, model, messages, tools) {
const formattedMessages = _formatMessagesForBackend(messages, backend);
if (backend === "ollama") {
const body = { model, messages: formattedMessages, stream: false };
if (tools) body.tools = tools;
const res = await fetch(`${url}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, messages, stream: false }),
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).message?.content || "";
const message = (await res.json()).message || {};
return { content: message.content || "", toolCalls: message.tool_calls || null };
} else {
const body = { model, messages: formattedMessages, stream: false };
if (tools) { body.tools = tools; body.tool_choice = "auto"; }
const res = await fetch(`${url}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, messages, stream: false }),
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).choices?.[0]?.message?.content || "";
const message = (await res.json()).choices?.[0]?.message || {};
return { content: message.content || "", toolCalls: message.tool_calls || null };
}
}
function appendChatBubble(messagesEl, role, content) {
function _parseToolArgs(toolCall) {
const raw = toolCall.function?.arguments ?? toolCall.arguments;
if (typeof raw === "string") {
try { return JSON.parse(raw); } catch { return {}; }
}
return raw || {};
}
async function _loadDedicatedChatGenWorkflow(filename) {
const resp = await fetch(`${comfyUI.baseUrl}/api/wfm/workflows/raw?filename=${encodeURIComponent(filename)}`);
if (!resp.ok) throw new Error(`Failed to load workflow (HTTP ${resp.status})`);
let workflow = await resp.json();
const format = comfyWorkflow.detectFormat(workflow, filename);
if (format === "ui") {
workflow = await comfyWorkflow.convertUiToApi(workflow);
} else if (format !== "api") {
throw new Error("Unsupported or unrecognized workflow format");
}
return { workflow, analysis: comfyWorkflow.analyzeWorkflow(workflow) };
}
async function generateImageFromChat(prompt, negativePrompt) {
if (!window._wfmGenerateTab?.generate) throw new Error(t("aiChatGenerateNotReady"));
const settings = loadAiSettings();
const isI2I = !!_toolsImage;
let workflow, analysis;
if (isI2I && settings.chatGenI2IDedicatedEnabled && settings.chatGenI2IDedicatedFilename) {
({ workflow, analysis } = await _loadDedicatedChatGenWorkflow(settings.chatGenI2IDedicatedFilename));
} else if (!isI2I && settings.chatGenDedicatedEnabled && settings.chatGenDedicatedFilename) {
({ workflow, analysis } = await _loadDedicatedChatGenWorkflow(settings.chatGenDedicatedFilename));
} else {
if (!comfyUI.currentWorkflow || !comfyUI.currentAnalysis) throw new Error(t("aiChatNoWorkflowLoaded"));
workflow = JSON.parse(JSON.stringify(comfyUI.currentWorkflow));
analysis = comfyUI.currentAnalysis;
}
if (isI2I) {
if (!(analysis.load_image_nodes?.length > 0)) throw new Error(t("aiChatNoLoadImageNode"));
await comfyEditor.applyImageToSlot(_toolsImage.file, 0, { workflow, analysis });
}
comfyEditor.setPromptText("positive", prompt || "", { workflow, analysis });
if (negativePrompt) comfyEditor.setPromptText("negative", negativePrompt, { workflow, analysis });
const result = await window._wfmGenerateTab.generate(workflow);
const images = result?.images || [];
if (images.length === 0) throw new Error(t("aiChatNoImageProduced"));
const img = images[0];
return `/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${encodeURIComponent(img.type || "output")}`;
}
function appendChatBubble(messagesEl, role, content, opts = {}) {
const div = document.createElement("div");
div.className = `wfm-ai-chat-msg wfm-ai-chat-msg-${role}`;
div.textContent = content;
if (content) div.textContent = content;
if (opts.imageUrl) {
const img = document.createElement("img");
img.src = opts.imageUrl;
img.className = "wfm-ai-chat-img";
div.appendChild(img);
}
if (opts.attachThumb) {
const img = document.createElement("img");
img.src = opts.attachThumb;
img.className = "wfm-ai-chat-attach-thumb";
div.appendChild(img);
}
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
return div;
@@ -403,9 +561,22 @@ function initChatTab() {
const sendBtn = document.getElementById("wfm-ai-chat-send-btn");
const clearBtn = document.getElementById("wfm-ai-chat-clear-btn");
const statusEl = document.getElementById("wfm-ai-chat-status");
const imgGenToggle = document.getElementById("wfm-ai-chat-imggen-toggle");
if (!messagesEl || !inputEl || !sendBtn) return;
if (imgGenToggle) {
const savedToggle = loadAiSettings();
imgGenToggle.checked = savedToggle.chatImageGenEnabled !== false;
imgGenToggle.addEventListener("change", () => {
saveAiSettings({ chatImageGenEnabled: imgGenToggle.checked });
});
}
document.getElementById("wfm-ai-chat-attachment-clear")?.addEventListener("click", () => {
_clearToolsImage();
});
let chatHistory = [];
async function sendMessage() {
@@ -414,7 +585,7 @@ function initChatTab() {
const settings = loadAiSettings();
const { backend = "ollama", backendUrl, model } = settings;
const url = backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
const url = backendUrl || getAiBackendDefaultUrl(backend);
if (!isValidBackendUrl(url)) {
showToast(t("aiToastInvalidUrl"), "error");
@@ -427,17 +598,39 @@ function initChatTab() {
inputEl.value = "";
inputEl.style.height = "";
chatHistory.push({ role: "user", content: text });
appendChatBubble(messagesEl, "user", text);
const userEntry = { role: "user", content: text };
if (_toolsImage) userEntry.images = [{ base64: _toolsImage.base64, mimeType: _toolsImage.mimeType }];
chatHistory.push(userEntry);
appendChatBubble(messagesEl, "user", text, _toolsImage ? { attachThumb: `data:${_toolsImage.mimeType};base64,${_toolsImage.base64}` } : {});
sendBtn.disabled = true;
statusEl.textContent = t("aiStatusChatting");
statusEl.className = "wfm-ai-trans-status wfm-ai-status-working";
try {
const reply = await callChat(url, backend, model, chatHistory);
chatHistory.push({ role: "assistant", content: reply });
appendChatBubble(messagesEl, "assistant", reply);
const tools = imgGenToggle?.checked !== false ? IMAGE_GEN_TOOLS : undefined;
const reply = await callChat(url, backend, model, chatHistory, tools);
const toolCall = reply.toolCalls?.find(
(tc) => (tc.function?.name || tc.name) === "generate_image"
);
if (toolCall) {
chatHistory.push({ role: "assistant", content: reply.content || "" });
if (reply.content) appendChatBubble(messagesEl, "assistant", reply.content);
statusEl.textContent = t("aiStatusGeneratingImage");
const args = _parseToolArgs(toolCall);
try {
const imageUrl = await generateImageFromChat(args.prompt, args.negative_prompt);
appendChatBubble(messagesEl, "assistant", "", { imageUrl });
} catch (genErr) {
appendChatBubble(messagesEl, "assistant", `${t("aiToastImageGenFailed")}${genErr.message}`);
showToast(t("aiToastImageGenFailed") + genErr.message, "error");
}
} else {
chatHistory.push({ role: "assistant", content: reply.content });
appendChatBubble(messagesEl, "assistant", reply.content);
}
statusEl.textContent = "";
statusEl.className = "wfm-ai-trans-status";
} catch (err) {
@@ -469,14 +662,49 @@ function initChatTab() {
});
}
// ============================================
// TOOLS ↔ Chat shared image attachment
// ============================================
let _toolsImage = null; // { file: File, base64: string, mimeType: string }
function _syncAttachmentUI() {
const vlmPreview = document.getElementById("wfm-ai-vlm-preview");
const vlmLabel = document.getElementById("wfm-ai-vlm-label");
const vlmClear = document.getElementById("wfm-ai-vlm-clear");
const chatAttach = document.getElementById("wfm-ai-chat-attachment");
const chatThumb = document.getElementById("wfm-ai-chat-attachment-thumb");
const chatLabel = document.getElementById("wfm-ai-chat-attachment-label");
if (_toolsImage) {
const dataUrl = `data:${_toolsImage.mimeType};base64,${_toolsImage.base64}`;
if (vlmPreview) { vlmPreview.src = dataUrl; vlmPreview.style.display = "block"; }
if (vlmLabel) vlmLabel.style.display = "none";
if (vlmClear) vlmClear.style.display = "flex";
if (chatAttach) chatAttach.style.display = "flex";
if (chatThumb) chatThumb.src = dataUrl;
if (chatLabel) chatLabel.textContent = t("aiChatAttachmentLabel");
} else {
if (vlmPreview) { vlmPreview.src = ""; vlmPreview.style.display = "none"; }
if (vlmLabel) vlmLabel.style.display = "";
if (vlmClear) vlmClear.style.display = "none";
if (chatAttach) chatAttach.style.display = "none";
if (chatThumb) chatThumb.src = "";
}
}
function _clearToolsImage() {
_toolsImage = null;
_syncAttachmentUI();
}
// ============================================
// VLM tab
// ============================================
function initVlmTab() {
const dropEl = document.getElementById("wfm-ai-vlm-drop");
const previewEl = document.getElementById("wfm-ai-vlm-preview");
const labelEl = document.getElementById("wfm-ai-vlm-label");
const clearEl = document.getElementById("wfm-ai-vlm-clear");
const fileInput = document.getElementById("wfm-ai-vlm-file");
const taskSel = document.getElementById("wfm-ai-vlm-task");
const runBtn = document.getElementById("wfm-ai-vlm-run");
@@ -489,8 +717,6 @@ function initVlmTab() {
if (!dropEl) return;
let vlmImage = null; // { base64, mimeType }
function updateTaskUI() {
const isWildcard = taskSel?.value === "wildcard";
dropEl.style.display = isWildcard ? "none" : "";
@@ -501,10 +727,9 @@ function initVlmTab() {
const loadImage = async (file) => {
if (!file || !file.type.startsWith("image/")) return;
vlmImage = await fileToBase64(file);
previewEl.src = `data:${vlmImage.mimeType};base64,${vlmImage.base64}`;
previewEl.style.display = "block";
labelEl.style.display = "none";
const { base64, mimeType } = await fileToBase64(file);
_toolsImage = { file, base64, mimeType };
_syncAttachmentUI();
};
dropEl.addEventListener("click", () => fileInput.click());
@@ -516,12 +741,17 @@ function initVlmTab() {
dropEl.classList.remove("drag-over");
if (e.dataTransfer.files[0]) loadImage(e.dataTransfer.files[0]);
});
clearEl?.addEventListener("click", (e) => {
e.stopPropagation();
_clearToolsImage();
if (fileInput) fileInput.value = "";
});
runBtn.addEventListener("click", async () => {
const task = taskSel?.value || "describe";
const settings = loadAiSettings();
const { backend = "ollama", backendUrl, model } = settings;
const url = backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
const url = backendUrl || getAiBackendDefaultUrl(backend);
if (!isValidBackendUrl(url)) { showToast(t("aiToastInvalidUrl"), "error"); return; }
if (!model) { showToast(t("aiToastNoModel"), "error"); return; }
@@ -557,7 +787,7 @@ function initVlmTab() {
return;
}
if (!vlmImage) { showToast(t("aiToastNoImage"), "error"); return; }
if (!_toolsImage) { showToast(t("aiToastNoImage"), "error"); return; }
runBtn.disabled = true;
statusEl.textContent = t("aiStatusRunning");
@@ -565,7 +795,7 @@ function initVlmTab() {
resultEl.value = "";
try {
const result = await callVLM(url, backend, model, VLM_PROMPTS[task], vlmImage.base64, vlmImage.mimeType);
const result = await callVLM(url, backend, model, VLM_PROMPTS[task], _toolsImage.base64, _toolsImage.mimeType);
resultEl.value = result.trim();
statusEl.textContent = t("aiStatusDone");
statusEl.className = "wfm-ai-trans-status wfm-ai-status-ok";
+11 -1
View File
@@ -310,6 +310,16 @@ function applyI18nToHtml() {
if (aiFreeDstLabel) aiFreeDstLabel.textContent = t("aiSettingsOutputLang");
const aiSaveBtn = document.getElementById("wfm-ai-settings-save-btn");
if (aiSaveBtn) aiSaveBtn.textContent = t("aiSettingsSaveBtn");
const aiChatImgGenLabel = document.getElementById("wfm-ai-chat-imggen-label-text");
if (aiChatImgGenLabel) aiChatImgGenLabel.textContent = t("aiChatImgGenToggle");
const aiChatGenTitle = document.getElementById("wfm-ai-settings-chatgen-title");
if (aiChatGenTitle) aiChatGenTitle.textContent = t("aiSettingsChatGenTitle");
const aiChatGenDedicatedLabel = document.getElementById("wfm-ai-chatgen-dedicated-label");
if (aiChatGenDedicatedLabel) aiChatGenDedicatedLabel.textContent = t("aiSettingsChatGenDedicated");
const aiChatGenI2ITitle = document.getElementById("wfm-ai-settings-chatgen-i2i-title");
if (aiChatGenI2ITitle) aiChatGenI2ITitle.textContent = t("aiSettingsChatI2IGenTitle");
const aiChatGenI2IDedicatedLabel = document.getElementById("wfm-ai-chatgen-i2i-dedicated-label");
if (aiChatGenI2IDedicatedLabel) aiChatGenI2IDedicatedLabel.textContent = t("aiSettingsChatGenDedicated");
// Group panel labels
const propTitle = document.querySelector("#wfm-side-tab-group .wfm-group-section-title");
@@ -482,7 +492,7 @@ function applyI18nToHtml() {
"wfm-help-trouble-1": "helpTrouble1", "wfm-help-trouble-2": "helpTrouble2",
"wfm-help-trouble-3": "helpTrouble3", "wfm-help-trouble-4": "helpTrouble4",
"wfm-help-trouble-5": "helpTrouble5", "wfm-help-trouble-6": "helpTrouble6",
"wfm-help-trouble-7": "helpTrouble7",
"wfm-help-trouble-7": "helpTrouble7", "wfm-help-trouble-8": "helpTrouble8",
"wfm-help-support-title": "helpSupportTitle",
"wfm-help-support-desc": "helpSupportDesc",
"wfm-help-github-desc": "helpGithubDesc",
+20 -6
View File
@@ -5,6 +5,7 @@
import { comfyUI } from "./comfyui-client.js";
import { syncJsonHighlight } from "./json-highlight.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./util.js";
// ── Latent Image preset state ─────────────────────────────
const _LATENT_PRESET_KEY = "wfm_latent_presets";
@@ -160,7 +161,10 @@ function _syncRawJson() {
// ComfyUIネイティブのLoadImageはアルファチャンネルから mask = 1 - alpha を抽出するため、
// 合成後の alpha は 255 - maskGray とする。
async function _loadImageElement(src) {
const isBlobLike = src instanceof Blob;
// `instanceof Blob` は別レルム(他ウィンドウ/iframe)で生成されたBlob/Fileでは
// falseになる(コンストラクタの参照が異なるため)。Comic CreaterからiframeへBlobを
// 直接渡すInpaint連携で発生するため、実体で判定する(文字列URLかどうかだけを見る)。
const isBlobLike = typeof src !== "string";
const url = isBlobLike ? URL.createObjectURL(src) : src;
try {
const img = new Image();
@@ -519,6 +523,7 @@ export const comfyEditor = {
.map((s) => {
const models = this.models[s.key] || [];
const currentVal = s.nodes?.[0]?.[s.inputKey] || "";
const isMissing = !!currentVal && !models.includes(currentVal);
const targetOpts = s.nodes
.map((n) => `<option value="${n.id}">ID:${n.id} (${n.title})</option>`)
.join("");
@@ -537,9 +542,11 @@ export const comfyEditor = {
<div class="wfm-form-group" style="border-bottom:1px solid var(--wfm-border);padding-bottom:12px;">
<label>${s.label}</label>
<input type="text" class="wfm-input wfm-model-filter" placeholder="Filter..." data-target="wfm-model-${s.key}" style="margin-bottom:4px;">
<select class="wfm-select" id="wfm-model-${s.key}" style="margin-bottom:4px;">
<select class="wfm-select ${isMissing ? "wfm-select-missing" : ""}" id="wfm-model-${s.key}" style="margin-bottom:4px;">
${isMissing ? `<option value="${escapeHtml(currentVal)}" selected>⚠ ${escapeHtml(currentVal)} (${t("modelNotFound")})</option>` : ""}
${models.map((m) => `<option value="${m}" ${m === currentVal ? "selected" : ""}>${m}</option>`).join("")}
</select>
${isMissing ? `<div class="wfm-model-missing-hint">${t("modelNotFoundHint")}</div>` : ""}
${extrasHtml}
<div style="display:flex;gap:8px;align-items:center;margin-top:4px;">
<select class="wfm-select" id="wfm-model-${s.key}-target" style="flex:1;">${targetOpts}</select>
@@ -1475,18 +1482,25 @@ export const comfyEditor = {
});
},
async applyImageToSlot(file, slotIndex = 0) {
const loadNodes = comfyUI.currentAnalysis?.load_image_nodes || [];
// opts.workflow/opts.analysis を渡すと comfyUI.currentWorkflow/currentAnalysis の代わりに
// そちらへ書き込み、DOM更新は行わずアップロード後のファイル名だけを返す
// (GenerateUIタブの表示状態には触れない。Chatペインの画像添付I2I連携から利用)。
async applyImageToSlot(file, slotIndex = 0, opts = {}) {
const workflow = opts.workflow || comfyUI.currentWorkflow;
const analysis = opts.analysis || comfyUI.currentAnalysis;
const loadNodes = analysis?.load_image_nodes || [];
const node = loadNodes[slotIndex];
if (!node) throw new Error("No LoadImage node at slot " + slotIndex);
const result = await comfyUI.uploadImage(file, file.name);
if (!result.name) throw new Error("Upload returned no filename");
if (comfyUI.currentWorkflow?.[node.id]) {
comfyUI.currentWorkflow[node.id].inputs.image = result.name;
if (workflow?.[node.id]) {
workflow[node.id].inputs.image = result.name;
}
if (opts.workflow) return result.name;
const previewWrap = document.getElementById(`wfm-i2i-preview-wrap-${slotIndex}`);
const previewImg = document.getElementById(`wfm-i2i-preview-${slotIndex}`);
const filenameEl = document.getElementById(`wfm-i2i-filename-${slotIndex}`);
+84 -2
View File
@@ -5,6 +5,50 @@
// Cache for /object_info (loaded once, shared)
let _objectInfoCache = null;
// convertUiToApi() の COMBO フォールバック(未一致時に選択肢の先頭へ無警告で差し替え)が
// ckpt_name に対して発生した場合の記録。呼び出し元が読み込み直後に確認して警告表示できるよう、
// 直近の convertUiToApi() 呼び出し分だけを保持する。
let _lastCheckpointSubstitutions = [];
// convertUiToApi() で除外された Bypass(mode:4) / Mute(mode:2) ノードの記録。
// API形式には mode 情報が残らないため、呼び出し元がRaw JSON表示時に
// 「このワークフローにはバイパス/ミュート中のノードがあった」と伝えられるよう、
// 直近の convertUiToApi() 呼び出し分だけを保持する。
let _lastBypassedNodes = [];
let _lastMutedNodes = [];
/**
* Trace a link's source through any Bypass-mode (mode 4) nodes, matching each bypassed
* node's output slot to an input slot of the same type and following the connection
* upstream (recursively, for chained bypasses). Mirrors ComfyUI's own Bypass behavior,
* where a bypassed node is removed but its wires are reconnected end-to-end.
* Returns null when no pass-through source exists (the matching input is unconnected),
* in which case the caller should treat the input as unlinked.
*/
function _resolveBypassSource(nodeById, linkMap, nodeId, slot, visited) {
const idStr = String(nodeId);
const node = nodeById[idStr];
if (!node || node.mode !== 4) return [idStr, slot];
if (visited.has(idStr)) return null; // circular bypass chain guard
visited.add(idStr);
const outputs = node.outputs || [];
const inputs = node.inputs || [];
const outType = outputs[slot]?.type;
let inIdx = -1;
if (inputs[slot]?.type === outType && linkMap[idStr]?.[slot]) {
inIdx = slot;
} else {
inIdx = inputs.findIndex((inp, i) => inp.type === outType && linkMap[idStr]?.[i]);
}
if (inIdx === -1) return null;
const upstream = linkMap[idStr][inIdx];
if (!upstream) return null;
return _resolveBypassSource(nodeById, linkMap, upstream[0], upstream[1], visited);
}
async function _loadObjectInfo() {
if (_objectInfoCache) return _objectInfoCache;
try {
@@ -267,7 +311,22 @@ export const comfyWorkflow = {
/**
* Convert UI-format workflow to API-format using /object_info for accurate widget mapping.
*/
getLastCheckpointSubstitutions() {
return _lastCheckpointSubstitutions;
},
getLastBypassedNodes() {
return _lastBypassedNodes;
},
getLastMutedNodes() {
return _lastMutedNodes;
},
async convertUiToApi(workflow) {
_lastCheckpointSubstitutions = [];
_lastBypassedNodes = [];
_lastMutedNodes = [];
if (!workflow.nodes || !workflow.links) return {};
// Flatten subgraphs before conversion
@@ -282,10 +341,23 @@ export const comfyWorkflow = {
if (!linkMap[dstNode]) linkMap[dstNode] = {};
linkMap[dstNode][dstSlot] = [String(srcNode), srcSlot];
}
const nodeById = {};
for (const n of flatWorkflow.nodes) nodeById[String(n.id)] = n;
const api = {};
for (const node of flatWorkflow.nodes) {
if (node.mode === 4) continue; // muted/bypassed
// Mute (mode 2): excluded, no pass-through — matches ComfyUI itself, where
// muting a node with required downstream dependents is expected to error.
// Bypass (mode 4): excluded here too, but its wires are rerouted below via
// _resolveBypassSource() so downstream nodes reconnect to its upstream source.
if (node.mode === 2) {
_lastMutedNodes.push({ nodeId: String(node.id), title: node.title || node.type });
continue;
}
if (node.mode === 4) {
_lastBypassedNodes.push({ nodeId: String(node.id), title: node.title || node.type });
continue;
}
// Skip note/display-only nodes that are unknown to ComfyUI backend
if (!objectInfo[node.type] && _isDisplayOnlyNode(node)) continue;
const nodeId = String(node.id);
@@ -301,7 +373,9 @@ export const comfyWorkflow = {
const linkedSlotNames = new Set();
inputDefs.forEach((inp, idx) => {
if (nodeLinks[idx]) {
inputs[inp.name] = nodeLinks[idx];
const resolved = _resolveBypassSource(nodeById, linkMap, nodeLinks[idx][0], nodeLinks[idx][1], new Set());
if (!resolved) return; // Bypass chain with no upstream source — leave unlinked
inputs[inp.name] = resolved;
linkedInputNames.add(inp.name);
linkedSlotNames.add(inp.name);
}
@@ -379,6 +453,14 @@ export const comfyWorkflow = {
if (spec) {
const choices = Array.isArray(spec[0]) ? spec[0] : null;
if (choices && choices.length > 0 && !choices.includes(val)) {
if (name === "ckpt_name") {
_lastCheckpointSubstitutions.push({
nodeId: String(node.id),
title: node.title || node.type,
original: val,
replaced: choices[0],
});
}
val = choices[0];
}
}
+46
View File
@@ -80,6 +80,52 @@ window._wfmReceiveImageForI2I = async (blob, name, workflowData, workflowFilenam
}
};
// ComfyUI Comic Creater からiframe越しに合成画像+マスクを受け取り、Image Edit タブの
// Inpaint機能をUIを開かずに直接実行して結果URLを返す(Inpaint連携)。
// Comic Creater側の Image タブ「Inpaint」から
// iframe.contentWindow._wfmReceiveInpaintRequest(imageBlob, maskBlob, params, workflowData?, workflowFilename?) として呼ばれる。
// workflowData が渡された場合(Comic Creater側の設定タブでInpaint用デフォルトワークフローが有効な時)は、
// 実行前にそのワークフローを読み込む。渡されない場合はGenerate UIに現在ロード中のワークフローをそのまま使う。
window._wfmReceiveInpaintRequest = async (imageBlob, maskBlob, params, workflowData, workflowFilename) => {
try {
if (workflowData) {
if (!comfyUI.connected) {
await comfyUI.checkConnection();
}
if (comfyUI.connected && (!comfyEditor.models.checkpoints || comfyEditor.models.checkpoints.length === 0)) {
await comfyEditor.loadModelLists();
}
await loadWorkflowIntoEditor(workflowData, workflowFilename || "workflow.json");
}
return await window._wfmImageEditTab.runInpaintExternal(imageBlob, maskBlob, params);
} catch (e) {
return { ok: false, message: e.message };
}
};
// ComfyUI Comic Creater からiframe越しに画像を受け取り、Image Edit タブの
// Select I2I機能をUIを開かずに直接実行して結果URLを返す(I2I連携、マスク不要)。
// Comic Creater側の Image タブ「Select→I2I」から
// iframe.contentWindow._wfmReceiveI2IRunRequest(imageBlob, params, workflowData?, workflowFilename?) として呼ばれる。
// workflowData が渡された場合(Comic Creater側のI2I設定でデフォルトワークフローが有効な時)は、
// 実行前にそのワークフローを読み込む。渡されない場合はGenerate UIに現在ロード中のワークフローをそのまま使う。
window._wfmReceiveI2IRunRequest = async (imageBlob, params, workflowData, workflowFilename) => {
try {
if (workflowData) {
if (!comfyUI.connected) {
await comfyUI.checkConnection();
}
if (comfyUI.connected && (!comfyEditor.models.checkpoints || comfyEditor.models.checkpoints.length === 0)) {
await comfyEditor.loadModelLists();
}
await loadWorkflowIntoEditor(workflowData, workflowFilename || "workflow.json");
}
return await window._wfmImageEditTab.runI2IExternal(imageBlob, params);
} catch (e) {
return { ok: false, message: e.message };
}
};
// ── ページング ────────────────────────────────────────────────
const PAGE_SIZE = 50;
let _renderedCount = 0;
+64 -3
View File
@@ -9,7 +9,7 @@ import { comfyEditor } from "./comfyui-editor.js";
import { t } from "./i18n.js";
import { syncJsonHighlight, syncScroll } from "./json-highlight.js";
import { initFeederTab, refreshFeederNodeList } from "./feeder-tab.js";
import { getSettings, readJsonStorage } from "./util.js";
import { getSettings, readJsonStorage, escapeHtml } from "./util.js";
// ============================================
// Eagle Auto-Save
@@ -218,21 +218,52 @@ async function saveCurrentWorkflow() {
// Workflow Loading
// ============================================
// ワークフロー読み込み時、設定タブで有効化されていれば checkpoint loader 系ノード全件の
// ckpt_name をデフォルトモデルへ強制上書きする(保存済みワークフローのモデルが
// 環境と合わず生成エラー/不鮮明な画像になるのを防ぐ回避策)。
// analyzeWorkflow() より前に apiWorkflow 自体を書き換える必要がある — analysis後に
// 書き換えると Model タブの表示(analysis.checkpoint_nodes 由来)が古い値のまま残るため。
async function _applyDefaultCheckpointIfEnabled(apiWorkflow) {
try {
const res = await fetch("/api/wfm/settings");
if (!res.ok) return;
const settings = await res.json();
if (!settings.default_checkpoint_enabled || !settings.default_checkpoint_name) return;
let applied = false;
for (const node of Object.values(apiWorkflow)) {
const ct = node?.class_type || "";
if (node?.inputs && (ct.includes("CheckpointLoader") || ct === "Checkpoint Loader" || ct === "ImageMetadataPromptLoader")) {
node.inputs.ckpt_name = settings.default_checkpoint_name;
applied = true;
}
}
if (applied) showToast(t("defaultCheckpointApplied", settings.default_checkpoint_name), "info");
} catch { /* non-critical, ignore */ }
}
export async function loadWorkflowIntoEditor(workflow, filename) {
let apiWorkflow = workflow;
const format = comfyWorkflow.detectFormat(workflow, filename);
let ckptWarning = null;
if (format === "app") {
showToast(t("appFormatNotSupported"), "error");
return false;
} else if (format === "ui") {
apiWorkflow = await comfyWorkflow.convertUiToApi(workflow);
const ckptSubs = comfyWorkflow.getLastCheckpointSubstitutions();
if (ckptSubs.length > 0) {
const names = [...new Set(ckptSubs.map((s) => s.original))].join(", ");
ckptWarning = t("checkpointSubstitutedWarning", names);
}
} else if (format === "unknown") {
showToast(t("unknownWorkflowFormat"), "error");
return false;
}
comfyUI.currentWorkflow = apiWorkflow;
await _applyDefaultCheckpointIfEnabled(apiWorkflow);
comfyUI.currentAnalysis = comfyWorkflow.analyzeWorkflow(apiWorkflow);
// Render editor tabs
@@ -247,6 +278,30 @@ export async function loadWorkflowIntoEditor(workflow, filename) {
syncJsonHighlight(rawHighlight, jsonStr);
}
// Update Bypass/Mute node indicator (shared Raw JSON widget header, visible on
// Input/Model/Settings tabs). API-format JSON has no mode field, so this info is
// only available right after a UI→API conversion — captured via comfyWorkflow's
// getLastBypassedNodes()/getLastMutedNodes() before it's discarded.
const bypassNote = document.getElementById("wfm-gen-rawjson-bypass-note");
if (bypassNote) {
const bypassed = format === "ui" ? comfyWorkflow.getLastBypassedNodes() : [];
const muted = format === "ui" ? comfyWorkflow.getLastMutedNodes() : [];
const lines = [];
if (bypassed.length > 0) {
lines.push(t("rawJsonBypassNote", bypassed.map((n) => `${n.title} (${n.nodeId})`).join(", ")));
}
if (muted.length > 0) {
lines.push(t("rawJsonMutedNote", muted.map((n) => `${n.title} (${n.nodeId})`).join(", ")));
}
if (lines.length > 0) {
bypassNote.innerHTML = lines.map((l) => escapeHtml(l)).join("<br>");
bypassNote.style.display = "";
} else {
bypassNote.textContent = "";
bypassNote.style.display = "none";
}
}
// Update workflow name display
const nameEl = document.getElementById("wfm-gen-wf-name");
if (nameEl) {
@@ -260,7 +315,11 @@ export async function loadWorkflowIntoEditor(workflow, filename) {
refreshFeederNodeList();
showToast(t("workflowLoadedName", filename || ""), "success");
if (ckptWarning) {
showToast(ckptWarning, "error");
} else {
showToast(t("workflowLoadedName", filename || ""), "success");
}
return true;
}
@@ -1468,6 +1527,8 @@ async function _coreGenerate(silent = false, workflowOverride = null) {
}
if (!silent) showToast(t("generationComplete"), "success");
return { images, seed };
}
// ============================================
@@ -1762,7 +1823,7 @@ async function handleGenerate(workflowOverride = null) {
await _runBatchGenerate();
} else {
try {
await _coreGenerate(false, workflowOverride);
return await _coreGenerate(false, workflowOverride);
} catch (err) {
const progressText = document.getElementById("wfm-gen-progress-text");
if (progressText) progressText.textContent = "Error";
+93 -27
View File
@@ -207,6 +207,10 @@ const LANGUAGES = {
unknownWorkflowFormat: "Unknown workflow format",
generationComplete: "Generation complete",
generationError: (msg) => `Generation error: ${msg}`,
defaultCheckpointApplied: (name) => `Default checkpoint applied: ${name}`,
checkpointSubstitutedWarning: (names) => `Checkpoint not found in this workflow, silently replaced with another model: ${names}. Please check the Model tab.`,
rawJsonBypassNote: (list) => `Bypassed (excluded, wires reconnected): ${list}`,
rawJsonMutedNote: (list) => `Muted (excluded): ${list}`,
batchNoneSelected: (what) => `No ${what} selected`,
batchItemFailed: (i, total, msg) => `[${i}/${total}] Failed: ${msg}`,
batchStopped: (completed, failed) => `Batch stopped — ${completed} completed, ${failed} failed`,
@@ -602,6 +606,17 @@ const LANGUAGES = {
aiStatusChatting: "Thinking...",
aiToastChatFailed: "Chat failed: ",
aiToastWcNoName: "Please enter a wildcard name",
aiStatusGeneratingImage: "Generating image...",
aiChatNoWorkflowLoaded: "No workflow loaded in GenerateUI",
aiChatGenerateNotReady: "GenerateUI is not ready yet",
aiChatNoImageProduced: "Generation completed but no image was produced",
aiToastImageGenFailed: "Image generation failed: ",
aiChatImgGenToggle: "Allow image generation",
aiSettingsChatGenTitle: "Chat Image Generation",
aiSettingsChatGenDedicated: "Use dedicated workflow",
aiSettingsChatI2IGenTitle: "Chat I2I Generation",
aiChatAttachmentLabel: "Image attached",
aiChatNoLoadImageNode: "No LoadImage node found in the target workflow for image-to-image",
// -- Help & Support --
tabHelp: "Help",
@@ -623,7 +638,7 @@ const LANGUAGES = {
helpGen4: "Input tab: inner tabs — Prompt and Image (drag-and-drop upload); Prompt tab has Positive Prompt, Negative Prompt, and an Embeddings selector at the bottom — select a model, set Weight (default 1.0), then click Paste to insert (embedding:Name:weight) at the cursor position of the last focused textarea (defaults to Positive if neither is focused); a ↺ button next to each Apply button reverts the textarea to the node's currently applied value; select text (or just place the cursor on a word/parenthesis block) and press Ctrl+↑/↓ to adjust its emphasis weight in (text:weight) syntax by ±0.05 — the parentheses are removed automatically when the weight returns to 1.0",
helpGen5: "Model tab: Checkpoint, VAE, LoRA (Single/Stack tabs), ControlNet, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — type and device selectors; DualCLIP shows two clip selectors), Hypernetwork (with Strength field) selectors with filter",
helpGen6: "Settings tab: KSampler and Latent Image side by side, each at 50% width",
helpGen7: "Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow",
helpGen7: "Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow; if the loaded workflow has Bypass or Mute nodes, a note listing them appears above the search bar (Bypass nodes are excluded with their wires reconnected around them; Mute nodes are excluded with no reconnection, matching ComfyUI itself)",
helpGen8: "Seed control: random, fixed, increment, decrement",
helpGen9: "Generate, progress display, stop, and view results",
helpGen10: "Eagle integration: auto-save generated images to Eagle",
@@ -658,7 +673,7 @@ const LANGUAGES = {
helpFeederGal5: "__Feeder__ group: reserved group pre-created automatically; cannot be renamed or deleted; add images to it from the Gallery tab using the F overlay button on each thumbnail",
helpFeederGal6: "WFS_GalleryFeeder canvas controls: the node also has Run / Stop / After Gen widgets added directly on the ComfyUI canvas — these work the same as the WFM UI controls and allow running the gallery feeder loop without opening WFM",
helpSearchPlaceholder: "Search help...",
helpPrompt1: "AI assistant (Ollama / LM Studio): model selection, chat for prompt consultation, image attachment (vision models); backend and URL configured in Settings tab",
helpPrompt1: "AI assistant (Ollama / LM Studio / Lemonade): model selection, chat for prompt consultation, image attachment (vision models); backend and URL configured in Settings tab",
helpPrompt2: "Apply AI responses to GenerateUI prompts",
helpPrompt3: "Translation: JA\u2194EN, ZH\u2194EN",
helpPrompt4: "Preset management: create, edit, positive/negative prompt clipboard copy (PP/NP)",
@@ -671,7 +686,7 @@ const LANGUAGES = {
helpSettings2: "Theme selection (13 presets) with customization: color overrides, background patterns (stripes, polka dot, checkerboard, SVG tiling), and font selection (16 Google Fonts including Japanese display fonts)",
helpSettings3: "Workflow data folder path",
helpSettings4: "ComfyUI connection URL and test",
helpSettings5: "AI Assistant (Prompt Tab): backend (Ollama / LM Studio), API URL, default model, and connection test",
helpSettings5: "AI Assistant (Prompt Tab): backend (Ollama / LM Studio / Lemonade), API URL, default model, and connection test",
helpSettings6: "Default workflow setting",
helpSettings7: "Eagle integration URL and auto-save toggle",
helpSettings8: "Data Management: export all plugin data (settings, metadata, prompts, etc.) to a JSON file, or import to restore. After importing, restart ComfyUI.",
@@ -746,15 +761,15 @@ const LANGUAGES = {
helpSidepanel13: "I tab (Information): drop a ComfyUI PNG/WebP/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF, QuadrupleCLIPLoader, and Lora Loader (LoraManager) node types",
helpSidepanel14: "Information tab — drag items to canvas: model sub-tab places the loader node (Checkpoint → CheckpointLoaderSimple, VAE → VAELoader, Diffusion Model → UNETLoader, Text Encoder → CLIPLoader); double-click also places at canvas center",
helpSidepanel15: "Information tab — LoRA sub-tab: detects LoRAs from LoraLoader, LoraLoaderModelOnly, and Lora Loader (LoraManager) nodes (API format supported); drag individual LoRA to place LoraLoader; \"Multiple LORA\" section places all LoRAs in a single Lora Loader (LoraManager) node with LoRA syntax pre-filled; Prompts sub-tab: drag a prompt to place CLIPTextEncode node with text pre-filled; click prompt to view full text and copy",
helpSidepanel16: "A tab (AI Tools): Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama or LM Studio; Chat supports multi-turn conversations; TOOLS includes VLM image analysis and wildcard generation; settings shared with the SPA AI TOOL tab via localStorage",
helpSidepanel16: "A tab (AI Tools): Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama, LM Studio, or Lemonade; Chat supports multi-turn conversations; TOOLS includes VLM image analysis and wildcard generation; settings shared with the SPA AI TOOL tab via localStorage",
helpSidepanel17: "Send to Canvas: clicking 'Send to Canvas' (Workflow tab) or 'Copy & Send Canvas' (Gallery tab) sends the workflow directly to the ComfyUI canvas — both UI and API formats are supported; if Workflow Studio was not opened from the ComfyUI toolbar (window.opener unavailable), UI-format workflows fall back to title drag (title turns blue with a green ●)",
helpAiTitle: "AI Tab (A)",
helpAi1: "4-pane layout — Translation | Chat | TOOLS | Settings — all panes are always visible simultaneously",
helpAi2: "Translation pane: translate between Japanese, English, Chinese, or Free language; ⇄ swap button swaps both language selectors and text content; selections are saved automatically",
helpAi3: "Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history",
helpAi4: "TOOLS pane — VLM: drop an image (PNG/JPG/WebP) and select a task (Describe Image, Create Prompt, Create Tags), click Run; or select \"Create wildcards\", enter a category name and count to generate plain-text entries one per line",
helpAi5: "Settings pane: choose backend (Ollama / LM Studio), set API URL, test connection, select model, configure Free language names for translation",
helpAi6: "Settings are saved to localStorage and shared with the Library panel's A tab",
helpAi3: "Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history; if the model supports tool calling and asks for an image (\"generate a cat picture\", etc.) it will run generation on the workflow currently loaded in GenerateUI and show the result inline; the \"Allow image generation\" checkbox in the pane header toggles this on/off; if an image is attached (dropped in the TOOLS pane) it is sent to the LLM as vision input and, when generate_image is called, used as the base for image-to-image generation instead of text-to-image; models verified to work well for tool calling and vision: Gemma 4:e4b, Qwen 3.5:9b, Ministral 3:3b (Ollama)",
helpAi4: "TOOLS pane — VLM: drop an image (PNG/JPG/WebP) and select a task (Describe Image, Create Prompt, Create Tags), click Run; or select \"Create wildcards\", enter a category name and count to generate plain-text entries one per line; the dropped image is also shared with the Chat pane as its attachment (shown there and used for image-to-image generation) until cleared with the ✕ button",
helpAi5: "Settings pane: choose backend (Ollama / LM Studio / Lemonade), set API URL, test connection, select model, configure Free language names for translation",
helpAi6: "Settings pane: choose backend (Ollama / LM Studio / Lemonade), set API URL, test connection, select model, configure Free language names; Chat Image Generation section lets you enable a dedicated workflow for Chat-triggered text-to-image generation instead of the one currently loaded in GenerateUI; Chat I2I Generation section is the same but for image-to-image generation when an image is attached; settings are shared with the Library panel's A tab via localStorage",
helpImageEditTitle: "Image Edit Tab",
helpImageEdit1: "Layer-based image editor — compose images with multiple layers (Image, Text, Draw, Mask types) and export the composite",
helpImageEdit2: "Loading images: drag & drop onto the canvas, Upload button, or send from the Gallery tab via the Image Edit button in the Gallery toolbar; the first image becomes Layer 1 (auto-locked); subsequent images are added as new layers scaled to fit the canvas",
@@ -841,12 +856,13 @@ const LANGUAGES = {
helpShortcuts4: "Double-click: Open detail modal",
helpTroubleTitle: "Troubleshooting",
helpTrouble1: "Cannot connect to ComfyUI: Check if ComfyUI can generate images, verify URL in Settings, try refreshing or restarting",
helpTrouble2: "Cannot connect to AI assistant: Use the connection test button in Settings tab; for Ollama check it is running and models are downloaded; for LM Studio check the server is running and a model is loaded",
helpTrouble2: "Cannot connect to AI assistant: Use the connection test button in Settings tab; for Ollama check it is running and models are downloaded; for LM Studio or Lemonade check the server is running and a model is loaded",
helpTrouble3: "Workflows not showing: Click refresh button, check if loadable in ComfyUI, try restarting browser/ComfyUI",
helpTrouble4: "Image generation fails: Check if specified models exist in ComfyUI, verify connection, try restarting",
helpTrouble5: "LoRA Stack fails with 'No such file or directory' error: The LoRA may not be recognized by comfyui-lora-manager (known issue). Try a different LoRA or check if the file appears in Lora Manager's own UI",
helpTrouble6: "Model loading error after moving or deleting models: ComfyUI caches file paths and may still reference old locations. Perform a Hard Reset from the ComfyUI menu (Queue → Hard Reset) or restart ComfyUI to clear the cache.",
helpTrouble7: "Tagger tab shows no models: verify the model folder exists at ComfyUI/models/tagger/<model-name>/ and contains a .onnx or .h5 file; if onnxruntime is not installed, run: python_embeded\\python.exe -m pip install onnxruntime",
helpTrouble8: "Model tab shows a checkpoint you didn't expect (e.g. always the same model regardless of the loaded workflow): the checkpoint saved in the workflow file is not installed, so it was silently replaced with another available model during loading — an error toast reports the original (missing) model name when this happens. Select the correct checkpoint manually in the Model tab, or enable \"Default Checkpoint\" in Settings to force a known-good model on every load.",
helpSupportTitle: "Support",
helpSupportDesc: "If you find this plugin useful, please consider supporting its development!",
helpGithubDesc: "Report issues, request features, and contribute",
@@ -870,6 +886,10 @@ const LANGUAGES = {
comfyuiConnection: "ComfyUI Connection",
comfyuiUrl: "ComfyUI URL",
comfyuiUrlHint: "When running as ComfyUI plugin, leave empty to use same origin. Only set if ComfyUI is on a different server.",
defaultCheckpointSettings: "Default Checkpoint",
defaultCheckpointEnable: "Force default checkpoint on workflow load",
defaultCheckpointModel: "Default checkpoint model",
defaultCheckpointHint: "When enabled, the Checkpoint of every checkpoint loader node is overridden with this model whenever a workflow is loaded into GenerateUI — useful to avoid errors or garbled images caused by a mismatched model saved in the workflow file.",
connectedCheck: "Connected \u2713",
failedConnect: "Failed to connect \u2717",
ollamaSettings: "AI Assistant Settings (Prompt Tab)",
@@ -878,6 +898,8 @@ const LANGUAGES = {
selectModel: "Select model...",
noModelsFound: "No models found",
failedLoadModels: "Failed to load models",
modelNotFound: "not found",
modelNotFoundHint: "The model saved in this workflow was not found among the installed models. Please select the correct one from the list.",
saveOllama: "Save AI Assistant Settings",
testConnection: "Test Connection",
ollamaConnected: "Connected \u2713",
@@ -1191,6 +1213,10 @@ const LANGUAGES = {
unknownWorkflowFormat: "不明なワークフロー形式です",
generationComplete: "生成が完了しました",
generationError: (msg) => `生成エラー: ${msg}`,
defaultCheckpointApplied: (name) => `デフォルトCheckpointを適用しました: ${name}`,
checkpointSubstitutedWarning: (names) => `このワークフローのCheckpoint「${names}」が見つからず、無警告で別のモデルに差し替えられました。Modelタブを確認してください。`,
rawJsonBypassNote: (list) => `バイパス中(除外・配線は再接続済み): ${list}`,
rawJsonMutedNote: (list) => `ミュート中(除外): ${list}`,
batchNoneSelected: (what) => `${what}が選択されていません`,
batchItemFailed: (i, total, msg) => `[${i}/${total}] 失敗: ${msg}`,
batchStopped: (completed, failed) => `バッチ停止 — 完了${completed}件、失敗${failed}件`,
@@ -1586,6 +1612,17 @@ const LANGUAGES = {
aiStatusChatting: "考え中...",
aiToastChatFailed: "チャット失敗: ",
aiToastWcNoName: "ワイルドカード名を入力してください",
aiStatusGeneratingImage: "画像を生成中...",
aiChatNoWorkflowLoaded: "GenerateUIにワークフローが読み込まれていません",
aiChatGenerateNotReady: "GenerateUIの準備ができていません",
aiChatNoImageProduced: "生成は完了しましたが画像が出力されませんでした",
aiToastImageGenFailed: "画像生成に失敗しました: ",
aiChatImgGenToggle: "画像生成を許可する",
aiSettingsChatGenTitle: "Chat画像生成",
aiSettingsChatGenDedicated: "専用ワークフローを使用",
aiSettingsChatI2IGenTitle: "Chat I2I生成",
aiChatAttachmentLabel: "画像を添付中",
aiChatNoLoadImageNode: "対象ワークフローにI2I用のLoadImageノードが見つかりません",
// -- Help & Support --
tabHelp: "ヘルプ",
@@ -1607,7 +1644,7 @@ const LANGUAGES = {
helpGen4: "Inputタブ:Prompt・Imageの内部タブ切り替え;PromptタブにはPositive Prompt・Negative Promptに加え最下部にEmbeddingsセレクタを表示(Filter+セレクト+Weight入力+Pasteボタン)— Pasteは最後にフォーカスしたテキストエリアのカーソル位置に(embedding:Name:weight)を挿入(未フォーカス時はPositiveに追加);各Applyボタンの左隣の↺ボタンはテキストエリアをノードの現在の適用値に戻す;テキストを選択(または単語・括弧内にカーソルを置くだけでも可)してCtrl+↑/↓を押すと(text:weight)形式で強調weightを±0.05単位で調整(weightが1.0に戻ると括弧を自動解除)",
helpGen5: "Modelタブ:Checkpoint/VAE/LoRA(Single/Stackタブ)/ControlNet/Diffusion Model(UNETLoader / UnetLoaderGGUF / LoaderGGUF対応)/Text Encoder(CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF対応 — typeおよびdevice選択付き、DualCLIPは2段モデル選択)/Hypernetwork(Strength入力付き)のセレクタ(フィルタ検索付き)",
helpGen6: "Settingsタブ:KSamplerとLatent Imageを横並び(各50%幅)",
helpGen7: "Raw JSONを各タブの右列に常時表示 — 直接編集してApplyでワークフロー即時反映",
helpGen7: "Raw JSONを各タブの右列に常時表示 — 直接編集してApplyでワークフロー即時反映;読み込んだワークフローにBypass/Muteノードがある場合、検索バー上部に一覧が表示される(Bypassは除外され配線が再接続される、Muteは除外されるが再接続はされない点はComfyUI本体と同じ)",
helpGen8: "シード管理:ランダム / 固定 / 増加 / 減少",
helpGen9: "画像生成、進捗表示、生成停止、結果表示",
helpGen10: "Eagle連携:生成画像をEagleへ自動保存",
@@ -1642,7 +1679,7 @@ const LANGUAGES = {
helpFeederGal5: "__Feeder__ グループ:自動で作成される予約グループ;名前変更・削除不可;ギャラリータブの各サムネイル上の F ボタンでこのグループに画像を追加",
helpFeederGal6: "WFS_GalleryFeeder キャンバスコントロール:このノードには ComfyUI キャンバス上に直接 Run / Stop / After Gen ウィジェットが追加されており、WFM を開かずにギャラリーフィーダーループを実行できます",
helpSearchPlaceholder: "ヘルプを検索...",
helpPrompt1: "AIアシスタント(Ollama / LM Studio):モデル選択、チャットでプロンプト相談、画像添付(ビジョンモデル対応);バックエンドとURLは設定タブで設定",
helpPrompt1: "AIアシスタント(Ollama / LM Studio / Lemonade):モデル選択、チャットでプロンプト相談、画像添付(ビジョンモデル対応);バックエンドとURLは設定タブで設定",
helpPrompt2: "AIの応答をGenerateUIのプロンプトに適用",
helpPrompt3: "翻訳機能:日⇔英、中⇔英",
helpPrompt4: "プリセット管理:作成・編集、ポジティブ/ネガティブプロンプトのクリップボードコピー(PP/NP)",
@@ -1655,7 +1692,7 @@ const LANGUAGES = {
helpSettings2: "テーマ選択(13種プリセット)+カスタマイズ:カラー調整、背景パターン(ストライプ、ポルカドット、チェック、SVGタイリング)、フォント選択(日本語デザインフォント含む16種のGoogle Fonts)",
helpSettings3: "ワークフローデータフォルダのパス変更・リセット",
helpSettings4: "ComfyUI接続URL設定と接続テスト",
helpSettings5: "AI Assistant(プロンプトタブ):バックエンド(Ollama / LM Studio)・API URL・デフォルトモデル・接続テスト",
helpSettings5: "AI Assistant(プロンプトタブ):バックエンド(Ollama / LM Studio / Lemonade)・API URL・デフォルトモデル・接続テスト",
helpSettings6: "デフォルトワークフローの設定・解除",
helpSettings7: "Eagle連携URL、自動保存の有効/無効",
helpSettings8: "データ管理:設定・メタデータ・プロンプトなど全データをJSONにエクスポート、またはインポートして復元。インポート後はComfyUIを再起動してください。",
@@ -1734,11 +1771,11 @@ const LANGUAGES = {
helpSidepanel17: "Send to Canvas:WorkflowタブのSend to CanvasまたはGalleryタブのCopy & Send Canvasをクリックするとワークフローをキャンバスへ直接送信(UI形式・API形式どちらも対応);ComfyUIのツールバーから開いていない場合はUI形式のみタイトルドラッグのフォールバック(タイトルが青くなり緑の●が表示される — ドラッグで読み込み)",
helpAiTitle: "AI タブ(A)",
helpAi1: "4ペインレイアウト — Translation | Chat | TOOLS | Settings — 全ペインが常時表示される",
helpAi2: "翻訳ペイン:Ollama または LM Studio を使って日本語・英語・中国語・カスタム言語(Free)間でテキストを翻訳;⇄ ボタンで言語とテキストを同時入替;選択は自動保存",
helpAi3: "Chat ペイン:LLM とのマルチターン会話;Enter で送信、Shift+Enter で改行;Clear ボタンで会話履歴をリセット",
helpAi4: "TOOLS ペイン — VLM:画像(PNG/JPG/WebP)をドロップしてタスク(画像解説 / プロンプト作成 / タグ作成)を選んで Run;または「Create wildcards」を選んでカテゴリ名と件数を入力すると1行1エントリのワイルドカードを生成",
helpAi5: "設定ペイン:バックエンド(Ollama / LM Studio)・API URL・接続テスト・モデル選択・Free 言語名を設定",
helpAi6: "設定は localStorage に保存され、Library パネルの A タブと共有される",
helpAi2: "翻訳ペイン:Ollama、LM Studio、または Lemonade を使って日本語・英語・中国語・カスタム言語(Free)間でテキストを翻訳;⇄ ボタンで言語とテキストを同時入替;選択は自動保存",
helpAi3: "Chat ペイン:LLM とのマルチターン会話;Enter で送信、Shift+Enter で改行;Clear ボタンで会話履歴をリセット;モデルがtool callingに対応している場合、「猫の画像を作って」のように頼むとGenerateUIに読み込み中のワークフローで生成を実行し、結果をチャット内に表示する;ペインヘッダーの「画像生成を許可する」チェックボックスでこの機能のON/OFFを切り替えられる;画像が添付されている場合(TOOLSペインにドロップした画像)はLLMにもビジョン入力として送信され、generate_image呼び出し時はT2Iの代わりにI2I(image-to-image)の元画像として使われる;tool callingとビジョンの動作を確認済みのモデル: Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)",
helpAi4: "TOOLS ペイン — VLM:画像(PNG/JPG/WebP)をドロップしてタスク(画像解説 / プロンプト作成 / タグ作成)を選んで Run;または「Create wildcards」を選んでカテゴリ名と件数を入力すると1行1エントリのワイルドカードを生成;ドロップした画像はChatペインの添付としても共有され(Chat側にも表示、I2I生成に使用)、✕ボタンでクリアするまで保持される",
helpAi5: "設定ペイン:バックエンド(Ollama / LM Studio / Lemonade)・API URL・接続テスト・モデル選択・Free 言語名を設定",
helpAi6: "Settings ペイン:バックエンド(Ollama / LM Studio / Lemonade)の選択、APIのURL設定、接続テスト、モデル選択、Free言語名の設定;Chat Image Generationセクションでは、ChatからのT2I画像生成にGenerateUIの現在の読み込みワークフローの代わりに専用ワークフローを使うよう設定できる;Chat I2I Generationセクションは同様の設定を画像添付時のI2I生成向けに提供する;設定は localStorage に保存され、Library パネルの A タブと共有される",
helpImageEditTitle: "Image Edit タブ",
helpImageEdit1: "レイヤーベースの画像エディタ — 複数のレイヤー(画像・テキスト・描画・マスク)を重ね合わせて画像を合成・エクスポート",
helpImageEdit2: "画像の読み込み:キャンバスへのドラッグ&ドロップ、Uploadボタン、またはGalleryタブのImage Editボタンから送信;最初の画像はLayer 1として自動ロック;以降の画像はキャンバスサイズに合わせてスケールされ新規レイヤーとして追加",
@@ -1824,12 +1861,13 @@ const LANGUAGES = {
helpShortcuts4: "ダブルクリック:詳細モーダルを開く",
helpTroubleTitle: "トラブルシューティング",
helpTrouble1: "ComfyUIに接続できない:ComfyUIで画像生成できるか確認、設定タブでURL確認、ブラウザ更新・再起動",
helpTrouble2: "AIアシスタントに接続できない:設定タブの接続テストボタンで確認;Ollamaの場合は起動とモデルのダウンロードを確認;LM Studioの場合はサーバー起動とモデルのロードを確認",
helpTrouble2: "AIアシスタントに接続できない:設定タブの接続テストボタンで確認;Ollamaの場合は起動とモデルのダウンロードを確認;LM StudioまたはLemonadeの場合はサーバー起動とモデルのロードを確認",
helpTrouble3: "ワークフローが表示されない:更新ボタン押下、ComfyUIで読み込めるか確認、ブラウザ・ComfyUI再起動",
helpTrouble4: "画像生成が失敗する:指定モデルの存在確認、ComfyUI接続確認、再起動",
helpTrouble5: "LoRAスタック実行時に「No such file or directory」エラー:comfyui-lora-managerが該当LoRAを認識していない可能性があります(既知の問題)。別のLoRAを試すか、Lora Manager自体のUIで該当ファイルが表示されるか確認してください",
helpTrouble6: "モデルの移動・削除後にモデル読み込みエラーが発生する:ComfyUIがファイルパスをキャッシュしており、古い場所を参照することがあります。ComfyUIメニューのHard Reset(Queue → Hard Reset)またはComfyUIを再起動してキャッシュをクリアしてください。",
helpTrouble7: "Taggerタブにモデルが表示されない:ComfyUI/models/tagger/<モデル名>/ フォルダの存在と .onnx または .h5 ファイルの有無を確認;onnxruntimeが未インストールの場合は python_embeded\\python.exe -m pip install onnxruntime を実行",
helpTrouble8: "ModelタブのCheckpointが想定と違うモデルになっている(どのワークフローを読み込んでも同じモデルになる等):ワークフローファイルに保存されたCheckpointが実際にはインストールされておらず、読み込み時に無警告で別のインストール済みモデルに差し替えられています。この場合、元の(見つからなかった)モデル名を含むエラートーストが表示されます。Modelタブで正しいCheckpointを手動選択するか、設定タブの「デフォルトCheckpoint」を有効にして、読み込み時に常に動作確認済みのモデルを強制適用してください。",
helpSupportTitle: "サポート",
helpSupportDesc: "このプラグインが役に立ったら、開発を支援していただけると嬉しいです!",
helpGithubDesc: "バグ報告、機能リクエスト、コントリビュート",
@@ -1853,6 +1891,10 @@ const LANGUAGES = {
comfyuiConnection: "ComfyUI 接続",
comfyuiUrl: "ComfyUI URL",
comfyuiUrlHint: "ComfyUIプラグインとして動作する場合は空欄(同一オリジン推奨)。別サーバーの場合のみ設定してください。",
defaultCheckpointSettings: "デフォルトCheckpoint",
defaultCheckpointEnable: "ワークフロー読み込み時にデフォルトCheckpointを強制適用する",
defaultCheckpointModel: "デフォルトCheckpointモデル",
defaultCheckpointHint: "有効にすると、GenerateUIにワークフローを読み込むたびに、すべてのCheckpointローダーノードのモデルがこの値で上書きされます。ワークフローファイルに保存されたモデルが環境と合わず、生成エラーや不鮮明な画像が発生するのを防ぐための設定です。",
connectedCheck: "接続成功 \u2713",
failedConnect: "接続失敗 \u2717",
ollamaSettings: "AI Assistant設定(プロンプトタブ)",
@@ -1861,6 +1903,8 @@ const LANGUAGES = {
selectModel: "モデルを選択...",
noModelsFound: "モデルが見つかりません",
failedLoadModels: "モデル一覧の取得に失敗",
modelNotFound: "見つかりません",
modelNotFoundHint: "このワークフローに保存されているモデルが、インストール済みモデルの中に見つかりませんでした。正しいモデルを一覧から選択してください。",
saveOllama: "AI Assistant設定を保存",
testConnection: "接続テスト",
ollamaConnected: "接続成功 \u2713",
@@ -2174,6 +2218,10 @@ const LANGUAGES = {
unknownWorkflowFormat: "未知的工作流格式",
generationComplete: "生成完成",
generationError: (msg) => `生成错误: ${msg}`,
defaultCheckpointApplied: (name) => `已应用默认 Checkpoint:${name}`,
checkpointSubstitutedWarning: (names) => `此工作流中的 Checkpoint「${names}」未找到,已被静默替换为其他模型。请检查 Model 标签页。`,
rawJsonBypassNote: (list) => `已绕过(排除,已重新连接布线): ${list}`,
rawJsonMutedNote: (list) => `已静音(排除): ${list}`,
batchNoneSelected: (what) => `未选择${what}`,
batchItemFailed: (i, total, msg) => `[${i}/${total}] 失败: ${msg}`,
batchStopped: (completed, failed) => `批处理已停止 — 完成${completed}个,失败${failed}个`,
@@ -2569,6 +2617,17 @@ const LANGUAGES = {
aiStatusChatting: "思考中...",
aiToastChatFailed: "聊天失败:",
aiToastWcNoName: "请输入通配符名称",
aiStatusGeneratingImage: "正在生成图像...",
aiChatNoWorkflowLoaded: "GenerateUI 中未加载工作流",
aiChatGenerateNotReady: "GenerateUI 尚未就绪",
aiChatNoImageProduced: "生成完成但未产生图像",
aiToastImageGenFailed: "图像生成失败:",
aiChatImgGenToggle: "允许生成图像",
aiSettingsChatGenTitle: "Chat 图像生成",
aiSettingsChatGenDedicated: "使用专用工作流",
aiSettingsChatI2IGenTitle: "Chat I2I 生成",
aiChatAttachmentLabel: "已附加图像",
aiChatNoLoadImageNode: "目标工作流中未找到用于图生图的 LoadImage 节点",
// -- Help & Support --
tabHelp: "帮助",
@@ -2590,7 +2649,7 @@ const LANGUAGES = {
helpGen4: "Input标签:含Prompt和Image子标签(拖放上传);Prompt标签显示Positive Prompt、Negative Prompt,以及底部的Embeddings选择器(过滤+下拉+权重+Paste按钮)— Paste将(embedding:Name:weight)插入最后聚焦文本框的光标位置(未聚焦时默认追加到Positive);每个Apply按钮左侧的↺按钮可将文本框恢复为节点当前已应用的值;选中文字(或将光标置于单词/括号内也可)后按Ctrl+↑/↓,即可以(text:weight)语法以±0.05为单位调整强调权重(权重恢复为1.0时自动去除括号)",
helpGen5: "Model标签:Checkpoint/VAE/LoRA(Single/Stack标签)/ControlNet/Diffusion Model(支持UNETLoader / UnetLoaderGGUF / LoaderGGUF)/Text Encoder(支持CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — 含type和device选择器,DualCLIP显示两段模型选择)/Hypernetwork(带Strength输入)选择器(含筛选)",
helpGen6: "Settings标签:KSampler与Latent Image并排显示(各占50%宽度)",
helpGen7: "Raw JSON在每个标签右列常驻显示 — 直接编辑并Apply即时重载工作流",
helpGen7: "Raw JSON在每个标签右列常驻显示 — 直接编辑并Apply即时重载工作流;如果加载的工作流包含Bypass或Mute节点,搜索栏上方会显示列表(Bypass节点会被排除但其连线会重新连接;Mute节点会被排除且不重新连接,与ComfyUI本体行为一致)",
helpGen8: "种子管理:随机/固定/递增/递减",
helpGen9: "生成图片、进度显示、停止生成、查看结果",
helpGen10: "Eagle集成:自动保存生成图片到Eagle",
@@ -2625,7 +2684,7 @@ const LANGUAGES = {
helpFeederGal5: "__Feeder__ 分组:自动预先创建的保留分组;不可重命名或删除;在图库标签页中点击每个缩略图上的 F 按钮可将图片添加到此分组",
helpFeederGal6: "WFS_GalleryFeeder 画布控件:该节点在 ComfyUI 画布上直接添加了 Run / Stop / After Gen 组件——与 WFM 界面中的控件功能相同,无需打开 WFM 即可运行图库供给循环",
helpSearchPlaceholder: "搜索帮助...",
helpPrompt1: "AI助手(Ollama / LM Studio):模型选择、聊天咨询提示词、图片附件(视觉模型);后端和URL在设置标签页配置",
helpPrompt1: "AI助手(Ollama / LM Studio / Lemonade):模型选择、聊天咨询提示词、图片附件(视觉模型);后端和URL在设置标签页配置",
helpPrompt2: "将AI回复应用到生成UI的提示词",
helpPrompt3: "翻译功能:日\u2194英、中\u2194英",
helpPrompt4: "预设管理:创建、编辑、正面/负面提示词剪贴板复制(PP/NP)",
@@ -2638,7 +2697,7 @@ const LANGUAGES = {
helpSettings2: "主题选择(13种预设)+ 自定义:颜色调整、背景图案(条纹、波点、棋盘格、SVG平铺)、字体选择(含日文显示字体在内的16种Google Fonts)",
helpSettings3: "工作流数据文件夹路径更改/重置",
helpSettings4: "ComfyUI连接URL设置和测试",
helpSettings5: "AI助手(提示词标签页):后端(Ollama / LM Studio)、API URL、默认模型、连接测试",
helpSettings5: "AI助手(提示词标签页):后端(Ollama / LM Studio / Lemonade)、API URL、默认模型、连接测试",
helpSettings6: "默认工作流设置/取消",
helpSettings7: "Eagle集成URL、自动保存开关",
helpSettings8: "数据管理:将所有插件数据(设置、元数据、提示词等)导出为JSON文件,或导入以恢复。导入后请重启ComfyUI。",
@@ -2717,11 +2776,11 @@ const LANGUAGES = {
helpSidepanel17: "Send to Canvas:在Workflow标签点击'Send to Canvas'或Gallery标签点击'Copy & Send Canvas',工作流直接发送到ComfyUI画布(支持UI和API所有格式);若Workflow Studio不是从ComfyUI工具栏打开,UI格式工作流将回退到标题拖拽(标题变蓝并显示绿色●——拖拽到画布加载)",
helpAiTitle: "AI 标签页(A)",
helpAi1: "4 面板布局 — Translation | Chat | TOOLS | Settings — 所有面板始终可见",
helpAi2: "翻译面板:使用 Ollama 或 LM Studio 在日语、英语、中文或自定义语言(Free)之间翻译;⇄ 按钮同时互换语言和文本内容;选择自动保存",
helpAi3: "Chat 面板:与 LLM 进行多轮对话;Enter 发送消息,Shift+Enter 换行;Clear 按钮重置对话历史",
helpAi4: "TOOLS 面板 — VLM:拖放图像(PNG/JPG/WebP)并选择任务(图像描述 / 提示词生成 / 标签生成),点击 Run;或选择「Create wildcards」,输入类别名称和数量生成纯文本通配符条目(每行一条)",
helpAi5: "设置面板:选择后端(Ollama / LM Studio)、设置 API URL、测试连接、选择模型、配置 Free 语言名称",
helpAi6: "设置保存至 localStorage,并与 Library 面板的 A 标签共享",
helpAi2: "翻译面板:使用 Ollama、LM Studio 或 Lemonade 在日语、英语、中文或自定义语言(Free)之间翻译;⇄ 按钮同时互换语言和文本内容;选择自动保存",
helpAi3: "Chat 面板:与 LLM 进行多轮对话;Enter 发送消息,Shift+Enter 换行;Clear 按钮重置对话历史;如果模型支持 tool calling,当你要求生成图像(例如\"生成一张猫的图片\")时,会使用 GenerateUI 中当前加载的工作流执行生成,并在对话中显示结果;面板标题栏的\"允许生成图像\"复选框可开关此功能;如果附加了图像(在 TOOLS 面板中拖放的图像),该图像会作为视觉输入发送给 LLM,调用 generate_image 时会以该图像为基础执行图生图(I2I),而不是文生图;已验证可正常支持 tool calling 与视觉功能的模型:Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)",
helpAi4: "TOOLS 面板 — VLM:拖放图像(PNG/JPG/WebP)并选择任务(图像描述 / 提示词生成 / 标签生成),点击 Run;或选择「Create wildcards」,输入类别名称和数量生成纯文本通配符条目(每行一条);拖放的图像也会作为 Chat 面板的附件共享(在该处显示并用于图生图生成),直到点击 ✕ 按钮清除",
helpAi5: "设置面板:选择后端(Ollama / LM Studio / Lemonade)、设置 API URL、测试连接、选择模型、配置 Free 语言名称",
helpAi6: "Settings 面板:选择后端(Ollama / LM Studio / Lemonade)、设置 API URL、测试连接、选择模型、配置 Free 语言名称;Chat 图像生成部分可让 Chat 触发的文生图使用专用工作流,而不是 GenerateUI 中当前加载的工作流;Chat I2I 生成部分提供相同设置,用于附加图像时的图生图生成;设置保存至 localStorage,并与 Library 面板的 A 标签共享",
helpImageEditTitle: "图像编辑选项卡",
helpImageEdit1: "基于图层的图像编辑器 — 通过多个图层(图像、文字、绘制、遮罩类型)合成图像并导出",
helpImageEdit2: "加载图像:拖放到画布、点击Upload按钮、或通过Gallery标签工具栏的Image Edit按钮发送;第一张图像成为图层1(自动锁定);后续图像缩放至画布大小作为新图层添加",
@@ -2807,12 +2866,13 @@ const LANGUAGES = {
helpShortcuts4: "双击:打开详情弹窗",
helpTroubleTitle: "故障排除",
helpTrouble1: "无法连接ComfyUI:确认ComfyUI能否生成图片、在设置中验证URL、尝试刷新或重启",
helpTrouble2: "无法连接AI助手:使用设置标签页的连接测试按钮;Ollama需确认已启动且模型已下载;LM Studio需确认服务器已启动且模型已加载",
helpTrouble2: "无法连接AI助手:使用设置标签页的连接测试按钮;Ollama需确认已启动且模型已下载;LM Studio或Lemonade需确认服务器已启动且模型已加载",
helpTrouble3: "工作流不显示:点击刷新按钮、确认能在ComfyUI中加载、尝试重启浏览器/ComfyUI",
helpTrouble4: "图片生成失败:确认指定模型存在、验证ComfyUI连接、尝试重启",
helpTrouble5: "LoRA Stack出现「No such file or directory」错误:该LoRA可能未被comfyui-lora-manager识别(已知问题)。请尝试其他LoRA,或检查该文件是否显示在Lora Manager自身的界面中",
helpTrouble6: "移动或删除模型后出现模型加载错误:ComfyUI会缓存文件路径,可能仍引用旧位置。请通过ComfyUI菜单执行Hard Reset(Queue → Hard Reset)或重启ComfyUI以清除缓存。",
helpTrouble7: "Tagger标签不显示模型:确认ComfyUI/models/tagger/<模型名>/文件夹存在且包含.onnx或.h5文件;如未安装onnxruntime,请运行:python_embeded\\python.exe -m pip install onnxruntime",
helpTrouble8: "Model 标签页显示的 Checkpoint 与预期不符(例如无论加载哪个工作流都显示相同模型):工作流文件中保存的 Checkpoint 实际未安装,加载时已被静默替换为其他已安装模型,此时会弹出包含原始(未找到)模型名称的错误提示。请在 Model 标签页手动选择正确的 Checkpoint,或在设置标签页启用\"默认 Checkpoint\",在每次加载时强制应用一个已验证可用的模型。",
helpSupportTitle: "支持",
helpSupportDesc: "如果您觉得这个插件有用,请考虑支持它的开发!",
helpGithubDesc: "报告问题、请求功能和贡献代码",
@@ -2836,6 +2896,10 @@ const LANGUAGES = {
comfyuiConnection: "ComfyUI 连接",
comfyuiUrl: "ComfyUI URL",
comfyuiUrlHint: "作为ComfyUI插件运行时留空(推荐同源)。仅在不同服务器时设置。",
defaultCheckpointSettings: "默认 Checkpoint",
defaultCheckpointEnable: "加载工作流时强制应用默认 Checkpoint",
defaultCheckpointModel: "默认 Checkpoint 模型",
defaultCheckpointHint: "启用后,每次将工作流加载到 GenerateUI 时,所有 Checkpoint 加载节点的模型都会被覆盖为此值,用于避免工作流文件中保存的模型与当前环境不匹配导致生成错误或图像模糊。",
connectedCheck: "已连接 \u2713",
failedConnect: "连接失败 \u2717",
ollamaSettings: "AI助手设置(提示词标签页)",
@@ -2844,6 +2908,8 @@ const LANGUAGES = {
selectModel: "选择模型...",
noModelsFound: "未找到模型",
failedLoadModels: "获取模型列表失败",
modelNotFound: "未找到",
modelNotFoundHint: "此工作流中保存的模型在已安装的模型列表中未找到,请从列表中选择正确的模型。",
saveOllama: "保存AI助手设置",
testConnection: "测试连接",
ollamaConnected: "已连接 \u2713",
+108 -37
View File
@@ -118,6 +118,8 @@ class ImageEditTab {
this._inpaintDenoise = null; // null = 未初期化(ワークフローの現在値から初期化)
this._inpaintRunning = false;
this._inpaintResultUrl = null;
// Select I2I(Comic Creator連携、マスク不要のI2I外部実行)
this._i2iExternalRunning = false;
// Inpaint 専用ワークフロー(OFF時は GenerateUI で読み込み中のワークフローを使用)
this._inpaintUseDedicated = false;
this._inpaintDedicatedFilename = null;
@@ -1360,10 +1362,11 @@ class ImageEditTab {
if (this._inpaintRunning) return;
if (!this._layerMgr) { showToast("No image loaded", "error"); return; }
const maskLayer = (this._layerMgr.activeLayer?.type === "mask")
// 非表示のマスクレイヤーはInpaint対象から除外する(目玉アイコンOFF=対象外)
const maskLayer = (this._layerMgr.activeLayer?.type === "mask" && this._layerMgr.activeLayer.visible)
? this._layerMgr.activeLayer
: this._layerMgr.layers.find(l => l.type === "mask");
if (!maskLayer) { showToast("Create a mask layer first (Mask tool)", "info"); return; }
: this._layerMgr.layers.find(l => l.type === "mask" && l.visible);
if (!maskLayer) { showToast("Create or show a mask layer first (Mask tool)", "info"); return; }
if (this._inpaintUseDedicated) {
if (!this._inpaintDedicatedFilename) { showToast("Select a dedicated workflow first", "info"); return; }
@@ -1383,8 +1386,7 @@ class ImageEditTab {
try {
// 専用ワークフロー使用時は GenerateUI の comfyUI.currentWorkflow/currentAnalysis には
// 一切触れず、ここでロード・解析したローカルなワークフロー/解析結果だけを使う。
let targetWorkflow = null;
let targetAnalysis = null;
let overrideOpts = {};
if (this._inpaintUseDedicated) {
const resp = await fetch(`${comfyUI.baseUrl}/api/wfm/workflows/raw?filename=${encodeURIComponent(this._inpaintDedicatedFilename)}`);
@@ -1396,17 +1398,7 @@ class ImageEditTab {
} else if (format !== "api") {
throw new Error("Unsupported or unrecognized workflow format");
}
targetWorkflow = dedicatedWorkflow;
targetAnalysis = comfyWorkflow.analyzeWorkflow(dedicatedWorkflow);
}
// LoadImage(の MASK 出力が使われている)スロットを優先し、無ければ Mask Editor One
// ノード(LoadImage を介さず画像/マスクを内包する custom node)にフォールバックする
const analysisForLookup = targetAnalysis || comfyUI.currentAnalysis;
const slotIndex = (analysisForLookup.load_image_nodes || []).findIndex(n => n.mask_used);
const meoNode = (analysisForLookup.mask_editor_one_nodes || [])[0];
if (slotIndex === -1 && !meoNode) {
throw new Error("No inpaint-capable LoadImage or Mask Editor One node found in the workflow");
overrideOpts = { workflow: dedicatedWorkflow, analysis: comfyWorkflow.analyzeWorkflow(dedicatedWorkflow) };
}
setStatus("Compositing...");
@@ -1415,30 +1407,15 @@ class ImageEditTab {
const maskCanvas = this._exportMaskCanvas(maskLayer);
const maskBlob = await new Promise((resolve) => maskCanvas.toBlob(resolve, "image/png"));
const overrideOpts = targetWorkflow ? { workflow: targetWorkflow, analysis: targetAnalysis } : {};
setStatus("Uploading...");
if (slotIndex !== -1) {
await comfyEditor.applyImageAndMaskToSlot(imageBlob, maskBlob, slotIndex, overrideOpts);
} else {
await comfyEditor.applyImageAndMaskToMaskEditorOneNode(imageBlob, maskBlob, meoNode.id, overrideOpts);
}
comfyEditor.setPromptText("positive", this._inpaintPositive, overrideOpts);
comfyEditor.setPromptText("negative", this._inpaintNegative, overrideOpts);
comfyEditor.setInpaintParams({
const resultUrl = await this._runInpaintWithImages(imageBlob, maskBlob, {
positive: this._inpaintPositive,
negative: this._inpaintNegative,
growMaskBy: this._inpaintGrowMaskBy,
denoise: this._inpaintDenoise,
...overrideOpts,
overrideOpts,
onStatus: setStatus,
});
if (!window._wfmGenerateTab?.generate) throw new Error("GenerateUI is not ready yet");
setStatus("Generating...");
await window._wfmGenerateTab.generate(targetWorkflow || undefined);
const resultImg = document.getElementById("wfm-gen-result-img");
if (resultImg?.src) this._inpaintResultUrl = resultImg.src;
if (resultUrl) this._inpaintResultUrl = resultUrl;
setStatus("Done");
showToast("Inpaint generation complete", "success");
@@ -1451,6 +1428,100 @@ class ImageEditTab {
}
}
// 共通処理: 画像+マスクBlobを対象ノード(LoadImage/MaskEditorOne)へ反映し、
// プロンプト/grow_mask_by/denoiseを設定してGenerate UIで実行、結果URLを返す。
// UI駆動の _runInpaint() と外部(Comic Creator等)向けの runInpaintExternal() の両方から使う。
async _runInpaintWithImages(imageBlob, maskBlob, { positive, negative, growMaskBy, denoise, overrideOpts = {}, onStatus } = {}) {
const setStatus = onStatus || (() => {});
// LoadImage(の MASK 出力が使われている)スロットを優先し、無ければ Mask Editor One
// ノード(LoadImage を介さず画像/マスクを内包する custom node)にフォールバックする
const analysisForLookup = overrideOpts.analysis || comfyUI.currentAnalysis;
if (!analysisForLookup) throw new Error("No workflow loaded in GenerateUI");
const slotIndex = (analysisForLookup.load_image_nodes || []).findIndex(n => n.mask_used);
const meoNode = (analysisForLookup.mask_editor_one_nodes || [])[0];
if (slotIndex === -1 && !meoNode) {
throw new Error("No inpaint-capable LoadImage or Mask Editor One node found in the workflow");
}
setStatus("Uploading...");
if (slotIndex !== -1) {
await comfyEditor.applyImageAndMaskToSlot(imageBlob, maskBlob, slotIndex, overrideOpts);
} else {
await comfyEditor.applyImageAndMaskToMaskEditorOneNode(imageBlob, maskBlob, meoNode.id, overrideOpts);
}
comfyEditor.setPromptText("positive", positive, overrideOpts);
comfyEditor.setPromptText("negative", negative, overrideOpts);
comfyEditor.setInpaintParams({ growMaskBy, denoise, ...overrideOpts });
if (!window._wfmGenerateTab?.generate) throw new Error("GenerateUI is not ready yet");
setStatus("Generating...");
await window._wfmGenerateTab.generate(overrideOpts.workflow || undefined);
const resultImg = document.getElementById("wfm-gen-result-img");
return resultImg?.src || null;
}
// Comic Creator等、同一オリジンiframe越しの外部呼び出し専用エントリポイント。
// 専用ワークフロー選択(_inpaintUseDedicated)は扱わず、常にGenerate UIに現在
// ロード中のワークフロー(comfyUI.currentWorkflow/currentAnalysis)を対象にする。
async runInpaintExternal(imageBlob, maskBlob, { positive = "", negative = "", growMaskBy = 6, denoise = 1.0 } = {}) {
if (this._inpaintRunning) throw new Error("Inpaint is already running");
this._inpaintRunning = true;
try {
const resultUrl = await this._runInpaintWithImages(imageBlob, maskBlob, { positive, negative, growMaskBy, denoise });
if (!resultUrl) throw new Error("No result image produced");
return { ok: true, url: resultUrl };
} finally {
this._inpaintRunning = false;
}
}
// 共通処理: 画像Blob(マスクなし)を対象LoadImageノード(slot 0)へ反映し、
// プロンプト/denoiseを設定してGenerate UIで実行、結果URLを返す。
// _runInpaintWithImages のマスクなし版(Comic CreatorのSelect I2I連携専用)。
async _runI2IWithImage(imageBlob, { positive, negative, denoise, overrideOpts = {}, onStatus } = {}) {
const setStatus = onStatus || (() => {});
const analysisForLookup = overrideOpts.analysis || comfyUI.currentAnalysis;
if (!analysisForLookup) throw new Error("No workflow loaded in GenerateUI");
if (!(analysisForLookup.load_image_nodes?.length > 0)) {
throw new Error("No LoadImage node found in the workflow");
}
setStatus("Uploading...");
const file = new File([imageBlob], `i2i_${Math.random().toString(36).slice(2, 10)}.png`, { type: "image/png" });
await comfyEditor.applyImageToSlot(file, 0, overrideOpts);
comfyEditor.setPromptText("positive", positive, overrideOpts);
comfyEditor.setPromptText("negative", negative, overrideOpts);
comfyEditor.setInpaintParams({ denoise, ...overrideOpts });
if (!window._wfmGenerateTab?.generate) throw new Error("GenerateUI is not ready yet");
setStatus("Generating...");
await window._wfmGenerateTab.generate(overrideOpts.workflow || undefined);
const resultImg = document.getElementById("wfm-gen-result-img");
return resultImg?.src || null;
}
// Comic Creator等、同一オリジンiframe越しの外部呼び出し専用エントリポイント(I2I版)。
// runInpaintExternal と対になる、マスク不要のシンプルなI2I実行。
async runI2IExternal(imageBlob, { positive = "", negative = "", denoise = 1.0 } = {}) {
if (this._i2iExternalRunning) throw new Error("I2I is already running");
this._i2iExternalRunning = true;
try {
const resultUrl = await this._runI2IWithImage(imageBlob, { positive, negative, denoise });
if (!resultUrl) throw new Error("No result image produced");
return { ok: true, url: resultUrl };
} finally {
this._i2iExternalRunning = false;
}
}
_renderMaskLayerOverlay(ctx, maskLayer) {
// アクティブなマスクレイヤーのみオーバーレイ表示(非アクティブなマスクは隠す)
if (this._layerMgr?.activeLayer !== maskLayer) return;
+3 -3
View File
@@ -4,7 +4,7 @@
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { readJsonStorage } from "./util.js";
import { readJsonStorage, getAiBackendDefaultUrl } from "./util.js";
// ============================================
// State
@@ -390,7 +390,7 @@ let _promptAiBackendForModels = null;
function getPromptAiConfig() {
const s = loadPromptAiSettings();
const backend = s.backend || "ollama";
const url = (s.backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234")).replace(/\/$/, "");
const url = (s.backendUrl || getAiBackendDefaultUrl(backend)).replace(/\/$/, "");
return { backend, url };
}
@@ -418,7 +418,7 @@ async function chatWithAi(model, messages) {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).message?.content || "";
} else {
// LM Studio: convert Ollama-style messages (with .images) to OpenAI content arrays
// LM Studio / Lemonade (OpenAI-compatible): convert Ollama-style messages (with .images) to OpenAI content arrays
const openAiMessages = messages.map(msg => {
if (msg.images?.length) {
return {
+62 -6
View File
@@ -6,7 +6,7 @@ import { showToast } from "./app.js";
import { comfyUI } from "./comfyui-client.js";
import { t, getLang, getSummaryLang, setLang, setSummaryLang, getLanguageOptions, getSummaryLanguageOptions } from "./i18n.js";
import { getSettings, readJsonStorage } from "./util.js";
import { getSettings, readJsonStorage, getAiBackendDefaultUrl } from "./util.js";
const SETTINGS_KEY = "wfm_settings";
@@ -637,6 +637,30 @@ export async function initSettingsTab() {
<div id="wfm-settings-url-status" style="font-size:12px;margin-top:4px;"></div>
</details>
<!-- Default Checkpoint -->
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("defaultCheckpointSettings")}</summary>
<div class="wfm-form-group">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="wfm-settings-default-ckpt-enabled" ${serverSettings.default_checkpoint_enabled ? "checked" : ""}>
<span id="wfm-settings-default-ckpt-enable-label">${t("defaultCheckpointEnable")}</span>
</label>
</div>
<div class="wfm-form-group">
<label id="wfm-settings-default-ckpt-model-label">${t("defaultCheckpointModel")}</label>
<div style="display:flex;gap:8px;">
<select class="wfm-select" id="wfm-settings-default-ckpt-select" style="flex:1;">
<option value="">${t("selectModel")}</option>
</select>
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-default-ckpt-refresh">${t("refresh")}</button>
</div>
<small id="wfm-settings-default-ckpt-hint" style="color:var(--wfm-text-secondary);font-size:11px;display:block;margin-top:4px;">
${t("defaultCheckpointHint")}
</small>
</div>
<button class="wfm-btn wfm-btn-primary wfm-btn-sm" id="wfm-settings-default-ckpt-save">${t("save")}</button>
</details>
<!-- AI Assistant Settings (Prompt Tab) -->
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("ollamaSettings")}</summary>
@@ -649,6 +673,9 @@ export async function initSettingsTab() {
<label class="wfm-ai-radio-label">
<input type="radio" name="wfm-prompt-ai-backend" value="lmstudio"> LM Studio
</label>
<label class="wfm-ai-radio-label">
<input type="radio" name="wfm-prompt-ai-backend" value="lemonade"> Lemonade
</label>
</div>
</div>
<div class="wfm-form-group">
@@ -1101,14 +1128,14 @@ export async function initSettingsTab() {
const promptAiUrlEl = document.getElementById("wfm-settings-prompt-ai-url");
if (promptAiUrlEl) {
promptAiUrlEl.value = promptAiSaved.backendUrl || "";
promptAiUrlEl.placeholder = promptAiBackendSaved === "ollama" ? "http://localhost:11434" : "http://localhost:1234";
promptAiUrlEl.placeholder = getAiBackendDefaultUrl(promptAiBackendSaved);
}
// Backend change → update URL placeholder
// Backend change → switch URL to the new backend's default
document.querySelectorAll('input[name="wfm-prompt-ai-backend"]').forEach(r => {
r.addEventListener("change", () => {
const backend = document.querySelector('input[name="wfm-prompt-ai-backend"]:checked')?.value || "ollama";
if (promptAiUrlEl) promptAiUrlEl.placeholder = backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234";
if (promptAiUrlEl) promptAiUrlEl.value = getAiBackendDefaultUrl(backend);
});
});
@@ -1117,7 +1144,7 @@ export async function initSettingsTab() {
const select = document.getElementById("wfm-settings-prompt-ai-model");
if (!select) return;
const backend = document.querySelector('input[name="wfm-prompt-ai-backend"]:checked')?.value || "ollama";
const url = (promptAiUrlEl?.value.trim() || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234")).replace(/\/$/, "");
const url = (promptAiUrlEl?.value.trim() || getAiBackendDefaultUrl(backend)).replace(/\/$/, "");
try {
let models = [];
if (backend === "ollama") {
@@ -1142,7 +1169,7 @@ export async function initSettingsTab() {
document.getElementById("wfm-settings-prompt-ai-test")?.addEventListener("click", async () => {
const statusEl = document.getElementById("wfm-settings-prompt-ai-status");
const backend = document.querySelector('input[name="wfm-prompt-ai-backend"]:checked')?.value || "ollama";
const url = (promptAiUrlEl?.value.trim() || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234")).replace(/\/$/, "");
const url = (promptAiUrlEl?.value.trim() || getAiBackendDefaultUrl(backend)).replace(/\/$/, "");
if (statusEl) { statusEl.textContent = t("aiStatusConnecting"); statusEl.style.color = "var(--wfm-text-secondary)"; }
try {
let count = 0;
@@ -1210,6 +1237,35 @@ export async function initSettingsTab() {
statusEl.style.color = ok ? "var(--wfm-success)" : "var(--wfm-danger)";
});
// Default Checkpoint
const defaultCkptSelect = document.getElementById("wfm-settings-default-ckpt-select");
async function loadDefaultCkptOptions() {
if (!defaultCkptSelect) return;
try {
const list = await comfyUI.fetchCheckpoints();
const saved = serverSettings.default_checkpoint_name || "";
defaultCkptSelect.innerHTML = `<option value="">${t("selectModel")}</option>` +
(list || []).map((m) => `<option value="${m}" ${m === saved ? "selected" : ""}>${m}</option>`).join("");
} catch { /* ignore */ }
}
loadDefaultCkptOptions();
document.getElementById("wfm-settings-default-ckpt-refresh")?.addEventListener("click", loadDefaultCkptOptions);
document.getElementById("wfm-settings-default-ckpt-save")?.addEventListener("click", async () => {
const enabled = !!document.getElementById("wfm-settings-default-ckpt-enabled")?.checked;
const ckptName = defaultCkptSelect?.value || "";
try {
await saveServerSettings({ default_checkpoint_enabled: enabled, default_checkpoint_name: ckptName });
serverSettings.default_checkpoint_enabled = enabled;
serverSettings.default_checkpoint_name = ckptName;
showToast(t("aiToastSettingsSaved"), "success");
} catch (err) {
showToast(`${t("saveError")}: ${err.message}`, "error");
}
});
// Test Eagle URL (via server proxy to avoid CORS)
document.getElementById("wfm-settings-test-eagle")?.addEventListener("click", async () => {
const urlInput = document.getElementById("wfm-settings-eagle-url");
+11
View File
@@ -27,6 +27,17 @@ export function getSettings() {
return readJsonStorage("wfm_settings");
}
// AIバックエンド(Ollama/LM Studio/Lemonade)ごとのデフォルトURL
export const AI_BACKEND_DEFAULT_URLS = {
ollama: "http://localhost:11434",
lmstudio: "http://localhost:1234",
lemonade: "http://localhost:13305",
};
export function getAiBackendDefaultUrl(backend) {
return AI_BACKEND_DEFAULT_URLS[backend] || AI_BACKEND_DEFAULT_URLS.ollama;
}
/**
* 検索inputにオーバーレイXボタンを設定する。
* @param {string} inputId - 検索inputのID
+49 -6
View File
@@ -751,6 +751,7 @@
<span class="wfm-gen-rawjson-label">Raw JSON</span>
<button id="wfm-gen-apply-raw-btn" class="wfm-btn wfm-btn-sm">Apply</button>
</div>
<div id="wfm-gen-rawjson-bypass-note" class="wfm-gen-rawjson-bypass-note" style="display:none;"></div>
<div class="wfm-rawjson-search">
<input type="text" id="wfm-gen-raw-search" class="wfm-rawjson-search-input" placeholder="Search...">
<span id="wfm-gen-raw-search-count" class="wfm-rawjson-search-count"></span>
@@ -1091,7 +1092,7 @@
<li id="wfm-help-gen-4">Input tab: inner tabs — <b>Prompt</b> and <b>Image</b> (drag-and-drop upload); Prompt tab has Positive Prompt, Negative Prompt, and an <b>Embeddings</b> selector at the bottom — select a model, set Weight (default 1.0), then click <b>Paste</b> to insert <code>(embedding:Name:weight)</code> at the cursor position of the last focused textarea (defaults to Positive if neither is focused); a <b>↺</b> button next to each Apply button reverts the textarea to the node's currently applied value; select text (or just place the cursor on a word/parenthesis block) and press <b>Ctrl+↑/↓</b> to adjust its emphasis weight in <code>(text:weight)</code> syntax by ±0.05 — the parentheses are removed automatically when the weight returns to 1.0</li>
<li id="wfm-help-gen-5">Model tab: Checkpoint, VAE, LoRA (Single/Stack tabs), ControlNet, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — type and device selectors; DualCLIP shows two clip selectors), Hypernetwork (with Strength field) selectors with filter; LoRA Stack — Apply writes all active LoRAs to the target node and simultaneously inserts LoRA syntax and CivitAI trigger words into the Positive prompt; re-applying updates the prompt by removing inactive LoRAs and re-adding active ones</li>
<li id="wfm-help-gen-6">Settings tab: KSampler and Latent Image side by side, each at 50% width</li>
<li id="wfm-help-gen-7">Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow</li>
<li id="wfm-help-gen-7">Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow; if the loaded workflow has Bypass or Mute nodes, a note listing them appears above the search bar (Bypass nodes are excluded with their wires reconnected around them; Mute nodes are excluded with no reconnection, matching ComfyUI itself)</li>
<li id="wfm-help-gen-8">Seed control: random, fixed, increment, decrement — seed input and mode selector stacked vertically for readability</li>
<li id="wfm-help-gen-9">Generate, progress display, stop, and view results</li>
<li id="wfm-help-gen-10">Eagle integration: auto-save generated images to Eagle</li>
@@ -1271,7 +1272,7 @@
<li id="wfm-help-sidepanel-13">I tab (Information): drop a ComfyUI PNG/WebP/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF and QuadrupleCLIPLoader node types</li>
<li id="wfm-help-sidepanel-14">Information tab — drag items to canvas: model sub-tab places the loader node (Checkpoint → CheckpointLoaderSimple, VAE → VAELoader, Diffusion Model → UNETLoader, Text Encoder → CLIPLoader); double-click also places at canvas center</li>
<li id="wfm-help-sidepanel-15">Information tab — LoRA sub-tab: drag individual LoRA to place LoraLoader; "Multiple LORA" section at bottom places all LoRAs in a single Lora Loader (LoraManager) node; Prompts sub-tab: drag a prompt to place CLIPTextEncode node with text pre-filled; click prompt to view full text and copy</li>
<li id="wfm-help-sidepanel-16">AI TOOL tab: Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama or LM Studio; Chat supports multi-turn conversations; TOOLS includes VLM image analysis and wildcard generation; settings shared with the SPA AI TOOL tab via localStorage</li>
<li id="wfm-help-sidepanel-16">AI TOOL tab: Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama, LM Studio, or Lemonade; Chat supports multi-turn conversations; TOOLS includes VLM image analysis and wildcard generation; settings shared with the SPA AI TOOL tab via localStorage</li>
<li id="wfm-help-sidepanel-17">Title drag (Send to Canvas): after clicking 'Send to Canvas' (Workflow tab) or 'Copy &amp; Send Canvas' (Gallery tab), the 'Workflow Studio Library' title turns blue with a green ● — drag it onto the canvas to load the workflow; title resets automatically after loading</li>
</ul>
</div>
@@ -1282,11 +1283,11 @@
<h3 id="wfm-help-ai-title">AI TOOL Tab</h3>
<ul>
<li id="wfm-help-ai-1">4-pane layout — Translation | Chat | TOOLS | Settings — all panes are always visible simultaneously</li>
<li id="wfm-help-ai-2">Translation pane: translate text between Japanese, English, Chinese, or a custom Free language using Ollama or LM Studio; ⇄ swap button swaps both language selectors and text content; selections are saved automatically</li>
<li id="wfm-help-ai-3">Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history</li>
<li id="wfm-help-ai-4">TOOLS pane — VLM: drop an image (PNG/JPG/WebP) and select a task (Describe Image, Create Prompt, Create Tags), then click Run to analyze with a vision model; result shown with Copy button</li>
<li id="wfm-help-ai-2">Translation pane: translate text between Japanese, English, Chinese, or a custom Free language using Ollama, LM Studio, or Lemonade; ⇄ swap button swaps both language selectors and text content; selections are saved automatically</li>
<li id="wfm-help-ai-3">Chat pane: multi-turn conversation with the LLM; Enter to send a message, Shift+Enter for a newline; Clear button resets the conversation history; if the model supports tool calling and asks for an image ("generate a cat picture", etc.) it will run generation on the workflow currently loaded in GenerateUI and show the result inline; the "Allow image generation" checkbox in the pane header toggles this on/off; if an image is attached (dropped in the TOOLS pane) it is sent to the LLM as vision input and, when generate_image is called, used as the base for image-to-image generation instead of text-to-image; models verified to work well for tool calling and vision: Gemma 4:e4b, Qwen 3.5:9b, Ministral 3:3b (Ollama)</li>
<li id="wfm-help-ai-4">TOOLS pane — VLM: drop an image (PNG/JPG/WebP) and select a task (Describe Image, Create Prompt, Create Tags), then click Run to analyze with a vision model; result shown with Copy button; the dropped image is also shared with the Chat pane as its attachment (shown there and used for image-to-image generation) until cleared with the ✕ button</li>
<li id="wfm-help-ai-5">TOOLS pane — Wildcards: select "Create wildcards" from the dropdown, enter a category name and count, click Run to generate plain-text wildcard entries one per line</li>
<li id="wfm-help-ai-6">Settings pane: choose backend (Ollama / LM Studio), set API URL, test connection, select model, configure Free language names; settings are shared with the Library panel's A tab via localStorage</li>
<li id="wfm-help-ai-6">Settings pane: choose backend (Ollama / LM Studio / Lemonade), set API URL, test connection, select model, configure Free language names; Chat Image Generation section lets you enable a dedicated workflow for Chat-triggered text-to-image generation instead of the one currently loaded in GenerateUI; Chat I2I Generation section is the same but for image-to-image generation when an image is attached; settings are shared with the Library panel's A tab via localStorage</li>
</ul>
</div>
</div>
@@ -1468,6 +1469,7 @@
<li id="wfm-help-trouble-5">LoRA Stack fails with "No such file or directory" error: The LoRA may not be recognized by comfyui-lora-manager (known issue). Try a different LoRA or check if the file appears in Lora Manager's own UI</li>
<li id="wfm-help-trouble-6">Model loading error after moving or deleting models: ComfyUI caches file paths and may still reference old locations. Perform a Hard Reset from the ComfyUI menu (Queue → Hard Reset) or restart ComfyUI to clear the cache.</li>
<li id="wfm-help-trouble-7"></li>
<li id="wfm-help-trouble-8"></li>
</ul>
</div>
</div>
@@ -2002,11 +2004,20 @@
<div class="wfm-ai-chat-container">
<div class="wfm-ai-chat-messages" id="wfm-ai-chat-messages"></div>
<div class="wfm-ai-chat-input-area">
<div id="wfm-ai-chat-attachment" class="wfm-ai-chat-attachment" style="display:none;">
<img id="wfm-ai-chat-attachment-thumb" class="wfm-ai-chat-attachment-thumb">
<span id="wfm-ai-chat-attachment-label">Image attached</span>
<button id="wfm-ai-chat-attachment-clear" class="wfm-ai-chat-attachment-clear" title="Close">✕</button>
</div>
<textarea id="wfm-ai-chat-input" class="wfm-ai-chat-input" placeholder="Type a message... (Enter to send, Shift+Enter for newline)"></textarea>
<div class="wfm-ai-chat-actions">
<button id="wfm-ai-chat-send-btn" class="wfm-btn wfm-btn-primary">Send</button>
<button id="wfm-ai-chat-clear-btn" class="wfm-btn">Clear</button>
<span id="wfm-ai-chat-status" class="wfm-ai-trans-status"></span>
<label class="wfm-ai-chat-toggle-label" for="wfm-ai-chat-imggen-toggle">
<input type="checkbox" id="wfm-ai-chat-imggen-toggle" checked>
<span id="wfm-ai-chat-imggen-label-text">Allow image generation</span>
</label>
</div>
</div>
</div>
@@ -2020,6 +2031,7 @@
<img id="wfm-ai-vlm-preview" style="display:none;max-width:100%;max-height:100%;object-fit:contain;pointer-events:none;">
<span id="wfm-ai-vlm-label" class="wfm-ai-vlm-label">Drop or click PNG / JPG / WebP</span>
<input type="file" id="wfm-ai-vlm-file" accept="image/png,image/jpeg,image/webp" style="display:none;">
<button id="wfm-ai-vlm-clear" class="wfm-ai-vlm-clear" style="display:none;" title="Close">✕</button>
</div>
<div class="wfm-ai-wc-inputs" id="wfm-ai-wc-inputs" style="display:none">
<div class="wfm-ai-wc-row">
@@ -2059,6 +2071,9 @@
<label class="wfm-ai-radio-label">
<input type="radio" name="wfm-ai-backend" value="lmstudio"> LM Studio
</label>
<label class="wfm-ai-radio-label">
<input type="radio" name="wfm-ai-backend" value="lemonade"> Lemonade
</label>
</div>
</div>
<div class="wfm-ai-settings-section">
@@ -2092,6 +2107,34 @@
<input type="text" id="wfm-ai-free-dst-lang" class="wfm-input" placeholder="e.g. German">
</div>
</div>
<div class="wfm-ai-settings-section">
<div class="wfm-ai-settings-title" id="wfm-ai-settings-chatgen-title">Chat Image Generation</div>
<div class="wfm-ai-settings-row">
<label class="wfm-ai-label" style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="wfm-ai-chatgen-dedicated-checkbox">
<span id="wfm-ai-chatgen-dedicated-label">Use dedicated workflow</span>
</label>
</div>
<div class="wfm-ai-settings-row" id="wfm-ai-chatgen-dedicated-row" style="display:none;">
<select id="wfm-ai-chatgen-dedicated-select" class="wfm-select">
<option value="">-- select workflow --</option>
</select>
</div>
</div>
<div class="wfm-ai-settings-section">
<div class="wfm-ai-settings-title" id="wfm-ai-settings-chatgen-i2i-title">Chat I2I Generation</div>
<div class="wfm-ai-settings-row">
<label class="wfm-ai-label" style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="wfm-ai-chatgen-i2i-dedicated-checkbox">
<span id="wfm-ai-chatgen-i2i-dedicated-label">Use dedicated workflow</span>
</label>
</div>
<div class="wfm-ai-settings-row" id="wfm-ai-chatgen-i2i-dedicated-row" style="display:none;">
<select id="wfm-ai-chatgen-i2i-dedicated-select" class="wfm-select">
<option value="">-- select workflow --</option>
</select>
</div>
</div>
<div class="wfm-ai-settings-section">
<button id="wfm-ai-settings-save-btn" class="wfm-btn wfm-btn-primary">Save</button>
</div>
+19 -3
View File
@@ -3003,6 +3003,14 @@ const renderInfoTab = (container) => {
const AI_SETTINGS_KEY = "wfm_ai_settings";
const AI_LANG_NAMES = { ja: "Japanese", en: "English", zh: "Chinese" };
const AI_BACKEND_DEFAULT_URLS = {
ollama: "http://localhost:11434",
lmstudio: "http://localhost:1234",
lemonade: "http://localhost:13305",
};
function getAiBackendDefaultUrl(backend) {
return AI_BACKEND_DEFAULT_URLS[backend] || AI_BACKEND_DEFAULT_URLS.ollama;
}
function isValidAiUrl(url) {
try {
@@ -3184,6 +3192,7 @@ const renderAiTab = (container) => {
<div class="wfm-nlp-ai-radio-row">
<label class="wfm-nlp-ai-radio"><input type="radio" name="wfm-nlp-ai-backend" value="ollama"> Ollama</label>
<label class="wfm-nlp-ai-radio"><input type="radio" name="wfm-nlp-ai-backend" value="lmstudio"> LM Studio</label>
<label class="wfm-nlp-ai-radio"><input type="radio" name="wfm-nlp-ai-backend" value="lemonade"> Lemonade</label>
</div>
</div>
<div class="wfm-nlp-ai-sec">
@@ -3244,6 +3253,13 @@ const setupAiHandlers = (container) => {
if (urlInput && cfg.backendUrl) urlInput.value = cfg.backendUrl;
else if (urlInput) urlInput.value = "http://localhost:11434";
// Backend change → switch URL to the new backend's default
backendRadios.forEach(r => {
r.addEventListener("change", () => {
if (urlInput) urlInput.value = getAiBackendDefaultUrl(r.value);
});
});
const freeSrcInput = container.querySelector("#wfm-nlp-ai-free-src");
const freeDstInput = container.querySelector("#wfm-nlp-ai-free-dst");
if (freeSrcInput && cfg.freeSrcLang) freeSrcInput.value = cfg.freeSrcLang;
@@ -3273,7 +3289,7 @@ const setupAiHandlers = (container) => {
const c = loadAiCfg();
const backend = c.backend || "ollama";
const url = c.backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
const url = c.backendUrl || getAiBackendDefaultUrl(backend);
const model = c.model;
if (!isValidAiUrl(url)) { showToast("URLは http:// または https:// で始まる必要があります", "error"); return; }
if (!model) { showToast("Please select a model in Settings", "error"); return; }
@@ -3403,7 +3419,7 @@ const setupAiHandlers = (container) => {
if (!text) return;
const c = loadAiCfg();
const backend = c.backend || "ollama";
const url = c.backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
const url = c.backendUrl || getAiBackendDefaultUrl(backend);
const model = c.model;
if (!isValidAiUrl(url)) { showToast("URLは http:// または https:// で始まる必要があります", "error"); return; }
if (!model) { showToast("Please select a model in Settings", "error"); return; }
@@ -3497,7 +3513,7 @@ const setupAiHandlers = (container) => {
const task = vlmTaskSel?.value || "describe";
const c = loadAiCfg();
const backend = c.backend || "ollama";
const url = c.backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
const url = c.backendUrl || getAiBackendDefaultUrl(backend);
const model = c.model;
if (!isValidAiUrl(url)) { showToast("URLは http:// または https:// で始まる必要があります", "error"); return; }
if (!model) { showToast("Please select a model in Settings", "error"); return; }