Compare commits
@@ -194,9 +194,8 @@ os.environ["HF_HOME"] = "D:\\hf_cache"
|
||||
|
||||
1. `pyproject.toml` の `version` を新バージョン番号に更新してステージングに含める
|
||||
2. `README.md` のバージョンバッジを更新(``)
|
||||
3. `README.md` の Changelog に新バージョンのエントリを追加
|
||||
4. `DEVLOG.md` の先頭に変更詳細エントリを追加
|
||||
5. 上記すべてを同一コミットにまとめてプッシュ
|
||||
6. `gh release create vX.Y.Z` でリリースを作成
|
||||
3. `DEVLOG.md` の先頭に変更詳細エントリを追加
|
||||
4. 上記すべてを同一コミットにまとめてプッシュ
|
||||
5. `gh release create vX.Y.Z` でリリースを作成(リリースノート本文に変更点を記載する — v0.3.90でREADME.mdのChangelogセクションは廃止したため、以後のバージョン履歴はGitHub Releaseのリリースノートのみで管理する)
|
||||
|
||||
`pyproject.toml` のバージョン変更が GitHub Actions のトリガー(`paths: pyproject.toml`)になっており、ComfyUI Registry への自動公開もここで行われる。バージョン更新を忘れると Registry に反映されない。
|
||||
|
||||
@@ -2,6 +2,329 @@
|
||||
|
||||
---
|
||||
|
||||
## v0.3.91
|
||||
|
||||
### 機能拡張: Labインデックス画像にPlanメタデータをPNG埋め込み+README構成整理(Changelogセクション廃止)
|
||||
|
||||
v0.3.90でLabタブに追加した「Run時インデックス画像Output保存」機能について、ユーザーから「インデックス画像にPlanのメタデータは埋め込まれていないのでは」と確認を受け調査したところ、実際に埋め込まれていないことが判明。`_buildIndexImageDataUrl()`は`canvas.toDataURL("image/png")`でピクセルデータのみのプレーンなPNGを生成しており、バックエンド側(`lab_plan_service.py`の`save_index_image()`/`save_index_image_to_output()`)も受け取ったbase64をそのままバイト列として書き込むだけだった。特にOutputフォルダ側のコピーは対になる`.json`が存在しないため、画像単体では「どのPlanで生成したか」を後から追跡する手段が一切なかった。「Planドロップエリアにドロップして読み込める形にしたい」という要望を受け対応。
|
||||
|
||||
**PNG埋め込みの実装**: `static/js/util.js`に`embedPngTextChunk(dataUrl, keyword, text)`を新規追加。PNGのIHDR直後に**iTXt**チャンクを挿入する(挿入位置・CRC計算ロジックは、ComfyUI本体が既に持つワークフロー埋め込み機能=`web/comfyui/top_menu_extension.js`の`embedWorkflowInPng`と同じ方式を踏襲)。**tEXtではなくiTXtを選んだ理由**: このコードベースの既存PNGチャンク読み取り(`metadata-tab.js`の`readAllPNGTextChunks`)は、tEXtのtextフィールドをLatin-1でデコードする実装になっている。一方書き込み側(`embedWorkflowInPng`含む)は`TextEncoder().encode()`でUTF-8エンコードしているため、非ASCII文字(日本語のNoteやプロンプトなど)を含むテキストをtEXt経由で埋め込むと、読み取り時にLatin-1誤変換で文字化けする。iTXtチャンクは仕様上UTF-8前提であり、`readAllPNGTextChunks`の`parseITXtChunk`は既に正しくUTF-8デコードを行っているため、新規キー`wfm_lab_plan`はiTXtで統一した(既存のtEXt/workflow埋め込みが同じ問題を抱えている可能性があるが、それは既存機能でありスコープ外として手を付けていない)。
|
||||
|
||||
**読み書きの配線**: `metadata-tab.js`の`readAllPNGTextChunks`を新たに`export`(元は非export、Lab側から再利用するため)。`lab-tab.js`に`LAB_PLAN_PNG_KEY = "wfm_lab_plan"`を追加し、`_buildIndexImageDataUrl(planData)`が省略可能引数`planData`を受け取ってJSON文字列化・埋め込むよう変更。呼び出し元の`_savePlan()`と`_maybeSaveIndexImageOnRun()`双方で、既に構築済みの`_buildPlanData()`の結果を渡すようにした——Plan Save側(lab_plan/内、対になる.jsonが既にある)にも同じ埋め込みを行い、画像単体を持ち出しても再現可能にする一貫性を優先。`_loadPlanFromIndexImage(file)`は、まず`readAllPNGTextChunks(file)`でチャンクを直接読み`wfm_lab_plan`があれば`JSON.parse`して`_applyPlanData()`へ渡す(サーバー通信不要)よう変更し、埋め込みが無い古いインデックス画像に対してのみ従来の「同名.jsonをサーバーから取得」にフォールバックする。
|
||||
|
||||
**検証**: (1) Node単体テストでembed→PNGバイト解析→JSON.parseの往復を検証(日本語+絵文字を含む文字列が完全一致することを確認)。(2) Kaptureで実際にComfyUIを操作し、T2Iモード+Save index on Run+日本語/絵文字入りNoteでRunを実行、実際に生成された`Lab_index_00003_.png`の実バイトを別のNodeスクリプトで直接読み取り、埋め込まれた`wfm_lab_plan`チャンクの内容が実際のPlan状態と完全一致することを確認。(3) ブラウザの`evaluate`経由で`fetch`+`File`+`DataTransfer`+合成`drop`イベントを構築し、サーバー側に対応する`.json`が一切存在しないこのOutputフォルダPNGを実際にPlanドロップゾーンへドロップ、T2Iモード・Save index on Runチェックボックス・Note(日本語+絵文字)・Batch数・ワークフロー記録(Open Workflowボタン)が全てサーバー通信なしで正しく復元されることをスクリーンショットで確認済み。
|
||||
|
||||
**README構成整理**: README.mdの`## Changelog`セクション(v0.1.1〜v0.3.90まで692行)を全削除。バージョンを重ねるごとに肥大化しており、GitHub Releaseのリリースノートと内容が重複していたため、ユーザーの意向で「今後はGitHub Releaseのリリースノートのみでバージョン履歴を管理する」方針に変更。あわせて`CLAUDE.md`のリリース手順から「READMEのChangelogにエントリを追加」の手順を削除し、代わりに`gh release create`のリリースノート本文に変更点を記載する旨を明記した。README.mdは692行分の削減(1248行→556行)。
|
||||
|
||||
**How to apply**: (1) 今後PNGに新しいカスタムメタデータを埋め込みたい場合は、必ずtEXtではなく**iTXt**を使うこと(このコードベースのtEXt読み取りはLatin-1decodeのため非ASCII文字が壊れる)。`util.js`の`embedPngTextChunk(dataUrl, keyword, text)`と`metadata-tab.js`の`readAllPNGTextChunks(file)`(export済み)がその書き込み/読み取りのペア。(2) リリース時のバージョン履歴はREADME.mdではなく`gh release create`のリリースノートに記載する(README.mdにChangelogセクションを復活させない)。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.90
|
||||
|
||||
### 機能拡張: Labタブの進化(ワークフロー呼び出し・T2I対応・Run時インデックス画像Output保存+Eagle連携・プラン接頭辞)+Galleryタブのルート表示簡略化
|
||||
|
||||
v0.3.87〜v0.3.89で実装したLabタブ(GenerateUI)に対する、実際の運用を通じたユーザーからの一連の追加要望をまとめて対応。
|
||||
|
||||
**1. Plan JSONへのワークフロー記録+呼び出し(Open Workflowボタン)**: これまでLabのPlanには「どのワークフローに対して」実行したキーフレーム設定なのかが記録されておらず、後日Planを見返しても再現できない問題があった。`_buildPlanData()`(`lab-tab.js`)が`#wfm-gen-wf-name`の`dataset.filename`(GenerateUIに現在ロード中のワークフローファイル名)を読み取り`workflow_filename`としてPlan JSONに保存するようにし、Plan読込後にPLANボックス内へ表示される**Open Workflow**ボタンから、その記録されたワークフローファイルを`/api/wfm/workflows/raw`経由で取得し`loadWorkflowIntoEditor()`へ渡してGenerateUIへ読み込み直せるようにした。自動読込はせず、常に`window.confirm`で確認を挟む(現在編集中のワークフローを不意に上書きしない安全策、ユーザー指定)。`loadWorkflowIntoEditor`は`generate-tab.js`にあり、`generate-tab.js`は既に`lab-tab.js`から`initLabTab`をimportしているため、静的importすると循環import(`generate-tab.js`→`lab-tab.js`→`generate-tab.js`)になる。v0.3.87で`getEagleSettings`/`saveToEagle`を`util.js`へ移設して回避した前例があるが、`loadWorkflowIntoEditor`はgenerate-tab.js固有の依存が多く移設は非現実的なため、今回はボタンのクリックハンドラ内で`const { loadWorkflowIntoEditor } = await import("./generate-tab.js");`と**動的import**する方式を採用(`gallery-tab.js`が`tagger-tab.js`に対して既に使っている前例パターンを踏襲)。
|
||||
|
||||
**2. T2Iワークフロー対応**: LabはもともとI2Iバッチ生成専用で、ソース画像が無いと`_runLabBatch()`が`labNoSourceImage`エラーで実行をブロックしていた。Imageペイン最上部に**T2I workflow (no source image)**チェックボックスを追加し、ON時は(a) ソース画像必須チェックをスキップ、(b) `_buildWorkflowForIteration()`でLoadImageノードへの書き込みを完全にスキップ(`imageRef = null`)、(c) Imageドロップゾーンと「Use generated image for next」をグレーアウト(既存の画像は削除せず、I2Iに戻したときのため保持)。Resultsパネルのソース画像欄には代わりに「T2I workflow (no source image)」の注記を表示。設定は`t2i_mode`としてPlan JSONに保存。実機確認では、実際にT2Iワークフロー(`EmptyLatentImage`使用、LoadImageノード無し)で画像生成に成功し、Eagle自動保存も正常動作することを確認した(検証中、テスト対象ワークフローのCheckpointが存在しないモデル名だったことによる`/prompt` 400エラーに遭遇したが、これはT2I機能自体のバグではなくテスト環境のモデル設定の問題だった)。
|
||||
|
||||
**3. Run時にインデックス画像をOutputフォルダへ保存+Eagle連携**: 右ペインに**Save index image to Output on Run**チェックボックスを追加。ONの場合、Run完了ごとに`_buildIndexImageDataUrl()`で作った9枚コンタクトシートを新設バックエンドAPI `POST /api/wfm/lab/index-image/save-to-output`へ送信し、`py/services/lab_plan_service.py`の`save_index_image_to_output()`が`folder_paths.get_output_directory()` + `folder_paths.get_save_image_path("Lab_index", output_dir)`を使って、通常の生成画像と同じ自動採番規則で`Lab_index_00001_.png`のように保存する(上書きしない)。Plan Save側の動作(`lab_plan/`への保存)は完全に独立したまま変更していない。続けて「インデックス画像もEagleに保存したい」という追加要望を受け、Output保存が成功した後に既存のグローバル設定(Settingsタブの`eagleAutoSave`、生成画像のEagle保存と同じ仕組み)を流用して`saveToEagle()`を呼ぶだけの実装を追加——Lab専用の別トグルは増やさず、既存パターンにそのまま乗せた。設定は`save_index_on_run`としてPlan JSONに保存。バックエンドのルート登録はComfyUI起動時の1回のみのため、この機能だけはPythonファイル変更を伴い**ComfyUI再起動後に実機確認**(Outputフォルダへの`Lab_index_00001_.png`保存、コンソールの`[Eagle] Saved: Lab_index_00002_.png`ログの両方を確認)。StabilityMatrix環境では`custom_nodes/.../output`が実体`Images/Text2Img`へのsymlinkになっている点に留意(`folder_paths.get_output_directory()`はsymlink越しに正しく解決される)。
|
||||
|
||||
**4. プランファイル名への`ws_labplan_`接頭辞自動付与**: `_savePlan()`(Plan Save/Save As共通処理)で、プロンプトに入力された名前から`_stripLabPlanPrefix()`で既存の接頭辞を除去してから`ws_labplan_`を付け直す方式にし、二重接頭辞を防止(ユーザーが手動で接頭辞込みの名前を入力しても安全)。JSON内部の`"name"`フィールドは接頭辞なしのプレーンな名前のまま(表示用と実ファイル名を分離)。Plan JSONサブタブのプレビュー(`_syncLabJsonView`)も同じ接頭辞除去ロジックで統一。
|
||||
|
||||
**5. Galleryタブ — ルートフォルダ表示の簡略化**: フォルダツリーのルートエントリが実際のディレクトリ名(例: StabilityMatrix環境の`Text2Img` — outputフォルダが`Images/Text2Img`へのsymlinkになっているため)をそのまま付けて「[root] Text2Img」と表示していたが、ルートにはT2I以外の画像(Lab_indexなど各種生成物)も含まれるため実態と合わず紛らわしいという指摘を受け、`gallery-tab.js`の`renderTreeNode()`で単に「[root]」とだけ表示するよう変更(`label.textContent = isRoot ? "[root]" : node.name;`)。
|
||||
|
||||
**README構成の見直し**: Featuresセクションが17個の`### <タブ名>`セクションで非常に長くなっていたため、GitHub Flavored MarkdownのHTML `<details><summary>`要素で各タブセクションを折りたたみ可能にした(見出しレベルは`<summary><h3>...</h3></summary>`として維持)。Changelog等の他セクションは対象外(ユーザー指定は「タブの項目ごと」=Featuresセクションのみ)。
|
||||
|
||||
**検証**: 4機能とも実機(Kapture経由でComfyUIの`http://127.0.0.1:8189/wfm`を直接操作)で確認済み。ワークフロー呼び出しはPlan保存直後にファイル名とボタンが表示されること・クリックで確認ダイアログ→再読込を確認(当初Save経路でUI更新関数`_updatePlanWorkflowUI()`を呼び忘れる不具合があり、実機確認で発覚・修正)。T2Iモードは実際の画像生成成功、Output保存はComfyUI再起動後に`Lab_index_00001_.png`の生成、Eagle連携はコンソールログとファイル両方で確認、Galleryルート表示はスクリーンショットで確認。いずれもテスト用に生成したファイルは検証後に削除(Output画像の削除はユーザー側で実施)。
|
||||
|
||||
**How to apply**: (1) Lab関連でstateを更新する処理(Save/Load/Clear)を追加・変更する際は、対応するUI更新関数を**全ての状態変更経路**で呼び忘れていないか確認すること — 今回まさにSave経路で呼び忘れて実機確認で発覚した。(2) `lab-tab.js`から`generate-tab.js`の関数を使いたい場合、`util.js`への機能移設が大げさすぎるなら動的import(`await import("./generate-tab.js")`)を検討する。(3) 新しいLabのRun時副作用(Output保存等)を追加する際は、可能な限り既存のグローバル設定(Eagle自動保存など)に乗せ、機能ごとに新しいトグルを増やさないこと。(4) StabilityMatrix環境のようにComfyUIのoutputフォルダがsymlinkになっている場合でも`folder_paths.get_output_directory()`は正しく解決されるが、`ls -lt`等で直接調査する際はsymlinkの実体を辿る必要がある。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.89
|
||||
|
||||
### バグ修正: v0.3.88で追加したLab機能への実動作確認フィードバック3件を反映
|
||||
|
||||
v0.3.88で追加したLabタブのPromptセル拡張機能について、実際に触ったユーザーから3件の修正依頼を受けた。
|
||||
|
||||
**1. Get from Imageが任意のファイルを選ばせる仕様だった**: 当初はファイル選択ダイアログで別途画像を選ぶ実装だったが、「Labタブで現在読み込んでいる画像から取得したい」という指摘を受け、`_lab.sourceImageFilename`(IMAGE欄に既にドロップ・アップロード済みの画像)を`/view?filename=...&type=input`経由で取得し`File`オブジェクトへ変換した上で`extractAllMetadata()`にかける方式に変更。ファイル選択ダイアログ(隠し`<input type="file">`)は不要になったため削除。画像未読込の場合は既存の`labNoSourceImage`トーストで案内する。
|
||||
|
||||
**2. スタイル適用が`{prompt}`プレースホルダを置換していた**: `generate-tab.js`の`_applyNamedStyle()`と同じ「スタイルテンプレートの`{prompt}`を既存テキストで置換する」ロジックを流用していたが、「既存のプロンプトはクリアしないで追加したい。既存があればカンマ区切り、なければそのまま」という明確な要望を受け、`_applyStyleToText()`をプレースホルダ置換なしの単純追記(`既存 + ", " + style.prompt`、既存が空ならstyle.promptをそのまま)に書き換えた。GenerateUI本来のStyle機能(`_applyNamedStyle`)とは意図的に異なる仕様になったため、コード上にその旨のコメントを残した。
|
||||
|
||||
**3. GenerateUIタブ Modelサブタブの各フィルターにクリアボタンがなかった**: Checkpoint/VAE/Diffusion Model/ControlNet/Hypernetwork(`comfyui-editor.js`の`renderModelTab`、ループでフィルター入力を動的生成)とLora Single(`renderLoraPane`)のフィルター入力に、Models/Nodes/Workflow/Galleryタブの検索ボックスやLabタブのCheckpoint/VAEフィルターと同じ`setupSearchClearBtn()`(`util.js`)+`.wfm-search-wrap`/`.wfm-search-clear-btn`パターンでオーバーレイ✕クリアボタンを追加。`renderModelTab`は複数フィルターをループ生成するため、各入力に`id="wfm-model-${s.key}-filter"`/クリアボタンに`id="wfm-model-${s.key}-filter-clear"`を採番し、フィルター再構築処理を`input`イベントと`setupSearchClearBtn`のコールバックで共有する形にした。
|
||||
|
||||
**あわせてヘルプを更新**: Labサブタブのヘルプ(`helpLab8`、index.html+i18n.js日英中)を新しい挙動(Get from Imageの取得元、スタイルの追記方式)に合わせて修正。
|
||||
|
||||
**検証**: Kaptureで実ブラウザ確認 — GenerateUI Modelタブの各フィルター(Checkpoint/Lora)で入力→✕クリア→リスト復元、Labタブで画像未読込時のGet from Imageのエラートースト、既存Positiveテキストへのスタイル追記結果(`{prompt}`が置換されずそのまま末尾に追加され、既存テキストが先頭に保持されることを確認)。いずれもコンソールエラーなし(既存の無関係なOllama接続エラーを除く)。
|
||||
|
||||
**How to apply**: 新機能を実装した直後の「実際に触ってみた」フィードバックは、当初の設計判断(本実装ではEagle本来のUI流用や`{prompt}`置換の踏襲)を覆す具体的な仕様変更を伴うことが多い。既存の類似機能(ここでは`generate-tab.js`の`_applyNamedStyle`)のロジックをそのまま流用するのではなく、要望に応じて意図的に別実装へ分岐させてよい場面があることを踏まえ、分岐させた箇所には理由をコメントで残すこと。フィルター入力を動的ループで生成するコード(`renderModelTab`等)にクリアボタンを追加する際は、ループ内で一意なIDを採番し`setupSearchClearBtn`をループ内で呼び出す。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.88
|
||||
|
||||
### 機能拡張: Labタブ Promptセルにスタイル適用・画像/GenerateUIからのプロンプト取得、Checkpoint/VAEフィルター、Plan JSONサブタブを追加
|
||||
|
||||
v0.3.87で追加したLabタブ(GenerateUI)に対するユーザーからの一連の改善要望をまとめて対応。
|
||||
|
||||
**Promptセル編集モーダルの拡張**:
|
||||
- **Get from GenerateUI**: 現在GenerateUIに読み込まれているワークフローの`comfyUI.currentAnalysis.prompt_nodes`からPositive/Negativeを直接取得
|
||||
- **Get from Image**: 画像/ワークフローJSONをドロップ/選択すると、Metadataタブの抽出ロジック(`metadata-tab.js`の`extractAllMetadata()`。新たに`export`して再利用)でプロンプトを抽出
|
||||
- **Style適用**: 当初はモーダル内に独自のスタイルドロップダウンを設けたが、ユーザーから「GenerateUIツールバー上部の`#wfm-style-enabled`チェックボックス/`#wfm-style-select`ドロップダウンをそのまま使えないか」と指摘を受け設計変更。モーダル内の選択UIを廃し、Applyボタンが常にツールバー側の現在値を読んで適用する方式にした(チェックボックスOFFまたは未選択ならエラートースト)。ボタンラベルにも選択中のスタイル名を動的表示(例:「Apply: cinematic-diva」)。プロンプトへのマージ処理自体は`generate-tab.js`の`_applyNamedStyle()`と同じ置換ルール(`{prompt}`プレースホルダ置換/ネガティブ追記)を、ワークフローではなく文字列に対して行う`_applyStyleToText()`として実装
|
||||
|
||||
**Checkpoint/VAEセルにフィルター機能追加**:
|
||||
- GenerateUIのModelサブタブ(`comfyui-editor.js`の`renderModelTab`)と同じ「Filter...」テキスト入力で`<select>`の候補を絞り込み
|
||||
- 続けて「オーバーレイ✕でクリア可能にしたい、Modelタブの各フィルタリング機能・Raw JSON検索と同様に」という要望を受け、Models/Nodes/Workflow/Gallery各タブの検索ボックスやRaw JSON検索と同じ`setupSearchClearBtn()`(`util.js`)+`.wfm-search-wrap`/`.wfm-search-clear-btn`(`main.css`)パターンを流用し、入力欄内に✕クリアボタンをオーバーレイ表示するよう変更
|
||||
|
||||
**Plan JSONサブタブ追加**:
|
||||
- Setting/Resultsに続く3つ目のサブタブとして新設。現在のLab状態(列のキーフレーム・Note・Batch数・画像連鎖設定・元画像・結果一覧)を、プランファイル保存時と同じ構造のJSONとして`json-highlight.js`の`syncJsonHighlight()`で構文ハイライト表示する
|
||||
- **Refresh**: Settingタブの現在値で再同期(未適用の編集は破棄)
|
||||
- **Apply to Setting**: テキストエリアの内容を`JSON.parse`し、Plan Load(`_loadPlanFromFile`/`_loadPlanFromServer`)と共通の`_applyPlanData(filename, data)`を呼んでSettingタブへ反映。不正なJSONの場合は`JSON.parse`の例外メッセージをそのままエラートーストに出し、状態は変更しない
|
||||
- 保存用のプランオブジェクト構築ロジックが`_savePlan()`内にインライン実装されていたのを`_buildPlanData(name)`として切り出し、Save・Plan JSON表示の双方から共用するようリファクタリング
|
||||
|
||||
**あわせてヘルプを更新**: Labサブタブのヘルプページ(`index.html`+`i18n.js`日英中+`app.js`の`helpIdMap`)に、Promptモーダルの追加ボタン群とPlan JSONサブタブの説明を追記。
|
||||
|
||||
**検証**: 各機能をKaptureで実ブラウザ確認 — Get from GenerateUI/Get from Imageでの実際のプロンプト取得、Style Apply(トップバーOFF時のエラートースト・ON時の`{prompt}`置換とネガティブ追記)、Checkpoint/VAEフィルターの絞り込みと✕クリア、Plan JSONの表示→編集→Apply→Setting反映、および不正JSON入力時のエラーハンドリング。いずれもコンソールエラーなし(既存の無関係なOllama接続エラーを除く)。
|
||||
|
||||
**How to apply**: 複数モジュールで使う汎用UI部品(`setupSearchClearBtn`、`extractAllMetadata`等)は積極的にexportして使い回すこと。「モーダル内に専用UIを新設する」より「既存のツールバー等の状態をそのまま参照する」方がユーザーにとって一貫性があり実装も軽い場合があるため、要望の意図が既存UIの流用にあるかどうかを都度確認する。プラン保存構造とJSONビュー表示は同じビルダー関数(`_buildPlanData`)を共有させ、二重管理を避ける。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.87
|
||||
|
||||
### 追加機能: GenerateUIタブに実験的な「Lab」サブタブを追加(I2Iバッチ生成・キーフレーム方式)
|
||||
|
||||
「I2Iのバッチ生成を、回数ごとにCheckpoint/VAE/Prompt/KSamplerを個別に変更しながら実行したい」という依頼を受け、GenerateUIの新しいサブタブとして実装(ブランチ`feature/lab-tab-i2i-batch`)。既存のBatchサブタブが「1軸を単純に流すバッチ」なのに対し、Labは「列(Checkpoint/VAE/Prompt/KSampler)ごとに独立したキーフレームで、何回目からどう変わるか」を指定できる点が異なる。
|
||||
|
||||
**データモデルと解決ロジック**: 各列は行グリッドを共有せず、`{atIteration, value, revertToBase}`のキーフレーム配列を個別に持つ。反復回数Nでの実効値は「N以下で最大のatIterationのキーフレーム」を採用し、次のキーフレームまで引き継ぐ。`revertToBase`は1回目の値(空なら読み込み中ワークフローの既定値)を再利用する。1回目のキーフレームは削除不可で、空欄なら常にワークフロー本来の設定を使う。**Labはどの列も`comfyUI.currentWorkflow`を直接書き換えず、実行時にディープクローンした上でのみ値を適用する**ため、Input/Model/Settings/Batchサブタブの編集状態と干渉しない。
|
||||
|
||||
**バックエンド**: `py/services/lab_plan_service.py`(`wildcard_service.py`のディレクトリCRUDパターンを踏襲)+`py/routes/lab_routes.py`を新設し、`GET/POST /api/wfm/lab/plans*`を`py/wfm.py`に登録。プランは`user/default/Workflow-Studio/lab_plan/<name>.json`として保存し、結果画像がある場合は同stemの`<name>.png`(最大9枚・3列のコンタクトシート、Canvasで生成しbase64で送信)を`workflow_service.py`の`change_thumbnail`と同様のサムネイル対のパターンで自動保存する。
|
||||
|
||||
**フロントエンド**: `static/js/lab-tab.js`(新規、`feeder-tab.js`と同じ「GenerateUIサブタブモジュール」パターン)を`generate-tab.js`から`initLabTab()`として呼び出し、サブタブ切替時に共通右ペイン(Generate/Seed/Batch状況パネル)を`display:none`にする分岐を追加。Setting/Resultsの2画面構成で、Setting画面は「画像ドロップ+プラン読込ドロップゾーン(左)/4列キーフレーム(中央)/Run・Batch数・Note・Plan Save(右)」、Results画面は「元画像(左)/生成結果グリッド最大9枚(中央)」。
|
||||
|
||||
**実装後にユーザーからの追加要望で反映した変更点**:
|
||||
|
||||
- キーフレームセルの表示を、2回目以降は「値/`Applied: N`」の2行表示に変更(1回目のみ従来の1行表示のまま)
|
||||
- 生成した各画像をEagle自動保存に対応(設定タブの`eagleAutoSave`を尊重)。`getEagleSettings`/`saveToEagle`は元々`generate-tab.js`にのみ定義されていたが、`lab-tab.js`からの循環import(`generate-tab.js`→`lab-tab.js`→`generate-tab.js`)を避けるため`static/js/util.js`へ移設し、両ファイルからimportする形に整理
|
||||
- Plan Saveの下に「Save As」ボタンを追加。Plan Saveは読込済みプランがあれば無言で上書き、Save Asはプラン読込中でも常に新しい名前をプロンプトで確認する
|
||||
- 「Auto」チェックボックス(実行完了時の自動保存)を削除 — インデックス画像の自動生成があるため不要と判断
|
||||
- 「Use generated image for next」チェックボックスを追加。ONの場合、2回目以降は直前の反復で生成された1枚目の画像をI2I入力として使う連鎖生成に対応。再アップロードは行わず、ComfyUIのLoadImageが解釈できる`"name [type]"`(`folder_paths.get_annotated_filepath`が対応する記法。サブフォルダがあれば`"subfolder/name [type]"`)形式の参照をそのまま`image`ウィジェットに設定する方式を採用
|
||||
- インデックス画像の最大枚数を10→9、グリッド列数を5→3(3×3相当)に変更
|
||||
- Plan Loadを、独自のサムネイル一覧モーダルから、IMAGEドロップゾーンと同じ見た目のドラッグ&ドロップ+クリック選択に変更。`.json`をドロップした場合はブラウザ内で直接読み込み(サーバー不要)、`.png`(インデックス画像)をドロップした場合は同じstemの`.json`をサーバーから取得する。ブラウザの仕様上Web page側からファイル選択ダイアログの初期フォルダを指定することはできないため、初回のみユーザーが`lab_plan`フォルダまで手動で移動する必要がある旨をヘルプに明記
|
||||
|
||||
**ヘルプ更新**: 既存の3点セット規約(`index.html`のヘルプページ+`i18n.js`の日英中3言語+`app.js`の`applyI18nToHtml`内`helpIdMap`)に沿って、サイドバーに「Lab subtab」ページを新設(Feederページと同じ構成: 概要+カード3枚 - 画像と列/実行とプランファイル/Results)。
|
||||
|
||||
**検証**: 各段階でKaptureによる実ブラウザE2E確認を実施 — サブタブ切替時の右ペイン非表示化、4列の+/-とセル編集モーダル(1回目/2回目以降の項目差)、Plan Save(無言上書き)とSave As(常にプロンプト表示)の挙動差、Plan Loadドロップゾーンのクリック/ドラッグ配線、ヘルプページの表示とスクロール、各段階でコンソールエラーが出ていないことを確認。実際のRun実行によるバッチ生成そのものはユーザー自身の環境で確認済み。
|
||||
|
||||
**How to apply**: GenerateUIサブタブを新設する際は`feeder-tab.js`のモジュールパターン(`export function initXxxTab()`を`generate-tab.js`から呼び出し、`moveRawJsonToTab()`の特殊タブ判定に追加)を踏襲する。サブタブがGenerateUI共通の右ペインを使わない場合は、`generate-tab.js`のサブタブ切替ハンドラで`.wfm-gen-right`の`display`を明示的に切り替えること(`.wfm-gen-subtab-content.active`は`display:flex; flex-direction:row`がデフォルトなので、縦積みレイアウトが必要なサブタブは`#wfm-gen-subtab-<name>.active { flex-direction: column; }`のようなCSS上書きが別途必要になる点に注意)。複数モジュールから使う汎用関数(Eagle保存など)は、モジュール間の循環importを避けるため`util.js`のような共有モジュールに置く。ComfyUIの生成済み画像を再度I2I入力として使いたい場合は、再アップロードせずとも`"filename [type]"`のannotated-filepath記法で直接参照できる。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.86
|
||||
|
||||
### バグ修正: 画像を一括生成(I2I)等の連続実行で "GalleryMetadataStore: save error: dictionary changed size during iteration" が発生
|
||||
|
||||
comfyui-comic-creator側で「画像を一括生成(I2I)」を実行すると、ログに`[ERROR] GalleryMetadataStore: save error: dictionary changed size during iteration`が出る不具合をユーザー報告により調査。
|
||||
|
||||
**原因**: ギャラリー機能のメタデータ永続化クラス`GalleryMetadataStore`(`py/services/gallery_metadata.py`)は、`self._data["images"]`という単一の辞書を、フォルダを定期的に再スキャンするバックグラウンドスレッド(`gallery_service.py`の`_bg_index_folder`)と、リクエストを処理するメインスレッドの双方から、ロックなしで並行に読み書きしていた。`_save_to_disk()`の`json.dump(self._data, ...)`は内部で`self._data["images"]`を反復してシリアライズするが、その最中に別スレッドがキーを追加/削除すると、Pythonの`RuntimeError: dictionary changed size during iteration`が発生する。画像を一括生成(I2I)はコマごとに生成結果のメタデータ保存リクエストを短時間に連発するため、バックグラウンドインデックススレッドとの競合が起きやすいタイミングだった。同様の反復(`.items()`/`.values()`/`for key in ...`)は`rename_group`/`delete_group`/`list_images_in_group`/`cleanup_stale_images`等、他の複数メソッドにも存在し、いずれも無防備だった。
|
||||
|
||||
**修正**: `GalleryMetadataStore`に`threading.RLock`を追加し、`_data`を読み書きする全メソッド(`save`/`get`/`delete`/`rename_path`/`cleanup_stale_images`/グループ管理系すべて)を`with self._lock:`で保護した。`ensure_group()`が内部で`create_group()`を呼ぶ再入呼び出しがあるため、通常の`Lock`ではなく`RLock`を採用。
|
||||
|
||||
**How to apply**: バックグラウンドスレッド(`_bg_index_folder`等)とリクエストハンドラの双方から触られる共有可変状態(辞書・リスト)を持つクラスを新設・変更する際は、読み取り専用に見えるメソッドも含めて必ずロックで保護すること。「dictionary changed size during iteration」はこの種の無防備な共有辞書の典型的な症状であり、例外を握りつぶすログ出力(`except Exception as e: logger.error(...)`)の裏に隠れて原因特定が遅れやすい。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.85
|
||||
|
||||
### 機能拡張: Generate UI連携ブリッジ(T2I)にデフォルトワークフロー・Negativeプロンプト対応を追加
|
||||
|
||||
Comic Creator側のスクリプトタブ「画像を一括生成」(T2I)が、既存のI2I/Inpaint連携(`_wfmReceiveI2IRunRequest`/`_wfmReceiveInpaintRequest`)と同じ「デフォルトワークフローを実行前に自動読み込み」機能を持てるよう、`_wfmReceiveGenerateRequest`(gallery-tab.js)を拡張した。
|
||||
|
||||
- 引数に`negative`・`workflowData`・`workflowFilename`を追加。`workflowData`が渡された場合は既存2関数と同じパターン(`comfyUI.connected`確認→未接続なら`checkConnection()`→モデルリスト未取得なら`loadModelLists()`→`loadWorkflowIntoEditor()`)で実行前にワークフローを読み込む。渡されない場合は従来通りGenerate UIに現在ロード中のワークフローをそのまま使う。
|
||||
- `negative`が渡された場合は`comfyEditor.setPromptText("negative", negative, { workflow, analysis })`で反映(`ai-tab.js`のInpaint連携と同じ呼び出しパターン)。
|
||||
|
||||
**経緯**: ユーザーから「Workflow Studioの生成UIタブに`ernie_t2i.json`を読み込んで実行したが、別のワークフローが実行されたようだ」との報告を受け調査。原因はコードのバグではなく、ユーザーが手動でワークフローをロードしたのがComic Creator画面内に埋め込まれたWorkflow Studio(`wfmgallery-iframe`)ではなく、ブラウザの別タブとして独立に開いた`/wfm`だったこと(同じURLでも別ウィンドウ=別JS実行コンテキストのため`comfyUI.currentWorkflow`等の状態が共有されない)と判明。この調査を踏まえ、T2I版にもI2I版同様のデフォルトワークフロー指定機能を持たせることで、Comic Creator画面から明示的にワークフローを固定できるようにした(対応する連携コードはComic Creator側`14-integrations.js`/`26-auto-comic-bridge.js`、詳細はcomfyui-comic-creator側のDEVLOG参照)。
|
||||
|
||||
Kaptureで実機E2E確認済み: Comic Creator画面内のWorkflow Studioタブ経由でT2I設定に`ernie_t2i.json`を指定し、スクリプトタブ「画像を一括生成」からRunを実行。ComfyUIサーバーの`/queue`監視・コンソールログで、`ernie_t2i.json`のモデル(`Ernie-Image-Turbo`)による生成が実際に行われ画像がEagleへ保存されたことを確認(2コマとも成功、レイアウトタブへ自動挿入)。新規コード起因のコンソールエラーなし。
|
||||
|
||||
## v0.3.84
|
||||
|
||||
### バグ修正: ernie_t2i.jsonをGenerateUIタブで実行すると "Prompt outputs failed validation" エラー
|
||||
|
||||
v0.3.83でプロンプト表示は修正できたが、ユーザーが実際にGenerateを実行すると`Required input is missing: source`で失敗する不具合が新たに見つかった。ComfyUIサーバーへ直接`/prompt`をPOSTして検証エラーの詳細を取得したところ、原因は`PreviewAny`("Preview as Text"、テキストデバッグ表示用)ノードの必須入力`source`(型`*`のワイルドカード、ウィジェット表現を持たない)が空になっていたこと。
|
||||
|
||||
Ernie Imageのサブグラフでは、外側の折りたたみノードが持つ`width`/`height`ウィジェット値が、内部で**2方向に分岐**していた: 一方は`EmptyFlux2LatentImage.width`(ウィジェット値として注入・解決済み)、もう一方は`PreviewAny.source`(幅の値をプロンプトテンプレート文字列 `{width}` に埋め込むために使われる中継ノード)。既存の`_flattenSubgraphs`の境界値注入ロジックは「1つの境界ポート→1つの内部ターゲット」を前提にしており、`inputPortTargets`が複数ターゲットのうち最後の1つで上書きされていたため、fan-out先の一方(多くの場合ウィジェットではない方)が完全に未接続のまま残っていた。
|
||||
|
||||
- `inputPortTargets`をポートごとに配列で保持するよう変更し、全fan-outターゲットを追跡できるようにした
|
||||
- ウィジェット注入ループをポート単位で回すよう再構成: ウィジェット型ターゲットには従来通り`widgets_values`へ注入しつつ、非ウィジェット型(`PreviewAny.source`のようなワイルドカード必須入力)には`_wfmLiteralInputs`という新しい経路でリテラル値をそのまま`inputs`に注入するようにした(`convertUiToApi`側で`_wfmLiteralInputs`をマージ)
|
||||
- 外側ノードの`widgets_values`が空(=ユーザーが未設定のテンプレートそのまま)の場合でも、fan-out先の別ターゲット(例: `EmptyFlux2LatentImage`)が保持する内部デフォルト値を読み取ってフォールバックとして使うようにし、`image_ernie_image.json`(未設定テンプレート)でも同様に動作するようにした
|
||||
|
||||
**検証**: `/object_info`を提供する実際のComfyUIサーバーに対しNode.jsから直接`/prompt`をPOSTして修正前後の検証結果を比較(修正前: `node_errors`に`Required input is missing: source`、修正後: `node_errors: {}`)。さらに実ブラウザのGenerateUIタブから`ernie_t2i.json`を読み込みGenerateボタンを実行し、実際に画像が生成され「Done (1 image)」になることを確認。
|
||||
|
||||
**How to apply**: サブグラフの境界ウィジェット値は1つのポートから複数の内部ノードへ分岐しうる(今回のような「実際に使う値」と「表示・テンプレート埋め込み用の値」の二重利用パターン)。`_flattenSubgraphs`の境界値解決ロジックを触る際は、常に「1ポート=1ターゲット」ではなく「1ポート=Nターゲット」であることを前提にすること。またウィジェット表現を持たない必須入力(型`*`など)はwidgets_valuesに書けないため、`_wfmLiteralInputs`のような別経路でリテラル値を注入する。
|
||||
|
||||
## v0.3.83
|
||||
|
||||
### 追加機能: Ernie Image(ernie-image / ernie-image-turbo)ワークフロー対応 + プロンプトがリンク経由のノードから供給されるパターンの修正
|
||||
|
||||
Flux2 Klein(`Flux2-Klein_00025.json`)とErnie Image系2ワークフロー(`image_ernie_image.json` / `ernie_t2i.json`)で、GenerateUIタブ・Metadataタブ・サイドパネルIタブのいずれもプロンプトが表示されない不具合を修正した。
|
||||
|
||||
いずれも「プロンプトの実体が`CLIPTextEncode`自身のウィジェット値ではなく、リンク経由で外部の別ノードから供給される」という、v0.3.81/v0.3.82で対応した構成とは異なる新しいパターンが原因だった。
|
||||
|
||||
- **Flux2 Klein**: `CLIPTextEncode.text`が`PrimitiveStringMultiline`ノードへのリンク参照になっている。GenerateUIタブ側(`comfyui-workflow.js`)は既存のrole伝播ロジックで対応済みだったが、Metadataタブ側(`metadata-tab.js`の`resolveLinkedText()`、API形式のリンク解決用)が`text`/`text_g`/`prompt`等のキーしか見ておらず、`PrimitiveStringMultiline`が使う`value`キーを見ていなかったため未検出だった。
|
||||
- **Ernie Image**: プロンプト強化トグル(`prompt_enhancement`)用に`ComfySwitchNode`(if/else switch)を使い、生プロンプトを保持する`PrimitiveStringMultiline`と、LLMで書き換える`TextGenerate`ノードのどちらかへ実行時に切り替える構成。`TextGenerate`の出力は実行時まで内容が決まらず静的解析では取得不能なため、3タブとも`ComfySwitchNode`の両分岐(`on_false`/`on_true`)を辿った上で、リテラルな文字列へ解決できる方(=ユーザーが入力した元のプロンプト)を採用するようにした。
|
||||
|
||||
`comfyui-workflow.js`(GenerateUIタブの`analyzeWorkflow()`のPass 1bロール伝播に`ComfySwitchNode`対応を追加)、`metadata-tab.js`(`resolveLinkedText()`と`extractPromptsFromNodeSet()`を再帰的な解決ロジックに書き直し、`ComfySwitchNode`と`value`キーに対応)、`node_sets_menu.js`(サイドパネルIタブのローカル複製、同様の修正)の3ファイルを修正。
|
||||
|
||||
**検証**: 実際に稼働中のComfyUIサーバー(`/object_info`)を使い、`convertUiToApi()`によるサブグラフ展開を経た上でGenerateUIタブ・Metadataタブ・サイドパネルIタブの3箇所すべてで実ブラウザ上からプロンプトが正しく表示されることを確認。
|
||||
|
||||
**あわせてヘルプを更新**: GenerateUIタブ(`helpGen2`)・Metadataタブ(`helpMetadata5`)のヘルプ本文、およびMetadataタブ内の対応フォーマット補足(`metaFormatTodo`)に記載している対応サブグラフワークフローの例にErnie Imageを追加(EN/JA/ZH)。
|
||||
|
||||
**How to apply**: [[v0.3.82]]で整理した「GenerateUIタブとMetadataタブ(およびIタブの複製)は別実装で自動同期しない」という注意点に加え、プロンプトが`CLIPTextEncode`自身の値ではなくリンク経由で別ノードから来るパターン(`PrimitiveStringMultiline`直結、`ComfySwitchNode`等の中継ノード経由)は今後も現れうる。中継ノードがLLM呼び出しなど実行時にしか値が決まらないノードへ分岐している場合は、無理に実際の出力値を再現しようとせず、静的に解決できる(=ユーザーが直接入力した)分岐を優先して表示する方針でよい。
|
||||
|
||||
## v0.3.82
|
||||
|
||||
### バグ修正: MetadataタブとサイドパネルIタブでLongCat/Boogu/HiDream E1/FireRed等のプロンプトが表示されない不具合
|
||||
|
||||
v0.3.81でFlux.2 Klein/LongCat/Boogu/HiDream E1/FireRedをGenerateUIタブに対応させた後、ユーザーがAI TOOL Chatで実際に5系統すべてを生成しMetadataタブで確認したところ、`qwen_image_edit2509`/`2511`・LongCat・Boogu・FireRedの画像でプロンプトが一切表示されない不具合が見つかった。同じ画像をGenerateUIタブへ読み込むと正しく表示されたことから、GenerateUIタブの解析(`comfyui-workflow.js`の`analyzeWorkflow()`)とは別に実装されているMetadataタブ独自のプロンプト抽出ロジック(`extractPromptsAPI()`/`extractPromptsFromNodeSet()`)が、v0.3.81で対応した新しいノードパターンに追従していなかったことが原因と判明した。
|
||||
|
||||
- **`prompt`キーの欠落**: PNG画像は`workflow`チャンク(UI形式)より`prompt`チャンク(ComfyUIが実行時に生成した、既にAPI形式のペイロードそのもの)が優先的に読まれるため、常に`extractPromptsAPI()`のtextMap構築ロジックが使われる。ここが`text`/`text_g`キーのみを見ており、`TextEncodeQwenImageEdit`(Plus含む)や`TextEncodeBooguEdit`が使う`prompt`キーを見ていなかった(LongCat/Boogu/FireRed、および同種のQwen-Image-Edit系公式テンプレートすべてに影響)。
|
||||
- **Guider経由のrole判定漏れ**: `SamplerCustomAdvanced`はpositive/negativeを直接持たず、`guider`入力の先にある`CFGGuider`/`DualCFGGuider`/`BasicGuider`ノードが持つため、サンプラーノードのinputsを直接見るだけの既存ロジックでは見つけられなかった(Flux.2 Klein/HiDream E1)。GenerateUIタブ側で先に実装した「guiderノードを辿る」対応と同じロジックをMetadataタブ側にも追加。
|
||||
- **`TextEncodeBooguEdit`の1ノード2ロール対応**: `prompt`/`negative_prompt`を1ノードに両方持つ構造は`TextEncodeMageFlowEdit`と同じパターンのため、既存の専用分岐にBooguも合流させた。
|
||||
- **`InstructPixToPixConditioning`の追加ホップ解決**: HiDream E1のこのノードはpositive/negativeを中継するだけで自身はテキストを持たないため、guiderの`cond1`/`negative`から一段先まで辿るようにした。
|
||||
|
||||
`extractPromptsAPI()`(API形式JSON・PNGの`prompt`チャンク用)と`extractPromptsFromNodeSet()`(UI形式・サブグラフ内共用)の両方を修正。さらに`node_sets_menu.js`(ComfyUIサイドパネルのIタブ、`web/comfyui/`はESモジュールをimportできない制約から`comfyui-workflow.js`/`metadata-tab.js`相当のロジックを`_wfm`接頭辞でローカル複製している)にも同じ修正を適用した。
|
||||
|
||||
**あわせてヘルプを更新**: GenerateUIタブ(`helpGen2`)・Metadataタブ(`helpMetadata5`)のヘルプ本文、およびMetadataタブ内の対応フォーマット補足(`metaFormatTodo`)に記載している対応サブグラフワークフローの例に、LongCat/Boogu/HiDream E1/FireRedを追加(EN/JA/ZH)。あわせて`metaFormatTodo`にMage-Flowが記載漏れだった既存の抜けも修正した。
|
||||
|
||||
**検証**: ユーザーがAI TOOL Chatで実際に5系統のワークフローを生成し、Metadataタブでプロンプトが正しく表示されることを確認。サイドパネルIタブでも同様に確認。
|
||||
|
||||
**How to apply**: GenerateUIタブとMetadataタブ(および複製先のIタブ)は、ワークフロー解析ロジックを別々に実装しており自動的には同期しない。新しいノードパターン(プロンプトの格納キー名、guider経由のconditioning構造など)へ対応する際は、`comfyui-workflow.js`の`analyzeWorkflow()`だけでなく`metadata-tab.js`の`extractPromptsAPI()`/`extractPromptsFromNodeSet()`、および`node_sets_menu.js`のローカル複製版も合わせて確認・修正すること。特にPNG画像のメタデータは`prompt`(API形式)チャンクが`workflow`(UI形式)チャンクより優先されるため、実機検証はワークフローJSONの直接ドロップだけでなく、実際に生成したPNG画像でも行うこと(今回はまさにこの違いで問題が後から発覚した)。
|
||||
|
||||
## v0.3.81
|
||||
|
||||
### 追加機能: GenerateUIタブでFlux.2 Klein / LongCat / Boogu / HiDream E1 / FireRedのImage Editワークフローに対応
|
||||
|
||||
「Flux2.Klein4b_comic.json / Flux2.Klein9b_comic.jsonをGenerateUIタブで動作させたい」という依頼を皮切りに、続けて`longcat_comic` / `boogu_image_comic` / `hidream_comic` / `Firered_comic`の4ワークフローにも対応してほしいという依頼が続いた。いずれも公式・サードパーティのImage Edit系ワークフローで、`KSampler`を使わない「Advanced Sampling」構成(`RandomNoise`+何らかのGuiderノード+`KSamplerSelect`+`*Scheduler`+`SamplerCustom`系ノードに分散した seed/steps/cfg/sampler/scheduler)や、独自のプロンプト/サンプラーノードを使っており、既存の`analyzeWorkflow()`(`comfyui-workflow.js`)が前提とするKSampler中心の設計では検出できず、GenerateUIタブのSettings/Latentパネルが「ノードが見つかりません」表示のまま操作不能だった。
|
||||
|
||||
- **Flux.2 Klein(4b/9b)**: `CFGGuider`+`RandomNoise`+`SamplerCustomAdvanced`+`KSamplerSelect`+`Flux2Scheduler`という構成。`SamplerCustomAdvanced`を起点にguider/noise/sampler/schedulerの4ノードをたどり、Settingsパネルに複合サンプラーとして集約表示・編集できるようにした。`EmptyFlux2LatentImage`もLatentパネルに対応させ、`GetImageSize`へリンクされたwidth/heightは編集不可の代わりに空欄+説明表示にして誤上書きを防止。`CFGGuider`/`BasicGuider`をpositive/negative判定の起点にも追加。
|
||||
- **LongCat**: 標準`KSampler`だが独自プロンプトノード`TextEncodeQwenImageEdit`(Plus無し版)を使用。`FluxGuidance`/`FluxKontextMultiReferenceLatentMethod`をrole伝播のパススルーノード集合(`COND_PASSTHROUGH`)に追加。
|
||||
- **Boogu**: `SamplerCustom`(`SamplerCustomAdvanced`とは入力構成が別物で、`CFGGuider`/`RandomNoise`を介さず自身が`positive`/`negative`/`cfg`/`noise_seed`を直接持つ)を誤って同一視していた既存実装を分離し、それぞれ専用ロジックで検出。独自プロンプトノード`TextEncodeBooguEdit`(1ノードに`prompt`/`negative_prompt`を両方保持)にも対応。
|
||||
- **HiDream E1**: `cond1`/`cond2`/`negative`という3つのconditioning入力を持つ特殊ガイダンスノード`DualCFGGuider`(`cfg`の代わりに`cfg_conds`/`cfg_cond2_negative`を持つ)と、画像編集用の中継ノード`InstructPixToPixConditioning`(positive/negative別々の入力・出力を持つため、通常の`COND_PASSTHROUGH`一括処理では役割を取り違える)のrole伝播に個別対応。
|
||||
- **FireRed**: `KSampler`のseed/steps/cfgがPrimitiveノード経由でリンクされる構成(値が数値ではなくリンク参照配列になる)で、既存の直接値前提のコードがリンク配列をそのままSettingsパネルに表示しようとしていた不具合を修正(数値でない場合は`undefined`扱いにし、リンク接続時は空欄+「linked」表示、Apply時は未入力なら書き込みをスキップ)。
|
||||
|
||||
**実機検証で発見・修正した既存バグ(今回の対応範囲拡大で初めて表面化)**
|
||||
|
||||
- サブグラフ内部で完結するリンク(`GetImageSize`→`EmptyFlux2LatentImage`など)の扱いが誤っており、`widgets_values`変換で`batch_size`が`1024`になったり`CLIPTextEncode`の`text`が消えたりする不具合。
|
||||
- 同じロール(positive/negative)のプロンプトノードが複数存在する場合、ドロップダウンの選択とテキストエリアの表示内容が食い違う不具合(全`<option>`に無条件で`selected`を付けていたため)。
|
||||
- ミュート/バイパスしたサブグラフの内部ノードにmode情報が伝播せず、無効なパイプラインのノードがModelタブ等のデフォルト表示に紛れ込む不具合。
|
||||
- サブグラフ展開処理(`_flattenSubgraphs`)の`widgets_values`ノーマライズが「配列長とwidget数が完全一致する場合のみ」動作する設計だったため、seed直後に`control_after_generate`拡張値(`'fixed'`等)が挟まるノード(`KSampler`等)では条件が不成立となり、リンクされたwidget(steps/cfg等)のエントリが除去されないまま後続処理に渡り、cfg/denoiseなど後続フィールドの値が1つずつズレて上書きされる不具合(Firered_comicの実機検証で発覚)。`_simulateWidgetValues`/`_stripLegacyLinkedWidgetValues`/`_findInjectedWidgetIndex`という共通ロジックに全面書き直し、legacy-full/modern形式の自動判定と`control_after_generate`込みの正確な位置計算・注入に統一した。
|
||||
- `Reroute`ノード(配線整理用のUI専用パススルーノードで、ComfyUIバックエンドにノードクラスが存在しない)がそのままAPIペイロードに含まれ、`missing_node_type`の400エラーで生成が失敗する不具合(hidream_comicを実際にGenerateして初めて発覚)。既存のBypass(mode:4)ノード解決ロジック(`_resolveBypassSource`)をReroute対応に拡張し、常に配線を透過解決するようにした。Rerouteの入力型は`"*"`(汎用型)のまま保存されることが多く、Bypass用の型一致検索では見つからないため、Reroute(常に1入力1出力)は型チェックを省略する専用分岐を追加。
|
||||
|
||||
**検証**: 実際にComfyUI上で6ワークフロー(Flux2.Klein4b_comic/Flux2.Klein9b_comic/longcat_comic/boogu_image_comic/hidream_comic/Firered_comic)をGenerateUIタブへ読み込み、Prompt/Settings/Model/Imageタブの表示とRaw JSON変換結果を1つずつ突き合わせて確認。hidream_comicは実際にGenerateを実行し、画像生成成功(Eagle保存ログ)まで確認。
|
||||
|
||||
**How to apply**: KSamplerを使わない「Advanced Sampling」構成(`CFGGuider`/`DualCFGGuider`/`BasicGuider` + `RandomNoise` + `SamplerCustom`(`Advanced`) + `KSamplerSelect` + `*Scheduler`)に新しく遭遇したら、`SamplerCustomAdvanced`/`SamplerCustom`自体を起点にguider/scheduler/samplerノードをたどるのが基本形だが、guiderノードのconditioning入力名(`positive`/`negative` vs `cond1`/`cond2`/`negative`等)やSamplerCustom系のwidget有無はノードごとに異なるため、実際のノード定義を確認してから個別対応を追加すること。サブグラフの`widgets_values`関連の不具合は、`_simulateWidgetValues`系の共通ロジックが「legacy-full/modern形式の自動判定」「`control_after_generate`込みの正確な位置計算」まで一括して担うようになったため、今後同種の問題が出たら個別対応ではなくこの共通関数を確認・拡張するのが筋。`Reroute`のようなComfyコア標準の「UI専用でバックエンドに実体がないノード」は他にも存在しうるため、同様のエラー(`missing_node_type`)に遭遇したら`_resolveBypassSource`の仕組みを拡張する方針で対応する。
|
||||
|
||||
## v0.3.80
|
||||
|
||||
### 追加機能: Comic Creator「半自動マンガ作成」向けブリッジ(LLMプロンプト下書き・バッチ画像生成)
|
||||
|
||||
別カスタムノード`comfyui-comic-creator`が実装中の「半自動マンガ作成」機能(スクリプトのコマ割り・セリフからコマ単位でフキダシ・画像を自動生成するPhase 3)から、iframe越しに呼び出される新規ブリッジ2つを`gallery-tab.js`に追加。既存の`_wfmReceiveImageForI2I`/`_wfmReceiveInpaintRequest`/`_wfmReceiveI2IRunRequest`と同じ「`window._wfmReceiveXxx`をwindowへ直接代入し、Comic Creator側からiframe越しに呼ぶ」配置パターンを踏襲。
|
||||
|
||||
- **`_wfmReceiveLLMPromptRequest(context)`**: シーン・登場人物・セリフ・現在のプロンプト下書きをコンテキストとして受け取り、AIタブの翻訳機能が既に持つLLM接続ロジック(`ai-tab.js`の`callLLM`/`loadAiSettings`/`isValidBackendUrl`、Ollama/LM Studio対応、設定タブの接続設定をそのまま再利用)で画像生成プロンプト案を1本生成して返す。これらの関数は元々`ai-tab.js`内に閉じていたため、`export`に追加してモジュール間で再利用できるようにした。
|
||||
- **`_wfmReceiveGenerateRequest(prompt, width, height)`**: 現在Generate UIタブにロード中のワークフローでtxt2img生成を実行し、結果画像のURLを返す。専用ワークフロー選択(I2I/Inpaint連携にある`workflowData`引数)は今回は非対応。幅・高さの指定は、設定タブの「Latent Image」パネルが`analysis.latent_nodes[0]`(`EmptyLatentImage`/`EmptySD3LatentImage`ノードを検出)を使って`workflow[id].inputs.width/height`を直接書き換えているのと同じ手法をそのまま踏襲。結果URLの取得は`_runInpaintWithImages`等と同じ「`window._wfmGenerateTab.generate()`実行後に`#wfm-gen-result-img`の`src`を読む」パターンを採用。
|
||||
|
||||
**検証**: Comic Creator側からKaptureで実機E2E確認済み(Ollama接続でのLLM呼び出し、t2iワークフローでの画像生成・結果URL取得とも成功)。詳細はComic Creator側DEVLOG「半自動マンガ作成 Phase 3」の項を参照。
|
||||
|
||||
---
|
||||
|
||||
## v0.3.79
|
||||
|
||||
### 追加機能: サイドパネル(Workflow Studio Library)IタブのMage-Flow等サブグラフワークフロー対応
|
||||
|
||||
v0.3.78でMetadataタブ(`metadata-tab.js`)に行ったMage-Flowサブグラフ対応と同様の対応を、「同様にサイドパネル(ComfyUIキャンバス上のIタブ)も更新したい」という依頼を受けて`node_sets_menu.js`にも適用。調査したところ、`node_sets_menu.js`のIタブは`metadata-tab.js`とほぼ同一の抽出関数群(`_extractCheckpoints`/`_extractPrompts`/`_collectAllNodes`等)を独立に重複実装しており、`web/comfyui/`は`static/js/`と配信URLが異なるためESモジュールをimportできない(既存のAI backend URL解決等と同じ制約)。そのためMetadataタブ側の対応がそのまま反映されない状態だった。
|
||||
|
||||
- `comfyui-workflow.js`の`convertUiToApi()`相当ロジック一式(`_flattenSubgraphs`による外側ウィジェット値の内部ノードへの注入、`_getWidgetInputNames`/`_getWidgetInputTypes`/`_getDynamicComboSubNames`によるobject_infoベースのwidgetマッピング、DynamicCombo型対応を含む、約450行)を`_wfm`接頭辞を付けて`node_sets_menu.js`にローカル複製。GenerateUIタブと異なりIタブは読み取り専用表示のため、Checkpoint代替やBypass/Muteノードの記録機能(`_lastCheckpointSubstitutions`等)は複製せず省略。
|
||||
- `fromWorkflow`をasync化し、サブグラフ(`definitions.subgraphs`)を検出した場合は複製版`_wfmConvertUiToApi()`でAPI形式に正規化してから抽出するよう変更(変換失敗時は元のUI形式のまま抽出する既存ロジックへフォールバック)。
|
||||
- `_extractPromptsFromNodeSet`/`_extractPromptsAPI`に`TextEncodeMageFlowEdit`(1ノードでprompt/negative_prompt両方を直接持つ複合ノード)専用の分岐を追加。既存のtextMap(1ノード1テキストしか保持できない)構造では両ロールを区別できず、同じテキストが両方に登録されてしまうバグの修正を複製。
|
||||
|
||||
**検証**: `node_sets_menu.js`は`../../scripts/app.js`をトップレベルでimportしモジュール自体をNode.jsで直接実行できないため、複製したロジックブロック(`_sanitizeJSON`〜`_extractAllMetadata`の自己完結範囲)を`sed`で抜き出しNode.jsでテスト。`mageflow_1.json`で`metadata-tab.js`側と完全に一致する結果(モデル名・プロンプトとも外側の実際の値)を確認。既存の他サブグラフワークフロー(Flux2/Qwen/Ernie/Krea2等)8件でもエラーなく動作しリグレッションがないことを確認。
|
||||
|
||||
**How to apply**: `web/comfyui/`はimport不可のためローカル複製パターンを踏襲する場合、複製元のロジックが今後も更新される可能性があるものは、複製箇所に「変更する場合は元ファイル側も同期すること」という注意コメントを残す。複製したコードの検証は、importが困難なファイルでも`sed`等で自己完結したコードブロックを抜き出しNode.jsで実行すれば、ブラウザなしでも複製元と出力を突き合わせられる。
|
||||
|
||||
### 追加機能: AI TOOL Chatにスキル機能を追加・SVG生成品質改善・ヘルプのカード形式再設計
|
||||
|
||||
「Chatで生成されるSVGの質を上げたい」という依頼から調査を開始したところ、Chat機能(`ai-tab.js`)にはsystem promptの仕組みが一切なく、SVG生成は「モデルが会話の流れで偶然SVGコードを書いた場合に正規表現で抽出するだけ」という後付け設計だったことが判明した(`_extractSvgCode`/`_appendSvgPreview`)。これを踏まえ、単発のSVG改善ではなく汎用的な「AIスキル」機構(選択式system promptライブラリ)を新設し、SVG生成品質改善をその第一弾スキルとして実装する方針に変更した。
|
||||
|
||||
- **AIスキル機構**: `.md`ファイル(frontmatterで`name`/`description`を定義)を`user/default/Workflow-Studio/ai_skills/`に保存し、Chatペイン上部のドロップダウンで選択すると、その内容がsystem promptとして会話履歴の先頭に(表示上のチャット履歴には含めず)注入される。バックエンドは既存の`wildcard_service.py`/`wildcard_routes.py`パターンを踏襲した`skill_service.py`/`skill_routes.py`(`GET/POST /api/wfm/skills`系)を新設。管理UI(一覧・追加・編集・削除)はワイルドカードエディタのUIパターンを再利用してChatペインに内蔵。
|
||||
- **SVG Icon Generatorスキル**: アイコンライブラリ(Feather/Heroicons等)の設計慣習(`viewBox`+`currentColor`+`stroke-width`)とSVGのXSS対策(`<script>`/イベント属性/`<foreignObject>`禁止)を組み込んだ初期スキルとして同梱。実機検証(gemma3系モデル等)でカモメ/ペンギン等の生成結果が「落書き」状態だったため追加調査したところ、LLMのSVG生成研究(Chat2SVG等)で言われる「Shape Decomposition」(対象を部位に分解してから図形を組み立てる)を明示的に指示していないことが原因と判明。「Composition process」セクション(部位分解→形状割当→組み立ての手順を明示指示)を追加し、有機的な輪郭には短い`<path>`曲線を許可するよう緩和した結果、体感で大幅に改善(ユーザー確認済み)。
|
||||
- **Skill Creator(対話式スキル作成)**: 「作りたいスキルを対話形式で聞き出し、完成品を返す」ためのメタスキルを第二弾として同梱。1問ずつ質問し、十分な情報が集まったら\`\`\`skill\`\`\`フェンスでfrontmatter付きの完成スキルを出力する設計。フロント側に\`\`\`skill\`\`\`ブロック検出+「スキルとして保存」ボタン(クリックでスキル管理パネルを開きfrontmatterの`name`から自動生成したファイル名とともにエディタへプリフィル)を追加し、実際にqwen3:4bで対話→保存までのフローを検証済み。SVGプレビュー機能と同じ理由でSPA(`ai-tab.js`)側のみの実装とし、サイドパネル(`node_sets_menu.js`)はスキル選択・system prompt注入のみ対応。
|
||||
- **ヘルプのカード形式再設計**: 別件で導入中の`comfyui-comic-creator`のヘルプUIが「セクションをカード形式で表示(workflow studio のヘルプ形式)」というコメント付きで実装されており、調査したところWFS自身のヘルプUI(サイドバーナビ+検索)を参考に作られたものだったが、1トピック=1カードという構造だけはWFS側になかったことが判明。AI TOOLページを皮切りに全16ヘルプページを「ページ見出し+タブへのジャンプボタン」+「トピックごとの複数カード(見出し+`<ul>`)」の構造に再構成した。個々の項目テキスト・id・i18n翻訳キーは変更せず、既存の`<li>`要素をどのカードの`<ul>`に含めるかという入れ物の再編成のみで実施(新規に追加したのはカード見出し用のキーのみ、約50個×3言語)。再構成の過程で、AI TOOLページの`helpAi5`(Settingsペインの説明)が`helpAi6`に完全に包含される重複項目だったこと、Taggerページと一部のTrouble項目がindex.html上は空の`<li>`(実テキストはi18n.js側にのみ存在)だったことも発見し、あわせて修正・記入した。
|
||||
|
||||
**検証**: 新規追加した`/api/wfm/skills`系エンドポイントをcurlで確認。ブラウザでSVGスキル・Skill Creatorスキルの選択→送信→system prompt注入をfetchフックで実データ検証、手書きの「Bird」few-shot例は実際にレンダリングして認識可能な形状であることを確認。ヘルプの再設計は全16ページを実ブラウザでクリックし、カード表示・ジャンプボタンの遷移・翻訳適用を確認。
|
||||
|
||||
**How to apply**: 新しい「選択式system promptライブラリ」的な機能を追加する際は、`skill_service.py`のfrontmatterパース(`_FRONTMATTER_RE`)とファイル一覧/読み書きAPIパターンがそのまま再利用できる。ヘルプページを新設・改修する際は、1ページ=1個の巨大な`<ul>`ではなく、トピックごとに`.wfm-help-card`で分割し、新規カード見出しはid+i18n 3言語キー+`app.js`の`helpIdMap`の3点セットで追加すること([[project_v0336_conventions]]のヘルプ更新規約を参照)。index.html上のテキストは`applyI18nToHtml()`がi18n.jsの内容で必ず上書きするため、実際に表示される文言はi18n.js側が正であり、index.html側は空にしたり古いままにしたりせず同期を保つこと。
|
||||
|
||||
## v0.3.78
|
||||
|
||||
### 追加機能: Eagle自動保存のSVG対応
|
||||
|
||||
「画像管理ソフトEagleとの連携機能(Eagleへ自動保存)をSVGに対応させたい」という依頼を受けて実装。既存のEagle自動保存(`generate-tab.js`のEagle送信ループ)は`history.outputs`の`images`キーのみを走査しており、SVG出力ノードには2系統あってそれぞれ挙動が異なることが判明した。
|
||||
|
||||
- **標準`SaveSVGNode`(ComfyUI本体)**: 通常の`images`キー(`{filename, subfolder, type}`)で結果を返すため既存の収集ロジックに拾われるが、ComfyUIの`/view`エンドポイントはSVGをXSS対策として`Content-Type: application/octet-stream` + `Content-Disposition: attachment`で強制返却するため、`<img>`表示にもEagleの`addFromURL`にも正しく使えない。`comfyui-client.js`の`getImageBlob()`で拡張子`.svg`を検出し`blob.slice(0, blob.size, "image/svg+xml")`でMIMEタイプを付け替えることで`<img>`表示を解決し、Eagle送信は`eagle_routes.py`側でローカルファイルパスを解決して`addFromPath`を使うルートに切り替えた。
|
||||
- **`comfyui-tosvg`の`Save SVG String`ノード**: `images`キーではなく独自の`saved_svg`(ファイル名)/`path`(絶対パス)キーで返す。さらに、ComfyUIの実行エンジン(`execution.py`の`get_output_from_returns`)は`ui`辞書の各値をイテラブルとして連結する仕様のため、このノードが単一文字列を返すと1文字ずつ分解された配列になる(`"a.svg"` → `["a",".","s","v","g"]`)。`comfyui-client.js`側でこの文字配列を検出し`join("")`で復元する対応を追加。絶対パスが既知なため、Eagle送信は`_resolve_absolute_svg_path()`(output/temp/inputディレクトリ配下限定の検証つき)経由で直接`addFromPath`する。
|
||||
- **プレビュー用一時画像の除外**: `SVG String Preview`や`PreviewImage`など`type: "temp"`の画像は、Eagle自動保存からもGenerateUI結果表示からも除外(`outputImages = images.filter(img => img.type !== "temp")`に統一)。SVG出力があるワークフローでは、GenerateUIの結果プレビューを実際に保存されたSVGファイルに切り替え、`/view`が使えないためGalleryタブの配信エンドポイント(`/wfm/gallery/image/serve?path=...`、絶対パス指定・正しいContent-Type)を再利用した。
|
||||
|
||||
**セキュリティ**: `_resolve_absolute_svg_path()`は任意の絶対パスをそのまま信頼せず、ComfyUIのoutput/temp/inputディレクトリ配下の実在するファイルのみ許可する(外部からこのAPIを叩かれてローカル任意ファイルを送信させられるリスクの回避)。Windowsのパス比較は大文字小文字を区別しないため`os.path.normcase`で正規化してから比較。
|
||||
|
||||
**検証**: Node.jsスクリプトで`comfyui-client.js`/`eagle_routes.py`のロジックを直接テストし、両ノードタイプでファイルが正しい形式でEagleへ送信されることを確認。ユーザーが実際にComfyUI上で`svg_1.json`/`svg_5.json`/`svg_7.json`の3種類のワークフローを実行し、Eagle保存・プレビュー表示それぞれで動作確認。
|
||||
|
||||
**How to apply**: [[project_v0342_gallery_feeder]]のSVG関連対応と合わせ、SVGはComfyUI本体がXSS対策で`/view`から生配信しない前提で設計する必要がある。新しいSVG出力ノードに対応する際は、まず`ui`辞書がどのキー名で結果を返すか(`images` vs 独自キー)、絶対パスが取得できるか、を確認してから送信経路(`addFromURL` vs `addFromPath`)を選ぶ。
|
||||
|
||||
### 追加機能: GenerateUI/MetadataタブでComfyUIサブグラフワークフロー対応(Mage-Flow等)
|
||||
|
||||
「mageflow_1.jsonをGenerateUIタブ、メタデータタブに対応させたい」という依頼を受けて調査したところ、`comfyui-workflow.js`の`detectFormat()`が`definitions.subgraphs`を持つワークフロー全般を一律「App形式」と判定し、GenerateUIタブが読み込みを拒否する設計になっていたことが判明した(`generate-tab.js`の`loadWorkflowIntoEditor()`が`format === "app"`で即エラー)。サブグラフ展開自体は`_flattenSubgraphs()`に既に実装済みで`convertUiToApi()`から呼ばれていたが、GenerateUIの通常フローには到達しない状態だった。
|
||||
|
||||
- **`detectFormat()`の判定基準を訂正**: 「サブグラフを含む」ことと「真のApp形式(ComfyUIの簡略化Webアプリ風UI)」は別軸であるべきところ、後者の判定条件に前者が混入していた。`extra.linearMode === true`または`.app.json`拡張子の場合のみ"app"、それ以外(サブグラフを含むだけの通常ノードグラフ)は"ui"として扱うよう修正。`workflow_analyzer.py`(Python側、サイドパネルのAPI/APPバッジ表示に使われる)にも同じ判定基準の食い違いがあり、揃えて修正。
|
||||
- **サブグラフ外側ウィジェット値の内部ノードへの注入(`_flattenSubgraphs()`)**: 最大の落とし穴だった。サブグラフの「外側ノード」(折りたたみ表示、ユーザーが実際に編集する値)のwidgets_valuesと、`definitions.subgraphs[].nodes`内の「内部ノード」(サブグラフ編集画面を開いた時のテンプレート初期値)のwidgets_valuesは、ComfyUIが保存時に同期しない場合があり、実データで検証すると別内容のプロンプトが入っていた。既存の`_flattenSubgraphs()`はリンクのリダイレクトのみ行い、ウィジェット値の注入は一切していなかったため、そのままではGenerateUIが古いテンプレートのデフォルト値で生成してしまう。外側ノードのinputs配列(`widget`かつ`link`なしの要素)を走査し、サブグラフ入力ポート経由で対応する内部ノードのwidgets_values配列へ値を上書きするロジックを新設。内部ノードのwidgets_values配列がリンク済み入力の分もエントリを持つ「legacy full形式」の場合があり、`convertUiToApi()`本体が前提とする「リンク済みwidgetにはエントリがない」形式との食い違いでインデックスがずれるバグも合わせて修正(リダイレクトされた実リンク先スロットのみをエントリから除去する形に変更)。
|
||||
- **`analyzeWorkflow()`に`TextEncodeMageFlowEdit`対応**: 1ノードでprompt/negative_prompt両方を直接持つ複合ノードのため、SDXLPromptStylerパターンを参考にrole固定(positive/negative)で個別登録。width/height/batch_sizeも内包しているため`latent_nodes`にも登録(EmptyLatentImage相当)。
|
||||
- **DynamicCombo型(`COMFY_DYNAMICCOMBO_V3`)対応**: `SaveImageAdvanced`の`format`入力(選択値に応じて`bit_depth`/`input_color_space`等のサブフィールドが動的に変わる新しいComfyUI V3 API機能)が未対応で、`inputs`から丸ごと欠落し`TypeError: missing 1 required positional argument: 'format'`が発生した。最初は`{"format": "png", "bit_depth": ..., ...}`という単一辞書にまとめて渡す実装を試みたが実機で同じエラーが再現し、ComfyUI本体の`comfy_api/latest/_io.py`の`DynamicCombo._expand_schema_for_dynamic`を読んだところ、バックエンドは`"format"`/`"format.bit_depth"`のような**フラットなドット区切りキー**を`inputs`辞書に個別に期待しており、実行エンジン自身(`build_nested_inputs()`)が実行時にネストした辞書へ自動組み立てる設計だったため、辞書化をやめてフラットキーをそのまま`inputs`へ設定する実装に修正。
|
||||
- **`metadata-tab.js`のサブグラフ対応強化**: 既存の`collectAllNodes()`ベースの抽出(トップレベル+サブグラフ内nodesを単純結合)はモデル名・プロンプンとも「内部テンプレートの古い値」を拾ってしまう同じ問題を抱えていたため、サブグラフを検出した場合は`comfyWorkflow.convertUiToApi()`を経由してAPI形式(外側ウィジェット値が注入済み)に正規化してから抽出するよう変更。`extractPromptsAPI`/`extractPromptsFromNodeSet`双方に`TextEncodeMageFlowEdit`のpositive/negative個別抽出(1ノードにつき2つのwidgets_valuesスロット)対応を追加。
|
||||
|
||||
**検証**: Node.jsで`comfyui-workflow.js`をブラウザ実行と同一ロジックのままimportし(`/object_info`はローカルComfyUIサーバーへのfetchで取得)、`mageflow_1.json`の変換結果を全項目突き合わせ。外側と内部でテキストが異なる実データを使い、正しく外側の値が採用されることを確認。既存のFlux2/Qwen/Ernie/Krea2等、サブグラフを持つ8ワークフローで`git stash`によるビフォーアフター比較を実施し回帰なしを確認(変更前は全て"app"判定で一律ブロックされていたため、影響は純増)。ユーザーが実機でGenerateUI実行・Eagle送信・Metadata抽出それぞれ確認。
|
||||
|
||||
**How to apply**: ComfyUIのサブグラフは「外側の折りたたみ表示ウィジェット値」と「内部テンプレート定義のwidgets_values」が別データであり、保存時に同期される保証がないという前提を忘れないこと。サブグラフを扱うロジックを新設・修正する際は、実データ(必ずしもテンプレートそのままでない、ユーザーが値を変更済みのワークフローファイル)で外側/内部の値を突き合わせて検証する。またComfyUIの新しいV3 API機能(DynamicCombo等)に遭遇したら、まず`comfy_api/latest/_io.py`の該当実装を読んでバックエンドが実際に何を期待しているかを確認してから実装する方が、推測で実装してハマるより早い。
|
||||
|
||||
### 改善: Workflowタブ Summarize機能を構造化データベースに変更
|
||||
|
||||
上記のサブグラフ対応の流れで、「ワークフロータブの要約、解析ができるようにしたい」という依頼を受けた。調査の結果、既存の「AI要約」ボタン(`workflow-tab.js`)は生ワークフローJSON全体を`JSON.stringify(wf).substring(0, 4000)`で先頭4000文字に切り詰めてLLM(Ollama)へ渡すだけの実装で、`MarkdownNote`の長文説明やUI座標情報が先頭を占有し、複雑なワークフロー(特にサブグラフ入り)では肝心のノード構成情報が切り捨てられて有効な要約ができない状態だった。「要約機能の修正」「解析結果表示の新規UI追加」のどちらを優先するか確認し、前者を選択。
|
||||
|
||||
- `_buildSummarySourceText()`を新設。既存の`comfyWorkflow.detectFormat()`/`convertUiToApi()`/`analyzeWorkflow()`(上記のMage-Flow対応で強化済み)を再利用し、Checkpoint/Diffusion Model/Text Encoder/VAE/LoRA/ControlNet/Hypernetwork/Positive・Negative Prompt/Sampler設定/使用ノード種別一覧をコンパクトなテキストに整形してからLLMへ渡すよう変更。変換・解析に失敗した場合は従来の生JSON切り詰め方式へ自動フォールバック。
|
||||
|
||||
**検証**: Node.jsで`_buildStructuredSummaryText()`相当のロジックを`analyzeWorkflow()`の実出力に対して実行し、`mageflow_1.json`(1183文字)・`qwen-image-edit_controlnet.json`(694文字)など、旧実装の4000文字切り詰めより大幅にコンパクトかつ正確な出力になることを確認。サブグラフを持たない通常ワークフロー3件でも正しく動作することを確認。
|
||||
|
||||
**How to apply**: LLMへワークフロー情報を渡す用途では、生JSONダンプではなく既存の解析結果(`analyzeWorkflow()`)を再利用してテキスト化する方が、トークン数を抑えつつ精度も上がる。生JSONは`MarkdownNote`やUI座標などLLMにとってノイズになる情報を大量に含む。
|
||||
|
||||
### バグ修正: Workflowタブ詳細パネルのP:/I:/→カウントとフォーマットバッジ
|
||||
|
||||
上記のMage-Flow対応の実機確認中、Workflowタブのサイドパネルで新しいノードタイプを使うワークフローの入出力カウントが「P:0 I:0 → 0」と表示される問題が見つかった。原因は`workflow_analyzer.py`の`analyze_workflow()`がノードタイプ名の完全一致(`ntype == "CLIPTextEncode"`、`ntype == "SaveImage"`)でカウントしており、`TextEncodeMageFlowEdit`(プロンプト)や`SaveImageAdvanced`(画像出力)のような新しいノードタイプを認識できていなかったため。
|
||||
|
||||
- プロンプト検出を`_PROMPT_NODE_TYPES`集合(`TextEncodeMageFlowEdit`/`TextEncodeQwenImageEditPlus`/`SDXLPromptStyler`等)に、出力画像検出を`_OUTPUT_IMAGE_NODE_TYPES`集合(`SaveImageAdvanced`追加)にそれぞれ拡張。
|
||||
- 副次的に、この関数のフォーマット判定が上記で修正したJS側`detectFormat()`と食い違っていること(`"definitions" in workflow_data`だけで"app"と判定する古い基準のまま)を発見し、同じ基準(`linearMode`のみ"app")に統一。これはComfyUIキャンバスのサイドパネル(`node_sets_menu.js`)のAPI/APPバッジ表示に使われており、放置するとMage-Flow系ワークフローに誤って「APP」バッジが付き続ける状態だった。
|
||||
|
||||
**検証**: Pythonで`analyze_workflow()`を直接呼び出し、`mageflow_1.json`で`prompts: 1`/`outputs.images: 1`/`format: "ui"`が正しく出ることを確認。ユーザーからの追加確認依頼を受け、真にlinearModeを持つワークフロー(`wf_20260322113657.json`等、サブグラフを含むケースも含む)では引き続き"app"、Mage-Flow等のサブグラフのみのワークフローは"ui"と、両者が正しく区別されることをJS/Python両実装で突き合わせ検証。既存の複数ワークフローでリグレッションがないことも確認。
|
||||
|
||||
### 改善: Send to Canvasトーストメッセージの区別
|
||||
|
||||
「ワークフロータブでUI形式のワークフローをキャンバスへ送るボタン押下時、『ワークフロー準備完了 タイトルをキャンバスにドラッグ』のトーストは現在ドラッグの必要がないためメッセージを変えたい」という依頼。`workflow-tab.js`/`gallery-tab.js`の「キャンバスへ送る」処理は、`window.opener`経由で直接ロードされる場合(ドラッグ不要)と、`localStorage`+タイトルドラッグのフォールバック(ドラッグ必要)の2経路があるが、両方が同じ`workflowSentToCanvas`(「タイトルをドラッグ」)トーストを共有していたため、直接ロードされた場合にも実際には不要なドラッグ手順が案内されていた。
|
||||
|
||||
- 新しいi18nキー`workflowSentToCanvasDirect`(「ワークフローをキャンバスへ送信しました」)を英日中3言語で追加し、`window.opener`経由の成功時のみこちらを使うよう変更。ドラッグが実際に必要なフォールバック側は既存メッセージのまま維持。
|
||||
|
||||
**検証**: `node --check`で構文チェック。ユーザーがComfyUIツールバーから開いた状態で実機確認。
|
||||
|
||||
## v0.3.76
|
||||
|
||||
### 追加機能: Comic Creator向けI2I実行ブリッジ`_wfmReceiveI2IRunRequest`
|
||||
@@ -15,6 +338,36 @@ Comic Creator(`comfyui-comic-creator`、本アプリをiframe埋め込みで
|
||||
|
||||
**How to apply**: 「画像をComfyUIのワークフローに自動セットしてキュー実行し、結果を返す」という外部連携パターンを新設する際は、`comfyEditor.applyImageToSlot`/`setPromptText`/`setInpaintParams`/`window._wfmGenerateTab.generate`が既にマスクなし用途にも流用できる設計になっている(オプション引数は未指定なら書き込みをスキップする)。Inpaint版と重複する新規プリミティブをWorkflow Studio側に増やす必要は薄く、`_runInpaintWithImages`と同型の薄いラッパーを追加するだけで足りる。
|
||||
|
||||
## v0.3.77
|
||||
|
||||
### 追加機能: GalleryタブでSVGファイルの表示・管理に対応
|
||||
|
||||
「ギャラリータブでSVGファイル表示対応させたい」という依頼を受けて調査したところ、`gallery_service.py`の`IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif"}`にSVGが含まれておらず、フォルダスキャン・一覧取得・削除・移動・配信のすべてで一律に除外されていたことが原因だった。フロントエンド(`gallery-tab.js`)側は拡張子フィルタを一切持たず、サムネイル・詳細プレビュー・ライトボックスすべて`<img>`タグで描画する設計だったため、バックエンドの許可リストにSVGを追加するだけで表示系は無改造で対応できた。
|
||||
|
||||
- `gallery_service.py`: `IMAGE_EXTENSIONS`に`.svg`を追加。`serve_thumbnail()`にSVG分岐を新設し、GIF(アニメーション保持のため元ファイルを返す既存分岐)と同じ扱いで元ファイルをそのまま返すようにした。SVGはベクター画像でPillowが`Image.open()`できないため、そもそもラスタライズ縮小の意味がなく、`<img>`タグ側でブラウザが任意サイズにスケーリングして表示する前提。
|
||||
- `gallery_routes.py`: `serve_image`/`serve_thumb`のContent-Type判定は変更不要だった。Pythonの`mimetypes.guess_type()`が標準で`.svg`→`image/svg+xml`を解決するため、既存の`mimetypes.guess_type()`フォールバック分岐がそのまま機能する。
|
||||
|
||||
**セキュリティ**: `<img src="...">`によるSVG表示は、ブラウザが`<script>`要素の実行や外部リソース読み込みを無効化する仕様のため、悪意あるSVG(埋め込みJS等)が混入していてもXSSリスクはない。`<object>`/`<iframe>`/`iframe.srcdoc`のような埋め込み方式(スクリプトが実行され得る)は使っていない。
|
||||
|
||||
**検証**: 変更した`gallery_service.py`を`ast.parse()`で構文チェック。開発リポジトリと実行時`custom_nodes`フォルダへ同期し`__pycache__`をクリアした上でComfyUIを再起動し、実際にoutputフォルダへ配置したSVGファイルがGalleryタブでサムネイル・詳細プレビューとも正しく表示されることをユーザーが確認。
|
||||
|
||||
**How to apply**: [[project_next_release_docs]]のような「フォーマット追加」系の変更は、フロントエンドがフォーマット非依存の`<img>`/`<video>`等で描画する設計になっている場合、バックエンドの許可拡張子リスト(`IMAGE_EXTENSIONS`)に追加するだけで大半のUIパスに波及する。逆に言えば、新形式を追加する際はこのリストが唯一のゲートになっている前提を崩さないよう注意する。
|
||||
|
||||
### 追加機能: AI TOOL Chatでワークフロー不要のSVG生成・表示・Gallery保存
|
||||
|
||||
Gallery SVG対応の直後、「AI TOOLのチャット機能でワークフローを使用せずにSVGを作成することは可能か」と質問された。既存のChat画像生成(`generate_image`ツールコール→ComfyUIのT2I/I2Iワークフロー実行)とは別に、SVGはXMLテキストに過ぎずLLMのテキスト生成だけで完結するため、ComfyUIワークフローもTool Calling機能も不要である旨を回答した上で、「実装する(表示+Gallery保存)」の合意を得て実装した。
|
||||
|
||||
- `ai-tab.js`: `_extractSvgCode(text)`を新設。アシスタント応答からマークダウンのコードフェンスで囲まれたSVGコードブロック、または生の`<svg>...</svg>`タグを正規表現で抽出する。`appendChatBubble()`の`role === "assistant"`分岐に組み込み、検出時は`_appendSvgPreview()`を呼ぶ。抽出ロジックは`reply.content`(プレーンテキスト)のみを見ており、`reply.toolCalls`は一切参照しないため、モデルがtool calling(function calling)に対応していなくても動作する——「画像生成を許可する」トグルがONの場合のみ送信される`tools`パラメータとは完全に独立した経路。
|
||||
- `_appendSvgPreview()`: 抽出したSVGコードを`btoa(unescape(encodeURIComponent(svgCode)))`でBase64化し、`<img src="data:image/svg+xml;base64,...">`としてプレビュー表示(`<img>`経由のためスクリプト実行はブラウザ側で無効化され安全)。「Galleryに保存」ボタンを併設し、クリックで`/wfm/gallery/image/save`へ`{filename: "chat_svg_<timestamp>.svg", imageData: dataUrl}`をPOSTする。
|
||||
- `gallery_routes.py`の`save_image_to_gallery`: 従来は`imageData`を無条件にPNGとして扱い拡張子`.png`を強制していたが、data URLのヘッダ(`data:<mime>;base64,`)からMIMEタイプを判定し`_SAVE_EXT_BY_MIME`(png/jpeg/webp/gif/svg+xml)で拡張子を決定する汎用処理に変更。Image Edit Tabの既存呼び出し(`canvas.toDataURL("image/png")`)は引き続き`.png`として保存され後方互換。
|
||||
- CSS(`main.css`)に`.wfm-ai-chat-svg-wrapper`/`.wfm-ai-chat-svg-img`を追加(白背景パディング付きプレビュー枠、透過SVGでも視認できるように)。
|
||||
- i18n(`i18n.js`)に`aiChatSaveSvgToGallery`/`aiChatSvgSaved`/`aiChatSvgSaveFailed`を英日中3言語で追加。
|
||||
- ヘルプ文言3点セット(`templates/index.html`の`wfm-help-ai-7`、`i18n.js`の`helpAi7`英日中3言語、`app.js`の`helpIdMap`)でこの機能を追記。
|
||||
|
||||
**検証**: 変更した`ai-tab.js`/`i18n.js`/`app.js`を`node --check`で構文チェック、`gallery_routes.py`を`ast.parse()`で構文チェック。開発リポジトリと実行時`custom_nodes`フォルダへ全ファイル同期し`__pycache__`をクリア。ユーザーがComfyUI再起動後、Chatで実際にSVGを生成させてプレビュー表示とGallery保存を確認済み。
|
||||
|
||||
**How to apply**: LLMチャット機能に新しい出力形式(SVG、Mermaid図など、テキストとして表現できるフォーマット)を追加検討する際は、まず「ラスター画像生成のようにComfyUIワークフロー実行が本当に必要か、それともLLMのテキスト生成だけで完結するか」を切り分ける。後者であれば`reply.content`に対する検出・抽出ロジックのみで実装でき、Tool Calling対応可否に依存しない機能として提供できる。
|
||||
|
||||
## v0.3.75
|
||||
|
||||
### Lemonadeバックエンド追加(3つ目のAIバックエンド)
|
||||
@@ -981,7 +1334,7 @@ v0.3.60 で実装した G'MIC-Qt 連携の動作修正・セキュリティ強
|
||||
Windows でバックグラウンドプロセス(ComfyUI)からGUIウィンドウを表示するために必須のフラグ。一度削除したが eagle_comic_creater_web の参考実装を確認して再追加した。
|
||||
|
||||
#### 設定パスのプレースホルダー修正
|
||||
`settings-tab.js` の G'MIC-Qt パス入力フィールドのデフォルト値とプレースホルダーからユーザー固有のパス(`statsu-11`)を削除し、汎用的な `C:\path\to\gmic_qt.exe` に変更。`gmic_routes.py` のフォールバック値も同様に空文字に変更。
|
||||
`settings-tab.js` の G'MIC-Qt パス入力フィールドのデフォルト値とプレースホルダーからユーザー固有のパスを削除し、汎用的な `C:\path\to\gmic_qt.exe` に変更。`gmic_routes.py` のフォールバック値も同様に空文字に変更。
|
||||
|
||||
---
|
||||
|
||||
@@ -1049,7 +1402,7 @@ G'MIC-Qt 連携用の API ルートを新規作成。
|
||||
| `GET /api/wfm/gmic/status/{job_id}` | ジョブの進行状況(`pending` / `processing` / `completed` / `failed`)を返す |
|
||||
| `POST /api/wfm/gmic/result` | `result_path` を受け取り、処理済み画像を base64 エンコードして返す |
|
||||
|
||||
- G'MIC-Qt の実行パスは Settings に保存された `gmic_qt_path` を参照。未設定の場合はデフォルトパス `C:\Users\statsu-11\gmic-3.6.5-qt-win64\gmic_qt.exe` にフォールバック。
|
||||
- G'MIC-Qt の実行パスは Settings に保存された `gmic_qt_path` を参照。未設定の場合はデフォルトパス `C:\path\to\gmic_qt.exe` にフォールバック。
|
||||
- プロセスは `subprocess.Popen` + `CREATE_NEW_CONSOLE` でノンブロッキングに起動し、スレッド内でポーリングして完了を検出。
|
||||
|
||||
#### `py/wfm.py`
|
||||
|
||||
@@ -12,6 +12,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
|
||||
- Batch generation across models, samplers, prompts, and workflows
|
||||
- Image Feeder for continuous folder-based generation
|
||||
- Lab: experimental I2I batch generation with independent per-iteration keyframes for Checkpoint/VAE/Prompt/KSampler
|
||||
|
||||
**📚 Workflow Studio Library** — a multi-function side panel for smooth ComfyUI integration
|
||||
|
||||
@@ -21,7 +22,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -57,7 +58,8 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
|
||||
## Features
|
||||
|
||||
### Workflow Tab
|
||||
<details>
|
||||
<summary><h3>Workflow Tab</h3></summary>
|
||||
|
||||
- **Thumbnail / Table views** — switch between view modes to browse your workflow library
|
||||
- **Thumbnail side panel** — preview workflow canvas snapshots in the side panel
|
||||
@@ -71,16 +73,22 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- **Search clear (✕)** — inline ✕ button appears inside the search box whenever text is entered; click to clear immediately
|
||||
- **Clear all filters (✕ Clear)** — rightmost toolbar button; resets search text, group filter, and badge filter (ALL) in one click
|
||||
|
||||
### Canvas Snapshot (v0.1.2)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Canvas Snapshot (v0.1.2)</h3></summary>
|
||||
|
||||
- **One-click capture** — click the camera button in ComfyUI's top bar to snapshot the current workflow canvas
|
||||
- **Auto-save as thumbnail** — the snapshot is saved directly to the workflow data folder as a PNG thumbnail
|
||||
- **Embedded workflow metadata** — workflow JSON is embedded in the PNG (tEXt chunk), compatible with ComfyUI's drag-and-drop import
|
||||
- **Auto-import** — the captured workflow is automatically imported and appears in the Workflow tab
|
||||
|
||||
### GenerateUI Tab (v0.3.5)
|
||||
</details>
|
||||
|
||||
- **5-tab layout** — Input / Model / Settings / Feeder / Batch tabs; Input, Model, and Settings each include a Raw JSON column on the right for instant preview and direct editing
|
||||
<details>
|
||||
<summary><h3>GenerateUI Tab (v0.3.5)</h3></summary>
|
||||
|
||||
- **6-tab layout** — Input / Model / Settings / Feeder / Batch / **Lab** tabs; Input, Model, and Settings each include a Raw JSON column on the right for instant preview and direct editing
|
||||
- **Save button** — located at the right end of the subtab row; opens a filename dialog (default: current workflow name) and saves the current workflow as a `.json` file to the Workflow tab via the import API
|
||||
- **Input tab** — Prompt and Image inner tabs (drag-and-drop upload); Prompt tab shows Positive Prompt and Negative Prompt textareas, plus an **Embeddings selector** at the bottom (Filter + Select + Weight input + Paste button); Paste inserts `(embedding:Name:weight)` at the cursor position of the last focused textarea (defaults to Positive when neither is focused); Raw JSON (540px) in the right column
|
||||
- **Revert button** (v0.3.70) — a ↺ button next to each Apply button reverts the textarea to the node's currently applied value (the reverse of Apply)
|
||||
@@ -99,10 +107,13 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- **Center pane** — group-based selection with inner tabs (Checkpoint | Lora | Prompt | Workflow); Checkpoint/Lora groups come from the Models tab, Prompt groups from the Prompt tab, Workflow groups from the Workflow tab — check a group to add all its members, expand ▶ to select individually
|
||||
- **Right pane (Batch Queue)** — 7 columns: Checkpoint / Lora / Prompt / Workflow / Sampler / Scheduler / **Style**; each column header has an enable checkbox (radio behavior: only one at a time); count shown per column; Style batch applies each selected style sequentially to a workflow copy
|
||||
- **Lora batch prompt sync** (v0.3.69) — for each selected LoRA, its syntax (`<lora:name:strength_model:strength_clip>`) and CivitAI trained words are automatically appended to the Positive prompt (rebuilt from the original prompt each time, not accumulated), then restored to the pre-batch state when the batch completes or is stopped; if a selected LoRA is not yet recognized by ComfyUI's model list, a warning toast suggests a Refresh or restart
|
||||
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion; improved analysis covers SDXL multi-hop CONDITIONING chains, CLIPTextEncodeSDXL, SDXLPromptStyler, KSamplerAdvanced, and more
|
||||
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion; improved analysis covers SDXL multi-hop CONDITIONING chains, CLIPTextEncodeSDXL, SDXLPromptStyler, KSamplerAdvanced, and Guider-based "Advanced Sampling" workflows (`SamplerCustomAdvanced`/`SamplerCustom` + `CFGGuider`/`DualCFGGuider`/`BasicGuider` + `RandomNoise` + `KSamplerSelect` + `*Scheduler` — Flux.2 Klein, LongCat, Boogu, HiDream E1, etc.)
|
||||
- **Eagle integration** — auto-save generated images to [Eagle](https://eagle.cool/) with metadata
|
||||
|
||||
### Feeder subtab (v0.3.5 / v0.3.42)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Feeder subtab (v0.3.5 / v0.3.42)</h3></summary>
|
||||
|
||||
Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons at the top of the left pane (persisted in `localStorage`).
|
||||
|
||||
@@ -127,7 +138,31 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **After Gen modes** — Loop (wrap at end and continue indefinitely), Increment (stop automatically when last image is reached), Fixed (always use the same index)
|
||||
- **Run / Stop controls** — left pane Run / Stop buttons manage the generation loop; After Gen combo (loop / increment / fixed), ▶ Run, and ■ Stop widgets are also available directly on the WFS_GalleryFeeder node in the ComfyUI canvas (`gallery_feeder_extension.js`)
|
||||
|
||||
### Prompt Tab
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Lab subtab (v0.3.87 – v0.3.91)</h3></summary>
|
||||
|
||||
An experimental batch generator: runs the workflow currently loaded in GenerateUI N times, letting Checkpoint / VAE / Prompt / KSampler each change independently starting at a chosen iteration — unlike the Batch tab's single-axis queue, every column keeps its own list of change points. Never mutates the loaded workflow; every iteration runs on a fresh clone. Works with both I2I and T2I workflows (v0.3.90).
|
||||
|
||||
- **Setting / Results / Plan JSON sub-panels** — Setting: image drop zone + 4 keyframe columns + run controls; Results: source image and up to 9 generated thumbnails (click to enlarge); Plan JSON: the current plan as raw editable JSON (v0.3.88)
|
||||
- **Per-column keyframes** — Checkpoint, VAE, Prompt, KSampler each hold an independent list of `{iteration, value}` keyframes; +/− per column adds/removes a keyframe (iteration #1 is fixed and can't be removed); click a cell to edit its value, the iteration it starts applying from, and a "revert to #1's setting" checkbox; the effective value at iteration N is the latest keyframe with iteration ≤ N, carried forward until the next one changes it
|
||||
- **Checkpoint / VAE cell filtering** (v0.3.88) — a Filter... box above the value dropdown narrows the list live, same as GenerateUI's Model tab; overlay ✕ clear button matches the Models/Nodes/Workflow/Gallery tab search boxes
|
||||
- **Prompt cell extras** (v0.3.88, refined in v0.3.89) — **Get from GenerateUI** fills Positive/Negative from the workflow currently loaded in GenerateUI; **Get from Image** extracts a prompt from the image currently loaded in Lab's own Image drop zone (same detection as the Metadata tab; no separate file picker); **Style** appends whichever style is currently selected in GenerateUI's own top-bar Style checkbox/dropdown to the Positive/Negative text below, comma-separated after any existing text — it never clears what's already there (Lab has no separate style picker of its own)
|
||||
- **T2I workflow support** (v0.3.90) — a **T2I workflow (no source image)** checkbox in the Image pane skips the source-image requirement and never touches a LoadImage node, so workflows with no image input (e.g. plain `EmptyLatentImage` txt2img) can also use the Checkpoint/VAE/Prompt/KSampler keyframes; the Image drop zone and "Use generated image for next" are greyed out (not cleared) while it's on; saved with the plan as `t2i_mode`
|
||||
- **Use generated image for next** — optional checkbox that chains iterations: from iteration 2 onward the previous iteration's first output image is fed back in as the I2I source instead of the original dropped image, using ComfyUI's `"name [type]"` annotated-filename reference (no re-upload needed); disabled together with the Image drop zone in T2I mode
|
||||
- **Workflow recall** (v0.3.90) — every saved plan records the filename of the workflow that was loaded in GenerateUI at save time (`workflow_filename` in the Plan JSON); after loading a plan, an **Open Workflow** button appears next to it — click to load that exact workflow file back into GenerateUI (asks for confirmation first, since it replaces whatever is currently loaded)
|
||||
- **Plan files** — Plan Save (silently overwrites the loaded plan) / Save As (always asks for a new name) / Plan Clear; plans are stored as `ws_labplan_<name>.json` in `user/default/Workflow-Studio/lab_plan/` (the `ws_labplan_` prefix is added automatically, v0.3.90 — type just the plain name when prompted), alongside an auto-generated `ws_labplan_<name>.png` index image (a 3-per-row contact sheet of up to 9 result thumbnails)
|
||||
- **Plan Load** — same drag-and-drop / click-to-browse drop zone as the Image slot; drop a plan's `.json` (read directly in the browser) or any of its index-image `.png`s — since v0.3.91 every index image carries the full plan embedded in it directly (see below), so it's read straight from the file with no server round-trip; older index images without that embedding fall back to fetching a same-named `.json` from the server
|
||||
- **Plan JSON tab** (v0.3.88) — view/edit the current plan as raw JSON (same shape saved to disk); Refresh re-reads it from the Setting tab, Apply to Setting parses your edits and writes them back into the Setting tab's columns/note/batch/chain-image/source-image/results — the same effect as loading that JSON as a plan file, without saving it first
|
||||
- **Save index image to Output on Run** (v0.3.90) — optional checkbox in the run controls; when checked, every completed Run also builds the same contact-sheet index image used for Plan files and saves it directly into ComfyUI's own Output folder as `Lab_index_<counter>_.png` (auto-numbered like a normal generated image, right alongside the images that Run just generated); independent of Plan Save, which always writes its own copy next to the plan file regardless of this checkbox; saved with the plan as `save_index_on_run`
|
||||
- **Index images are self-contained plan files** (v0.3.91) — every index-image PNG, whether from Plan Save/Save As or from Save index image to Output on Run, carries the entire plan embedded directly in the file as a PNG `wfm_lab_plan` chunk; this is what lets the Output-folder copy above work as a Plan Load source even though it has no matching `.json` anywhere
|
||||
- **Eagle integration** — every image Lab generates is auto-saved to Eagle the same way as GenerateUI's own Generate button, if enabled in the Settings tab; the Output-folder index image above is included too (v0.3.90) once saved
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Prompt Tab</h3></summary>
|
||||
|
||||
- **3-column layout** — AI Assistant (left), Presets/Preset Manager tab-panel (center), Wildcard support (right)
|
||||
- **AI chat assistant** — powered by [Ollama](https://ollama.com/), generate and refine prompts interactively
|
||||
@@ -141,18 +176,24 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **Wildcard input toolbar** — one-click buttons to insert `{|}`, `{n$|}`, `__|__`, `<lora::1:LBW=;>` and other wildcard syntax; wraps selected text when applicable
|
||||
- **Wildcard file manager** — create, view, and edit `.txt` / `.yaml` wildcard files stored in `user/default/Workflow-Studio/wildcard/`; click a filename in the file picker to insert `__filename__` at cursor
|
||||
|
||||
### Metadata Tab (v0.3.8)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Metadata Tab (v0.3.8)</h3></summary>
|
||||
|
||||
- **3-column layout** — Drop zone (left) | Model info (center) | LoRA + Prompt (right)
|
||||
- **File drop** — drop a ComfyUI-generated PNG / WebP or workflow JSON onto the drop zone (or click to open a file picker); PNG/WebP images are shown as a preview
|
||||
- **Model extraction** — automatically extracts Checkpoint, VAE, Diffusion Model, and Text Encoder names from the workflow; supports both standard and subgraph-based workflows (Flux.2 Dev/Klein, Qwen-Image-Edit/2511/Layered, Z-Image Base/Turbo, Ernie Image, WAN2.2); node types covered: UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, QuadrupleCLIPLoader (e.g. HiDream 4-CLIP)
|
||||
- **Model extraction** — automatically extracts Checkpoint, VAE, Diffusion Model, and Text Encoder names from the workflow; supports both standard and subgraph-based workflows (Flux.2 Dev/Klein, Qwen-Image-Edit/2511/Layered, Z-Image Base/Turbo, Ernie Image, WAN2.2, Mage-Flow, LongCat, Boogu, HiDream E1, FireRed); node types covered: UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, QuadrupleCLIPLoader (e.g. HiDream 4-CLIP)
|
||||
- **LoRA extraction** — lists all LoRA models with `strength_model / strength_clip` values
|
||||
- **Prompt extraction** — lists prompts with POS / NEG badges when positive/negative can be determined; when distinction is not possible (e.g. `SamplerCustomAdvanced`, intermediate nodes, cross-level connections), prompts are shown without a badge as plain **Text**; click any entry to view the full text below
|
||||
- **Prompt actions** — Copy to clipboard, **GenUI:P/N** (set GenerateUI positive/negative prompt), **Prompt:P/N** (set Prompt tab preset positive/negative)
|
||||
- **Format support** — ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Ernie Image / WAN2.2 subgraph workflows), SD WebUI, SD Forge, Fooocus
|
||||
- **Format support** — ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Ernie Image / WAN2.2 / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed subgraph workflows), SD WebUI, SD Forge, Fooocus
|
||||
- **Format note** — supported formats and covered model types are always shown in the left column
|
||||
|
||||
### Settings Tab
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Settings Tab</h3></summary>
|
||||
|
||||
- **2-column layout** — left column for all settings; right column shows the Theme panel fixed in place (sticky)
|
||||
- **Collapsible sections** — all settings organized in accordion panels for a clean layout
|
||||
@@ -173,9 +214,13 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **G'MIC-Qt Integration** — set the path to `gmic_qt.exe` for the Image Edit Filter tool; download G'MIC-Qt Standalone from `gmic.eu/download.html` (Windows: `gmic_qt-win64.zip`), extract anywhere, then enter the full path to `gmic_qt.exe` and click Save
|
||||
- **Language** — English / Japanese / Chinese
|
||||
|
||||
### Gallery Tab (v0.3.44)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Gallery Tab (v0.3.44)</h3></summary>
|
||||
|
||||
- **Image browser** — browse ComfyUI output images (Thumbnail / Table views) with server-side scanning optimized for 6,000+ image libraries
|
||||
- **Folder tree root label** (v0.3.90) — the root entry in the folder tree is labeled simply **[root]**, no longer suffixed with the scanned folder's own directory name (which could be a misleading, environment-specific name — e.g. StabilityMatrix installs where the output folder is a symlink named `Text2Img` — even though the folder holds every kind of generated output, not just text-to-image results)
|
||||
- **Thumbnail / Table views** — switch view modes; Favorites column shown leftmost in Table view
|
||||
- **Folder management** — create subfolders ("+ New") or delete the selected folder with all contents ("Del") from the folder tree header
|
||||
- **File operations** — move or delete individual images from the detail panel's Info tab; bulk Move To..., Export, and Delete File from the multi-select bar
|
||||
@@ -197,9 +242,13 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **Clear all filters (✕ Clear)** — rightmost toolbar button; resets search text, tag filter, group filter, and favorites filter in one click (sort order is preserved)
|
||||
- **Workflow auto-save** — images generated from the Generate UI tab have their workflow automatically saved to gallery metadata
|
||||
- **Output folder configurable** — set the scanned output folder from Settings tab
|
||||
- **SVG file support** (v0.3.77) — `.svg` files are browsable, previewable (detail panel and lightbox), and included in favorites/tags/groups/move/delete/export alongside raster formats; served as-is with no rasterized thumbnail, since `<img>` scales vector graphics natively at any size
|
||||
- **Performance** — server-side 256px JPEG thumbnail generation with disk cache (`data/thumb_cache/`); infinite-scroll paging (50 images per page, IntersectionObserver); folder-level mtime cache (60s TTL); bulk operations use single-request API endpoints
|
||||
|
||||
### Nodes Tab (v0.1.7)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Nodes Tab (v0.1.7)</h3></summary>
|
||||
|
||||
- **Node Browser** — browse all installed ComfyUI nodes from `/object_info` API with Card/Table views
|
||||
- **Search & Filter** — full-text search, filter by category, package, tags, groups, and favorites
|
||||
@@ -211,7 +260,10 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **Search clear (✕)** — inline ✕ button appears inside the search box whenever text is entered; click to clear immediately
|
||||
- **Clear all filters (✕ Clear)** — rightmost toolbar button; resets search text, category, package, tag, group, and favorites filter in one click
|
||||
|
||||
### Models Tab (v0.2.3)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Models Tab (v0.2.3)</h3></summary>
|
||||
|
||||
- **Model Browser** — browse all installed ComfyUI models (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding) with sub-tab switching
|
||||
- **Thumbnail / Table views** — switch between view modes with pagination (24 items per page); Table view includes **Type** and **Base Model** columns sourced from cached CivitAI data, displayed between the Subdirectory and Extension columns; Enable/Disable column labeled **E/D**
|
||||
@@ -236,7 +288,10 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **Search clear (✕)** — inline ✕ button appears inside the search box whenever text is entered; click to clear immediately
|
||||
- **Clear all filters (✕ Clear)** — rightmost toolbar button; resets search text, tag filter, folder filter, group filter, favorites filter, and status filter in one click
|
||||
|
||||
### Tagger Tab (v0.3.38)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Tagger Tab (v0.3.38)</h3></summary>
|
||||
|
||||
- **3 sub-tabs** — Single / Batch / DB for single-image tagging, folder batch processing, and tag database management
|
||||
- **Model support** — WD Tagger (ONNX, NCHW/NHWC auto-detect), SwinV2 (ONNX), DeepDanbooru (.h5, requires TensorFlow); place each model in its own subfolder under `ComfyUI/models/tagger/<model-name>/` containing the `.onnx` + `selected_tags.csv` (or `.h5` + label file)
|
||||
@@ -248,7 +303,10 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **DB tab** — searchable SQLite database of all tagged images; click a row to open the edit panel and modify WD Tags / VLM Tags; Save updates the record, Delete removes it; Export CSV downloads all records
|
||||
- **Dependencies** — install into ComfyUI's embedded Python: `python_embedded\python.exe -m pip install -r requirements.txt`; for GPU inference use `onnxruntime-gpu`; TensorFlow (DeepDanbooru) is optional and commented out in `requirements.txt`
|
||||
|
||||
### Image Edit Tab (v0.3.65)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Image Edit Tab (v0.3.65)</h3></summary>
|
||||
|
||||
- **Layer-based image editor** — compose images with multiple layers (Image, Text, Draw, Mask types) and export the composite as PNG
|
||||
- **Loading images** — drag & drop onto the canvas, Upload button, or send from the Gallery tab via the **Image Edit** toolbar button; first image becomes Layer 1 (auto-locked), subsequent images are added as new layers scaled to fit the canvas
|
||||
@@ -270,7 +328,10 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **Canvas navigation** — scroll-wheel zoom, Space + drag to pan; zoom indicator in the bottom bar
|
||||
- **Undo / Redo** — Ctrl+Z / Ctrl+Y (or toolbar buttons); keyboard shortcuts: V (Select), B (Draw), T (Text), S (Shape), Delete (remove selected layer when 2+ exist)
|
||||
|
||||
### AI TOOL Tab (v0.3.14)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>AI TOOL Tab (v0.3.14)</h3></summary>
|
||||
|
||||
- **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, LM Studio, or Lemonade; language selectors with ⇄ swap button (swaps both language selectors and text content); selections saved automatically
|
||||
@@ -280,6 +341,8 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **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
|
||||
- **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
|
||||
- **Chat pane — SVG generation, no ComfyUI workflow involved** (v0.3.77) — the LLM can also produce SVG graphics directly as text; if a reply contains SVG code (wrapped in a markdown code fence or written directly as a bare `<svg>...</svg>`), it's rendered inline as a preview and a "Save to Gallery" button writes it as a `.svg` file to the Gallery tab's output folder; unlike the Tool Calling image generation flow above, this is plain text pattern matching, so it works with any model regardless of tool-calling support
|
||||
- **Chat pane — Skills** (v0.3.79) — select a `.md` skill file from a dropdown above the message list to prepend its instructions to the conversation as a system prompt (not shown in the visible chat log); a pencil button opens an in-pane manager (list + inline editor, same pattern as the Prompt tab's wildcard file manager) to create, edit, or delete skill files stored in `user/default/Workflow-Studio/ai_skills/`; a file's `---`-delimited frontmatter (`name`/`description`) is shown in the dropdown; two starter skills are bundled — **SVG Icon Generator** (explicitly decomposes the subject into parts before drawing, which markedly improves recognizability from small local models) and **Skill Creator** (interviews you conversationally and drafts a new skill as a ```skill code block; a "Save as new skill" button appears under such replies and opens the manager pre-filled with the draft)
|
||||
- **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
|
||||
@@ -288,7 +351,10 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **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)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Workflow Studio Library (ComfyUI Side Panel) (v0.3.9)</h3></summary>
|
||||
|
||||
- **Tab layout (W / N / P / M / I / A)** — compact single-letter tabs with full name shown on hover
|
||||
- **W — Workflows tab** — browse favorite workflows (All / ★ Favorites / Groups / By Badge sub-tabs), ★ star shown for favorites in All view
|
||||
@@ -311,13 +377,18 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
- **Search** — search within each sub-tab to quickly find items
|
||||
- **⚙ Theme settings** — customize panel background, sub-header background, text, border, and secondary text colors; saved to localStorage and applied on every open
|
||||
|
||||
### Help & Support Tab (v0.1.3)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Help & Support Tab (v0.1.3)</h3></summary>
|
||||
|
||||
- **Sidebar navigation** — 2-column layout: left sidebar (15 topics) + right content pane; click any topic to switch the displayed content
|
||||
- **Support** — fixed at the bottom of the sidebar; shows GitHub, Ko-fi, **ComfyUI Image Feeder**, and **Mask Editor One** repository links in the right pane
|
||||
- **Feature list** — overview of all features organized by tab
|
||||
- **Keyboard Shortcuts** and **Troubleshooting** sections included
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
@@ -405,633 +476,6 @@ 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
|
||||
- **Image Edit — Inpaint: dedicated workflow** — "Use dedicated workflow" checkbox lets you pick a saved workflow to run the inpaint against instead of the one currently loaded in GenerateUI, without disturbing what's shown there
|
||||
- **Image Edit — Inpaint: Mask Editor One support** — workflows built around the `comfyui-mask-editor-one` custom node (no LoadImage) are also supported; image/mask are pushed via its node_id-keyed server cache and `layer_data` widget
|
||||
- **GenerateUI — Input/Image tab mask slots** — LoadImage nodes whose MASK output feeds an inpaint node (e.g. `VAEEncodeForInpaint`) now show an additional Mask drop zone; image + mask are composited into a single RGBA upload matching ComfyUI's native alpha→MASK extraction
|
||||
- New example workflows: `ws_inpaint_basic.json`, `ws_inpaint_mask_editor_one.json`
|
||||
|
||||
### v0.3.70
|
||||
|
||||
- **Workflow AI summary — Ollama 404 fix** — the summarize button in the workflow detail modal now uses the Ollama URL/model saved in Settings → AI Assistant Settings (previously it silently fell back to a hardcoded default model tag that didn't match locally installed models, causing a 404)
|
||||
- **Image Edit — New button auto-adds Layer 1** — creating a blank canvas via the New button now automatically adds an empty "Layer 1", matching the existing image-load behavior
|
||||
- **Image Edit — mask overlay shown only for the active mask layer** — previously every visible mask layer's overlay was rendered regardless of selection; now only the currently active mask layer shows its overlay, other mask layers stay hidden until selected
|
||||
- **GenerateUI — Revert button for Positive/Negative Prompt** — a ↺ button next to each Apply button reverts the textarea to the node's currently applied value
|
||||
- **GenerateUI — Ctrl+↑/↓ prompt emphasis weight editing** — A1111 / native ComfyUI CLIP Text Encode style: select text (or just place the cursor on a word/parenthesis block) and press Ctrl+↑/↓ to adjust `(text:weight)` by ±0.05; parentheses are removed automatically when the weight returns to 1.0
|
||||
|
||||
### v0.3.69
|
||||
|
||||
- **Gallery — Send CC button** — new toolbar button, shown only when the Gallery tab is embedded as an iframe inside ComfyUI Comic Creator (a separate custom node); sends the selected image directly into Comic Creator's currently selected panel/overlay via its existing `insertImageFromUrl(url)` helper (no changes required on the Comic Creator side)
|
||||
- **Lora batch — validation fix** — fixed a `Prompt outputs failed validation` error where LoRA files that actually existed were rejected by the standard `LoraLoader` node; the root cause was a path-separator mismatch (ComfyUI on Windows returns `\`-separated enum values while WFS tracked LoRAs internally with `/`); a shared `resolveLoraName()` helper now converts to ComfyUI's actual notation before applying, used consistently by Lora Single apply, Stack apply, the Models tab GenUI Model button, and Lora batch generation
|
||||
- **Lora batch — pre-flight check** — before running a batch on a standard `LoraLoader` workflow, selected LoRAs are checked against ComfyUI's current model list; if any are not yet recognized, a warning toast suggests a Refresh or ComfyUI restart (skipped for Lora Manager–based workflows, which have no enum constraint)
|
||||
- **Lora batch — automatic prompt sync** — for each LoRA applied during a batch, its syntax (`<lora:name:strength_model:strength_clip>`) and CivitAI trained words are automatically appended to the Positive prompt, rebuilt from the original prompt each time (not accumulated); the prompt is automatically restored to its pre-batch state when the batch completes, is stopped, or errors
|
||||
- **Models tab — Close button in detail modal** — added a Close button at the right end of the Save / GenUI PP·NP / Delete button row in the model detail modal (all sub-tabs), reducing mouse travel to the top-right ✕ during repeated batch edits
|
||||
- **Help** — new help entry (`helpGen19`) added in EN / JA / ZH describing the Lora batch prompt sync, restore, and pre-flight check behavior
|
||||
|
||||
### v0.3.68
|
||||
|
||||
- **Image Edit — MASK EDITOR ONE section always visible** — the ABR brush section in the Draw tool's (`MASK EDITOR ONE (COLOR)`) and Mask Paint mode's (`MASK EDITOR ONE`) Properties pane is now always shown instead of being hidden when no brushes are imported; the header text turns green when at least one ABR brush is available and gray when none are imported yet; the Select button is disabled until a brush exists
|
||||
- **Help — MASK EDITOR ONE status** — updated `helpImageEdit5` (Draw tool) and `helpImageEditMask4` (Mask Paint mode) in EN / JA / ZH to describe the always-visible section and its color-coded availability state
|
||||
|
||||
### v0.3.67
|
||||
|
||||
- **GenerateUI — Alt+Click Apply & Generate** — Alt+Click any Apply button to apply the parameter and immediately queue a generation in one step; applies to Positive Prompt Apply (Input tab), all model Apply buttons (Model tab: Checkpoint / VAE / Diffusion Model / ControlNet / Hypernetwork), and KSampler Apply (Settings tab); only triggers generation when the target node exists in the loaded workflow (no action on missing nodes); each Apply button shows a tooltip `Apply (Alt+Click: Apply & Generate)` on hover
|
||||
- **Help — Alt+Click Apply & Generate** — new help entry (`helpGen18`) added in EN / JA / ZH
|
||||
|
||||
### v0.3.66
|
||||
|
||||
- **Image Edit — Mask Tool: 5 new sub-tools** — Paint / Color / Alpha / Text / Vector / Shape / SAM3 (7 sub-tools total); Color, Alpha, Text, Vector, and Shape work without Mask Editor One
|
||||
- **Color** — click the canvas to flood-select pixels similar in color to the clicked point; Properties pane: Mode (Add / Subtract), Tolerance (0–255), Feather % (0–100)
|
||||
- **Alpha** — extract the active layer's alpha channel as a mask; Properties pane: Threshold, Invert checkbox, Extract Alpha button
|
||||
- **Text** — click to open a stamp overlay; enter text, set Font / Size / Bold / Italic / Align / Mode (Add / Erase), confirm with Stamp or Ctrl+Enter; multi-line text supported
|
||||
- **Vector** — click to add Catmull-Rom spline control points; Enter (or click the first point) closes and fills the polygon; Backspace removes the last point; Esc resets; Mode (Add / Erase)
|
||||
- **Shape** — drag to draw a rectangle or ellipse; hold Shift for a perfect square/circle; blue dashed outline preview while dragging; Properties pane: Mode (Add / Erase), Shape (Rect / Ellipse)
|
||||
- **Help — Support page** — added **ComfyUI Image Feeder** and **Mask Editor One** GitHub repository links to the Support card
|
||||
|
||||
### v0.3.65
|
||||
|
||||
- **Image Edit — Mask Editor One integration fixes** — `store_image` API field mismatch (`image_b64` → `bg_image_b64`) corrected so BiRefNet and SAM3 no longer report "no image available"; `server.py` updated to accept both field names for backward compatibility
|
||||
- **Image Edit — top bar icon persistence** — WFS / Snapshot / Node Sets SVG icons in the ComfyUI action bar disappeared when the Properties Panel was opened (Vue re-render reset custom `innerHTML` to `<i>`); fixed with a `MutationObserver` that re-applies SVG icons whenever a reset is detected
|
||||
- **Image Edit — Mask Tool: ABR brush library** (requires Mask Editor One) — Select button in the Properties pane opens a folder-tree + thumbnail-grid brush picker; stamp brushes are loaded from Mask Editor One's `/mask_editor/brushes/` folder; ✕ clears back to the default circle brush
|
||||
- **Image Edit — Mask Tool: ABR brush controls** — when an image brush is active, Hardness is disabled and new controls appear in the Properties pane: Spacing (stamp interval 5–100%), Angle (fixed rotation 0–359°), Sz Jitter (random size reduction per stamp 0–100%), Rot. Jitter (random angle per stamp on/off); size jitter clamped to minimum 1 px to prevent zero-dimension canvas errors
|
||||
- **Image Edit — Mask brush cursor** — brush size circle is now hidden outside the canvas; only shown when the pointer is within the canvas bounds (window-level mousemove no longer shows the cursor overlay outside)
|
||||
|
||||
### v0.3.64
|
||||
|
||||
- **Image Edit — Mask layer Add/Subtract** — A/S toggle button on each mask layer row; Add (green, default) composites the mask normally; Subtract (red) cuts the mask area from the layer below; multiple mask layers are composited back-to-front matching Mask Editor One's CanvasCompositor logic
|
||||
- **Image Edit — BiRefNet BG Remove** (requires Mask Editor One + `birefnet.safetensors` in `ComfyUI/models/background_removal/`) — BiRefNet model now fully functional via Mask Editor One's Python backend; grayscale mask returned as RGBA PNG and applied as a new layer or in-place per the New Layer toggle
|
||||
- **Image Edit — SAM3 segmentation** (requires Mask Editor One) — new SAM3 sub-tool in the Mask tool options bar; enter a text prompt, set Max candidates (3/6/9/12), click Segment; candidate mask thumbnails shown in the Properties pane; click to select (multi-select), Apply Selected (N) applies all chosen masks with Add or Erase mode; new mask layer auto-created if active layer is not a mask
|
||||
|
||||
### v0.3.63
|
||||
|
||||
- **Nodes tab — multi-select** — Ctrl+click to toggle individual nodes, Shift+click for range selection; bulk action bar appears with add to group / remove from group / create & add group / bulk favorite / unfavorite; works in both Card and Table view modes
|
||||
- **All tabs — search clear (✕)** — inline ✕ overlay button inside each search box (Workflow / Nodes / Models / Gallery); shown only when text is entered, click to clear and re-filter immediately
|
||||
- **All tabs — clear all filters button** — new ✕ Clear button at the rightmost end of each tab's toolbar; resets all active filters (search, group, tag, folder, favorites, badge) in one click; Gallery sort order is preserved
|
||||
|
||||
### v0.3.62
|
||||
|
||||
- **Gallery tab — Send GenUI Image button** — new toolbar button (next to Image Edit); uploads the selected image to ComfyUI and sets it as the input for the first LoadImage node in the GenerateUI tab; automatically switches to GenerateUI → Input → Image; `comfyEditor.applyImageToSlot(file, slotIndex)` added to `comfyui-editor.js` for reusable image-slot injection
|
||||
- **Bug fix: tagger_settings.json missing from data export** — `tagger_settings.json` was absent from `_DATA_FILES` in `settings_routes.py`; Tagger model directory and threshold settings were silently excluded from Settings → Data Management export/import; fixed by adding the file to the export list
|
||||
- **Help — Send GenUI Image** — new help card (`helpGallery17`) added in EN/JA/ZH; registered in `helpIdMap`
|
||||
- **Screenshots — Tagger and Image Edit tabs** — `docs/12_Tagger.png` and `docs/13_ImageEdit.png` added to the README screenshots section
|
||||
|
||||
### v0.3.61
|
||||
|
||||
- **Image Edit — Draw / Mask brush cursor** — cursor replaced with a size-accurate circle overlay matching the current brush size; the circle scales correctly with canvas zoom so the visible area matches exactly what will be painted
|
||||
- **Image Edit — Draw / Mask out-of-canvas strokes** — strokes can now start from the canvas margin area (outside the image boundary) and continue past the edge without interruption; `window`-level `mousemove` / `mouseup` tracking keeps the stroke alive when the pointer leaves the canvas element
|
||||
- **G'MIC — argument order fix** — corrected `gmic_qt.exe` launch arguments to `-o <output> <input>` (the only order G'MIC-Qt Standalone accepts); the previous order caused the GUI to exit immediately without opening
|
||||
- **G'MIC — settings path placeholder** — replaced the hard-coded user-specific default path with a generic `C:\path\to\gmic_qt.exe` placeholder; settings load without a pre-filled path for new users
|
||||
- **G'MIC — security: path traversal fix** — `POST /api/wfm/gmic/result` now validates that `result_path` is inside the `gmic_temp` directory before reading the file; requests for paths outside that directory are rejected with 403
|
||||
- **Help — G'MIC Filter section** — added two new help cards (G'MIC Filter usage and G'MIC-Qt Installation) with full i18n support (English / Japanese / Chinese)
|
||||
|
||||
### v0.3.60
|
||||
|
||||
- **Image Edit — Filter tool (G'MIC-Qt integration)** — the previously inactive ★ Filter tool now launches G'MIC-Qt GUI with the active layer image; select a filter and click OK, then close the "G'MIC-Qt filter output" window to apply the result back as the active layer; polling-based job system with progress display, Cancel button, and Undo support; requires G'MIC-Qt Standalone (`gmic_qt.exe`) configured in Settings
|
||||
- **Settings — G'MIC-Qt Executable Path** — new collapsible section in Settings for entering and saving the path to `gmic_qt.exe`
|
||||
|
||||
### v0.3.59
|
||||
|
||||
- **Image Edit — Mask Tool** — new Mask tool (🎭) in the Image Edit sidebar; click **M** in the Layers header to add a mask layer (Mask tool activates automatically); clicking a mask layer in the layer list also switches to the Mask tool; **Tool Options bar**: Paint button, Invert checkbox (inverts overlay preview), Overlay color picker (default red), Blur slider (0–50 px) for overlay edge softening; **Properties pane** (left of the Layers panel, shown when Mask tool is active): Size (1–200 px), Hardness (0–100%), Mode — **Add** / **Erase** toggle buttons (active mode highlighted in blue)
|
||||
- **Image Edit — Clipping mask (✂ button)** — each mask layer row shows a ✂ toggle; when enabled (blue), the mask clips the layer directly below it so only the painted area is visible; clipping is applied to PNG save / Save to Gallery / Send to ComfyUI exports
|
||||
- **Image Edit — Draw & Mask paint bugfix** — Draw and Mask tools now paint directly onto `layer.canvas`; all other layers remain fully visible during brush strokes (previously the composite view was replaced by the active layer only while painting)
|
||||
- **Image Edit — Layer panel width** — increased from 190 px to 220 px to accommodate the additional ✂ button on mask layer rows
|
||||
- **Help — Mask Tool section** — added Mask Tool card with full i18n support (English / Japanese / Chinese); updated Tools list, layer type icon list, and Layer panel "M button" description
|
||||
|
||||
### v0.3.58
|
||||
|
||||
- **GenerateUI Model tab — Diffusion Model GGUF support** — `LoaderGGUF` and `LoaderGGUFAdvanced` nodes are now recognized alongside `UNETLoader` / `UnetLoaderGGUF`; the correct input key (`gguf_name` vs `unet_name`) is selected automatically on Apply; the Models tab "GenUI Model" button also applies correctly to `LoaderGGUF` workflows
|
||||
- **GenerateUI Model tab — Text Encoder expanded (CLIPLoader / DualCLIPLoader / GGUF variants)** — the Text Encoder section now renders a node-type-aware UI: **single CLIP** (`CLIPLoader` / `ClipLoaderGGUF`) shows one clip selector + **type** dropdown + **device** (GGUF only); **Dual CLIP** (`DualCLIPLoader` / `DualClipLoaderGGUF`) shows two clip selectors (clip_name1 / clip_name2) + **type** dropdown + **device** (GGUF only); type options are fetched dynamically from `/object_info`; filter applies to the first selector only
|
||||
|
||||
### v0.3.57
|
||||
|
||||
- **Image Edit — Blur tool** — new Blur tool (≈) in the Image Edit sidebar; **Whole Blur**: Gaussian blur to the entire active layer with intensity slider (1–50 px); **Whole Mosaic**: pixelation mosaic with block-size slider (5–100 px); **Rect Blur / Rect Mosaic**: toggle to enter rect-draw mode then drag to select a region — blur or mosaic is applied on mouse release; Rect Blur and Rect Mosaic are mutually exclusive; all operations support Undo (Ctrl+Z)
|
||||
- **Image Edit — BG Remove tool** — new BG Remove tool (⬚); **Lightweight (@imgly)**: background removal running entirely in the browser via CDN (~40 MB model cached after first load); **BiRefNet**: stub wired to a future Python backend endpoint (coming soon); **New Layer** checkbox (default on) adds the result as a new layer above the original — uncheck to replace the active layer in-place; supports Undo
|
||||
- **Image Edit — sidebar icons** — removed `ie-tool-placeholder` dimming from Blur and BG Remove buttons; both now display at full brightness matching other active tools
|
||||
- **Help — Blur / BG Remove sections** — added two new help cards (Blur Tool and BG Remove Tool) with full i18n support (English / Japanese / Chinese); Tools list updated with Blur and BG Remove overview items
|
||||
|
||||
### v0.3.56
|
||||
|
||||
- **GenerateUI — Style selector** — new checkbox and dropdown next to the Reset Workflow button; when enabled, a Fooocus-style JSON style is applied to positive and negative prompts at generation time; style files (`*.json`) are read from `user/default/Workflow-Studio/style/`; multiple files are merged into a single list sorted by filename; the style's `{prompt}` placeholder is replaced with the original prompt text, otherwise the style text is appended; `negative_prompt` is appended to the existing negative prompt; styles are applied to a per-generation workflow copy and never modify the loaded workflow
|
||||
- **Batch tab — Style batch** — new **Style** tab in the Batch left pane lists all styles loaded from the style directory (flat list with All / None buttons); select any combination, enable the **Style** column in Batch Queue, then Generate to run each selected style sequentially; Batch Queue expanded from 6 to 7 columns (Style added as the rightmost column)
|
||||
|
||||
### v0.3.55
|
||||
|
||||
- **Group feature — fix data loss on Windows (backslash/slash mismatch)** — on Windows, ComfyUI returns model paths with backslashes (`sub\model.safetensors`) while the backend normalized to forward slashes; this caused all subdir group members to be flagged as missing and deleted on save; fixed by normalizing both stored and scanned paths to forward slashes in `get_model_groups` and `save_model_groups`
|
||||
- **Group feature — network drive protection** — if no model directories are accessible (e.g. external drive disconnected), the group cleanup step is now skipped entirely to prevent wiping all group memberships
|
||||
- **Group feature — scan cache** — `_scan_model_names` now caches results for 30 seconds to eliminate redundant full directory scans on startup (caused high CPU load / crash on large model sets)
|
||||
- **Group feature — RESERVED_GROUPS protection** — `bulkRemoveFromGroup` and `renderPmGroups` now prevent Batch/Stack reserved group keys from being deleted even when all members are removed
|
||||
- **Group feature — stale groupFilter reset** — after deleting a group, `state.groupFilter` is automatically cleared if it pointed to the now-deleted group (previously showed 0 models)
|
||||
- **Group feature — toggleGroupEnable partial failure** — only models listed in `data.ok` are updated client-side, preventing state divergence when some enable/disable calls fail
|
||||
- **GenerateUI batch groups — restore Batch/Stack visibility** — reverts an incorrect filter that hid Batch and Stack groups from the batch group selection UI; these groups are now selectable for batch execution as intended
|
||||
- **GenerateUI LoRA groups — path normalization** — backslash-to-slash normalization added after fetching LoRA groups from the API, matching the Checkpoint fix
|
||||
- **Batch complete toast — i18n** — replaced hardcoded English toast with `t("batchComplete", ...)` supporting all 3 languages
|
||||
- **Prompt tab migration — fix negText loss** — `migrateLocalStoragePresets` now correctly passes `negText` when calling `apiCreatePreset`, preventing negative prompts from being silently dropped during localStorage→API migration
|
||||
- **Help tab — Image Edit section i18n** — the Image Edit help page was hardcoded in English with no language switching support; all 6 sections (30 items + 7 headings) now have i18n IDs wired to English / Japanese / Chinese translations
|
||||
|
||||
### v0.3.54
|
||||
|
||||
- **Image Edit — Shape tool (S)** — new Shape tool added to the Image Edit toolbar; draw Rect, Ellipse, Line, and FreeLine shapes by dragging; each committed shape becomes an independent draw layer that can be undone in a single step; Tool Options: shape type selector, Rounded toggle (Rect/Ellipse only), Fill color + None toggle (hidden for Line/FreeLine), Stroke color + width (Stroke None hidden for Line/FreeLine where stroke is always active), Opacity slider, Undo button; blue dashed outline previews the shape during drag; keyboard shortcut **S** activates the tool
|
||||
- **Models tab — fix B/S overlay buttons not highlighting** — the Batch (B) and Stack (S) card overlay buttons failed to turn yellow after clicking because `e.currentTarget` is nulled by the browser after an `async` event handler yields at `await`; fixed by capturing `const btn = e.currentTarget` before the await in both handlers
|
||||
|
||||
### v0.3.53
|
||||
|
||||
- **Image Edit Tab** — new layer-based image editing tab with Select / Draw / Text tools, multi-layer compositing, canvas zoom & pan, and Undo/Redo; text layers are sized to exact bounding-box dimensions and regenerated at display resolution on resize to prevent blurring; double-click a text layer to re-edit its content; Gallery tab gains an "Image Edit" toolbar button to send the selected image directly to the editor
|
||||
- **Image Edit — Layer lock** — lock icon (🔒/🔓) on each layer row; locked layers display an orange bounding box and ignore all Select-tool transforms; Layer 1 is auto-locked when the first image is loaded to prevent accidental moves
|
||||
- **Image Edit — Save to Gallery** — "Save to Gallery" button (next to Save PNG) saves the composite image to the Gallery root folder; filename defaults to `wfs-image-YYYYMMDDHHmmss`; backed by new `POST /wfm/gallery/image/save` API endpoint
|
||||
- **Help tab — Image Edit page** — new dedicated help page covering tools, layer panel, text quality, export options, and keyboard shortcuts
|
||||
|
||||
### v0.3.52
|
||||
|
||||
- **GenerateUI Model tab — fix LoRA selection reset when switching subtabs** — applying a LoRA via "GenUI Model" button and then switching to another subTab (Prompt/Image/Settings) and back to Model caused the LoRA selector, syntax display, and trigger words to revert to the original workflow values; fixed by reading `currentVal` directly from `currentWorkflow` instead of the stale `currentAnalysis`, and by saving/restoring the Single tab's syntax and trigger words across re-renders
|
||||
|
||||
### v0.3.51
|
||||
|
||||
- **Models tab — Embedding type: GenUI PP / GenUI NP buttons** — Embedding models now show two buttons ("GenUI PP" and "GenUI NP") in the side panel nav, detail side panel, and detail modal (replacing the single "GenUI Model" button); clicking either appends `(embedding:Name:1.0)` to the Positive or Negative prompt of the loaded workflow
|
||||
- **GenerateUI Prompt tab — Embeddings selector** — new section at the bottom of the Prompt tab: Filter input, model select dropdown, Weight input (default 1.0), and Paste button; Paste inserts `(embedding:Stem:weight)` at the cursor position of the last focused textarea (Positive or Negative); focus state is tracked on click/keyup/blur so the button works even after clicking away
|
||||
- **GenerateUI Model tab — Hypernetwork support** — Hypernetwork section added with model selector (filter) and Strength field; applies to `HypernetworkLoader` nodes in the workflow; GenUI Model button in Models tab also triggers Hypernetwork apply
|
||||
- **Models tab — fix Embedding and Hypernetwork models not shown** — `_fetchModelList` now handles the ComfyUI V3 node format (`options` key) in addition to V1/V2 formats; `fetchEmbeddings` switched from the `/embeddings` ComfyUI endpoint to the WFS backend API (`/api/wfm/models/files?type=embedding`) to avoid SPA URL routing conflicts
|
||||
- **Models tab — GenUI Model button in side panel nav** — "GenUI Model" button added to the tab navigation bar (Info / Group / CivitAI) of the side panel; appears for supported model types (Checkpoint, LoRA, VAE, etc.) and hides for unsupported types; Embedding type shows PP/NP nav buttons instead
|
||||
|
||||
### v0.3.50
|
||||
|
||||
- **Gallery tab — fix prompt search causing ComfyUI hang under large folders** — the v0.3.49 on-demand metadata read ran synchronously on the aiohttp event loop, blocking ComfyUI when folders contained many uncached images; fixed by: (1) removing on-demand reads from `list_images()`; (2) wrapping `list_images()` in `asyncio.to_thread()` to keep the event loop free; (3) adding a background indexer (`start_background_index`) that automatically indexes uncached images after a folder is loaded — processes 10 images per batch with a 50 ms sleep between batches; cancellable on folder switch
|
||||
|
||||
### v0.3.49
|
||||
|
||||
- **GenerateUI tab — A1111-style SPA-side wildcard expansion** — when prompt text in non-ImpactWildcard nodes (e.g. CLIPTextEncode) contains `__name__` syntax, the SPA expands it at generation time by randomly picking a line from the matching file in the WFS wildcard folder; nested wildcards are re-expanded up to 5 passes; unresolved wildcards are passed through unchanged; ImpactWildcardEncode/Processor nodes are skipped (handled server-side by Impact Pack); works in both single and batch generation
|
||||
- **Gallery tab — fix Export button missing label** — the bulk action bar's Export button had no text label because the `textContent` assignment was missing from `updateUITexts()`; fixed by adding the `galleryBulkExport` i18n key assignment alongside the existing Move/Delete entries
|
||||
- **Gallery tab — fix prompt search not matching images with no cached index** — `prompt_cache` was only built when an image was opened in the detail panel; A1111 images (or any images never opened) were invisible to prompt search; now when search is active and `prompt_cache` is empty, the image file is read on-demand and the cache is populated and persisted for future searches
|
||||
|
||||
### v0.3.48
|
||||
|
||||
- **Gallery tab — single image download** — hover over a detail-panel preview image to reveal a download overlay (⬇ icon); click to download the original file with its original filename preserved
|
||||
- **Gallery tab — bulk export to ZIP** — multiple selected images can be exported as a ZIP archive via the "Export" button in the bulk action bar; filename: `gallery_export_<timestamp>.zip`; backend: `POST /wfm/gallery/images/export-zip` with path-traversal protection
|
||||
|
||||
### v0.3.47
|
||||
|
||||
- **Models tab — fix stale group state after file move** — when models were moved to a subdirectory via "Move To...", `state.modelGroups` in the JS client was not updated to reflect the new model paths; the group filter view would lose the moved models immediately after the operation. Now the client-side group membership is updated in sync with the server-side rename (mirrors the existing Python `move_models` logic). Generate tab Batch groups are unaffected as they always fetch fresh from the server.
|
||||
|
||||
### v0.3.46
|
||||
|
||||
- **Prompt tab AI Assistant — LM Studio support** — switched from Python proxy (`/api/wfm/ollama/*`) to direct browser-to-backend API calls (same pattern as the AI TOOL tab); supports Ollama (`/api/chat`) and LM Studio (`/v1/chat/completions`); image attachments are automatically converted to the correct format per backend
|
||||
- **Settings tab — AI Assistant Settings (Prompt Tab)** — renamed from "Ollama Settings"; added backend selector (Ollama / LM Studio), URL input with per-backend placeholder, model select with Refresh, and Test button; settings saved to localStorage `wfm_prompt_ai_settings` (independent of AI TOOL tab settings)
|
||||
- **Auto-refresh on backend change** — if the backend is switched in Settings tab while Prompt tab is already open, `sendMessage()` now detects the mismatch and automatically re-fetches the model list before sending; model status shows the active backend name (`[ollama]` / `[lmstudio]`)
|
||||
- **Help text updated** (EN/JA/ZH) — Prompt tab, Settings tab, and Troubleshooting sections updated to reflect Ollama / LM Studio dual-backend support
|
||||
|
||||
### v0.3.45
|
||||
|
||||
- **Group auto-cleanup — Models tab** — stale group entries (models that no longer exist on disk) are automatically removed when the Models tab is loaded or the model type is switched; models moved within the app via "Move To..." also update their group membership immediately
|
||||
- **Group auto-cleanup — Gallery tab (folder view)** — metadata entries for missing files are cleaned up each time a folder is scanned (`list_images`), synchronized with the 60s TTL + folder-mtime cache
|
||||
- **Group auto-cleanup — Gallery tab (group image list)** — `list_images_in_group` (used by the Feeder tab Gallery mode) now filters out non-existent paths and removes them from the group in a single write operation
|
||||
- **Gallery tab — Shift+click range selection** — Shift+click selects all images between the last clicked item (anchor) and the current item; works in both thumbnail and table views; anchor is updated on Ctrl+click and regular click; resets on folder change, image reload, and Deselect All; help text updated (EN/JA/ZH)
|
||||
|
||||
### v0.3.44
|
||||
|
||||
- **Gallery tab — server-side thumbnail generation** — `GET /wfm/gallery/image/thumb?path=...&w=256` endpoint generates 256px JPEG thumbnails via Pillow and caches them to `data/thumb_cache/` keyed by `md5(path:mtime:width)`; GIF served as-is to preserve animation; falls back to the original file if Pillow is unavailable; thumbnail view and table view both switched from `/image/serve` to `/image/thumb`
|
||||
- **Gallery tab — infinite-scroll paging** — thumbnail view renders 50 images initially; an IntersectionObserver sentinel (rootMargin 300px) appends the next page on scroll; sentinel is disconnected and removed when all images are rendered; paging resets on every `renderImages()` call (folder change, filter, sort)
|
||||
- **Gallery tab — bulk API endpoints** — `POST /wfm/gallery/bulk/favorite` and `POST /wfm/gallery/bulk/group` handle favorite set and group add/remove for any number of images in a single request; replaced N-parallel individual fetches in `bulkSetFavorite()` and `bulkAddToGroup()`
|
||||
- **Gallery tab — image compare mode** — Ctrl+click 2–4 images, then click "Compare" in the Bulk Bar to open a side-by-side lightbox; CSS grid adapts to the image count via `--compare-cols` custom property; close button is `position:fixed` at the top-right corner to avoid `overflow:auto` clipping
|
||||
- **Gallery tab — "Remove from Group" bulk button** — new button in the Bulk Bar (right of "Add to Group"); uses the same group selector dropdown; calls `POST /wfm/gallery/bulk/group` with `action:"remove"`
|
||||
- **Gallery tab — bulk bar UI labels** — dropdown placeholder "Add to Group..." → "Select Group"; "Add" button → "Add to Group"; "Delete" button → "Delete File" (distinguishes file deletion from group removal)
|
||||
- **Gallery tab — prompt search** — text search now covers A1111 `parameters` text embedded in PNG/JPEG metadata; prompt text is extracted on first detail-panel open and stored as `prompt_cache` in `metadata.json`; subsequent `list_images` searches include `prompt_cache` without re-reading image files
|
||||
- **Help tab — Gallery mode i18n** — Feeder subtab Gallery mode section (h4, description, 6 bullet points) now translated to Japanese and Chinese via `i18n.js`
|
||||
- **Help tab — font size ×1.5** — nav items, headings, body text, link cards, and support card text all scaled up in `main.css`
|
||||
- **Help tab — search box** — live search input above the nav sidebar filters matching pages and auto-clicks the first result; placeholder text localized (EN/JA/ZH)
|
||||
|
||||
### v0.3.43
|
||||
|
||||
- **Send to Canvas — direct canvas load via `window.opener`** — clicking "Send to Canvas" (Workflow tab) or "Copy & Send Canvas" (Gallery tab) now sends the workflow directly to the ComfyUI canvas without requiring a title drag; uses `window.opener.wfmReceiveWorkflow()` registered by `node_sets_menu.js` via `app.handleFile()`; both UI and API formats are supported; fallback to localStorage + title drag when `window.opener` is unavailable (e.g. opened from a bookmark), UI-format only
|
||||
- **Workflow Studio Library — API-format DnD enabled** — API-format workflows in the W (Workflows) sub-tab are no longer grayed out; drag & drop and double-click now work for all formats via `app.handleFile()`
|
||||
|
||||
### v0.3.42
|
||||
|
||||
- **Feeder tab — Gallery mode** — new [Image Loop] / [Gallery] toggle in the Feeder tab; Gallery mode drives a WFS_GalleryFeeder node without any external plugin: node selector, group select, sort mode / index / seed, After Gen (Loop / Increment / Fixed), Run / Stop; center pane shows the selected group's images — click any image to jump to that index; `__Feeder__` group is auto-created on first open
|
||||
- **WFS_GalleryFeeder custom node** — new ComfyUI node (Workflow Studio category) that reads images from a Gallery group and outputs one `IMAGE` at a time; inputs: `group_name`, `index`, `sort_mode` (filename_asc / filename_desc / random), `seed`; skips missing files; `IS_CHANGED` ensures re-execution on every queue
|
||||
- **WFS_GalleryFeeder — ComfyUI canvas controls** — `gallery_feeder_extension.js` adds After Gen combo (loop / increment / fixed), ▶ Run button, and ■ Stop button as non-serialized widgets directly on the node in the ComfyUI canvas; manages per-node loop state with `waitForExecution()` Promise pattern; `onRemoved` hook auto-stops the loop on node deletion
|
||||
- **Gallery tab — thumbnail F button** — cyan overlay button (top-left of each image card) toggles `__Feeder__` group membership; active state is always visible in cyan, inactive shows only on hover; initial state derived from `img.groups` returned by `list_images()`
|
||||
- **Gallery tab — `__Feeder__` reserved group** — shown with 🔒 prefix in group dropdowns; rename and delete blocked server-side (403) and client-side (buttons disabled); **FC** toolbar button clears all group members without deleting the group
|
||||
- **Security: XSS fixes in `gallery-tab.js`** — tag names, filenames, and server error messages now escaped via `escapeHtml()` before insertion into `innerHTML` (tag badge list, detail panel `alt` attributes, lightbox footer, folder tree errors, grid errors); `util.js` `escapeHtml` updated to also escape single quotes (`'` → `'`)
|
||||
- **Security: path validation in `gallery_service.py`** — `save_image_meta`, `toggle_favorite`, `add_to_group`, `remove_from_group` now call `_check_path_allowed()` before any file operation; previously these four methods were missing the guard that other write methods already had
|
||||
- **Security: `_init_allowed_root()` in `gallery_routes.py`** — `_allowed_root` is now initialized at module load from saved `gallery_output_dir` (falls back to ComfyUI default output dir); prevents `list_folder_tree` from overriding `_allowed_root` via a crafted `?root=` request, and also fixes serve_image returning 404 in Feeder Gallery mode before the Gallery tab is opened
|
||||
- **Bug fix: WFS_GalleryFeeder seed validation error** — seed `max` changed from `0x7FFFFFFF` to `0xffffffffffffffff` (matching KSampler); `applySeedToWorkflow()` was writing values exceeding the old max, causing `value_bigger_than_max` validation failures
|
||||
|
||||
### v0.3.41
|
||||
|
||||
- **Generate UI — ImpactWildcardEncode / ImpactWildcardProcessor prompt detection** — fixed: wildcard text inputs from comfy-impact-pack nodes were not shown in the Prompt tab of Generate UI
|
||||
- `CLIPTextEncodeEditPlus` node (used as the text encoder in these workflows) now correctly propagates positive/negative roles upstream to ImpactWildcard nodes via BFS
|
||||
- `CLIPTextEncodeEditPlus` with an empty `text_edit` field is no longer added to `prompt_nodes` ahead of the upstream ImpactWildcard node, preventing a blank text area
|
||||
- `widgets[0]` is now always written to `wildcard_text` in the API conversion step, regardless of `object_info` widget-order differences between impact-pack versions
|
||||
- Static fallback mapping added for `ImpactWildcardProcessor` and `ImpactWildcardEncode` so detection works even when the ComfyUI server is offline
|
||||
|
||||
### v0.3.40
|
||||
|
||||
- **AI TOOL tab — Chat pane** — new pane (between Translation and TOOLS) for multi-turn conversation with the LLM; full conversation history sent each turn for context; Enter to send, Shift+Enter for newline; Clear button resets history; Ollama uses `/api/chat`, LM Studio uses `/v1/chat/completions`
|
||||
- **AI TOOL tab — Wildcard generation** — new "Create wildcards" task in the TOOLS pane dropdown; enter a category name and count, click Run to generate plain-text entries one per line (no markdown, no numbering); output can be pasted directly into wildcard `.txt` files
|
||||
- **Library A tab — Chat and Wildcards** — both features added to the side panel's A tab: Chat as a new sub-tab (Translation → Chat → TOOLS → Settings), wildcard generation as a new option in the TOOLS sub-tab dropdown
|
||||
- **Help updated** — AI TOOL tab help card updated to 6 items (Chat and Wildcards added); Library panel sidepanel-16 updated; EN/JA/ZH
|
||||
|
||||
### v0.3.39
|
||||
|
||||
- **Workflow tab — Send to Canvas button** — replaced "Open in ComfyUI" (toolbar and detail modal) with "Send to Canvas"; stores the selected workflow in `localStorage` for title-drag to the canvas without opening a new browser tab
|
||||
- **Gallery tab — Copy & Send Canvas button** — renamed "Copy Workflow" to "Copy & Send Canvas"; copies the embedded workflow JSON to the clipboard and simultaneously stores it for title-drag
|
||||
- **Workflow Studio Library — title drag** — after "Send to Canvas" or "Copy & Send Canvas" is clicked, the "Workflow Studio Library" panel title turns blue with a green ● indicator; drag the title onto the ComfyUI canvas to load the workflow; title resets automatically after loading
|
||||
|
||||
### v0.3.38
|
||||
|
||||
- **Tagger tab** — new tab for automatic image tagging; supports WD Tagger (ONNX), SwinV2, DeepDanbooru (.h5), and Ollama VLM; 3 sub-tabs: Single (drag & drop / upload, 5 output targets), Batch (folder processing with real-time progress), DB (searchable SQLite database with CSV export)
|
||||
- **Tagger single — GenUI:P button** — new button (left of "Send to Prompt") that appends generated tags to the GenerateUI positive prompt and immediately applies them to the loaded workflow; shows a warning if no workflow is loaded in GenerateUI
|
||||
- **Tagger batch — Write .txt output** — new output option that creates `<filename>.txt` alongside each processed image containing all generated tags (comma-separated)
|
||||
- **Gallery detail panel — Tagger button** — new action button in the Gallery detail panel opens the selected image directly in the Tagger tab for instant single-image tagging
|
||||
- **Gallery detail panel layout** — tabs (Info / JSON / Groups) and action buttons (Metadata / Load GenUI / Tagger) are now on separate rows, preventing button overflow on narrow panels
|
||||
- **Gallery — Save to Gallery tags fix** — tags saved from the Tagger tab were stored as a string instead of an array, causing `forEach is not a function` errors on the next Gallery refresh; now correctly stored as a string array
|
||||
- **GenerateUI — ZIT/Lumina2 workflow prompt fix** — `ConditioningZeroOut` removed from the CONDITIONING passthrough set; previously it propagated the "negative" role upstream through the shared CLIPTextEncode node, marking it as "unknown" and hiding the positive prompt in the GenerateUI Input tab; ZIT (Z-Image Turbo) workflows now correctly show the positive prompt
|
||||
- **Workflow analyzer — lumina2 model type** — CLIPLoader `type: "lumina2"` is now correctly classified as Z-IMAGE in the workflow type detector
|
||||
|
||||
### v0.3.37
|
||||
|
||||
- **Models tab — Select All button** — new button added to the right of "Deselect All" in the multi-select action bar; selects all models currently visible after applying the active search / filter / group filters
|
||||
- **Gallery tab — Select All button** — new button added to the right of "Deselect All" in the bulk action bar; selects all images in the current folder view
|
||||
- **Gallery bulk bar — i18n** — all bulk action bar buttons and the selection count were hard-coded English strings; they now use the `t()` translation system (EN / JA / ZH) so the bar respects the selected UI language
|
||||
|
||||
### v0.3.36
|
||||
|
||||
- **GenerateUI Save — overwrite protection** — saving now checks for an existing file and asks for confirmation before overwriting; if the target file is in UI format, a dedicated warning explains that saving in API format will lose the node layout
|
||||
- **GenerateUI Save — Raw JSON sync** — unapplied Raw JSON edits are now included in the save (parsed and synced back to the editor); invalid JSON aborts the save with an error instead of silently saving stale content
|
||||
- **GenerateUI Save — hardening** — filename is set via DOM (no HTML-attribute injection), image extensions (`.png` etc.) are stripped from the default name, and a double-submit guard prevents duplicate POSTs
|
||||
- **Help i18n fixed** — v0.3.35 help text updates (Save button, table sort, JSON tab rename) were being overwritten at startup by stale i18n strings and never displayed; all help keys synced in EN / JA / ZH and new entries registered in the help ID map
|
||||
- **Toast i18n** — ~115 hard-coded English toasts replaced with translated messages (~60 new shared keys in EN / JA / ZH); Settings labels for Text Size / RAW JSON Colors that displayed raw key names are now properly defined
|
||||
- **Refactoring** — new shared `util.js` (`escapeHtml` unified across 5 files — the Metadata tab copy was missing `"` escaping; `readJsonStorage` / `getSettings` replace 15 direct `localStorage` parses); Models table sort now precomputes sort keys (faster on large libraries); Gallery action buttons moved off the tab-button class, removing all `!important` CSS overrides
|
||||
- **README refreshed** — intro rewritten around the three pillars (Management / GenerateUI / Library side panel); new screenshots: GenUI LoRA Stack, GenUI Batch, Models Multi-select Menu; WS Library image updated
|
||||
|
||||
### v0.3.35
|
||||
|
||||
- **Models tab — Table column sort** — click any column header to sort ascending (▲), click again for descending (▼), click a third time to clear; active column name highlighted in accent color; Enable/Disable column header now labeled **E/D**
|
||||
- **Gallery tab — Load GenUI button** — new button to the right of the Metadata button in the detail panel; loads the embedded ComfyUI workflow from the selected image directly into the GenerateUI tab; warning toast shown if no workflow is embedded or the format is unsupported; Metadata button restyled green, Load GenUI uses primary accent color
|
||||
- **GenerateUI tab — Save button** — new button at the right end of the subtab row; opens a filename dialog (default: current workflow name) and saves the current workflow as a `.json` file to the Workflow tab via the import API; workflow name label updates to the saved filename on success
|
||||
|
||||
### v0.3.33
|
||||
|
||||
- **LoRA pane — workflow load no longer overwrites LoraManager** — removed the auto-apply block that replaced `inputs.loras` / `inputs.text` with Stack group contents on every workflow load; Stack is now applied only when the Apply button is pressed
|
||||
- **LoRA pane — 🔄 button no longer resets to Single tab** — active tab state is saved before `innerHTML` rebuild and restored afterwards; staying on Stack tab after refresh
|
||||
- **Models tab — Stack group hidden for non-LoRA types** — when switching to Checkpoint or other non-LoRA types, the Stack group is now excluded from the group filter dropdown and side-panel group management (LoRA-only concept)
|
||||
- **CLIPTextEncodeEditPlus widget mapping fix** — `convertUiToApi` no longer advances `wIdx` for UI slot inputs that have an actual link, preventing index misalignment when `object_info` lists optional STRING inputs (e.g. `text1`) before widget inputs (e.g. `text_edit`); fixes `+af` appearing as the positive prompt instead of the edit text
|
||||
- **Stack Apply — trigger words now applied correctly on first press** — trigger words (`currentAllTriggers` / `currentActiveTriggers`) are now recomputed at click time from the latest `_stackActive` + `metadata` + `civitaiCache`; previously used a stale snapshot captured at render time, causing trigger words to be missing on the first Apply and to linger after disabling a model
|
||||
|
||||
### v0.3.32
|
||||
|
||||
- **Lora Loader (LoraManager) LoRA detection** — I tab and Metadata tab now correctly detect LoRAs from `Lora Loader (LoraManager)` nodes in API format (`inputs.loras.__value__`); `strength`/`clipStrength` stored as strings (e.g. `"0.20"`) are now parsed correctly
|
||||
- **LoraManager node placement — LoRA syntax** — placing a `Lora Loader (LoraManager)` node via drag now also updates the `text` widget with `<lora:name:strength>` syntax (previously only the `loras` array widget was set)
|
||||
- **M tab Groups — LoRA → LoraManager drop** — LoRA groups in the Library M tab show an **All N LoRAs** item; drag to canvas places a `Lora Loader (LoraManager)` node with all group LoRAs; double-click also places at canvas center
|
||||
- **P tab — Groups sub-tab** — new Groups sub-tab (row 2) in the Library P tab; groups are read from `localStorage["wfm_prompt_preset_groups"]` (shared with Batch); expanding a group shows draggable preset items
|
||||
|
||||
### v0.3.31
|
||||
|
||||
- **LoRA pane — Single/Stack tabs** — LoRA pane split into two tabs: Single (filter, model dropdown, M/C strength, target node, Apply, P, LORA SYNTAX, TRIGGER WORDS) and Stack (target node, Apply, P, Toggle-all, global strength adjuster, LORA SYNTAX, TRIGGER WORDS, model list); each tab has independent strength settings
|
||||
- **GenUI Model — LoRA improvements** — now correctly handles `Lora Loader (LoraManager)` nodes (previously failed to find the node); on apply: switches to Single tab, disables all Stack models, and updates LORA SYNTAX / TRIGGER WORDS in the Single tab panel
|
||||
|
||||
### v0.3.30
|
||||
|
||||
- **LoRA Stack trigger words fix** — inactive stack models no longer show their trigger words in the Trigger words display; both initial render and per-model toggle/toggle-all now filter to active models only
|
||||
- **Help troubleshooting** — added entry for "No such file or directory" error when running LoRA Stack (comfyui-lora-manager compatibility issue)
|
||||
|
||||
### v0.3.29
|
||||
|
||||
- **LoRA section unified** — single LoRA and Stack sections merged into one pane; Apply button handles both cases (stack models present → apply all active + prompt sync; no stack → apply selected single LoRA + prompt sync); Ctrl+Apply removed, now plain Apply by default
|
||||
- **LoRA prompt sync** — Apply inserts `<lora:stem:M:C>` syntax and CivitAI trigger words into the Positive prompt; re-applying updates the prompt by diff (removes stale tokens, re-adds active ones); works for both single LoRA and Stack
|
||||
- **P button** — placed to the right of Apply; immediately writes the current Positive prompt textarea to the target workflow node without switching tabs
|
||||
- **LoRA UI layout fixes** — "Stack" label moved from ID row to Strength row left of the toggle-all checkbox; ID dropdown expanded to `flex:1` (no max-width cap); M/C input width 54 px → 60 px to prevent value truncation
|
||||
- **Models tab — CivitAI default** — side panel opens to the CivitAI tab by default when a model is selected
|
||||
- **Help tab — sidebar navigation** — redesigned as 2-column layout: left sidebar (14 topics + Support fixed at bottom) + right content pane; content switches on click
|
||||
- **Input tab — Prompt/Image tabs + taller textboxes** — Prompt and Image split into inner tabs; Positive prompt rows 4 → 8, Negative prompt rows 3 → 6
|
||||
- **Reset Workflow button** — added to the GenerateUI toolbar; re-reads the loaded workflow file from disk and reloads the editor
|
||||
- **LoRA Stack trigger words** — CivitAI trigger words for active stack models appended to Positive prompt on Apply; diff-updated on re-apply
|
||||
- **Feeder subtab UI** — (root) folder selected automatically on tab open; PREVIEW pane moved to the far right of the layout; preview auto-updates to the current processing image during a RUN
|
||||
- **Batch tab — Sampler / Scheduler** — left pane gains Sampler and Scheduler tabs (lists fetched from ComfyUI); Batch Queue gains two new columns; batch runs each value through KSampler sequentially
|
||||
- **Lora Loader (LoraManager) support** — workflow analysis, single Apply, Stack Apply, and Batch all write `inputs.loras.__value__` array + `inputs.text` syntax for LoraManager nodes
|
||||
- **Stack enable/disable** — per-model checkbox and Toggle All checkbox; disabled models excluded from syntax/prompt; LoraManager apply preserves them as `active: false`
|
||||
- **Global strength adjuster** — `[−][step][+]` controls for Model and CLIP strength applied to all stack models at once
|
||||
- **model-and-prompt-from-metadata node support** — `CLIPTextEncodeEditPlus`, `ImageMetadataCheckpointLoader`, `ImageMetadataPromptLoader`, `ImageMetadataLoRALoader` added to workflow analysis
|
||||
- **Models thumbnail bug fixes** — preview endpoint now sends `Cache-Control: no-cache`; files under 100 bytes skipped; `.disabled` model files included in preview change lookup
|
||||
|
||||
### v0.3.23
|
||||
|
||||
- **Bug fix: workflow batch 404** — `_runBatchGenerate()` workflow case was fetching `/api/wfm/workflows/{filename}` (non-existent route); corrected to `/api/wfm/workflows/raw?filename={filename}`
|
||||
- **Bug fix: rename double-submit** — `addEventListener("blur", commitRename)` was accumulating on the persistent `#wfm-modal-title-input` element each time the modal opened; subsequent renames fired the handler twice, sending two rename requests back-to-back (first succeeds, second returns 409 or 404); fixed by storing the listener reference on `titleInput._commitRename` and calling `removeEventListener` before re-registering
|
||||
|
||||
### v0.3.22
|
||||
|
||||
- **Card view removed** — Workflow tab and Models tab no longer offer Card view; Thumbnail and Table remain; if `"card"` was previously stored in localStorage it falls back to Thumbnail automatically
|
||||
- **Batch type selector** — replaced the Checkpoint Batch toggle with per-type checkboxes on the Batch Queue column headers (Checkpoint / Lora / Prompt / Workflow); radio behavior ensures only one type is active at a time; the Batch panel below Generate repurposed as a status display showing the active type, amber progress bar, and Pause/Resume / Stop controls
|
||||
- **4-type batch generation** — `_runBatchGenerate()` dispatches to Checkpoint (file-tree + group selection), Lora (group selection), Prompt (preset group items applied to positive/negative nodes), and Workflow (group selection, restores original workflow after batch); all types use the same generic `_runBatchLoop()` with pause/abort support
|
||||
- **Help updated** — EN / JA / ZH strings updated for `helpWf1`, `helpModels2`, `helpGen11`, `helpGen13`, `helpGen14`
|
||||
|
||||
### v0.3.21
|
||||
|
||||
- **CivitAI Host setting** — new dropdown in Settings tab (CivitAI section): choose `civitai.com` (SFW only) or `civitai.red` (unrestricted); selection is saved to `settings.json` and synced to `localStorage` so the Models tab uses it immediately without extra server calls
|
||||
- **CivitAI panel — Info / Sample sub-tabs** — the CivitAI side panel is split into two sub-tabs; Info tab contains model details and actions; Sample tab shows all sample images (count displayed in label); images open full-size in a new browser tab on click
|
||||
- **Bug fix: model page URL for existing cache** — `modelId: null` entries (the majority of the cache) previously generated broken links (`/models?modelVersionId=…`); both the Python service and the JS renderer now fall back to `/{host}/model-versions/{versionId}` which redirects correctly to the model page; all 324 existing cache entries are fixed immediately without re-fetching
|
||||
- **Help updated** — Batch tab entries (gen-12 / gen-13 / gen-14) and Settings entries (settings-11 Text Size / settings-12 RAW JSON Colors / settings-13 CivitAI Host) are now fully localized in EN / JA / ZH; `helpGen3` updated to reflect 5-tab layout; `helpGen11` rewritten to describe the Batch toggle; JA/ZH translations no longer contain untranslated English terms (Filter, Pause/Resume, Stop)
|
||||
|
||||
### v0.3.20
|
||||
|
||||
- **CivitAI panel — Type / Base Model / Hash display** — side panel CivitAI tab now shows a Type badge (e.g. CHECKPOINT, LORA), Base Model row, and Hash row (BLAKE3 preferred, SHA256 fallback from CivitAI API or local metadata); click the hash to copy the full value to clipboard
|
||||
- **CivitAI panel — images open in new tab** — sample images in the CivitAI tab are now wrapped in links; clicking an image opens the full-size version in a new browser tab
|
||||
- **CivitAI panel — 3-state UI** — the panel now distinguishes between "not yet checked" (blue fetch button), "checked but not found on CivitAI" (re-check button with a notice), and "found" (full info); no more ambiguity after running batch fetch
|
||||
- **CivitAI tab click refresh** — clicking the CivitAI tab in the side panel now re-renders its content, ensuring the latest fetch state is always shown without needing to reselect the model
|
||||
- **Bug fix: model page URL broken after batch fetch** — the batch `POST /model-versions/by-hash` endpoint can omit `model.id` inside the version object; `_extract_info` now falls back to the top-level `modelId` field so the model page link is always correct; existing cached entries with a broken URL are also fixed client-side from `modelId` + `versionId`
|
||||
- **CivitAI cache — `fileHashes` field added** — `_extract_info` now stores the primary file's full hash map (BLAKE3, SHA256, AutoV2, etc.) so the Hash row is populated for newly fetched or refreshed models
|
||||
|
||||
### v0.3.19
|
||||
|
||||
- **CivitAI batch fetch — major speedup** — switched from one-by-one `GET` requests (0.5 s delay each) to `POST /model-versions/by-hash` batch endpoint; up to 100 hashes per request, reducing 100-model fetch from ~50 s to a few seconds
|
||||
- **Retry with exponential backoff** — `fetch_by_hash` and the new batch POST both retry on HTTP 429 / 500 / 502 / 503 / 504 with 1 s → 2 s → 4 s delays (max 3 attempts), as recommended in the CivitAI API docs
|
||||
- **CivitAI API key support** — optional Bearer token can be set in Settings tab (CivitAI API Key section); environment variable `CIVITAI_API_KEY` takes priority over the stored key; API keys are excluded from data exports
|
||||
- **Richer metadata** — `_extract_info` now captures `nsfwLevel` (numeric), `air` (AIR URN identifier), `stats` (download count, thumbs-up/down), `updatedAt`, `publishedAt`, image dimensions / per-image NSFW level (`imageDetails`), and primary file precision / format (`fileMeta`)
|
||||
- **Hash consistency** — cache keys use lowercase hex; API requests send uppercase hex as required by the CivitAI spec
|
||||
- **Bug fix: CivitAI tab blank after batch fetch** — the `done` SSE event now applies `data.hashes` directly to `state.modelMetadata` so the side-panel CivitAI tab renders correctly even when the subsequent `fetchModelMetadata()` call races ahead of the server flush
|
||||
|
||||
### v0.3.18
|
||||
|
||||
- **Batch tab** — new 5th subtab in GenerateUI with a dedicated 3-pane layout for assembling checkpoint batch queues; left pane: file-tree checkpoint selection (filter, All/None, initially unchecked); center pane: group-based selection with inner tabs (Checkpoint / Lora / Prompt / Workflow — Lora/Prompt/Workflow coming soon); right pane: live Batch Queue preview combining both pane selections with duplicates removed
|
||||
- **Checkpoint selector moved** — the checkpoint dropdown is removed from the right panel; selection is now done exclusively in the Batch tab left and center panes; the right panel retains only the enable/disable toggle and progress display
|
||||
- **Group selection fix** — batch group selection now stores selected group names (not raw model names), so changes to group membership in the Models tab are always reflected in the queue without stale entries
|
||||
- **Models tab bulk operations expanded** — bulk action bar now includes: Deselect All (clear all selections without exiting select mode), ★ Favorite / ☆ Unfavorite, +Badge / −Badge (apply or remove a badge from all selected models); existing group and delete operations unchanged
|
||||
|
||||
### v0.3.17
|
||||
|
||||
- **CivitAI SSL fix** — resolved `CERTIFICATE_VERIFY_FAILED` errors when fetching model info on Windows Portable Python environments; uses `certifi` CA bundle when available, falls back to system store, and disables verification only as a last resort with a warning log
|
||||
- **Lora Manager support** — `Lora Loader (LoraManager)` nodes (ComfyUI-Lora-Manager) are now correctly converted from UI format to API format in the GenerateUI tab; the `loras` widget array is wrapped to `{"__value__": [...]}` as required by the node, enabling generation with LoRA Manager workflows
|
||||
|
||||
### v0.3.16
|
||||
|
||||
- **CivitAI preview fallback** — model cards and the detail panel's Info tab now display the first CivitAI image directly when no local preview file exists; if the backend download fails (e.g. network restriction server-side), the browser fetches the image URL from the CivitAI cache as a fallback so the preview is always shown after fetching
|
||||
|
||||
### v0.3.15
|
||||
|
||||
- **Sample workflows** — added `workflows/` directory containing 13 sample workflows (SD1.5 / SDXL / DWPose / Face Detailer / Image Editing) with PNG thumbnails, bundled in the package
|
||||
- **CivitAI auto-preview** — when fetching CivitAI info (individual or batch), the first CivitAI image is automatically downloaded and saved as `{model_stem}.preview.png` if no preview exists; batch summary reports the count of auto-saved previews
|
||||
- **TOOLS pane — Create Tags** — new task option in the VLM dropdown (both SPA AI TOOL tab and Library A tab): generates a comma-separated list of descriptive English tags from the image
|
||||
|
||||
### v0.3.14
|
||||
|
||||
- **AI TOOL tab redesign** — tab renamed from "A" to "AI TOOL"; sub-tab navigation removed in favor of a permanent 3-pane layout (Translation 40% | TOOLS 40% | Settings 20%) — all panes always visible simultaneously; "VLM" pane renamed to "TOOLS"
|
||||
- **Settings — RAW JSON Colors** — new accordion section with 6 color pickers to customize the syntax highlight colors for the Raw JSON editor in Generate UI: Default Text, Name/Scheduler, Title, Width/Height, Prompt/Text, Image/File; changes apply immediately on color pick; Reset Defaults restores the original scheme; saved to `localStorage` under `wfm_settings.jsonColors` and applied on startup
|
||||
- **Help tab updated** — AI TOOL tab card rewritten; Settings card updated with RAW JSON Colors entry; Library panel sidepanel-16 entry updated
|
||||
|
||||
### v0.3.13
|
||||
|
||||
- **AI Tab (A)** — new tab added to both the SPA and the Workflow Studio Library side panel (rightmost tab)
|
||||
- **Translation sub-tab** — translate text via Ollama or LM Studio between Japanese, English, Chinese, or a custom Free language; ⇄ swap button exchanges both language selectors and text content simultaneously; language selections persisted automatically
|
||||
- **VLM sub-tab** — drop zone (110px, matching the I tab) accepts images via drag & drop or click; task selector (Describe Image / Create Prompt); Run button streams result into output area with Copy button; Ollama uses `images:[base64]` payload, LM Studio uses `image_url` content block
|
||||
- **Settings sub-tab** — backend radio (Ollama / LM Studio), API URL input, connection test with live status, model selector with refresh, Free language name fields (source and destination); saved to `localStorage` key `wfm_ai_settings` and shared between SPA and Library panel
|
||||
- **URL security** — backend URL validated via `new URL()` to enforce `http://` or `https://` scheme before any network call
|
||||
- **Settings Tab — Ollama section renamed** — "Ollama 設定" → "Ollama 設定(プロンプトタブ)" to distinguish it from the new AI tab's settings
|
||||
- **Help Tab updated** — added AI Tab (A) feature card and Library panel A tab entry; i18n support in EN/JA/ZH
|
||||
|
||||
### v0.3.12
|
||||
|
||||
- **GenerateUI — Raw JSON search** — always-visible search bar above the Raw JSON editor; finds all matches as you type with match count (`3/12`); navigate forward/back with ↑/↓ buttons, Enter / Shift+Enter, or Escape / ✕ to clear; current match highlighted in orange, other matches in yellow; search overlay syncs with editor scroll and stays updated while editing
|
||||
- **Workflow analyzer — extended model type detection** — added CLIPLoader `type` field mapping to model family (`flux`, `hidream_i`, `wan`, `cosmos`, `lumina`, `ovis`, etc.) via `_CLIP_TYPE_TO_MODEL`; subgraph nodes (`definitions.subgraphs`) now scanned for model detection; `UnetLoaderGGUF` and similar loaders matched via `"UnetLoader" in ntype`; new families added: NewBie, Ovis, HiDream, Wan, Cosmos, Lumina; model-name detection refactored into reusable `_detect_model_type_from_name()`
|
||||
- **Screenshots refreshed** — replaced tab-by-tab screenshots with 8 feature-focused images
|
||||
|
||||
### v0.3.11
|
||||
|
||||
- **Metadata Tab — extended loader support** — `metadata-tab.js` now recognizes `UnetLoaderGGUF` and `UNETLoaderGGUF` as Diffusion Model (e.g. HiDream GGUF workflows), and `QuadrupleCLIPLoader` as Text Encoder (e.g. HiDream 4-CLIP); previously these were detected only in the side panel I tab, not in the Metadata tab itself
|
||||
- **Settings Tab — Text Size** — new slider (10–28 px) in the Settings tab adjusts font size for all prompt and chat text areas at once: Generate UI positive/negative prompts, AI Assistant chat input, Preset prompts, Wildcard text area and file editor, and Metadata prompt full preview; applied immediately on drag and persisted with Save Settings
|
||||
|
||||
### v0.3.10
|
||||
|
||||
- **Information tab — drag to canvas** — model, lora, and Prompts sub-tabs in the side panel I tab now support drag & drop onto the ComfyUI canvas; double-click also places nodes at canvas center
|
||||
- model sub-tab: each item places the corresponding loader node (Checkpoint → `CheckpointLoaderSimple`, VAE → `VAELoader`, Diffusion Model → `UNETLoader`, Text Encoder → `CLIPLoader`)
|
||||
- lora sub-tab: drag individual LoRA to place `LoraLoader`; **Multiple LORA** section (shown for 1+ LoRAs) places all LoRAs in a single `Lora Loader (LoraManager)` node
|
||||
- Prompts sub-tab: drag a prompt to place `CLIPTextEncode` with the full text pre-filled
|
||||
- **Extended node type support** — `UnetLoaderGGUF` / `UNETLoaderGGUF` now detected as Diffusion Model; `QuadrupleCLIPLoader` (HiDream and similar 4-CLIP workflows) now detected as Text Encoder
|
||||
- **Bug fix: CLIPLoader widget name** — `textencoder` type in `MODEL_NODE_MAP` was incorrectly using `clip_name1` (DualCLIPLoader's widget) instead of `clip_name`; Text Encoder models now drop with the correct filename
|
||||
- **Preview area fixed height** — Information tab drop zone fixed at 110px so model/prompt lists are no longer compressed when a large image is loaded
|
||||
|
||||
### v0.3.9
|
||||
|
||||
- **Side panel I tab** — new Information tab in the Workflow Studio Library side panel; drop a ComfyUI PNG/WebP/JSON directly in the panel to inspect models, LoRAs, and prompts without opening the Metadata tab; sub-tabs: model / lora / Prompts with full-text preview and Copy button
|
||||
- **Side panel tab redesign** — tabs shortened to single letters (W / N / P / M / I) with full name shown on hover
|
||||
- **Bug fix: top bar icons** — icons (Workflow Studio / camera / Node Sets) were invisible in newer ComfyUI versions because Iconify converted the MDI CSS class to an inline SVG before the custom SVG could be injected; fixed by removing the early-return guard and adopting the same simple `requestAnimationFrame` retry pattern used by lora-manager
|
||||
|
||||
### v0.3.8
|
||||
|
||||
- **Metadata Tab: extended subgraph support** — added extraction for Flux.2 Dev fp8 (`SamplerCustomAdvanced`), Flux.2 Klein 4B Distilled (image edit), Ernie Image, Qwen-Image-Edit 2511 (`TextEncodeQwenImageEditPlus`), and WAN2.2 14B Animate (top-level CLIPTextEncode + subgraph KSampler)
|
||||
- **Prompt type: Text (no badge)** — when positive/negative polarity cannot be determined (indirect sampler connections, cross-level links, `PrimitiveStringMultiline` etc.), prompts are listed without a POS/NEG badge; action buttons still work normally
|
||||
- **Prompt extraction improvements** — `extractPromptsFromNodeSet` now resolves link sources from any STRING-output node (e.g. `ComfySwitchNode`, `PrimitiveStringMultiline`) without type restriction; `PrimitiveStringMultiline` search and final text-encoder fallback both extended to include subgraph nodes via `collectAllNodes`
|
||||
- **i18n** — added `metaPromptText` key (EN: "Text" / JA: "テキスト" / ZH: "文本")
|
||||
|
||||
### v0.3.7
|
||||
|
||||
- **Metadata Tab: subgraph workflow support** — Flux.2 (Dev fp8 / Klein), Qwen-Image-Edit (/ 2511 / Layered), and Z-Image (Base / Turbo) official templates now correctly extract models and prompts; these use ComfyUI's `definitions.subgraphs` format with `UNETLoader`, `CLIPLoader`, and `VAELoader` inside the subgraph
|
||||
- **Prompt extraction improvements** — 7-stage fallback chain: ImageMetadataPromptLoader → WFS_PromptText → top-level CLIPTextEncode+KSampler → `PrimitiveStringMultiline` (flux2-klein) → subgraph CLIPTextEncode+KSampler → PromptStyler → all CLIPTextEncode text
|
||||
- **MarkdownNote model fallback** — `extractMarkdownNoteModels()` parses `**section** → - [model](url)` patterns from MarkdownNote nodes as a supplemental model source
|
||||
- **Format note updated** — "Flux.2 / Qwen-Image / Z-Image subgraph workflows supported" (EN/JA/ZH)
|
||||
|
||||
### v0.3.6
|
||||
|
||||
- **Metadata Tab** — new tab (between Prompt and Gallery) that extracts model and prompt information from ComfyUI-generated PNG/WebP images and workflow JSON files; 3-column layout: drop zone with image preview (left), Checkpoint/VAE/Diffusion Model/Text Encoder lists (center), LoRA list with strength values + Prompt list with full-text viewer (right)
|
||||
- **Prompt action buttons** — Copy, GenUI:P/N (set GenerateUI prompts), Prompt:P/N (set Prompt tab preset prompts) directly from the extracted prompt text
|
||||
- **SDXL Prompt Styler support** — improved prompt extraction resolves `PromptStyler → CLIPTextEncode` link chains in both LiteGraph and API workflow formats
|
||||
- **Format support** — ComfyUI (PNG/WebP/JSON), SD WebUI, SD Forge, Fooocus
|
||||
|
||||
### v0.3.5
|
||||
|
||||
- **Feeder subtab** — new subtab in GenerateUI for controlling [comfyui-image-feeder](https://github.com/ketle-man/comfyui-image-feeder) nodes; left pane: node selector + all ImageFeeder parameters + presets + Run/Stop controls; center pane: folder tree, image grid with checkbox selection, and preview panel
|
||||
- **Run loop with After gen modes** — Loop (wrap and continue), Increment (advance and auto-stop), Fixed (constant index); index updates automatically via WebSocket `image_loop_node_sync` after each generation; seed comes from the right-pane seed setting
|
||||
- **Workflow analysis improvements** — `analyzeWorkflow` now handles multi-hop CONDITIONING chains via BFS (up to 5 iterations), CLIPTextEncodeSDXL / CLIPTextEncodeSDXLRefiner, SDXLPromptStyler / SDXLPromptStylerAdvanced, KSamplerAdvanced (`noise_seed`), TextEncodeQwenImageEditPlus, PrimitiveStringMultiline, Power Lora Loader (rgthree), and `Checkpoint Loader` (WAS) with space in the class name
|
||||
|
||||
### v0.3.4
|
||||
|
||||
- **Wildcard support panel** — new right column in the Prompt tab with a one-click toolbar for inserting wildcard syntax (`{|}`, `{n$|}`, `__|__`, `:`, `;`, `$`, `<lora::1:LBW=;>`, `[]`, single/multi pick) and wrapping selected text; dedicated prompt textarea and wildcard file picker (click to insert `__filename__`)
|
||||
- **Wildcard file manager** — create, view, edit, and delete `.txt` / `.yaml` wildcard files stored in `user/default/Workflow-Studio/wildcard/`; editor opens inline with filename input and save/cancel controls
|
||||
- **Preset/Preset Manager tab switch** — the center pane of the Prompt tab is now a two-tab panel (Presets / Preset Manager) to reclaim horizontal space for the new wildcard panel
|
||||
- **Wildcard Integration setting** — new accordion section in Settings tab; auto-detects ComfyUI-Impact-Pack; when installed, provides a button to create a directory junction (Windows) or symlink (other OS) from the WFS wildcard directory to Impact Pack's `wildcards/` directory; existing WFS files are migrated automatically; a Remove Link button reverses the operation
|
||||
|
||||
### v0.3.2
|
||||
|
||||
- **Checkpoint Batch** — new panel in the GenerateUI right column (below Seed, above results); enable via checkbox to auto-generate once per checkpoint model; filter included/excluded subfolders by comma-separated names (empty = all checkpoints); real-time preview of matched model count; amber progress bar shows current model name and index; Stop button aborts the loop after the current generation completes; failed models are counted and reported in the summary toast
|
||||
- **Seed layout fix** — seed value input and mode selector (Random / Fixed / Increment / Decrement) now stacked vertically so neither is clipped in narrow panels
|
||||
|
||||
### v0.3.1
|
||||
|
||||
- **Folder operations** — create subfolders and delete folders (with all contents) directly from the folder tree header buttons
|
||||
- **File operations** — move or delete individual images from the detail panel; bulk move and bulk delete from the multi-select bar
|
||||
- **Tree state preserved** — folder expansion state is maintained after create/delete/move/refresh operations
|
||||
- **Workflow viewer improved** — Metadata tab now reads both `prompt` (API format, the default for most ComfyUI-generated images) and `workflow` (UI format) keys from PNG metadata, covering virtually all ComfyUI output
|
||||
- **Workflow auto-save** — after each generation in the Generate UI tab, the current workflow is automatically saved to gallery metadata for the generated images; shown in the Metadata tab even if PNG embedding is absent
|
||||
|
||||
### v0.3.0
|
||||
|
||||
- **Cross-type group filter** — the group filter dropdown in the Models tab now shows groups from all model types simultaneously, each prefixed with a type label (`[Checkpoint]`, `[LoRA]`, etc.); selecting a group automatically switches to the corresponding model type
|
||||
- **Sidebar group display fixed** — the ComfyUI sidebar "Model Groups" view now correctly renders per-type groups (introduced in v0.2.9) with a type label on each group header; fixes a crash where `.filter is not a function` was thrown on the new nested group structure
|
||||
|
||||
### v0.2.9
|
||||
|
||||
- **Model Enable/Disable** — hide models from ComfyUI by renaming the file extension to `.disabled`; toggle per model (⏸ button on each card/row), per group (Enable All / Disable All buttons in the Group panel), or filter the grid by status (All / Enabled only / Disabled only). Changes take effect after ComfyUI refresh.
|
||||
- **Per-type model groups** — groups are now scoped to each model type; checkpoint groups only appear in the Checkpoint tab, LoRA groups only in the LoRA tab, etc. Existing flat groups are automatically migrated.
|
||||
- **Multi-select mode** — click the Select button in the toolbar to enter selection mode; check individual models across Thumbnail / Card / Table views
|
||||
- **Bulk group operations** — with models selected, add or remove them from any group in one click, or create a new group and add them simultaneously
|
||||
- **Bulk file delete** — permanently delete selected model files along with all associated preview images and sidecar files (`.json`, `.civitai.info`, `.info`); requires confirmation before deleting
|
||||
- **Bug fix** — models in subdirectories could not be disabled due to a path-doubling error; fixed by computing the rename target from `path.name` instead of `model_name`
|
||||
- **Bug fix** — Hypernetwork tab showed `C`, `O`, `M`, `B` as fake model items; caused by the new ComfyUI API format returning `"COMBO"` as the first element; fixed in `comfyui-client.js`
|
||||
|
||||
### v0.2.8
|
||||
|
||||
- **Data storage moved** — plugin data (settings, metadata, prompts, etc.) is now stored in `ComfyUI/user/default/Workflow-Studio/` instead of `custom_nodes/ComfyUI-Workflow-Studio/data/`; falls back to the old `data/` directory if `user/default/` does not exist
|
||||
- **Data Management section** — new Settings tab section to export all plugin data to a single JSON file and import it back; useful for migration, backup, and reinstallation
|
||||
|
||||
### v0.2.7
|
||||
|
||||
- **Gallery tab** — new image browser for ComfyUI output folder; Thumbnail/Table views, multi-select with Bulk Bar, server-side group/tag/favorite filtering, group management (rename support), performance-optimized with + mtime folder cache
|
||||
- **Group UI unified** — Workflow, Nodes, and Gallery group panels rebuilt to match the Models tab 4-section pattern (Current Groups / Add to Group / Create New Group / Manage Groups); Rename support added to Workflow and Nodes tabs
|
||||
- **Settings 2-column layout** — Theme panel fixed on the right (50% width, sticky); all other settings flow continuously on the left with no visual gap
|
||||
- **Gallery output directory setting** — configure the output folder scanned by Gallery tab from Settings
|
||||
- **Help tab** — Gallery tab description card added
|
||||
|
||||
### v0.2.5
|
||||
|
||||
- **Sidebar Category/Package sub-tabs** — added 📂 Category and 🧩 Package sub-tabs to the Nodes tab in Workflow Studio Library; each shows a dropdown to filter nodes by category or custom node package (reads `/object_info` for accurate package detection)
|
||||
- **Sidebar theme settings** — added ⚙ button in the panel header to customize background, sub-header background, text, border, and secondary text colors with live preview; settings saved to localStorage
|
||||
- **Sidebar TextEncoder (By Type)** — fixed TextEncoder not appearing in Models → By Type; now uses `DualCLIPLoader` → `CLIPLoader` fallback to retrieve `clip_name1` list
|
||||
- **Hide `.index.json`** — excluded `.index.json` from both the Workflow tab and the Workflow Studio Library side panel
|
||||
|
||||
### v0.2.4
|
||||
|
||||
- **GenerateUI tab redesign** — reorganized from 5 tabs (Prompt/Image/Model/Settings/Raw JSON) to 3 tabs (Input/Model/Settings); each tab shows a Raw JSON column (540px) for instant JSON preview and direct editing from any tab
|
||||
- **Input tab** — Prompt and Image stacked vertically in the left column
|
||||
- **Settings tab** — KSampler and LatentImage stacked vertically in the left column
|
||||
- **Raw JSON tab removed** — Raw JSON is now always visible as a right-column panel within each tab
|
||||
|
||||
### v0.2.3
|
||||
|
||||
- **Badge system unified** — Workflow and Models tabs now share a single badge palette (`⚙ Badge` button); workflow auto-analysis model type system removed in favor of user-defined free-label badges
|
||||
- **GenUI Model button** — apply any model from the Models tab directly to the corresponding node in GenerateUI's current workflow (available in detail modal and side panel; Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)
|
||||
- **Sidebar Models tab** — added All / ★ Favorites / Groups / By Type sub-tabs to the Models section of Workflow Studio Library
|
||||
- **Sidebar All views** — ★ star displayed for favorite items in Workflows and Nodes All sub-tabs
|
||||
- **Sidebar panel width** — widened from 280px to 310px
|
||||
|
||||
### v0.2.2
|
||||
|
||||
- **Models tab** — browse, search, and manage all installed ComfyUI models (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder) with Thumbnail/Card/Table views
|
||||
- **CivitAI integration** — fetch model metadata by SHA256 hash (individual or batch per model type) with SSE progress streaming
|
||||
- **Model groups** — create, rename, delete groups and assign/remove models for organization
|
||||
- **Model metadata** — persistent favorites, tags, memo, and SHA256 hash per model
|
||||
- **Preview images** — auto-detect and display preview images, change via file upload in detail modal
|
||||
- **Table view memo column** — memo displayed in table view with compact subdir/extension columns
|
||||
- **Side panel file path** — display full file path with click-to-copy in model info panel
|
||||
- **Node card redesign** — removed package badge, added left border color-coding by package
|
||||
- **Toolbar pagination** — pagination controls moved to toolbar for all tabs (Models, Workflows, Nodes)
|
||||
- **Help tab updated** — Models Tab section added to feature list
|
||||
|
||||
### v0.2.0
|
||||
|
||||
- **Prompt presets in sidebar** — added Prompts tab to Workflow Studio Library (ComfyUI side panel) with All / Favorites / Categories sub-tabs
|
||||
- **WFS_PromptText custom node** — drag prompt presets onto the canvas to create nodes with positive/negative prompt outputs
|
||||
- **Preset Manager** — 3-column layout in Prompt tab: AI Assistant | Presets editor | Preset Manager (All / Favorites / Groups)
|
||||
- **Group management** — create groups, assign/remove presets, delete groups from the Presets panel
|
||||
- **GenUI Set** — renamed "Apply" button to clarify its purpose (applies presets to GenerateUI)
|
||||
- **Sidebar P/N copy buttons** — copy positive or negative prompts individually from sidebar items
|
||||
- **Panel renamed** — "WF & Node Library" renamed to "Workflow Studio Library"
|
||||
- **Backend API for presets** — presets migrated from localStorage to server-side API with one-time migration
|
||||
- **Help tab updated** — Prompt Tab and Workflow Studio Library sections reflect new features
|
||||
|
||||
### v0.1.9
|
||||
|
||||
- **Side panel 2-row sub-tabs** — Workflows: row 1 (Workflows / Favorites / Groups), row 2 (Model Type); Nodes: row 1 (Nodes / Favorites / Groups), row 2 (Sets)
|
||||
- **Save dialog** — canvas snapshot now shows a filename edit dialog instead of auto-naming
|
||||
- **API/App format badge** — workflow items in side panel show red (API) or orange (APP) badge
|
||||
- **Group auto-cleanup** — deleted workflows are automatically removed from groups on refresh
|
||||
|
||||
### v0.1.8
|
||||
|
||||
- **WF & Node Library side panel** — renamed from "Node Library", added two top-level tabs (Workflows / Nodes) accessible from ComfyUI top bar
|
||||
- **Workflows tab** — browse favorite workflows, filter by model type, filter by group with collapsible sections
|
||||
- **Workflow drag & drop** — drag workflows from the side panel onto the canvas to load them instantly
|
||||
- **Help tab update** — added Nodes Tab and WF & Node Library sections to the feature list
|
||||
|
||||
### v0.1.7
|
||||
|
||||
- **Nodes tab** — browse, search, and filter all installed ComfyUI nodes with Card/Table views, pagination (50 nodes/page), package color badges, and node detail side panel with I/O specs
|
||||
- **Node Sets** — save selected nodes + connections from ComfyUI canvas as reusable sets via right-click context menu
|
||||
- **Node Library side panel** — accessible from ComfyUI top bar with Favorites, Sets, and Groups tabs, drag & drop placement
|
||||
- **3 top bar buttons** — Workflow Studio, Canvas Snapshot, and Node Library buttons in ComfyUI's action bar
|
||||
|
||||
### v0.1.6
|
||||
|
||||
- **Security fix** — path traversal vulnerability in `workflow_service.py` (reported via ComfyUI-Manager PR review)
|
||||
|
||||
### v0.1.5
|
||||
|
||||
- **Theme system** — 13 built-in themes selectable from Settings tab with instant preview (Deep Ocean Dark, Pop & Vibrant, Light Minimalist, Cyberpunk, Glassmorphism, Neumorphism, Retro 8-bit, Pastel Dream, Brutalism, Earthy, Material UI, Monotone + Accent, Corporate Trust)
|
||||
- Theme preference persisted in localStorage and restored on page load (no flash)
|
||||
- Special CSS effects per theme: neon glow (Cyberpunk), backdrop blur (Glassmorphism), dual shadow (Neumorphism), pixel borders (Retro/Brutalism)
|
||||
|
||||
### v0.1.4
|
||||
|
||||
- **App format support** — detect `.app.json` workflows (ComfyUI App mode), show "App Format" badge, block loading in GenerateUI with guidance message
|
||||
- **Preset clipboard copy** — added PP Copy / NP Copy buttons to copy positive/negative prompts to clipboard
|
||||
- **Analysis bugfix** — fixed workflow analysis crash when `widgets_values` contains non-string values (e.g. integers)
|
||||
|
||||
### v0.1.3
|
||||
|
||||
- Added **Help & Support tab** — feature list, tips, and support links (GitHub, Ko-fi)
|
||||
- Multi-language support (EN/JA/ZH) for all help content
|
||||
|
||||
### v0.1.2
|
||||
|
||||
- Added **Canvas Snapshot** button to ComfyUI top bar — capture workflow canvas as PNG thumbnail with embedded workflow metadata
|
||||
- Added **Thumbnail tab** to the workflow side panel for quick visual preview
|
||||
- Added **Thumbnail section** to the workflow detail modal
|
||||
- Snapshot images are auto-imported as workflows with thumbnails
|
||||
|
||||
### v0.1.1
|
||||
|
||||
- Initial feature set: Workflow management, GenerateUI, Prompt assistant, Settings
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
|
||||
@@ -69,6 +69,8 @@ NODE_SETS_FILE = DATA_DIR / "node_sets.json"
|
||||
PROMPTS_FILE = DATA_DIR / "prompts.json"
|
||||
MODEL_METADATA_FILE = DATA_DIR / "model_metadata.json"
|
||||
WILDCARD_DIR = DATA_DIR / "wildcard"
|
||||
AI_SKILLS_DIR = DATA_DIR / "ai_skills"
|
||||
LAB_PLAN_DIR = DATA_DIR / "lab_plan"
|
||||
|
||||
# Tagger
|
||||
TAGGER_DB_FILE = DATA_DIR / "tagger.db"
|
||||
|
||||
+103
-2
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
@@ -17,6 +18,84 @@ def setup_routes(app: web.Application):
|
||||
app.router.add_post("/api/wfm/eagle/test", handle_test)
|
||||
|
||||
|
||||
def _resolve_local_path(filename, subfolder, type_name):
|
||||
"""filename/subfolder/type を ComfyUI 出力ディレクトリ上の実パスに解決する。
|
||||
ComfyUI と Eagle が同一マシン上で動作している場合のみ有効なパスが返る
|
||||
(別マシンの場合は None を返し、呼び出し側は addFromURL にフォールバックする)。"""
|
||||
try:
|
||||
import folder_paths # type: ignore
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
base_dir = folder_paths.get_directory_by_type(type_name or "output")
|
||||
if not base_dir:
|
||||
return None
|
||||
base_dir = os.path.abspath(base_dir)
|
||||
|
||||
filename = os.path.basename(filename or "")
|
||||
if not filename:
|
||||
return None
|
||||
|
||||
full_dir = os.path.abspath(os.path.join(base_dir, subfolder or ""))
|
||||
# Windowsはパス比較で大文字小文字を区別しないため normcase で正規化してから比較する
|
||||
if os.path.commonpath((os.path.normcase(full_dir), os.path.normcase(base_dir))) != os.path.normcase(base_dir):
|
||||
# ディレクトリトラバーサル対策: base_dir 配下以外は拒否
|
||||
return None
|
||||
|
||||
full_path = os.path.join(full_dir, filename)
|
||||
if not os.path.isfile(full_path):
|
||||
return None
|
||||
return full_path
|
||||
|
||||
|
||||
def _resolve_absolute_svg_path(abs_path):
|
||||
"""comfyui-tosvg の Save SVG String ノードなどが返す絶対パスを検証する。
|
||||
ComfyUI管理下(output/input/temp)ディレクトリ配下の実在する .svg ファイルのみ許可する
|
||||
(任意の絶対パスをそのまま信頼すると、外部からこのAPIを叩かれた際に
|
||||
ローカルの任意ファイルを送信させられてしまうため)。"""
|
||||
if not abs_path or not abs_path.lower().endswith(".svg"):
|
||||
return None
|
||||
try:
|
||||
import folder_paths # type: ignore
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
abs_path = os.path.abspath(abs_path)
|
||||
if not os.path.isfile(abs_path):
|
||||
return None
|
||||
|
||||
for type_name in ("output", "temp", "input"):
|
||||
base_dir = folder_paths.get_directory_by_type(type_name)
|
||||
if not base_dir:
|
||||
continue
|
||||
base_dir = os.path.abspath(base_dir)
|
||||
try:
|
||||
# Windowsはパス比較で大文字小文字を区別しないため normcase で正規化してから比較する
|
||||
if os.path.commonpath((os.path.normcase(abs_path), os.path.normcase(base_dir))) == os.path.normcase(base_dir):
|
||||
return abs_path
|
||||
except ValueError:
|
||||
# Windowsで異なるドライブレターの場合 commonpath が例外を投げる
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _eagle_add_from_path(eagle_url, file_path, name, tags):
|
||||
"""Proxy request to Eagle's addFromPath API (runs in thread).
|
||||
SVG は ComfyUI の /view がセキュリティ上 application/octet-stream +
|
||||
attachment で返すため、addFromURL 経由だと正しい種別で保存されないことがある。
|
||||
ComfyUI と Eagle が同一マシン上にある場合はローカルパスを直接渡す方が確実。"""
|
||||
eagle_url = eagle_url.rstrip("/")
|
||||
payload = json.dumps({"path": file_path, "name": name, "tags": tags}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{eagle_url}/api/item/addFromPath",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def _eagle_add(eagle_url, image_url, name, tags):
|
||||
"""Proxy request to Eagle API (runs in thread)."""
|
||||
eagle_url = eagle_url.rstrip("/")
|
||||
@@ -71,11 +150,33 @@ async def handle_add(request: web.Request) -> web.Response:
|
||||
image_url = body.get("url", "")
|
||||
name = body.get("name", "image.png")
|
||||
tags = body.get("tags", [])
|
||||
filename = body.get("filename", "")
|
||||
subfolder = body.get("subfolder", "")
|
||||
file_type = body.get("type", "output")
|
||||
local_path_hint = body.get("localPath", "")
|
||||
|
||||
if not image_url:
|
||||
if not image_url and not local_path_hint:
|
||||
return web.json_response({"status": "error", "message": "No URL provided"}, status=400)
|
||||
|
||||
result = await asyncio.to_thread(_eagle_add, eagle_url, image_url, name, tags)
|
||||
# SVG は /view が application/octet-stream + attachment で返すため、
|
||||
# ローカルファイルが解決できる場合は addFromPath で直接渡す
|
||||
local_path = None
|
||||
if local_path_hint:
|
||||
# comfyui-tosvg の Save SVG String ノードなど、絶対パスが既知の場合
|
||||
local_path = _resolve_absolute_svg_path(local_path_hint)
|
||||
elif (filename or name or "").lower().endswith(".svg"):
|
||||
local_path = _resolve_local_path(filename or name, subfolder, file_type)
|
||||
|
||||
if not local_path and not image_url:
|
||||
return web.json_response(
|
||||
{"status": "error", "message": f"Could not resolve local SVG path: {local_path_hint}"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if local_path:
|
||||
result = await asyncio.to_thread(_eagle_add_from_path, eagle_url, local_path, name, tags)
|
||||
else:
|
||||
result = await asyncio.to_thread(_eagle_add, eagle_url, image_url, name, tags)
|
||||
return web.json_response(result)
|
||||
except urllib.error.URLError as e:
|
||||
logger.error("Eagle URL error: %s", e)
|
||||
|
||||
@@ -260,8 +260,18 @@ async def toggle_favorite(request: web.Request) -> web.Response:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
_SAVE_EXT_BY_MIME = {
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/webp": ".webp",
|
||||
"image/gif": ".gif",
|
||||
"image/svg+xml": ".svg",
|
||||
}
|
||||
|
||||
|
||||
async def save_image_to_gallery(request: web.Request) -> web.Response:
|
||||
"""Image Edit Tab からの PNG をギャラリーのルートフォルダに保存する。"""
|
||||
"""Image Edit Tab / AI Chat からの画像(PNG等)やSVGをギャラリーのルートフォルダに保存する。
|
||||
imageData は data URL (data:<mime>;base64,<data>) を想定。ヘッダがなければPNG扱い(旧互換)。"""
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "").strip()
|
||||
@@ -273,15 +283,21 @@ async def save_image_to_gallery(request: web.Request) -> web.Response:
|
||||
if _service._allowed_root is None:
|
||||
return web.json_response({"error": "Gallery root not configured. Open Gallery tab and set output folder first."}, status=500)
|
||||
|
||||
# data URL から MIME タイプを判定し、拡張子を決定する
|
||||
raw = image_data
|
||||
mime = "image/png"
|
||||
if raw.startswith("data:"):
|
||||
header, _, raw = raw.partition(",")
|
||||
m = re.match(r"data:([^;]+)", header)
|
||||
if m:
|
||||
mime = m.group(1)
|
||||
ext = _SAVE_EXT_BY_MIME.get(mime, ".png")
|
||||
|
||||
# ファイル名サニタイズ(OSで使えない文字を除去)
|
||||
safe = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", filename)
|
||||
if not safe.lower().endswith(".png"):
|
||||
safe += ".png"
|
||||
if not safe.lower().endswith(ext):
|
||||
safe += ext
|
||||
|
||||
# base64 data URL をデコード
|
||||
raw = image_data
|
||||
if "," in raw:
|
||||
raw = raw.split(",", 1)[1]
|
||||
image_bytes = base64.b64decode(raw)
|
||||
|
||||
save_path = _service._allowed_root / safe
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Lab tab plan file management API routes."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..config import LAB_PLAN_DIR
|
||||
from ..services.lab_plan_service import LabPlanService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_service = LabPlanService(LAB_PLAN_DIR)
|
||||
|
||||
|
||||
def setup_routes(app: web.Application):
|
||||
app.router.add_get("/api/wfm/lab/plans", handle_list)
|
||||
app.router.add_get("/api/wfm/lab/plans/content", handle_get_content)
|
||||
app.router.add_post("/api/wfm/lab/plans/save", handle_save)
|
||||
app.router.add_post("/api/wfm/lab/plans/delete", handle_delete)
|
||||
app.router.add_post("/api/wfm/lab/index-image/save-to-output", handle_save_index_to_output)
|
||||
|
||||
|
||||
async def handle_list(request: web.Request) -> web.Response:
|
||||
try:
|
||||
result = await asyncio.to_thread(_service.list_plans)
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
logger.error("Error listing lab plans: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_get_content(request: web.Request) -> web.Response:
|
||||
filename = request.rel_url.query.get("filename", "")
|
||||
try:
|
||||
data = await asyncio.to_thread(_service.get_plan, filename)
|
||||
return web.json_response({"data": data})
|
||||
except FileNotFoundError:
|
||||
return web.json_response({"error": "not found"}, status=404)
|
||||
except ValueError as e:
|
||||
return web.json_response({"error": str(e)}, status=400)
|
||||
except Exception as e:
|
||||
logger.error("Error getting lab plan content: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_save(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
data = body.get("data", {})
|
||||
index_image_base64 = body.get("index_image_base64")
|
||||
result = await asyncio.to_thread(_service.save_plan, filename, data, index_image_base64)
|
||||
return web.json_response({"status": "ok", **result})
|
||||
except ValueError as e:
|
||||
return web.json_response({"error": str(e)}, status=400)
|
||||
except Exception as e:
|
||||
logger.error("Error saving lab plan: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_delete(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
await asyncio.to_thread(_service.delete_plan, filename)
|
||||
return web.json_response({"status": "ok"})
|
||||
except ValueError as e:
|
||||
return web.json_response({"error": str(e)}, status=400)
|
||||
except Exception as e:
|
||||
logger.error("Error deleting lab plan: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_save_index_to_output(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
image_base64 = body.get("image_base64", "")
|
||||
if not image_base64:
|
||||
return web.json_response({"error": "image_base64 required"}, status=400)
|
||||
result = await asyncio.to_thread(_service.save_index_image_to_output, image_base64)
|
||||
return web.json_response({"status": "ok", **result})
|
||||
except Exception as e:
|
||||
logger.error("Error saving lab index image to output: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""AI skill (.md system prompt) file management API routes."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..config import AI_SKILLS_DIR
|
||||
from ..services.skill_service import SkillService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_service = SkillService(AI_SKILLS_DIR)
|
||||
|
||||
|
||||
def setup_routes(app: web.Application):
|
||||
app.router.add_get("/api/wfm/skills", handle_list)
|
||||
app.router.add_get("/api/wfm/skills/content", handle_get_content)
|
||||
app.router.add_post("/api/wfm/skills/save", handle_save)
|
||||
app.router.add_post("/api/wfm/skills/delete", handle_delete)
|
||||
|
||||
|
||||
async def handle_list(request: web.Request) -> web.Response:
|
||||
try:
|
||||
result = await asyncio.to_thread(_service.list_skills)
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
logger.error("Error listing skills: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_get_content(request: web.Request) -> web.Response:
|
||||
filename = request.rel_url.query.get("filename", "")
|
||||
try:
|
||||
content = await asyncio.to_thread(_service.get_content, filename)
|
||||
return web.json_response({"content": content})
|
||||
except FileNotFoundError:
|
||||
return web.json_response({"error": "not found"}, status=404)
|
||||
except Exception as e:
|
||||
logger.error("Error getting skill content: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_save(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
content = body.get("content", "")
|
||||
result = await asyncio.to_thread(_service.save_file, filename, content)
|
||||
return web.json_response({"status": "ok", "file": result})
|
||||
except Exception as e:
|
||||
logger.error("Error saving skill: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_delete(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
await asyncio.to_thread(_service.delete_file, filename)
|
||||
return web.json_response({"status": "ok"})
|
||||
except Exception as e:
|
||||
logger.error("Error deleting skill: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
+107
-81
@@ -3,6 +3,7 @@ Gallery Metadata Store - ギャラリー画像のメタデータ永続化
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,14 +25,24 @@ class GalleryMetadataStore:
|
||||
{"name": str}
|
||||
]
|
||||
}
|
||||
|
||||
self._data(特に "images" 辞書)は、バックグラウンドのフォルダインデックススレッド
|
||||
(gallery_service.pyの_bg_index_folder)と、リクエストハンドラを処理するメインスレッドの
|
||||
双方から並行に読み書きされる。ロックなしで辞書を反復(.items()/.values()/for key in ...や
|
||||
json.dumpによるシリアライズ)している最中に別スレッドがキーを追加/削除すると
|
||||
"dictionary changed size during iteration" が発生するため、_data へアクセスする
|
||||
全メソッドを self._lock で保護する(re-entrant: ensure_group()がcreate_group()を
|
||||
内部呼び出しするため、通常のLockではなくRLockを使う)。
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
self._lock = threading.RLock()
|
||||
self._data: dict = {"images": {}, "groups": []}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
# コンストラクタからのみ呼ばれ、この時点ではまだ他スレッドと共有されていないためロック不要
|
||||
if self.file_path.exists():
|
||||
try:
|
||||
with open(self.file_path, "r", encoding="utf-8") as f:
|
||||
@@ -44,6 +55,7 @@ class GalleryMetadataStore:
|
||||
logger.warning("GalleryMetadataStore: load error: %s", e)
|
||||
|
||||
def _save_to_disk(self):
|
||||
# 呼び出し元(save/create_group等)が既にself._lockを保持している前提の内部メソッド
|
||||
try:
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.file_path, "w", encoding="utf-8") as f:
|
||||
@@ -58,113 +70,126 @@ class GalleryMetadataStore:
|
||||
|
||||
def get(self, image_path: str) -> dict:
|
||||
key = self._normalize_path(image_path)
|
||||
return dict(self._data["images"].get(key, {}))
|
||||
with self._lock:
|
||||
return dict(self._data["images"].get(key, {}))
|
||||
|
||||
def save(self, image_path: str, data: dict) -> bool:
|
||||
key = self._normalize_path(image_path)
|
||||
existing = self._data["images"].get(key, {})
|
||||
existing.update(data)
|
||||
allowed = {"favorite", "tags", "memo", "groups", "workflow"}
|
||||
existing = {k: v for k, v in existing.items() if k in allowed}
|
||||
self._data["images"][key] = existing
|
||||
return self._save_to_disk()
|
||||
with self._lock:
|
||||
existing = self._data["images"].get(key, {})
|
||||
existing.update(data)
|
||||
allowed = {"favorite", "tags", "memo", "groups", "workflow"}
|
||||
existing = {k: v for k, v in existing.items() if k in allowed}
|
||||
self._data["images"][key] = existing
|
||||
return self._save_to_disk()
|
||||
|
||||
# ── グループ管理 ──────────────────────────────────────────────
|
||||
|
||||
def list_groups(self) -> list[dict]:
|
||||
return list(self._data.get("groups", []))
|
||||
with self._lock:
|
||||
return list(self._data.get("groups", []))
|
||||
|
||||
def create_group(self, name: str) -> bool:
|
||||
groups = self._data.get("groups", [])
|
||||
if any(g["name"] == name for g in groups):
|
||||
return False
|
||||
groups.append({"name": name})
|
||||
self._data["groups"] = groups
|
||||
return self._save_to_disk()
|
||||
with self._lock:
|
||||
groups = self._data.get("groups", [])
|
||||
if any(g["name"] == name for g in groups):
|
||||
return False
|
||||
groups.append({"name": name})
|
||||
self._data["groups"] = groups
|
||||
return self._save_to_disk()
|
||||
|
||||
def rename_group(self, old_name: str, new_name: str) -> bool:
|
||||
"""グループ名を変更し、全画像のgroupsフィールドも更新する"""
|
||||
groups = self._data.get("groups", [])
|
||||
if not any(g["name"] == old_name for g in groups):
|
||||
return False
|
||||
if any(g["name"] == new_name for g in groups):
|
||||
return False # 新名前が既に存在
|
||||
# グループリスト更新
|
||||
for g in groups:
|
||||
if g["name"] == old_name:
|
||||
g["name"] = new_name
|
||||
break
|
||||
# 全画像のgroupsフィールドを更新
|
||||
for meta in self._data["images"].values():
|
||||
img_groups = meta.get("groups", [])
|
||||
if old_name in img_groups:
|
||||
meta["groups"] = [new_name if g == old_name else g for g in img_groups]
|
||||
return self._save_to_disk()
|
||||
with self._lock:
|
||||
groups = self._data.get("groups", [])
|
||||
if not any(g["name"] == old_name for g in groups):
|
||||
return False
|
||||
if any(g["name"] == new_name for g in groups):
|
||||
return False # 新名前が既に存在
|
||||
# グループリスト更新
|
||||
for g in groups:
|
||||
if g["name"] == old_name:
|
||||
g["name"] = new_name
|
||||
break
|
||||
# 全画像のgroupsフィールドを更新
|
||||
for meta in self._data["images"].values():
|
||||
img_groups = meta.get("groups", [])
|
||||
if old_name in img_groups:
|
||||
meta["groups"] = [new_name if g == old_name else g for g in img_groups]
|
||||
return self._save_to_disk()
|
||||
|
||||
def delete_group(self, name: str) -> bool:
|
||||
groups = [g for g in self._data.get("groups", []) if g["name"] != name]
|
||||
self._data["groups"] = groups
|
||||
# 画像のgroupsからも削除
|
||||
for meta in self._data["images"].values():
|
||||
if name in meta.get("groups", []):
|
||||
meta["groups"] = [g for g in meta["groups"] if g != name]
|
||||
return self._save_to_disk()
|
||||
with self._lock:
|
||||
groups = [g for g in self._data.get("groups", []) if g["name"] != name]
|
||||
self._data["groups"] = groups
|
||||
# 画像のgroupsからも削除
|
||||
for meta in self._data["images"].values():
|
||||
if name in meta.get("groups", []):
|
||||
meta["groups"] = [g for g in meta["groups"] if g != name]
|
||||
return self._save_to_disk()
|
||||
|
||||
def list_images_in_group(self, group_name: str) -> list[str]:
|
||||
result = []
|
||||
for key, meta in self._data["images"].items():
|
||||
if group_name in meta.get("groups", []):
|
||||
result.append(key)
|
||||
return result
|
||||
with self._lock:
|
||||
result = []
|
||||
for key, meta in self._data["images"].items():
|
||||
if group_name in meta.get("groups", []):
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
def remove_stale_paths_from_group(self, group_name: str, stale_paths: list) -> int:
|
||||
"""存在しないパスをグループから一括削除する(1回の保存で完結)"""
|
||||
stale_set = {self._normalize_path(p) for p in stale_paths}
|
||||
count = 0
|
||||
for key, meta in self._data["images"].items():
|
||||
if key in stale_set and group_name in meta.get("groups", []):
|
||||
meta["groups"] = [g for g in meta["groups"] if g != group_name]
|
||||
count += 1
|
||||
if count:
|
||||
self._save_to_disk()
|
||||
return count
|
||||
with self._lock:
|
||||
stale_set = {self._normalize_path(p) for p in stale_paths}
|
||||
count = 0
|
||||
for key, meta in self._data["images"].items():
|
||||
if key in stale_set and group_name in meta.get("groups", []):
|
||||
meta["groups"] = [g for g in meta["groups"] if g != group_name]
|
||||
count += 1
|
||||
if count:
|
||||
self._save_to_disk()
|
||||
return count
|
||||
|
||||
def get_group_member_set(self, group_name: str) -> set:
|
||||
"""グループメンバーのパスをsetで返す(高速フィルタ用)"""
|
||||
return {
|
||||
key for key, meta in self._data["images"].items()
|
||||
if group_name in meta.get("groups", [])
|
||||
}
|
||||
with self._lock:
|
||||
return {
|
||||
key for key, meta in self._data["images"].items()
|
||||
if group_name in meta.get("groups", [])
|
||||
}
|
||||
|
||||
def clear_group(self, group_name: str) -> bool:
|
||||
"""グループ内の全画像を除外する(グループ自体は残す)"""
|
||||
for meta in self._data["images"].values():
|
||||
if group_name in meta.get("groups", []):
|
||||
meta["groups"] = [g for g in meta["groups"] if g != group_name]
|
||||
return self._save_to_disk()
|
||||
with self._lock:
|
||||
for meta in self._data["images"].values():
|
||||
if group_name in meta.get("groups", []):
|
||||
meta["groups"] = [g for g in meta["groups"] if g != group_name]
|
||||
return self._save_to_disk()
|
||||
|
||||
def ensure_group(self, name: str) -> bool:
|
||||
"""グループが存在しない場合のみ作成する"""
|
||||
if any(g["name"] == name for g in self._data.get("groups", [])):
|
||||
return True
|
||||
return self.create_group(name)
|
||||
with self._lock:
|
||||
if any(g["name"] == name for g in self._data.get("groups", [])):
|
||||
return True
|
||||
return self.create_group(name)
|
||||
|
||||
def delete(self, image_path: str) -> bool:
|
||||
"""画像のメタデータエントリを削除する"""
|
||||
key = self._normalize_path(image_path)
|
||||
if key in self._data["images"]:
|
||||
del self._data["images"][key]
|
||||
return self._save_to_disk()
|
||||
return True
|
||||
with self._lock:
|
||||
if key in self._data["images"]:
|
||||
del self._data["images"][key]
|
||||
return self._save_to_disk()
|
||||
return True
|
||||
|
||||
def rename_path(self, old_path: str, new_path: str) -> bool:
|
||||
"""画像のパスキーを変更する(ファイル移動後のメタデータ引継ぎ)"""
|
||||
old_key = self._normalize_path(old_path)
|
||||
new_key = self._normalize_path(new_path)
|
||||
if old_key in self._data["images"]:
|
||||
self._data["images"][new_key] = self._data["images"].pop(old_key)
|
||||
return self._save_to_disk()
|
||||
return True
|
||||
with self._lock:
|
||||
if old_key in self._data["images"]:
|
||||
self._data["images"][new_key] = self._data["images"].pop(old_key)
|
||||
return self._save_to_disk()
|
||||
return True
|
||||
|
||||
def cleanup_stale_images(self, folder_path: str, existing_paths: set) -> int:
|
||||
"""folder_path 配下の存在しないファイルのメタデータを削除する。
|
||||
@@ -173,14 +198,15 @@ class GalleryMetadataStore:
|
||||
folder_key = self._normalize_path(folder_path)
|
||||
if not folder_key.endswith("/"):
|
||||
folder_key += "/"
|
||||
stale = [
|
||||
key for key in self._data["images"]
|
||||
if key.startswith(folder_key) and key not in existing_paths
|
||||
]
|
||||
if not stale:
|
||||
return 0
|
||||
for key in stale:
|
||||
del self._data["images"][key]
|
||||
self._save_to_disk()
|
||||
logger.info("Cleaned up %d stale metadata entries from %s", len(stale), folder_path)
|
||||
return len(stale)
|
||||
with self._lock:
|
||||
stale = [
|
||||
key for key in self._data["images"]
|
||||
if key.startswith(folder_key) and key not in existing_paths
|
||||
]
|
||||
if not stale:
|
||||
return 0
|
||||
for key in stale:
|
||||
del self._data["images"][key]
|
||||
self._save_to_disk()
|
||||
logger.info("Cleaned up %d stale metadata entries from %s", len(stale), folder_path)
|
||||
return len(stale)
|
||||
|
||||
@@ -16,7 +16,7 @@ from typing import Any
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# サポートする画像拡張子
|
||||
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
||||
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"}
|
||||
|
||||
# メタデータ保存ファイル (gallery専用)
|
||||
from .gallery_metadata import GalleryMetadataStore
|
||||
@@ -684,6 +684,11 @@ class GalleryService:
|
||||
if p.suffix.lower() == ".gif":
|
||||
return p
|
||||
|
||||
# SVG はベクター画像でPillowが開けないため、元ファイルをそのまま返す
|
||||
# (<img>タグはブラウザ側で任意サイズにスケーリングして表示する)
|
||||
if p.suffix.lower() == ".svg":
|
||||
return p
|
||||
|
||||
try:
|
||||
mtime = int(p.stat().st_mtime * 1000)
|
||||
except OSError:
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Lab tab plan file management service (JSON plan + optional PNG index thumbnail)."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LabPlanService:
|
||||
def __init__(self, plan_dir: Path):
|
||||
self.plan_dir = plan_dir
|
||||
self.plan_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Path safety
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _safe_path(self, filename: str, ext: str = ".json") -> Path | None:
|
||||
if not filename or not filename.strip():
|
||||
return None
|
||||
name = filename.strip()
|
||||
if not name.endswith(ext):
|
||||
name = name + ext
|
||||
if "/" in name or "\\" in name or name in (".", "..") or "\x00" in name:
|
||||
return None
|
||||
resolved = (self.plan_dir / name).resolve()
|
||||
plan_dir_resolved = self.plan_dir.resolve()
|
||||
if resolved != plan_dir_resolved and not str(resolved).startswith(str(plan_dir_resolved) + "\\") and not str(resolved).startswith(str(plan_dir_resolved) + "/"):
|
||||
return None
|
||||
return resolved
|
||||
|
||||
def _now_iso(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_plans(self) -> list[dict]:
|
||||
if not self.plan_dir.is_dir():
|
||||
return []
|
||||
result = []
|
||||
for path in sorted(self.plan_dir.glob("*.json")):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning("Skipping unreadable lab plan %s: %s", path.name, e)
|
||||
continue
|
||||
|
||||
stem = path.stem
|
||||
thumbnail = None
|
||||
thumb_path = self.plan_dir / f"{stem}.png"
|
||||
if thumb_path.is_file():
|
||||
thumbnail = f"/wfm_data/lab_plan/{stem}.png"
|
||||
|
||||
result.append({
|
||||
"filename": path.name,
|
||||
"name": data.get("name", stem),
|
||||
"note": data.get("note", ""),
|
||||
"batch_count": data.get("batch_count", 1),
|
||||
"updated_at": data.get("updated_at", ""),
|
||||
"thumbnail": thumbnail,
|
||||
})
|
||||
result.sort(key=lambda p: p.get("updated_at", ""), reverse=True)
|
||||
return result
|
||||
|
||||
def get_plan(self, filename: str) -> dict:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"Plan not found: {filename}")
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_plan(self, filename: str, data: dict, index_image_base64: str | None = None) -> dict:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
|
||||
data = dict(data)
|
||||
data["updated_at"] = self._now_iso()
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
if index_image_base64:
|
||||
self.save_index_image(filename, index_image_base64)
|
||||
|
||||
return {"filename": path.name, "updated_at": data["updated_at"]}
|
||||
|
||||
def save_index_image(self, filename: str, image_base64: str) -> None:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
stem = path.stem
|
||||
thumb_path = self.plan_dir / f"{stem}.png"
|
||||
|
||||
# Accept both raw base64 and data: URL forms
|
||||
b64 = image_base64
|
||||
if "," in b64 and b64.strip().lower().startswith("data:"):
|
||||
b64 = b64.split(",", 1)[1]
|
||||
|
||||
image_bytes = base64.b64decode(b64)
|
||||
with open(thumb_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
|
||||
def save_index_image_to_output(self, image_base64: str, filename_prefix: str = "Lab_index") -> dict:
|
||||
"""Saves the contact-sheet index image into ComfyUI's own output folder,
|
||||
alongside the images generated during the run (unlike save_index_image,
|
||||
which saves it next to the plan file in plan_dir)."""
|
||||
b64 = image_base64
|
||||
if "," in b64 and b64.strip().lower().startswith("data:"):
|
||||
b64 = b64.split(",", 1)[1]
|
||||
image_bytes = base64.b64decode(b64)
|
||||
|
||||
import folder_paths # type: ignore
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(filename_prefix, output_dir)
|
||||
save_name = f"{filename}_{counter:05}_.png"
|
||||
save_path = Path(full_output_folder) / save_name
|
||||
save_path.write_bytes(image_bytes)
|
||||
return {"filename": save_name, "subfolder": subfolder}
|
||||
|
||||
def delete_plan(self, filename: str) -> None:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
thumb_path = self.plan_dir / f"{path.stem}.png"
|
||||
if thumb_path.is_file():
|
||||
thumb_path.unlink()
|
||||
@@ -0,0 +1,228 @@
|
||||
"""AI skill (.md system prompt) file management service."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_EXTS = {"md"}
|
||||
|
||||
DEFAULT_SKILL_FILENAME = "svg-icon.md"
|
||||
|
||||
DEFAULT_SKILL_CONTENT = """---
|
||||
name: SVG Icon Generator
|
||||
description: Produces clean, minimal, valid SVG icons and simple illustrations.
|
||||
---
|
||||
|
||||
You are an expert SVG icon and illustration designer. When the user asks for an icon, logo, pictogram, diagram, character, animal, or any vector graphic, respond with valid, clean SVG markup.
|
||||
|
||||
## Composition process (do this before writing markup - skipping this step is the main cause of unrecognizable output)
|
||||
- Break the subject down into 3-8 distinct parts that make it recognizable (e.g. a bird: body, head, beak, eye, wing, tail; a house: wall, roof, door, window). Drop parts that are not essential.
|
||||
- Decide each part's approximate position, size, and draw order within the viewBox (elements listed later in the markup are drawn on top).
|
||||
- Pick the simplest shape that captures each part's silhouette: `<circle>`/`<ellipse>` for round parts, `<rect>` for rectangular parts, `<polygon>` for angular parts (beaks, ears, leaves, roofs). For a rounded organic outline that no primitive can approximate well (a body silhouette, a wing, a leaf edge), use a short `<path>` with 2-4 smooth curve commands (`Q` or `C`) instead of a plain circle - a few deliberate curve points beat a distorted primitive.
|
||||
- Never fall back to drawing an unrelated blob plus a few disconnected decorative lines when a subject is hard to render - always decompose it into the named parts above instead, even if each part is crude.
|
||||
|
||||
## Output rules
|
||||
- Always wrap the SVG markup in a fenced code block labeled `svg`, e.g. ```svg ... ```.
|
||||
- Keep any explanation brief and place it OUTSIDE the code block, never mixed inside the SVG.
|
||||
- Output exactly one `<svg>...</svg>` element per code block.
|
||||
|
||||
## Structure rules
|
||||
- Always include a `viewBox` attribute (e.g. `viewBox="0 0 24 24"`). Do not hardcode large pixel `width`/`height`; if you set them, make them match the viewBox aspect ratio.
|
||||
- Use a small, round coordinate system (0-24 or 0-100).
|
||||
- Round coordinate values to at most 2 decimal places.
|
||||
- Keep every `<path>` short (at most about 6 commands) - long, intricate paths are where quality breaks down most often.
|
||||
- Group related elements with `<g>` when it clarifies structure, but do not over-nest.
|
||||
|
||||
## Color rules
|
||||
- For single-color icons, use `fill="currentColor"` (or `stroke="currentColor"` for line icons) so the icon inherits the surrounding text color.
|
||||
- For multi-color illustrations, use explicit hex colors (e.g. `#4A90D9`) - never named CSS colors like `"blue"`.
|
||||
- Avoid gradients, filters, and blend modes unless the user explicitly asks for a glossy/gradient style - they rarely render correctly from generated coordinates.
|
||||
|
||||
## Accessibility
|
||||
- Include a `<title>` element as the first child of `<svg>` describing what the icon depicts.
|
||||
|
||||
## Security (mandatory - do not violate)
|
||||
- Never include `<script>`, `<foreignObject>`, event handler attributes (`onload`, `onclick`, etc.), or `<a>`/`href` links to external URLs.
|
||||
- Never reference external resources (no `xlink:href` to remote URLs, no `<image>` with external `src`).
|
||||
|
||||
## Example 1 - simple geometric icon (single color, primitives only)
|
||||
|
||||
Request: "Sun icon"
|
||||
|
||||
Parts: disc (circle), 8 rays (lines).
|
||||
|
||||
```svg
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Sun</title>
|
||||
<circle cx="12" cy="12" r="5" fill="currentColor"/>
|
||||
<g stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="12" y1="1" x2="12" y2="4"/>
|
||||
<line x1="12" y1="20" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/>
|
||||
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="4" y2="12"/>
|
||||
<line x1="20" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/>
|
||||
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
|
||||
</g>
|
||||
</svg>
|
||||
```
|
||||
|
||||
## Example 2 - organic subject (multi-color, decomposed into parts)
|
||||
|
||||
Request: "Bird icon"
|
||||
|
||||
Parts: body (rounded outline via a short curved path), head (circle), beak (small triangle), eye (dot), wing (small curved path) - each drawn as its own element, layered head/beak/eye on top of the body.
|
||||
|
||||
```svg
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Bird</title>
|
||||
<path d="M4 16 Q4 10 11 9 Q18 8 20 13 Q20 17 14 18 Q7 19 4 16 Z" fill="#4A90D9"/>
|
||||
<path d="M8 14 Q5 12 3 14 Q6 16 9 16 Z" fill="#3576B8"/>
|
||||
<circle cx="17" cy="10" r="3.2" fill="#4A90D9"/>
|
||||
<polygon points="20,10 23,9.3 20,11.4" fill="#F2B807"/>
|
||||
<circle cx="18" cy="9" r="0.6" fill="#1A1A1A"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Follow this pattern for every request: list the parts first, represent each with the simplest adequate shape, then assemble - never skip straight to unstructured shapes.
|
||||
"""
|
||||
|
||||
SKILL_CREATOR_FILENAME = "skill-creator.md"
|
||||
|
||||
SKILL_CREATOR_CONTENT = """---
|
||||
name: Skill Creator
|
||||
description: Interactively interviews you and drafts a new WFS chat skill (.md system prompt) ready to save.
|
||||
---
|
||||
|
||||
You are a skill-design assistant embedded in ComfyUI Workflow Studio's AI TOOL Chat pane. Your job is to interactively help the user design a NEW "skill" - a markdown file with frontmatter (`name`, `description`) followed by system-prompt instructions. Once saved, a skill can be selected from a dropdown and is silently prepended to future chat messages as a system prompt.
|
||||
|
||||
## How to conduct the interview
|
||||
- Ask ONE focused question at a time, never a bulleted list of questions. Wait for the user's answer before asking the next one.
|
||||
- Cover, in this order, only what is not already clear from context:
|
||||
1. What task or domain should the skill specialize in (what should the model do differently once this skill is active)?
|
||||
2. What output format is expected - plain prose, a specific fenced code block language, a fixed structure, a length limit?
|
||||
3. Any hard constraints or things to avoid (tone, forbidden content, security concerns, things past attempts got wrong)?
|
||||
4. Would a short worked example help smaller/local models follow the pattern more reliably? If yes, ask what a good example input/output looks like, or offer to draft one.
|
||||
- Keep it short - 3 to 5 questions is usually enough. If the user says "just generate it", "that's enough", or similar at any point, stop asking and produce the best skill you can from what you already have.
|
||||
|
||||
## Output format for the finished skill
|
||||
- Once you have enough information, output the complete skill file in a single fenced code block labeled `skill` (```skill ... ```). Never mix prose inside that block - explanation goes outside it.
|
||||
- The block must start with frontmatter exactly in this shape:
|
||||
---
|
||||
name: <short display name, shown in the skill dropdown>
|
||||
description: <one sentence, shown as a tooltip in the dropdown>
|
||||
---
|
||||
- After the frontmatter, write clear, imperative system-prompt instructions for the target skill: what role the model should take, concrete output-format rules, hard constraints, and - if useful - one short worked example following a "request -> ideal output" shape.
|
||||
- After the code block, tell the user in one line that they can click the "Save as new skill" button below the message to add it, or copy the block manually into a new skill.
|
||||
"""
|
||||
|
||||
_FRONTMATTER_RE = re.compile(r"^---\r?\n(.*?)\r?\n---\r?\n?", re.DOTALL)
|
||||
|
||||
|
||||
def _parse_frontmatter(content: str) -> dict:
|
||||
"""Extract a shallow `key: value` frontmatter block, if present."""
|
||||
m = _FRONTMATTER_RE.match(content)
|
||||
if not m:
|
||||
return {}
|
||||
fields: dict = {}
|
||||
for line in m.group(1).splitlines():
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, _, value = line.partition(":")
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if key:
|
||||
fields[key] = value
|
||||
return fields
|
||||
|
||||
|
||||
class SkillService:
|
||||
def __init__(self, skills_dir: Path):
|
||||
self.skills_dir = skills_dir
|
||||
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._seed_default_skill()
|
||||
|
||||
def _seed_default_skill(self) -> None:
|
||||
"""Seed a starter skill file the first time the directory is empty."""
|
||||
try:
|
||||
has_any = any(
|
||||
p.is_file() and p.suffix.lstrip(".").lower() in ALLOWED_EXTS
|
||||
for p in self.skills_dir.iterdir()
|
||||
)
|
||||
except FileNotFoundError:
|
||||
has_any = False
|
||||
if not has_any:
|
||||
(self.skills_dir / DEFAULT_SKILL_FILENAME).write_text(
|
||||
DEFAULT_SKILL_CONTENT, encoding="utf-8"
|
||||
)
|
||||
(self.skills_dir / SKILL_CREATOR_FILENAME).write_text(
|
||||
SKILL_CREATOR_CONTENT, encoding="utf-8"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File CRUD
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_skills(self) -> list[dict]:
|
||||
"""Return sorted list of skill files with parsed frontmatter metadata."""
|
||||
if not self.skills_dir.exists():
|
||||
return []
|
||||
files = []
|
||||
for path in sorted(self.skills_dir.glob("*")):
|
||||
if not (path.is_file() and path.suffix.lstrip(".").lower() in ALLOWED_EXTS):
|
||||
continue
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
content = ""
|
||||
meta = _parse_frontmatter(content)
|
||||
files.append({
|
||||
"filename": path.name,
|
||||
"name": meta.get("name") or path.stem,
|
||||
"description": meta.get("description", ""),
|
||||
"size": path.stat().st_size,
|
||||
})
|
||||
return files
|
||||
|
||||
def get_content(self, filename: str) -> str:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"File not found: {filename}")
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
def save_file(self, filename: str, content: str) -> dict:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
path.write_text(content, encoding="utf-8")
|
||||
meta = _parse_frontmatter(content)
|
||||
return {
|
||||
"filename": path.name,
|
||||
"name": meta.get("name") or path.stem,
|
||||
"description": meta.get("description", ""),
|
||||
"size": path.stat().st_size,
|
||||
}
|
||||
|
||||
def delete_file(self, filename: str) -> None:
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
raise ValueError(f"Invalid filename: {filename}")
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
|
||||
def _safe_path(self, filename: str) -> Path | None:
|
||||
if not filename or not filename.strip():
|
||||
return None
|
||||
# Skills are a flat directory - reject any path separator (no subfolders).
|
||||
name = filename.strip()
|
||||
if "/" in name or "\\" in name or name in (".", ".."):
|
||||
return None
|
||||
ext = Path(name).suffix.lstrip(".").lower()
|
||||
if ext not in ALLOWED_EXTS:
|
||||
return None
|
||||
return self.skills_dir / name
|
||||
@@ -3,6 +3,17 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# プロンプトを直接持つノードタイプ(CLIPTextEncode以外の複合ノード含む)
|
||||
_PROMPT_NODE_TYPES = {
|
||||
"CLIPTextEncode", "CLIPTextEncodeSDXL", "CLIPTextEncodeSDXLRefiner",
|
||||
"CLIPTextEncodeEditPlus", "TextEncodeQwenImageEditPlus", "TextEncodeMageFlowEdit",
|
||||
"SDXLPromptStyler", "SDXLPromptStylerAdvanced", "ImageMetadataPromptLoader",
|
||||
"WFS_PromptText", "ImpactWildcardEncode", "ImpactWildcardProcessor",
|
||||
}
|
||||
|
||||
# 画像を最終出力として保存するノードタイプ
|
||||
_OUTPUT_IMAGE_NODE_TYPES = {"SaveImage", "SaveImageAdvanced"}
|
||||
|
||||
# CLIPLoader / DualCLIPLoader の type フィールド → モデル種別マッピング
|
||||
_CLIP_TYPE_TO_MODEL = {
|
||||
"flux": "Flux",
|
||||
@@ -181,25 +192,27 @@ def analyze_workflow(workflow_data, filename=""):
|
||||
if "hidream" in title:
|
||||
model_types.add("HiDream")
|
||||
|
||||
if ntype == "CLIPTextEncode" or "prompt" in title:
|
||||
if ntype in _PROMPT_NODE_TYPES or "prompt" in title:
|
||||
inputs["prompts"] += 1
|
||||
if ntype in ("LoadImage", "LoadImageMask"):
|
||||
inputs["images"] += 1
|
||||
if ntype == "LoadVideo" or "load video" in title:
|
||||
inputs["videos"] += 1
|
||||
if ntype == "SaveImage":
|
||||
if ntype in _OUTPUT_IMAGE_NODE_TYPES:
|
||||
outputs["images"] += 1
|
||||
if ntype in ("SaveVideo", "VH_VideoCombine"):
|
||||
outputs["videos"] += 1
|
||||
|
||||
# Detect workflow format
|
||||
if raw_nodes is not None:
|
||||
# App format: UI-based structure with definitions (subgraphs) or linearMode
|
||||
# App format: simplified web-app-style UI (linearMode) or .app.json filename.
|
||||
# A workflow that merely *contains* subgraphs (definitions.subgraphs) is still a
|
||||
# normal node graph (comfyui-workflow.js's convertUiToApi flattens subgraphs before
|
||||
# conversion), so it's treated as "ui" here too — must match detectFormat() in
|
||||
# static/js/comfyui-workflow.js or the App/UI badge shown to the user disagrees
|
||||
# with what GenerateUI actually does with the file.
|
||||
extra = workflow_data.get("extra", {})
|
||||
if (
|
||||
"definitions" in workflow_data
|
||||
or extra.get("linearMode") is True
|
||||
):
|
||||
if extra.get("linearMode") is True:
|
||||
wf_format = "app"
|
||||
elif filename and filename.lower().endswith(".app.json"):
|
||||
wf_format = "app"
|
||||
|
||||
@@ -4,7 +4,7 @@ import jinja2
|
||||
from aiohttp import web
|
||||
from pathlib import Path
|
||||
|
||||
from .config import TEMPLATES_DIR, STATIC_DIR, DATA_DIR, WORKFLOWS_DIR
|
||||
from .config import TEMPLATES_DIR, STATIC_DIR, DATA_DIR, WORKFLOWS_DIR, LAB_PLAN_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,6 +41,22 @@ async def serve_workflow_file(request: web.Request) -> web.Response:
|
||||
})
|
||||
|
||||
|
||||
async def serve_lab_plan_file(request: web.Request) -> web.Response:
|
||||
"""Serve files (index thumbnails) from the Lab plan directory."""
|
||||
filename = request.match_info.get("filename", "")
|
||||
if not filename or ".." in filename or "/" in filename or "\\" in filename:
|
||||
return web.Response(status=404)
|
||||
|
||||
file_path = LAB_PLAN_DIR / filename
|
||||
if not file_path.is_file():
|
||||
return web.Response(status=404)
|
||||
|
||||
content_type, _ = mimetypes.guess_type(str(file_path))
|
||||
return web.FileResponse(file_path, headers={
|
||||
"Content-Type": content_type or "application/octet-stream",
|
||||
})
|
||||
|
||||
|
||||
class WorkflowStudio:
|
||||
"""Main entry point for Workflow Studio plugin."""
|
||||
|
||||
@@ -57,11 +73,14 @@ class WorkflowStudio:
|
||||
# Dynamic workflow file serving (thumbnails etc.)
|
||||
app.router.add_get("/wfm_data/workflows/{filename}", serve_workflow_file)
|
||||
|
||||
# Dynamic Lab plan file serving (index thumbnails)
|
||||
app.router.add_get("/wfm_data/lab_plan/{filename}", serve_lab_plan_file)
|
||||
|
||||
# Main page
|
||||
app.router.add_get("/wfm", serve_index_page)
|
||||
|
||||
# API routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes, tagger_routes, gmic_routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes, tagger_routes, gmic_routes, skill_routes, lab_routes
|
||||
|
||||
workflow_routes.setup_routes(app)
|
||||
settings_routes.setup_routes(app)
|
||||
@@ -74,5 +93,7 @@ class WorkflowStudio:
|
||||
wildcard_routes.setup_routes(app)
|
||||
tagger_routes.setup_routes(app)
|
||||
gmic_routes.setup_routes(app)
|
||||
skill_routes.setup_routes(app)
|
||||
lab_routes.setup_routes(app)
|
||||
|
||||
logger.info("Workflow Studio: Routes registered successfully")
|
||||
|
||||
+1
-1
@@ -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.76"
|
||||
version = "0.3.91"
|
||||
license = "MIT"
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
/* Lab tab — experimental I2I batch generation with per-column keyframes */
|
||||
|
||||
/* .wfm-gen-subtab-content.active defaults to flex-direction:row (main.css);
|
||||
Lab needs its Setting/Results nav stacked above the panel body. */
|
||||
#wfm-gen-subtab-lab.active {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wfm-lab-subtab-nav {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-lab-subtab-btn {
|
||||
padding: 6px 16px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--wfm-text-secondary);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.wfm-lab-subtab-btn:hover { color: var(--wfm-text-primary); }
|
||||
.wfm-lab-subtab-btn.active { color: var(--wfm-primary); border-bottom-color: var(--wfm-primary); }
|
||||
|
||||
.wfm-lab-panel {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
height: calc(100% - 33px);
|
||||
}
|
||||
|
||||
.wfm-lab-panel.active { display: flex; }
|
||||
|
||||
.wfm-lab-planjson-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-lab-pane-header {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 4px 0 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---------------- Setting layout: left | center (4 columns) | right ---------------- */
|
||||
|
||||
.wfm-lab-setting-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.wfm-lab-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--wfm-border);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.wfm-lab-pane:last-child { border-right: none; }
|
||||
|
||||
.wfm-lab-left { width: 220px; flex-shrink: 0; }
|
||||
.wfm-lab-center { flex: 1; padding: 0; }
|
||||
.wfm-lab-right { width: 260px; flex-shrink: 0; overflow-y: auto; }
|
||||
|
||||
.wfm-lab-side-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.wfm-lab-plan-workflow-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
padding: 6px 8px;
|
||||
background: var(--wfm-bg-secondary);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wfm-lab-plan-workflow-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: var(--wfm-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wfm-lab-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.wfm-lab-field-row label {
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wfm-lab-field-row input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wfm-lab-note {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wfm-lab-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ---------------- Keyframe columns ---------------- */
|
||||
|
||||
.wfm-lab-columns {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-lab-column {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--wfm-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-lab-column:last-child { border-right: none; }
|
||||
|
||||
.wfm-lab-col-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-lab-col-btns { display: flex; gap: 2px; }
|
||||
|
||||
.wfm-lab-col-cells {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wfm-lab-cell {
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius-sm);
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
background: var(--wfm-bg-tertiary);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.wfm-lab-cell:hover { border-color: var(--wfm-primary); }
|
||||
|
||||
.wfm-lab-cell-index {
|
||||
font-weight: 600;
|
||||
color: var(--wfm-primary);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.wfm-lab-cell-value {
|
||||
color: var(--wfm-text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.wfm-lab-cell.wfm-lab-cell-revert .wfm-lab-cell-value {
|
||||
color: var(--wfm-text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.wfm-lab-cell-applied {
|
||||
margin-top: 3px;
|
||||
padding-top: 3px;
|
||||
border-top: 1px dashed var(--wfm-border);
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary);
|
||||
}
|
||||
|
||||
.wfm-lab-cell-prompt-line {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ---------------- Modal form (checkpoint/vae/prompt/ksampler edit) ---------------- */
|
||||
|
||||
.wfm-lab-modal-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wfm-lab-modal-row label {
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary);
|
||||
}
|
||||
|
||||
.wfm-lab-modal-inline {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wfm-lab-modal-btnrow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wfm-lab-modal-inline > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wfm-lab-modal-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ---------------- Results layout ---------------- */
|
||||
|
||||
.wfm-lab-results-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-lab-results-left {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--wfm-border);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.wfm-lab-results-center {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wfm-lab-results-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wfm-lab-result-thumb {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
border-radius: var(--wfm-radius-sm);
|
||||
border: 1px solid var(--wfm-border);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.wfm-lab-result-thumb:hover { border-color: var(--wfm-primary); }
|
||||
|
||||
.wfm-lab-result-item { position: relative; }
|
||||
|
||||
.wfm-lab-result-index {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border-radius: var(--wfm-radius-sm);
|
||||
}
|
||||
@@ -2948,6 +2948,12 @@ body {
|
||||
background: rgba(74, 158, 255, 0.08);
|
||||
}
|
||||
|
||||
.wfm-i2i-drop-zone.wfm-disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wfm-i2i-drop-label {
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary);
|
||||
@@ -3182,6 +3188,21 @@ body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wfm-help-article-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.wfm-help-article-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--wfm-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wfm-help-card {
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
@@ -5624,6 +5645,23 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wfm-ai-chat-svg-wrapper {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wfm-ai-chat-svg-img {
|
||||
max-width: 240px;
|
||||
max-height: 240px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--wfm-border);
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.wfm-ai-chat-attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -5697,3 +5735,42 @@ body {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wfm-ai-chat-skill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-ai-chat-skill-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wfm-ai-skill-panel {
|
||||
margin: 6px 8px 0;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
background: var(--wfm-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfm-ai-skill-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wfm-ai-skill-panel-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--wfm-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
+306
-2
@@ -5,7 +5,7 @@
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { readJsonStorage, getAiBackendDefaultUrl } from "./util.js";
|
||||
import { readJsonStorage, getAiBackendDefaultUrl, escapeHtml } from "./util.js";
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { comfyEditor } from "./comfyui-editor.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
@@ -534,6 +534,294 @@ async function generateImageFromChat(prompt, negativePrompt) {
|
||||
return `/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${encodeURIComponent(img.type || "output")}`;
|
||||
}
|
||||
|
||||
// アシスタント応答からSVGコードを抽出する(```svg フェンス or 生の <svg>...</svg>)
|
||||
function _extractSvgCode(text) {
|
||||
if (!text) return null;
|
||||
const fenced = text.match(/```(?:svg|xml)?\s*([\s\S]*?<svg[\s\S]*?<\/svg>)[\s\S]*?```/i);
|
||||
if (fenced) return fenced[1].trim();
|
||||
const bare = text.match(/<svg[\s\S]*?<\/svg>/i);
|
||||
if (bare) return bare[0].trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
// SVGコードをプレビュー表示し、Galleryへ保存するボタンを追加する。
|
||||
// <img src="data:..."> でのレンダリングはブラウザがスクリプト実行を無効化するため安全。
|
||||
function _appendSvgPreview(container, svgCode) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "wfm-ai-chat-svg-wrapper";
|
||||
|
||||
const dataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgCode)))}`;
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = dataUrl;
|
||||
img.className = "wfm-ai-chat-svg-img";
|
||||
img.alt = "Generated SVG";
|
||||
wrapper.appendChild(img);
|
||||
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.className = "wfm-btn wfm-btn-sm";
|
||||
saveBtn.textContent = t("aiChatSaveSvgToGallery");
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const res = await fetch("/wfm/gallery/image/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename: `chat_svg_${Date.now()}.svg`, imageData: dataUrl }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || data.error) throw new Error(data.error || `HTTP ${res.status}`);
|
||||
showToast(t("aiChatSvgSaved"), "success");
|
||||
} catch (e) {
|
||||
showToast(t("aiChatSvgSaveFailed") + e.message, "error");
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
wrapper.appendChild(saveBtn);
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
// アシスタント応答から ```skill フェンスを抽出する(Skill Creator用スキルが出力する完成品ブロック)
|
||||
function _extractSkillBlock(text) {
|
||||
if (!text) return null;
|
||||
const m = text.match(/```skill\s*([\s\S]*?)```/i);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
// frontmatterの name から保存用ファイル名を推測する(未設定時は my-skill.md)
|
||||
function _slugifySkillName(content) {
|
||||
const m = content.match(/^---\r?\n[\s\S]*?name:\s*(.+?)\r?\n/);
|
||||
const name = (m ? m[1] : "").trim();
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
return `${slug || "my-skill"}.md`;
|
||||
}
|
||||
|
||||
// 応答内にスキル定義ブロックがあれば「スキルとして保存」ボタンを追加する。
|
||||
// クリックすると管理パネルを開き、新規スキルとしてエディタに内容を流し込む(保存前にレビュー可能)。
|
||||
function _appendSkillSaveButton(container, skillContent) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "wfm-btn wfm-btn-sm";
|
||||
btn.textContent = t("aiChatSaveAsSkill");
|
||||
btn.addEventListener("click", () => {
|
||||
const panel = document.getElementById("wfm-ai-skill-panel");
|
||||
if (panel) panel.style.display = "";
|
||||
skillOpenEditor(null, skillContent);
|
||||
const nameInput = document.getElementById("wfm-ai-skill-editor-filename");
|
||||
if (nameInput) nameInput.value = _slugifySkillName(skillContent);
|
||||
});
|
||||
container.appendChild(btn);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AI skills (Chat system prompt library)
|
||||
// ============================================
|
||||
|
||||
async function skillFetchFiles() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/skills");
|
||||
return res.ok ? await res.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function skillFetchContent(filename) {
|
||||
try {
|
||||
const res = await fetch(`/api/wfm/skills/content?filename=${encodeURIComponent(filename)}`);
|
||||
const data = await res.json();
|
||||
return data.content ?? null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function skillSaveFile(filename, content) {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/skills/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, content }),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.status === "ok" ? data.file : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function skillDeleteFile(filename) {
|
||||
try {
|
||||
await fetch("/api/wfm/skills/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// system prompt として送る前に frontmatter (---...---) を取り除く
|
||||
function _stripSkillFrontmatter(content) {
|
||||
const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
||||
return (m ? content.slice(m[0].length) : content).trim();
|
||||
}
|
||||
|
||||
let skillFiles = [];
|
||||
let skillEditingFilename = null; // null = new file
|
||||
let _activeSkillCache = null; // { filename, content } キャッシュ(送信毎の再フェッチを避ける)
|
||||
|
||||
function skillRenderList() {
|
||||
const list = document.getElementById("wfm-ai-skill-list");
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
|
||||
if (skillFiles.length === 0) {
|
||||
list.innerHTML = `<div class="wfm-pm-empty" style="font-size:12px;">${t("aiSkillNoFiles")}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const f of skillFiles) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-wc-file-item";
|
||||
item.innerHTML = `
|
||||
<div class="wfm-wc-file-item-info">
|
||||
<span class="wfm-wc-file-name" title="${escapeHtml(f.description || "")}">${escapeHtml(f.name)}</span>
|
||||
<span class="wfm-wc-file-ext">.md</span>
|
||||
</div>
|
||||
<div class="wfm-wc-file-item-actions">
|
||||
<button class="wfm-pm-action-btn wfm-ai-skill-edit-btn" title="Edit">✎</button>
|
||||
</div>
|
||||
`;
|
||||
item.querySelector(".wfm-ai-skill-edit-btn").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const content = await skillFetchContent(f.filename);
|
||||
skillOpenEditor(f.filename, content ?? "");
|
||||
});
|
||||
list.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function skillOpenEditor(filename, content) {
|
||||
skillEditingFilename = filename || null;
|
||||
|
||||
const editor = document.getElementById("wfm-ai-skill-editor");
|
||||
const nameInput = document.getElementById("wfm-ai-skill-editor-filename");
|
||||
const contentTA = document.getElementById("wfm-ai-skill-editor-content");
|
||||
const deleteBtn = document.getElementById("wfm-ai-skill-editor-delete-btn");
|
||||
|
||||
if (!editor) return;
|
||||
nameInput.value = filename || "";
|
||||
contentTA.value = content || "";
|
||||
deleteBtn.style.display = filename ? "" : "none";
|
||||
editor.style.display = "";
|
||||
nameInput.focus();
|
||||
}
|
||||
|
||||
function skillCloseEditor() {
|
||||
skillEditingFilename = null;
|
||||
const editor = document.getElementById("wfm-ai-skill-editor");
|
||||
if (editor) editor.style.display = "none";
|
||||
}
|
||||
|
||||
function skillPopulateSelect() {
|
||||
const sel = document.getElementById("wfm-ai-chat-skill-select");
|
||||
if (!sel) return;
|
||||
const settings = loadAiSettings();
|
||||
const wanted = sel.value || settings.activeSkillFilename || "";
|
||||
|
||||
sel.innerHTML = `<option value="">-- ${t("aiSkillNone")} --</option>` +
|
||||
skillFiles.map((f) => `<option value="${escapeHtml(f.filename)}" title="${escapeHtml(f.description || "")}">${escapeHtml(f.name)}</option>`).join("");
|
||||
|
||||
sel.value = skillFiles.some((f) => f.filename === wanted) ? wanted : "";
|
||||
_activeSkillCache = null;
|
||||
}
|
||||
|
||||
async function skillRefreshFiles() {
|
||||
skillFiles = await skillFetchFiles();
|
||||
skillRenderList();
|
||||
skillPopulateSelect();
|
||||
}
|
||||
|
||||
// 選択中スキルの本文を取得(system prompt注入用、選択が変わるまでキャッシュ)
|
||||
async function _getActiveSkillSystemPrompt() {
|
||||
const sel = document.getElementById("wfm-ai-chat-skill-select");
|
||||
const filename = sel?.value || "";
|
||||
if (!filename) return null;
|
||||
if (_activeSkillCache?.filename === filename) return _activeSkillCache.content;
|
||||
const raw = await skillFetchContent(filename);
|
||||
const content = _stripSkillFrontmatter(raw || "");
|
||||
_activeSkillCache = { filename, content };
|
||||
return content;
|
||||
}
|
||||
|
||||
function initSkillManager() {
|
||||
const selectEl = document.getElementById("wfm-ai-chat-skill-select");
|
||||
const manageBtn = document.getElementById("wfm-ai-chat-skill-manage-btn");
|
||||
const panel = document.getElementById("wfm-ai-skill-panel");
|
||||
const closeBtn = document.getElementById("wfm-ai-skill-close-btn");
|
||||
const newBtn = document.getElementById("wfm-ai-skill-new-btn");
|
||||
const saveBtn = document.getElementById("wfm-ai-skill-editor-save-btn");
|
||||
const deleteBtn = document.getElementById("wfm-ai-skill-editor-delete-btn");
|
||||
const cancelBtn = document.getElementById("wfm-ai-skill-editor-cancel-btn");
|
||||
|
||||
if (!selectEl) return;
|
||||
|
||||
skillRefreshFiles();
|
||||
|
||||
selectEl.addEventListener("change", () => {
|
||||
saveAiSettings({ activeSkillFilename: selectEl.value });
|
||||
_activeSkillCache = null;
|
||||
});
|
||||
|
||||
manageBtn?.addEventListener("click", () => {
|
||||
if (!panel) return;
|
||||
const show = panel.style.display === "none";
|
||||
panel.style.display = show ? "" : "none";
|
||||
if (show) skillRefreshFiles();
|
||||
});
|
||||
|
||||
closeBtn?.addEventListener("click", () => {
|
||||
if (panel) panel.style.display = "none";
|
||||
});
|
||||
|
||||
newBtn?.addEventListener("click", () => {
|
||||
skillOpenEditor(null, "---\nname: \ndescription: \n---\n\n");
|
||||
});
|
||||
|
||||
saveBtn?.addEventListener("click", async () => {
|
||||
const nameInput = document.getElementById("wfm-ai-skill-editor-filename");
|
||||
const contentTA = document.getElementById("wfm-ai-skill-editor-content");
|
||||
let filename = (nameInput?.value || "").trim();
|
||||
const content = contentTA?.value || "";
|
||||
|
||||
if (!filename) { showToast(t("pleaseEnterFilename"), "error"); return; }
|
||||
if (!/\.md$/i.test(filename)) filename += ".md";
|
||||
if (!/^[\w\-. ]+\.md$/i.test(filename)) { showToast(t("invalidPathFormat"), "error"); return; }
|
||||
|
||||
const saved = await skillSaveFile(filename, content);
|
||||
if (saved) {
|
||||
showToast(t("savedAs", filename), "success");
|
||||
skillCloseEditor();
|
||||
await skillRefreshFiles();
|
||||
selectEl.value = filename;
|
||||
saveAiSettings({ activeSkillFilename: filename });
|
||||
_activeSkillCache = null;
|
||||
} else {
|
||||
showToast(t("saveFailed"), "error");
|
||||
}
|
||||
});
|
||||
|
||||
deleteBtn?.addEventListener("click", async () => {
|
||||
if (!skillEditingFilename) return;
|
||||
if (!confirm(`Delete "${skillEditingFilename}"?`)) return;
|
||||
await skillDeleteFile(skillEditingFilename);
|
||||
showToast(t("deletedName", skillEditingFilename), "success");
|
||||
if (selectEl.value === skillEditingFilename) {
|
||||
saveAiSettings({ activeSkillFilename: "" });
|
||||
_activeSkillCache = null;
|
||||
}
|
||||
skillCloseEditor();
|
||||
await skillRefreshFiles();
|
||||
});
|
||||
|
||||
cancelBtn?.addEventListener("click", () => skillCloseEditor());
|
||||
}
|
||||
|
||||
function appendChatBubble(messagesEl, role, content, opts = {}) {
|
||||
const div = document.createElement("div");
|
||||
div.className = `wfm-ai-chat-msg wfm-ai-chat-msg-${role}`;
|
||||
@@ -550,6 +838,12 @@ function appendChatBubble(messagesEl, role, content, opts = {}) {
|
||||
img.className = "wfm-ai-chat-attach-thumb";
|
||||
div.appendChild(img);
|
||||
}
|
||||
if (role === "assistant" && content) {
|
||||
const svgCode = _extractSvgCode(content);
|
||||
if (svgCode) _appendSvgPreview(div, svgCode);
|
||||
const skillBlock = _extractSkillBlock(content);
|
||||
if (skillBlock) _appendSkillSaveButton(div, skillBlock);
|
||||
}
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
return div;
|
||||
@@ -577,6 +871,8 @@ function initChatTab() {
|
||||
_clearToolsImage();
|
||||
});
|
||||
|
||||
initSkillManager();
|
||||
|
||||
let chatHistory = [];
|
||||
|
||||
async function sendMessage() {
|
||||
@@ -609,7 +905,11 @@ function initChatTab() {
|
||||
|
||||
try {
|
||||
const tools = imgGenToggle?.checked !== false ? IMAGE_GEN_TOOLS : undefined;
|
||||
const reply = await callChat(url, backend, model, chatHistory, tools);
|
||||
const skillPrompt = await _getActiveSkillSystemPrompt();
|
||||
const messagesToSend = skillPrompt
|
||||
? [{ role: "system", content: skillPrompt }, ...chatHistory]
|
||||
: chatHistory;
|
||||
const reply = await callChat(url, backend, model, messagesToSend, tools);
|
||||
const toolCall = reply.toolCalls?.find(
|
||||
(tc) => (tc.function?.name || tc.name) === "generate_image"
|
||||
);
|
||||
@@ -825,3 +1125,7 @@ export function initAiTab() {
|
||||
initVlmTab();
|
||||
initSettingsTab();
|
||||
}
|
||||
|
||||
// gallery-tab.js の外部(Comic Creator)向けブリッジ(半自動マンガ作成の画像プロンプト
|
||||
// 下書き機能)が、設定タブと同じAI接続設定・LLM呼び出しロジックを再利用するためのexport。
|
||||
export { callLLM, loadAiSettings, isValidBackendUrl };
|
||||
|
||||
+110
-1
@@ -320,6 +320,19 @@ function applyI18nToHtml() {
|
||||
if (aiChatGenI2ITitle) aiChatGenI2ITitle.textContent = t("aiSettingsChatI2IGenTitle");
|
||||
const aiChatGenI2IDedicatedLabel = document.getElementById("wfm-ai-chatgen-i2i-dedicated-label");
|
||||
if (aiChatGenI2IDedicatedLabel) aiChatGenI2IDedicatedLabel.textContent = t("aiSettingsChatGenDedicated");
|
||||
const aiSkillSel = document.getElementById("wfm-ai-chat-skill-select");
|
||||
if (aiSkillSel) {
|
||||
const firstOpt = aiSkillSel.querySelector("option[value='']");
|
||||
if (firstOpt) firstOpt.textContent = `-- ${t("aiSkillNone")} --`;
|
||||
}
|
||||
const aiSkillManageBtn = document.getElementById("wfm-ai-chat-skill-manage-btn");
|
||||
if (aiSkillManageBtn) aiSkillManageBtn.title = t("aiSkillManageBtnTitle");
|
||||
const aiSkillPanelTitle = document.querySelector(".wfm-ai-skill-panel-title");
|
||||
if (aiSkillPanelTitle) aiSkillPanelTitle.textContent = t("aiSkillPanelTitle");
|
||||
const aiSkillNewBtn = document.getElementById("wfm-ai-skill-new-btn");
|
||||
if (aiSkillNewBtn) aiSkillNewBtn.textContent = t("aiSkillNewBtn");
|
||||
const aiSkillContentTA = document.getElementById("wfm-ai-skill-editor-content");
|
||||
if (aiSkillContentTA) aiSkillContentTA.placeholder = t("aiSkillContentPlaceholder");
|
||||
|
||||
// Group panel labels
|
||||
const propTitle = document.querySelector("#wfm-side-tab-group .wfm-group-section-title");
|
||||
@@ -381,6 +394,24 @@ function applyI18nToHtml() {
|
||||
"wfm-help-feeder-gal-1": "helpFeederGal1", "wfm-help-feeder-gal-2": "helpFeederGal2",
|
||||
"wfm-help-feeder-gal-3": "helpFeederGal3", "wfm-help-feeder-gal-4": "helpFeederGal4",
|
||||
"wfm-help-feeder-gal-5": "helpFeederGal5", "wfm-help-feeder-gal-6": "helpFeederGal6",
|
||||
"wfm-help-lab-title": "helpLabTitle",
|
||||
"wfm-help-lab-desc": "helpLabDesc",
|
||||
"wfm-help-lab-card-columns-title": "helpLabCardColumnsTitle",
|
||||
"wfm-help-lab-1": "helpLab1", "wfm-help-lab-2": "helpLab2",
|
||||
"wfm-help-lab-3": "helpLab3", "wfm-help-lab-4": "helpLab4",
|
||||
"wfm-help-lab-5": "helpLab5", "wfm-help-lab-6": "helpLab6",
|
||||
"wfm-help-lab-7": "helpLab7", "wfm-help-lab-8": "helpLab8",
|
||||
"wfm-help-lab-9": "helpLab9",
|
||||
"wfm-help-lab-card-plan-title": "helpLabCardPlanTitle",
|
||||
"wfm-help-lab-plan-1": "helpLabPlan1", "wfm-help-lab-plan-2": "helpLabPlan2",
|
||||
"wfm-help-lab-plan-3": "helpLabPlan3", "wfm-help-lab-plan-4": "helpLabPlan4",
|
||||
"wfm-help-lab-plan-5": "helpLabPlan5", "wfm-help-lab-plan-6": "helpLabPlan6",
|
||||
"wfm-help-lab-plan-7": "helpLabPlan7", "wfm-help-lab-plan-8": "helpLabPlan8",
|
||||
"wfm-help-lab-plan-9": "helpLabPlan9", "wfm-help-lab-plan-10": "helpLabPlan10",
|
||||
"wfm-help-lab-plan-11": "helpLabPlan11",
|
||||
"wfm-help-lab-card-results-title": "helpLabCardResultsTitle",
|
||||
"wfm-help-lab-results-1": "helpLabResults1", "wfm-help-lab-results-2": "helpLabResults2",
|
||||
"wfm-help-lab-results-3": "helpLabResults3",
|
||||
"wfm-help-prompt-1": "helpPrompt1", "wfm-help-prompt-2": "helpPrompt2",
|
||||
"wfm-help-prompt-3": "helpPrompt3", "wfm-help-prompt-4": "helpPrompt4",
|
||||
"wfm-help-prompt-5": "helpPrompt5", "wfm-help-prompt-6": "helpPrompt6",
|
||||
@@ -432,7 +463,18 @@ function applyI18nToHtml() {
|
||||
"wfm-help-sidepanel-17": "helpSidepanel17",
|
||||
"wfm-help-ai-title": "helpAiTitle",
|
||||
"wfm-help-ai-1": "helpAi1", "wfm-help-ai-2": "helpAi2", "wfm-help-ai-3": "helpAi3",
|
||||
"wfm-help-ai-4": "helpAi4", "wfm-help-ai-5": "helpAi5", "wfm-help-ai-6": "helpAi6",
|
||||
"wfm-help-ai-4": "helpAi4", "wfm-help-ai-6": "helpAi6",
|
||||
"wfm-help-ai-7": "helpAi7",
|
||||
"wfm-help-ai-8": "helpAi8",
|
||||
"wfm-help-ai-9": "helpAi9",
|
||||
"wfm-help-ai-card-layout-title": "helpAiCardLayout",
|
||||
"wfm-help-ai-card-translate-title": "helpAiCardTranslate",
|
||||
"wfm-help-ai-card-chat-title": "helpAiCardChat",
|
||||
"wfm-help-ai-card-svg-title": "helpAiCardSvg",
|
||||
"wfm-help-ai-card-skills-title": "helpAiCardSkills",
|
||||
"wfm-help-ai-card-skilltips-title": "helpAiCardSkillTips",
|
||||
"wfm-help-ai-card-tools-title": "helpAiCardTools",
|
||||
"wfm-help-ai-card-settings-title": "helpAiCardSettings",
|
||||
"wfm-help-imageedit-title": "helpImageEditTitle",
|
||||
"wfm-help-imageedit-tools-title": "helpImageEditToolsTitle",
|
||||
"wfm-help-imageedit-layer-title": "helpImageEditLayerTitle",
|
||||
@@ -493,11 +535,66 @@ function applyI18nToHtml() {
|
||||
"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-8": "helpTrouble8",
|
||||
"wfm-help-trouble-9": "helpTrouble9",
|
||||
"wfm-help-support-title": "helpSupportTitle",
|
||||
"wfm-help-support-desc": "helpSupportDesc",
|
||||
"wfm-help-github-desc": "helpGithubDesc",
|
||||
"wfm-help-kofi-desc": "helpKofiDesc",
|
||||
"wfm-help-thanks": "helpThanks",
|
||||
// -- Help card headings (card-based redesign) --
|
||||
"wfm-help-about-card-getting-started-title": "helpAboutCardGettingStarted",
|
||||
"wfm-help-wf-title": "helpWfTitle",
|
||||
"wfm-help-wf-card-browse-title": "helpWfCardBrowse",
|
||||
"wfm-help-wf-card-groups-title": "helpWfCardGroups",
|
||||
"wfm-help-wf-card-panel-title": "helpWfCardPanel",
|
||||
"wfm-help-gen-title": "helpGenTitle",
|
||||
"wfm-help-gen-card-connect-title": "helpGenCardConnect",
|
||||
"wfm-help-gen-card-tabs-title": "helpGenCardTabs",
|
||||
"wfm-help-gen-card-seed-title": "helpGenCardSeed",
|
||||
"wfm-help-gen-card-batchsetup-title": "helpGenCardBatchSetup",
|
||||
"wfm-help-gen-card-batchtypes-title": "helpGenCardBatchTypes",
|
||||
"wfm-help-prompt-title": "helpPromptTitle",
|
||||
"wfm-help-prompt-card-ai-title": "helpPromptCardAi",
|
||||
"wfm-help-prompt-card-presets-title": "helpPromptCardPresets",
|
||||
"wfm-help-prompt-card-wildcards-title": "helpPromptCardWildcards",
|
||||
"wfm-help-metadata-card-overview-title": "helpMetadataCardOverview",
|
||||
"wfm-help-metadata-card-extracted-title": "helpMetadataCardExtracted",
|
||||
"wfm-help-metadata-card-formats-title": "helpMetadataCardFormats",
|
||||
"wfm-help-settings-title": "helpSettingsTitle",
|
||||
"wfm-help-settings-card-general-title": "helpSettingsCardGeneral",
|
||||
"wfm-help-settings-card-connections-title": "helpSettingsCardConnections",
|
||||
"wfm-help-settings-card-workflow-title": "helpSettingsCardWorkflow",
|
||||
"wfm-help-settings-card-editor-title": "helpSettingsCardEditor",
|
||||
"wfm-help-settings-card-misc-title": "helpSettingsCardMisc",
|
||||
"wfm-help-nodes-title": "helpNodesTitle",
|
||||
"wfm-help-nodes-card-browse-title": "helpNodesCardBrowse",
|
||||
"wfm-help-nodes-card-groups-title": "helpNodesCardGroups",
|
||||
"wfm-help-nodes-card-panel-title": "helpNodesCardPanel",
|
||||
"wfm-help-gallery-card-browse-title": "helpGalleryCardBrowse",
|
||||
"wfm-help-gallery-card-folder-title": "helpGalleryCardFolder",
|
||||
"wfm-help-gallery-card-detail-title": "helpGalleryCardDetail",
|
||||
"wfm-help-gallery-card-actions-title": "helpGalleryCardActions",
|
||||
"wfm-help-gallery-card-formats-title": "helpGalleryCardFormats",
|
||||
"wfm-help-models-title": "helpModelsTitle",
|
||||
"wfm-help-models-card-browse-title": "helpModelsCardBrowse",
|
||||
"wfm-help-models-card-civitai-title": "helpModelsCardCivitai",
|
||||
"wfm-help-models-card-apply-title": "helpModelsCardApply",
|
||||
"wfm-help-models-card-groups-title": "helpModelsCardGroups",
|
||||
"wfm-help-models-card-bulk-title": "helpModelsCardBulk",
|
||||
"wfm-help-sidepanel-card-tabs-title": "helpSidepanelCardTabs",
|
||||
"wfm-help-sidepanel-card-dnd-title": "helpSidepanelCardDnd",
|
||||
"wfm-help-sidepanel-card-info-title": "helpSidepanelCardInfo",
|
||||
"wfm-help-sidepanel-card-ai-title": "helpSidepanelCardAi",
|
||||
"wfm-help-sidepanel-card-nav-title": "helpSidepanelCardNav",
|
||||
"wfm-help-tagger-card-overview-title": "helpTaggerCardOverview",
|
||||
"wfm-help-tagger-card-single-title": "helpTaggerCardSingle",
|
||||
"wfm-help-tagger-card-sending-title": "helpTaggerCardSending",
|
||||
"wfm-help-tagger-card-batch-title": "helpTaggerCardBatch",
|
||||
"wfm-help-imageedit-card-overview-title": "helpImageEditCardOverview",
|
||||
"wfm-help-shortcuts-card-general-title": "helpShortcutsCardGeneral",
|
||||
"wfm-help-trouble-card-connection-title": "helpTroubleCardConnection",
|
||||
"wfm-help-trouble-card-workflow-title": "helpTroubleCardWorkflow",
|
||||
"wfm-help-trouble-card-tagger-title": "helpTroubleCardTagger",
|
||||
};
|
||||
for (const [id, key] of Object.entries(helpIdMap)) {
|
||||
const el = document.getElementById(id);
|
||||
@@ -510,6 +607,11 @@ function applyI18nToHtml() {
|
||||
helpSearch.placeholder = t("helpSearchPlaceholder");
|
||||
helpSearch.addEventListener("input", _onHelpSearch);
|
||||
}
|
||||
|
||||
// Jump-to-tab buttons
|
||||
document.querySelectorAll(".wfm-help-jump-btn").forEach((btn) => {
|
||||
btn.textContent = t("helpJumpToTab");
|
||||
});
|
||||
}
|
||||
|
||||
function _onHelpSearch(e) {
|
||||
@@ -603,6 +705,13 @@ function initHelpTab() {
|
||||
document.getElementById(`wfm-help-page-${page}`)?.classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
// Jump-to-tab buttons: switch to the real feature tab this help page documents
|
||||
document.querySelectorAll(".wfm-help-jump-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelector(`.wfm-tab[data-tab="${btn.dataset.jumpTab}"]`)?.click();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
@@ -240,7 +240,13 @@ export const comfyUI = {
|
||||
});
|
||||
const res = await fetch(`${this.baseUrl}/view?${params}`);
|
||||
if (!res.ok) throw new Error(`Failed to fetch image: HTTP ${res.status}`);
|
||||
return await res.blob();
|
||||
const blob = await res.blob();
|
||||
// /view はSVGをセキュリティ上application/octet-streamで返すため、
|
||||
// <img>で表示できるよう拡張子から正しいMIMEタイプへ付け替える
|
||||
if (imageData.filename?.toLowerCase().endsWith(".svg") && blob.type !== "image/svg+xml") {
|
||||
return blob.slice(0, blob.size, "image/svg+xml");
|
||||
}
|
||||
return blob;
|
||||
},
|
||||
|
||||
async uploadImage(file, filename, overwrite = true) {
|
||||
@@ -300,6 +306,9 @@ export const comfyUI = {
|
||||
|
||||
// Extract images from SaveImage outputs only
|
||||
const images = [];
|
||||
// comfyui-tosvg の "Save SVG String" ノードは images ではなく
|
||||
// saved_svg(ファイル名) / path(絶対パス) という独自キーで返す
|
||||
const svgOutputs = [];
|
||||
const outputs = history.outputs || {};
|
||||
for (const nodeOutput of Object.values(outputs)) {
|
||||
if (nodeOutput.images) {
|
||||
@@ -307,10 +316,23 @@ export const comfyUI = {
|
||||
images.push(img);
|
||||
}
|
||||
}
|
||||
if (nodeOutput.saved_svg) {
|
||||
// ComfyUI の実行エンジンは ui 出力の各値をイテラブルとして連結するため、
|
||||
// ノード側が単一文字列で返すと 1 文字ずつ分解された配列になってしまう。
|
||||
// (例: "a.svg" -> ["a",".","s","v","g"]) 文字配列を検出したら結合して復元する。
|
||||
const joinIfCharArray = (v) =>
|
||||
Array.isArray(v) && v.every((c) => typeof c === "string" && c.length === 1)
|
||||
? v.join("")
|
||||
: v;
|
||||
svgOutputs.push({
|
||||
filename: joinIfCharArray(nodeOutput.saved_svg),
|
||||
path: joinIfCharArray(nodeOutput.path) || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onComplete?.(images, seed);
|
||||
return { images, seed };
|
||||
return { images, seed, svgOutputs };
|
||||
} catch (err) {
|
||||
onError?.(err);
|
||||
throw err;
|
||||
|
||||
+71
-34
@@ -5,7 +5,7 @@
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { syncJsonHighlight } from "./json-highlight.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { escapeHtml } from "./util.js";
|
||||
import { escapeHtml, setupSearchClearBtn } from "./util.js";
|
||||
|
||||
// ── Latent Image preset state ─────────────────────────────
|
||||
const _LATENT_PRESET_KEY = "wfm_latent_presets";
|
||||
@@ -367,7 +367,7 @@ export const comfyEditor = {
|
||||
<label>Positive Prompt</label>
|
||||
<div style="display:flex;gap:8px;margin-bottom:6px;">
|
||||
<select id="wfm-prompt-pos-target" class="wfm-select" style="width:auto;flex:1;min-width:0;">
|
||||
${positiveNodes.map((n) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" selected>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${positiveNodes.map((n, i) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" ${i === 0 ? "selected" : ""}>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${nodeOpts}
|
||||
</select>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-pos-revert" title="${t("revertPromptTitle")}">↺</button>
|
||||
@@ -379,7 +379,7 @@ export const comfyEditor = {
|
||||
<label>Negative Prompt</label>
|
||||
<div style="display:flex;gap:8px;margin-bottom:6px;">
|
||||
<select id="wfm-prompt-neg-target" class="wfm-select" style="width:auto;flex:1;min-width:0;">
|
||||
${negativeNodes.map((n) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" selected>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${negativeNodes.map((n, i) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" ${i === 0 ? "selected" : ""}>ID:${n.id} (${n.title})</option>`).join("")}
|
||||
${nodeOpts}
|
||||
</select>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-neg-revert" title="${t("revertPromptTitle")}">↺</button>
|
||||
@@ -541,7 +541,10 @@ export const comfyEditor = {
|
||||
return `
|
||||
<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;">
|
||||
<div class="wfm-search-wrap" style="margin-bottom:4px;width:100%;">
|
||||
<input type="text" class="wfm-input wfm-search-input wfm-model-filter" id="wfm-model-${s.key}-filter" placeholder="Filter..." data-target="wfm-model-${s.key}">
|
||||
<button type="button" class="wfm-search-clear-btn" id="wfm-model-${s.key}-filter-clear" title="Clear search">✕</button>
|
||||
</div>
|
||||
<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("")}
|
||||
@@ -559,8 +562,8 @@ export const comfyEditor = {
|
||||
|
||||
// Filter inputs
|
||||
el.querySelectorAll(".wfm-model-filter").forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
const targetId = input.dataset.target;
|
||||
const targetId = input.dataset.target;
|
||||
const rebuild = () => {
|
||||
const select = document.getElementById(targetId);
|
||||
if (!select) return;
|
||||
const filter = input.value.toLowerCase();
|
||||
@@ -570,7 +573,9 @@ export const comfyEditor = {
|
||||
.filter((m) => m.toLowerCase().includes(filter))
|
||||
.map((m) => `<option value="${m}">${m}</option>`)
|
||||
.join("");
|
||||
});
|
||||
};
|
||||
input.addEventListener("input", rebuild);
|
||||
setupSearchClearBtn(input.id, `${input.id}-clear`, rebuild);
|
||||
});
|
||||
|
||||
// Apply buttons
|
||||
@@ -704,7 +709,10 @@ export const comfyEditor = {
|
||||
|
||||
<!-- Single tab -->
|
||||
<div class="wfm-lora-tab-content" id="wfm-lora-panel-single">
|
||||
<input type="text" class="wfm-input" id="wfm-lora-filter" placeholder="Filter...">
|
||||
<div class="wfm-search-wrap" style="width:100%;">
|
||||
<input type="text" class="wfm-input wfm-search-input" id="wfm-lora-filter" placeholder="Filter...">
|
||||
<button type="button" class="wfm-search-clear-btn" id="wfm-lora-filter-clear" title="Clear search">✕</button>
|
||||
</div>
|
||||
<select class="wfm-select" id="wfm-lora-select">
|
||||
${loras.map((m) => `<option value="${m}" ${m === currentVal ? "selected" : ""}>${m}</option>`).join("")}
|
||||
</select>
|
||||
@@ -804,8 +812,9 @@ export const comfyEditor = {
|
||||
}
|
||||
|
||||
// ── Single: filter ───────────────────────────────────
|
||||
document.getElementById("wfm-lora-filter")?.addEventListener("input", (e) => {
|
||||
const filter = e.target.value.toLowerCase();
|
||||
const loraFilterInput = document.getElementById("wfm-lora-filter");
|
||||
const rebuildLoraSingleList = () => {
|
||||
const filter = loraFilterInput?.value.toLowerCase() || "";
|
||||
const select = document.getElementById("wfm-lora-select");
|
||||
if (!select) return;
|
||||
select.innerHTML = loras
|
||||
@@ -813,7 +822,9 @@ export const comfyEditor = {
|
||||
.map((m) => `<option value="${m}">${m}</option>`)
|
||||
.join("");
|
||||
_refreshLoraSingleDynamic(metadata, civitaiCache);
|
||||
});
|
||||
};
|
||||
loraFilterInput?.addEventListener("input", rebuildLoraSingleList);
|
||||
setupSearchClearBtn("wfm-lora-filter", "wfm-lora-filter-clear", rebuildLoraSingleList);
|
||||
|
||||
// ── Single: update SYNTAX/TRIGGERS on model or strength change ──
|
||||
document.getElementById("wfm-lora-select")?.addEventListener("change", () => _refreshLoraSingleDynamic(metadata, civitaiCache));
|
||||
@@ -1030,23 +1041,39 @@ export const comfyEditor = {
|
||||
const sampler = analysis.sampler_nodes?.[0];
|
||||
const latent = analysis.latent_nodes?.[0];
|
||||
|
||||
// "Advanced Sampling" workflows (Flux.1/Flux.2, SD3.5, Chroma...) spread seed/steps/cfg/
|
||||
// sampler/scheduler/denoise across RandomNoise + CFGGuider + KSamplerSelect + a *Scheduler
|
||||
// node instead of one KSampler. sampler.*NodeId (set in analyzeWorkflow) says which real
|
||||
// node each field writes back to; null/"" means that workflow has no such control at all
|
||||
// (e.g. Flux2Scheduler has no named "scheduler" combo) so the field is disabled here.
|
||||
const schedulerAvailable = !!sampler && sampler.schedulerNodeId != null;
|
||||
const denoiseAvailable = !!sampler && sampler.denoiseNodeId != null;
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="display:flex;flex-direction:row;gap:0;align-items:flex-start;">
|
||||
<div style="flex:1;min-width:0;padding-right:14px;border-right:1px solid var(--wfm-border);">
|
||||
<h3 style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--wfm-text-secondary);margin:0 0 12px;">KSampler</h3>
|
||||
<h3 style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--wfm-text-secondary);margin:0 0 12px;">${sampler?.advanced ? "Sampler (Advanced)" : "KSampler"}</h3>
|
||||
${sampler ? `
|
||||
<input type="hidden" id="wfm-settings-sampler-id" value="${sampler.id}" data-seed-key="${sampler.seedKey || "seed"}">
|
||||
<input type="hidden" id="wfm-settings-sampler-id"
|
||||
data-seed-key="${sampler.seedKey || "seed"}"
|
||||
data-seed-node-id="${sampler.seedNodeId ?? sampler.id}"
|
||||
data-steps-node-id="${sampler.stepsNodeId ?? sampler.id}"
|
||||
data-cfg-key="${sampler.cfgKey || "cfg"}"
|
||||
data-cfg-node-id="${sampler.cfgNodeId ?? sampler.id}"
|
||||
data-sampler-node-id="${sampler.samplerNodeId ?? sampler.id}"
|
||||
data-scheduler-node-id="${sampler.schedulerNodeId ?? ""}"
|
||||
data-denoise-node-id="${sampler.denoiseNodeId ?? ""}">
|
||||
<div class="wfm-form-group">
|
||||
<label>Seed</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-seed" value="${sampler.seed ?? -1}">
|
||||
<input type="number" class="wfm-input" id="wfm-settings-seed" value="${sampler.seed ?? ""}" placeholder="${sampler.seed === undefined ? "linked" : ""}">
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Steps</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-steps" value="${sampler.steps ?? 20}" min="1" max="200">
|
||||
<input type="number" class="wfm-input" id="wfm-settings-steps" value="${sampler.steps ?? ""}" placeholder="${sampler.steps === undefined ? "linked" : ""}" min="1" max="200">
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>CFG</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-cfg" value="${sampler.cfg ?? 7}" step="0.5" min="0">
|
||||
<input type="number" class="wfm-input" id="wfm-settings-cfg" value="${sampler.cfg ?? ""}" placeholder="${sampler.cfg === undefined ? "linked" : ""}" step="0.5" min="0">
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Sampler</label>
|
||||
@@ -1056,13 +1083,15 @@ export const comfyEditor = {
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Scheduler</label>
|
||||
<select class="wfm-select" id="wfm-settings-scheduler">
|
||||
${this.models.schedulers.map((s) => `<option value="${s}" ${s === sampler.scheduler ? "selected" : ""}>${s}</option>`).join("")}
|
||||
<select class="wfm-select" id="wfm-settings-scheduler" ${schedulerAvailable ? "" : "disabled"}>
|
||||
${schedulerAvailable
|
||||
? this.models.schedulers.map((s) => `<option value="${s}" ${s === sampler.scheduler ? "selected" : ""}>${s}</option>`).join("")
|
||||
: `<option value="">N/A</option>`}
|
||||
</select>
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Denoise</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-denoise" value="${sampler.denoise ?? 1.0}" step="0.05" min="0" max="1">
|
||||
<input type="number" class="wfm-input" id="wfm-settings-denoise" value="${sampler.denoise ?? ""}" placeholder="${denoiseAvailable && sampler.denoise === undefined ? "linked" : ""}" step="0.05" min="0" max="1" ${denoiseAvailable ? "" : "disabled"}>
|
||||
</div>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-sampler-apply" title="Apply (Alt+Click: Apply & Generate)">Apply</button>
|
||||
` : "<p class='wfm-placeholder'>No KSampler node found</p>"}
|
||||
@@ -1073,17 +1102,18 @@ export const comfyEditor = {
|
||||
<input type="hidden" id="wfm-settings-latent-id" value="${latent.id}">
|
||||
<div class="wfm-form-group">
|
||||
<label>Width</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-width" value="${latent.width ?? 512}" step="8" min="64">
|
||||
<input type="number" class="wfm-input" id="wfm-settings-width" value="${latent.width ?? ""}" placeholder="${latent.width === undefined ? "linked" : ""}" step="8" min="64">
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Height</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-height" value="${latent.height ?? 512}" step="8" min="64">
|
||||
<input type="number" class="wfm-input" id="wfm-settings-height" value="${latent.height ?? ""}" placeholder="${latent.height === undefined ? "linked" : ""}" step="8" min="64">
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<label>Batch Size</label>
|
||||
<input type="number" class="wfm-input" id="wfm-settings-batch" value="${latent.batch_size ?? 1}" min="1" max="64">
|
||||
</div>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-latent-apply">Apply</button>
|
||||
${latent.width === undefined || latent.height === undefined ? `<p class="wfm-placeholder" style="margin-top:6px;">Width/Height is linked to another node (e.g. auto-follows the input image) — leave blank to keep it linked.</p>` : ""}
|
||||
<div style="margin-top:10px;border-top:1px solid var(--wfm-border);padding-top:10px;">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;">
|
||||
${[512, 768, 1024, 2048].map((s) => `<button class="wfm-btn wfm-btn-sm wfm-latent-sq-btn" data-size="${s}">${s}</button>`).join("")}
|
||||
@@ -1109,17 +1139,21 @@ export const comfyEditor = {
|
||||
`;
|
||||
|
||||
document.getElementById("wfm-settings-sampler-apply")?.addEventListener("click", (e) => {
|
||||
const nodeId = document.getElementById("wfm-settings-sampler-id")?.value;
|
||||
if (!nodeId || !comfyUI.currentWorkflow?.[nodeId]) return;
|
||||
const inputs = comfyUI.currentWorkflow[nodeId].inputs;
|
||||
// KSamplerAdvanced uses noise_seed; fall back to seed
|
||||
const seedKey = document.getElementById("wfm-settings-sampler-id")?.dataset?.seedKey || "seed";
|
||||
inputs[seedKey] = parseInt(document.getElementById("wfm-settings-seed")?.value) || -1;
|
||||
inputs.steps = parseInt(document.getElementById("wfm-settings-steps")?.value) || 20;
|
||||
inputs.cfg = parseFloat(document.getElementById("wfm-settings-cfg")?.value) || 7;
|
||||
inputs.sampler_name = document.getElementById("wfm-settings-sampler-name")?.value;
|
||||
inputs.scheduler = document.getElementById("wfm-settings-scheduler")?.value;
|
||||
inputs.denoise = parseFloat(document.getElementById("wfm-settings-denoise")?.value) || 1.0;
|
||||
const ds = document.getElementById("wfm-settings-sampler-id")?.dataset || {};
|
||||
const write = (nodeId, key, value) => {
|
||||
if (!nodeId || !comfyUI.currentWorkflow?.[nodeId]) return;
|
||||
comfyUI.currentWorkflow[nodeId].inputs[key] = value;
|
||||
};
|
||||
const seedEl = document.getElementById("wfm-settings-seed");
|
||||
if (seedEl?.value !== "") write(ds.seedNodeId, ds.seedKey || "seed", parseInt(seedEl.value) || -1);
|
||||
const stepsEl = document.getElementById("wfm-settings-steps");
|
||||
if (stepsEl?.value !== "") write(ds.stepsNodeId, "steps", parseInt(stepsEl.value) || 20);
|
||||
const cfgEl = document.getElementById("wfm-settings-cfg");
|
||||
if (cfgEl?.value !== "") write(ds.cfgNodeId, ds.cfgKey || "cfg", parseFloat(cfgEl.value) || 7);
|
||||
write(ds.samplerNodeId, "sampler_name", document.getElementById("wfm-settings-sampler-name")?.value);
|
||||
if (ds.schedulerNodeId) write(ds.schedulerNodeId, "scheduler", document.getElementById("wfm-settings-scheduler")?.value);
|
||||
const denoiseEl = document.getElementById("wfm-settings-denoise");
|
||||
if (ds.denoiseNodeId && denoiseEl?.value !== "") write(ds.denoiseNodeId, "denoise", parseFloat(denoiseEl.value) || 1.0);
|
||||
_syncRawJson();
|
||||
if (e.altKey) document.dispatchEvent(new CustomEvent("wfm:apply-and-generate"));
|
||||
});
|
||||
@@ -1128,8 +1162,11 @@ export const comfyEditor = {
|
||||
const nodeId = document.getElementById("wfm-settings-latent-id")?.value;
|
||||
if (!nodeId || !comfyUI.currentWorkflow?.[nodeId]) return;
|
||||
const inputs = comfyUI.currentWorkflow[nodeId].inputs;
|
||||
inputs.width = parseInt(document.getElementById("wfm-settings-width")?.value) || 512;
|
||||
inputs.height = parseInt(document.getElementById("wfm-settings-height")?.value) || 512;
|
||||
const wEl = document.getElementById("wfm-settings-width");
|
||||
const hEl = document.getElementById("wfm-settings-height");
|
||||
// Blank = leave the (linked) width/height alone instead of clobbering it with a default
|
||||
if (wEl?.value !== "") inputs.width = parseInt(wEl.value) || 512;
|
||||
if (hEl?.value !== "") inputs.height = parseInt(hEl.value) || 512;
|
||||
inputs.batch_size = parseInt(document.getElementById("wfm-settings-batch")?.value) || 1;
|
||||
_syncRawJson();
|
||||
});
|
||||
|
||||
+502
-37
@@ -18,31 +18,42 @@ 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.
|
||||
* Trace a link's source through any Bypass-mode (mode 4) nodes AND Reroute nodes, matching each
|
||||
* one's output slot to an input slot of the same type and following the connection upstream
|
||||
* (recursively, for chains of either/both). Mirrors ComfyUI's own Bypass behavior, where a
|
||||
* bypassed node is removed but its wires are reconnected end-to-end — Reroute needs the identical
|
||||
* treatment because it's a UI-only passthrough node with no backend node class at all: leaving it
|
||||
* in the API payload makes ComfyUI reject the whole prompt with "missing_node_type" (Reroute
|
||||
* always has to be resolved through, regardless of its mode).
|
||||
* 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
|
||||
if (!node || (node.mode !== 4 && node.type !== "Reroute")) return [idStr, slot];
|
||||
if (visited.has(idStr)) return null; // circular bypass/reroute 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;
|
||||
let inIdx;
|
||||
if (node.type === "Reroute") {
|
||||
// Reroute is always exactly one input -> one output. Its stored input type is commonly
|
||||
// the generic "*" even when the output shows the propagated concrete type (e.g. MODEL),
|
||||
// so the type-match search used for Bypass below would fail to find it — just take the
|
||||
// sole input.
|
||||
inIdx = 0;
|
||||
} else {
|
||||
inIdx = inputs.findIndex((inp, i) => inp.type === outType && linkMap[idStr]?.[i]);
|
||||
const outType = outputs[slot]?.type;
|
||||
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;
|
||||
if (inIdx === -1 || inIdx === undefined) return null;
|
||||
|
||||
const upstream = linkMap[idStr][inIdx];
|
||||
if (!upstream) return null;
|
||||
@@ -81,7 +92,7 @@ function _getWidgetInputNames(objectInfo, classType) {
|
||||
names.push(name);
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO") {
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
names.push(name);
|
||||
}
|
||||
// Other uppercase types (MODEL, CLIP, etc.) are link inputs, skip
|
||||
@@ -95,7 +106,7 @@ function _getWidgetInputNames(objectInfo, classType) {
|
||||
names.push(name);
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN") {
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +117,7 @@ function _getWidgetInputNames(objectInfo, classType) {
|
||||
|
||||
/**
|
||||
* Get ordered widget input types for a node class (parallel to _getWidgetInputNames).
|
||||
* Returns type info: "INT", "FLOAT", "STRING", "BOOLEAN", or "COMBO".
|
||||
* Returns type info: "INT", "FLOAT", "STRING", "BOOLEAN", "COMBO", or "COMFY_DYNAMICCOMBO_V3".
|
||||
*/
|
||||
function _getWidgetInputTypes(objectInfo, classType) {
|
||||
const info = objectInfo[classType];
|
||||
@@ -121,7 +132,7 @@ function _getWidgetInputTypes(objectInfo, classType) {
|
||||
types.push("COMBO");
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO") {
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
types.push(upper);
|
||||
}
|
||||
}
|
||||
@@ -132,7 +143,7 @@ function _getWidgetInputTypes(objectInfo, classType) {
|
||||
types.push("COMBO");
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO") {
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
types.push(upper);
|
||||
}
|
||||
}
|
||||
@@ -140,6 +151,19 @@ function _getWidgetInputTypes(objectInfo, classType) {
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the sub-input names for a DynamicCombo's currently-selected option.
|
||||
* objectInfo spec shape: ["COMFY_DYNAMICCOMBO_V3", { options: [{ key, inputs: { required: {...} } }, ...] }]
|
||||
*/
|
||||
function _getDynamicComboSubNames(objectInfo, classType, widgetName, selectedKey) {
|
||||
const info = objectInfo[classType];
|
||||
const spec = info?.input?.required?.[widgetName] || info?.input?.optional?.[widgetName];
|
||||
const options = spec?.[1]?.options;
|
||||
if (!Array.isArray(options)) return [];
|
||||
const opt = options.find((o) => o.key === selectedKey) || options[0];
|
||||
return Object.keys(opt?.inputs?.required || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a widget value is a frontend-only extra (like "control_after_generate").
|
||||
* These values appear in widgets_values but are not in object_info inputs.
|
||||
@@ -157,6 +181,124 @@ function _isExtraWidgetValue(val, expectedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a subgraph-template node's widget input (by name) is actually wired end-to-end —
|
||||
* either redirected from an outer parent-graph link, or wired to another internal node within the
|
||||
* same subgraph template (boundary links from the subgraph's own input ports, origin_id === -10,
|
||||
* don't count: those resolve to a plain injected widget value instead, handled separately).
|
||||
*/
|
||||
function _isLinkedWidgetName(remappedNode, sgDef, origInternalId, redirectedTargets, name) {
|
||||
const inputDef = (remappedNode.inputs || []).find((i) => i.widget && i.name === name);
|
||||
if (!inputDef) return false;
|
||||
const slotIdx = remappedNode.inputs.indexOf(inputDef);
|
||||
if (redirectedTargets.has(`${remappedNode.id}:${slotIdx}`)) return true;
|
||||
return inputDef.link != null && (sgDef.links || []).some(
|
||||
(l) => l.id === inputDef.link && String(l.target_id) === origInternalId && l.target_slot === slotIdx && l.origin_id !== -10
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate consuming widgets_values against object_info's widget name/type order, accounting for
|
||||
* frontend-only "control_after_generate" extras (e.g. "fixed" after a seed) that occupy one extra
|
||||
* slot and shift every later index by one. When skipLinked is true, widgets the caller's
|
||||
* isLinkedName() flags as linked are treated as consuming zero widgets_values entries (the
|
||||
* "modern" ComfyUI format); with it false, every widget is assumed present (the "legacy full"
|
||||
* format some subgraph templates store). Returns each widget's actual widgets_values index
|
||||
* (or -1 if the array ran out), plus consumedIndices — every index a widget's own turn advanced
|
||||
* past (normally just its value, but two when a control_after_generate extra was skipped) — and
|
||||
* how many entries were consumed in total (the latter is how callers detect which of the two
|
||||
* formats a given widgets_values array actually follows).
|
||||
*
|
||||
* consumedIndices matters because convertUiToApi's own widget-mapping loop only skips a
|
||||
* control_after_generate extra while advancing past the *next* widget's turn (see its
|
||||
* `!linkedSlotNames.has(name)` branch) — if that next widget turns out to be linked, the loop
|
||||
* `continue`s before ever looking at the extra, leaving it unconsumed and silently misread as the
|
||||
* value of whatever non-linked widget comes after. Stripping a linked widget's position alone
|
||||
* (without its swallowed extra) reproduces exactly that bug one level up, in the injection step.
|
||||
*/
|
||||
function _simulateWidgetValues(widgetNames, widgetTypes, widgetsValues, isLinkedName, skipLinked) {
|
||||
let idx = 0;
|
||||
const positions = {};
|
||||
const consumedIndices = {};
|
||||
for (let nIdx = 0; nIdx < widgetNames.length; nIdx++) {
|
||||
const name = widgetNames[nIdx];
|
||||
if (skipLinked && isLinkedName(name)) {
|
||||
positions[name] = -1;
|
||||
consumedIndices[name] = [];
|
||||
continue;
|
||||
}
|
||||
if (idx >= widgetsValues.length) {
|
||||
positions[name] = -1;
|
||||
consumedIndices[name] = [];
|
||||
continue;
|
||||
}
|
||||
const consumed = [idx];
|
||||
let p = idx;
|
||||
idx++;
|
||||
if (_isExtraWidgetValue(widgetsValues[p], widgetTypes[nIdx]) && idx < widgetsValues.length) {
|
||||
consumed.push(idx);
|
||||
p = idx;
|
||||
idx++;
|
||||
}
|
||||
positions[name] = p;
|
||||
consumedIndices[name] = consumed;
|
||||
}
|
||||
return { positions, consumedIndices, consumed: idx };
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a subgraph-template node's widgets_values entries belonging to widget slots that are
|
||||
* actually linked (see _isLinkedWidgetName), so convertUiToApi's own widget-mapping loop — which
|
||||
* assumes the "modern" format where linked widgets contribute zero widgets_values entries — maps
|
||||
* every remaining widget to the right value instead of drifting by however many linked entries
|
||||
* were left in place. No-op when widgets_values already follows the modern format (nothing to
|
||||
* strip) or the node has no widget inputs at all.
|
||||
*/
|
||||
function _stripLegacyLinkedWidgetValues(objectInfo, remappedNode, sgDef, origInternalId, redirectedTargets) {
|
||||
const widgetsValues = remappedNode.widgets_values;
|
||||
const allWidgetInputs = (remappedNode.inputs || []).filter((i) => i.widget);
|
||||
if (!widgetsValues || allWidgetInputs.length === 0) return;
|
||||
|
||||
const widgetNames = _getWidgetInputNames(objectInfo, remappedNode.type);
|
||||
const widgetTypes = _getWidgetInputTypes(objectInfo, remappedNode.type);
|
||||
const isLinkedName = (name) => _isLinkedWidgetName(remappedNode, sgDef, origInternalId, redirectedTargets, name);
|
||||
|
||||
// If skipping linked widgets already accounts for every entry, widgets_values is already in
|
||||
// the modern (no dangling linked entries) format — nothing to strip.
|
||||
const skipSim = _simulateWidgetValues(widgetNames, widgetTypes, widgetsValues, isLinkedName, true);
|
||||
if (skipSim.consumed === widgetsValues.length) return;
|
||||
|
||||
// Otherwise it's "legacy full": recompute with every widget present to find exactly which
|
||||
// indices the linked ones occupy — including any control_after_generate extra a linked
|
||||
// widget's own turn would have consumed, which must be dropped too (see _simulateWidgetValues).
|
||||
const fullSim = _simulateWidgetValues(widgetNames, widgetTypes, widgetsValues, isLinkedName, false);
|
||||
const linkedIdx = new Set();
|
||||
for (const name of widgetNames) {
|
||||
if (isLinkedName(name)) {
|
||||
for (const i of fullSim.consumedIndices[name] || []) linkedIdx.add(i);
|
||||
}
|
||||
}
|
||||
if (linkedIdx.size > 0) {
|
||||
remappedNode.widgets_values = widgetsValues.filter((_, i) => !linkedIdx.has(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the widgets_values array index that corresponds to a given widget input name on a
|
||||
* subgraph-template node, for injecting an outer subgraph-node's un-linked widget value into it.
|
||||
* Assumes _stripLegacyLinkedWidgetValues has already normalized widgets_values to the modern
|
||||
* format (linked widgets contribute zero entries), matching what convertUiToApi's own
|
||||
* widget-mapping loop expects.
|
||||
*/
|
||||
function _findInjectedWidgetIndex(objectInfo, remappedNode, sgDef, origInternalId, redirectedTargets, targetName) {
|
||||
const widgetNames = _getWidgetInputNames(objectInfo, remappedNode.type);
|
||||
const widgetTypes = _getWidgetInputTypes(objectInfo, remappedNode.type);
|
||||
const widgetsValues = remappedNode.widgets_values || [];
|
||||
const isLinkedName = (name) => _isLinkedWidgetName(remappedNode, sgDef, origInternalId, redirectedTargets, name);
|
||||
const sim = _simulateWidgetValues(widgetNames, widgetTypes, widgetsValues, isLinkedName, true);
|
||||
return sim.positions[targetName] ?? -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a subgraph-containing UI workflow into a plain UI workflow.
|
||||
*
|
||||
@@ -167,10 +309,17 @@ function _isExtraWidgetValue(val, expectedType) {
|
||||
*
|
||||
* Strategy: expand internal nodes into the parent, redirect parent links through
|
||||
* the boundary ports, and convert internal links from object to array format.
|
||||
* Widget values in internal nodes are preserved as-is (they are synced at save time
|
||||
* by ComfyUI frontend). convertUiToApi handles widget mapping via object_info.
|
||||
*
|
||||
* Widget values: the outer subgraph node's own widgets_values hold the values actually
|
||||
* set via the subgraph's collapsed form (what the user sees/edits). The internal nodes'
|
||||
* widgets_values, stored in the subgraph template (definitions.subgraphs[].nodes), are
|
||||
* only stale defaults from when the template was authored — ComfyUI does NOT keep them
|
||||
* synced in saved workflow JSON. Un-linked outer widget inputs are therefore copied onto
|
||||
* the matching internal node's widgets_values (via objectInfo to get the correct index,
|
||||
* since the internal node's real class type is known). Linked outer inputs are left to
|
||||
* the "Redirect parent links" step above, which takes priority.
|
||||
*/
|
||||
function _flattenSubgraphs(workflow) {
|
||||
function _flattenSubgraphs(workflow, objectInfo) {
|
||||
const defs = workflow.definitions?.subgraphs;
|
||||
if (!defs || defs.length === 0) return workflow;
|
||||
|
||||
@@ -206,16 +355,20 @@ function _flattenSubgraphs(workflow) {
|
||||
}
|
||||
|
||||
// === Map subgraph input ports → internal target {nodeId, slot} ===
|
||||
// A single boundary port can fan out to MULTIPLE internal targets (e.g. a "width" pin
|
||||
// feeding both EmptyFlux2LatentImage.width as a widget AND a PreviewAny.source wildcard
|
||||
// link purely for canvas display) — collect all of them, not just the last one seen.
|
||||
const inputPortTargets = {};
|
||||
for (let portIdx = 0; portIdx < (sgDef.inputs || []).length; portIdx++) {
|
||||
const port = sgDef.inputs[portIdx];
|
||||
for (const linkId of (port.linkIds || [])) {
|
||||
const iLink = (sgDef.links || []).find(l => l.id === linkId);
|
||||
if (iLink && iLink.origin_id === -10) {
|
||||
inputPortTargets[portIdx] = {
|
||||
if (!inputPortTargets[portIdx]) inputPortTargets[portIdx] = [];
|
||||
inputPortTargets[portIdx].push({
|
||||
targetNodeId: nodeIdRemap[iLink.target_id],
|
||||
targetSlot: iLink.target_slot,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +389,12 @@ function _flattenSubgraphs(workflow) {
|
||||
}
|
||||
|
||||
// === Redirect parent links ===
|
||||
// Track which {internal node, input slot} pairs end up resolved by a real outer
|
||||
// link after redirection — needed below to tell apart "boundary-only" links
|
||||
// (prompt/model-name ports that are actually widget values, see injection step)
|
||||
// from ports that truly carry a link end-to-end (e.g. width/height wired to a
|
||||
// ResolutionSelector node).
|
||||
const redirectedTargets = new Set();
|
||||
for (const link of wf.links) {
|
||||
if (link[3] === node.id) {
|
||||
const dstSlot = link[4];
|
||||
@@ -244,10 +403,15 @@ function _flattenSubgraphs(workflow) {
|
||||
? (sgDef.inputs || []).findIndex(p => p.name === inputName)
|
||||
: -1;
|
||||
if (portIdx === -1) portIdx = dstSlot;
|
||||
const target = inputPortTargets[portIdx];
|
||||
// Only the port's first fanned-out target is redirected here (a real outer link
|
||||
// feeding a fanned-out port is rare — not seen in any known template so far).
|
||||
// The far more common case (an outer *widget value*, no real link, fanning out to
|
||||
// multiple internal targets) is handled fully below, for every target.
|
||||
const target = inputPortTargets[portIdx]?.[0];
|
||||
if (target) {
|
||||
link[3] = target.targetNodeId;
|
||||
link[4] = target.targetSlot;
|
||||
redirectedTargets.add(`${target.targetNodeId}:${target.targetSlot}`);
|
||||
}
|
||||
}
|
||||
if (link[1] === node.id) {
|
||||
@@ -261,10 +425,102 @@ function _flattenSubgraphs(workflow) {
|
||||
}
|
||||
|
||||
// === Add internal nodes (remapped IDs) ===
|
||||
const remappedByOrigId = {};
|
||||
for (const iNode of sgDef.nodes) {
|
||||
const remapped = JSON.parse(JSON.stringify(iNode));
|
||||
remapped.id = nodeIdRemap[iNode.id];
|
||||
|
||||
// Propagate the outer subgraph-node's Mute/Bypass mode onto every internal node.
|
||||
// Without this, muting or bypassing a whole subgraph instance (e.g. disabling one of
|
||||
// several parallel Flux.2 sampling pipelines via Ctrl-B) leaves its internal nodes at
|
||||
// their template-authored mode (normally 0/enabled), so convertUiToApi's mode check
|
||||
// below never excludes them — they end up in the API payload and can be picked up by
|
||||
// node-existence-based UI logic (e.g. "first matching node found") ahead of the nodes
|
||||
// actually driving the enabled pipeline.
|
||||
if (node.mode === 2 || node.mode === 4) {
|
||||
remapped.mode = node.mode;
|
||||
}
|
||||
|
||||
// Some subgraph-template nodes store widgets_values in the "legacy full" format (one
|
||||
// entry per widget input, including ones that are actually linked — either redirected
|
||||
// from an outer parent-graph link, or wired to another internal node within the same
|
||||
// subgraph template, e.g. EmptyFlux2LatentImage.width <- GetImageSize). Strip those
|
||||
// entries so convertUiToApi's widget-mapping loop (which assumes the "modern" format:
|
||||
// linked widgets contribute zero entries) doesn't drift every subsequent widget by
|
||||
// however many linked entries were left in place — see _stripLegacyLinkedWidgetValues.
|
||||
// Boundary links that terminate in a plain widget value (e.g. prompt text) are NOT
|
||||
// treated as linked here — those resolve via the outer-value injection step below.
|
||||
_stripLegacyLinkedWidgetValues(objectInfo, remapped, sgDef, String(iNode.id), redirectedTargets);
|
||||
|
||||
addedNodes.push(remapped);
|
||||
remappedByOrigId[iNode.id] = remapped;
|
||||
}
|
||||
|
||||
// === Inject outer subgraph-node widget values into internal nodes ===
|
||||
// Only un-linked widget inputs — linked ones are already redirected above. A port can fan
|
||||
// out to several internal targets (see inputPortTargets collection above) — apply the same
|
||||
// value to every one of them, not just the first. Iterating per PORT (rather than per
|
||||
// outer widget) matters for unfilled templates whose outer widgets_values is empty ([]) —
|
||||
// there is then no outer override to apply, but the fan-out's widget-type target (e.g.
|
||||
// EmptyFlux2LatentImage.width) still holds its own template-authored default, and that
|
||||
// same value is what a sibling non-widget target (e.g. a PreviewAny used to render the
|
||||
// value into a text template) needs too.
|
||||
const outerWidgetInputs = (node.inputs || []).filter((inp) => inp.widget);
|
||||
for (let portIdx = 0; portIdx < (sgDef.inputs || []).length; portIdx++) {
|
||||
const targets = inputPortTargets[portIdx];
|
||||
if (!targets || targets.length === 0) continue;
|
||||
|
||||
const port = sgDef.inputs[portIdx];
|
||||
const outerInp = outerWidgetInputs.find((inp) => inp.name === port.name);
|
||||
const outerWidgetIdx = outerInp ? outerWidgetInputs.indexOf(outerInp) : -1;
|
||||
const hasOuterOverride = !!outerInp && outerInp.link == null && outerWidgetIdx !== -1
|
||||
&& outerWidgetIdx < (node.widgets_values || []).length;
|
||||
const outerValue = hasOuterOverride ? node.widgets_values[outerWidgetIdx] : undefined;
|
||||
|
||||
// Value to apply to non-widget (wildcard) fan-out targets: the outer override if
|
||||
// present, otherwise whatever a sibling widget-type target ends up holding (its own
|
||||
// unmodified template default, when there is no outer override).
|
||||
let fallbackLiteral = outerValue;
|
||||
|
||||
for (const target of targets) {
|
||||
const origInternalId = String(target.targetNodeId).split(":").pop();
|
||||
const remappedNode = remappedByOrigId[origInternalId];
|
||||
if (!remappedNode) continue;
|
||||
const targetInputDef = (remappedNode.inputs || [])[target.targetSlot];
|
||||
if (!targetInputDef?.widget) continue;
|
||||
|
||||
// Compute where in widgets_values this widget's value actually lives — accounting
|
||||
// for linked slots (which consume no entry) and control_after_generate-style
|
||||
// extras (which shift later indices by one). See _findInjectedWidgetIndex for why
|
||||
// a plain name-index lookup silently corrupts sibling widgets here.
|
||||
const iWidgetIdx = _findInjectedWidgetIndex(
|
||||
objectInfo, remappedNode, sgDef, origInternalId, redirectedTargets, targetInputDef.name
|
||||
);
|
||||
if (iWidgetIdx === -1) continue;
|
||||
|
||||
if (!remappedNode.widgets_values) remappedNode.widgets_values = [];
|
||||
if (hasOuterOverride) remappedNode.widgets_values[iWidgetIdx] = outerValue;
|
||||
if (fallbackLiteral === undefined) fallbackLiteral = remappedNode.widgets_values[iWidgetIdx];
|
||||
}
|
||||
|
||||
if (fallbackLiteral === undefined) continue;
|
||||
|
||||
for (const target of targets) {
|
||||
const origInternalId = String(target.targetNodeId).split(":").pop();
|
||||
const remappedNode = remappedByOrigId[origInternalId];
|
||||
if (!remappedNode) continue;
|
||||
const targetInputDef = (remappedNode.inputs || [])[target.targetSlot];
|
||||
if (!targetInputDef || targetInputDef.widget) continue;
|
||||
|
||||
// Not a widget-representable input — typically a required wildcard ("*") link slot
|
||||
// with no widget UI (e.g. a "Preview as Text" node used to render a width/height
|
||||
// value into a prompt template string). There is no upstream node to link it to
|
||||
// once flattened (the value only ever existed as this outer widget or a sibling
|
||||
// target's own default), so stash it for convertUiToApi() to inject as a plain
|
||||
// literal value instead of a link.
|
||||
if (!remappedNode._wfmLiteralInputs) remappedNode._wfmLiteralInputs = {};
|
||||
remappedNode._wfmLiteralInputs[targetInputDef.name] = fallbackLiteral;
|
||||
}
|
||||
}
|
||||
|
||||
// === Add internal links (object format → array format) ===
|
||||
@@ -297,8 +553,11 @@ export const comfyWorkflow = {
|
||||
detectFormat(workflow, filename) {
|
||||
if (!workflow || typeof workflow !== "object") return "unknown";
|
||||
if (Array.isArray(workflow.nodes) && workflow.links !== undefined) {
|
||||
// App format: UI-based structure with definitions (subgraphs) or linearMode
|
||||
if (workflow.definitions || workflow.extra?.linearMode === true) return "app";
|
||||
// App format: simplified web-app-style UI (linearMode) or .app.json filename.
|
||||
// A workflow that merely *contains* subgraphs (definitions.subgraphs) is still
|
||||
// a normal node graph — convertUiToApi() flattens subgraphs before conversion,
|
||||
// so it's treated as "ui" rather than blocked as unsupported "app" format.
|
||||
if (workflow.extra?.linearMode === true) return "app";
|
||||
if (filename && /\.app\.json$/i.test(filename)) return "app";
|
||||
return "ui";
|
||||
}
|
||||
@@ -329,11 +588,12 @@ export const comfyWorkflow = {
|
||||
_lastMutedNodes = [];
|
||||
if (!workflow.nodes || !workflow.links) return {};
|
||||
|
||||
// Flatten subgraphs before conversion
|
||||
const flatWorkflow = _flattenSubgraphs(workflow);
|
||||
|
||||
const objectInfo = await _loadObjectInfo();
|
||||
|
||||
// Flatten subgraphs before conversion (needs objectInfo to correctly map outer
|
||||
// subgraph-node widget values onto internal nodes' widgets_values indices)
|
||||
const flatWorkflow = _flattenSubgraphs(workflow, objectInfo);
|
||||
|
||||
// Build link map: dstNode -> { dstSlot -> [srcNodeId, srcSlot] }
|
||||
const linkMap = {};
|
||||
for (const link of flatWorkflow.links) {
|
||||
@@ -358,6 +618,10 @@ export const comfyWorkflow = {
|
||||
_lastBypassedNodes.push({ nodeId: String(node.id), title: node.title || node.type });
|
||||
continue;
|
||||
}
|
||||
// Reroute: UI-only passthrough node, no backend node class — including it in the API
|
||||
// payload triggers a "missing_node_type" 400 from ComfyUI. Its wires are already
|
||||
// resolved through via _resolveBypassSource() below, same as a Bypassed node's.
|
||||
if (node.type === "Reroute") continue;
|
||||
// Skip note/display-only nodes that are unknown to ComfyUI backend
|
||||
if (!objectInfo[node.type] && _isDisplayOnlyNode(node)) continue;
|
||||
const nodeId = String(node.id);
|
||||
@@ -439,6 +703,22 @@ export const comfyWorkflow = {
|
||||
wIdx++;
|
||||
}
|
||||
}
|
||||
// DynamicCombo (e.g. SaveImageAdvanced's "format"): val is the selected
|
||||
// option's key (e.g. "png"). Per comfy_api/latest/_io.py
|
||||
// DynamicCombo._expand_schema_for_dynamic, the backend looks up flat
|
||||
// dot-prefixed keys ("format", "format.bit_depth", ...) in the raw
|
||||
// inputs dict and assembles them into a single dict itself at execution
|
||||
// time — it does NOT accept a pre-built dict under the "format" key.
|
||||
if (expectedType === "COMFY_DYNAMICCOMBO_V3") {
|
||||
inputs[name] = val;
|
||||
const subNames = _getDynamicComboSubNames(objectInfo, node.type, name, val);
|
||||
for (const subName of subNames) {
|
||||
if (wIdx >= widgets.length) break;
|
||||
inputs[`${name}.${subName}`] = widgets[wIdx];
|
||||
wIdx++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// For COMBO inputs, validate the value against the available choices.
|
||||
// Dynamic COMBO options (e.g. Impact Pack's "Select to add Wildcard") may
|
||||
// differ between the saved workflow and the current ComfyUI instance
|
||||
@@ -489,6 +769,15 @@ export const comfyWorkflow = {
|
||||
inputs["wildcard_text"] = widgets[0];
|
||||
}
|
||||
|
||||
// Literal values for non-widget (e.g. wildcard "*") inputs that a subgraph boundary
|
||||
// widget fanned out to — see the injection step in _flattenSubgraphs for why these
|
||||
// can't be represented as widgets_values entries or resolved as a link.
|
||||
if (node._wfmLiteralInputs) {
|
||||
for (const [k, v] of Object.entries(node._wfmLiteralInputs)) {
|
||||
if (!(k in inputs)) inputs[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
api[nodeId] = {
|
||||
class_type: node.type,
|
||||
inputs,
|
||||
@@ -598,15 +887,48 @@ export const comfyWorkflow = {
|
||||
|
||||
// KSamplerAdvanced uses noise_seed instead of seed
|
||||
const seedKey = "seed" in inputs ? "seed" : "noise_seed";
|
||||
const seedVal = inputs.seed ?? inputs.noise_seed;
|
||||
// seed/steps/cfg/denoise are sometimes wired to Primitive nodes (e.g. a shared
|
||||
// "steps" value switched between two LoRA presets) instead of holding a direct
|
||||
// number — leave those undefined so the settings-tab UI shows "linked" instead of
|
||||
// a link-reference array, and Apply doesn't clobber the link with a literal value.
|
||||
result.sampler_nodes.push({
|
||||
id, type: ct, title,
|
||||
seed: inputs.seed ?? inputs.noise_seed,
|
||||
seedKey,
|
||||
steps: inputs.steps, cfg: inputs.cfg,
|
||||
sampler_name: inputs.sampler_name, scheduler: inputs.scheduler,
|
||||
denoise: inputs.denoise,
|
||||
seed: typeof seedVal === "number" ? seedVal : undefined,
|
||||
seedKey, seedNodeId: id,
|
||||
steps: typeof inputs.steps === "number" ? inputs.steps : undefined, stepsNodeId: id,
|
||||
cfg: typeof inputs.cfg === "number" ? inputs.cfg : undefined, cfgNodeId: id,
|
||||
sampler_name: inputs.sampler_name, samplerNodeId: id,
|
||||
scheduler: inputs.scheduler, schedulerNodeId: id,
|
||||
denoise: typeof inputs.denoise === "number" ? inputs.denoise : undefined, denoiseNodeId: id,
|
||||
});
|
||||
}
|
||||
|
||||
// CFGGuider / BasicGuider / DualCFGGuider — used by "Advanced Sampling" workflows
|
||||
// (Flux.1/Flux.2, SD3.5, Chroma, HiDream E1, etc.) in place of KSampler's
|
||||
// positive/negative inputs. These feed SamplerCustomAdvanced rather than driving a
|
||||
// sampler themselves, so role propagation must start here too, or CLIPTextEncode
|
||||
// nodes downstream stay "unknown" role.
|
||||
if (ct === "CFGGuider") {
|
||||
if (Array.isArray(inputs.positive)) samplerPositiveRef[inputs.positive[0]] = true;
|
||||
if (Array.isArray(inputs.negative)) samplerNegativeRef[inputs.negative[0]] = true;
|
||||
}
|
||||
if (ct === "BasicGuider") {
|
||||
if (Array.isArray(inputs.conditioning)) samplerPositiveRef[inputs.conditioning[0]] = true;
|
||||
}
|
||||
// DualCFGGuider (HiDream E1 instruct-pix2pix style edit): cond1/cond2 both derive from
|
||||
// the positive text (often via InstructPixToPixConditioning), negative is separate —
|
||||
// cond2 is left unmarked since it doesn't cleanly map to either role.
|
||||
if (ct === "DualCFGGuider") {
|
||||
if (Array.isArray(inputs.cond1)) samplerPositiveRef[inputs.cond1[0]] = true;
|
||||
if (Array.isArray(inputs.negative)) samplerNegativeRef[inputs.negative[0]] = true;
|
||||
}
|
||||
// SamplerCustom (older/simpler counterpart to SamplerCustomAdvanced): holds
|
||||
// positive/negative directly instead of going through a separate Guider node.
|
||||
if (ct === "SamplerCustom") {
|
||||
if (Array.isArray(inputs.positive)) samplerPositiveRef[inputs.positive[0]] = true;
|
||||
if (Array.isArray(inputs.negative)) samplerNegativeRef[inputs.negative[0]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// === Pass 1b: Expand refs through CONDITIONING graph (BFS, 5 iterations) ===
|
||||
@@ -620,6 +942,7 @@ export const comfyWorkflow = {
|
||||
"ControlNetApply", "ControlNetApplyAdvanced",
|
||||
"IPAdapterApply", "IPAdapterApplyFaceID",
|
||||
"StyleModelApply",
|
||||
"FluxGuidance", "FluxKontextMultiReferenceLatentMethod",
|
||||
]);
|
||||
|
||||
for (let iter = 0; iter < 5; iter++) {
|
||||
@@ -674,6 +997,29 @@ export const comfyWorkflow = {
|
||||
}
|
||||
}
|
||||
}
|
||||
// InstructPixToPixConditioning (HiDream E1 edit) — unlike COND_PASSTHROUGH nodes,
|
||||
// this has *separate* positive/negative inputs feeding separate outputs, so each
|
||||
// role must route only to its own input (not both) or the negative CLIPTextEncode
|
||||
// would incorrectly get marked positive too via the positive-output branch.
|
||||
if (ct === "InstructPixToPixConditioning") {
|
||||
if (isPos && Array.isArray(inputs.positive)) samplerPositiveRef[inputs.positive[0]] = true;
|
||||
if (isNeg && Array.isArray(inputs.negative)) samplerNegativeRef[inputs.negative[0]] = true;
|
||||
}
|
||||
// ComfySwitchNode ("If/Else Switch", on_false/on_true/switch) — used by Ernie
|
||||
// Image's prompt-enhancement toggle to pick between the raw PrimitiveStringMultiline
|
||||
// text and an LLM-generated (TextGenerate) rewrite. The rewrite can't be known
|
||||
// statically, so propagate the role through both branches — only the branch that
|
||||
// resolves to a literal string source (e.g. PrimitiveStringMultiline) will end up
|
||||
// producing a prompt_nodes entry in Pass 2.
|
||||
if (ct === "ComfySwitchNode") {
|
||||
for (const key of ["on_false", "on_true"]) {
|
||||
const tv = inputs[key];
|
||||
if (Array.isArray(tv)) {
|
||||
if (isPos) samplerPositiveRef[tv[0]] = true;
|
||||
if (isNeg) samplerNegativeRef[tv[0]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,6 +1041,72 @@ export const comfyWorkflow = {
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
// --- Advanced Sampler: SamplerCustomAdvanced ---
|
||||
// "Advanced Sampling" pattern used by Flux.1/Flux.2, SD3.5, Chroma, HiDream E1, etc.:
|
||||
// instead of one KSampler node holding seed/steps/cfg/sampler/scheduler, those values
|
||||
// are spread across RandomNoise (noise_seed) → a Guider node (cfg) → KSamplerSelect
|
||||
// (sampler_name) → a *Scheduler node (steps/denoise), all feeding SamplerCustomAdvanced.
|
||||
// Collect them into one composite entry so Settings-tab editing still works; each
|
||||
// value's *NodeId tracks which real node to write back to (null = no such control
|
||||
// exists on this workflow, e.g. Flux2Scheduler has no named "scheduler" combo).
|
||||
if (ct === "SamplerCustomAdvanced") {
|
||||
const noiseId = Array.isArray(inputs.noise) ? String(inputs.noise[0]) : null;
|
||||
const guiderId = Array.isArray(inputs.guider) ? String(inputs.guider[0]) : null;
|
||||
const samplerSelId = Array.isArray(inputs.sampler) ? String(inputs.sampler[0]) : null;
|
||||
const schedulerId = Array.isArray(inputs.sigmas) ? String(inputs.sigmas[0]) : null;
|
||||
|
||||
const noiseInputs = (noiseId && workflow[noiseId]?.inputs) || {};
|
||||
const guiderNode = guiderId ? workflow[guiderId] : null;
|
||||
const guiderInputs = guiderNode?.inputs || {};
|
||||
// DualCFGGuider (HiDream E1 instruct-pix2pix edit) has no plain "cfg" input — its
|
||||
// two CFG knobs are cfg_conds/cfg_cond2_negative. Use cfg_conds as the
|
||||
// representative value so Settings still shows/edits *something* meaningful.
|
||||
const guiderCfgKey = guiderNode?.class_type === "DualCFGGuider" ? "cfg_conds" : "cfg";
|
||||
const samplerSelInputs = (samplerSelId && workflow[samplerSelId]?.inputs) || {};
|
||||
const schedulerInputs = (schedulerId && workflow[schedulerId]?.inputs) || {};
|
||||
|
||||
result.sampler_nodes.push({
|
||||
id, type: ct, title, advanced: true,
|
||||
seed: typeof noiseInputs.noise_seed === "number" ? noiseInputs.noise_seed : undefined,
|
||||
seedKey: "noise_seed", seedNodeId: noiseId,
|
||||
steps: typeof schedulerInputs.steps === "number" ? schedulerInputs.steps : undefined,
|
||||
stepsNodeId: schedulerId,
|
||||
cfg: typeof guiderInputs[guiderCfgKey] === "number" ? guiderInputs[guiderCfgKey] : undefined,
|
||||
cfgNodeId: guiderCfgKey in guiderInputs ? guiderId : null,
|
||||
cfgKey: guiderCfgKey,
|
||||
sampler_name: samplerSelInputs.sampler_name, samplerNodeId: samplerSelId,
|
||||
scheduler: undefined, schedulerNodeId: "scheduler" in schedulerInputs ? schedulerId : null,
|
||||
denoise: typeof schedulerInputs.denoise === "number" ? schedulerInputs.denoise : undefined,
|
||||
denoiseNodeId: "denoise" in schedulerInputs ? schedulerId : null,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Advanced Sampler: SamplerCustom ---
|
||||
// Older/simpler counterpart to SamplerCustomAdvanced: no RandomNoise/Guider
|
||||
// indirection — noise_seed and cfg are its own widgets, and positive/negative feed it
|
||||
// directly. sampler_name/steps/scheduler/denoise still come from separate
|
||||
// KSamplerSelect/*Scheduler nodes via its sampler/sigmas inputs.
|
||||
if (ct === "SamplerCustom") {
|
||||
const samplerSelId = Array.isArray(inputs.sampler) ? String(inputs.sampler[0]) : null;
|
||||
const schedulerId = Array.isArray(inputs.sigmas) ? String(inputs.sigmas[0]) : null;
|
||||
const samplerSelInputs = (samplerSelId && workflow[samplerSelId]?.inputs) || {};
|
||||
const schedulerInputs = (schedulerId && workflow[schedulerId]?.inputs) || {};
|
||||
|
||||
result.sampler_nodes.push({
|
||||
id, type: ct, title, advanced: true,
|
||||
seed: typeof inputs.noise_seed === "number" ? inputs.noise_seed : undefined,
|
||||
seedKey: "noise_seed", seedNodeId: id,
|
||||
steps: typeof schedulerInputs.steps === "number" ? schedulerInputs.steps : undefined,
|
||||
stepsNodeId: schedulerId,
|
||||
cfg: typeof inputs.cfg === "number" ? inputs.cfg : undefined,
|
||||
cfgNodeId: id,
|
||||
sampler_name: samplerSelInputs.sampler_name, samplerNodeId: samplerSelId,
|
||||
scheduler: schedulerInputs.scheduler, schedulerNodeId: "scheduler" in schedulerInputs ? schedulerId : null,
|
||||
denoise: typeof schedulerInputs.denoise === "number" ? schedulerInputs.denoise : undefined,
|
||||
denoiseNodeId: "denoise" in schedulerInputs ? schedulerId : null,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Checkpoint nodes ---
|
||||
// Matches: CheckpointLoaderSimple, CheckpointLoader, "Checkpoint Loader" (WAS), etc.
|
||||
if (ct.includes("CheckpointLoader") || ct === "Checkpoint Loader") {
|
||||
@@ -761,8 +1173,9 @@ export const comfyWorkflow = {
|
||||
}
|
||||
}
|
||||
|
||||
// Qwen-based image+text encoder
|
||||
if (ct === "TextEncodeQwenImageEditPlus") {
|
||||
// Qwen-based image+text encoder (Plus variant takes up to 3 reference images;
|
||||
// the base variant takes one — both share the same "prompt" widget)
|
||||
if (ct === "TextEncodeQwenImageEditPlus" || ct === "TextEncodeQwenImageEdit") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title, role: getRole(),
|
||||
text: inputs.prompt || "",
|
||||
@@ -770,6 +1183,39 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// TextEncodeBooguEdit (Boogu image-edit model) — prompt + negative_prompt in one node
|
||||
if (ct === "TextEncodeBooguEdit") {
|
||||
if (typeof inputs.prompt === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title: `${title} [positive]`, role: "positive",
|
||||
text: inputs.prompt, textKey: "prompt",
|
||||
});
|
||||
}
|
||||
if (typeof inputs.negative_prompt === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title: `${title} [negative]`, role: "negative",
|
||||
text: inputs.negative_prompt, textKey: "negative_prompt",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mage-Flow text encoder — prompt + negative_prompt in one node (fixed roles,
|
||||
// unlike KSampler-derived getRole() since both wires originate from this node)
|
||||
if (ct === "TextEncodeMageFlowEdit") {
|
||||
if (typeof inputs.prompt === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title: `${title} [positive]`, role: "positive",
|
||||
text: inputs.prompt, textKey: "prompt",
|
||||
});
|
||||
}
|
||||
if (typeof inputs.negative_prompt === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title: `${title} [negative]`, role: "negative",
|
||||
text: inputs.negative_prompt, textKey: "negative_prompt",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SDXLPromptStyler / SDXLPromptStylerAdvanced — contains both pos & neg in one node
|
||||
if (ct === "SDXLPromptStyler" || ct === "SDXLPromptStylerAdvanced") {
|
||||
if (inputs.text_positive !== undefined) {
|
||||
@@ -823,10 +1269,29 @@ export const comfyWorkflow = {
|
||||
}
|
||||
|
||||
// --- Latent nodes ---
|
||||
if (ct === "EmptyLatentImage" || ct === "EmptySD3LatentImage") {
|
||||
// EmptyFlux2LatentImage (Flux.2 templates) often has width/height wired to a
|
||||
// GetImageSize node (auto-follows the reference image) rather than a direct number —
|
||||
// leave those as undefined, same treatment as TextEncodeMageFlowEdit below, so the
|
||||
// settings-tab UI shows "linked" instead of a stale default and Apply doesn't clobber it.
|
||||
if (ct === "EmptyLatentImage" || ct === "EmptySD3LatentImage" || ct === "EmptyFlux2LatentImage") {
|
||||
result.latent_nodes.push({
|
||||
id, type: ct, title,
|
||||
width: inputs.width, height: inputs.height,
|
||||
width: typeof inputs.width === "number" ? inputs.width : undefined,
|
||||
height: typeof inputs.height === "number" ? inputs.height : undefined,
|
||||
batch_size: inputs.batch_size,
|
||||
});
|
||||
}
|
||||
|
||||
// TextEncodeMageFlowEdit also carries width/height/batch_size directly
|
||||
// (it builds the latent internally instead of a separate EmptyLatentImage node).
|
||||
// width/height are commonly wired to a ResolutionSelector-style helper node
|
||||
// (array link ref) rather than a direct number — leave those as undefined so
|
||||
// the settings-tab UI falls back to its default instead of rendering a link ref.
|
||||
if (ct === "TextEncodeMageFlowEdit") {
|
||||
result.latent_nodes.push({
|
||||
id, type: ct, title,
|
||||
width: typeof inputs.width === "number" ? inputs.width : undefined,
|
||||
height: typeof inputs.height === "number" ? inputs.height : undefined,
|
||||
batch_size: inputs.batch_size,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { loadFileIntoMetadataTab } from "./metadata-tab.js";
|
||||
import { loadWorkflowIntoEditor } from "./generate-tab.js";
|
||||
import { escapeHtml, setupSearchClearBtn } from "./util.js";
|
||||
import { escapeHtml, setupSearchClearBtn, getAiBackendDefaultUrl } from "./util.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { comfyEditor } from "./comfyui-editor.js";
|
||||
import { callLLM, loadAiSettings, isValidBackendUrl } from "./ai-tab.js";
|
||||
|
||||
// ── 定数 ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -126,6 +127,87 @@ window._wfmReceiveI2IRunRequest = async (imageBlob, params, workflowData, workfl
|
||||
}
|
||||
};
|
||||
|
||||
// ComfyUI Comic Creater からiframe越しにコマの設定(シーン・要素・セリフ等)を受け取り、
|
||||
// AIタブの設定タブと同じLLM接続設定(Ollama/LM Studio)で画像生成プロンプトの下書きを
|
||||
// 生成して返す(半自動マンガ作成の「L」ボタン連携用)。
|
||||
// Comic Creater側のスクリプトタブ「L」ボタンから
|
||||
// iframe.contentWindow._wfmReceiveLLMPromptRequest(context) として呼ばれる。
|
||||
// context: { scene, elements, dialogues, existingPrompt }(いずれも文字列、空文字可)
|
||||
window._wfmReceiveLLMPromptRequest = async (context) => {
|
||||
try {
|
||||
const settings = loadAiSettings();
|
||||
const { backend = "ollama", backendUrl, model } = settings;
|
||||
const url = backendUrl || getAiBackendDefaultUrl(backend);
|
||||
if (!isValidBackendUrl(url)) return { ok: false, message: "AI settings: invalid backend URL (configure it in the Settings tab)" };
|
||||
if (!model) return { ok: false, message: "AI settings: no model selected (configure it in the Settings tab)" };
|
||||
|
||||
const { scene = "", elements = "", dialogues = "", existingPrompt = "" } = context || {};
|
||||
const prompt = [
|
||||
"Create a concise Stable Diffusion image generation prompt (English, comma-separated tags/phrases describing scene, composition, character appearance and action) for one manga panel, based on the following context.",
|
||||
"Output only the prompt text, nothing else.",
|
||||
scene ? `Scene: ${scene}` : null,
|
||||
elements ? `Characters/elements: ${elements}` : null,
|
||||
dialogues ? `Dialogue: ${dialogues}` : null,
|
||||
existingPrompt ? `Current draft prompt (revise/improve it): ${existingPrompt}` : null,
|
||||
].filter(Boolean).join("\n");
|
||||
|
||||
const result = await callLLM(url, backend, model, prompt);
|
||||
const text = (result || "").trim();
|
||||
if (!text) return { ok: false, message: "LLM returned an empty response" };
|
||||
return { ok: true, text };
|
||||
} catch (e) {
|
||||
return { ok: false, message: e.message };
|
||||
}
|
||||
};
|
||||
|
||||
// ComfyUI Comic Creater からiframe越しに画像プロンプト・サイズを受け取り、Generate UIで
|
||||
// テキストから画像を生成して結果URLを返す(半自動マンガ作成のコマ単位バッチ画像生成用)。
|
||||
// Comic Creater側のスクリプトタブ「画像を一括生成」から
|
||||
// iframe.contentWindow._wfmReceiveGenerateRequest(prompt, width, height, negative, workflowData?, workflowFilename?)
|
||||
// として呼ばれる。I2I/Inpaintと異なり画像アップロードは行わない(txt2img)。
|
||||
// workflowData が渡された場合(Comic Creater側のT2I設定でデフォルトワークフローが有効な時)は、
|
||||
// 実行前にそのワークフローを読み込む。渡されない場合はGenerate UIに現在ロード中のワークフローをそのまま使う。
|
||||
window._wfmReceiveGenerateRequest = async (prompt, width, height, negative, workflowData, workflowFilename) => {
|
||||
try {
|
||||
if (!window._wfmGenerateTab?.generate) return { ok: false, message: "Generate UI is not ready yet" };
|
||||
|
||||
if (workflowData) {
|
||||
try {
|
||||
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");
|
||||
} catch (e) {
|
||||
console.warn("[T2I] failed to load default workflow:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const analysis = comfyUI.currentAnalysis;
|
||||
if (!comfyUI.currentWorkflow || !analysis) return { ok: false, message: "No workflow loaded in Generate UI" };
|
||||
|
||||
const workflow = JSON.parse(JSON.stringify(comfyUI.currentWorkflow));
|
||||
const latent = analysis.latent_nodes?.[0];
|
||||
if (latent && width && height && workflow[latent.id]) {
|
||||
workflow[latent.id].inputs.width = width;
|
||||
workflow[latent.id].inputs.height = height;
|
||||
}
|
||||
|
||||
comfyEditor.setPromptText("positive", prompt || "", { workflow, analysis });
|
||||
if (negative) comfyEditor.setPromptText("negative", negative, { workflow, analysis });
|
||||
|
||||
await window._wfmGenerateTab.generate(workflow);
|
||||
|
||||
const resultUrl = document.getElementById("wfm-gen-result-img")?.src;
|
||||
if (!resultUrl) return { ok: false, message: "No result image produced" };
|
||||
return { ok: true, url: resultUrl };
|
||||
} catch (e) {
|
||||
return { ok: false, message: e.message };
|
||||
}
|
||||
};
|
||||
|
||||
// ── ページング ────────────────────────────────────────────────
|
||||
const PAGE_SIZE = 50;
|
||||
let _renderedCount = 0;
|
||||
@@ -319,7 +401,7 @@ function renderTreeNode(node, container, depth, isRoot) {
|
||||
// フォルダ名
|
||||
const label = document.createElement("span");
|
||||
label.className = "wfm-gallery-tree-label";
|
||||
label.textContent = isRoot ? `[root] ${node.name}` : node.name;
|
||||
label.textContent = isRoot ? "[root]" : node.name;
|
||||
if (node.image_count > 0) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "wfm-gallery-tree-count";
|
||||
@@ -1736,7 +1818,7 @@ function bindEvents() {
|
||||
if (window.opener && typeof window.opener.wfmReceiveWorkflow === "function") {
|
||||
window.opener.wfmReceiveWorkflow(state.embeddedWorkflow);
|
||||
await navigator.clipboard.writeText(JSON.stringify(state.embeddedWorkflow, null, 2)).catch(() => {});
|
||||
showToast(t("workflowSentToCanvas"), "success");
|
||||
showToast(t("workflowSentToCanvasDirect"), "success");
|
||||
return;
|
||||
}
|
||||
// フォールバック: localStorage + タイトルドラッグ(UI形式のみ)
|
||||
|
||||
+57
-48
@@ -9,44 +9,8 @@ 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, escapeHtml } from "./util.js";
|
||||
|
||||
// ============================================
|
||||
// Eagle Auto-Save
|
||||
// ============================================
|
||||
|
||||
function getEagleSettings() {
|
||||
const s = getSettings();
|
||||
return {
|
||||
url: s.eagleUrl || "http://localhost:41595",
|
||||
autoSave: !!s.eagleAutoSave,
|
||||
};
|
||||
}
|
||||
|
||||
async function saveToEagle(imageUrl, name, tags = []) {
|
||||
const eagle = getEagleSettings();
|
||||
if (!eagle.autoSave) return;
|
||||
try {
|
||||
const res = await fetch("/api/wfm/eagle/add", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
eagleUrl: eagle.url,
|
||||
url: imageUrl,
|
||||
name,
|
||||
tags: ["wfm-comfyui", ...tags],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === "success") {
|
||||
console.log("[Eagle] Saved:", name);
|
||||
} else {
|
||||
console.warn("[Eagle] Save failed:", data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[Eagle] Save error:", err.message);
|
||||
}
|
||||
}
|
||||
import { initLabTab } from "./lab-tab.js";
|
||||
import { getSettings, readJsonStorage, escapeHtml, getEagleSettings, saveToEagle } from "./util.js";
|
||||
|
||||
// ============================================
|
||||
// Gallery Metadata - ワークフロー保存
|
||||
@@ -1471,7 +1435,7 @@ async function _coreGenerate(silent = false, workflowOverride = null) {
|
||||
const baseWorkflow = workflowOverride || comfyUI.currentWorkflow;
|
||||
const workflowExpanded = await _expandWildcardsInWorkflow({ ...baseWorkflow });
|
||||
const workflowForGenerate = _applyStyleToWorkflow(workflowExpanded);
|
||||
const { images, seed } = await comfyUI.generate(
|
||||
const { images, seed, svgOutputs } = await comfyUI.generate(
|
||||
workflowForGenerate,
|
||||
{
|
||||
seedMode,
|
||||
@@ -1489,18 +1453,49 @@ async function _coreGenerate(silent = false, workflowOverride = null) {
|
||||
if (progressText) progressText.textContent = `Done (${images.length} image${images.length !== 1 ? "s" : ""})`;
|
||||
if (progressBar) progressBar.style.width = "100%";
|
||||
|
||||
if (images.length > 0) {
|
||||
const blob = await comfyUI.getImageBlob(images[0]);
|
||||
// type=temp はプレビュー用の一時画像 (PreviewImage, SVG String Preview など) のため
|
||||
// 生成結果表示からは除外し、実際に保存された成果物のみを表示する
|
||||
const outputImages = images.filter((img) => img.type !== "temp");
|
||||
|
||||
if (svgOutputs?.length > 0) {
|
||||
// comfyui-tosvg の Save SVG String は images ではなく saved_svg/path で返すため、
|
||||
// images配列には(あれば)プレビュー用の一時PNGしか入らず実際の保存物と食い違う。
|
||||
// 実際に保存されたSVGファイルをそのままプレビューに表示する。
|
||||
// /view はSVGをセキュリティ上application/octet-streamで返すため使えず、
|
||||
// 代わりにGalleryタブの配信エンドポイント(絶対パス指定・正しいContent-Type)を使う。
|
||||
const svgUrl = (svg) => `/wfm/gallery/image/serve?path=${encodeURIComponent(svg.path)}`;
|
||||
if (resultImg && svgOutputs[0].path) {
|
||||
resultImg.src = svgUrl(svgOutputs[0]);
|
||||
resultImg.style.display = "block";
|
||||
}
|
||||
|
||||
if (resultThumbs && svgOutputs.length > 1) {
|
||||
resultThumbs.innerHTML = "";
|
||||
svgOutputs.forEach((svg, i) => {
|
||||
if (!svg.path) return;
|
||||
const thumb = document.createElement("img");
|
||||
thumb.src = svgUrl(svg);
|
||||
thumb.className = `wfm-gen-thumb ${i === 0 ? "active" : ""}`;
|
||||
thumb.addEventListener("click", () => {
|
||||
resultImg.src = thumb.src;
|
||||
resultThumbs.querySelectorAll(".wfm-gen-thumb").forEach((t) => t.classList.remove("active"));
|
||||
thumb.classList.add("active");
|
||||
});
|
||||
resultThumbs.appendChild(thumb);
|
||||
});
|
||||
}
|
||||
} else if (outputImages.length > 0) {
|
||||
const blob = await comfyUI.getImageBlob(outputImages[0]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
if (resultImg) {
|
||||
resultImg.src = url;
|
||||
resultImg.style.display = "block";
|
||||
}
|
||||
|
||||
if (resultThumbs && images.length > 1) {
|
||||
if (resultThumbs && outputImages.length > 1) {
|
||||
resultThumbs.innerHTML = "";
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const b = i === 0 ? blob : await comfyUI.getImageBlob(images[i]);
|
||||
for (let i = 0; i < outputImages.length; i++) {
|
||||
const b = i === 0 ? blob : await comfyUI.getImageBlob(outputImages[i]);
|
||||
const u = i === 0 ? url : URL.createObjectURL(b);
|
||||
const thumb = document.createElement("img");
|
||||
thumb.src = u;
|
||||
@@ -1515,10 +1510,18 @@ async function _coreGenerate(silent = false, workflowOverride = null) {
|
||||
}
|
||||
}
|
||||
|
||||
if (getEagleSettings().autoSave && images.length > 0) {
|
||||
for (const img of images) {
|
||||
if (getEagleSettings().autoSave && outputImages.length > 0) {
|
||||
for (const img of outputImages) {
|
||||
const viewUrl = `/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${encodeURIComponent(img.type || "output")}`;
|
||||
saveToEagle(viewUrl, img.filename);
|
||||
saveToEagle(viewUrl, img.filename, [], img);
|
||||
}
|
||||
}
|
||||
|
||||
if (getEagleSettings().autoSave && svgOutputs?.length > 0) {
|
||||
// comfyui-tosvg の "Save SVG String" は絶対パスを直接返すため、/view を介さず
|
||||
// サーバー側でそのパスを検証した上で addFromPath させる
|
||||
for (const svg of svgOutputs) {
|
||||
saveToEagle("", svg.filename, [], { filename: svg.filename, localPath: svg.path });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1916,7 +1919,7 @@ export async function initGenerateTab() {
|
||||
function moveRawJsonToTab(tabKey) {
|
||||
const widget = document.getElementById("wfm-gen-rawjson-widget");
|
||||
if (!widget) return;
|
||||
if (tabKey === "feeder" || tabKey === "batch") {
|
||||
if (tabKey === "feeder" || tabKey === "batch" || tabKey === "lab") {
|
||||
widget.style.display = "none";
|
||||
return;
|
||||
}
|
||||
@@ -1940,6 +1943,9 @@ export async function initGenerateTab() {
|
||||
if (target === "model" && comfyUI.currentAnalysis) {
|
||||
comfyEditor.renderLoraPane(comfyUI.currentAnalysis, "wfm-gen-lora-fields");
|
||||
}
|
||||
// Lab has its own Run/Results panel — hide the shared Generate/Seed/Batch column
|
||||
const rightCol = document.querySelector(".wfm-gen-right");
|
||||
if (rightCol) rightCol.style.display = (target === "lab") ? "none" : "";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2116,6 +2122,9 @@ export async function initGenerateTab() {
|
||||
// Feeder tab
|
||||
await initFeederTab();
|
||||
|
||||
// Lab tab (experimental I2I batch generation)
|
||||
initLabTab();
|
||||
|
||||
// Style dropdown
|
||||
await _loadStyles();
|
||||
|
||||
|
||||
+465
-15
@@ -92,6 +92,7 @@ const LANGUAGES = {
|
||||
sendToCanvas: "Send to Canvas",
|
||||
copyAndSendCanvas: "Copy & Send Canvas",
|
||||
workflowSentToCanvas: "Workflow ready — drag 'Workflow Studio Library' title to canvas",
|
||||
workflowSentToCanvasDirect: "Workflow sent to canvas",
|
||||
comfyUILoadTimeout: "ComfyUI load timeout",
|
||||
popupBlocked: "Popup blocked. Please allow popups for this site.",
|
||||
setAsDefault: "Set as Default",
|
||||
@@ -216,6 +217,49 @@ const LANGUAGES = {
|
||||
batchStopped: (completed, failed) => `Batch stopped — ${completed} completed, ${failed} failed`,
|
||||
batchComplete: (completed, total, failed) => failed > 0 ? `Batch complete: ${completed}/${total} (${failed} failed)` : `Batch complete: ${completed}/${total}`,
|
||||
batchLoraMissing: (count, total, sample) => `${count}/${total} selected LoRAs are not recognized by ComfyUI yet (e.g. ${sample}). Click Refresh in ComfyUI or restart the server, then try again.`,
|
||||
|
||||
// -- Lab tab --
|
||||
labImageUploaded: (name) => `Image uploaded: ${name}`,
|
||||
labUploadFailed: "Upload failed",
|
||||
labBatchCountTooSmall: "Increase the Batch count first to add a keyframe beyond it",
|
||||
labUseWorkflowDefault: "(workflow default)",
|
||||
labAppliedAt: (n) => `Applied: ${n}`,
|
||||
labRevertToBase: "↩ back to #1",
|
||||
labApplyFromIteration: "Applies from iteration #",
|
||||
labRevertCheckboxLabel: "Revert to iteration #1's setting",
|
||||
labSeedEmptyRandom: "Seed (empty = random)",
|
||||
labPause: "Pause",
|
||||
labResume: "Resume",
|
||||
labNoWorkflowLoaded: "Load a workflow into GenerateUI first",
|
||||
labNoSourceImage: "Drop a source image first",
|
||||
labNoResultsYet: "No results yet",
|
||||
labResultPreview: "Preview",
|
||||
labEnterPlanName: "Plan name:",
|
||||
labPlanSaved: "Plan saved",
|
||||
labPlanSaveFailed: "Failed to save plan",
|
||||
labPlanLoaded: "Plan loaded",
|
||||
labPlanLoadFailed: "Failed to load plan",
|
||||
labPlanInvalidFile: "Drop a .json plan file or its .png index image",
|
||||
labConfirmClear: "Clear all Lab settings (image, columns, note, results)?",
|
||||
labGetFromGenUI: "Get from GenerateUI",
|
||||
labGetFromImage: "Get from Image",
|
||||
labStyle: "Style",
|
||||
labApply: "Apply",
|
||||
labNoStyles: "No styles",
|
||||
labEnableStyleFirst: "Enable the Style checkbox and pick a style at the top of the GenerateUI tab first",
|
||||
labPlanJsonRefreshed: "Refreshed from the current Setting tab",
|
||||
labPlanJsonApplied: "Plan JSON applied to Setting",
|
||||
labPlanJsonInvalid: "Invalid JSON",
|
||||
labStyleApplied: "Style applied",
|
||||
labPromptFetched: "Prompt loaded",
|
||||
labNoPromptInImage: "No prompt found in that file",
|
||||
labMetadataExtractFailed: "Failed to read metadata",
|
||||
labConfirmLoadWorkflow: (filename) => `Load "${filename}" into GenerateUI? This replaces the currently loaded workflow.`,
|
||||
labWorkflowLoadFailed: "Failed to load the workflow",
|
||||
labT2IImageIgnored: "T2I mode is on — image upload is not needed and was skipped",
|
||||
labIndexImageSaved: (filename) => `Index image saved to Output: ${filename}`,
|
||||
labIndexImageSaveFailed: "Failed to save the index image to Output",
|
||||
|
||||
connectedToComfyUI: "Connected to ComfyUI",
|
||||
failedToConnect: "Failed to connect",
|
||||
modelListsRefreshed: "Model lists refreshed",
|
||||
@@ -611,12 +655,23 @@ const LANGUAGES = {
|
||||
aiChatGenerateNotReady: "GenerateUI is not ready yet",
|
||||
aiChatNoImageProduced: "Generation completed but no image was produced",
|
||||
aiToastImageGenFailed: "Image generation failed: ",
|
||||
aiChatSaveSvgToGallery: "Save to Gallery",
|
||||
aiChatSvgSaved: "SVG saved to gallery",
|
||||
aiChatSvgSaveFailed: "Failed to save SVG: ",
|
||||
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",
|
||||
aiSkillNone: "No skill",
|
||||
aiSkillManageBtnTitle: "Manage skills",
|
||||
aiSkillPanelTitle: "Skills",
|
||||
aiSkillNewBtn: "+ New",
|
||||
aiSkillNoFiles: "No skills yet. Click \"+ New\" to create one.",
|
||||
aiSkillFilenamePlaceholder: "filename.md",
|
||||
aiSkillContentPlaceholder: "---\nname: My Skill\ndescription: What this skill does\n---\n\nSystem prompt instructions for the LLM...",
|
||||
aiChatSaveAsSkill: "Save as new skill",
|
||||
|
||||
// -- Help & Support --
|
||||
tabHelp: "Help",
|
||||
@@ -633,7 +688,7 @@ const LANGUAGES = {
|
||||
helpWf6: "Detail modal: rename, favorite, thumbnail change, badge assignment, tags, memo, AI summary, send to canvas, load in GenerateUI, set as default, delete",
|
||||
helpWf7: "Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)",
|
||||
helpGen1: "Connect/disconnect ComfyUI and check connection status",
|
||||
helpGen2: "Load workflows from Workflow tab (App format .app.json not supported); Reset Workflow button re-reads the current workflow file from disk and reloads the editor; Save button (right end of the tab bar) saves the current workflow — including any edits made in the editor — as a new .json file to the Workflow tab; a filename dialog appears with the current workflow name as the default",
|
||||
helpGen2: "Load workflows from Workflow tab (subgraph-containing workflows are supported, e.g. Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image templates; only ComfyUI's simplified App format [.app.json / Linear Mode] is not); Reset Workflow button re-reads the current workflow file from disk and reloads the editor; Save button (right end of the tab bar) saves the current workflow — including any edits made in the editor — as a new .json file to the Workflow tab; a filename dialog appears with the current workflow name as the default",
|
||||
helpGen3: "5-tab layout — Input (Prompt + Image), Model, Settings, Feeder, Batch — Input/Model/Settings each include a Raw JSON column for instant preview and direct editing",
|
||||
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",
|
||||
@@ -641,7 +696,7 @@ const LANGUAGES = {
|
||||
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",
|
||||
helpGen10: "Eagle integration: auto-save generated images to Eagle (SVG output supported too — both the standard SaveSVGNode and comfyui-tosvg's Save SVG String node)",
|
||||
helpGen11: "Batch tab — enable one batch type at a time by checking the checkbox in the Batch Queue column header; Generate then runs through all queued items sequentially with Pause/Resume and Stop controls",
|
||||
helpGen12: "Batch tab — Left pane has 3 tabs: Checkpoints (folder tree with Filter/All/None; (root) header checkbox selects all), Sampler (KSampler sampler_name list with (root) checkbox), Scheduler (KSampler scheduler list with (root) checkbox); All/None toolbar buttons also available",
|
||||
helpGen13: "Batch tab — Center pane: group-based selection with inner tabs (Checkpoint | Lora | Prompt | Workflow); Checkpoint/Lora groups come from the Models tab, Prompt groups from the Prompt tab, Workflow groups from the Workflow tab — check a group header to select all its members, or expand ▶ to select individually",
|
||||
@@ -672,6 +727,36 @@ const LANGUAGES = {
|
||||
helpFeederGal4: "Right pane — Preview: displays the currently selected input image; updates when a thumbnail is clicked",
|
||||
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",
|
||||
|
||||
helpLabTitle: "GenerateUI Tab — Lab subtab",
|
||||
helpLabDesc: "Experimental I2I batch generation. Runs the workflow currently loaded in GenerateUI N times, letting Checkpoint / VAE / Prompt / KSampler each change independently starting at a chosen iteration. Two subtabs: Setting and Results.",
|
||||
helpLabCardColumnsTitle: "Setting subtab — Image & Columns",
|
||||
helpLab1: "Left pane — Image: drop or click to select an I2I source image; it is uploaded to ComfyUI's input folder and used for every iteration unless \"Use generated image for next\" is checked",
|
||||
helpLab2: "Left pane — Use generated image for next: from iteration 2 onward, feeds the previous iteration's first output image back in as the I2I source instead of the original dropped image (iteration 1 always uses the original)",
|
||||
helpLab3: "Center pane — 4 independent columns: Checkpoint, VAE, Prompt, KSampler; each column keeps its own list of keyframes rather than sharing a row grid",
|
||||
helpLab4: "+ / − per column: + appends a new keyframe at the next free iteration number (up to the Batch count) and opens its edit modal immediately; − removes the last keyframe (iteration #1 can never be removed)",
|
||||
helpLab5: "Click a cell to edit it: iteration #1 sets the baseline value (leave it blank to keep the loaded workflow's own setting); iteration #2+ also lets you choose which iteration it starts applying from, plus a \"revert to #1's setting\" checkbox",
|
||||
helpLab6: "Effective value at iteration N: the latest keyframe with iteration ≤ N is used and carries forward until the next keyframe changes it; \"revert to #1\" reuses iteration #1's value (or the workflow default if #1 is blank) instead of specifying a new one",
|
||||
helpLab7: "Lab never edits the workflow shown in Input/Model/Settings — every iteration runs on a fresh clone of the loaded workflow, so keyframes here never affect the other subtabs",
|
||||
helpLab8: "Prompt cell editor extras: Get from GenerateUI fills Positive/Negative from the workflow currently loaded in GenerateUI; Get from Image extracts a prompt from the image currently loaded in Lab's own Image drop zone (same detection as the Metadata tab; no separate file picker); Style appends whichever style is currently selected in the Style checkbox/dropdown at the top of the GenerateUI toolbar to the Positive/Negative text below, comma-separated after any existing text (never clears it) — enable that checkbox and pick a style first, Lab doesn't have its own separate style picker",
|
||||
helpLab9: "Left pane — T2I workflow (no source image): check this when the loaded workflow is text-to-image (no LoadImage node). It skips the \"drop a source image first\" requirement, ignores any dropped image, and the Checkpoint/VAE/Prompt/KSampler keyframes still apply normally; the Image drop zone and \"Use generated image for next\" are greyed out while it's on (any previously dropped image is kept, not deleted, in case you switch back to I2I); the setting is saved with the plan as t2i_mode",
|
||||
helpLabCardPlanTitle: "Setting subtab — Run & Plan files",
|
||||
helpLabPlan1: "Right pane — Batch: number of iterations to run; Note: free-text memo saved together with the plan",
|
||||
helpLabPlan2: "Run / Pause / Cancel: Run starts the batch loop; Pause/Resume freezes it between iterations; Cancel interrupts the current generation and stops the loop",
|
||||
helpLabPlan3: "Plan Save vs Save As: Plan Save silently overwrites the currently loaded plan (or asks for a name the first time); Save As always asks for a new name, even while a plan is already loaded",
|
||||
helpLabPlan4: "Plan Load: drop a plan's .json file, or any of its index-image PNGs, onto the PLAN box (or click it to browse) — a dropped .json is read directly; a dropped .png is read directly too if it carries embedded plan data (every index image since v0.3.90 does, in a wfm_lab_plan chunk), falling back to fetching a same-named .json from the server for older index images that predate this",
|
||||
helpLabPlan5: "Plan Clear resets the Setting tab to an empty plan (asks for confirmation first)",
|
||||
helpLabPlan6: "Plan files are stored in user/default/Workflow-Studio/lab_plan/ as ws_labplan_<name>.json plus a ws_labplan_<name>.png index image — a contact sheet of up to 9 result thumbnails (3 per row) generated automatically whenever a plan with results is saved. The ws_labplan_ prefix is added automatically by Plan Save/Save As (type just the plain name when prompted) so plan files are easy to pick out among other files in that folder",
|
||||
helpLabPlan7: "Eagle auto-save (configured in the Settings tab) applies to every image Lab generates, the same as GenerateUI's own Generate button",
|
||||
helpLabPlan8: "Plan JSON subtab: shows the current Setting tab's plan as raw JSON; Refresh re-reads it from the Setting tab (discarding any unapplied text edits), Apply to Setting parses your edits and writes them back into the Setting tab (columns, note, batch count, chain-image, source image, results) — the same effect as loading that JSON as a plan file, without needing to save it first",
|
||||
helpLabPlan9: "Each saved plan records the filename of the workflow that was loaded in GenerateUI at save time (workflow_filename in the Plan JSON). After loading a plan, an Open Workflow button appears in the PLAN box — click it to load that exact workflow file back into GenerateUI (asks for confirmation, since it replaces whatever is currently loaded)",
|
||||
helpLabPlan10: "Right pane — Save index image to Output on Run: when checked, every time Run finishes it builds the same contact-sheet index image used for Plan files and saves it directly into ComfyUI's own Output folder as Lab_index_<counter>_.png (auto-numbered like a normal generated image, never overwriting a previous run's). Once saved, it also goes through Eagle auto-save (if enabled in the Settings tab) just like every other Lab-generated image — no separate checkbox for that, it follows the same global toggle. This is independent of Plan Save, which still always writes its own index image next to the plan file in lab_plan/ regardless of this checkbox; this setting is saved with the plan as save_index_on_run",
|
||||
helpLabPlan11: "Every index image PNG — whether from Plan Save/Save As or from Save index image to Output on Run — carries the entire plan (columns, note, batch count, chain-image/T2I settings, workflow filename, results) embedded in it directly as a PNG wfm_lab_plan chunk, so the Output-folder copy is self-contained and works as a Plan Load source too even though it has no matching .json next to it anywhere",
|
||||
helpLabCardResultsTitle: "Results subtab",
|
||||
helpLabResults1: "Left pane shows the source image used for the run",
|
||||
helpLabResults2: "Center pane shows up to 9 generated images in generation order; click a thumbnail to view it enlarged",
|
||||
helpLabResults3: "Loading a saved plan restores its Results grid from the image list recorded in the plan file",
|
||||
|
||||
helpSearchPlaceholder: "Search help...",
|
||||
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",
|
||||
@@ -768,8 +853,72 @@ const LANGUAGES = {
|
||||
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; 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",
|
||||
helpAi7: "Chat pane — SVG: the LLM can also produce SVG graphics directly as text, with no ComfyUI workflow involved; ask it to draw something in SVG and, if the reply contains SVG code, it is rendered inline as a preview with a \"Save to Gallery\" button that writes it as a .svg file to the Gallery tab's output folder; this does not use tool calling, so it works with any model regardless of tool-calling support",
|
||||
helpAi8: "Chat pane — Skills: pick a skill from the dropdown above the message list to prepend its instructions to the conversation as a system prompt (not shown in the visible chat log); use the pencil button to open the skill manager and create, edit, or delete .md skill files, stored in user/default/Workflow-Studio/ai_skills/; a file's first line block between --- markers can define name/description shown in the dropdown; two starter skills are included by default: \"SVG Icon Generator\" (decomposes the subject into parts before drawing, for more recognizable SVG output) and \"Skill Creator\" (interviews you conversationally and drafts a new skill file, ready to save); when a reply contains a ```skill code block, a \"Save as new skill\" button appears below it that opens the manager with the draft pre-filled",
|
||||
helpAi9: "Tips for writing your own skills: give a clear frontmatter name/description so it reads well in the dropdown and tooltip; state the exact output format you want and, if there is a deliverable to extract later (like SVG or another skill file), wrap it in a distinctive fenced code block; write concrete, imperative instructions rather than vague ones - smaller local models follow explicit rules and a short worked example far better than general guidance; state hard constraints (forbidden content, security, length limits) explicitly rather than assuming they are implied; when in doubt, select the \"Skill Creator\" skill in the dropdown and describe what you want - it will interview you and draft the file for you",
|
||||
helpAiCardLayout: "Layout",
|
||||
helpAiCardTranslate: "Translation pane",
|
||||
helpAiCardChat: "Chat pane",
|
||||
helpAiCardSvg: "Chat pane — SVG generation",
|
||||
helpAiCardSkills: "Chat pane — Skills",
|
||||
helpAiCardSkillTips: "Writing your own skills",
|
||||
helpAiCardTools: "TOOLS pane",
|
||||
helpAiCardSettings: "Settings pane",
|
||||
helpJumpToTab: "Open tab →",
|
||||
helpAboutCardGettingStarted: "Getting Started",
|
||||
helpWfTitle: "Workflow Tab",
|
||||
helpWfCardBrowse: "Browsing & Filtering",
|
||||
helpWfCardGroups: "Groups & Badges",
|
||||
helpWfCardPanel: "Side Panel & Detail Modal",
|
||||
helpGenTitle: "GenerateUI Tab",
|
||||
helpGenCardConnect: "Connecting & Loading Workflows",
|
||||
helpGenCardTabs: "Tab Layout",
|
||||
helpGenCardSeed: "Seed & Generation",
|
||||
helpGenCardBatchSetup: "Batch Tab — Setup",
|
||||
helpGenCardBatchTypes: "Batch Tab — Batch Types",
|
||||
helpPromptTitle: "Prompt Tab",
|
||||
helpPromptCardAi: "AI Assistant & Translation",
|
||||
helpPromptCardPresets: "Presets",
|
||||
helpPromptCardWildcards: "Wildcards",
|
||||
helpMetadataCardOverview: "Overview",
|
||||
helpMetadataCardExtracted: "Extracted Information",
|
||||
helpMetadataCardFormats: "Supported Formats",
|
||||
helpSettingsTitle: "Settings Tab",
|
||||
helpSettingsCardGeneral: "General",
|
||||
helpSettingsCardConnections: "Connections",
|
||||
helpSettingsCardWorkflow: "Workflow Defaults",
|
||||
helpSettingsCardEditor: "Editor Customization",
|
||||
helpSettingsCardMisc: "Data & Misc",
|
||||
helpNodesTitle: "Nodes Tab",
|
||||
helpNodesCardBrowse: "Browse & Organize",
|
||||
helpNodesCardGroups: "Groups & Sets",
|
||||
helpNodesCardPanel: "Side Panel & Multi-select",
|
||||
helpGalleryCardBrowse: "Browsing",
|
||||
helpGalleryCardFolder: "Folder & Selection",
|
||||
helpGalleryCardDetail: "Detail Panel",
|
||||
helpGalleryCardActions: "Actions Toolbar",
|
||||
helpGalleryCardFormats: "Formats & Auto-save",
|
||||
helpModelsTitle: "Models Tab",
|
||||
helpModelsCardBrowse: "Browsing",
|
||||
helpModelsCardCivitai: "Side Panel & CivitAI",
|
||||
helpModelsCardApply: "Apply & Detail",
|
||||
helpModelsCardGroups: "Groups & Badges",
|
||||
helpModelsCardBulk: "Enable/Disable & Bulk Operations",
|
||||
helpSidepanelCardTabs: "Tabs Overview",
|
||||
helpSidepanelCardDnd: "Drag & Drop / Interaction",
|
||||
helpSidepanelCardInfo: "Information (I) tab",
|
||||
helpSidepanelCardAi: "AI TOOL tab",
|
||||
helpSidepanelCardNav: "Customization & Navigation",
|
||||
helpTaggerCardOverview: "Overview & Setup",
|
||||
helpTaggerCardSingle: "Single Tab",
|
||||
helpTaggerCardSending: "Sending Results",
|
||||
helpTaggerCardBatch: "Batch & DB Tabs",
|
||||
helpImageEditCardOverview: "Overview",
|
||||
helpShortcutsCardGeneral: "General",
|
||||
helpTroubleCardConnection: "Connection Issues",
|
||||
helpTroubleCardWorkflow: "Workflow & Generation Issues",
|
||||
helpTroubleCardTagger: "Tagger & Model Issues",
|
||||
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",
|
||||
@@ -863,6 +1012,7 @@ const LANGUAGES = {
|
||||
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.",
|
||||
helpTrouble9: "Generate runs a different workflow than the one just loaded: a Batch type checkbox (Checkpoint/LoRA/Prompt/Workflow/Sampler/Scheduler) is left ON in the Batch subtab. While any Batch type is active, the Generate button runs the batch selection instead of the currently loaded workflow. Open the Batch subtab and turn off the active batch type checkbox.",
|
||||
helpSupportTitle: "Support",
|
||||
helpSupportDesc: "If you find this plugin useful, please consider supporting its development!",
|
||||
helpGithubDesc: "Report issues, request features, and contribute",
|
||||
@@ -1002,14 +1152,14 @@ const LANGUAGES = {
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
|
||||
metaFmtSdwebui: "SD WebUI / SD Forge — PNG (parameters)",
|
||||
metaFmtFooocus: "Fooocus — PNG (fooocus_scheme)",
|
||||
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image subgraph workflows supported",
|
||||
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image subgraph workflows supported",
|
||||
helpMetadataTitle: "Metadata Tab",
|
||||
helpMetadataDesc: "Drop a ComfyUI-generated PNG/WebP or workflow JSON to instantly extract and view the embedded model and prompt information.",
|
||||
helpMetadata1: "Left column: drop zone (image preview for PNG/WebP) and file info; click or drop to load",
|
||||
helpMetadata2: "Center column: Checkpoint, VAE, Diffusion Model, and Text Encoder lists extracted from the workflow",
|
||||
helpMetadata3: "Right column: LoRA list (with strength values) and Prompt list (click to view full text below)",
|
||||
helpMetadata4: "Prompt actions — Copy: copy to clipboard; GenUI:P/N: set GenerateUI positive/negative prompt; Prompt:P/N: set Prompt tab preset positive/negative",
|
||||
helpMetadata5: "Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image subgraph workflows), SD WebUI, SD Forge, Fooocus.",
|
||||
helpMetadata5: "Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image subgraph workflows), SD WebUI, SD Forge, Fooocus.",
|
||||
},
|
||||
|
||||
ja: {
|
||||
@@ -1098,6 +1248,7 @@ const LANGUAGES = {
|
||||
sendToCanvas: "キャンバスへ送る",
|
||||
copyAndSendCanvas: "コピー&キャンバスへ送る",
|
||||
workflowSentToCanvas: "ワークフロー準備完了 — タイトルをキャンバスへドラッグ",
|
||||
workflowSentToCanvasDirect: "ワークフローをキャンバスへ送信しました",
|
||||
comfyUILoadTimeout: "ComfyUIの読み込みがタイムアウトしました",
|
||||
popupBlocked: "ポップアップがブロックされました。このサイトのポップアップを許可してください。",
|
||||
setAsDefault: "デフォルトに設定",
|
||||
@@ -1222,6 +1373,49 @@ const LANGUAGES = {
|
||||
batchStopped: (completed, failed) => `バッチ停止 — 完了${completed}件、失敗${failed}件`,
|
||||
batchComplete: (completed, total, failed) => failed > 0 ? `バッチ完了: ${completed}/${total}件 (${failed}件失敗)` : `バッチ完了: ${completed}/${total}件`,
|
||||
batchLoraMissing: (count, total, sample) => `選択したLoRAのうち${count}/${total}件がComfyUIに認識されていません(例: ${sample})。ComfyUIでRefreshするかサーバーを再起動してから再実行してください。`,
|
||||
|
||||
// -- Lab タブ --
|
||||
labImageUploaded: (name) => `画像をアップロードしました: ${name}`,
|
||||
labUploadFailed: "アップロードに失敗しました",
|
||||
labBatchCountTooSmall: "先にBatch数を増やしてからキーフレームを追加してください",
|
||||
labUseWorkflowDefault: "(ワークフローの設定のまま)",
|
||||
labAppliedAt: (n) => `適用:${n}`,
|
||||
labRevertToBase: "↩ 1回目に戻す",
|
||||
labApplyFromIteration: "何回目から適用するか",
|
||||
labRevertCheckboxLabel: "1回目の設定に戻す",
|
||||
labSeedEmptyRandom: "Seed(空欄でランダム)",
|
||||
labPause: "一時停止",
|
||||
labResume: "再開",
|
||||
labNoWorkflowLoaded: "先にGenerateUIでワークフローを読み込んでください",
|
||||
labNoSourceImage: "先に元になる画像をドロップしてください",
|
||||
labNoResultsYet: "まだ結果がありません",
|
||||
labResultPreview: "プレビュー",
|
||||
labEnterPlanName: "プラン名:",
|
||||
labPlanSaved: "プランを保存しました",
|
||||
labPlanSaveFailed: "プランの保存に失敗しました",
|
||||
labPlanLoaded: "プランを読み込みました",
|
||||
labPlanLoadFailed: "プランの読み込みに失敗しました",
|
||||
labPlanInvalidFile: "プランの.jsonファイル、またはそのインデックス画像(.png)をドロップしてください",
|
||||
labConfirmClear: "Labの設定(画像・列・メモ・結果)をすべてクリアしますか?",
|
||||
labGetFromGenUI: "GenerateUIから取得",
|
||||
labGetFromImage: "画像から取得",
|
||||
labStyle: "スタイル",
|
||||
labApply: "適用",
|
||||
labNoStyles: "スタイルがありません",
|
||||
labEnableStyleFirst: "先にGenerateUIタブ上部のStyleチェックボックスをオンにし、スタイルを選択してください",
|
||||
labPlanJsonRefreshed: "現在のSettingタブの内容で更新しました",
|
||||
labPlanJsonApplied: "Plan JSONをSettingに適用しました",
|
||||
labPlanJsonInvalid: "JSONが不正です",
|
||||
labStyleApplied: "スタイルを適用しました",
|
||||
labPromptFetched: "プロンプトを取得しました",
|
||||
labNoPromptInImage: "このファイルにはプロンプトが見つかりませんでした",
|
||||
labMetadataExtractFailed: "メタデータの読み取りに失敗しました",
|
||||
labConfirmLoadWorkflow: (filename) => `「${filename}」をGenerateUIに読み込みますか?現在読み込まれているワークフローは置き換えられます。`,
|
||||
labWorkflowLoadFailed: "ワークフローの読み込みに失敗しました",
|
||||
labT2IImageIgnored: "T2Iモードが有効なため、画像のアップロードは不要としてスキップしました",
|
||||
labIndexImageSaved: (filename) => `インデックス画像をOutputへ保存しました: ${filename}`,
|
||||
labIndexImageSaveFailed: "インデックス画像のOutputへの保存に失敗しました",
|
||||
|
||||
connectedToComfyUI: "ComfyUIに接続しました",
|
||||
failedToConnect: "接続に失敗しました",
|
||||
modelListsRefreshed: "モデルリストを更新しました",
|
||||
@@ -1617,12 +1811,23 @@ const LANGUAGES = {
|
||||
aiChatGenerateNotReady: "GenerateUIの準備ができていません",
|
||||
aiChatNoImageProduced: "生成は完了しましたが画像が出力されませんでした",
|
||||
aiToastImageGenFailed: "画像生成に失敗しました: ",
|
||||
aiChatSaveSvgToGallery: "Galleryに保存",
|
||||
aiChatSvgSaved: "SVGをGalleryに保存しました",
|
||||
aiChatSvgSaveFailed: "SVGの保存に失敗しました: ",
|
||||
aiChatImgGenToggle: "画像生成を許可する",
|
||||
aiSettingsChatGenTitle: "Chat画像生成",
|
||||
aiSettingsChatGenDedicated: "専用ワークフローを使用",
|
||||
aiSettingsChatI2IGenTitle: "Chat I2I生成",
|
||||
aiChatAttachmentLabel: "画像を添付中",
|
||||
aiChatNoLoadImageNode: "対象ワークフローにI2I用のLoadImageノードが見つかりません",
|
||||
aiSkillNone: "スキルなし",
|
||||
aiSkillManageBtnTitle: "スキルを管理",
|
||||
aiSkillPanelTitle: "スキル",
|
||||
aiSkillNewBtn: "+ 新規",
|
||||
aiSkillNoFiles: "スキルがまだありません。「+ 新規」から作成してください。",
|
||||
aiSkillFilenamePlaceholder: "filename.md",
|
||||
aiSkillContentPlaceholder: "---\nname: マイスキル\ndescription: このスキルの説明\n---\n\nLLMへのsystem prompt指示...",
|
||||
aiChatSaveAsSkill: "スキルとして保存",
|
||||
|
||||
// -- Help & Support --
|
||||
tabHelp: "ヘルプ",
|
||||
@@ -1639,7 +1844,7 @@ const LANGUAGES = {
|
||||
helpWf6: "詳細モーダル:名前変更、お気に入り、サムネイル変更、バッジ付与、タグ、メモ、AI要約、キャンバスへ送る、生成UIに読み込み、デフォルト設定、削除",
|
||||
helpWf7: "バッジ管理:バッジの追加・名前変更・削除、カスタム色設定(Modelsタブと共有)— ⚙ Badgeボタン",
|
||||
helpGen1: "ComfyUIの接続・切断、接続状態の確認",
|
||||
helpGen2: "Workflowタブからワークフローを読み込み(App形式 .app.json は非対応)。Reset Workflowボタンは現在のワークフローファイルをディスクから再読込してエディタをリロード。Saveボタン(タブバー右端)はエディタでの編集を含む現在のワークフローを新しい.jsonファイルとしてWorkflowタブに保存(現在のワークフロー名を初期値としたファイル名ダイアログを表示)",
|
||||
helpGen2: "Workflowタブからワークフローを読み込み(サブグラフを含むワークフローにも対応 — Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 等のテンプレート含む;ComfyUIの簡略化App形式 [.app.json / Linear Mode] のみ非対応)。Reset Workflowボタンは現在のワークフローファイルをディスクから再読込してエディタをリロード。Saveボタン(タブバー右端)はエディタでの編集を含む現在のワークフローを新しい.jsonファイルとしてWorkflowタブに保存(現在のワークフロー名を初期値としたファイル名ダイアログを表示)",
|
||||
helpGen3: "5タブ構成 — Input(プロンプト+画像)、Model、Settings、Feeder、Batch — Input/Model/SettingsタブにはRaw JSON列を常時表示",
|
||||
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入力付き)のセレクタ(フィルタ検索付き)",
|
||||
@@ -1647,7 +1852,7 @@ const LANGUAGES = {
|
||||
helpGen7: "Raw JSONを各タブの右列に常時表示 — 直接編集してApplyでワークフロー即時反映;読み込んだワークフローにBypass/Muteノードがある場合、検索バー上部に一覧が表示される(Bypassは除外され配線が再接続される、Muteは除外されるが再接続はされない点はComfyUI本体と同じ)",
|
||||
helpGen8: "シード管理:ランダム / 固定 / 増加 / 減少",
|
||||
helpGen9: "画像生成、進捗表示、生成停止、結果表示",
|
||||
helpGen10: "Eagle連携:生成画像をEagleへ自動保存",
|
||||
helpGen10: "Eagle連携:生成画像をEagleへ自動保存(SVG出力にも対応 — 標準SaveSVGNodeおよびcomfyui-tosvgのSave SVG Stringノードの両方)",
|
||||
helpGen11: "Batchタブ — バッチキューのカラムヘッダーのチェックボックスで1種類だけ有効化;Generateで全キュー項目を順次処理;一時停止/再開・停止ボタンで制御可能",
|
||||
helpGen12: "Batchタブ — 左ペインは3タブ構成:Checkpoints(フォルダツリー、Filter/All/None、(root)ヘッダーで全選択)、Sampler(KSamplerのsampler_nameリスト、(root)チェックボックスで全選択)、Scheduler(KSamplerのschedulerリスト、(root)チェックボックスで全選択);各タブにAll/Noneボタンも完備",
|
||||
helpGen13: "Batchタブ — 中央ペイン:グループ選択(Checkpoint | Lora | Prompt | Workflowの内部タブ);Checkpoint/LoraグループはModelsタブ、PromptグループはPromptタブ、WorkflowグループはWorkflowタブの定義を反映 — グループヘッダーにチェックで全メンバーを選択、▶で展開して個別選択も可能",
|
||||
@@ -1678,6 +1883,36 @@ const LANGUAGES = {
|
||||
helpFeederGal4: "右ペイン — プレビュー:現在選択中の入力画像を表示;サムネイルをクリックすると更新",
|
||||
helpFeederGal5: "__Feeder__ グループ:自動で作成される予約グループ;名前変更・削除不可;ギャラリータブの各サムネイル上の F ボタンでこのグループに画像を追加",
|
||||
helpFeederGal6: "WFS_GalleryFeeder キャンバスコントロール:このノードには ComfyUI キャンバス上に直接 Run / Stop / After Gen ウィジェットが追加されており、WFM を開かずにギャラリーフィーダーループを実行できます",
|
||||
|
||||
helpLabTitle: "生成UIタブ — Labサブタブ",
|
||||
helpLabDesc: "実験的なI2Iバッチ生成機能です。GenerateUIに現在読み込まれているワークフローをN回実行し、Checkpoint・VAE・Prompt・KSamplerをそれぞれ独立して任意の回数目から変更できます。Setting/Resultsの2つのサブタブがあります。",
|
||||
helpLabCardColumnsTitle: "Settingサブタブ — 画像と列",
|
||||
helpLab1: "左ペイン — Image:I2Iの元になる画像をドロップまたはクリックして選択します。ComfyUIのinputフォルダにアップロードされ、「Use generated image for next」がオフの間は毎回この画像が使われます",
|
||||
helpLab2: "左ペイン — Use generated image for next:オンにすると2回目以降、直前の回で生成された1枚目の画像を次のI2I入力として使用します(1回目は常に元画像を使用)",
|
||||
helpLab3: "中央ペイン — Checkpoint・VAE・Prompt・KSamplerの4列は独立しています。共通の行グリッドではなく、列ごとに個別のキーフレーム(設定変更点)のリストを持ちます",
|
||||
helpLab4: "各列の+/-:+は次に空いている回数目(Batch数まで)にキーフレームを追加し、すぐに編集モーダルを開きます。-は末尾のキーフレームを削除します(1回目は削除できません)",
|
||||
helpLab5: "セルをクリックすると編集できます。1回目は基準となる設定値です(空欄のままにするとワークフロー本来の設定を使用)。2回目以降はさらに「何回目から適用するか」の指定と「1回目の設定に戻す」チェックボックスがあります",
|
||||
helpLab6: "N回目時点での実効値は、N以下で最も大きい回数目のキーフレームが次のキーフレームまで引き継がれます。「1回目に戻す」を使うと、新しい値を指定する代わりに1回目の値(空欄ならワークフロー本来の設定)がそのまま使われます",
|
||||
helpLab7: "LabはInput/Model/Settingsに表示されているワークフローを直接編集しません。各回の実行はワークフローの複製に対して行われるため、Labでのキーフレーム設定が他のサブタブに影響することはありません",
|
||||
helpLab8: "Promptセル編集モーダルの追加機能:「Get from GenerateUI」で現在GenerateUIに読み込まれているワークフローからPositive/Negativeを取得、「Get from Image」でLab左ペインのIMAGEに現在読み込まれている画像からプロンプトを抽出(Metadataタブと同じ検出ロジック。別途ファイル選択は不要)、「Style」でGenerateUIツールバー上部のStyleチェックボックス/ドロップダウンで現在選択されているスタイルを、既存のPositive/Negativeテキストを消さずにカンマ区切りで追記(先に上部でチェックボックスをオンにしスタイルを選択しておく必要あり。Lab独自のスタイル選択はありません)",
|
||||
helpLab9: "左ペイン — 「T2I workflow (no source image)」:読み込んでいるワークフローがT2I(LoadImageノードなし)の場合にオンにします。「先に元になる画像をドロップしてください」という要求をスキップし、ドロップ済みの画像があっても無視します。Checkpoint/VAE/Prompt/KSamplerのキーフレームは通常どおり適用されます。オンの間はImageドロップ領域と「Use generated image for next」がグレーアウトします(既にドロップ済みの画像は削除されず、I2Iに戻したときのために保持されます)。この設定はプランに t2i_mode として保存されます",
|
||||
helpLabCardPlanTitle: "Settingサブタブ — 実行とプランファイル",
|
||||
helpLabPlan1: "右ペイン — Batch:実行する回数。Note:プランと一緒に保存される自由記述のメモ",
|
||||
helpLabPlan2: "Run/Pause/Cancel:Runでバッチループを開始、Pause/Resumeで回と回の間を一時停止・再開、Cancelで現在の生成を中断してループを止めます",
|
||||
helpLabPlan3: "Plan SaveとSave Asの違い:Plan Saveは読込済みのプランがあればダイアログなしで上書き保存します(未読込の場合のみ名前を確認)。Save Asはプラン読込中でも常に新しい名前を確認します",
|
||||
helpLabPlan4: "Plan Load:プランの.jsonファイル、またはそのインデックス画像(.png)をPLAN欄にドロップします(クリックでファイル選択も可)。.jsonをドロップした場合はそのまま読み込みます。.pngをドロップした場合も、v0.3.90以降のインデックス画像であればwfm_lab_planチャンクに埋め込まれたプランデータを直接読み込みます。それより古いインデックス画像の場合のみ、同名の.jsonをサーバーから取得するフォールバック動作になります",
|
||||
helpLabPlan5: "Plan ClearはSettingタブの内容を空のプランにリセットします(確認あり)",
|
||||
helpLabPlan6: "プランファイルは user/default/Workflow-Studio/lab_plan/ に ws_labplan_<名前>.json として保存され、結果がある場合は最大9枚(1行3枚)のサムネイルを並べたインデックス画像 ws_labplan_<名前>.png も自動生成されます。ws_labplan_ の接頭辞はPlan Save/Save Asが自動的に付与するため、プロンプトには接頭辞なしの名前だけを入力すればよく、フォルダ内でプランファイルを見分けやすくなります",
|
||||
helpLabPlan7: "設定タブで構成したEagle自動保存は、GenerateUI本来のGenerateボタンと同様にLabで生成したすべての画像に適用されます",
|
||||
helpLabPlan8: "Plan JSONサブタブ:現在のSettingタブのプラン内容をそのままJSONで表示します。「Refresh」でSettingタブの現在の内容から再読込(未適用の編集は破棄)、「Apply to Setting」で編集内容を解析しSettingタブへ書き戻します(列・メモ・Batch数・画像連鎖・元画像・結果)。保存せずにそのままプランファイルとして読み込んだのと同じ効果があります",
|
||||
helpLabPlan9: "保存されたプランには、保存時にGenerateUIで読み込まれていたワークフローのファイル名が記録されます(Plan JSON内のworkflow_filename)。プランを読み込むとPLANボックスに「Open Workflow」ボタンが表示され、クリックするとその記録された正確なワークフローファイルをGenerateUIへ読み込み直せます(現在読み込まれているワークフローを置き換えるため、確認が入ります)",
|
||||
helpLabPlan10: "右ペイン — 「Save index image to Output on Run」:チェックすると、Run実行が終わるたびにPlanファイル用と同じコンタクトシート形式のインデックス画像を作成し、ComfyUI本来のOutputフォルダへ Lab_index_<counter>_.png として直接保存します(通常の生成画像と同様に自動採番され、前回の実行結果を上書きしません)。保存後は他のLab生成画像と同様にEagle自動保存(設定タブで有効な場合)にも渡されます——これ専用のチェックボックスはなく、同じ設定タブのグローバルなトグルに従います。Plan Saveの動作とは独立しており、Plan Saveは常にこのチェックボックスに関係なく lab_plan/ 内のプランファイルの隣に自分専用のインデックス画像を書き込みます。この設定はプランに save_index_on_run として保存されます",
|
||||
helpLabPlan11: "全てのインデックス画像PNG(Plan Save/Save Asで作成されたものも、Save index image to Output on Runで作成されたものも)には、プラン全体(列・メモ・Batch数・画像連鎖/T2I設定・ワークフローファイル名・結果一覧)がPNGのwfm_lab_planチャンクとして直接埋め込まれています。そのためOutputフォルダ側のコピーは単体で完結しており、隣に対応する.jsonが一切存在しなくてもPlan Loadの読込元として使えます",
|
||||
helpLabCardResultsTitle: "Resultsサブタブ",
|
||||
helpLabResults1: "左ペインには実行に使用した元画像が表示されます",
|
||||
helpLabResults2: "中央ペインには生成順に最大9枚の画像が表示されます。サムネイルをクリックすると拡大表示されます",
|
||||
helpLabResults3: "保存済みプランを読み込むと、プランファイルに記録された画像一覧からResultsの表示が復元されます",
|
||||
|
||||
helpSearchPlaceholder: "ヘルプを検索...",
|
||||
helpPrompt1: "AIアシスタント(Ollama / LM Studio / Lemonade):モデル選択、チャットでプロンプト相談、画像添付(ビジョンモデル対応);バックエンドとURLは設定タブで設定",
|
||||
helpPrompt2: "AIの応答をGenerateUIのプロンプトに適用",
|
||||
@@ -1774,8 +2009,72 @@ const LANGUAGES = {
|
||||
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 タブと共有される",
|
||||
helpAi7: "Chat ペイン — SVG:LLMはComfyUIワークフローを一切使わず、SVGグラフィックをテキストとして直接生成することもできる;「SVGで〜を描いて」のように頼み、応答にSVGコードが含まれていれば自動でインラインプレビュー表示され、「Galleryに保存」ボタンでGalleryタブの出力フォルダに.svgファイルとして保存できる;tool calling(function calling)を使用しないため、tool calling非対応のモデルでも動作する",
|
||||
helpAi8: "Chat ペイン — スキル:メッセージ一覧上部のドロップダウンでスキルを選ぶと、その指示内容が system prompt として会話の先頭に付加される(表示上のチャット履歴には含まれない);鉛筆ボタンでスキル管理パネルを開き、.mdスキルファイルを作成・編集・削除できる(保存先: user/default/Workflow-Studio/ai_skills/);ファイル先頭の---で囲まれた部分にname/descriptionを書くとドロップダウンの表示に反映される;初期状態で2つのスキルを同梱: 「SVG Icon Generator」(描く前に対象を部位分解することで認識しやすいSVGを生成)と「Skill Creator」(対話形式で質問しながら新しいスキルファイルを下書きする);応答に```skillコードブロックが含まれる場合、メッセージ下部に「スキルとして保存」ボタンが表示され、内容が入った状態で管理パネルが開く",
|
||||
helpAi9: "スキル作成のヒント:ドロップダウンやツールチップで分かりやすいよう、frontmatterのname/descriptionは明確に書く;期待する出力形式を具体的に指示し、SVGや別のスキルファイルのように後で抽出したい成果物がある場合は専用のコードフェンスで囲むよう指示する;曖昧な指示より具体的で命令形の指示の方がローカルの小型モデルには効きやすく、短い実例を1つ添えるとさらに効果的;禁止事項(不適切な内容、セキュリティ、文字数制限など)は暗黙に期待せず明示的に書く;迷ったらドロップダウンで「Skill Creator」スキルを選び、作りたいスキルの概要を伝えれば対話形式で下書きしてくれる",
|
||||
helpAiCardLayout: "レイアウト",
|
||||
helpAiCardTranslate: "Translationペイン",
|
||||
helpAiCardChat: "Chatペイン",
|
||||
helpAiCardSvg: "Chatペイン — SVG生成",
|
||||
helpAiCardSkills: "Chatペイン — スキル",
|
||||
helpAiCardSkillTips: "スキル作成のヒント",
|
||||
helpAiCardTools: "TOOLSペイン",
|
||||
helpAiCardSettings: "Settingsペイン",
|
||||
helpJumpToTab: "タブを開く →",
|
||||
helpAboutCardGettingStarted: "はじめに",
|
||||
helpWfTitle: "ワークフロータブ",
|
||||
helpWfCardBrowse: "閲覧・フィルタリング",
|
||||
helpWfCardGroups: "グループ・バッジ",
|
||||
helpWfCardPanel: "サイドパネル・詳細モーダル",
|
||||
helpGenTitle: "GenerateUIタブ",
|
||||
helpGenCardConnect: "接続・ワークフロー読込",
|
||||
helpGenCardTabs: "タブレイアウト",
|
||||
helpGenCardSeed: "シード・生成",
|
||||
helpGenCardBatchSetup: "Batchタブ — セットアップ",
|
||||
helpGenCardBatchTypes: "Batchタブ — バッチ種別",
|
||||
helpPromptTitle: "プロンプトタブ",
|
||||
helpPromptCardAi: "AIアシスタント・翻訳",
|
||||
helpPromptCardPresets: "プリセット",
|
||||
helpPromptCardWildcards: "ワイルドカード",
|
||||
helpMetadataCardOverview: "概要",
|
||||
helpMetadataCardExtracted: "抽出情報",
|
||||
helpMetadataCardFormats: "対応フォーマット",
|
||||
helpSettingsTitle: "設定タブ",
|
||||
helpSettingsCardGeneral: "一般",
|
||||
helpSettingsCardConnections: "接続",
|
||||
helpSettingsCardWorkflow: "ワークフロー既定値",
|
||||
helpSettingsCardEditor: "エディタカスタマイズ",
|
||||
helpSettingsCardMisc: "データ・その他",
|
||||
helpNodesTitle: "ノードタブ",
|
||||
helpNodesCardBrowse: "閲覧・整理",
|
||||
helpNodesCardGroups: "グループ・セット",
|
||||
helpNodesCardPanel: "サイドパネル・複数選択",
|
||||
helpGalleryCardBrowse: "閲覧",
|
||||
helpGalleryCardFolder: "フォルダ・選択",
|
||||
helpGalleryCardDetail: "詳細パネル",
|
||||
helpGalleryCardActions: "アクションツールバー",
|
||||
helpGalleryCardFormats: "対応フォーマット・自動保存",
|
||||
helpModelsTitle: "モデルタブ",
|
||||
helpModelsCardBrowse: "閲覧",
|
||||
helpModelsCardCivitai: "サイドパネル・CivitAI",
|
||||
helpModelsCardApply: "適用・詳細",
|
||||
helpModelsCardGroups: "グループ・バッジ",
|
||||
helpModelsCardBulk: "有効/無効・一括操作",
|
||||
helpSidepanelCardTabs: "タブ概要",
|
||||
helpSidepanelCardDnd: "ドラッグ&ドロップ操作",
|
||||
helpSidepanelCardInfo: "Iタブ(情報)",
|
||||
helpSidepanelCardAi: "AI TOOLタブ",
|
||||
helpSidepanelCardNav: "カスタマイズ・ナビゲーション",
|
||||
helpTaggerCardOverview: "概要・セットアップ",
|
||||
helpTaggerCardSingle: "Singleタブ",
|
||||
helpTaggerCardSending: "結果の送信",
|
||||
helpTaggerCardBatch: "Batch・DBタブ",
|
||||
helpImageEditCardOverview: "概要",
|
||||
helpShortcutsCardGeneral: "一般",
|
||||
helpTroubleCardConnection: "接続の問題",
|
||||
helpTroubleCardWorkflow: "ワークフロー・生成の問題",
|
||||
helpTroubleCardTagger: "Tagger・モデルの問題",
|
||||
helpImageEditTitle: "Image Edit タブ",
|
||||
helpImageEdit1: "レイヤーベースの画像エディタ — 複数のレイヤー(画像・テキスト・描画・マスク)を重ね合わせて画像を合成・エクスポート",
|
||||
helpImageEdit2: "画像の読み込み:キャンバスへのドラッグ&ドロップ、Uploadボタン、またはGalleryタブのImage Editボタンから送信;最初の画像はLayer 1として自動ロック;以降の画像はキャンバスサイズに合わせてスケールされ新規レイヤーとして追加",
|
||||
@@ -1868,6 +2167,7 @@ const LANGUAGES = {
|
||||
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」を有効にして、読み込み時に常に動作確認済みのモデルを強制適用してください。",
|
||||
helpTrouble9: "Generateを押すと、読み込んだワークフローとは別のワークフローで実行される:Batchサブタブでバッチタイプ(Checkpoint/LoRA/Prompt/Workflow/Sampler/Scheduler)のチェックボックスがONのまま残っています。いずれかのバッチタイプが有効な間は、Generateボタンは現在読み込まれているワークフローではなくバッチ選択された内容を実行します。Batchサブタブを開き、有効になっているバッチタイプのチェックボックスをOFFにしてください。",
|
||||
helpSupportTitle: "サポート",
|
||||
helpSupportDesc: "このプラグインが役に立ったら、開発を支援していただけると嬉しいです!",
|
||||
helpGithubDesc: "バグ報告、機能リクエスト、コントリビュート",
|
||||
@@ -2007,14 +2307,14 @@ const LANGUAGES = {
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
|
||||
metaFmtSdwebui: "SD WebUI / SD Forge — PNG(parameters チャンク)",
|
||||
metaFmtFooocus: "Fooocus — PNG(fooocus_scheme チャンク)",
|
||||
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image サブグラフ形式に対応済み",
|
||||
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image サブグラフ形式に対応済み",
|
||||
helpMetadataTitle: "Metadata タブ",
|
||||
helpMetadataDesc: "ComfyUI で生成した PNG / WebP または ワークフロー JSON をドロップすると、埋め込まれたモデル・プロンプト情報を即座に抽出・表示します。",
|
||||
helpMetadata1: "左列: ドロップゾーン(PNG/WebP は画像プレビュー表示)+ファイル情報・対応フォーマット説明",
|
||||
helpMetadata2: "中列: ワークフローから抽出した Checkpoint・VAE・Diffusion Model・Text Encoder の一覧",
|
||||
helpMetadata3: "右列: LoRA 一覧(強度値付き)+プロンプト一覧(クリックで下部に全文表示)",
|
||||
helpMetadata4: "プロンプトアクション — Copy: クリップボードにコピー; GenUI:P/N: GenerateUI のポジティブ/ネガティブプロンプトにセット; Prompt:P/N: Prompt タブ Presets のポジティブ/ネガティブにセット",
|
||||
helpMetadata5: "対応フォーマット: ComfyUI PNG/WebP/JSON(標準形式 + Flux.2 / Qwen-Image / Z-Image サブグラフ形式)・SD WebUI・SD Forge・Fooocus。",
|
||||
helpMetadata5: "対応フォーマット: ComfyUI PNG/WebP/JSON(標準形式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image サブグラフ形式)・SD WebUI・SD Forge・Fooocus。",
|
||||
},
|
||||
|
||||
zh: {
|
||||
@@ -2103,6 +2403,7 @@ const LANGUAGES = {
|
||||
sendToCanvas: "发送到画布",
|
||||
copyAndSendCanvas: "复制并发送到画布",
|
||||
workflowSentToCanvas: "工作流已就绪 — 将标题拖拽到画布",
|
||||
workflowSentToCanvasDirect: "工作流已发送到画布",
|
||||
comfyUILoadTimeout: "ComfyUI加载超时",
|
||||
popupBlocked: "弹窗被阻止。请允许此站点的弹窗。",
|
||||
setAsDefault: "设为默认",
|
||||
@@ -2227,6 +2528,49 @@ const LANGUAGES = {
|
||||
batchStopped: (completed, failed) => `批处理已停止 — 完成${completed}个,失败${failed}个`,
|
||||
batchComplete: (completed, total, failed) => failed > 0 ? `批处理完成: ${completed}/${total}个 (${failed}个失败)` : `批处理完成: ${completed}/${total}个`,
|
||||
batchLoraMissing: (count, total, sample) => `所选LoRA中有${count}/${total}个尚未被ComfyUI识别(例如:${sample})。请在ComfyUI中点击Refresh或重启服务器后再试。`,
|
||||
|
||||
// -- Lab 标签页 --
|
||||
labImageUploaded: (name) => `图片已上传: ${name}`,
|
||||
labUploadFailed: "上传失败",
|
||||
labBatchCountTooSmall: "请先增加Batch数量再添加关键帧",
|
||||
labUseWorkflowDefault: "(保持工作流原设置)",
|
||||
labAppliedAt: (n) => `应用:${n}`,
|
||||
labRevertToBase: "↩ 恢复第1次的设置",
|
||||
labApplyFromIteration: "从第几次开始应用",
|
||||
labRevertCheckboxLabel: "恢复第1次的设置",
|
||||
labSeedEmptyRandom: "Seed(留空为随机)",
|
||||
labPause: "暂停",
|
||||
labResume: "继续",
|
||||
labNoWorkflowLoaded: "请先在GenerateUI中加载工作流",
|
||||
labNoSourceImage: "请先拖入源图片",
|
||||
labNoResultsYet: "暂无结果",
|
||||
labResultPreview: "预览",
|
||||
labEnterPlanName: "方案名称:",
|
||||
labPlanSaved: "方案已保存",
|
||||
labPlanSaveFailed: "方案保存失败",
|
||||
labPlanLoaded: "方案已加载",
|
||||
labPlanLoadFailed: "方案加载失败",
|
||||
labPlanInvalidFile: "请拖入方案的.json文件或其索引图片(.png)",
|
||||
labConfirmClear: "确定要清除所有Lab设置(图片、列、备注、结果)吗?",
|
||||
labGetFromGenUI: "从GenerateUI获取",
|
||||
labGetFromImage: "从图片获取",
|
||||
labStyle: "风格",
|
||||
labApply: "应用",
|
||||
labNoStyles: "暂无风格",
|
||||
labEnableStyleFirst: "请先在GenerateUI标签页顶部勾选Style复选框并选择一个风格",
|
||||
labPlanJsonRefreshed: "已根据当前Setting标签页内容刷新",
|
||||
labPlanJsonApplied: "已将Plan JSON应用到Setting",
|
||||
labPlanJsonInvalid: "JSON格式无效",
|
||||
labStyleApplied: "已应用风格",
|
||||
labPromptFetched: "已获取提示词",
|
||||
labNoPromptInImage: "该文件中未找到提示词",
|
||||
labMetadataExtractFailed: "读取元数据失败",
|
||||
labConfirmLoadWorkflow: (filename) => `将"${filename}"加载到GenerateUI?这将替换当前已加载的工作流。`,
|
||||
labWorkflowLoadFailed: "工作流加载失败",
|
||||
labT2IImageIgnored: "T2I模式已开启,无需上传图片,已跳过",
|
||||
labIndexImageSaved: (filename) => `索引图片已保存到Output:${filename}`,
|
||||
labIndexImageSaveFailed: "索引图片保存到Output失败",
|
||||
|
||||
connectedToComfyUI: "已连接到ComfyUI",
|
||||
failedToConnect: "连接失败",
|
||||
modelListsRefreshed: "模型列表已刷新",
|
||||
@@ -2622,12 +2966,23 @@ const LANGUAGES = {
|
||||
aiChatGenerateNotReady: "GenerateUI 尚未就绪",
|
||||
aiChatNoImageProduced: "生成完成但未产生图像",
|
||||
aiToastImageGenFailed: "图像生成失败:",
|
||||
aiChatSaveSvgToGallery: "保存到图库",
|
||||
aiChatSvgSaved: "SVG已保存到图库",
|
||||
aiChatSvgSaveFailed: "SVG保存失败:",
|
||||
aiChatImgGenToggle: "允许生成图像",
|
||||
aiSettingsChatGenTitle: "Chat 图像生成",
|
||||
aiSettingsChatGenDedicated: "使用专用工作流",
|
||||
aiSettingsChatI2IGenTitle: "Chat I2I 生成",
|
||||
aiChatAttachmentLabel: "已附加图像",
|
||||
aiChatNoLoadImageNode: "目标工作流中未找到用于图生图的 LoadImage 节点",
|
||||
aiSkillNone: "无技能",
|
||||
aiSkillManageBtnTitle: "管理技能",
|
||||
aiSkillPanelTitle: "技能",
|
||||
aiSkillNewBtn: "+ 新建",
|
||||
aiSkillNoFiles: "还没有技能。点击「+ 新建」创建一个。",
|
||||
aiSkillFilenamePlaceholder: "filename.md",
|
||||
aiSkillContentPlaceholder: "---\nname: 我的技能\ndescription: 该技能的说明\n---\n\n给 LLM 的 system prompt 指令...",
|
||||
aiChatSaveAsSkill: "保存为技能",
|
||||
|
||||
// -- Help & Support --
|
||||
tabHelp: "帮助",
|
||||
@@ -2644,7 +2999,7 @@ const LANGUAGES = {
|
||||
helpWf6: "详情弹窗:重命名、收藏、缩略图更换、徽章分配、标签、备注、AI摘要、发送到画布、加载到生成UI、设为默认、删除",
|
||||
helpWf7: "徽章管理:添加、重命名、删除徽章并自定义颜色(与Models标签共享)— ⚙ Badge按钮",
|
||||
helpGen1: "连接/断开ComfyUI,检查连接状态",
|
||||
helpGen2: "从工作流标签页加载工作流(不支持App格式.app.json)。Reset Workflow按钮从磁盘重新读取当前工作流文件并重新加载编辑器。Save按钮(标签栏右端)将当前工作流(包括在编辑器中所做的编辑)保存为新的.json文件到工作流标签页;会显示以当前工作流名称为默认值的文件名对话框",
|
||||
helpGen2: "从工作流标签页加载工作流(支持含子图的工作流,例如 Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 等模板;仅不支持ComfyUI的简化App格式 [.app.json / Linear Mode])。Reset Workflow按钮从磁盘重新读取当前工作流文件并重新加载编辑器。Save按钮(标签栏右端)将当前工作流(包括在编辑器中所做的编辑)保存为新的.json文件到工作流标签页;会显示以当前工作流名称为默认值的文件名对话框",
|
||||
helpGen3: "5标签布局 — Input(提示词+图像)、Model、Settings、Feeder、Batch — Input/Model/Settings各含实时预览和直接编辑的Raw JSON列",
|
||||
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输入)选择器(含筛选)",
|
||||
@@ -2652,7 +3007,7 @@ const LANGUAGES = {
|
||||
helpGen7: "Raw JSON在每个标签右列常驻显示 — 直接编辑并Apply即时重载工作流;如果加载的工作流包含Bypass或Mute节点,搜索栏上方会显示列表(Bypass节点会被排除但其连线会重新连接;Mute节点会被排除且不重新连接,与ComfyUI本体行为一致)",
|
||||
helpGen8: "种子管理:随机/固定/递增/递减",
|
||||
helpGen9: "生成图片、进度显示、停止生成、查看结果",
|
||||
helpGen10: "Eagle集成:自动保存生成图片到Eagle",
|
||||
helpGen10: "Eagle集成:自动保存生成图片到Eagle(也支持SVG输出 — 标准SaveSVGNode节点和comfyui-tosvg的Save SVG String节点均可)",
|
||||
helpGen11: "Batch标签 — 勾选批次队列列标题复选框激活对应批次类型(每次仅一种);Generate按顺序处理所有队列项目;支持暂停/继续/停止控制",
|
||||
helpGen12: "Batch标签 — 左窗格含3个标签:Checkpoints(文件树,Filter/All/None,(root)标题勾选全选)、Sampler(KSampler采样器列表,(root)复选框全选)、Scheduler(KSampler调度器列表,(root)复选框全选);每个标签均有All/None按钮",
|
||||
helpGen13: "Batch标签 — 中央窗格:基于组的选择(含Checkpoint | Lora | Prompt | Workflow内部标签);Checkpoint/Lora组来自Models标签,Prompt组来自Prompt标签,Workflow组来自Workflow标签 — 勾选组标题选中所有成员,▶展开后也可单独选择",
|
||||
@@ -2683,6 +3038,36 @@ const LANGUAGES = {
|
||||
helpFeederGal4: "右侧面板 — 预览:显示当前选中的输入图片;点击缩略图时更新",
|
||||
helpFeederGal5: "__Feeder__ 分组:自动预先创建的保留分组;不可重命名或删除;在图库标签页中点击每个缩略图上的 F 按钮可将图片添加到此分组",
|
||||
helpFeederGal6: "WFS_GalleryFeeder 画布控件:该节点在 ComfyUI 画布上直接添加了 Run / Stop / After Gen 组件——与 WFM 界面中的控件功能相同,无需打开 WFM 即可运行图库供给循环",
|
||||
|
||||
helpLabTitle: "GenerateUI标签页 — Lab子标签",
|
||||
helpLabDesc: "实验性的I2I批量生成功能。将GenerateUI当前加载的工作流运行N次,可分别独立地从任意指定次数开始更改Checkpoint、VAE、Prompt、KSampler。包含Setting和Results两个子标签。",
|
||||
helpLabCardColumnsTitle: "Setting子标签 — 图片与列",
|
||||
helpLab1: "左侧面板 — Image:拖入或点击选择作为I2I源图片;会上传到ComfyUI的input文件夹,只要未勾选\"Use generated image for next\",每次迭代都会使用该图片",
|
||||
helpLab2: "左侧面板 — Use generated image for next:勾选后,从第2次开始使用上一次迭代生成的第1张图片作为I2I输入,而不是原始拖入的图片(第1次始终使用原始图片)",
|
||||
helpLab3: "中间面板 — Checkpoint、VAE、Prompt、KSampler这4列相互独立;每列各自维护自己的关键帧列表,而不是共享同一个行网格",
|
||||
helpLab4: "每列的+/-:+会在下一个空闲的迭代序号(不超过Batch数)添加关键帧并立即打开编辑弹窗;-会删除最后一个关键帧(第1个关键帧无法删除)",
|
||||
helpLab5: "点击单元格即可编辑:第1个关键帧设置基准值(留空则保持工作流本身的设置);第2个及以后的关键帧还可以选择从第几次开始应用,以及是否勾选\"恢复第1次的设置\"",
|
||||
helpLab6: "第N次迭代的实际生效值:使用迭代序号≤N中序号最大的关键帧,并一直延续到下一个关键帧改变它为止;勾选\"恢复第1次的设置\"时会直接复用第1次的值(若第1次为空则使用工作流默认值),而不是指定新值",
|
||||
helpLab7: "Lab不会修改Input/Model/Settings中显示的工作流——每次迭代都在加载工作流的一个副本上运行,因此这里的关键帧设置不会影响其他子标签",
|
||||
helpLab8: "Prompt单元格编辑弹窗的附加功能:「Get from GenerateUI」从GenerateUI当前加载的工作流中获取Positive/Negative;「Get from Image」从Lab左侧IMAGE区域当前加载的图片中提取提示词(与Metadata标签相同的检测逻辑,无需另外选择文件);「Style」会将GenerateUI工具栏顶部Style复选框/下拉菜单中当前选中的风格,以逗号分隔追加到下方的Positive/Negative文本之后(不会清空已有内容)——需先在顶部勾选复选框并选择风格,Lab没有自己独立的风格选择器",
|
||||
helpLab9: "左侧面板 — 「T2I workflow (no source image)」:当加载的工作流是文生图(T2I,没有LoadImage节点)时勾选此项。它会跳过\"请先拖入源图片\"的要求,忽略任何已拖入的图片,Checkpoint/VAE/Prompt/KSampler的关键帧仍会正常应用;开启期间Image拖放区域和「Use generated image for next」会变灰(之前拖入的图片不会被删除,以便切回I2I时使用);该设置会作为 t2i_mode 保存在方案中",
|
||||
helpLabCardPlanTitle: "Setting子标签 — 运行与方案文件",
|
||||
helpLabPlan1: "右侧面板 — Batch:要运行的迭代次数;Note:与方案一起保存的自由备注",
|
||||
helpLabPlan2: "Run/Pause/Cancel:Run开始批量循环;Pause/Resume在各次迭代之间暂停/继续;Cancel会中断当前生成并停止循环",
|
||||
helpLabPlan3: "Plan Save与Save As的区别:Plan Save会在已加载方案时静默覆盖保存(未加载时才会询问名称);Save As无论是否已加载方案都会始终询问新名称",
|
||||
helpLabPlan4: "Plan Load:将方案的.json文件或其任意索引图片(.png)拖入PLAN区域(也可点击浏览选择)——拖入.json会直接读取;拖入.png时,如果是v0.3.90之后生成的索引图片(都会内嵌wfm_lab_plan数据块),也会直接读取其中的方案数据;仅对更早期、不含此数据的索引图片才会回退为从服务器获取同名的.json",
|
||||
helpLabPlan5: "Plan Clear会将Setting标签重置为空方案(会先确认)",
|
||||
helpLabPlan6: "方案文件保存在 user/default/Workflow-Studio/lab_plan/ 下,命名为ws_labplan_<名称>.json,若方案含有结果,还会自动生成ws_labplan_<名称>.png索引图片——最多9张结果缩略图组成的联系表(每行3张)。ws_labplan_前缀由Plan Save/Save As自动添加,输入时只需填写不带前缀的名称,方便在该文件夹中辨认方案文件",
|
||||
helpLabPlan7: "设置标签页中配置的Eagle自动保存,会像GenerateUI自身的Generate按钮一样应用于Lab生成的每一张图片",
|
||||
helpLabPlan8: "Plan JSON子标签:以原始JSON形式显示当前Setting标签页的方案内容。「Refresh」会从Setting标签页重新读取(放弃尚未应用的编辑);「Apply to Setting」会解析你的编辑并写回Setting标签页(列、备注、Batch数量、图片链式生成、源图片、结果)——效果与直接将该JSON作为方案文件加载相同,无需先保存",
|
||||
helpLabPlan9: "每个已保存的方案都会记录保存时GenerateUI中加载的工作流文件名(Plan JSON中的workflow_filename)。加载方案后,PLAN框中会出现「Open Workflow」按钮——点击即可将该确切的工作流文件重新加载到GenerateUI中(会先弹出确认,因为这会替换当前已加载的工作流)",
|
||||
helpLabPlan10: "右侧面板 — 「Save index image to Output on Run」:勾选后,每次Run结束都会生成与方案文件相同的联系表索引图片,并直接保存到ComfyUI自身的Output文件夹,命名为 Lab_index_<counter>_.png(像普通生成图片一样自动编号,不会覆盖上一次运行的结果)。保存后还会像其他Lab生成的图片一样经过Eagle自动保存(若在设置标签页中启用)——没有单独的复选框,遵循同一个全局开关。这与Plan Save的行为相互独立——无论是否勾选此项,Plan Save都会照常在 lab_plan/ 中的方案文件旁写入自己的索引图片;该设置会作为 save_index_on_run 保存在方案中",
|
||||
helpLabPlan11: "每一张索引图片PNG——无论是来自Plan Save/Save As,还是来自Save index image to Output on Run——都会将完整的方案数据(列、备注、Batch数量、图片链式生成/T2I设置、工作流文件名、结果列表)直接嵌入为PNG的wfm_lab_plan数据块。因此Output文件夹中的那份副本是完全自包含的,即使旁边没有任何对应的.json文件,也可以作为Plan Load的加载来源",
|
||||
helpLabCardResultsTitle: "Results子标签",
|
||||
helpLabResults1: "左侧面板显示本次运行使用的源图片",
|
||||
helpLabResults2: "中间面板按生成顺序显示最多9张生成图片;点击缩略图可放大查看",
|
||||
helpLabResults3: "加载已保存的方案时,会根据方案文件中记录的图片列表恢复Results网格",
|
||||
|
||||
helpSearchPlaceholder: "搜索帮助...",
|
||||
helpPrompt1: "AI助手(Ollama / LM Studio / Lemonade):模型选择、聊天咨询提示词、图片附件(视觉模型);后端和URL在设置标签页配置",
|
||||
helpPrompt2: "将AI回复应用到生成UI的提示词",
|
||||
@@ -2779,8 +3164,72 @@ const LANGUAGES = {
|
||||
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 标签共享",
|
||||
helpAi7: "Chat 面板 — SVG:LLM 还可以直接以文本形式生成 SVG 图形,完全不涉及 ComfyUI 工作流;只需要求它用 SVG 绘制内容,如果回复中包含 SVG 代码,会自动渲染为内嵌预览,并提供\"保存到图库\"按钮,将其保存为 .svg 文件到 Gallery 标签页的输出文件夹;该功能不使用 tool calling(函数调用),因此不支持 tool calling 的模型也可以使用",
|
||||
helpAi8: "Chat 面板 — 技能:在消息列表上方的下拉菜单中选择一个技能,其内容会作为 system prompt 添加到对话开头(不会显示在可见的聊天记录中);点击铅笔按钮打开技能管理面板,可创建、编辑、删除 .md 技能文件(保存路径:user/default/Workflow-Studio/ai_skills/);文件开头 --- 之间的 name/description 字段会显示在下拉菜单中;默认内置两个技能:「SVG Icon Generator」(在绘制前先将对象拆解为多个部件,以生成更易识别的 SVG)和「Skill Creator」(通过对话向你提问并起草新的技能文件);当回复中包含```skill代码块时,消息下方会出现「保存为技能」按钮,点击后会打开管理面板并预填内容",
|
||||
helpAi9: "编写技能的小贴士:为 frontmatter 的 name/description 写清楚的内容,便于在下拉菜单和提示中阅读;明确说明期望的输出格式,如果有需要后续提取的成果物(如 SVG 或另一个技能文件),指示它用专门的代码围栏包裹;具体、祈使句式的指令比笼统的说明对本地小模型更有效,附上一个简短示例效果更佳;明确写出硬性约束(禁止内容、安全性、长度限制等),不要指望模型会自行推断;如果不确定怎么写,可以在下拉菜单中选择「Skill Creator」技能并描述你想要的技能,它会通过提问帮你起草",
|
||||
helpAiCardLayout: "布局",
|
||||
helpAiCardTranslate: "Translation 面板",
|
||||
helpAiCardChat: "Chat 面板",
|
||||
helpAiCardSvg: "Chat 面板 — SVG 生成",
|
||||
helpAiCardSkills: "Chat 面板 — 技能",
|
||||
helpAiCardSkillTips: "编写技能的小贴士",
|
||||
helpAiCardTools: "TOOLS 面板",
|
||||
helpAiCardSettings: "Settings 面板",
|
||||
helpJumpToTab: "打开标签页 →",
|
||||
helpAboutCardGettingStarted: "开始使用",
|
||||
helpWfTitle: "工作流标签页",
|
||||
helpWfCardBrowse: "浏览与筛选",
|
||||
helpWfCardGroups: "分组与徽章",
|
||||
helpWfCardPanel: "侧边面板与详情弹窗",
|
||||
helpGenTitle: "GenerateUI 标签页",
|
||||
helpGenCardConnect: "连接与加载工作流",
|
||||
helpGenCardTabs: "标签布局",
|
||||
helpGenCardSeed: "种子与生成",
|
||||
helpGenCardBatchSetup: "Batch 标签 — 设置",
|
||||
helpGenCardBatchTypes: "Batch 标签 — 批量类型",
|
||||
helpPromptTitle: "提示词标签页",
|
||||
helpPromptCardAi: "AI 助手与翻译",
|
||||
helpPromptCardPresets: "预设",
|
||||
helpPromptCardWildcards: "通配符",
|
||||
helpMetadataCardOverview: "概述",
|
||||
helpMetadataCardExtracted: "提取的信息",
|
||||
helpMetadataCardFormats: "支持的格式",
|
||||
helpSettingsTitle: "设置标签页",
|
||||
helpSettingsCardGeneral: "常规",
|
||||
helpSettingsCardConnections: "连接",
|
||||
helpSettingsCardWorkflow: "工作流默认值",
|
||||
helpSettingsCardEditor: "编辑器自定义",
|
||||
helpSettingsCardMisc: "数据与其他",
|
||||
helpNodesTitle: "节点标签页",
|
||||
helpNodesCardBrowse: "浏览与整理",
|
||||
helpNodesCardGroups: "分组与节点集",
|
||||
helpNodesCardPanel: "侧边面板与多选",
|
||||
helpGalleryCardBrowse: "浏览",
|
||||
helpGalleryCardFolder: "文件夹与选择",
|
||||
helpGalleryCardDetail: "详情面板",
|
||||
helpGalleryCardActions: "操作工具栏",
|
||||
helpGalleryCardFormats: "格式与自动保存",
|
||||
helpModelsTitle: "模型标签页",
|
||||
helpModelsCardBrowse: "浏览",
|
||||
helpModelsCardCivitai: "侧边面板与 CivitAI",
|
||||
helpModelsCardApply: "应用与详情",
|
||||
helpModelsCardGroups: "分组与徽章",
|
||||
helpModelsCardBulk: "启用/禁用与批量操作",
|
||||
helpSidepanelCardTabs: "标签概览",
|
||||
helpSidepanelCardDnd: "拖放操作",
|
||||
helpSidepanelCardInfo: "I 标签(信息)",
|
||||
helpSidepanelCardAi: "AI TOOL 标签",
|
||||
helpSidepanelCardNav: "自定义与导航",
|
||||
helpTaggerCardOverview: "概述与设置",
|
||||
helpTaggerCardSingle: "Single 标签",
|
||||
helpTaggerCardSending: "发送结果",
|
||||
helpTaggerCardBatch: "Batch 与 DB 标签",
|
||||
helpImageEditCardOverview: "概述",
|
||||
helpShortcutsCardGeneral: "常规",
|
||||
helpTroubleCardConnection: "连接问题",
|
||||
helpTroubleCardWorkflow: "工作流与生成问题",
|
||||
helpTroubleCardTagger: "Tagger 与模型问题",
|
||||
helpImageEditTitle: "图像编辑选项卡",
|
||||
helpImageEdit1: "基于图层的图像编辑器 — 通过多个图层(图像、文字、绘制、遮罩类型)合成图像并导出",
|
||||
helpImageEdit2: "加载图像:拖放到画布、点击Upload按钮、或通过Gallery标签工具栏的Image Edit按钮发送;第一张图像成为图层1(自动锁定);后续图像缩放至画布大小作为新图层添加",
|
||||
@@ -2873,6 +3322,7 @@ const LANGUAGES = {
|
||||
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\",在每次加载时强制应用一个已验证可用的模型。",
|
||||
helpTrouble9: "点击 Generate 后,执行的是与刚加载的工作流不同的工作流:Batch 子标签页中的某个批量类型(Checkpoint/LoRA/Prompt/Workflow/Sampler/Scheduler)复选框仍处于开启状态。只要有任意批量类型处于启用状态,Generate 按钮执行的就是批量选择的内容,而不是当前加载的工作流。请打开 Batch 子标签页,关闭已启用的批量类型复选框。",
|
||||
helpSupportTitle: "支持",
|
||||
helpSupportDesc: "如果您觉得这个插件有用,请考虑支持它的开发!",
|
||||
helpGithubDesc: "报告问题、请求功能和贡献代码",
|
||||
@@ -3012,14 +3462,14 @@ const LANGUAGES = {
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
|
||||
metaFmtSdwebui: "SD WebUI / SD Forge — PNG(parameters)",
|
||||
metaFmtFooocus: "Fooocus — PNG(fooocus_scheme)",
|
||||
metaFormatTodo: "已支持 Flux.2 / Qwen-Image / Z-Image 子图工作流",
|
||||
metaFormatTodo: "已支持 Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 子图工作流",
|
||||
helpMetadataTitle: "Metadata 标签页",
|
||||
helpMetadataDesc: "拖放 ComfyUI 生成的 PNG/WebP 或工作流 JSON,即可提取并显示其中的模型和提示词信息。",
|
||||
helpMetadata1: "左列:拖放区(PNG/WebP 显示图片预览)及文件信息与支持格式说明",
|
||||
helpMetadata2: "中列:从工作流中提取的 Checkpoint、VAE、扩散模型、文本编码器列表",
|
||||
helpMetadata3: "右列:LoRA 列表(含强度值)和提示词列表(点击在下方显示全文)",
|
||||
helpMetadata4: "提示词操作 — Copy:复制到剪贴板;GenUI:P/N:设置 GenerateUI 正/负向提示;Prompt:P/N:设置 Prompt 标签页预设正/负向提示",
|
||||
helpMetadata5: "支持格式:ComfyUI PNG/WebP/JSON(标准格式 + Flux.2 / Qwen-Image / Z-Image 子图工作流)、SD WebUI、SD Forge、Fooocus。",
|
||||
helpMetadata5: "支持格式:ComfyUI PNG/WebP/JSON(标准格式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 子图工作流)、SD WebUI、SD Forge、Fooocus。",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+157
-35
@@ -6,6 +6,7 @@
|
||||
|
||||
import { t } from "./i18n.js";
|
||||
import { escapeHtml } from "./util.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
|
||||
// ── File size limit ───────────────────────────────────────────
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
||||
@@ -74,7 +75,7 @@ function parseITXtChunk(data, latin1, utf8) {
|
||||
pos = findNull(data, pos); if (pos === -1) return null; pos++;
|
||||
return { keyword, text: utf8.decode(data.slice(pos)) };
|
||||
}
|
||||
async function readAllPNGTextChunks(file) {
|
||||
export async function readAllPNGTextChunks(file) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const PNG_SIG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
||||
@@ -192,6 +193,45 @@ function extractPrompts(wf) {
|
||||
return Array.isArray(wf.nodes) ? extractPromptsLiteGraph(wf) : extractPromptsAPI(wf);
|
||||
}
|
||||
|
||||
// LiteGraph形式: リンクを辿ってテキストを解決(ComfySwitchNode等の中継ノードにも対応)
|
||||
function resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, srcId, slot, depth = 0) {
|
||||
if (depth > 6) return null;
|
||||
const srcNode = nodeMap.get(srcId);
|
||||
if (!srcNode) return null;
|
||||
const srcType = srcNode.type ?? "";
|
||||
if (isPromptStylerNode(srcType)) {
|
||||
const v = srcNode.widgets_values?.[slot];
|
||||
return (v && typeof v === "string") ? v : null;
|
||||
}
|
||||
// ComfySwitchNode ("If/Else Switch") — 片方(TextGenerateなどLLMノード)は静的解決不能なため
|
||||
// on_false/on_true の両方を試し、リテラルへ解決できた方を採用する。
|
||||
if (srcType === "ComfySwitchNode" && Array.isArray(srcNode.inputs)) {
|
||||
for (const name of ["on_false", "on_true"]) {
|
||||
const inp = srcNode.inputs.find(i => i.name === name);
|
||||
if (inp?.link == null) continue;
|
||||
const originId = linkOrigin.get(inp.link);
|
||||
const originSlot = linkSlot.get(inp.link) ?? 0;
|
||||
if (originId == null) continue;
|
||||
const text = resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, originId, originSlot, depth + 1);
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// WFS_PromptText, PrimitiveStringMultiline 等、任意の STRING 出力ノード
|
||||
const v = srcNode.widgets_values?.[slot] ?? srcNode.widgets_values?.[0];
|
||||
if (v && typeof v === "string") return v;
|
||||
// さらにリンクされている場合(StringReplace等の中継ノード)は追跡する
|
||||
if (Array.isArray(srcNode.inputs)) {
|
||||
const nextInput = srcNode.inputs.find(i => ["text", "value", "prompt", "string"].includes(i.name));
|
||||
if (nextInput?.link != null) {
|
||||
const originId = linkOrigin.get(nextInput.link);
|
||||
const originSlot = linkSlot.get(nextInput.link) ?? 0;
|
||||
if (originId != null) return resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, originId, originSlot, depth + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// CLIPTextEncode + KSampler でプロンプト抽出(トップレベル・サブグラフ共用)
|
||||
// サンプラーが見つかりテキストが取れた場合のみ非null を返す
|
||||
function extractPromptsFromNodeSet(nodes, links) {
|
||||
@@ -220,18 +260,8 @@ function extractPromptsFromNodeSet(nodes, links) {
|
||||
if (textInput?.link != null) {
|
||||
const originId = linkOrigin.get(textInput.link);
|
||||
const originSlot = linkSlot.get(textInput.link) ?? 0;
|
||||
const srcNode = originId != null ? nodeMap.get(originId) : null;
|
||||
if (srcNode) {
|
||||
const srcType = srcNode.type ?? "";
|
||||
if (isPromptStylerNode(srcType)) {
|
||||
const v = srcNode.widgets_values?.[originSlot];
|
||||
if (v && typeof v === "string") textMap.set(n.id, v);
|
||||
} else {
|
||||
// WFS_PromptText, PrimitiveStringMultiline, ComfySwitchNode 等、任意の STRING 出力ノード
|
||||
const v = srcNode.widgets_values?.[originSlot] ?? srcNode.widgets_values?.[0];
|
||||
if (v && typeof v === "string") textMap.set(n.id, v);
|
||||
}
|
||||
}
|
||||
const text2 = resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, originId, originSlot);
|
||||
if (text2) textMap.set(n.id, text2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,15 +270,48 @@ function extractPromptsFromNodeSet(nodes, links) {
|
||||
for (const n of nodes) {
|
||||
if (!isSamplerNode(n.type ?? "") || !Array.isArray(n.inputs)) continue;
|
||||
foundSampler = true;
|
||||
for (const inp of n.inputs) {
|
||||
// SamplerCustomAdvanced doesn't hold positive/negative directly — it drives a separate
|
||||
// Guider node (CFGGuider/DualCFGGuider/BasicGuider) via its "guider" input. Scan that
|
||||
// node's inputs instead so the loop below can find the positive/negative-role slots.
|
||||
let inputsToScan = n.inputs;
|
||||
const guiderInput = n.inputs.find(inp => inp.name === "guider");
|
||||
if (guiderInput?.link != null) {
|
||||
const guiderId = linkOrigin.get(guiderInput.link);
|
||||
const guiderNode = guiderId != null ? nodeMap.get(guiderId) : null;
|
||||
if (Array.isArray(guiderNode?.inputs)) inputsToScan = guiderNode.inputs;
|
||||
}
|
||||
for (const inp of inputsToScan) {
|
||||
if (!inp || inp.link == null) continue;
|
||||
const originId = linkOrigin.get(inp.link);
|
||||
if (originId == null) continue;
|
||||
const text = textMap.get(originId);
|
||||
if (!text) continue;
|
||||
const name = inp.name ?? "";
|
||||
if (name === "positive" || name.startsWith("positive")) pos.add(text);
|
||||
else if (name === "negative" || name.startsWith("negative")) neg.add(text);
|
||||
// DualCFGGuider (HiDream E1): cond1 carries the positive-derived conditioning.
|
||||
const isPos = name === "positive" || name === "cond1" || name.startsWith("positive");
|
||||
const isNeg = name === "negative" || name.startsWith("negative");
|
||||
if (!isPos && !isNeg) continue;
|
||||
// TextEncodeMageFlowEdit / TextEncodeBooguEdit — one node carries both prompt &
|
||||
// negative_prompt directly, so textMap (single text per node) can't represent both
|
||||
// roles.
|
||||
const srcNode = nodeMap.get(originId);
|
||||
if (srcNode?.type === "TextEncodeMageFlowEdit" || srcNode?.type === "TextEncodeBooguEdit") {
|
||||
const widgetIdx = isPos ? 0 : 1; // widgets_values: [prompt, negative_prompt, ...]
|
||||
const text = srcNode.widgets_values?.[widgetIdx];
|
||||
if (text && typeof text === "string") { if (isPos) pos.add(text); else neg.add(text); }
|
||||
continue;
|
||||
}
|
||||
// InstructPixToPixConditioning (HiDream E1): forwards the actual text-encoder
|
||||
// conditioning through its own positive/negative inputs — follow one more hop.
|
||||
let resolvedOriginId = originId;
|
||||
if (srcNode?.type === "InstructPixToPixConditioning" && Array.isArray(srcNode.inputs)) {
|
||||
const innerInput = srcNode.inputs.find(i => i.name === (isPos ? "positive" : "negative"));
|
||||
if (innerInput?.link != null) {
|
||||
const innerId = linkOrigin.get(innerInput.link);
|
||||
if (innerId != null) resolvedOriginId = innerId;
|
||||
}
|
||||
}
|
||||
const text = textMap.get(resolvedOriginId);
|
||||
if (!text) continue;
|
||||
if (isPos) pos.add(text); else neg.add(text);
|
||||
}
|
||||
}
|
||||
if (!foundSampler) return null;
|
||||
@@ -300,7 +363,8 @@ function extractMarkdownNoteModels(wf) {
|
||||
}
|
||||
|
||||
// API形式: リンク参照 [srcNodeId, slot] からテキストを解決
|
||||
function resolveLinkedText(wf, srcId, slot) {
|
||||
function resolveLinkedText(wf, srcId, slot, depth = 0) {
|
||||
if (depth > 6) return null;
|
||||
const src = wf[String(srcId)];
|
||||
if (!src || typeof src !== "object") return null;
|
||||
const ct = src.class_type ?? "";
|
||||
@@ -309,13 +373,32 @@ function resolveLinkedText(wf, srcId, slot) {
|
||||
const v = slot === 0 ? src.inputs?.text_positive : src.inputs?.text_negative;
|
||||
return (v && typeof v === "string") ? v : null;
|
||||
}
|
||||
// 汎用テキストキー
|
||||
// ComfySwitchNode ("If/Else Switch", on_false/on_true/switch) — Ernie Imageのプロンプト強化
|
||||
// トグルなどで使われる。片方(TextGenerateなどLLMノード)は静的解決不能なため両方試し、
|
||||
// リテラルへ解決できた方を採用する。
|
||||
if (ct === "ComfySwitchNode") {
|
||||
for (const key of ["on_false", "on_true"]) {
|
||||
const v = src.inputs?.[key];
|
||||
if (Array.isArray(v)) {
|
||||
const text = resolveLinkedText(wf, v[0], v[1] ?? 0, depth + 1);
|
||||
if (text) return text;
|
||||
} else if (typeof v === "string" && v) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// 汎用テキストキー("value" は PrimitiveString/PrimitiveStringMultiline)
|
||||
const keys = slot === 0
|
||||
? ["text_positive", "text", "text_g", "prompt"]
|
||||
? ["text_positive", "text", "text_g", "prompt", "value"]
|
||||
: ["text_negative", "text_l"];
|
||||
for (const k of keys) {
|
||||
const v = src.inputs?.[k];
|
||||
if (v && typeof v === "string") return v;
|
||||
if (typeof v === "string" && v) return v;
|
||||
if (Array.isArray(v)) {
|
||||
const text = resolveLinkedText(wf, v[0], v[1] ?? 0, depth + 1);
|
||||
if (text) return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -330,7 +413,9 @@ function extractPromptsAPI(wf) {
|
||||
const textMap = new Map();
|
||||
for (const [id, n] of Object.entries(wf)) {
|
||||
if (!n || !isTextEncoderNode(n.class_type ?? "")) continue;
|
||||
const raw = n.inputs?.text ?? n.inputs?.text_g ?? null;
|
||||
// "prompt" — TextEncodeQwenImageEdit(Plus)/TextEncodeBooguEdit and similar Image Edit
|
||||
// model text encoders use this key instead of "text"/"text_g".
|
||||
const raw = n.inputs?.text ?? n.inputs?.text_g ?? n.inputs?.prompt ?? null;
|
||||
if (raw && typeof raw === "string") {
|
||||
textMap.set(id, raw);
|
||||
} else if (Array.isArray(raw)) {
|
||||
@@ -344,12 +429,37 @@ function extractPromptsAPI(wf) {
|
||||
for (const n of Object.values(wf)) {
|
||||
if (!n || !isSamplerNode(n.class_type ?? "")) continue;
|
||||
foundSampler = true;
|
||||
for (const [key, val] of Object.entries(n.inputs ?? {})) {
|
||||
// SamplerCustomAdvanced doesn't hold positive/negative directly — it drives a separate
|
||||
// Guider node (CFGGuider/DualCFGGuider/BasicGuider) via its "guider" input. Scan that
|
||||
// node's inputs instead so the loop below can find the positive/negative-role keys.
|
||||
let inputsToScan = n.inputs ?? {};
|
||||
if (Array.isArray(n.inputs?.guider)) {
|
||||
const guiderNode = wf[String(n.inputs.guider[0])];
|
||||
if (guiderNode?.inputs) inputsToScan = guiderNode.inputs;
|
||||
}
|
||||
for (const [key, val] of Object.entries(inputsToScan)) {
|
||||
if (!Array.isArray(val)) continue;
|
||||
const text = textMap.get(String(val[0]));
|
||||
// DualCFGGuider (HiDream E1): cond1 carries the positive-derived conditioning.
|
||||
const isPos = key === "positive" || key === "cond1" || key.startsWith("positive");
|
||||
const isNeg = key === "negative" || key.startsWith("negative");
|
||||
if (!isPos && !isNeg) continue;
|
||||
// TextEncodeMageFlowEdit / TextEncodeBooguEdit — one node carries both prompt &
|
||||
// negative_prompt directly, so textMap (single text per node) can't represent both
|
||||
// roles.
|
||||
const srcNode = wf[String(val[0])];
|
||||
if (srcNode?.class_type === "TextEncodeMageFlowEdit" || srcNode?.class_type === "TextEncodeBooguEdit") {
|
||||
const text = isPos ? srcNode.inputs?.prompt : srcNode.inputs?.negative_prompt;
|
||||
if (text && typeof text === "string") { if (isPos) pos.add(text); else neg.add(text); }
|
||||
continue;
|
||||
}
|
||||
// InstructPixToPixConditioning (HiDream E1): forwards the actual text-encoder
|
||||
// conditioning through its own positive/negative inputs — follow one more hop.
|
||||
const resolvedId = srcNode?.class_type === "InstructPixToPixConditioning" && Array.isArray(srcNode.inputs?.[isPos ? "positive" : "negative"])
|
||||
? String(srcNode.inputs[isPos ? "positive" : "negative"][0])
|
||||
: String(val[0]);
|
||||
const text = textMap.get(resolvedId);
|
||||
if (!text) continue;
|
||||
if (key === "positive" || key.startsWith("positive")) pos.add(text);
|
||||
else if (key === "negative" || key.startsWith("negative")) neg.add(text);
|
||||
if (isPos) pos.add(text); else neg.add(text);
|
||||
}
|
||||
}
|
||||
if (!foundSampler || (pos.size === 0 && neg.size === 0)) { const all = [...textMap.values()].filter(t => t && t.trim()); return { positives: [], negatives: [], texts: all }; }
|
||||
@@ -446,15 +556,27 @@ function parseFooocusMetadata(raw) {
|
||||
}
|
||||
|
||||
// ── Master extractAllMetadata ─────────────────────────────────
|
||||
async function extractAllMetadata(file) {
|
||||
export async function extractAllMetadata(file) {
|
||||
const name = file.name.toLowerCase();
|
||||
const isJSON = file.type === "application/json" || name.endsWith(".json");
|
||||
const isWebP = file.type === "image/webp" || name.endsWith(".webp");
|
||||
|
||||
function fromWorkflow(wf, source) {
|
||||
async function fromWorkflow(originalWf, source) {
|
||||
let wf = originalWf;
|
||||
// サブグラフを持つワークフローは、外側のサブグラフノードに実際に設定された値
|
||||
// (ユーザーが見ている値)が内部ノードのwidgets_valuesへ反映されていないことがある
|
||||
// (テンプレートの古いデフォルト値のまま)。convertUiToApi() はこの注入も含めて
|
||||
// 正しく解決するため、そちらを経由してから抽出する。通常のワークフローには影響しない。
|
||||
if (Array.isArray(wf?.nodes) && wf.definitions?.subgraphs?.length > 0) {
|
||||
try {
|
||||
const apiWf = await comfyWorkflow.convertUiToApi(wf);
|
||||
if (apiWf && Object.keys(apiWf).length > 0) wf = apiWf;
|
||||
} catch { /* 変換失敗時は元のUI形式のまま抽出(既存ロジックにフォールバック) */ }
|
||||
}
|
||||
const base = { source, checkpoints: extractCheckpoints(wf), vaes: extractVAEs(wf), diffusionModels: extractDiffusionModels(wf), textEncoders: extractTextEncoders(wf), loras: extractLoRAs(wf), ...extractPrompts(wf) };
|
||||
// MarkdownNote からモデル情報を補完(subgraph形式の flux/qwen/z-image など)
|
||||
const mdm = extractMarkdownNoteModels(wf);
|
||||
// MarkdownNote からモデル情報を補完(subgraph形式の flux/qwen/z-image など)。
|
||||
// API変換後は nodes 情報が失われるため、常に元のUI形式ワークフローから抽出する。
|
||||
const mdm = extractMarkdownNoteModels(originalWf);
|
||||
if (mdm) {
|
||||
if (!base.checkpoints.length) base.checkpoints = mdm.checkpoints;
|
||||
if (!base.vaes.length) base.vaes = mdm.vaes;
|
||||
@@ -467,20 +589,20 @@ async function extractAllMetadata(file) {
|
||||
|
||||
if (isJSON) {
|
||||
let wf; try { wf = JSON.parse(sanitizeJSON(await file.text())); } catch { return null; }
|
||||
return wf ? fromWorkflow(wf, "comfyui") : null;
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
if (isWebP) {
|
||||
const exif = await readWebPEXIFChunk(file);
|
||||
if (!exif) return null;
|
||||
const wf = extractWorkflowFromEXIF(exif);
|
||||
return wf ? fromWorkflow(wf, "comfyui") : null;
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
// PNG
|
||||
const chunks = await readAllPNGTextChunks(file);
|
||||
if (!chunks) return null;
|
||||
|
||||
if (chunks.prompt) { let wf; try { wf = JSON.parse(sanitizeJSON(chunks.prompt)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.workflow) { let wf; try { wf = JSON.parse(sanitizeJSON(chunks.workflow)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.prompt) { let wf; try { wf = JSON.parse(sanitizeJSON(chunks.prompt)); } catch { return null; } return wf ? await fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.workflow) { let wf; try { wf = JSON.parse(sanitizeJSON(chunks.workflow)); } catch { return null; } return wf ? await fromWorkflow(wf, "comfyui") : null; }
|
||||
|
||||
if (chunks.fooocus_scheme === "fooocus" && chunks.parameters) {
|
||||
const f = parseFooocusMetadata(chunks.parameters);
|
||||
|
||||
@@ -2,6 +2,76 @@
|
||||
* Shared utilities
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// PNG metadata embedding (iTXt chunk)
|
||||
// ============================================
|
||||
// Embeds an arbitrary key/UTF-8-text pair into a canvas-generated PNG data URL as
|
||||
// an iTXt chunk, inserted right after IHDR — same insertion point/CRC approach as
|
||||
// ComfyUI's own workflow-embedding (web/comfyui/top_menu_extension.js's
|
||||
// embedWorkflowInPng), but iTXt instead of tEXt so non-ASCII text (Japanese notes,
|
||||
// prompts, etc.) round-trips correctly through this codebase's PNG chunk reader
|
||||
// (metadata-tab.js's readAllPNGTextChunks already decodes iTXt text as UTF-8;
|
||||
// tEXt text there is decoded as Latin-1, which would mangle non-ASCII payloads).
|
||||
|
||||
function _n2b(n) {
|
||||
return new Uint8Array([(n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
|
||||
function _joinBytes(...arrs) {
|
||||
const result = new Uint8Array(arrs.reduce((total, a) => total + a.byteLength, 0));
|
||||
arrs.reduce((offset, a) => { result.set(a, offset); return offset + a.byteLength; }, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
let _pngCrcTable = null;
|
||||
function _pngCrc32(data) {
|
||||
if (!_pngCrcTable) {
|
||||
_pngCrcTable = [];
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
_pngCrcTable[n] = c;
|
||||
}
|
||||
}
|
||||
let crc = 0 ^ -1;
|
||||
for (let i = 0; i < data.byteLength; i++) crc = (crc >>> 8) ^ _pngCrcTable[(crc ^ data[i]) & 0xff];
|
||||
return (crc ^ -1) >>> 0;
|
||||
}
|
||||
|
||||
function _dataUrlToBytes(dataUrl) {
|
||||
const binary = atob(dataUrl.slice(dataUrl.indexOf(",") + 1));
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function _bytesToDataUrl(bytes, mime) {
|
||||
let binary = "";
|
||||
const chunkSize = 0x8000;
|
||||
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
|
||||
}
|
||||
return `data:${mime};base64,${btoa(binary)}`;
|
||||
}
|
||||
|
||||
export function embedPngTextChunk(dataUrl, keyword, text) {
|
||||
const bytes = _dataUrlToBytes(dataUrl);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
|
||||
const data = _joinBytes(
|
||||
new TextEncoder().encode(`iTXt${keyword}`),
|
||||
new Uint8Array([0, 0, 0, 0, 0]), // keyword-NUL, compression flag(0), compression method(0), empty lang tag+NUL, empty translated keyword+NUL
|
||||
new TextEncoder().encode(text),
|
||||
);
|
||||
const chunk = _joinBytes(_n2b(data.byteLength - 4), data, _n2b(_pngCrc32(data)));
|
||||
|
||||
// Insert after IHDR (8-byte signature + 4-byte length + 4-byte type + IHDR data + 4-byte CRC)
|
||||
const ihdrDataLen = view.getUint32(8);
|
||||
const insertAt = ihdrDataLen + 20;
|
||||
const result = _joinBytes(bytes.subarray(0, insertAt), chunk, bytes.subarray(insertAt));
|
||||
return _bytesToDataUrl(result, "image/png");
|
||||
}
|
||||
|
||||
// HTMLエスケープ(属性値・テキストコンテンツ両方で安全に使える)
|
||||
export function escapeHtml(s) {
|
||||
return String(s)
|
||||
@@ -38,6 +108,49 @@ export function getAiBackendDefaultUrl(backend) {
|
||||
return AI_BACKEND_DEFAULT_URLS[backend] || AI_BACKEND_DEFAULT_URLS.ollama;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Eagle Auto-Save
|
||||
// ============================================
|
||||
|
||||
export function getEagleSettings() {
|
||||
const s = getSettings();
|
||||
return {
|
||||
url: s.eagleUrl || "http://localhost:41595",
|
||||
autoSave: !!s.eagleAutoSave,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveToEagle(imageUrl, name, tags = [], fileInfo = null) {
|
||||
const eagle = getEagleSettings();
|
||||
if (!eagle.autoSave) return;
|
||||
try {
|
||||
const res = await fetch("/api/wfm/eagle/add", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
eagleUrl: eagle.url,
|
||||
url: imageUrl,
|
||||
name,
|
||||
tags: ["wfm-comfyui", ...tags],
|
||||
// SVG はサーバー側でローカルパス解決(addFromPath)するために必要
|
||||
filename: fileInfo?.filename || "",
|
||||
subfolder: fileInfo?.subfolder || "",
|
||||
type: fileInfo?.type || "output",
|
||||
// comfyui-tosvg の Save SVG String など、絶対パスが既知の場合に渡す
|
||||
localPath: fileInfo?.localPath || "",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === "success") {
|
||||
console.log("[Eagle] Saved:", name);
|
||||
} else {
|
||||
console.warn("[Eagle] Save failed:", data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[Eagle] Save error:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 検索inputにオーバーレイXボタンを設定する。
|
||||
* @param {string} inputId - 検索inputのID
|
||||
|
||||
@@ -51,7 +51,7 @@ function sendToCanvas(workflowData) {
|
||||
// window.opener経由でComfyUIキャンバスに直接ロード(推奨)
|
||||
if (window.opener && typeof window.opener.wfmReceiveWorkflow === "function") {
|
||||
window.opener.wfmReceiveWorkflow(workflowData);
|
||||
showToast(t("workflowSentToCanvas"), "success");
|
||||
showToast(t("workflowSentToCanvasDirect"), "success");
|
||||
return;
|
||||
}
|
||||
// フォールバック: localStorage + タイトルドラッグ(UI形式のみ)
|
||||
@@ -270,6 +270,85 @@ async function getRawWorkflow(filename) {
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AI Summary — structured extraction (not raw JSON dump)
|
||||
// ============================================
|
||||
|
||||
// analyzeWorkflow() の結果をコンパクトなテキストに整形する。
|
||||
// 生JSONをそのままLLMに渡すと、MarkdownNoteの長文やUI座標情報などのノイズで
|
||||
// 文字数制限に達し、肝心のノード構成情報(サブグラフ内含む)が切り捨てられやすい。
|
||||
function _buildStructuredSummaryText(analysis) {
|
||||
const lines = [];
|
||||
|
||||
if (analysis.checkpoint_nodes.length) {
|
||||
lines.push(`Checkpoint: ${analysis.checkpoint_nodes.map((n) => n.ckpt_name).filter(Boolean).join(", ")}`);
|
||||
}
|
||||
if (analysis.diffusion_model_nodes.length) {
|
||||
lines.push(`Diffusion Model (UNET): ${analysis.diffusion_model_nodes.map((n) => n.unet_name).filter(Boolean).join(", ")}`);
|
||||
}
|
||||
if (analysis.text_encoder_nodes.length) {
|
||||
lines.push(`Text Encoder (CLIP): ${analysis.text_encoder_nodes.map((n) => [n.clip_name1, n.clip_name2].filter(Boolean).join("+")).join(", ")}`);
|
||||
}
|
||||
if (analysis.vae_nodes.length) {
|
||||
lines.push(`VAE: ${analysis.vae_nodes.map((n) => n.vae_name).filter(Boolean).join(", ")}`);
|
||||
}
|
||||
if (analysis.lora_nodes.length) {
|
||||
lines.push(`LoRA: ${analysis.lora_nodes.map((n) => {
|
||||
if (n.is_lora_manager) return "(LoraManager)";
|
||||
const strengths = n.strength_clip !== undefined
|
||||
? `model:${n.strength_model}, clip:${n.strength_clip}`
|
||||
: `model:${n.strength_model}`;
|
||||
return `${n.lora_name} (${strengths})`;
|
||||
}).join(", ")}`);
|
||||
}
|
||||
if (analysis.controlnet_nodes.length) {
|
||||
lines.push(`ControlNet: ${analysis.controlnet_nodes.map((n) => n.control_net_name).filter(Boolean).join(", ")}`);
|
||||
}
|
||||
if (analysis.hypernetwork_nodes.length) {
|
||||
lines.push(`Hypernetwork: ${analysis.hypernetwork_nodes.map((n) => n.hypernetwork_name).filter(Boolean).join(", ")}`);
|
||||
}
|
||||
|
||||
const positives = analysis.prompt_nodes.filter((n) => n.role === "positive").map((n) => n.text).filter(Boolean);
|
||||
const negatives = analysis.prompt_nodes.filter((n) => n.role === "negative").map((n) => n.text).filter(Boolean);
|
||||
if (positives.length) lines.push(`Positive Prompt: ${positives.join(" / ")}`);
|
||||
if (negatives.length) lines.push(`Negative Prompt: ${negatives.join(" / ")}`);
|
||||
|
||||
if (analysis.sampler_nodes.length) {
|
||||
const s = analysis.sampler_nodes[0];
|
||||
lines.push(`Sampler: steps=${s.steps}, cfg=${s.cfg}, sampler=${s.sampler_name}, scheduler=${s.scheduler}, denoise=${s.denoise}`);
|
||||
}
|
||||
if (analysis.latent_nodes.length) {
|
||||
const l = analysis.latent_nodes[0];
|
||||
lines.push(`Latent size: ${l.width ?? "?"}x${l.height ?? "?"}, batch=${l.batch_size ?? "?"}`);
|
||||
}
|
||||
|
||||
if (analysis.all_nodes.length) {
|
||||
const nodeTypes = [...new Set(analysis.all_nodes.map((n) => n.type))];
|
||||
lines.push(`Node types used (${analysis.all_nodes.length} nodes total): ${nodeTypes.join(", ")}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ワークフロー(UI/API/サブグラフ入りいずれの形式でも)から要約用テキストを組み立てる。
|
||||
// 変換・解析に失敗した場合は、従来通り生JSON先頭部分にフォールバックする。
|
||||
async function _buildSummarySourceText(wfJson, filename) {
|
||||
try {
|
||||
const fmt = comfyWorkflow.detectFormat(wfJson, filename);
|
||||
let apiWf = wfJson;
|
||||
if (fmt === "ui") {
|
||||
apiWf = await comfyWorkflow.convertUiToApi(wfJson);
|
||||
} else if (fmt !== "api") {
|
||||
return JSON.stringify(wfJson).substring(0, 4000);
|
||||
}
|
||||
const analysis = comfyWorkflow.analyzeWorkflow(apiWf);
|
||||
const text = _buildStructuredSummaryText(analysis);
|
||||
return text || JSON.stringify(wfJson).substring(0, 4000);
|
||||
} catch {
|
||||
return JSON.stringify(wfJson).substring(0, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Rendering Helpers
|
||||
// ============================================
|
||||
@@ -1128,7 +1207,8 @@ function openDetailModal(wf) {
|
||||
btn.textContent = t("summarizing");
|
||||
try {
|
||||
const wfJson = await getRawWorkflow(wf.filename);
|
||||
const promptText = getSummaryPrompt() + JSON.stringify(wfJson).substring(0, 4000);
|
||||
const sourceText = await _buildSummarySourceText(wfJson, wf.filename);
|
||||
const promptText = getSummaryPrompt() + sourceText;
|
||||
const aiCfg = readJsonStorage("wfm_prompt_ai_settings", {});
|
||||
if (aiCfg.backend && aiCfg.backend !== "ollama") {
|
||||
throw new Error(t("summarizeOllamaOnly"));
|
||||
|
||||
+539
-57
@@ -9,6 +9,7 @@
|
||||
<link rel="stylesheet" href="/wfm_static/css/gallery-tab.css">
|
||||
<link rel="stylesheet" href="/wfm_static/css/tagger-tab.css">
|
||||
<link rel="stylesheet" href="/wfm_static/css/image-edit-tab.css">
|
||||
<link rel="stylesheet" href="/wfm_static/css/lab-tab.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
@@ -310,6 +311,7 @@
|
||||
<button class="wfm-gen-subtab-btn" data-subtab="settings">Settings</button>
|
||||
<button class="wfm-gen-subtab-btn" data-subtab="feeder">Feeder</button>
|
||||
<button class="wfm-gen-subtab-btn" data-subtab="batch">Batch</button>
|
||||
<button class="wfm-gen-subtab-btn" data-subtab="lab">Lab</button>
|
||||
<button id="wfm-gen-save-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary" style="margin-left:auto;" title="Save current workflow to Workflow tab">Save</button>
|
||||
</div>
|
||||
|
||||
@@ -501,6 +503,163 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Lab — experimental I2I batch generation with per-column keyframes -->
|
||||
<div id="wfm-gen-subtab-lab" class="wfm-gen-subtab-content">
|
||||
<div class="wfm-lab-subtab-nav">
|
||||
<button class="wfm-lab-subtab-btn active" data-lab-subtab="setting">Setting</button>
|
||||
<button class="wfm-lab-subtab-btn" data-lab-subtab="results">Results</button>
|
||||
<button class="wfm-lab-subtab-btn" data-lab-subtab="planjson">Plan JSON</button>
|
||||
</div>
|
||||
|
||||
<!-- Setting panel -->
|
||||
<div id="wfm-lab-panel-setting" class="wfm-lab-panel active">
|
||||
<div class="wfm-lab-setting-layout">
|
||||
<!-- Left: source image + plan load/clear -->
|
||||
<div class="wfm-lab-pane wfm-lab-left">
|
||||
<div class="wfm-lab-pane-header">Image</div>
|
||||
<label class="wfm-lab-checkbox-label">
|
||||
<input type="checkbox" id="wfm-lab-t2i-mode">
|
||||
T2I workflow (no source image)
|
||||
</label>
|
||||
<div class="wfm-i2i-preview-wrap" id="wfm-lab-preview-wrap" style="display:none;">
|
||||
<img class="wfm-i2i-preview-img" id="wfm-lab-preview-img" alt="">
|
||||
</div>
|
||||
<div class="wfm-i2i-drop-zone" id="wfm-lab-drop-zone">
|
||||
<label class="wfm-i2i-drop-label">
|
||||
<input type="file" id="wfm-lab-file-input" accept="image/*" style="display:none;">
|
||||
Drop image here or click to select
|
||||
</label>
|
||||
</div>
|
||||
<label class="wfm-lab-checkbox-label">
|
||||
<input type="checkbox" id="wfm-lab-chain-image">
|
||||
Use generated image for next
|
||||
</label>
|
||||
|
||||
<div class="wfm-lab-pane-header" style="margin-top:10px;">Plan</div>
|
||||
<div class="wfm-i2i-drop-zone" id="wfm-lab-plan-drop-zone">
|
||||
<label class="wfm-i2i-drop-label">
|
||||
<input type="file" id="wfm-lab-plan-file-input" accept="application/json,.json,image/png,.png" style="display:none;">
|
||||
Drop plan (.json/.png) here or click to select
|
||||
</label>
|
||||
</div>
|
||||
<div class="wfm-lab-plan-workflow-row" id="wfm-lab-plan-workflow-row" style="display:none;">
|
||||
<span id="wfm-lab-plan-workflow-name" class="wfm-lab-plan-workflow-name"></span>
|
||||
<button type="button" id="wfm-lab-plan-workflow-load-btn" class="wfm-btn wfm-btn-xs" title="Load this plan's recorded workflow into GenerateUI">Open Workflow</button>
|
||||
</div>
|
||||
<div class="wfm-lab-side-btns">
|
||||
<button id="wfm-lab-plan-clear-btn" class="wfm-btn">Plan Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: 4 keyframe columns -->
|
||||
<div class="wfm-lab-pane wfm-lab-center">
|
||||
<div class="wfm-lab-columns">
|
||||
<div class="wfm-lab-column" data-col="checkpoint">
|
||||
<div class="wfm-lab-col-header">
|
||||
<span>checkpoint</span>
|
||||
<div class="wfm-lab-col-btns">
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-add" data-col="checkpoint" title="Add keyframe">+</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-remove" data-col="checkpoint" title="Remove last keyframe">-</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-lab-col-cells" id="wfm-lab-col-cells-checkpoint"></div>
|
||||
</div>
|
||||
<div class="wfm-lab-column" data-col="vae">
|
||||
<div class="wfm-lab-col-header">
|
||||
<span>vae</span>
|
||||
<div class="wfm-lab-col-btns">
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-add" data-col="vae" title="Add keyframe">+</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-remove" data-col="vae" title="Remove last keyframe">-</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-lab-col-cells" id="wfm-lab-col-cells-vae"></div>
|
||||
</div>
|
||||
<div class="wfm-lab-column" data-col="prompt">
|
||||
<div class="wfm-lab-col-header">
|
||||
<span>プロンプト</span>
|
||||
<div class="wfm-lab-col-btns">
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-add" data-col="prompt" title="Add keyframe">+</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-remove" data-col="prompt" title="Remove last keyframe">-</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-lab-col-cells" id="wfm-lab-col-cells-prompt"></div>
|
||||
</div>
|
||||
<div class="wfm-lab-column" data-col="ksampler">
|
||||
<div class="wfm-lab-col-header">
|
||||
<span>Ksampler</span>
|
||||
<div class="wfm-lab-col-btns">
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-add" data-col="ksampler" title="Add keyframe">+</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-remove" data-col="ksampler" title="Remove last keyframe">-</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-lab-col-cells" id="wfm-lab-col-cells-ksampler"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: run controls -->
|
||||
<div class="wfm-lab-pane wfm-lab-right">
|
||||
<div style="display:flex;gap:6px;">
|
||||
<button id="wfm-lab-run-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">Run</button>
|
||||
<button id="wfm-lab-cancel-btn" class="wfm-btn wfm-btn-danger" style="display:none;">Cancel</button>
|
||||
</div>
|
||||
<button id="wfm-lab-pause-btn" class="wfm-btn" style="width:100%;margin-top:6px;" disabled>Pause</button>
|
||||
|
||||
<div class="wfm-gen-progress-container" style="margin-top:8px;">
|
||||
<div class="wfm-gen-progress-track">
|
||||
<div id="wfm-lab-progress-bar" class="wfm-gen-progress-bar"></div>
|
||||
</div>
|
||||
<span id="wfm-lab-progress-text" class="wfm-gen-progress-text">Ready</span>
|
||||
</div>
|
||||
|
||||
<div class="wfm-lab-field-row">
|
||||
<label>Batch:</label>
|
||||
<input type="number" id="wfm-lab-batch-count" class="wfm-input" value="4" min="1" max="50">
|
||||
</div>
|
||||
<label class="wfm-lab-checkbox-label" style="margin-top:6px;">
|
||||
<input type="checkbox" id="wfm-lab-save-index-on-run">
|
||||
Save index image to Output on Run
|
||||
</label>
|
||||
|
||||
<div class="wfm-lab-pane-header" style="margin-top:12px;">Note</div>
|
||||
<textarea id="wfm-lab-note" class="wfm-input wfm-lab-note" rows="6"></textarea>
|
||||
|
||||
<button id="wfm-lab-plan-save-btn" class="wfm-btn wfm-btn-primary" style="width:100%;margin-top:8px;">Plan Save</button>
|
||||
<button id="wfm-lab-plan-saveas-btn" class="wfm-btn" style="width:100%;margin-top:6px;">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results panel -->
|
||||
<div id="wfm-lab-panel-results" class="wfm-lab-panel">
|
||||
<div class="wfm-lab-results-layout">
|
||||
<div class="wfm-lab-pane wfm-lab-results-left">
|
||||
<div class="wfm-lab-pane-header">Image</div>
|
||||
<img id="wfm-lab-results-source-img" class="wfm-i2i-preview-img" style="display:none;" alt="">
|
||||
<p id="wfm-lab-results-t2i-note" class="wfm-placeholder" style="display:none;">T2I workflow (no source image)</p>
|
||||
</div>
|
||||
<div class="wfm-lab-pane wfm-lab-results-center">
|
||||
<div class="wfm-lab-pane-header">Results</div>
|
||||
<div id="wfm-lab-results-grid" class="wfm-lab-results-grid">
|
||||
<p class="wfm-placeholder">No results yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plan JSON panel -->
|
||||
<div id="wfm-lab-panel-planjson" class="wfm-lab-panel">
|
||||
<div class="wfm-lab-planjson-toolbar">
|
||||
<button type="button" id="wfm-lab-planjson-refresh" class="wfm-btn wfm-btn-sm">Refresh</button>
|
||||
<button type="button" id="wfm-lab-planjson-apply" class="wfm-btn wfm-btn-primary wfm-btn-sm">Apply to Setting</button>
|
||||
</div>
|
||||
<div class="wfm-json-container">
|
||||
<pre id="wfm-lab-planjson-highlight" class="wfm-json-highlight"></pre>
|
||||
<textarea id="wfm-lab-planjson-editor" class="wfm-json-editor" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Feeder — full-width: settings(left) + library(center) -->
|
||||
<div id="wfm-gen-subtab-feeder" class="wfm-gen-subtab-content">
|
||||
<div class="wfm-feeder-layout">
|
||||
@@ -1036,6 +1195,7 @@
|
||||
<button class="wfm-help-nav-item" data-help-page="workflow">Workflow Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="generate">GenerateUI Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="feeder">Feeder subtab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="lab">Lab subtab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="prompt">Prompt Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="metadata">Metadata Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="settings">Settings Tab</button>
|
||||
@@ -1057,8 +1217,11 @@
|
||||
<div class="wfm-help-content">
|
||||
|
||||
<div class="wfm-help-page active" id="wfm-help-page-about">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-about-title">About</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-about-title">About</h3>
|
||||
<h3 id="wfm-help-about-card-getting-started-title">Getting Started</h3>
|
||||
<p id="wfm-help-about-desc">A centralized control UI for managing ComfyUI workflow JSON files, generating images via API, assisting with prompt creation, and browsing/organizing nodes.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-about-1">Install via ComfyUI Manager or git clone to custom_nodes</li>
|
||||
@@ -1068,53 +1231,94 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-workflow">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-wf-title">Workflow Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="workflow">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Workflow Tab</h3>
|
||||
<h3 id="wfm-help-wf-card-browse-title">Browsing & Filtering</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-wf-1">Switch between Thumbnail, Card, and Table view modes</li>
|
||||
<li id="wfm-help-wf-2">Search, category filter, favorites filter, and group filter</li>
|
||||
<li id="wfm-help-wf-3">Create, rename, delete groups and assign/remove workflows</li>
|
||||
<li id="wfm-help-wf-4">Import JSON/PNG files via button or drag & drop</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-wf-card-groups-title">Groups & Badges</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-wf-3">Create, rename, delete groups and assign/remove workflows</li>
|
||||
<li id="wfm-help-wf-7">Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-wf-card-panel-title">Side Panel & Detail Modal</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-wf-5">Side panel: thumbnail, JSON viewer (format badge: UI/API/App), clipboard copy, group management</li>
|
||||
<li id="wfm-help-wf-6">Detail modal: rename, favorite, thumbnail change, badge assignment, tags, memo, AI summary, send to canvas, load in GenerateUI, set as default, delete</li>
|
||||
<li id="wfm-help-wf-7">Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-generate">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-gen-title">GenerateUI Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="generate">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>GenerateUI Tab</h3>
|
||||
<h3 id="wfm-help-gen-card-connect-title">Connecting & Loading Workflows</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-1">Connect/disconnect ComfyUI and check connection status</li>
|
||||
<li id="wfm-help-gen-2">Load workflows from Workflow tab (App format .app.json not supported); Reset Workflow button re-reads the current workflow file from disk and reloads the editor; <b>Save</b> button (right end of the tab bar) saves the current workflow — including any edits made in the editor — as a new .json file to the Workflow tab; a filename dialog appears with the current workflow name as the default; <b>Style</b> checkbox and dropdown (next to Reset Workflow): enable to apply a Fooocus-style JSON style to the positive and negative prompts at generation time — style files are loaded from <code>user/default/Workflow-Studio/style/*.json</code>; the style's <code>{prompt}</code> placeholder is replaced with the original prompt, or the style text is appended if no placeholder exists</li>
|
||||
<li id="wfm-help-gen-2">Load workflows from Workflow tab (subgraph-containing workflows are supported, e.g. Flux.2 / Qwen-Image / Z-Image / Mage-Flow templates; only ComfyUI's simplified App format [.app.json / Linear Mode] is not); Reset Workflow button re-reads the current workflow file from disk and reloads the editor; <b>Save</b> button (right end of the tab bar) saves the current workflow — including any edits made in the editor — as a new .json file to the Workflow tab; a filename dialog appears with the current workflow name as the default; <b>Style</b> checkbox and dropdown (next to Reset Workflow): enable to apply a Fooocus-style JSON style to the positive and negative prompts at generation time — style files are loaded from <code>user/default/Workflow-Studio/style/*.json</code>; the style's <code>{prompt}</code> placeholder is replaced with the original prompt, or the style text is appended if no placeholder exists</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-tabs-title">Tab Layout</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-3">5-tab layout — Input (Prompt + Image), Model, Settings, Feeder, Batch — Input/Model/Settings each include a Raw JSON column for instant preview and direct editing</li>
|
||||
<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; 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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-seed-title">Seed & Generation</h3>
|
||||
<ul>
|
||||
<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>
|
||||
<li id="wfm-help-gen-10">Eagle integration: auto-save generated images to Eagle (SVG output supported too — both the standard SaveSVGNode and comfyui-tosvg's Save SVG String node)</li>
|
||||
<li id="wfm-help-gen-18">Alt+Click any Apply button (Positive Prompt / Checkpoint / KSampler) to Apply & Generate in one step — applies the parameter to the workflow then immediately queues a generation; only fires when the target node exists in the loaded workflow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-batchsetup-title">Batch Tab — Setup</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-11">Batch tab — enable one batch type at a time by checking the checkbox in the Batch Queue column header; Generate then runs through all queued items sequentially with Pause/Resume and Stop controls</li>
|
||||
<li id="wfm-help-gen-12">Batch tab — Left pane has 4 tabs: <b>Checkpoints</b> (folder tree with Filter/All/None; (root) header checkbox selects all), <b>Sampler</b> (KSampler sampler_name list with (root) checkbox), <b>Scheduler</b> (KSampler scheduler list with (root) checkbox), <b>Style</b> (flat list of all styles loaded from <code>Workflow-Studio/style/</code> with All/None buttons); All/None toolbar buttons available for each tab</li>
|
||||
<li id="wfm-help-gen-13">Batch tab — Center pane: group-based selection with inner tabs (Checkpoint | Lora | Prompt | Workflow); Checkpoint tab shows groups defined in the Models tab — check a group header to select all its members, or expand ▶ to check individual models; group membership always reflects the current Models tab state</li>
|
||||
<li id="wfm-help-gen-14">Batch tab — Right pane (Batch Queue): 7 columns — Checkpoint, Lora, Prompt, Workflow, Sampler, Scheduler, <b>Style</b>; each column has an enable checkbox; only one type active at a time; count shown per column</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gen-card-batchtypes-title">Batch Tab — Batch Types</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gen-15">Sampler/Scheduler batch: select one or more samplers or schedulers from the left pane, enable the column in Batch Queue, then Generate to run each value through the KSampler node sequentially; sampler/scheduler lists are fetched from ComfyUI (requires connection)</li>
|
||||
<li id="wfm-help-gen-17">Style batch: select styles in the left pane Style tab, enable the Style column in Batch Queue, then Generate — each selected style is applied in sequence to the positive and negative prompts; styles are applied to workflow copies so the loaded workflow is not modified; style files are re-read each time the page loads from <code>user/default/Workflow-Studio/style/*.json</code></li>
|
||||
<li id="wfm-help-gen-16">SPA-side wildcard expansion (A1111-style): when prompt text in non-ImpactWildcard nodes (e.g. CLIPTextEncode) contains __name__ syntax, the SPA expands it at generation time by randomly picking a line from the matching file in the WFS wildcard folder; nested wildcards are re-expanded up to 5 passes; unresolved wildcards are passed through unchanged; ImpactWildcardEncode/Processor nodes are skipped (handled server-side by Impact Pack); works in both single and batch generation</li>
|
||||
<li id="wfm-help-gen-18">Alt+Click any Apply button (Positive Prompt / Checkpoint / KSampler) to Apply & Generate in one step — applies the parameter to the workflow then immediately queues a generation; only fires when the target node exists in the loaded workflow</li>
|
||||
<li id="wfm-help-gen-19">Lora batch: for each LoRA, its syntax (<lora:name:strength_model:strength_clip>) and CivitAI trained words are automatically appended to the Positive prompt — rebuilt from the original prompt each time, not accumulated; the prompt is automatically restored to its pre-batch state when the batch completes or is stopped; if a selected LoRA is not yet recognized by ComfyUI's model list, a warning toast is shown (Refresh or restart ComfyUI)</li>
|
||||
<li id="wfm-help-gen-16">SPA-side wildcard expansion (A1111-style): when prompt text in non-ImpactWildcard nodes (e.g. CLIPTextEncode) contains __name__ syntax, the SPA expands it at generation time by randomly picking a line from the matching file in the WFS wildcard folder; nested wildcards are re-expanded up to 5 passes; unresolved wildcards are passed through unchanged; ImpactWildcardEncode/Processor nodes are skipped (handled server-side by Impact Pack); works in both single and batch generation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-feeder">
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-feeder-title">GenerateUI Tab — Feeder subtab</h3>
|
||||
<p id="wfm-help-feeder-desc">Two modes switchable via the [Image Loop] / [Gallery] toggle at the top of the left pane.</p>
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-feeder-title">GenerateUI Tab — Feeder subtab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="generate">Open tab →</button>
|
||||
</div>
|
||||
<p id="wfm-help-feeder-desc" style="margin-bottom:12px;">Two modes switchable via the [Image Loop] / [Gallery] toggle at the top of the left pane.</p>
|
||||
|
||||
<h4 id="wfm-help-feeder-imgloop-title" style="margin:10px 0 4px;color:var(--wfm-accent)">Image Loop mode</h4>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-feeder-imgloop-title">Image Loop mode</h3>
|
||||
<p id="wfm-help-feeder-imgloop-desc">Requires the comfyui-image-feeder custom node. Controls ImageFeeder nodes and runs continuous generation loops over an image library stored in user/default/image-loop-data/.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-feeder-1">Left pane — Node settings: select the target ImageFeeder node from the dropdown; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection); click Apply to write changes to the loaded workflow</li>
|
||||
@@ -1127,8 +1331,10 @@
|
||||
<li id="wfm-help-feeder-8">Seed during Run: uses the right-pane seed setting (Random / Fixed / Increment / Decrement) — the ImageFeeder's own Seed field controls random sort order only</li>
|
||||
<li id="wfm-help-feeder-9">Index sync: after each generation the node returns the next index via WebSocket; the Index field updates automatically so the display stays in sync</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 id="wfm-help-feeder-gal-title" style="margin:14px 0 4px;color:var(--wfm-accent)">Gallery mode</h4>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-feeder-gal-title">Gallery mode</h3>
|
||||
<p id="wfm-help-feeder-gal-desc">Uses the built-in WFS_GalleryFeeder node (no external plugin required). Feeds images from a Gallery group into a continuous generation loop.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-feeder-gal-1">Left pane — Node: select a WFS_GalleryFeeder node from the dropdown and click Apply to link it to the workflow; select the source group (defaults to <b>__Feeder__</b>), Sort Mode, Index (current image position), and Seed</li>
|
||||
@@ -1141,17 +1347,80 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-prompt">
|
||||
<div class="wfm-help-page" id="wfm-help-page-lab">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-lab-title">GenerateUI Tab — Lab subtab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="generate">Open tab →</button>
|
||||
</div>
|
||||
<p id="wfm-help-lab-desc" style="margin-bottom:12px;">Experimental I2I batch generation. Runs the workflow currently loaded in GenerateUI N times, letting Checkpoint / VAE / Prompt / KSampler each change independently starting at a chosen iteration. Two subtabs: Setting and Results.</p>
|
||||
|
||||
<div class="wfm-help-card">
|
||||
<h3>Prompt Tab</h3>
|
||||
<h3 id="wfm-help-lab-card-columns-title">Setting subtab — Image & Columns</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-lab-1">Left pane — Image: drop or click to select an I2I source image; it is uploaded to ComfyUI's input folder and used for every iteration unless "Use generated image for next" is checked</li>
|
||||
<li id="wfm-help-lab-2">Left pane — Use generated image for next: from iteration 2 onward, feeds the previous iteration's first output image back in as the I2I source instead of the original dropped image (iteration 1 always uses the original)</li>
|
||||
<li id="wfm-help-lab-3">Center pane — 4 independent columns: Checkpoint, VAE, Prompt, KSampler; each column keeps its own list of keyframes rather than sharing a row grid</li>
|
||||
<li id="wfm-help-lab-4">+ / − per column: + appends a new keyframe at the next free iteration number (up to the Batch count) and opens its edit modal immediately; − removes the last keyframe (iteration #1 can never be removed)</li>
|
||||
<li id="wfm-help-lab-5">Click a cell to edit it: iteration #1 sets the baseline value (leave it blank to keep the loaded workflow's own setting); iteration #2+ also lets you choose which iteration it starts applying from, plus a "revert to #1's setting" checkbox</li>
|
||||
<li id="wfm-help-lab-6">Effective value at iteration N: the latest keyframe with iteration ≤ N is used and carries forward until the next keyframe changes it; "revert to #1" reuses iteration #1's value (or the workflow default if #1 is blank) instead of specifying a new one</li>
|
||||
<li id="wfm-help-lab-7">Lab never edits the workflow shown in Input/Model/Settings — every iteration runs on a fresh clone of the loaded workflow, so keyframes here never affect the other subtabs</li>
|
||||
<li id="wfm-help-lab-8">Prompt cell editor extras: <b>Get from GenerateUI</b> fills Positive/Negative from the workflow currently loaded in GenerateUI; <b>Get from Image</b> extracts a prompt from the image currently loaded in Lab's own Image drop zone (same detection as the Metadata tab; no separate file picker); <b>Style</b> appends whichever style is currently selected in the Style checkbox/dropdown at the top of the GenerateUI toolbar to the Positive/Negative text below, comma-separated after any existing text (never clears it) — enable that checkbox and pick a style first, Lab doesn't have its own separate style picker</li>
|
||||
<li id="wfm-help-lab-9">Left pane — <b>T2I workflow (no source image)</b>: check this when the loaded workflow is text-to-image (no LoadImage node). It skips the "drop a source image first" requirement, ignores any dropped image and the Checkpoint/VAE/Prompt/KSampler keyframes still apply normally; the Image drop zone and "Use generated image for next" are greyed out while it's on (any previously dropped image is kept, not deleted, in case you switch back to I2I); the setting is saved with the plan as <code>t2i_mode</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-lab-card-plan-title">Setting subtab — Run & Plan files</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-lab-plan-1">Right pane — Batch: number of iterations to run; Note: free-text memo saved together with the plan</li>
|
||||
<li id="wfm-help-lab-plan-2">Run / Pause / Cancel: Run starts the batch loop; Pause/Resume freezes it between iterations; Cancel interrupts the current generation and stops the loop</li>
|
||||
<li id="wfm-help-lab-plan-3">Plan Save vs Save As: Plan Save silently overwrites the currently loaded plan (or asks for a name the first time); Save As always asks for a new name, even while a plan is already loaded</li>
|
||||
<li id="wfm-help-lab-plan-4">Plan Load: drop a plan's .json file, or any of its index-image PNGs, onto the PLAN box (or click it to browse) — a dropped .json is read directly; a dropped .png is read directly too if it carries embedded plan data (every index image since v0.3.90 does, in a <code>wfm_lab_plan</code> chunk — see below), falling back to fetching a same-named .json from the server for older index images that predate this</li>
|
||||
<li id="wfm-help-lab-plan-5">Plan Clear resets the Setting tab to an empty plan (asks for confirmation first)</li>
|
||||
<li id="wfm-help-lab-plan-6">Plan files are stored in <code>user/default/Workflow-Studio/lab_plan/</code> as <code>ws_labplan_<name>.json</code> plus a <code>ws_labplan_<name>.png</code> index image — a contact sheet of up to 9 result thumbnails (3 per row) generated automatically whenever a plan with results is saved. The <code>ws_labplan_</code> prefix is added automatically by Plan Save/Save As (type just the plain name when prompted) so plan files are easy to pick out among other files in that folder</li>
|
||||
<li id="wfm-help-lab-plan-7">Eagle auto-save (configured in the Settings tab) applies to every image Lab generates, the same as GenerateUI's own Generate button</li>
|
||||
<li id="wfm-help-lab-plan-8">Plan JSON subtab: shows the current Setting tab's plan as raw JSON; <b>Refresh</b> re-reads it from the Setting tab (discarding any unapplied text edits), <b>Apply to Setting</b> parses your edits and writes them back into the Setting tab (columns, note, batch count, chain-image, source image, results) — the same effect as loading that JSON as a plan file, without needing to save it first</li>
|
||||
<li id="wfm-help-lab-plan-9">Each saved plan records the filename of the workflow that was loaded in GenerateUI at save time (<code>workflow_filename</code> in the Plan JSON). After loading a plan, an <b>Open Workflow</b> button appears in the PLAN box — click it to load that exact workflow file back into GenerateUI (asks for confirmation, since it replaces whatever is currently loaded)</li>
|
||||
<li id="wfm-help-lab-plan-10">Right pane — <b>Save index image to Output on Run</b>: when checked, every time Run finishes it builds the same contact-sheet index image used for Plan files and saves it directly into ComfyUI's own Output folder as <code>Lab_index_<counter>_.png</code> (auto-numbered like a normal generated image, never overwriting a previous run's). Once saved, it also goes through Eagle auto-save (if enabled in the Settings tab) just like every other Lab-generated image — no separate checkbox for that, it follows the same global toggle. This is independent of Plan Save, which still always writes its own index image next to the plan file in <code>lab_plan/</code> regardless of this checkbox; this setting is saved with the plan as <code>save_index_on_run</code></li>
|
||||
<li id="wfm-help-lab-plan-11">Every index image PNG — whether from Plan Save/Save As or from <b>Save index image to Output on Run</b> — carries the entire plan (columns, note, batch count, chain-image/T2I settings, workflow filename, results) embedded in it directly as a PNG <code>wfm_lab_plan</code> chunk, so the Output-folder copy is self-contained and works as a Plan Load source too even though it has no matching .json next to it anywhere</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-lab-card-results-title">Results subtab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-lab-results-1">Left pane shows the source image used for the run</li>
|
||||
<li id="wfm-help-lab-results-2">Center pane shows up to 9 generated images in generation order; click a thumbnail to view it enlarged</li>
|
||||
<li id="wfm-help-lab-results-3">Loading a saved plan restores its Results grid from the image list recorded in the plan file</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-prompt">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-prompt-title">Prompt Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="prompt">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-ai-title">AI Assistant & Translation</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-1">AI assistant (Ollama): model selection, chat for prompt consultation, image attachment (vision models)</li>
|
||||
<li id="wfm-help-prompt-2">Apply AI responses to GenerateUI prompts</li>
|
||||
<li id="wfm-help-prompt-3">Translation: JA↔EN, ZH↔EN</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-presets-title">Presets</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-4">Preset management: create, edit, positive/negative prompt clipboard copy (PP/NP)</li>
|
||||
<li id="wfm-help-prompt-5">Preset Manager: browse all presets, favorites, and group-based filtering</li>
|
||||
<li id="wfm-help-prompt-6">Group management: create groups, assign/remove presets, delete groups</li>
|
||||
<li id="wfm-help-prompt-7">GenUI Set: apply preset prompts directly to GenerateUI</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-wildcards-title">Wildcards</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-8">Wildcard toolbar: one-click buttons to insert wildcard syntax ({|}, {n$|}, __|__, <lora:>, etc.); wraps selected text</li>
|
||||
<li id="wfm-help-prompt-9">Wildcard file manager: create/edit/delete .txt/.yaml wildcard files; click filename to insert __filename__ at cursor</li>
|
||||
</ul>
|
||||
@@ -1159,71 +1428,140 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-metadata">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-metadata-title">Metadata Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="metadata">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-title">Metadata Tab</h3>
|
||||
<h3 id="wfm-help-metadata-card-overview-title">Overview</h3>
|
||||
<p id="wfm-help-metadata-desc">Drop a ComfyUI-generated PNG/WebP or workflow JSON to instantly extract and view the embedded model and prompt information.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-metadata-1">Left column: drop zone (image preview for PNG/WebP) and file info; click or drop to load</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-card-extracted-title">Extracted Information</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-metadata-2">Center column: Checkpoint, VAE, Diffusion Model, and Text Encoder lists extracted from the workflow</li>
|
||||
<li id="wfm-help-metadata-3">Right column: LoRA list (with strength values) and Prompt list (click to view full text below)</li>
|
||||
<li id="wfm-help-metadata-4">Prompt actions — Copy: copy to clipboard; GenUI:P/N: set GenerateUI positive/negative prompt; Prompt:P/N: set Prompt tab preset positive/negative</li>
|
||||
<li id="wfm-help-metadata-5">Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image subgraph workflows), SD WebUI, SD Forge, Fooocus. Model extraction covers UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, and QuadrupleCLIPLoader (e.g. HiDream 4-CLIP).</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-card-formats-title">Supported Formats</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-metadata-5">Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow subgraph workflows), SD WebUI, SD Forge, Fooocus. Model extraction covers UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, and QuadrupleCLIPLoader (e.g. HiDream 4-CLIP).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-settings">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-settings-title">Settings Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="settings">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Settings Tab</h3>
|
||||
<h3 id="wfm-help-settings-card-general-title">General</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-1">UI language and summary language</li>
|
||||
<li id="wfm-help-settings-2">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)</li>
|
||||
<li id="wfm-help-settings-3">Workflow data folder path</li>
|
||||
<li id="wfm-help-settings-9">All sections are collapsible accordion panels</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-connections-title">Connections</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-4">ComfyUI connection URL and test</li>
|
||||
<li id="wfm-help-settings-5">Ollama URL, default model, and connection test</li>
|
||||
<li id="wfm-help-settings-6">Default workflow setting</li>
|
||||
<li id="wfm-help-settings-7">Eagle integration URL and auto-save toggle</li>
|
||||
<li id="wfm-help-settings-8">Data Management: export all plugin data to JSON / import to restore</li>
|
||||
<li id="wfm-help-settings-9">All sections are collapsible accordion panels</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-workflow-title">Workflow Defaults</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-3">Workflow data folder path</li>
|
||||
<li id="wfm-help-settings-6">Default workflow setting</li>
|
||||
<li id="wfm-help-settings-10">Wildcard Integration: link WFS wildcard dir to ComfyUI-Impact-Pack wildcards (junction/symlink); existing files migrated automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-editor-title">Editor Customization</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-11">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 positive/negative, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings</li>
|
||||
<li id="wfm-help-settings-12">RAW JSON Colors: customize the 6 syntax highlight colors for the Raw JSON editor in Generate UI — Default Text, Name/Scheduler, Title, Width/Height, Prompt/Text, Image/File; changes apply immediately on color pick; Reset Defaults restores the original color scheme</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-misc-title">Data & Misc</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-8">Data Management: export all plugin data to JSON / import to restore</li>
|
||||
<li id="wfm-help-settings-13">CivitAI Host: choose between civitai.com (SFW only) and civitai.red (unrestricted) — determines which site opens when clicking a model link in the Models tab CivitAI panel</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-nodes">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-nodes-title">Nodes Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="nodes">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Nodes Tab</h3>
|
||||
<h3 id="wfm-help-nodes-card-browse-title">Browse & Organize</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-nodes-1">Node Browser: search by name/description/alias/tag, filter by category, package, tag, group, favorites</li>
|
||||
<li id="wfm-help-nodes-2">Card and Table view modes with pagination</li>
|
||||
<li id="wfm-help-nodes-3">Favorite nodes and add custom tags for organization</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-nodes-card-groups-title">Groups & Sets</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-nodes-4">Create, rename, delete node groups and assign/remove nodes</li>
|
||||
<li id="wfm-help-nodes-5">Side panel: Details (class name, package, category, description, flags), I/O Spec (inputs/outputs with types and defaults), Groups management</li>
|
||||
<li id="wfm-help-nodes-6">Node Sets: create reusable node collections with descriptions, tags, and link definitions between nodes</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-nodes-card-panel-title">Side Panel & Multi-select</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-nodes-5">Side panel: Details (class name, package, category, description, flags), I/O Spec (inputs/outputs with types and defaults), Groups management</li>
|
||||
<li id="wfm-help-nodes-7">Multi-select: Ctrl+click to toggle individual nodes, Shift+click for range selection; bulk action bar appears with add to group / remove from group / create & add group / bulk favorite / unfavorite</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-gallery">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-gallery-title">Gallery Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="gallery">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-title">Gallery Tab</h3>
|
||||
<h3 id="wfm-help-gallery-card-browse-title">Browsing</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-1">Browse images in the ComfyUI output folder — set the folder path in Settings > Gallery Output Folder</li>
|
||||
<li id="wfm-help-gallery-2">3-column layout: folder tree (left) | image grid (center) | detail panel (right)</li>
|
||||
<li id="wfm-help-gallery-3">Thumbnail and Table view modes; double-click any image to open fullscreen lightbox</li>
|
||||
<li id="wfm-help-gallery-4">Toolbar filters: favorites (★), text search (filename / tag / prompt), tag filter, group filter, sort by date/name</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-folder-title">Folder & Selection</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-5">Folder tree: click to load images, expand with ▶; "+ New" creates a subfolder inside the selected folder, "Del" deletes it with all contents</li>
|
||||
<li id="wfm-help-gallery-6">Multi-select with Ctrl+click — bulk bar: select group → 'Add to Group' / 'Remove from Group'; 'Favorite All' / 'Unfavorite All'; 'Compare' (2–4 images); 'Move To...'; '<b>Export</b>' (downloads all selected images as a ZIP file); 'Delete File'</li>
|
||||
<li id="wfm-help-gallery-15"><b>F button</b> (cyan overlay, top-left of each thumbnail): toggle the image in/out of the <b>__Feeder__</b> group with a single click; active state stays visible (cyan) when the image is already in the group; use this to quickly queue images for the Feeder tab Gallery mode</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-detail-title">Detail Panel</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-7">Detail panel — Info tab: filename, size, date, tags (add/remove), memo; <b>download icon</b> (on image hover) to download the single image; "Move To..." moves the image to another folder, "Delete File" removes it permanently</li>
|
||||
<li id="wfm-help-gallery-8">Detail panel — JSON tab: displays workflow JSON from PNG embedded data (prompt / workflow keys) or from metadata saved by the Generate UI tab; 'Copy & Send Canvas' button copies to clipboard and stores the workflow for title-drag to canvas</li>
|
||||
<li id="wfm-help-gallery-9">Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image; the <b>__Feeder__</b> group is reserved (shown with 🔒) and cannot be renamed or deleted — rename and delete buttons are disabled when it is selected</li>
|
||||
<li id="wfm-help-gallery-15"><b>F button</b> (cyan overlay, top-left of each thumbnail): toggle the image in/out of the <b>__Feeder__</b> group with a single click; active state stays visible (cyan) when the image is already in the group; use this to quickly queue images for the Feeder tab Gallery mode</li>
|
||||
<li id="wfm-help-gallery-10">Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the JSON tab</li>
|
||||
<li id="wfm-help-gallery-11">Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-actions-title">Actions Toolbar</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-12"><b>Metadata</b> button (green, in the detail panel tab bar): opens the selected image in the Metadata tab for full embedded-data inspection; Alt+click on any image thumbnail has the same effect</li>
|
||||
<li id="wfm-help-gallery-13"><b>Load GenUI</b> button (blue, in the detail panel tab bar): loads the embedded ComfyUI workflow from the selected image directly into the GenerateUI tab and switches to it; shows a warning toast if no workflow is embedded or the format is not supported</li>
|
||||
<li id="wfm-help-gallery-14"><b>Tagger</b> button (purple, in the detail panel action row): opens the selected image directly in the Tagger tab's Single mode for instant tagging</li>
|
||||
@@ -1231,21 +1569,52 @@
|
||||
<li id="wfm-help-gallery-17"><b>Send GenUI Image</b> button (toolbar, next to Image Edit): uploads the selected image to ComfyUI and sets it as the input for the first LoadImage node in the GenerateUI tab; automatically switches to GenerateUI → Input → Image; requires a workflow with at least one LoadImage node to be loaded first</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-formats-title">Formats & Auto-save</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-10">Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the JSON tab</li>
|
||||
<li id="wfm-help-gallery-11">Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-models">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-models-title">Models Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="models">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3>Models Tab</h3>
|
||||
<h3 id="wfm-help-models-card-browse-title">Browsing</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-1">Browse installed models: Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding with sub-tab switching</li>
|
||||
<li id="wfm-help-models-2">Thumbnail and Table view modes with pagination; Table view shows Type and Base Model columns sourced from CivitAI data alongside filename, subdirectory, and extension; <b>column headers are clickable to sort</b> — click once for ascending (▲), again for descending (▼), a third time to clear the sort; the <b>E/D</b> column sorts by enable/disable status</li>
|
||||
<li id="wfm-help-models-3">Search, tag filter, group filter, favorites filter, and status filter (All / Enabled only / Disabled only)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-civitai-title">Side Panel & CivitAI</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-4">Side panel: opens to CivitAI tab by default when a model is selected; Info (file path, tags, memo), Groups management, CivitAI integration</li>
|
||||
<li id="wfm-help-models-5">CivitAI integration: fetch model info by SHA256 hash, batch fetch per model type</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-apply-title">Apply & Detail</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-6">Detail modal: preview image, CivitAI info (base model, trained words, tags), thumbnail change — upload any image format to replace the preview; changes take effect immediately without restarting ComfyUI; broken/empty preview files are automatically skipped; Delete button permanently removes the model file and all associated sidecar files</li>
|
||||
<li id="wfm-help-models-7">Group management: create, rename, delete groups and assign/remove models; Checkpoint and LoRA tabs support B (Batch) and S (Stack) quick-assign buttons per model in both card and table views</li>
|
||||
<li id="wfm-help-models-8">GenUI Model button: apply the selected model to the corresponding node in GenerateUI's current workflow (Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder / Hypernetwork); <b>Embedding</b> tab shows <b>GenUI PP</b> / <b>GenUI NP</b> buttons instead — appends <code>(embedding:Name:1.0)</code> to the Positive or Negative prompt of the loaded workflow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-groups-title">Groups & Badges</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-7">Group management: create, rename, delete groups and assign/remove models; Checkpoint and LoRA tabs support B (Batch) and S (Stack) quick-assign buttons per model in both card and table views</li>
|
||||
<li id="wfm-help-models-9">User-defined badges: assign free-label badges with custom colors shared with the Workflow tab</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-models-card-bulk-title">Enable/Disable & Bulk Operations</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-models-10">Enable/Disable models: toggle visibility in ComfyUI by renaming file extension (.disabled suffix). Disable per model (⏸ button on each card) or per group (Enable All / Disable All in Group panel). Changes take effect after ComfyUI refresh.</li>
|
||||
<li id="wfm-help-models-11">Multi-select & bulk operations: click Select (green) to enter selection mode, then check multiple models; the bulk action bar is organized into three rows — <b>Group row:</b> select an existing group and Add/Remove, or type a new name and Create & Add; <b>Badge row:</b> select a badge and +Badge / −Badge to apply or remove; <b>File row:</b> select a destination subfolder (or type a new folder name) and Move to relocate model files with all sidecar files (.preview.png, .json, .civitai.info, etc.), Delete Files to permanently remove selected models; header row provides Deselect All and ★/☆ Favorite toggles</li>
|
||||
<li id="wfm-help-models-12">After moving or deleting models, ComfyUI may fail to load them because it caches the old file paths. Perform a Hard Reset from the ComfyUI menu (Queue → Hard Reset) or restart ComfyUI to refresh the model cache.</li>
|
||||
@@ -1254,64 +1623,135 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-sidepanel">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-sidepanel-title">Workflow Studio Library (ComfyUI Side Panel)</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-title">Workflow Studio Library (ComfyUI Side Panel)</h3>
|
||||
<h3 id="wfm-help-sidepanel-card-tabs-title">Tabs Overview</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-1">Open from the Workflow Studio Library button in the ComfyUI toolbar</li>
|
||||
<li id="wfm-help-sidepanel-2">Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view</li>
|
||||
<li id="wfm-help-sidepanel-3">Nodes tab: All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs; Category and Package show a dropdown to filter nodes</li>
|
||||
<li id="wfm-help-sidepanel-11">Models tab: All / ★ Favorites / Groups / By Type sub-tabs</li>
|
||||
<li id="wfm-help-sidepanel-4">Prompts tab: browse prompt presets with All/Favorites/Categories sub-tabs</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-dnd-title">Drag & Drop / Interaction</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-5">Drag & drop workflows onto the canvas to load them</li>
|
||||
<li id="wfm-help-sidepanel-6">Drag & drop nodes/node sets onto the canvas to place them</li>
|
||||
<li id="wfm-help-sidepanel-7">Drag & drop prompt presets to create WFS_PromptText nodes with positive/negative prompts</li>
|
||||
<li id="wfm-help-sidepanel-8">Copy individual positive (P) or negative (N) prompts from sidebar items</li>
|
||||
<li id="wfm-help-sidepanel-9">Double-click items to load/place without dragging</li>
|
||||
<li id="wfm-help-sidepanel-10">Search within each sub-tab to quickly find items</li>
|
||||
<li id="wfm-help-sidepanel-12">⚙ Theme button (header): customize panel background, text, border colors — saved to localStorage</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-info-title">Information (I) tab</h3>
|
||||
<ul>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-ai-title">AI TOOL tab</h3>
|
||||
<ul>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-nav-title">Customization & Navigation</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-12">⚙ Theme button (header): customize panel background, text, border colors — saved to localStorage</li>
|
||||
<li id="wfm-help-sidepanel-17">Title drag (Send to Canvas): after clicking 'Send to Canvas' (Workflow tab) or 'Copy & 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>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-aitool">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-ai-title">AI TOOL Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="ai">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<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, 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 / 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>
|
||||
<h3 id="wfm-help-ai-card-layout-title">Layout</h3>
|
||||
<p id="wfm-help-ai-1">4-pane layout — Translation | Chat | TOOLS | Settings — all panes are always visible simultaneously</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-translate-title">Translation pane</h3>
|
||||
<p 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</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-chat-title">Chat pane</h3>
|
||||
<p 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)</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-svg-title">Chat pane — SVG generation</h3>
|
||||
<p id="wfm-help-ai-7">Chat pane — SVG: the LLM can also produce SVG graphics directly as text, with no ComfyUI workflow involved; ask it to draw something in SVG and, if the reply contains SVG code, it is rendered inline as a preview with a "Save to Gallery" button that writes it as a .svg file to the Gallery tab's output folder; this does not use tool calling, so it works with any model regardless of tool-calling support</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-skills-title">Chat pane — Skills</h3>
|
||||
<p id="wfm-help-ai-8">Chat pane — Skills: pick a skill from the dropdown above the message list to prepend its instructions to the conversation as a system prompt (not shown in the visible chat log); use the pencil button to open the skill manager and create, edit, or delete .md skill files, stored in user/default/Workflow-Studio/ai_skills/; a file's first line block between --- markers can define name/description shown in the dropdown; two starter skills are included by default: "SVG Icon Generator" (decomposes the subject into parts before drawing, for more recognizable SVG output) and "Skill Creator" (interviews you conversationally and drafts a new skill file, ready to save); when a reply contains a ```skill code block, a "Save as new skill" button appears below it that opens the manager with the draft pre-filled</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-skilltips-title">Writing your own skills</h3>
|
||||
<p id="wfm-help-ai-9">Tips for writing your own skills: give a clear frontmatter name/description so it reads well in the dropdown and tooltip; state the exact output format you want and, if there is a deliverable to extract later (like SVG or another skill file), wrap it in a distinctive fenced code block; write concrete, imperative instructions rather than vague ones - smaller local models follow explicit rules and a short worked example far better than general guidance; state hard constraints (forbidden content, security, length limits) explicitly rather than assuming they are implied; when in doubt, select the "Skill Creator" skill in the dropdown and describe what you want - it will interview you and draft the file for you</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-tools-title">TOOLS pane</h3>
|
||||
<p id="wfm-help-ai-4">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</p>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-ai-card-settings-title">Settings pane</h3>
|
||||
<p 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</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-tagger">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-tagger-title">Tagger Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="tagger">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-title">Tagger Tab</h3>
|
||||
<h3 id="wfm-help-tagger-card-overview-title">Overview & Setup</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-1"></li>
|
||||
<li id="wfm-help-tagger-2"></li>
|
||||
<li id="wfm-help-tagger-3"></li>
|
||||
<li id="wfm-help-tagger-4"></li>
|
||||
<li id="wfm-help-tagger-5"></li>
|
||||
<li id="wfm-help-tagger-6"></li>
|
||||
<li id="wfm-help-tagger-7"></li>
|
||||
<li id="wfm-help-tagger-8"></li>
|
||||
<li id="wfm-help-tagger-9"></li>
|
||||
<li id="wfm-help-tagger-1">Automatic image tagging using WD Tagger (ONNX), SwinV2, DeepDanbooru (.h5), and Ollama VLM — 3 sub-tabs: Single / Batch / DB</li>
|
||||
<li id="wfm-help-tagger-2">Model placement: put each model in its own subfolder under ComfyUI/models/tagger/<model-name>/; the folder must contain a .onnx file + selected_tags.csv (or .h5 + label file for DeepDanbooru)</li>
|
||||
<li id="wfm-help-tagger-9">Install dependencies into ComfyUI's embedded Python: python_embeded\python.exe -m pip install -r requirements.txt; for GPU ONNX inference install onnxruntime-gpu instead of onnxruntime; TensorFlow (for DeepDanbooru) is commented out by default due to size</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-card-single-title">Single Tab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-3">Single tab: drag & drop an image onto the preview area, or click Upload to pick a file; the Tagger button in the Gallery detail panel opens the selected image directly here</li>
|
||||
<li id="wfm-help-tagger-4">Threshold slider: minimum confidence score for general tags (default 0.35); Character Threshold: separate threshold for character: prefix tags (default 0.85)</li>
|
||||
<li id="wfm-help-tagger-5">Ollama VLM: enable the checkbox to run a vision model alongside WD Tagger; set API URL, select model (click ↻ to refresh), and customize the prompt; results from both models are merged</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-card-sending-title">Sending Results</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-6">Result tags can be sent to: (1) GenUI:P — appended to the GenerateUI tab's positive prompt textarea and applied to the loaded workflow immediately; (2) Send to Prompt — appended to the Prompt tab's positive textarea; (3) Save to Gallery — saved to Gallery image metadata (requires image opened from Gallery); (4) Write to File — embedded into JPEG EXIF (ImageDescription) or PNG tEXt (Tags key); (5) Save to DB — stored in the internal SQLite database</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-tagger-card-batch-title">Batch & DB Tabs</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-tagger-7">Batch tab: enter a folder path, configure WD Tagger and Ollama settings; output options: Save to DB (default on), Write to File (EXIF/PNG metadata), Write .txt (creates <filename>.txt alongside each image); click Start Batch to process all images in the folder; progress bar and log are updated in real time; Stop cancels after the current image</li>
|
||||
<li id="wfm-help-tagger-8">DB tab: searchable database of all tagged images; click a row to open the edit panel and modify WD Tags / VLM Tags; Save updates the record, Delete removes it; Export CSV downloads all records as a CSV file</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-imageedit">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-imageedit-title">Image Edit Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="image-edit">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-imageedit-title">Image Edit Tab</h3>
|
||||
<h3 id="wfm-help-imageedit-card-overview-title">Overview</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-imageedit-1">Layer-based image editor — compose images with multiple layers (Image, Text, Draw, Mask types) and export the composite</li>
|
||||
<li id="wfm-help-imageedit-2">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</li>
|
||||
@@ -1434,8 +1874,11 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-shortcuts">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-shortcuts-title">Keyboard Shortcuts</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-shortcuts-title">Keyboard Shortcuts</h3>
|
||||
<h3 id="wfm-help-shortcuts-card-general-title">General</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-shortcuts-1">Escape: Close modal/panel</li>
|
||||
<li id="wfm-help-shortcuts-2">Enter: Confirm rename, send chat</li>
|
||||
@@ -1459,17 +1902,31 @@
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-trouble">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-trouble-title">Troubleshooting</h2>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-trouble-title">Troubleshooting</h3>
|
||||
<h3 id="wfm-help-trouble-card-connection-title">Connection Issues</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-trouble-1">Cannot connect to ComfyUI: Check if ComfyUI can generate images, verify URL in Settings, try refreshing or restarting</li>
|
||||
<li id="wfm-help-trouble-2">Cannot connect to Ollama: Use connection test button, check if Ollama is running and models are downloaded</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-trouble-card-workflow-title">Workflow & Generation Issues</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-trouble-3">Workflows not showing: Click refresh button, check if loadable in ComfyUI, try restarting browser/ComfyUI</li>
|
||||
<li id="wfm-help-trouble-4">Image generation fails: Check if specified models exist in ComfyUI, verify connection, try restarting</li>
|
||||
<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>
|
||||
<li id="wfm-help-trouble-9">Generate runs a different workflow than the one just loaded: a Batch type checkbox (Checkpoint/LoRA/Prompt/Workflow/Sampler/Scheduler) is left ON in the Batch subtab. While any Batch type is active, the Generate button runs the batch selection instead of the currently loaded workflow. Open the Batch subtab and turn off the active batch type checkbox.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-trouble-card-tagger-title">Tagger & Model Issues</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-trouble-7">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</li>
|
||||
<li id="wfm-help-trouble-8">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.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2001,6 +2458,31 @@
|
||||
<!-- Chat pane -->
|
||||
<div class="wfm-ai-pane wfm-ai-pane-chat">
|
||||
<div class="wfm-ai-pane-header">Chat</div>
|
||||
<div class="wfm-ai-chat-skill-row">
|
||||
<select id="wfm-ai-chat-skill-select" class="wfm-select wfm-ai-chat-skill-select">
|
||||
<option value="">-- No skill --</option>
|
||||
</select>
|
||||
<button id="wfm-ai-chat-skill-manage-btn" class="wfm-btn wfm-btn-sm" title="Manage skills">✎</button>
|
||||
</div>
|
||||
<div id="wfm-ai-skill-panel" class="wfm-ai-skill-panel" style="display:none;">
|
||||
<div class="wfm-ai-skill-panel-header">
|
||||
<span class="wfm-ai-skill-panel-title">Skills</span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button id="wfm-ai-skill-new-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New</button>
|
||||
<button id="wfm-ai-skill-close-btn" class="wfm-btn wfm-btn-sm" title="Close">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wfm-ai-skill-list" class="wfm-wc-file-list"></div>
|
||||
<div id="wfm-ai-skill-editor" class="wfm-wc-editor" style="display:none;">
|
||||
<input type="text" id="wfm-ai-skill-editor-filename" class="wfm-input" placeholder="filename.md">
|
||||
<textarea id="wfm-ai-skill-editor-content" class="wfm-textarea wfm-wc-editor-ta" rows="10" placeholder="--- name: My Skill description: What this skill does --- System prompt instructions for the LLM..."></textarea>
|
||||
<div class="wfm-wc-editor-actions">
|
||||
<button id="wfm-ai-skill-editor-save-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">Save</button>
|
||||
<button id="wfm-ai-skill-editor-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" style="display:none;">Delete</button>
|
||||
<button id="wfm-ai-skill-editor-cancel-btn" class="wfm-btn wfm-btn-sm" style="margin-left:auto;">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
+850
-26
@@ -2394,6 +2394,461 @@ async function _readAllPNGTextChunks(file) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI→API変換(static/js/comfyui-workflow.js の convertUiToApi() 相当ロジックの複製)
|
||||
// web/comfyui/ は static/js/ と配信URLが異なりESモジュールをimportできないため、
|
||||
// AI backend URL解決等の既存パターンと同じくローカルに複製している。
|
||||
// サブグラフ(definitions.subgraphs)を含むワークフローは、外側の折りたたみ表示
|
||||
// ウィジェット値(実際にユーザーが編集する値)が定義テンプレート内のノードの
|
||||
// widgets_valuesへ反映されているとは限らないため、この変換を経由してから
|
||||
// モデル名・プロンプトを抽出する。ロジックを変更する場合は
|
||||
// static/js/comfyui-workflow.js の convertUiToApi()/_flattenSubgraphs() 側も
|
||||
// 同期すること。GenerateUIタブと異なりIタブは読み取り専用表示のため、
|
||||
// Checkpoint代替やBypass/Muteノードの記録機能は省いている。
|
||||
// ============================================
|
||||
|
||||
let _wfmObjectInfoCache = null;
|
||||
async function _wfmLoadObjectInfo() {
|
||||
if (_wfmObjectInfoCache) return _wfmObjectInfoCache;
|
||||
try {
|
||||
const res = await fetch("/object_info");
|
||||
if (res.ok) _wfmObjectInfoCache = await res.json();
|
||||
} catch {}
|
||||
return _wfmObjectInfoCache || {};
|
||||
}
|
||||
|
||||
function _wfmResolveBypassSource(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;
|
||||
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 _wfmResolveBypassSource(nodeById, linkMap, upstream[0], upstream[1], visited);
|
||||
}
|
||||
|
||||
function _wfmGetWidgetInputNames(objectInfo, classType) {
|
||||
const info = objectInfo[classType];
|
||||
if (!info) return [];
|
||||
const names = [];
|
||||
const required = info.input?.required || {};
|
||||
const optional = info.input?.optional || {};
|
||||
for (const [name, spec] of Object.entries(required)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
names.push(name);
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [name, spec] of Object.entries(optional)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
names.push(name);
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function _wfmGetWidgetInputTypes(objectInfo, classType) {
|
||||
const info = objectInfo[classType];
|
||||
if (!info) return [];
|
||||
const types = [];
|
||||
const required = info.input?.required || {};
|
||||
const optional = info.input?.optional || {};
|
||||
for (const [name, spec] of Object.entries(required)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
types.push("COMBO");
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
types.push(upper);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [name, spec] of Object.entries(optional)) {
|
||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||
if (Array.isArray(type)) {
|
||||
types.push("COMBO");
|
||||
} else if (typeof type === "string") {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper === "INT" || upper === "FLOAT" || upper === "STRING" || upper === "BOOLEAN" || upper === "COMBO" || upper === "COMFY_DYNAMICCOMBO_V3") {
|
||||
types.push(upper);
|
||||
}
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
function _wfmGetDynamicComboSubNames(objectInfo, classType, widgetName, selectedKey) {
|
||||
const info = objectInfo[classType];
|
||||
const spec = info?.input?.required?.[widgetName] || info?.input?.optional?.[widgetName];
|
||||
const options = spec?.[1]?.options;
|
||||
if (!Array.isArray(options)) return [];
|
||||
const opt = options.find((o) => o.key === selectedKey) || options[0];
|
||||
return Object.keys(opt?.inputs?.required || {});
|
||||
}
|
||||
|
||||
const _WFM_CONTROL_AFTER_GENERATE = new Set(["fixed", "increment", "decrement", "randomize"]);
|
||||
function _wfmIsExtraWidgetValue(val, expectedType) {
|
||||
if (expectedType === "INT" || expectedType === "FLOAT") {
|
||||
if (typeof val === "string" && _WFM_CONTROL_AFTER_GENERATE.has(val)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function _wfmFlattenSubgraphs(workflow, objectInfo) {
|
||||
const defs = workflow.definitions?.subgraphs;
|
||||
if (!defs || defs.length === 0) return workflow;
|
||||
const subgraphMap = {};
|
||||
for (const sg of defs) subgraphMap[sg.id] = sg;
|
||||
if (!workflow.nodes.some(n => subgraphMap[n.type])) return workflow;
|
||||
|
||||
const wf = JSON.parse(JSON.stringify(workflow));
|
||||
const sgDefs = {};
|
||||
for (const sg of wf.definitions?.subgraphs || []) sgDefs[sg.id] = sg;
|
||||
|
||||
let maxNodeId = 0;
|
||||
let maxLinkId = 0;
|
||||
for (const n of wf.nodes) if (n.id > maxNodeId) maxNodeId = n.id;
|
||||
for (const l of wf.links) if (l[0] > maxLinkId) maxLinkId = l[0];
|
||||
|
||||
const addedNodes = [];
|
||||
const addedLinks = [];
|
||||
const nodesToRemove = new Set();
|
||||
|
||||
for (const node of wf.nodes) {
|
||||
const sgDef = sgDefs[node.type];
|
||||
if (!sgDef) continue;
|
||||
nodesToRemove.add(node.id);
|
||||
|
||||
const nodeIdRemap = {};
|
||||
for (const iNode of sgDef.nodes) {
|
||||
nodeIdRemap[iNode.id] = `${node.id}:${iNode.id}`;
|
||||
}
|
||||
|
||||
const inputPortTargets = {};
|
||||
for (let portIdx = 0; portIdx < (sgDef.inputs || []).length; portIdx++) {
|
||||
const port = sgDef.inputs[portIdx];
|
||||
for (const linkId of (port.linkIds || [])) {
|
||||
const iLink = (sgDef.links || []).find(l => l.id === linkId);
|
||||
if (iLink && iLink.origin_id === -10) {
|
||||
inputPortTargets[portIdx] = {
|
||||
targetNodeId: nodeIdRemap[iLink.target_id],
|
||||
targetSlot: iLink.target_slot,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outputPortSources = {};
|
||||
for (let portIdx = 0; portIdx < (sgDef.outputs || []).length; portIdx++) {
|
||||
const port = sgDef.outputs[portIdx];
|
||||
for (const linkId of (port.linkIds || [])) {
|
||||
const iLink = (sgDef.links || []).find(l => l.id === linkId);
|
||||
if (iLink && iLink.target_id === -20) {
|
||||
outputPortSources[portIdx] = {
|
||||
sourceNodeId: nodeIdRemap[iLink.origin_id],
|
||||
sourceSlot: iLink.origin_slot,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redirectedTargets = new Set();
|
||||
for (const link of wf.links) {
|
||||
if (link[3] === node.id) {
|
||||
const dstSlot = link[4];
|
||||
const inputName = node.inputs?.[dstSlot]?.name;
|
||||
let portIdx = inputName
|
||||
? (sgDef.inputs || []).findIndex(p => p.name === inputName)
|
||||
: -1;
|
||||
if (portIdx === -1) portIdx = dstSlot;
|
||||
const target = inputPortTargets[portIdx];
|
||||
if (target) {
|
||||
link[3] = target.targetNodeId;
|
||||
link[4] = target.targetSlot;
|
||||
redirectedTargets.add(`${target.targetNodeId}:${target.targetSlot}`);
|
||||
}
|
||||
}
|
||||
if (link[1] === node.id) {
|
||||
const srcSlot = link[2];
|
||||
const source = outputPortSources[srcSlot];
|
||||
if (source) {
|
||||
link[1] = source.sourceNodeId;
|
||||
link[2] = source.sourceSlot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const remappedByOrigId = {};
|
||||
const normalizedNodeIds = new Set();
|
||||
for (const iNode of sgDef.nodes) {
|
||||
const remapped = JSON.parse(JSON.stringify(iNode));
|
||||
remapped.id = nodeIdRemap[iNode.id];
|
||||
|
||||
const allWidgetInputs = (remapped.inputs || []).filter((i) => i.widget);
|
||||
if (remapped.widgets_values && remapped.widgets_values.length === allWidgetInputs.length) {
|
||||
const linkedIdx = new Set();
|
||||
allWidgetInputs.forEach((inp, i) => {
|
||||
const slotIdx = remapped.inputs.indexOf(inp);
|
||||
if (redirectedTargets.has(`${remapped.id}:${slotIdx}`)) linkedIdx.add(i);
|
||||
});
|
||||
if (linkedIdx.size > 0) {
|
||||
remapped.widgets_values = remapped.widgets_values.filter((_, i) => !linkedIdx.has(i));
|
||||
normalizedNodeIds.add(remapped.id);
|
||||
}
|
||||
}
|
||||
|
||||
addedNodes.push(remapped);
|
||||
remappedByOrigId[iNode.id] = remapped;
|
||||
}
|
||||
|
||||
const outerWidgetInputs = (node.inputs || []).filter((inp) => inp.widget);
|
||||
outerWidgetInputs.forEach((inp, widgetIdx) => {
|
||||
if (inp.link != null) return;
|
||||
if (widgetIdx >= (node.widgets_values || []).length) return;
|
||||
const outerValue = node.widgets_values[widgetIdx];
|
||||
|
||||
const portIdx = (sgDef.inputs || []).findIndex((p) => p.name === inp.name);
|
||||
if (portIdx === -1) return;
|
||||
const target = inputPortTargets[portIdx];
|
||||
if (!target) return;
|
||||
|
||||
const origInternalId = String(target.targetNodeId).split(":").pop();
|
||||
const remappedNode = remappedByOrigId[origInternalId];
|
||||
if (!remappedNode) return;
|
||||
|
||||
const targetInputDef = (remappedNode.inputs || [])[target.targetSlot];
|
||||
if (!targetInputDef?.widget) return;
|
||||
|
||||
let iWidgetIdx;
|
||||
if (normalizedNodeIds.has(remappedNode.id)) {
|
||||
const remainingNames = (remappedNode.inputs || [])
|
||||
.filter((i) => i.widget)
|
||||
.filter((i) => {
|
||||
const slotIdx = remappedNode.inputs.indexOf(i);
|
||||
return !redirectedTargets.has(`${remappedNode.id}:${slotIdx}`);
|
||||
})
|
||||
.map((i) => i.name);
|
||||
iWidgetIdx = remainingNames.indexOf(targetInputDef.name);
|
||||
} else {
|
||||
const widgetNames = _wfmGetWidgetInputNames(objectInfo, remappedNode.type);
|
||||
iWidgetIdx = widgetNames.indexOf(targetInputDef.name);
|
||||
}
|
||||
if (iWidgetIdx === -1) return;
|
||||
|
||||
if (!remappedNode.widgets_values) remappedNode.widgets_values = [];
|
||||
remappedNode.widgets_values[iWidgetIdx] = outerValue;
|
||||
});
|
||||
|
||||
for (const iLink of (sgDef.links || [])) {
|
||||
if (iLink.origin_id === -10 || iLink.target_id === -20) continue;
|
||||
maxLinkId++;
|
||||
addedLinks.push([
|
||||
maxLinkId,
|
||||
nodeIdRemap[iLink.origin_id],
|
||||
iLink.origin_slot,
|
||||
nodeIdRemap[iLink.target_id],
|
||||
iLink.target_slot,
|
||||
iLink.type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
wf.nodes = [...wf.nodes.filter(n => !nodesToRemove.has(n.id)), ...addedNodes];
|
||||
wf.links = [
|
||||
...wf.links.filter(l => !nodesToRemove.has(l[1]) && !nodesToRemove.has(l[3])),
|
||||
...addedLinks,
|
||||
];
|
||||
|
||||
delete wf.definitions;
|
||||
return wf;
|
||||
}
|
||||
|
||||
function _wfmIsDisplayOnlyNode(node) {
|
||||
const hasConnectedOutput = node.outputs?.some(o => o.links && o.links.length > 0);
|
||||
const hasConnectedInput = node.inputs?.some(i => i.link != null);
|
||||
return !hasConnectedOutput && !hasConnectedInput;
|
||||
}
|
||||
|
||||
function _wfmGetWidgetMapping(nodeType) {
|
||||
const mappings = {
|
||||
CheckpointLoaderSimple: ["ckpt_name"],
|
||||
KSampler: ["seed", "steps", "cfg", "sampler_name", "scheduler", "denoise"],
|
||||
CLIPTextEncode: ["text"],
|
||||
CLIPTextEncodeEditPlus: ["text_edit", "mode"],
|
||||
ImageMetadataCheckpointLoader: ["ckpt_name", "vae_name", "_metadata_json"],
|
||||
ImageMetadataPromptLoader: ["ckpt_name", "vae_name", "positive_text", "negative_text", "_metadata_json"],
|
||||
ImageMetadataLoRALoader: ["lora_1", "strength_model_1", "strength_clip_1", "lora_2", "strength_model_2", "strength_clip_2", "lora_3", "strength_model_3", "strength_clip_3"],
|
||||
EmptyLatentImage: ["width", "height", "batch_size"],
|
||||
LoraLoader: ["lora_name", "strength_model", "strength_clip"],
|
||||
VAELoader: ["vae_name"],
|
||||
SaveImage: ["filename_prefix"],
|
||||
LoadImage: ["image", "upload"],
|
||||
UNETLoader: ["unet_name", "weight_dtype"],
|
||||
LoaderGGUF: ["gguf_name"],
|
||||
LoaderGGUFAdvanced: ["gguf_name", "dequant_dtype", "patch_dtype", "patch_on_device"],
|
||||
ResolutionSelector: ["aspect_ratio", "megapixels"],
|
||||
EmptySD3LatentImage: ["width", "height", "batch_size"],
|
||||
CLIPLoader: ["clip_name", "type", "device"],
|
||||
ClipLoaderGGUF: ["clip_name", "type"],
|
||||
DualClipLoaderGGUF: ["clip_name1", "clip_name2", "type"],
|
||||
FluxGuidance: ["guidance"],
|
||||
CFGNorm: ["strength"],
|
||||
ImpactWildcardProcessor: ["wildcard_text"],
|
||||
ImpactWildcardEncode: ["wildcard_text"],
|
||||
WFS_PromptText: ["positive", "negative"],
|
||||
};
|
||||
return mappings[nodeType] || null;
|
||||
}
|
||||
|
||||
async function _wfmConvertUiToApi(workflow) {
|
||||
if (!workflow.nodes || !workflow.links) return {};
|
||||
|
||||
const objectInfo = await _wfmLoadObjectInfo();
|
||||
const flatWorkflow = _wfmFlattenSubgraphs(workflow, objectInfo);
|
||||
|
||||
const linkMap = {};
|
||||
for (const link of flatWorkflow.links) {
|
||||
const [id, srcNode, srcSlot, dstNode, dstSlot] = link;
|
||||
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 === 2 || node.mode === 4) continue;
|
||||
if (!objectInfo[node.type] && _wfmIsDisplayOnlyNode(node)) continue;
|
||||
const nodeId = String(node.id);
|
||||
const inputs = {};
|
||||
|
||||
const nodeLinks = linkMap[node.id] || {};
|
||||
const inputDefs = node.inputs || [];
|
||||
const linkedInputNames = new Set();
|
||||
const linkedSlotNames = new Set();
|
||||
inputDefs.forEach((inp, idx) => {
|
||||
if (nodeLinks[idx]) {
|
||||
const resolved = _wfmResolveBypassSource(nodeById, linkMap, nodeLinks[idx][0], nodeLinks[idx][1], new Set());
|
||||
if (!resolved) return;
|
||||
inputs[inp.name] = resolved;
|
||||
linkedInputNames.add(inp.name);
|
||||
linkedSlotNames.add(inp.name);
|
||||
}
|
||||
});
|
||||
|
||||
const widgets = node.widgets_values || [];
|
||||
if (widgets.length > 0) {
|
||||
const lmWidgetIds = node.properties?.__lm_widget_ids;
|
||||
if (lmWidgetIds && Array.isArray(lmWidgetIds)) {
|
||||
lmWidgetIds.forEach((name, idx) => {
|
||||
if (idx >= widgets.length || linkedInputNames.has(name)) return;
|
||||
let val = widgets[idx];
|
||||
if (name === "loras" && Array.isArray(val)) {
|
||||
val = { "__value__": val };
|
||||
}
|
||||
inputs[name] = val;
|
||||
});
|
||||
} else {
|
||||
const widgetNames = _wfmGetWidgetInputNames(objectInfo, node.type);
|
||||
if (widgetNames.length > 0) {
|
||||
const widgetTypes = _wfmGetWidgetInputTypes(objectInfo, node.type);
|
||||
let wIdx = 0;
|
||||
for (let nIdx = 0; nIdx < widgetNames.length; nIdx++) {
|
||||
if (wIdx >= widgets.length) break;
|
||||
const name = widgetNames[nIdx];
|
||||
const expectedType = widgetTypes[nIdx];
|
||||
if (linkedInputNames.has(name)) {
|
||||
if (!linkedSlotNames.has(name)) {
|
||||
wIdx++;
|
||||
if (wIdx < widgets.length && _wfmIsExtraWidgetValue(widgets[wIdx], expectedType)) {
|
||||
wIdx++;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let val = widgets[wIdx];
|
||||
wIdx++;
|
||||
if (_wfmIsExtraWidgetValue(val, expectedType)) {
|
||||
if (wIdx < widgets.length) {
|
||||
val = widgets[wIdx];
|
||||
wIdx++;
|
||||
}
|
||||
}
|
||||
if (expectedType === "COMFY_DYNAMICCOMBO_V3") {
|
||||
inputs[name] = val;
|
||||
const subNames = _wfmGetDynamicComboSubNames(objectInfo, node.type, name, val);
|
||||
for (const subName of subNames) {
|
||||
if (wIdx >= widgets.length) break;
|
||||
inputs[`${name}.${subName}`] = widgets[wIdx];
|
||||
wIdx++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (expectedType === "COMBO") {
|
||||
const allInputDefs = {
|
||||
...(objectInfo[node.type]?.input?.required || {}),
|
||||
...(objectInfo[node.type]?.input?.optional || {}),
|
||||
};
|
||||
const spec = allInputDefs[name];
|
||||
if (spec) {
|
||||
const choices = Array.isArray(spec[0]) ? spec[0] : null;
|
||||
if (choices && choices.length > 0 && !choices.includes(val)) {
|
||||
val = choices[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
inputs[name] = val;
|
||||
}
|
||||
} else {
|
||||
const mapping = _wfmGetWidgetMapping(node.type);
|
||||
if (mapping) {
|
||||
mapping.forEach((key, idx) => {
|
||||
if (idx < widgets.length && key && !(key in inputs)) {
|
||||
inputs[key] = widgets[idx];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((node.type === "ImpactWildcardEncode" || node.type === "ImpactWildcardProcessor")
|
||||
&& widgets.length > 0) {
|
||||
inputs["wildcard_text"] = widgets[0];
|
||||
}
|
||||
|
||||
api[nodeId] = {
|
||||
class_type: node.type,
|
||||
inputs,
|
||||
_meta: { title: node.title || node.type },
|
||||
};
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
function _collectUnique(arr) {
|
||||
const seen = new Set(), out = [];
|
||||
for (const v of arr) { if (v && typeof v === "string" && !seen.has(v)) { seen.add(v); out.push(v); } }
|
||||
@@ -2484,6 +2939,41 @@ function _isTextEncoderNode(ct) { return ct === "CLIPTextEncode" || ct.includes(
|
||||
function _isSamplerNode(ct) { return ct === "KSampler" || ct === "KSamplerAdvanced" || ct.includes("KSampler") || ct.includes("Sampler"); }
|
||||
function _isPromptStylerNode(ct) { return ct.includes("PromptStyler"); }
|
||||
|
||||
// LiteGraph形式: リンクを辿ってテキストを解決(ComfySwitchNode等の中継ノードにも対応)
|
||||
function _resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, srcId, slot, depth = 0) {
|
||||
if (depth > 6) return null;
|
||||
const srcNode = nodeMap.get(srcId);
|
||||
if (!srcNode) return null;
|
||||
const srcType = srcNode.type ?? "";
|
||||
if (_isPromptStylerNode(srcType)) {
|
||||
const v = srcNode.widgets_values?.[slot];
|
||||
return (v && typeof v === "string") ? v : null;
|
||||
}
|
||||
if (srcType === "ComfySwitchNode" && Array.isArray(srcNode.inputs)) {
|
||||
for (const name of ["on_false", "on_true"]) {
|
||||
const inp = srcNode.inputs.find(i => i.name === name);
|
||||
if (inp?.link == null) continue;
|
||||
const originId = linkOrigin.get(inp.link);
|
||||
const originSlot = linkSlot.get(inp.link) ?? 0;
|
||||
if (originId == null) continue;
|
||||
const text = _resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, originId, originSlot, depth + 1);
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const v = srcNode.widgets_values?.[slot] ?? srcNode.widgets_values?.[0];
|
||||
if (v && typeof v === "string") return v;
|
||||
if (Array.isArray(srcNode.inputs)) {
|
||||
const nextInput = srcNode.inputs.find(i => ["text", "value", "prompt", "string"].includes(i.name));
|
||||
if (nextInput?.link != null) {
|
||||
const originId = linkOrigin.get(nextInput.link);
|
||||
const originSlot = linkSlot.get(nextInput.link) ?? 0;
|
||||
if (originId != null) return _resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, originId, originSlot, depth + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _extractPromptsFromNodeSet(nodes, links) {
|
||||
const nodeMap = new Map();
|
||||
for (const n of nodes) nodeMap.set(n.id, n);
|
||||
@@ -2504,12 +2994,8 @@ function _extractPromptsFromNodeSet(nodes, links) {
|
||||
if (textInput?.link != null) {
|
||||
const originId = linkOrigin.get(textInput.link);
|
||||
const originSlot = linkSlot.get(textInput.link) ?? 0;
|
||||
const srcNode = originId != null ? nodeMap.get(originId) : null;
|
||||
if (srcNode) {
|
||||
const srcType = srcNode.type ?? "";
|
||||
if (_isPromptStylerNode(srcType)) { const v = srcNode.widgets_values?.[originSlot]; if (v && typeof v === "string") textMap.set(n.id, v); }
|
||||
else { const v = srcNode.widgets_values?.[originSlot] ?? srcNode.widgets_values?.[0]; if (v && typeof v === "string") textMap.set(n.id, v); }
|
||||
}
|
||||
const text2 = _resolveLinkedTextInNodeSet(nodeMap, linkOrigin, linkSlot, originId, originSlot);
|
||||
if (text2) textMap.set(n.id, text2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2518,15 +3004,46 @@ function _extractPromptsFromNodeSet(nodes, links) {
|
||||
for (const n of nodes) {
|
||||
if (!_isSamplerNode(n.type ?? "") || !Array.isArray(n.inputs)) continue;
|
||||
foundSampler = true;
|
||||
for (const inp of n.inputs) {
|
||||
// SamplerCustomAdvanced doesn't hold positive/negative directly — resolve through its
|
||||
// "guider" input (CFGGuider/DualCFGGuider/BasicGuider) instead.
|
||||
let inputsToScan = n.inputs;
|
||||
const guiderInput = n.inputs.find(inp => inp.name === "guider");
|
||||
if (guiderInput?.link != null) {
|
||||
const guiderId = linkOrigin.get(guiderInput.link);
|
||||
const guiderNode = guiderId != null ? nodeMap.get(guiderId) : null;
|
||||
if (Array.isArray(guiderNode?.inputs)) inputsToScan = guiderNode.inputs;
|
||||
}
|
||||
for (const inp of inputsToScan) {
|
||||
if (!inp || inp.link == null) continue;
|
||||
const originId = linkOrigin.get(inp.link);
|
||||
if (originId == null) continue;
|
||||
const txt = textMap.get(originId);
|
||||
if (!txt) continue;
|
||||
const name = inp.name ?? "";
|
||||
if (name === "positive" || name.startsWith("positive")) pos.add(txt);
|
||||
else if (name === "negative" || name.startsWith("negative")) neg.add(txt);
|
||||
// DualCFGGuider (HiDream E1): cond1 carries the positive-derived conditioning.
|
||||
const isPos = name === "positive" || name === "cond1" || name.startsWith("positive");
|
||||
const isNeg = name === "negative" || name.startsWith("negative");
|
||||
if (!isPos && !isNeg) continue;
|
||||
// TextEncodeMageFlowEdit / TextEncodeBooguEdit — 1ノードでprompt/negative_prompt両方を
|
||||
// 直接持つため、textMap(1ノード1テキスト)では両ロールを表現できない
|
||||
const srcNode = nodeMap.get(originId);
|
||||
if (srcNode?.type === "TextEncodeMageFlowEdit" || srcNode?.type === "TextEncodeBooguEdit") {
|
||||
const widgetIdx = isPos ? 0 : 1; // widgets_values: [prompt, negative_prompt, ...]
|
||||
const txt = srcNode.widgets_values?.[widgetIdx];
|
||||
if (txt && typeof txt === "string") { if (isPos) pos.add(txt); else neg.add(txt); }
|
||||
continue;
|
||||
}
|
||||
// InstructPixToPixConditioning (HiDream E1): forwards the actual text-encoder
|
||||
// conditioning through its own positive/negative inputs — follow one more hop.
|
||||
let resolvedOriginId = originId;
|
||||
if (srcNode?.type === "InstructPixToPixConditioning" && Array.isArray(srcNode.inputs)) {
|
||||
const innerInput = srcNode.inputs.find(i => i.name === (isPos ? "positive" : "negative"));
|
||||
if (innerInput?.link != null) {
|
||||
const innerId = linkOrigin.get(innerInput.link);
|
||||
if (innerId != null) resolvedOriginId = innerId;
|
||||
}
|
||||
}
|
||||
const txt = textMap.get(resolvedOriginId);
|
||||
if (!txt) continue;
|
||||
if (isPos) pos.add(txt); else neg.add(txt);
|
||||
}
|
||||
}
|
||||
if (!foundSampler) return null;
|
||||
@@ -2574,13 +3091,28 @@ function _extractMarkdownNoteModels(wf) {
|
||||
return hasAny ? result : null;
|
||||
}
|
||||
|
||||
function _resolveLinkedText(wf, srcId, slot) {
|
||||
function _resolveLinkedText(wf, srcId, slot, depth = 0) {
|
||||
if (depth > 6) return null;
|
||||
const src = wf[String(srcId)];
|
||||
if (!src || typeof src !== "object") return null;
|
||||
const ct = src.class_type ?? "";
|
||||
if (_isPromptStylerNode(ct)) { const v = slot === 0 ? src.inputs?.text_positive : src.inputs?.text_negative; return (v && typeof v === "string") ? v : null; }
|
||||
const keys = slot === 0 ? ["text_positive", "text", "text_g", "prompt"] : ["text_negative", "text_l"];
|
||||
for (const k of keys) { const v = src.inputs?.[k]; if (v && typeof v === "string") return v; }
|
||||
// ComfySwitchNode ("If/Else Switch") — 片方(TextGenerateなどLLMノード)は静的解決不能なため
|
||||
// on_false/on_true の両方を試し、リテラルへ解決できた方を採用する。
|
||||
if (ct === "ComfySwitchNode") {
|
||||
for (const key of ["on_false", "on_true"]) {
|
||||
const v = src.inputs?.[key];
|
||||
if (Array.isArray(v)) { const text = _resolveLinkedText(wf, v[0], v[1] ?? 0, depth + 1); if (text) return text; }
|
||||
else if (typeof v === "string" && v) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const keys = slot === 0 ? ["text_positive", "text", "text_g", "prompt", "value"] : ["text_negative", "text_l"];
|
||||
for (const k of keys) {
|
||||
const v = src.inputs?.[k];
|
||||
if (typeof v === "string" && v) return v;
|
||||
if (Array.isArray(v)) { const text = _resolveLinkedText(wf, v[0], v[1] ?? 0, depth + 1); if (text) return text; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2594,7 +3126,9 @@ function _extractPromptsAPI(wf) {
|
||||
const textMap = new Map();
|
||||
for (const [id, n] of Object.entries(wf)) {
|
||||
if (!n || !_isTextEncoderNode(n.class_type ?? "")) continue;
|
||||
const raw = n.inputs?.text ?? n.inputs?.text_g ?? null;
|
||||
// "prompt" — TextEncodeQwenImageEdit(Plus)/TextEncodeBooguEdit and similar Image Edit
|
||||
// model text encoders use this key instead of "text"/"text_g".
|
||||
const raw = n.inputs?.text ?? n.inputs?.text_g ?? n.inputs?.prompt ?? null;
|
||||
if (raw && typeof raw === "string") { textMap.set(id, raw); }
|
||||
else if (Array.isArray(raw)) { const txt = _resolveLinkedText(wf, raw[0], raw[1] ?? 0); if (txt) textMap.set(id, txt); }
|
||||
}
|
||||
@@ -2603,12 +3137,36 @@ function _extractPromptsAPI(wf) {
|
||||
for (const n of Object.values(wf)) {
|
||||
if (!n || !_isSamplerNode(n.class_type ?? "")) continue;
|
||||
foundSampler = true;
|
||||
for (const [key, val] of Object.entries(n.inputs ?? {})) {
|
||||
// SamplerCustomAdvanced doesn't hold positive/negative directly — it drives a separate
|
||||
// Guider node (CFGGuider/DualCFGGuider/BasicGuider) via its "guider" input. Scan that
|
||||
// node's inputs instead so the loop below can find the positive/negative-role keys.
|
||||
let inputsToScan = n.inputs ?? {};
|
||||
if (Array.isArray(n.inputs?.guider)) {
|
||||
const guiderNode = wf[String(n.inputs.guider[0])];
|
||||
if (guiderNode?.inputs) inputsToScan = guiderNode.inputs;
|
||||
}
|
||||
for (const [key, val] of Object.entries(inputsToScan)) {
|
||||
if (!Array.isArray(val)) continue;
|
||||
const txt = textMap.get(String(val[0]));
|
||||
// DualCFGGuider (HiDream E1): cond1 carries the positive-derived conditioning.
|
||||
const isPos = key === "positive" || key === "cond1" || key.startsWith("positive");
|
||||
const isNeg = key === "negative" || key.startsWith("negative");
|
||||
if (!isPos && !isNeg) continue;
|
||||
// TextEncodeMageFlowEdit / TextEncodeBooguEdit — 1ノードでprompt/negative_prompt両方を
|
||||
// 直接持つため、textMap(1ノード1テキスト)では両ロールを表現できない
|
||||
const srcNode = wf[String(val[0])];
|
||||
if (srcNode?.class_type === "TextEncodeMageFlowEdit" || srcNode?.class_type === "TextEncodeBooguEdit") {
|
||||
const txt = isPos ? srcNode.inputs?.prompt : srcNode.inputs?.negative_prompt;
|
||||
if (txt && typeof txt === "string") { if (isPos) pos.add(txt); else neg.add(txt); }
|
||||
continue;
|
||||
}
|
||||
// InstructPixToPixConditioning (HiDream E1): forwards the actual text-encoder
|
||||
// conditioning through its own positive/negative inputs — follow one more hop.
|
||||
const resolvedId = srcNode?.class_type === "InstructPixToPixConditioning" && Array.isArray(srcNode.inputs?.[isPos ? "positive" : "negative"])
|
||||
? String(srcNode.inputs[isPos ? "positive" : "negative"][0])
|
||||
: String(val[0]);
|
||||
const txt = textMap.get(resolvedId);
|
||||
if (!txt) continue;
|
||||
if (key === "positive" || key.startsWith("positive")) pos.add(txt);
|
||||
else if (key === "negative" || key.startsWith("negative")) neg.add(txt);
|
||||
if (isPos) pos.add(txt); else neg.add(txt);
|
||||
}
|
||||
}
|
||||
if (!foundSampler || (pos.size === 0 && neg.size === 0)) { const all = [...textMap.values()].filter(t => t && t.trim()); return { positives: [], negatives: [], texts: all }; }
|
||||
@@ -2695,9 +3253,20 @@ async function _extractAllMetadata(file) {
|
||||
const isJSON = file.type === "application/json" || name.endsWith(".json");
|
||||
const isWebP = file.type === "image/webp" || name.endsWith(".webp");
|
||||
|
||||
function fromWorkflow(wf, source) {
|
||||
async function fromWorkflow(originalWf, source) {
|
||||
let wf = originalWf;
|
||||
// サブグラフを持つワークフローは、外側の折りたたみ表示ウィジェット値(実際に
|
||||
// ユーザーが見ている値)がテンプレート内のノードのwidgets_valuesへ反映されて
|
||||
// いないことがあるため、_wfmConvertUiToApi() で正規化してから抽出する。
|
||||
if (Array.isArray(wf?.nodes) && wf.definitions?.subgraphs?.length > 0) {
|
||||
try {
|
||||
const apiWf = await _wfmConvertUiToApi(wf);
|
||||
if (apiWf && Object.keys(apiWf).length > 0) wf = apiWf;
|
||||
} catch { /* 変換失敗時は元のUI形式のまま抽出(既存ロジックにフォールバック) */ }
|
||||
}
|
||||
const base = { source, checkpoints: _extractCheckpoints(wf), vaes: _extractVAEs(wf), diffusionModels: _extractDiffusionModels(wf), textEncoders: _extractTextEncoders(wf), loras: _extractLoRAs(wf), ..._extractPrompts(wf) };
|
||||
const mdm = _extractMarkdownNoteModels(wf);
|
||||
// MarkdownNote は常に元のUI形式ワークフローから抽出する(API変換後は nodes 情報が失われるため)
|
||||
const mdm = _extractMarkdownNoteModels(originalWf);
|
||||
if (mdm) {
|
||||
if (!base.checkpoints.length) base.checkpoints = mdm.checkpoints;
|
||||
if (!base.vaes.length) base.vaes = mdm.vaes;
|
||||
@@ -2710,19 +3279,19 @@ async function _extractAllMetadata(file) {
|
||||
|
||||
if (isJSON) {
|
||||
let wf; try { wf = JSON.parse(_sanitizeJSON(await file.text())); } catch { return null; }
|
||||
return wf ? fromWorkflow(wf, "comfyui") : null;
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
if (isWebP) {
|
||||
const exif = await _readWebPEXIFChunk(file);
|
||||
if (!exif) return null;
|
||||
const wf = _extractWorkflowFromEXIF(exif);
|
||||
return wf ? fromWorkflow(wf, "comfyui") : null;
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
// PNG
|
||||
const chunks = await _readAllPNGTextChunks(file);
|
||||
if (!chunks) return null;
|
||||
if (chunks.prompt) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.prompt)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.workflow) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.workflow)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.prompt) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.prompt)); } catch { return null; } return wf ? await fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.workflow) { let wf; try { wf = JSON.parse(_sanitizeJSON(chunks.workflow)); } catch { return null; } return wf ? await fromWorkflow(wf, "comfyui") : null; }
|
||||
if (chunks.fooocus_scheme === "fooocus" && chunks.parameters) {
|
||||
const f = _parseFooocusMetadata(chunks.parameters);
|
||||
if (!f) return null;
|
||||
@@ -3116,6 +3685,48 @@ async function aiFetchModels(url, backend) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- AI skills (Chat system prompt library) ----
|
||||
|
||||
async function aiSkillFetchFiles() {
|
||||
try {
|
||||
const r = await fetch("/api/wfm/skills");
|
||||
return r.ok ? await r.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function aiSkillFetchContent(filename) {
|
||||
try {
|
||||
const r = await fetch(`/api/wfm/skills/content?filename=${encodeURIComponent(filename)}`);
|
||||
const d = await r.json();
|
||||
return d.content ?? null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function aiSkillSaveFile(filename, content) {
|
||||
try {
|
||||
const r = await fetch("/api/wfm/skills/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, content }),
|
||||
});
|
||||
const d = await r.json();
|
||||
return d.status === "ok" ? d.file : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function aiSkillDeleteFile(filename) {
|
||||
try {
|
||||
await fetch("/api/wfm/skills/delete", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function aiSkillStripFrontmatter(content) {
|
||||
const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
||||
return (m ? content.slice(m[0].length) : content).trim();
|
||||
}
|
||||
|
||||
const renderAiTab = (container) => {
|
||||
if (!container.querySelector(".wfm-nlp-ai-layout")) {
|
||||
container.innerHTML = `
|
||||
@@ -3147,6 +3758,31 @@ const renderAiTab = (container) => {
|
||||
</div>
|
||||
<!-- Chat sub -->
|
||||
<div id="wfm-nlp-ai-chat" class="wfm-nlp-ai-pane" style="display:none;">
|
||||
<div class="wfm-nlp-ai-row" style="flex-shrink:0;">
|
||||
<select id="wfm-nlp-ai-skill-select" class="wfm-nlp-ai-sel" style="flex:1;width:auto;">
|
||||
<option value="">-- No skill --</option>
|
||||
</select>
|
||||
<button id="wfm-nlp-ai-skill-manage-btn" class="wfm-nlp-ai-btn" title="Manage skills">✎</button>
|
||||
</div>
|
||||
<div id="wfm-nlp-ai-skill-panel" class="wfm-nlp-ai-skill-panel" style="display:none;">
|
||||
<div class="wfm-nlp-ai-row" style="justify-content:space-between;">
|
||||
<span class="wfm-nlp-ai-sec-title">Skills</span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button id="wfm-nlp-ai-skill-new-btn" class="wfm-nlp-ai-btn wfm-nlp-ai-btn-primary">+ New</button>
|
||||
<button id="wfm-nlp-ai-skill-close-btn" class="wfm-nlp-ai-btn">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wfm-nlp-ai-skill-list" class="wfm-nlp-ai-skill-list"></div>
|
||||
<div id="wfm-nlp-ai-skill-editor" class="wfm-nlp-ai-skill-editor" style="display:none;">
|
||||
<input type="text" id="wfm-nlp-ai-skill-editor-filename" class="wfm-nlp-ai-input" placeholder="filename.md">
|
||||
<textarea id="wfm-nlp-ai-skill-editor-content" class="wfm-nlp-ai-input wfm-nlp-ai-skill-editor-ta" rows="6" placeholder="--- name: My Skill description: ... --- System prompt..."></textarea>
|
||||
<div class="wfm-nlp-ai-actions">
|
||||
<button id="wfm-nlp-ai-skill-editor-save-btn" class="wfm-nlp-ai-btn wfm-nlp-ai-btn-primary">Save</button>
|
||||
<button id="wfm-nlp-ai-skill-editor-delete-btn" class="wfm-nlp-ai-btn wfm-nlp-ai-skill-danger-btn" style="display:none;">Delete</button>
|
||||
<button id="wfm-nlp-ai-skill-editor-cancel-btn" class="wfm-nlp-ai-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-nlp-ai-chat-msgs" id="wfm-nlp-ai-chat-msgs"></div>
|
||||
<textarea id="wfm-nlp-ai-chat-input" class="wfm-nlp-ai-textarea wfm-nlp-ai-chat-input" placeholder="Type a message..."></textarea>
|
||||
<div class="wfm-nlp-ai-actions">
|
||||
@@ -3433,7 +4069,11 @@ const setupAiHandlers = (container) => {
|
||||
chatStatusEl.className = "wfm-nlp-ai-status wfm-nlp-ai-working";
|
||||
|
||||
try {
|
||||
const reply = await aiCallChat(url, backend, model, chatHistory);
|
||||
const skillPrompt = await getActiveSkillSystemPrompt();
|
||||
const messagesToSend = skillPrompt
|
||||
? [{ role: "system", content: skillPrompt }, ...chatHistory]
|
||||
: chatHistory;
|
||||
const reply = await aiCallChat(url, backend, model, messagesToSend);
|
||||
chatHistory.push({ role: "assistant", content: reply });
|
||||
appendChatBubble("assistant", reply);
|
||||
chatStatusEl.textContent = "";
|
||||
@@ -3459,6 +4099,141 @@ const setupAiHandlers = (container) => {
|
||||
if (chatStatusEl) { chatStatusEl.textContent = ""; chatStatusEl.className = "wfm-nlp-ai-status"; }
|
||||
});
|
||||
|
||||
// ---- Skill handlers (Chat system prompt library) ----
|
||||
let skillFilesNlp = [];
|
||||
let skillEditingFilenameNlp = null;
|
||||
let activeSkillCacheNlp = null; // { filename, content }
|
||||
|
||||
const skillSelectEl = container.querySelector("#wfm-nlp-ai-skill-select");
|
||||
const skillManageBtn = container.querySelector("#wfm-nlp-ai-skill-manage-btn");
|
||||
const skillPanelEl = container.querySelector("#wfm-nlp-ai-skill-panel");
|
||||
const skillCloseBtn = container.querySelector("#wfm-nlp-ai-skill-close-btn");
|
||||
const skillNewBtn = container.querySelector("#wfm-nlp-ai-skill-new-btn");
|
||||
const skillListEl = container.querySelector("#wfm-nlp-ai-skill-list");
|
||||
const skillEditorEl = container.querySelector("#wfm-nlp-ai-skill-editor");
|
||||
const skillEditorFilenameEl = container.querySelector("#wfm-nlp-ai-skill-editor-filename");
|
||||
const skillEditorContentEl = container.querySelector("#wfm-nlp-ai-skill-editor-content");
|
||||
const skillEditorSaveBtn = container.querySelector("#wfm-nlp-ai-skill-editor-save-btn");
|
||||
const skillEditorDeleteBtn = container.querySelector("#wfm-nlp-ai-skill-editor-delete-btn");
|
||||
const skillEditorCancelBtn = container.querySelector("#wfm-nlp-ai-skill-editor-cancel-btn");
|
||||
|
||||
async function getActiveSkillSystemPrompt() {
|
||||
const filename = skillSelectEl?.value || "";
|
||||
if (!filename) return null;
|
||||
if (activeSkillCacheNlp?.filename === filename) return activeSkillCacheNlp.content;
|
||||
const raw = await aiSkillFetchContent(filename);
|
||||
const content = aiSkillStripFrontmatter(raw || "");
|
||||
activeSkillCacheNlp = { filename, content };
|
||||
return content;
|
||||
}
|
||||
|
||||
function renderSkillList() {
|
||||
if (!skillListEl) return;
|
||||
skillListEl.innerHTML = "";
|
||||
if (skillFilesNlp.length === 0) {
|
||||
skillListEl.innerHTML = `<div style="padding:8px;font-size:11px;color:var(--descrip-text,#888);">No skills yet.</div>`;
|
||||
return;
|
||||
}
|
||||
for (const f of skillFilesNlp) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-nlp-ai-skill-item";
|
||||
item.title = f.description || "";
|
||||
item.innerHTML = `<span class="wfm-nlp-ai-skill-item-name">${f.name}</span><span>✎</span>`;
|
||||
item.addEventListener("click", async () => {
|
||||
const content = await aiSkillFetchContent(f.filename);
|
||||
openSkillEditor(f.filename, content ?? "");
|
||||
});
|
||||
skillListEl.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function openSkillEditor(filename, content) {
|
||||
skillEditingFilenameNlp = filename || null;
|
||||
if (!skillEditorEl) return;
|
||||
if (skillEditorFilenameEl) skillEditorFilenameEl.value = filename || "";
|
||||
if (skillEditorContentEl) skillEditorContentEl.value = content || "";
|
||||
if (skillEditorDeleteBtn) skillEditorDeleteBtn.style.display = filename ? "" : "none";
|
||||
skillEditorEl.style.display = "";
|
||||
skillEditorFilenameEl?.focus();
|
||||
}
|
||||
|
||||
function closeSkillEditor() {
|
||||
skillEditingFilenameNlp = null;
|
||||
if (skillEditorEl) skillEditorEl.style.display = "none";
|
||||
}
|
||||
|
||||
function populateSkillSelect() {
|
||||
if (!skillSelectEl) return;
|
||||
const c = loadAiCfg();
|
||||
const wanted = skillSelectEl.value || c.activeSkillFilename || "";
|
||||
skillSelectEl.innerHTML = `<option value="">-- No skill --</option>` +
|
||||
skillFilesNlp.map(f => `<option value="${f.filename}" title="${f.description || ""}">${f.name}</option>`).join("");
|
||||
skillSelectEl.value = skillFilesNlp.some(f => f.filename === wanted) ? wanted : "";
|
||||
activeSkillCacheNlp = null;
|
||||
}
|
||||
|
||||
async function refreshSkillFiles() {
|
||||
skillFilesNlp = await aiSkillFetchFiles();
|
||||
renderSkillList();
|
||||
populateSkillSelect();
|
||||
}
|
||||
|
||||
refreshSkillFiles();
|
||||
|
||||
skillSelectEl?.addEventListener("change", () => {
|
||||
saveAiCfg({ activeSkillFilename: skillSelectEl.value });
|
||||
activeSkillCacheNlp = null;
|
||||
});
|
||||
|
||||
skillManageBtn?.addEventListener("click", () => {
|
||||
if (!skillPanelEl) return;
|
||||
const show = skillPanelEl.style.display === "none";
|
||||
skillPanelEl.style.display = show ? "" : "none";
|
||||
if (show) refreshSkillFiles();
|
||||
});
|
||||
|
||||
skillCloseBtn?.addEventListener("click", () => {
|
||||
if (skillPanelEl) skillPanelEl.style.display = "none";
|
||||
});
|
||||
|
||||
skillNewBtn?.addEventListener("click", () => {
|
||||
openSkillEditor(null, "---\nname: \ndescription: \n---\n\n");
|
||||
});
|
||||
|
||||
skillEditorSaveBtn?.addEventListener("click", async () => {
|
||||
let filename = (skillEditorFilenameEl?.value || "").trim();
|
||||
const content = skillEditorContentEl?.value || "";
|
||||
if (!filename) { showToast("ファイル名を入力してください", "error"); return; }
|
||||
if (!/\.md$/i.test(filename)) filename += ".md";
|
||||
if (!/^[\w\-. ]+\.md$/i.test(filename)) { showToast("無効なファイル名です", "error"); return; }
|
||||
const saved = await aiSkillSaveFile(filename, content);
|
||||
if (saved) {
|
||||
showToast(`保存しました: ${filename}`, "success");
|
||||
closeSkillEditor();
|
||||
await refreshSkillFiles();
|
||||
skillSelectEl.value = filename;
|
||||
saveAiCfg({ activeSkillFilename: filename });
|
||||
activeSkillCacheNlp = null;
|
||||
} else {
|
||||
showToast("保存に失敗しました", "error");
|
||||
}
|
||||
});
|
||||
|
||||
skillEditorDeleteBtn?.addEventListener("click", async () => {
|
||||
if (!skillEditingFilenameNlp) return;
|
||||
if (!confirm(`Delete "${skillEditingFilenameNlp}"?`)) return;
|
||||
await aiSkillDeleteFile(skillEditingFilenameNlp);
|
||||
showToast(`削除しました: ${skillEditingFilenameNlp}`, "success");
|
||||
if (skillSelectEl.value === skillEditingFilenameNlp) {
|
||||
saveAiCfg({ activeSkillFilename: "" });
|
||||
activeSkillCacheNlp = null;
|
||||
}
|
||||
closeSkillEditor();
|
||||
await refreshSkillFiles();
|
||||
});
|
||||
|
||||
skillEditorCancelBtn?.addEventListener("click", () => closeSkillEditor());
|
||||
|
||||
// ---- VLM handlers ----
|
||||
let vlmImage = null; // { base64, mimeType }
|
||||
|
||||
@@ -4455,6 +5230,55 @@ const injectStyles = () => {
|
||||
.wfm-nlp-ai-wc-count {
|
||||
width: 60px;
|
||||
}
|
||||
/* AI skills */
|
||||
.wfm-nlp-ai-skill-panel {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--border-color, #4e4e4e);
|
||||
border-radius: 4px;
|
||||
background: var(--comfy-menu-bg, #1e1e1e);
|
||||
}
|
||||
.wfm-nlp-ai-skill-list {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color, #4e4e4e);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.wfm-nlp-ai-skill-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
color: var(--input-text, #ddd);
|
||||
border-bottom: 1px solid var(--border-color, #3a3a3a);
|
||||
cursor: pointer;
|
||||
}
|
||||
.wfm-nlp-ai-skill-item:last-child { border-bottom: none; }
|
||||
.wfm-nlp-ai-skill-item:hover { background: rgba(74,158,255,0.1); }
|
||||
.wfm-nlp-ai-skill-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wfm-nlp-ai-skill-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.wfm-nlp-ai-skill-editor-ta {
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
min-height: 90px;
|
||||
}
|
||||
.wfm-nlp-ai-skill-danger-btn {
|
||||
color: #ff4757;
|
||||
border-color: #ff4757;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user