Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99f342cc89 | ||
|
|
9681e76848 |
@@ -0,0 +1,60 @@
|
||||
"""PostToolUse hook: regenerate README.index.md / DEVLOG.index.md right after
|
||||
Claude Code edits README.md or DEVLOG.md, so graphify's excluded-doc index
|
||||
stand-ins (see .graphifyignore) never drift from the source files within a
|
||||
session. A git pre-commit hook covers edits made outside Claude Code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TARGETS = {"readme.md": "readme", "devlog.md": "devlog"}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return
|
||||
|
||||
if payload.get("tool_name") not in ("Edit", "Write"):
|
||||
return
|
||||
|
||||
file_path = payload.get("tool_input", {}).get("file_path", "")
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
name = Path(file_path).name.lower()
|
||||
target = TARGETS.get(name)
|
||||
if target is None:
|
||||
return
|
||||
|
||||
project_dir = os.environ.get("CLAUDE_PROJECT_DIR") or str(Path(file_path).resolve().parent)
|
||||
script = Path(project_dir) / "tools" / "generate_doc_index.py"
|
||||
if not script.exists():
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), target],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(json.dumps({"systemMessage": f"[doc-index] {result.stdout.strip()}"}))
|
||||
else:
|
||||
print(
|
||||
json.dumps(
|
||||
{"systemMessage": f"[doc-index] generate_doc_index.py {target} failed: {result.stderr.strip()}"}
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -19,6 +19,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/on_doc_edit.py\"",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
# Private/unpublished planning notes — never committed to git, must not leak
|
||||
# into the published graphify-out/ knowledge graph either.
|
||||
QWEN_CHAT_INTEGRATION_PLAN.md
|
||||
|
||||
# Too large for reliable local-Ollama semantic extraction (large chunks make the
|
||||
# model summarize in prose instead of emitting JSON, and time out on slower
|
||||
# models/hardware). README.index.md / DEVLOG.index.md are condensed stand-ins
|
||||
# that graphify extracts instead — regenerate them after editing the originals.
|
||||
README.md
|
||||
DEVLOG.md
|
||||
|
||||
@@ -209,3 +209,14 @@ Rules:
|
||||
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
|
||||
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
|
||||
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
|
||||
|
||||
### README.md / DEVLOG.md の索引ファイル
|
||||
|
||||
`README.md`/`DEVLOG.md`はサイズが大きすぎてローカルOllamaでの意味抽出が不安定になる(大きな塊を渡すとJSONではなく散文の要約を返す、または処理がタイムアウトする)ため、`.graphifyignore`でgraphifyの抽出対象から除外している。代わりに縮小版の`README.index.md`/`DEVLOG.index.md`を抽出対象にしている。
|
||||
|
||||
- 生成スクリプト: `python tools/generate_doc_index.py [readme|devlog|all]`
|
||||
- 自動化: `README.md`/`DEVLOG.md`を編集すると以下の2つのフックが自動でindexを再生成する
|
||||
- Claude Code PostToolUseフック(`.claude/hooks/on_doc_edit.py`)— Edit/Write直後に即時実行
|
||||
- git pre-commitフック(`.git/hooks/pre-commit`)— コミット時にstagedファイルを検知して再生成し、同じコミットに含める(他エディタでの編集にも対応)
|
||||
- 索引ファイルを手動で編集しない(次の生成で上書きされる)
|
||||
- 索引ファイル再生成後は`graphify update .`を実行してグラフに反映する
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
# DEVLOG Index — ComfyUI-Workflow-Studio
|
||||
|
||||
Condensed version-by-version index of DEVLOG.md (each line: version + one-line summary of what changed and why). Full rationale, code details, and "How to apply" lessons live in the excluded DEVLOG.md itself; this index exists so graphify can extract a queryable semantic node per release without feeding the full log to the local Ollama model. Regenerate with `python tools/generate_doc_index.py devlog` whenever DEVLOG.md changes.
|
||||
|
||||
- **v0.4.4** — 機能追加: Lab/GenerateUI ModelタブへLoRAバイパス機能 + AI TOOL Chatの画像生成パラメータ拡張
|
||||
- **v0.4.3** — バグ修正: 共有モーダルのお気に入り☆/リネームがLabモーダルへ残留 + Lab Model列の+/-をCheckpoint/LoRA/VAEで完全独立化
|
||||
- **v0.4.2** — 機能追加: Lab「Model」列統合(Checkpoint / LoRA Single / VAE)+ フィールド単位継承 + C/L/V表示切替
|
||||
- **v0.4.1** — UX改善: Mask Editor One連携ボタンを「選択中ノード優先」方式に再設計
|
||||
- **v0.4.0** — リファクタリング: image-edit-tab.js / models-tab.js の低凝集度モジュール分割
|
||||
- **v0.3.99** — 機能追加: AI TOOLタブ + サイドパネルAタブにUnslothバックエンド追加(APIキーは.env管理、SSRF脆弱性修正、reasoning_content対応)
|
||||
- **v0.3.98** — 機能追加・バグ修正: AI TOOL翻訳の信頼性向上 + GenerateUI Modelタブ再構成(ハイライト色・折りたたみ・並び替え)+ Thinking mode/Max tokens設定
|
||||
- **v0.3.97** — 機能追加・バグ修正: KREA-2ワークフロー対応 + Labタブ拡張(Clear/Get from Previous/Bypass/ライブ反映)+ Input Imageプレースホルダ機能
|
||||
- **v0.3.96** — バグ修正: カタログ作成バッチが特定件数で無言停止する問題の修正 + Style Catalog/Promptタブ機能拡張
|
||||
- **v0.3.95** — 機能追加: Style/Prompt→ImagePrompt改名 + Style_Catalogギャラリー新設 + GenerateUI「カタログ作成」機能
|
||||
- **v0.3.94** — 機能追加: Gallery タブのサブタブ化 + Style/Prompt ギャラリー新設(視覚的プロンプトビルダー)
|
||||
- **v0.3.93** — バグ修正・機能改善: Labタブ VAEキーフレーム不発バグの修正+Style適用UI刷新+Plan JSONスクロール同期修正+キーフレーム個別削除+Batch数超過警告+ワイルドカード対応+Save index image デフォルトON
|
||||
- **v0.3.92** — バグ修正: CLIPTextEncodeEditPlus使用ワークフローがGenerateUI経由だと直接実行と異なる画像になる問題(根本原因はconvertUiToApiのForceInput 未考慮バグ)+LoRA Stack機能の改善一式
|
||||
- **v0.3.91** — 機能拡張: Labインデックス画像にPlanメタデータをPNG埋め込み+README構成整理(Changelogセクション廃止)
|
||||
- **v0.3.90** — 機能拡張: Labタブの進化(ワークフロー呼び出し・T2I対応・Run時インデックス画像Output保存+Eagle連携・プラン接頭辞)+Galleryタブのルート表示簡略化
|
||||
- **v0.3.89** — バグ修正: v0.3.88で追加したLab機能への実動作確認フィードバック3件を反映
|
||||
- **v0.3.88** — 機能拡張: Labタブ Promptセルにスタイル適用・画像/GenerateUIからのプロンプト取得、Checkpoint/VAEフィルター、Plan JSONサブタブを追加
|
||||
- **v0.3.87** — 追加機能: GenerateUIタブに実験的な「Lab」サブタブを追加(I2Iバッチ生成・キーフレーム方式)
|
||||
- **v0.3.86** — バグ修正: 画像を一括生成(I2I)等の連続実行で "GalleryMetadataStore: save error: dictionary changed size during iteration" が発生
|
||||
- **v0.3.85** — 機能拡張: Generate UI連携ブリッジ(T2I)にデフォルトワークフロー・Negativeプロンプト対応を追加
|
||||
- **v0.3.84** — バグ修正: ernie_t2i.jsonをGenerateUIタブで実行すると "Prompt outputs failed validation" エラー
|
||||
- **v0.3.83** — 追加機能: Ernie Image(ernie-image / ernie-image-turbo)ワークフロー対応 + プロンプトがリンク経由のノードから供給されるパターンの修正
|
||||
- **v0.3.82** — バグ修正: MetadataタブとサイドパネルIタブでLongCat/Boogu/HiDream E1/FireRed等のプロンプトが表示されない不具合
|
||||
- **v0.3.81** — 追加機能: GenerateUIタブでFlux.2 Klein / LongCat / Boogu / HiDream E1 / FireRedのImage Editワークフローに対応
|
||||
- **v0.3.80** — 追加機能: Comic Creator「半自動マンガ作成」向けブリッジ(LLMプロンプト下書き・バッチ画像生成)
|
||||
- **v0.3.79** — 追加機能: サイドパネル(Workflow Studio Library)IタブのMage-Flow等サブグラフワークフロー対応
|
||||
- **v0.3.78** — 追加機能: Eagle自動保存のSVG対応
|
||||
- **v0.3.76** — 追加機能: Comic Creator向けI2I実行ブリッジ`_wfmReceiveI2IRunRequest`
|
||||
- **v0.3.77** — 追加機能: GalleryタブでSVGファイルの表示・管理に対応
|
||||
- **v0.3.75** — Lemonadeバックエンド追加(3つ目のAIバックエンド)
|
||||
- **v0.3.74** — AI TOOL Chatペインの画像添付対応 + I2I連携
|
||||
- **v0.3.73** — AI TOOL Chatペインからの対話式画像生成(Tool Calling)+ Checkpoint関連の安全策
|
||||
- **v0.3.72** — Comic Creator連携用の外部Inpaintエントリポイントを追加
|
||||
- **v0.3.71** — Inpaint機能の追加(GenerateUI Imageタブ + Image Edit「Inpaint」タブ)
|
||||
- **v0.3.70** — Workflow AI要約: Ollama 404エラー修正
|
||||
- **v0.3.69** — Gallery → ComfyUI Comic Creator 連携(Send CC ボタン)
|
||||
- **v0.3.68** — MASK EDITOR ONE セクション常時表示化
|
||||
- **v0.3.67** — Alt+Click Apply & Generate
|
||||
- **計画: Image Edit 拡張 — ABRブラシ連携 + Mask Editor One 統合** — フェーズ1: Draw ツール — ABRブラシ対応
|
||||
- **2026-06-29: v0.3.66 — Mask ツール 5サブツール追加(Color / Alpha / Text / Vector / Shape)・Support リンク追加** — 概要
|
||||
- **2026-06-29: v0.3.65 — Mask Editor One 連携修正・トップバーアイコン修正・Mask ABRブラシ対応** — 概要
|
||||
- **2026-06-28: v0.3.64 — Image Edit マスク Add/Subtract・BiRefNet BG Remove・SAM3 セグメンテーション・ブラシカーソルバグ修正** — 概要
|
||||
- **2026-06-28: v0.3.63 — ノードタブ複数選択・全タブフィルタークリア・検索Xボタン** — 概要
|
||||
- **2026-06-27: WFS_PromptText の GenerateUI 対応・ドロップ時 title 固定** — 概要
|
||||
- **2026-06-27: v0.3.62 — Send GenUI Image ボタン追加・tagger_settings.json バグ修正・README 更新** — 概要
|
||||
- **2026-06-27: データ管理バグ修正 — tagger_settings.json をエクスポート対象に追加** — 概要
|
||||
- **2026-06-27: v0.3.61 — G'MIC バグ修正・セキュリティ・Draw/Mask ブラシ操作性改善** — 概要
|
||||
- **2026-06-27: v0.3.60 — Image Edit Filter Tool (G'MIC-Qt 連携) 実装** — 概要
|
||||
- **2026-06-27: v0.3.59 — Image Edit Mask Tool 実装・Draw/Mask 描画バグ修正・クリッピング保存対応** — 概要
|
||||
- **2026-06-27: v0.3.58 — GenerateUI Model タブ GGUF対応拡張(Diffusion Model / Text Encoder)** — 概要
|
||||
- **2026-06-26: v0.3.57 — Image Edit Blur Tool / BG Remove Tool 追加** — 概要
|
||||
- **2026-06-26: v0.3.56 — Style機能追加(GenerateUI ツールバー & Batchタブ)** — 概要
|
||||
- **2026-06-25: v0.3.55 — グループ機能バグ修正(全タブ)** — 根本原因(データ消失): パス区切り文字の不一致
|
||||
- **2026-06-25: v0.3.54 — Image Edit Shape ツール追加・Models タブ overlay バグ修正** — Image Edit Tab — Shape ツール(□ / S キー)
|
||||
- **2026-06-25: v0.3.53 — Image Edit Tab 実装・レイヤーロック・ギャラリー保存** — Image Edit Tab(新規タブ)
|
||||
- **2026-06-25: GenUI設定タブ LATENT IMAGE にプリセット機能を追加** — 変更内容
|
||||
- **2026-06-24: GenUI ModelでLoRA適用後、Modelサブタブ再表示時に値がリセットされるバグ修正** — 問題
|
||||
- **2026-06-24: v0.3.51 — Embedding GenUI PP/NP対応 + GenerateUIプロンプトタブEmbeddingセレクタ追加** — 変更内容
|
||||
- **2026-06-24: v0.3.51 — GenerateUIタブ Hypernetwork対応 + ModelsタブGenUI Modelボタン追加** — 変更内容
|
||||
- **2026-06-24: v0.3.51 — Hypernetwork/Embeddingモデル未表示バグ修正** — 原因
|
||||
- **2026-06-24: v0.3.51 — ModelsタブサイドパネルにGenUI Modelボタン追加** — 変更内容
|
||||
- **2026-06-22: v0.3.50 — ギャラリータブ プロンプト検索負荷改善(バックグラウンドインデックス)** — 問題
|
||||
- **2026-06-22: v0.3.49 — 生成UIタブ A1111風ワイルドカード展開・ギャラリータブ バグ修正2件** — 生成UIタブ — A1111風SPAサイドワイルドカード展開
|
||||
- **2026-06-22: v0.3.48 — ギャラリータブ 画像ダウンロード・一括エクスポート機能追加** — 詳細パネルの画像プレビューにダウンロード機能を追加
|
||||
- **2026-06-22: v0.3.47 — モデルタブ ファイル移動後グループ状態修正** — モデル移動後のグループ状態不整合を修正
|
||||
- **2026-06-21: v0.3.46 — プロンプトタブ AI Assistant LM Studio対応・設定タブ AI Assistant設定に変更** — 設定タブ「AI Assistant設定(プロンプトタブ)」
|
||||
- **2026-06-21: v0.3.45 — グループ孤立エントリ自動クリーンアップ・Galleryタブ Shift+クリック範囲選択** — グループ孤立エントリ自動クリーンアップ
|
||||
- **2026-06-21: v0.3.44 — Galleryタブ高速化・比較モード・バルク強化・バルクバーUI整理** — フェーズ2-A: サーバーサイドサムネイル生成
|
||||
- **2026-06-21: ヘルプタブ改善 — Gallery mode 多言語対応・フォント拡大・検索機能追加** — Gallery mode の多言語対応
|
||||
- **2026-06-21: Send to Canvas — window.opener経由のキャンバス直接ロード対応**
|
||||
- **2026-06-20: API形式ワークフローのキャンバス読み込み修正・画像メタデータ埋め込み修正** — API形式ワークフローをキャンバスへドラッグすると空になる問題を修正
|
||||
- **2026-06-20: セキュリティ修正(XSS・パスバリデーション)** — 修正内容
|
||||
- **2026-06-20: WFS_GalleryFeeder — ComfyUI キャンバス上コントロール追加** — 新規ファイル(`web/comfyui/gallery_feeder_extension.js`)
|
||||
- **2026-06-20: __Feeder__ グループ保護 + サムネイル F ボタン + seed バグ修正** — `__Feeder__` グループの予約済み保護
|
||||
- **2026-06-20: Gallery Feeder 機能追加(WFS_GalleryFeeder ノード)** — 概要
|
||||
- **2026-06-19: Generate UI — ImpactWildcardEncode/Processor プロンプト検出バグ修正(v0.3.41)** — 修正内容(`static/js/comfyui-workflow.js`)
|
||||
- **2026-06-19: AI TOOL タブ Chat ペイン・ワイルドカード生成追加(v0.3.40)** — Chat ペイン(`templates/index.html`, `static/css/main.css`, `static/js/ai-tab.js`, `web/comfyui/node_sets_menu.js`)
|
||||
- **2026-06-18: Send to Canvas機能追加(ワークフロータブ・ギャラリータブ→LibraryタイトルDnD)** — ワークフロータブ — Send to Canvas(`static/js/workflow-tab.js`, `static/js/app.js`, `templates/index.html`)
|
||||
- **2026-06-15: TaggerシングルGenUI:Pボタン追加+生成UIのZITワークフロープロンプト解析修正** — TaggerシングルGenUI:Pボタン(`templates/index.html`, `static/js/tagger-tab.js`, `static/js/i18n.js`)
|
||||
- **2026-06-15: Taggerバッチ.txt出力追加+Galleryタグ保存バグ修正** — Taggerバッチ.txt出力(`templates/index.html`, `static/js/tagger-tab.js`, `static/js/i18n.js`, `py/routes/tagger_routes.py`, `py/services/tagger_service.py`)
|
||||
- **2026-06-15: Taggerタブ新規実装+ギャラリー詳細パネルUI改善+パストラバーサル修正** — Taggerタブ新規実装
|
||||
- **2026-06-15: 全選択ボタン追加+ギャラリーバルクバーi18n対応** — 変更内容
|
||||
- **2026-06-10: v0.3.36 — コードレビュー修正+リファクタリング+README刷新** — コードレビュー修正(v0.3.35の不具合10件)
|
||||
- **2026-06-10: v0.3.35 — テーブルソート・Load GenUI・ワークフロー保存** — 変更内容
|
||||
- **2026-06-09: v0.3.34 — Modelsタブ テーブルビュー強化・バルク操作改善** — 変更内容
|
||||
- **2026-06-09: v0.3.33 — LoRAペイン・ワークフロー解析バグ修正** — 変更内容
|
||||
- **2026-06-08: v0.3.32 — Lora Loader (LoraManager) LoRA検出対応・ライブラリ機能拡張** — 変更内容
|
||||
- **2026-06-08: v0.3.31 — LoRAペイン Single/Stack タブ分割・GenUI Model LoRA対応強化** — 変更内容
|
||||
- **2026-06-08: v0.3.30 — LoRAスタック トリガーワードバグ修正・ヘルプ更新** — 変更内容
|
||||
- **2026-06-08: v0.3.29 — LoRAセクション統合・ヘルプタブサイドバー・UI細部改善** — 変更内容
|
||||
- **2026-06-08: 生成UIタブ 複数改善 — LoRA Stackトリガーワード連携・Reset Workflowボタン・Inputタブタブ化** — 変更内容
|
||||
- **2026-06-08: Feederタブ UI改善 — (root)自動選択・PREVIEWペイン右移動・RUN中プレビュー反映** — 変更内容
|
||||
- **2026-06-07: v0.3.28 — Batchタブ Sampler/Scheduler対応 + Modelsサムネイルバグ修正** — 概要
|
||||
- **2026-06-07: v0.3.27 — model-and-prompt-from-metadata カスタムノード対応 + README修正** — 概要
|
||||
- **2026-06-07: v0.3.26 — Stack LORA SYNTAX 表示修正 + 全体強度調整 UI** — 概要
|
||||
- **2026-06-07: v0.3.25 — Lora Loader (LoraManager) 対応 + Stack 有効無効切り替え** — 概要
|
||||
- **2026-06-06: v0.3.24 — Lora Stack 登録機能 + 生成UI Model タブ 3ペイン化** — 概要
|
||||
- **2026-06-05: v0.3.23 — バグ修正2件** — バグ修正
|
||||
- **2026-06-05: v0.3.22 — カードビュー廃止 + Batch タイプ切り替え UI + 4タイプバッチ生成** — 概要
|
||||
- **2026-06-05: (未リリース) — 生成UIタブ Batch 4タイプ対応(Lora/Prompt/Workflow グループ選択 + BATCH QUEUE 4列化)** — 概要
|
||||
- **2026-06-05: (未リリース) — Prompt・Workflow Batchグループ登録UI** — 概要
|
||||
- **2026-06-04: (未リリース) — モデルBatchグループ登録UI(サムネイル/カード/テーブル・フィルター・一括解除)** — 概要
|
||||
- **2026-06-04: (未リリース) — Batch予約グループ・Presets Save後リセット・グループフィルター種別絞り込み** — 概要
|
||||
- **2026-05-31: v0.3.21 — CivitAI ホスト設定・Sample サブタブ・URL 修正・ヘルプ i18n 整備** — 概要
|
||||
- **2026-05-31: v0.3.20 — CivitAI 詳細パネル強化(Type / Hash 表示・URL 修正・画像別タブ)** — 概要
|
||||
- **2026-05-31: v0.3.19 — CivitAI 連携強化(バッチ高速化・リトライ・APIキー・フィールド拡充)** — 概要
|
||||
- **2026-05-31: v0.3.18 — Batch タブ追加・Models 複数選択強化** — 概要
|
||||
- **2026-05-31: v0.3.17 patch — SSLコンテキスト セキュリティ強化** — 概要
|
||||
- **2026-05-30: v0.3.17 — SSL証明書エラー修正・Lora Manager対応** — 概要
|
||||
- **2026-05-20: v0.3.16 — CivitAIプレビューフォールバック表示** — 概要
|
||||
- **2026-05-20: v0.3.15 — サンプルワークフロー同梱・CivitAI自動プレビュー・Create Tagsオプション** — 概要
|
||||
- **2026-05-20: AI TOOL タブ 英語化(SPA・Libraryパネル)** — 概要
|
||||
- **2026-05-19: v0.3.14 — AI TOOL タブ 3ペイン化・RAW JSON 文字色カスタマイズ** — 概要
|
||||
- **2026-05-19: v0.3.13 — AI タブ(A)追加** — 概要
|
||||
- **2026-05-18: v0.3.12 リリース — README スクリーンショット刷新 / workflow_analyzer 拡張** — 概要
|
||||
- **2026-05-17: RAW JSON パネルに検索機能を追加** — 概要
|
||||
- **2026-05-17: v0.3.11 Metadata タブ対応ノードタイプ拡張 / Settings テキストサイズ UI 追加** — 概要
|
||||
- **2026-05-17: v0.3.10 Information タブ キャンバスドラッグ対応 / ノードタイプ拡張 / バグ修正** — 概要
|
||||
- **2026-05-16: v0.3.9 サイドパネル I タブ追加 / トップバーアイコン修正** — 概要
|
||||
- **2026-05-16: v0.3.8 Metadata タブ: data9 対応 / プロンプト判別不能テキスト** — 概要
|
||||
- **2026-05-15: v0.3.7 Metadata タブ: Flux2 / Qwen / Z-Image 対応** — 概要
|
||||
- **2026-05-15: v0.3.6 Metadata タブ追加** — 概要
|
||||
- **2026-05-09: v0.3.5 GenerateUI — Feeder サブタブ・ワークフロー解析精度向上** — 概要
|
||||
- **2026-04-29: v0.3.4 GenerateUI — Checkpoint Batch 刷新・Settings 横並びレイアウト・ヘルプ i18n 修正** — 概要
|
||||
- **2026-04-28: v0.3.3 Promptタブ — ワイルドカード支援パネル・Impact Packシンボリックリンク連携・サブディレクトリ対応・GenerateUI修正** — 概要
|
||||
- **2026-04-28: v0.3.2 生成UIタブ — Checkpoint Batch・Seed UIレイアウト修正** — 概要
|
||||
- **2026-04-27: v0.3.1 ギャラリー拡張(フォルダ/ファイル操作・ワークフロー保存)** — 概要
|
||||
- **2026-04-18: v0.3.0 モデルグループ表示改善(全タイプ横断・サイドパネル修正)** — 概要
|
||||
- **2026-04-18: v0.2.9 モデル有効/無効・複数選択・一括削除** — 概要
|
||||
- **2026-04-14: v0.2.8 データ保存先変更・エクスポート/インポート機能追加** — 概要
|
||||
- **2026-04-06: v0.2.6 UI改善・バッジ保存修正・フォルダフィルター追加** — 概要
|
||||
- **2026-04-04: v0.2.5 追加修正** — 概要
|
||||
- **2026-04-03: v0.2.5 サイドパネル Category/Package サブタブ・テーマ設定・Workflow修正** — 概要
|
||||
- **2026-03-29: v0.2.4 GenerateUIタブ レイアウト再設計** — 概要
|
||||
- **2026-03-29: v0.2.3 バッジ統一・GenUI Model・サイドパネルModels拡充** — 概要
|
||||
- **2026-03-28: v0.2.2 Modelsタブ・CivitAI連携** — 概要
|
||||
- **2026-03-27: テーマカスタマイズ機能・設定タブ改善** — 概要
|
||||
- **2026-03-26: トップバーボタンアイコン修正・Appモードバッジ調査** — 概要
|
||||
- **2026-03-25: v0.2.0 プロンプトプリセット機能・Workflow Studio Library** — 概要
|
||||
- **2026-03-24: v0.1.9 サイドパネルUI改善・保存ダイアログ・バグ修正** — 概要
|
||||
- **2026-03-24: v0.1.8 WF & Node Library サイドパネル拡張** — 概要
|
||||
- **2026-03-23: v0.1.7 Nodes タブ追加** — 概要
|
||||
- **2026-03-23: v0.1.6 パストラバーサル脆弱性修正** — 概要
|
||||
- **2026-03-22: v0.1.5 テーマシステム追加** — 概要
|
||||
- **2026-03-22: v0.1.4 App形式対応・プリセットコピー・分析バグ修正** — 概要
|
||||
- **2026-03-21: v0.1.3 ヘルプ&サポートタブ追加** — 概要
|
||||
- **2026-03-19: v0.1.2 リリース** — 概要
|
||||
- **2026-03-19: v0.1.1 リリース** — 概要
|
||||
- **2026-03-18: プロジェクトリネーム & アイコン適用** — 概要
|
||||
- **2026-03-18: デフォルトビュー設定追加** — 概要
|
||||
- **2026-03-18: MarkdownNote表示専用ノード除外** — 概要
|
||||
- **2026-03-18: サブグラフワークフロー対応 & 変換精度修正** — 概要
|
||||
- **2026-03-18: Raw JSON同期修正** — 概要
|
||||
- **プロジェクト構成 (現在)**
|
||||
- **2026-03-18: JSONシンタックスハイライト** — 概要
|
||||
- **2026-03-18: プロンプトタブ 中国語↔英語翻訳追加** — 概要
|
||||
- **2026-03-18: v0.1.0 リリース** — 概要
|
||||
- **2026-04-13: Gallery タブ追加 & 全タブ Group UI 統一** — 概要
|
||||
@@ -4,6 +4,62 @@
|
||||
|
||||
---
|
||||
|
||||
## v0.4.4
|
||||
|
||||
### 機能追加: Lab/GenerateUI ModelタブへLoRAバイパス機能 + AI TOOL Chatの画像生成パラメータ拡張
|
||||
|
||||
ユーザーから「生成UIタブのLab機能のLora、ModelタブのLoraのLoraノード自体をバイパスする機能を追加したい。Lab機能ではキーフレームごとに切り替え対応可能としたい」との要望を受けて着手。同じセッション内で続けて「Lab機能のLoraにLora syntax、Trigger wordsの適用を可能にしたい」「AI TOOLタブのCHATの画像生成でチャットによる設定項目を増やしたい」という要望にも順次対応した一連のセッション。
|
||||
|
||||
**1. LoRAノードのバイパス機能**: 実装方針の分岐点(ComfyUI本来のBypass(mode:4)相当の配線迂回か、strength=0にする簡易実装か)をAskUserQuestionでユーザーに確認し、簡易実装(strength=0)を選択。理由は、Lab/GenerateUIが扱う`workflow`は既に`comfyui-workflow.js`の`convertUiToApi()`でAPI形式に変換済みで`mode`の概念自体が失われており、`mode:4`を書き込んでも実行エンジンには一切効果がないため——本物のBypass相当の配線再接続を自前実装するのは過剰投資と判断した。
|
||||
- **GenerateUI Model タブ**: LoRA列ヘッダーの「Refresh Stack group」ボタン左に⛔トグルボタンを追加(`comfyui-editor.js`の`toggleLoraNodeBypass()`/`_loraNodeBypass`/`_loraBypassSnapshot`)。対象ノードのstrength値をゼロ化し、退避したスナップショットから復元をトグルする。Lora Managerノードの場合はLoRAリスト自体(複数エントリ)は差し替えず維持したまま各エントリのstrength/clipStrengthだけを0にする。Single/Stack Applyボタンを押すとバイパス状態は自動クリアされ、あとから無言で再適用されることはない。
|
||||
- **Lab機能**: 既存の行単位「bypassed」(キーフレーム自体を無視するスキップ機能)とは独立した、LoRA列専用の`nodeBypass`フィールドをキーフレームの値に追加。編集モーダルに「LoRAノードをバイパス(強度を0にする)」チェックボックスを新設。`_applyLabLoraToWorkflow()`で`nodeBypass`時はstrength/active/textを0/false/空に強制する。
|
||||
|
||||
**2. Lab: LoRA syntax / Trigger words のポジティブプロンプト適用**: LoRA値に`applyToPrompt`フィールドを追加。ONにすると`<lora:stem:strengthModel:strengthClip>`とCivitAIキャッシュのtrainedWordsをポジティブプロンプトへカンマ区切りで追加する(`comfyui-editor.js`のSingle Applyと同じフォーマット)。「次のキーフレームでLora変更時はクリアされた上で新たなloraで追加」という要件は、`_buildWorkflowForIteration()`に`prevLoraInjection`引数を追加し、**前回のイテレーションが正確に注入した文字列だけ**を次回ビルド時に文字列一致で取り除いてから新規注入を追加する設計で実現した——正規表現による汎用パターン除去ではなく、自分が直前に足した分だけを覚えて消す方式なので、ユーザーが手で書いた他のプロンプト内容を誤って消す心配がない。metadata/civitaiCacheはRun開始時に一度だけフェッチしてキャッシュする。
|
||||
|
||||
**3. AI TOOL Chat: generate_imageツールのパラメータ拡張**: AskUserQuestionで追加したい項目をユーザーに確認し、KSampler系(steps/cfg/sampler_name/scheduler/denoise/seed)・画像サイズ(width/height)・生成枚数(batch_size)を選択。`comfyui-editor.js`に`setGenerationParams()`を新設し、Lab機能の`_buildWorkflowForIteration`と同じ`sampler_nodes`のstepsNodeId/cfgNodeId等のノードID間接参照パターンを再利用することで、Advanced Samplingワークフロー(Flux.1/2、SD3.5等でKSamplerが複数ノードに分割される構成)にも対応した。seedは`window._wfmGenerateTab.generate()`(実体は`handleGenerate`)へのオプション引数として渡すよう、`generate-tab.js`の`handleGenerate`/`_coreGenerate`にgenOptions引数を追加(既存の呼び出し元との後方互換は維持)。batch_size>1時は複数画像がチャットバブルに表示されるよう`appendChatBubble`に`imageUrls`(複数URL)対応を追加した。実機のOllama(ornith-1.5:9b)でツール呼び出しから画像生成までEnd-to-Endで検証し、指定した各パラメータが正しく反映されることを確認した。
|
||||
|
||||
**4. Resolution Selectorノード対応**: ユーザーから実際のワークフロー画像(aspect_ratio COMBO・megapixels・multipleウィジェットを持つ「Resolution Selector」ノード)を見せられ、「3:2などで指定可能か」と問われて調査。`/object_info/ResolutionSelector`で確認したところComfyUI本体組み込みノード(`comfy_extras.nodes_resolution`)で、aspect_ratioは`"3:2 (Photo)"`のような固定ラベル付きCOMBO値と判明した。当初のwidth/height直接指定実装ではこの手のノードに非対応だったため、追加で以下を実装:
|
||||
- `comfyui-workflow.js`の`analyzeWorkflow()`に`resolution_selector_nodes`カテゴリを新設。
|
||||
- `comfyui-editor.js`に`resolveAspectRatioOption(nodeType, requested)`を新設。LLMが渡す短い`"3:2"`形式を、`/object_info`から取得した実際のCOMBO選択肢と前方一致でマッチさせ`"3:2 (Photo)"`のような完全な文字列に解決する(未知の比率はそのままフォールバックし、ComfyUI側の検証エラーに委ねる)。
|
||||
- `setGenerationParams()`にResolution Selector優先ロジックを追加:aspect_ratio指定時はEmptyLatentImageへのwidth/height直接書き込みをスキップし、ResolutionSelectorノードのaspect_ratioだけを書き換える——EmptyLatentImageのwidth/heightがResolutionSelectorの出力へのリンクになっている場合、直接数値を書き込むとそのリンクを上書きしてしまい、意図した比率指定が無視されてしまうため。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、`helpLab16`/`helpLab17`(Lab LoRAバイパス・プロンプト適用)、`helpGen29`(GenerateUI Model LoRAバイパスボタン)を新規追加し、`helpAi3`(AI TOOL Chatの説明)を拡張パラメータの説明を含めて更新した。README.mdのGenerateUI Tab/Lab subtab/AI TOOL Tabの各セクションも同内容で更新した。
|
||||
|
||||
**検証**: 全JSファイルを`node --check`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルのみ両フォルダへ同期しハッシュ一致を確認。Kapture MCP経由で実機ブラウザ操作: (1) GenerateUI ModelタブのLoRAバイパストグルでLora Managerノードのloras配列のstrength/clipStrengthが0⇔元の値へ正しく切り替わることを確認、(2) LabタブのLoRAキーフレームモーダルで両チェックボックスが表示・保存され、セル表示にラベルが反映されることを確認、(3) 実際にLabをRunし`/history`APIから最終ワークフローの`CLIPTextEncodeEditPlus.text_edit`を直接確認してLoRA構文+トリガーワードが正しく追加されていることを確認、(4) Ollama(ornith-1.5:9b)へChatで生成パラメータ付きの依頼を送りgenerate_imageのtool_callsが正しい引数を返し、実際に指定通りのwidth/height/seed/batch_sizeで2枚の画像が生成されることをネットワークログと生成結果の両方で確認、(5) `resolveAspectRatioOption`/`setGenerationParams`をモックworkflow/analysisで単体テストし、ResolutionSelectorノードのaspect_ratioが正しく書き換わりEmptyLatentImageのリンクは維持されることを確認。ヘルプページも実機で3箇所とも日本語表示が正しく反映されていることを確認した。
|
||||
|
||||
**How to apply**:
|
||||
|
||||
1. 「ノードをバイパスする」という要望を受けたら、まずそのワークフローが実行時にどのフォーマット(UI形式 mode:4対応/API形式 mode概念なし)で保持・実行されているかを確認する。今回のケースのように既にAPI形式へ変換済みの場合、本物のBypass(mode:4)相当の配線再接続を自前実装するのは過剰投資になりやすく、「効果が等価な簡易実装(strength=0)」で要件を満たせないか先にユーザーに確認するとよい。
|
||||
2. LLMのfunction calling引数にCOMBO値(enum文字列)を渡させる場合、ワークフロー解析結果には「ノードの現在値」しか含まれず「取りうる全選択肢」は含まれないことが多い。`/object_info`をその場でフェッチして選択肢を取得し、LLMの短い入力("3:2")を実際の完全な文字列("3:2 (Photo)")へ解決するワンクッションを挟むと、ノードごとに異なるラベル文言に振り回されずに済む。
|
||||
3. 「切り替えたらクリアしてから新しいものを追加する」という要求は、正規表現による汎用パターン除去よりも「前回自分が何を注入したかを正確に覚えておいて、その文字列だけを取り除く」設計の方が、ユーザーが手で書いた他のテキストを誤って消すリスクが低く安全。
|
||||
|
||||
関連: [[project_v042_lab_model_column]], [[project_dev_deploy_sync]]
|
||||
|
||||
---
|
||||
|
||||
## v0.4.3
|
||||
|
||||
### バグ修正: 共有モーダルのお気に入り☆/リネームがLabモーダルへ残留 + Lab Model列の+/-をCheckpoint/LoRA/VAEで完全独立化
|
||||
|
||||
v0.4.2リリース直後、ユーザーからLabタブに関する2件のフィードバックを受けて修正した一連のセッション。
|
||||
|
||||
**1. 共有モーダルのお気に入り☆・リネームがLabモーダルに残留するバグ**: ユーザーから「Labタブモーダルについて確認したい。各モーダルでお気に入り☆、名前変更が可能なようですがこれは機能していますか?」との指摘を受けて調査。Workflow Studioの全「詳細モーダル」(Workflow/Models/Nodesタブの詳細表示、Labのキーフレーム編集)は`#wfm-modal-overlay`というDOMを1つだけ共有しており、共通の`openModal()`(`app.js`)はタイトルの**テキストだけ**を書き換え、呼び出し元が動的に注入したヘッダー要素(お気に入り☆ボタン、タイトルクリックでリネームするイベントリスナー)は一切クリアしていなかった。そのため「Workflowタブでワークフローの詳細モーダルを開いた直後にLabのキーフレームモーダルを開く」と、直前のワークフロー用の☆ボタンとリネームリスナーがそのまま残留表示され、Lab側で☆をクリックすると**Labのキーフレームとは無関係に、直前に見ていたワークフローのお気に入り状態が実際に切り替わってしまう**ことをPlaywright MCPで実機再現・立証した。`openModal()`にクリーンアップ処理(`.wfm-fav-btn`の除去、タイトル要素を`cloneNode`で複製・差し替えて前の呼び出し元のクリックリスナー/属性を一掃、リネーム用隠し入力欄のリセット)を追加して修正。副次効果として、Workflow詳細モーダルを連続で複数回開いた際にタイトルのクリックリスナーが際限なく累積していく別の潜在バグ(`workflow-tab.js`側にはリネーム用clickリスナーの重複防止ガードが無かった)も同時に解消した。
|
||||
|
||||
**2. Lab Model列の+/-が独立していなかった問題**: v0.4.2で実装したModel列(Checkpoint/LoRA/VAEを1つの`{checkpoint, lora, vae}`マージ行として1つのキーフレーム配列にまとめる設計)には設計上の欠陥があった。ユーザーから「checkpoint/lora/vaeいずれかで+で追加すると他のモデルも追加されてしまいます。逆に-で他の2つも削除される」との指摘を受け、根本的な設計見直しを実施。マージ行方式ではなく、**Checkpoint・LoRA・VAEを(Prompt・KSamplerと全く同じ)完全に独立したキーフレーム配列に戻し**、C/L/Vトグルは「今どの配列を表示・操作するか」を切り替えるルーティングとしてのみ機能する設計に変更した。この結果、v0.4.2で新設した`_resolveModelKeyframe()`(3フィールドを1行にバンドルしたまま個別に前方向解決するカスタムロジック)が丸ごと不要になり、既存の汎用`_resolveKeyframe()`(Checkpoint/VAE列が元々使っていたもの)にLoRAを1つ追加するだけで済むようになった——継承機能・ハイライト・inheritedヒント表示など、v0.4.2で追加した機能はすべてこのシンプルな独立配列方式のままで自然に成立する。
|
||||
|
||||
**設計上の教訓**: 複数の独立した設定を1つのUI列にまとめる際、「見た目を1列にする」ことと「データを1行にマージする」ことは別の問題であり、後者を選ぶと今回のような「+/-が意図せず道連れになる」バグを生みやすい。データモデルは独立させたまま、UI層(表示するフィールドの選択・共有される+/-ボタンのルーティング)だけを1列に見せかける設計の方が、機能追加・バグ修正のコストが低く、直感的な挙動にもなりやすい。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習に従い、Lab関連ヘルプ(`helpLab14`・`helpLab15`、英/日/中3言語+`templates/index.html`の英語フォールバック)を独立配列方式の挙動に合わせて更新(特に`helpLab15`の「Revert to #1's settingは3項目まとめて戻る」という記述はもう正しくないため修正)。README.mdのLabセクションも同内容に更新した。
|
||||
|
||||
**検証**: 全JSファイルを`node --check`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルのみ両フォルダへ同期しハッシュ一致を確認。Playwright MCP経由で実機ブラウザ操作: (1) Workflow詳細モーダル→Labモーダルの順で開き、☆・リネームツールチップ・cursorスタイルが一切残留しないことを確認、(2) 「C」表示での+/-がCheckpointのみに作用しLoRA/VAEは不変であること、「L」表示での+/-がLoRAのみに作用しCheckpoint/VAEは不変であることをPlan JSONタブのデータ構造で確認、(3) v0.4.2形式(マージ行)のPlan JSONを読み込ませ、新しい独立配列形式へ正しくマイグレーションされることを確認。実際の画像生成を伴うRun実行は割愛。
|
||||
|
||||
**How to apply**:
|
||||
|
||||
1. DOM/モーダルを複数の呼び出し元で共有する設計では、各呼び出し元が「自分固有のUI要素」を動的に注入する場合、その呼び出し元自身が毎回クリーンアップするのではなく、**共有側の入り口関数(今回は`openModal()`)で毎回リセットする**設計にしないと、呼び出し順序に依存した残留バグ・誤操作バグを生む。cloneNode+replaceWithは、addEventListenerの参照を保持していなくてもリスナーを確実に一掃できる簡便な手段。
|
||||
2. 複数の独立した設定を1つのUI要素(列・パネル等)にまとめる要望を実装する際、「データも1つにまとめる」のではなく「データは独立させたままUI層だけ共有する」設計を最初から検討する。今回はユーザーの2度のフィードバック(継承挙動の誤り→+/-の誤り)を経てようやくこの設計にたどり着いたが、根本原因は同じ「フィールドを1行にバンドルした」という初期設計判断にあった。
|
||||
|
||||
---
|
||||
|
||||
## v0.4.2
|
||||
|
||||
### 機能追加: Lab「Model」列統合(Checkpoint / LoRA Single / VAE)+ フィールド単位継承 + C/L/V表示切替
|
||||
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
# README Index — ComfyUI-Workflow-Studio
|
||||
|
||||
Condensed index of README.md: intro summary, feature names per tab (full descriptions live in the excluded README.md), and the installation/usage/requirements/project-structure sections verbatim. Exists so graphify can extract this without feeding the full README (with its screenshot table) to the local Ollama model. Regenerate with `python tools/generate_doc_index.py readme` whenever README.md changes.
|
||||
|
||||
# ComfyUI-Workflow-Studio
|
||||
|
||||
A comprehensive workflow, asset management, and generation UI plugin for [ComfyUI](https://github.com/comfyanonymous/ComfyUI).
|
||||
|
||||
**📁 Management** — organize all your assets in one studio
|
||||
|
||||
- Workflow, model, image, and prompt management
|
||||
- AI-powered prompt writing assistance, translation, and tag generation
|
||||
- File drop & Gallery-linked metadata viewing — inspect and reuse generation settings instantly
|
||||
|
||||
**⚡ GenerateUI** — a productivity-focused generation tab connected to every other tab
|
||||
|
||||
- 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 Model (Checkpoint/LoRA Single/VAE)/Prompt/KSampler
|
||||
|
||||
**📚 Workflow Studio Library** — a multi-function side panel for smooth ComfyUI integration
|
||||
|
||||
- Drag & drop models, nodes, prompts, and workflows straight onto the canvas
|
||||
- **Send to Canvas**: click "Send to Canvas" (Workflow tab) or "Copy & Send Canvas" (Gallery tab) to send the workflow directly to the ComfyUI canvas — UI and API formats both supported
|
||||
- View metadata from images / JSON, then drop the detected models and prompts onto the canvas
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||
|
||||
## Features (tab -> feature names)
|
||||
|
||||
### Workflow Tab
|
||||
Thumbnail / Table views, Thumbnail side panel, Badge filtering, Search, Side panel tabs, Badge management, AI summary, Import / Export, Default view setting, Search clear (✕), Clear all filters (✕ Clear)
|
||||
|
||||
### Canvas Snapshot (v0.1.2)
|
||||
One-click capture, Auto-save as thumbnail, Embedded workflow metadata, Auto-import
|
||||
|
||||
### GenerateUI Tab (v0.3.5)
|
||||
6-tab layout, Save button, Input tab, Model tab, Settings tab, Always-visible Raw JSON, Bypass/Mute node handling, One-click generation, Seed control, Style selector, Create Catalog / Catalog buttons, Batch type selector, Batch tab, UI-to-API conversion, Eagle integration
|
||||
|
||||
### Feeder subtab (v0.3.5 / v0.3.42)
|
||||
ImageFeeder node control, Image library, Selection management, Presets, Continuous Run loop, Index sync, Seed, WFS_GalleryFeeder node, Node & group selector, Image grid, After Gen modes, Run / Stop controls
|
||||
|
||||
### Lab subtab (v0.3.87 – v0.4.4)
|
||||
Setting / Results / Plan JSON sub-panels, Per-column keyframes, Model column — Checkpoint / LoRA Single / VAE, Wildcards, Prompt cell extras, Keyframe #1 live-reflect, Bypass checkbox, LoRA node bypass & prompt injection, T2I workflow support, Use generated image for next, Workflow recall, Plan files, Plan Load, Plan JSON tab, Save index image to Output on Run, Index images are self-contained plan files, Eagle integration
|
||||
|
||||
### Prompt Tab
|
||||
3-column layout, AI chat assistant, Image attachment, Translation, Prompt presets, Preset Manager, Group management, Clipboard copy, GenUI Set, Wildcard/Style tab bar, Wildcard input toolbar, Wildcard file manager, Style manager
|
||||
|
||||
### Metadata Tab (v0.3.8)
|
||||
3-column layout, File drop, Model extraction, LoRA extraction, Prompt extraction, Prompt actions, Format support, Format note
|
||||
|
||||
### Settings Tab
|
||||
2-column layout, Collapsible sections, Theme selection, Theme customization, Workflows directory, Gallery output directory, Eagle connection, Ollama connection, CivitAI Host, CivitAI API Key, Default workflow, Default Checkpoint, Data Management, Text Size, RAW JSON Colors, GenerateUI Model Tab Highlight, Wildcard Integration, G'MIC-Qt Integration, Language
|
||||
|
||||
### Gallery Tab (v0.3.44)
|
||||
Output / Style-Prompt sub-tabs, Image browser, Folder tree root label, Thumbnail / Table views, Folder management, File operations, Download, Multi-select, Image Compare, Prompt search, Server-side filtering, Group management, Thumbnail F button, Favorites, Detail panel, Workflow viewer, Load GenUI button, Image Edit button, Send GenUI Image button, Send CC button, Search clear (✕), Clear all filters (✕ Clear), Workflow auto-save, Output folder configurable, SVG file support, Performance
|
||||
|
||||
### ImagePrompt Gallery subtab (v0.3.94, renamed from Style/Prompt in v0.3.95)
|
||||
Visual prompt library, 3-column layout, Prompt builder (right pane), Plain-text prompt storage, ponyxlWildcardsVault format support, Seed data importer
|
||||
|
||||
### Style_Catalog Gallery subtab (v0.3.95)
|
||||
Visual Style picker, 3-column layout, Positive/Negative panel, Select as Style button, Load in GenerateUI / Open in Metadata Tab buttons, Double-click to enlarge
|
||||
|
||||
### Nodes Tab (v0.1.7)
|
||||
Node Browser, Search & Filter, Package badges, Node detail panel, Node Sets, Right-click context menu, Multi-select, Search clear (✕), Clear all filters (✕ Clear)
|
||||
|
||||
### Models Tab (v0.2.3)
|
||||
Model Browser, Thumbnail / Table views, Table column sort, Search & Filter, User-defined badges, Side panel tabs, CivitAI integration, CivitAI panel — Info / Sample sub-tabs, CivitAI panel states, Batch CivitAI fetch, Detail modal, GenUI Model button, Group management, Table view memo, Preview images, Enable / Disable models, Multi-select & bulk operations, Search clear (✕), Clear all filters (✕ Clear)
|
||||
|
||||
### Tagger Tab (v0.3.38)
|
||||
3 sub-tabs, Model support, Threshold sliders, Ollama VLM, Single tab, Single output options, Batch tab, DB tab, Dependencies
|
||||
|
||||
### Image Edit Tab (v0.3.65)
|
||||
Layer-based image editor, Loading images, New button, Tools, Layer panel, Layer lock, Text quality, Export, Canvas navigation, Undo / Redo
|
||||
|
||||
### AI TOOL Tab (v0.3.14)
|
||||
4-pane layout, Translation pane, Chat pane, Chat pane — image generation via Tool Calling, Chat pane — generate_image extra parameters, TOOLS pane (VLM), TOOLS pane — shared Chat attachment, TOOLS pane (Wildcards), Chat pane — image-to-image (I2I), Chat pane — SVG generation, no ComfyUI workflow involved, Chat pane — Skills, Settings pane, Unsloth backend, Settings pane — Chat Image Generation, Settings pane — Chat I2I Generation, Settings pane — Generation, Settings shared, Backend support, URL security, Lemonade's other endpoints (not integrated)
|
||||
|
||||
### Workflow Studio Library (ComfyUI Side Panel) (v0.3.9)
|
||||
Tab layout (W / N / P / M / I / A), W — Workflows tab, N — Nodes tab, M — Models tab, P — Prompts tab, I — Information tab, A — AI TOOL tab, Drag & drop workflows, Drag & drop nodes, Drag & drop prompts, Send to Canvas, Copy prompts, Double-click, Search, ⚙ Theme settings
|
||||
|
||||
### Help & Support Tab (v0.1.3)
|
||||
Sidebar navigation, Support, Feature list, Keyboard Shortcuts
|
||||
|
||||
## Installation
|
||||
|
||||
### Via ComfyUI Manager (Recommended)
|
||||
|
||||
Search for **Workflow Studio** in ComfyUI Manager and install.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/ketle-man/ComfyUI-Workflow-Studio.git
|
||||
```
|
||||
|
||||
Restart ComfyUI after installation.
|
||||
|
||||
---
|
||||
|
||||
## Sample Workflows
|
||||
|
||||
Sample workflows are included in the `workflows/` folder. You can open them directly in ComfyUI via drag & drop, or load them from the Workflow tab.
|
||||
|
||||
> **Note:** Some sample workflows require additional custom nodes.
|
||||
> If a node is shown as missing (red/unknown) after loading, install the required custom nodes via **ComfyUI Manager** or by cloning the repository from GitHub into your `ComfyUI/custom_nodes/` directory.
|
||||
>
|
||||
> ```bash
|
||||
> cd ComfyUI/custom_nodes
|
||||
> git clone <repository-url>
|
||||
> ```
|
||||
>
|
||||
> After installation, restart ComfyUI to activate the new nodes.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Launch
|
||||
|
||||
Click the **W** button in the ComfyUI top menu bar, or navigate to:
|
||||
|
||||
```
|
||||
http://127.0.0.1:8188/wfm
|
||||
```
|
||||
|
||||
> **Tip:** Shift+Click the W button to open in a new window.
|
||||
|
||||
### Canvas Snapshot
|
||||
|
||||
Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture the current workflow canvas as a thumbnail. The image is automatically saved to the workflow data folder and appears in Workflow Studio's workflow list.
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. **Workflow Tab** — Your workflows from `ComfyUI/user/default/workflows/` are automatically listed
|
||||
2. **Click a workflow** — View thumbnail, JSON details, and metadata in the side panel
|
||||
3. **Load in GenerateUI** — Click the button to load a workflow into the generation interface
|
||||
4. **Adjust parameters** — Modify prompts, models, seeds, and settings via the auto-generated UI
|
||||
5. **Generate** — Hit the Generate button to queue the prompt
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- **ComfyUI** — any recent version (v1.33.9+ recommended for action bar integration)
|
||||
- **Python 3.10+**
|
||||
- **Jinja2** — `pip install jinja2` (usually included with ComfyUI)
|
||||
|
||||
### Optional
|
||||
|
||||
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features
|
||||
- **[LM Studio](https://lmstudio.ai/)** — alternative backend for translation and VLM (OpenAI-compatible API)
|
||||
- **[Lemonade Server](https://lemonade-server.ai/)** — alternative backend for translation, chat, and VLM (OpenAI-compatible API)
|
||||
- **[Unsloth](https://unsloth.ai/)** — alternative backend for translation, chat, and VLM (OpenAI-compatible API); always requires an API key, set via `UNSLOTH_API_KEY` in a `.env` file — `pip install -r requirements.txt` for `python-dotenv`
|
||||
- **[Eagle](https://eagle.cool/)** — for auto-saving generated images with metadata
|
||||
- **[comfyui-mask-editor-one](https://github.com/ketle-man/comfyui-mask-editor-one) (v0.1.9+)** — enables BiRefNet background removal, SAM3 text-prompt segmentation, and ABR stamp-brush library in the Image Edit Mask tool; `birefnet.safetensors` must be placed in `ComfyUI/models/background_removal/` for BiRefNet
|
||||
|
||||
---
|
||||
|
||||
## Supported Languages
|
||||
|
||||
| Language | Status |
|
||||
| -------- | ------ |
|
||||
| English | Full |
|
||||
| Japanese | Full |
|
||||
| Chinese | Full |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ComfyUI-Workflow-Studio/
|
||||
├── __init__.py # ComfyUI entry point
|
||||
├── py/
|
||||
│ ├── wfm.py # Main class & route registration
|
||||
│ ├── config.py # Path configuration
|
||||
│ ├── nodes/
|
||||
│ │ ├── prompt_text.py # WFS_PromptText custom node (positive/negative prompt)
|
||||
│ │ └── gallery_feeder_node.py # WFS_GalleryFeeder custom node (gallery group → IMAGE)
|
||||
│ ├── routes/
|
||||
│ │ ├── workflow_routes.py # Workflow CRUD & analysis API
|
||||
│ │ ├── nodes_routes.py # Nodes metadata & node sets API
|
||||
│ │ ├── models_routes.py # Model metadata, preview, CivitAI API
|
||||
│ │ ├── prompts_routes.py # Prompt presets CRUD API
|
||||
│ │ ├── settings_routes.py # Settings API
|
||||
│ │ ├── ollama_routes.py # Ollama proxy API
|
||||
│ │ ├── eagle_routes.py # Eagle integration API
|
||||
│ │ └── gallery_routes.py # Gallery tab API (Output + ImagePrompt + Style_Catalog sub-tabs)
|
||||
│ └── services/
|
||||
│ ├── workflow_service.py # Workflow file operations
|
||||
│ ├── nodes_service.py # Node metadata & node sets
|
||||
│ ├── models_service.py # Model metadata & preview images
|
||||
│ ├── civitai_service.py # CivitAI API integration & cache
|
||||
│ ├── prompts_service.py # Prompt presets persistence
|
||||
│ ├── workflow_analyzer.py # Model/node detection
|
||||
│ ├── settings_service.py # Settings persistence
|
||||
│ ├── png_extractor.py # PNG metadata extraction
|
||||
│ ├── gallery_service.py # Gallery image scanning, .txt sidecar & ponyxlWildcardsVault prompt resolution
|
||||
│ └── gallery_metadata.py # Gallery per-image metadata store (favorites/tags/memo/groups)
|
||||
├── templates/
|
||||
│ └── index.html # SPA template (Workflow/GenerateUI/Prompt/Metadata/Gallery/Nodes/Models/Settings/Help/AI)
|
||||
├── static/
|
||||
│ ├── favicon.svg # Browser tab icon (W+S Wave)
|
||||
│ ├── css/main.css # Styles
|
||||
│ └── js/
|
||||
│ ├── app.js # App initialization & routing
|
||||
│ ├── workflow-tab.js # Workflow browser
|
||||
│ ├── generate-tab.js # Generation UI
|
||||
│ ├── feeder-tab.js # Feeder subtab (Image Loop + Gallery modes)
|
||||
│ ├── gallery-tab.js # Gallery tab: Output sub-tab (image browser, groups, metadata) + sub-tab switching
|
||||
│ ├── image-prompt-tab.js # Gallery tab: ImagePrompt sub-tab (visual prompt builder)
|
||||
│ ├── style-catalog-tab.js # Gallery tab: Style_Catalog sub-tab (visual Style picker)
|
||||
│ ├── tagger-tab.js # Tagger tab (WD Tagger, DeepDanbooru, Ollama VLM)
|
||||
│ ├── prompt-tab.js # AI assistant & presets
|
||||
│ ├── metadata-tab.js # Metadata extraction & display (PNG/WebP/JSON)
|
||||
│ ├── settings-tab.js # Settings panel
|
||||
│ ├── comfyui-client.js # ComfyUI WebSocket/API client
|
||||
│ ├── nodes-tab.js # Node browser & node sets
|
||||
│ ├── models-tab.js # Model browser & CivitAI integration
|
||||
│ ├── ai-tab.js # AI TOOL tab (Translation | Chat | TOOLS/VLM | Settings)
|
||||
│ ├── comfyui-workflow.js # UI-to-API format conversion
|
||||
│ ├── comfyui-editor.js # Dynamic parameter editor
|
||||
│ ├── json-highlight.js # JSON syntax highlighting
|
||||
│ ├── util.js # Shared utilities (escapeHtml, readJsonStorage, getSettings)
|
||||
│ └── i18n.js # Internationalization
|
||||
├── web/comfyui/
|
||||
│ ├── top_menu_extension.js # ComfyUI menu bar integration
|
||||
│ ├── node_sets_menu.js # Workflow Studio Library side panel
|
||||
│ └── gallery_feeder_extension.js # WFS_GalleryFeeder canvas widgets (After Gen / Run / Stop)
|
||||
├── tools/
|
||||
│ └── import_style_prompt_seed.py # Dev tool: pre-process a wildcards-vault source into ws_image_prompt (not run by the plugin itself)
|
||||
└── data/ # Fallback data dir (used when ComfyUI user/default/ is not found)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) by comfyanonymous
|
||||
- [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) by pythongosssss — Canvas snapshot and PNG workflow embedding implementation reference
|
||||
- [ComfyUI-Lora-Manager](https://github.com/willmiao/ComfyUI-Lora-Manager) — Plugin architecture and UI pattern reference
|
||||
- [Ollama](https://ollama.com/) for local LLM inference
|
||||
- [Eagle](https://eagle.cool/) for image management
|
||||
- [Pillow (PIL Fork)](https://python-pillow.org/) — server-side thumbnail generation (`data/thumb_cache/`)
|
||||
- [ComfyUI-Gallery](https://github.com/PanicTitan/ComfyUI-Gallery) by PanicTitan — thumbnail grid UX reference
|
||||
- [infinite-image-browsing](https://github.com/zanllp/sd-webui-infinite-image-browsing) by zanllp — thumbnail caching and index strategy reference
|
||||
- [Fooocus](https://github.com/lllyasviel/Fooocus) by lllyasviel — style preset JSON format reference
|
||||
|
||||
@@ -22,7 +22,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -98,6 +98,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- **Model tab** — Checkpoint, VAE, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), and Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — single clip: type + device; dual clip: two clip selectors + type + device) are always shown; **ControlNet** and **Hypernetwork** (with Strength field) are collapsed by default (v0.3.98) since they're used less often — click their header to expand; **LoRA** (Single/Stack tabs) has its own column; all selectors have a filter; Raw JSON on the right
|
||||
- **Highlight color** (v0.3.98) — the label of each model type above, plus the LoRA column header, turns a distinct color (default bright green, customizable in the Settings tab) when the loaded workflow actually contains that type of node; stays visible even while ControlNet/Hypernetwork are collapsed, so relevance is visible without expanding
|
||||
- **LoRA Stack Group toggle** (v0.3.92) — for `Lora Loader (LoraManager)` nodes, a **Use Stack Group** checkbox next to the Single/Stack tabs switches what the Stack tab shows and edits: OFF (the default whenever a workflow is loaded) displays and edits the LoRAs actually configured in the workflow's own Lora Manager node — correctly showing every LoRA it holds, not just the first one; ON switches to the LoRA group registered in the Models tab instead; toggling either direction clears the node's LoRA list first, then loads it from the newly selected source, and switching back to OFF restores exactly the list that was present when the workflow was loaded
|
||||
- **LoRA Bypass button** (v0.4.4) — a ⛔ toggle next to the Refresh Stack group button in the LoRA column header; forces the target LoRA node's strength to 0 (`strength_model`/`strength_clip` for a standard LoraLoader, or every entry's `strength`/`clipStrength` for a Lora Manager node — its LoRA list stays intact, not replaced) without touching the actual ComfyUI node's mode; click again to restore the exact values that were there before toggling; targets whichever node Single/Stack Apply default to (the workflow's Lora Manager node if it has one, else the first LoRA node); clicking a Single or Stack Apply button clears the bypass state so it can't silently reassert itself later
|
||||
- **Settings tab** — KSampler and Latent Image side by side at 50% width each; Raw JSON on the right
|
||||
- **Always-visible Raw JSON** — edit the API-format JSON directly from any tab with syntax highlighting; Apply button reloads the workflow; built-in **search bar** (always shown) finds all matches as you type with count display (`3/12`); navigate with ↑/↓ buttons or Enter / Shift+Enter; Escape or ✕ clears; current match highlighted in orange, other matches in yellow
|
||||
- **Bypass/Mute node handling** (v0.3.75) — loading a UI-format workflow with Bypass-mode nodes now reroutes their wires to the upstream source (matching same-type input/output slots, recursively through chained bypasses) instead of leaving a dangling reference that failed validation; Mute-mode nodes are excluded without rerouting, same as ComfyUI itself; when either is present, a note listing the affected nodes appears above the Raw JSON search bar in the Input/Model/Settings tabs
|
||||
@@ -146,17 +147,18 @@ Two independent modes selectable via **[Image Loop] / [Gallery]** toggle buttons
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Lab subtab (v0.3.87 – v0.4.2)</h3></summary>
|
||||
<summary><h3>Lab subtab (v0.3.87 – v0.4.4)</h3></summary>
|
||||
|
||||
An experimental batch generator: runs the workflow currently loaded in GenerateUI N times, letting Model (Checkpoint/LoRA Single/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 + 3 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** — Model, Prompt, KSampler each hold an independent list of `{iteration, value}` keyframes; +/− per column adds/removes the last keyframe (iteration #1 is fixed and can't be removed); any keyframe other than #1 can also be deleted individually from inside its own edit modal (v0.3.93), not just the last one; 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 — except the Model column, which resolves each of its three fields independently (see below); lowering the Batch count below an existing keyframe's iteration doesn't delete or renumber it — a warning toast appears and it just stays dormant until Batch count is raised again (v0.3.93)
|
||||
- **Model column — Checkpoint / LoRA Single / VAE** (v0.4.2) — the old separate Checkpoint and VAE columns merged into one Model column; a `[C][L][V]` toggle in its header picks which single field its cells summarize and its edit modal shows — switching the toggle never touches the other two fields, Save only updates whichever one is currently shown — and each button lights up the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type; the three fields carry forward independently across keyframes, so a keyframe that only sets a LoRA doesn't reset Checkpoint/VAE back to the workflow default — it keeps whatever an earlier keyframe last set until a later one explicitly changes it (a field left blank in the edit modal shows "(inherits: ...)" instead of "(workflow default)" when there's something to inherit); "revert to #1's setting" still resets all three fields at once. LoRA support is Single only (no Stack, matching the experimental scope of this tab); if a LoRA is set but the loaded workflow has no LoRA node at all, Run warns once and skips it rather than auto-inserting one — unlike VAE (which does auto-inject a VAELoader when missing), wiring in a LoraLoader would require rewiring the Checkpoint's model/clip outputs through it
|
||||
- **Per-column keyframes** — Checkpoint, LoRA, VAE, Prompt, and KSampler each hold their own fully independent list of `{iteration, value}` keyframes; +/− per column adds/removes the last keyframe (iteration #1 is fixed and can't be removed); any keyframe other than #1 can also be deleted individually from inside its own edit modal (v0.3.93), not just the last one; 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; lowering the Batch count below an existing keyframe's iteration doesn't delete or renumber it — a warning toast appears and it just stays dormant until Batch count is raised again (v0.3.93)
|
||||
- **Model column — Checkpoint / LoRA Single / VAE** (v0.4.2, redesigned v0.4.3) — Checkpoint and VAE (and now LoRA) share one visual "Model" column; a `[C][L][V]` toggle in its header picks which of the three independent keyframe lists the shared cell list, its own +/-, and its edit modal currently operate on — each behaves exactly like its own column (same as Prompt/KSampler), just sharing the Model column's screen space, so adding, removing, editing, reverting, or bypassing a keyframe while viewing one never touches the other two. Each toggle button lights up the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. A field left blank in a later keyframe's edit modal shows "(inherits: ...)" instead of "(workflow default)" when an earlier keyframe in that same list already set something. LoRA support is Single only (no Stack, matching the experimental scope of this tab); if a LoRA is set but the loaded workflow has no LoRA node at all, Run warns once and skips it rather than auto-inserting one — unlike VAE (which does auto-inject a VAELoader when missing), wiring in a LoraLoader would require rewiring the Checkpoint's model/clip outputs through it
|
||||
- **Wildcards** (v0.3.93) — Prompt keyframes' Positive/Negative text supports the same A1111-style `__name__` syntax as the main GenerateUI prompt fields; each keyframe is expanded fresh right before that iteration runs, so reusing the same keyframe across iterations can still draw a different random line each time
|
||||
- **Prompt cell extras** (v0.3.88, refined in v0.3.89, v0.3.93, and v0.3.97) — **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); **Get from Previous** (v0.3.97) copies both Positive and Negative wholesale from the keyframe immediately before this one in the same column (e.g. editing #3 copies #2's prompt; not shown on keyframe #1, which has no earlier row); **Clear** (v0.3.97) empties both fields; **Style** row — an **Apply** button merges whichever style is currently selected in GenerateUI's own top-bar Style dropdown into the Positive/Negative text below, comma-separated after any existing text (never clears it) and works regardless of that top-bar's own Style enable checkbox; a checkbox next to it tracks on/off and shows the applied style's name beside itself (not on the button) — unchecking only clears that name label, the already-merged text stays and is yours to edit/delete by hand
|
||||
- **Keyframe #1 live-reflect** (v0.3.97) — in every column, keyframe #1 shows the currently loaded workflow's own live setting instead of a blank placeholder — for the Model column this covers Checkpoint and VAE only, LoRA is never live-reflected since there's no reliable way to infer "the" active LoRA back out of a workflow node — both in its grid cell (dashed border) and pre-filled into its edit modal, for as long as it hasn't been explicitly saved with an override; saving it — even unchanged — locks that value in as a real override and the dashed/live styling goes away; switching to the Lab subtab always refreshes this against whatever is currently loaded in GenerateUI
|
||||
- **Bypass checkbox** (v0.3.97) — available on every keyframe including #1; skips that row entirely when picking which setting applies at a given iteration, as if it didn't exist, so whatever was active before it keeps running instead; Lab-internal only, never touches the actual ComfyUI node's mode; disables that row's value fields and its Revert checkbox while checked (mutually exclusive with Revert); the cell shows an amber "⏭ Bypassed" label, taking priority over both the Revert and live-reflect display
|
||||
- **LoRA node bypass & prompt injection** (v0.4.4) — inside the LoRA keyframe's own edit modal, two checkboxes independent of the row-level Bypass above: **Bypass LoRA node** forces that keyframe's `strength_model`/`strength_clip` to 0 at apply-time — the behavioral equivalent of ComfyUI's node Bypass (mode:4), since the workflow used here is already API-format with no `mode` concept; **Apply LoRA syntax + trigger words to Positive prompt** appends `<lora:name:strengthModel:strengthClip>` plus that LoRA's cached CivitAI trigger words to the Positive prompt, comma-separated, at apply-time — the same format GenerateUI's own Model tab Apply button writes. Switching keyframes (a different LoRA, this checkbox turned off, or the row Bypassed) always strips exactly what the previous iteration injected before adding anything new, so a batch never accumulates every LoRA it has used; the prompt checkbox is disabled automatically while Bypass LoRA node is checked
|
||||
- **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 Model/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)
|
||||
@@ -379,6 +381,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **Reliability fix** (v0.3.98) — switched from a raw single-string prompt to a system-role chat request (Ollama `/api/chat`, LM Studio/Lemonade `/v1/chat/completions` with a `system` message) so small local models follow the "translation only" instruction more reliably; output is cleaned of common LLM preambles/quotes, and a warning toast appears if the model appears to have echoed the source text unchanged instead of translating it
|
||||
- **Chat pane** (v0.3.40) — multi-turn conversation with the LLM; full conversation history sent each turn for context; Enter to send, Shift+Enter for a newline; Clear button resets history; Ollama uses `/api/chat`, LM Studio, Lemonade, and Unsloth use `/v1/chat/completions`
|
||||
- **Chat pane — image generation via Tool Calling** (v0.3.73) — if the selected model supports tool calling and the user asks for an image, a `generate_image` tool is invoked and the request runs through the same generation pipeline as the GenerateUI Generate button, with the result shown inline in the chat; an "Allow image generation" checkbox in the input row toggles this on/off (default on); the workflow used is the one currently loaded in GenerateUI, or a dedicated saved workflow configured in the Settings pane; models verified to work well for tool calling and vision: **Gemma 4:e4b**, **Qwen 3.5:9b**, **Ministral 3:3b** (Ollama)
|
||||
- **Chat pane — generate_image extra parameters** (v0.4.4) — besides `prompt`/`negative_prompt`, the tool call also accepts optional `steps`, `cfg`, `sampler_name`, `scheduler`, `denoise`, `seed`, `width`, `height`, `batch_size` (number of images produced in that one call), and `aspect_ratio` (a ratio preset like `3:2`, resolved against the node's actual COMBO options via `/object_info` and applied only if the workflow has a **Resolution Selector** node feeding width/height — ignored otherwise); anything the model doesn't supply keeps the loaded workflow's own current setting; `batch_size` > 1 shows every generated image inline in the same chat bubble
|
||||
- **TOOLS pane (VLM)** — drop an image into the 110px drop zone, select a task (Describe Image / Create Prompt / Create Tags), and click Run to analyze with a vision model; result shown in the output area with a Copy button
|
||||
- **TOOLS pane — shared Chat attachment** (v0.3.74) — an image dropped in the TOOLS drop zone is also usable as the Chat pane's attachment: it's sent to the LLM as vision input, and used as the base image when `generate_image` runs image-to-image; a ✕ button clears it (from either the TOOLS preview or the Chat attachment indicator); it stays attached across turns until explicitly cleared
|
||||
- **TOOLS pane (Wildcards)** (v0.3.40) — select "Create wildcards" from the task dropdown; enter a category name and count; click Run to generate plain-text wildcard entries one per line (no markdown, no numbering); result can be copied directly into wildcard `.txt` files
|
||||
|
||||
File diff suppressed because one or more lines are too long
+353
-546
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+81005
-85287
File diff suppressed because it is too large
Load Diff
+561
-543
File diff suppressed because it is too large
Load Diff
+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.4.2"
|
||||
version = "0.4.4"
|
||||
license = "MIT"
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
@@ -270,6 +270,16 @@
|
||||
color: var(--wfm-warning);
|
||||
}
|
||||
|
||||
/* LoRA node-bypass (strength forced to 0 at apply-time) — red to distinguish from the
|
||||
amber row-level bypassed above; the row itself still applies, only the LoRA doesn't. */
|
||||
.wfm-lab-cell.wfm-lab-cell-node-bypassed {
|
||||
border-color: var(--wfm-danger);
|
||||
}
|
||||
|
||||
.wfm-lab-cell.wfm-lab-cell-node-bypassed .wfm-lab-cell-value {
|
||||
color: var(--wfm-danger);
|
||||
}
|
||||
|
||||
.wfm-lab-cell-applied {
|
||||
margin-top: 3px;
|
||||
padding-top: 3px;
|
||||
|
||||
+14
-1
@@ -653,8 +653,8 @@ body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wfm-lora-bypass-btn,
|
||||
.wfm-lora-refresh-btn {
|
||||
margin-left: auto;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
padding: 2px 5px;
|
||||
@@ -662,8 +662,21 @@ body {
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.wfm-lora-bypass-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
.wfm-lora-bypass-btn:hover,
|
||||
.wfm-lora-refresh-btn:hover { opacity: 1; }
|
||||
|
||||
/* Bypass is active for the current LoRA target node: strength forced to 0 (see
|
||||
comfyui-editor.js renderLoraPane / _loraNodeBypass). */
|
||||
.wfm-lora-bypass-btn.active {
|
||||
opacity: 1;
|
||||
background: var(--wfm-danger);
|
||||
border-color: var(--wfm-danger);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wfm-gen-lora-col-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
+37
-7
@@ -548,12 +548,22 @@ const IMAGE_GEN_TOOLS = [{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "generate_image",
|
||||
description: "Generate an image using the currently loaded ComfyUI workflow. If an image is currently attached to the conversation, this performs image-to-image generation using that image as the base; otherwise it performs text-to-image. Call this when the user asks to create, draw, paint, edit, or transform an image.",
|
||||
description: "Generate an image using the currently loaded ComfyUI workflow. If an image is currently attached to the conversation, this performs image-to-image generation using that image as the base; otherwise it performs text-to-image. Call this when the user asks to create, draw, paint, edit, or transform an image. All parameters besides prompt are optional — omit any the user didn't specify and the workflow's own current setting is kept.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
prompt: { type: "string", description: "Positive prompt describing the desired image." },
|
||||
negative_prompt: { type: "string", description: "What to avoid in the image. Optional." },
|
||||
steps: { type: "integer", description: "Sampling steps. Optional." },
|
||||
cfg: { type: "number", description: "Classifier-free guidance scale. Optional." },
|
||||
sampler_name: { type: "string", description: "ComfyUI sampler name (e.g. euler, dpmpp_2m, ddim). Optional." },
|
||||
scheduler: { type: "string", description: "ComfyUI scheduler name (e.g. normal, karras, simple). Optional." },
|
||||
denoise: { type: "number", description: "Denoise strength 0-1 (mainly relevant for image-to-image). Optional." },
|
||||
seed: { type: "integer", description: "Fixed seed. Omit for a random seed each time. Optional." },
|
||||
width: { type: "integer", description: "Output image width in pixels. Ignored if the workflow has a Resolution Selector node — use aspect_ratio instead for those. Optional." },
|
||||
height: { type: "integer", description: "Output image height in pixels. Ignored if the workflow has a Resolution Selector node — use aspect_ratio instead for those. Optional." },
|
||||
aspect_ratio: { type: "string", enum: ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9", "21:9"], description: "Aspect ratio preset. Only applied if the workflow has a Resolution Selector node (feeding width/height from a ratio + megapixel target); ignored otherwise. Optional." },
|
||||
batch_size: { type: "integer", description: "Number of images to generate in this single call. Optional." },
|
||||
},
|
||||
required: ["prompt"],
|
||||
},
|
||||
@@ -635,7 +645,7 @@ async function _loadDedicatedChatGenWorkflow(filename) {
|
||||
return { workflow, analysis: comfyWorkflow.analyzeWorkflow(workflow) };
|
||||
}
|
||||
|
||||
async function generateImageFromChat(prompt, negativePrompt) {
|
||||
async function generateImageFromChat(prompt, negativePrompt, params = {}) {
|
||||
if (!window._wfmGenerateTab?.generate) throw new Error(t("aiChatGenerateNotReady"));
|
||||
|
||||
const settings = loadAiSettings();
|
||||
@@ -660,12 +670,24 @@ async function generateImageFromChat(prompt, negativePrompt) {
|
||||
comfyEditor.setPromptText("positive", prompt || "", { workflow, analysis });
|
||||
if (negativePrompt) comfyEditor.setPromptText("negative", negativePrompt, { workflow, analysis });
|
||||
|
||||
const result = await window._wfmGenerateTab.generate(workflow);
|
||||
const { steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size, aspect_ratio } = params;
|
||||
let resolvedAspectRatio = null;
|
||||
const resSelector = analysis.resolution_selector_nodes?.[0];
|
||||
if (aspect_ratio && resSelector) {
|
||||
resolvedAspectRatio = await comfyEditor.resolveAspectRatioOption(resSelector.type, aspect_ratio);
|
||||
}
|
||||
comfyEditor.setGenerationParams({
|
||||
steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size,
|
||||
resolvedAspectRatio, workflow, analysis,
|
||||
});
|
||||
|
||||
const result = await window._wfmGenerateTab.generate(workflow, seed != null ? { seedValue: seed } : {});
|
||||
const images = result?.images || [];
|
||||
if (images.length === 0) throw new Error(t("aiChatNoImageProduced"));
|
||||
|
||||
const img = images[0];
|
||||
return `/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${encodeURIComponent(img.type || "output")}`;
|
||||
return images.map((img) =>
|
||||
`/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${encodeURIComponent(img.type || "output")}`
|
||||
);
|
||||
}
|
||||
|
||||
// アシスタント応答からSVGコードを抽出する(```svg フェンス or 生の <svg>...</svg>)
|
||||
@@ -966,6 +988,14 @@ function appendChatBubble(messagesEl, role, content, opts = {}) {
|
||||
img.className = "wfm-ai-chat-img";
|
||||
div.appendChild(img);
|
||||
}
|
||||
if (opts.imageUrls?.length) {
|
||||
opts.imageUrls.forEach((url) => {
|
||||
const img = document.createElement("img");
|
||||
img.src = url;
|
||||
img.className = "wfm-ai-chat-img";
|
||||
div.appendChild(img);
|
||||
});
|
||||
}
|
||||
if (opts.attachThumb) {
|
||||
const img = document.createElement("img");
|
||||
img.src = opts.attachThumb;
|
||||
@@ -1055,8 +1085,8 @@ function initChatTab() {
|
||||
statusEl.textContent = t("aiStatusGeneratingImage");
|
||||
const args = _parseToolArgs(toolCall);
|
||||
try {
|
||||
const imageUrl = await generateImageFromChat(args.prompt, args.negative_prompt);
|
||||
appendChatBubble(messagesEl, "assistant", "", { imageUrl });
|
||||
const imageUrls = await generateImageFromChat(args.prompt, args.negative_prompt, args);
|
||||
appendChatBubble(messagesEl, "assistant", "", { imageUrls });
|
||||
} catch (genErr) {
|
||||
appendChatBubble(messagesEl, "assistant", `${t("aiToastImageGenFailed")}${genErr.message}`);
|
||||
showToast(t("aiToastImageGenFailed") + genErr.message, "error");
|
||||
|
||||
+24
-1
@@ -42,11 +42,32 @@ export function showToast(message, type = "info", duration = 3000) {
|
||||
|
||||
export function openModal(title, contentHtml) {
|
||||
const overlay = document.getElementById("wfm-modal-overlay");
|
||||
const titleEl = document.getElementById("wfm-modal-title");
|
||||
let titleEl = document.getElementById("wfm-modal-title");
|
||||
const bodyEl = document.getElementById("wfm-modal-body");
|
||||
const titleInput = document.getElementById("wfm-modal-title-input");
|
||||
|
||||
if (!overlay || !titleEl || !bodyEl) return;
|
||||
|
||||
// This modal shell is shared by every caller (Workflow/Models/Nodes detail
|
||||
// modals, Lab's keyframe editor, etc.). Some callers (e.g. workflow-tab.js's
|
||||
// openDetailModal) inject extra header chrome after calling openModal() — a
|
||||
// favorite star button, and a click-to-rename listener + tooltip/cursor on the
|
||||
// title — but nothing ever tore that down again. The next caller to reuse this
|
||||
// same DOM inherited it as stale leftovers: a Lab cell modal could show a
|
||||
// favorite star wired to the PREVIOUS caller's workflow object, and clicking it
|
||||
// silently toggled that unrelated workflow's favorite state. Clean slate every
|
||||
// time, before any caller-specific chrome is added back on top.
|
||||
titleEl.parentElement?.querySelectorAll(".wfm-fav-btn").forEach((el) => el.remove());
|
||||
const freshTitleEl = titleEl.cloneNode(false); // drops any addEventListener from a prior caller
|
||||
freshTitleEl.removeAttribute("title");
|
||||
freshTitleEl.removeAttribute("style"); // also clears any leftover display:none from an interrupted rename
|
||||
titleEl.replaceWith(freshTitleEl);
|
||||
titleEl = freshTitleEl;
|
||||
if (titleInput) {
|
||||
titleInput.style.display = "none";
|
||||
titleInput.value = "";
|
||||
}
|
||||
|
||||
titleEl.textContent = title;
|
||||
bodyEl.innerHTML = contentHtml;
|
||||
overlay.style.display = "flex";
|
||||
@@ -455,6 +476,7 @@ function applyI18nToHtml() {
|
||||
"wfm-help-gen-24": "helpGen24", "wfm-help-gen-25": "helpGen25",
|
||||
"wfm-help-gen-26": "helpGen26", "wfm-help-gen-27": "helpGen27",
|
||||
"wfm-help-gen-28": "helpGen28",
|
||||
"wfm-help-gen-29": "helpGen29",
|
||||
"wfm-help-feeder-title": "helpFeederTitle",
|
||||
"wfm-help-feeder-desc": "helpFeederDesc",
|
||||
"wfm-help-feeder-imgloop-title": "helpFeederImgloopTitle",
|
||||
@@ -480,6 +502,7 @@ function applyI18nToHtml() {
|
||||
"wfm-help-lab-11": "helpLab11", "wfm-help-lab-12": "helpLab12",
|
||||
"wfm-help-lab-13": "helpLab13", "wfm-help-lab-14": "helpLab14",
|
||||
"wfm-help-lab-15": "helpLab15",
|
||||
"wfm-help-lab-16": "helpLab16", "wfm-help-lab-17": "helpLab17",
|
||||
"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",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { comfyUI } from "./comfyui-client.js";
|
||||
import { syncJsonHighlight } from "./json-highlight.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { escapeHtml, setupSearchClearBtn } from "./util.js";
|
||||
import { showToast } from "./app.js";
|
||||
|
||||
// ── Latent Image preset state ─────────────────────────────
|
||||
const _LATENT_PRESET_KEY = "wfm_latent_presets";
|
||||
@@ -156,6 +157,16 @@ function _refreshPresetSelect(customPresets) {
|
||||
let _stackStrengths = {};
|
||||
// { modelFullPath: boolean } — true = active (default)
|
||||
let _stackActive = {};
|
||||
// ── LoRA node-bypass state (GenerateUI Model tab header toggle) ───────────
|
||||
// Force the LoRA node's strength to 0 without touching the actual ComfyUI node's mode —
|
||||
// currentWorkflow is already-converted API format (see comfyui-workflow.js
|
||||
// convertUiToApi), where mode has no effect at execution time, so a real Bypass(mode:4)
|
||||
// can't be reproduced here; zeroing strength is the behavioral equivalent.
|
||||
// { nodeId: boolean } — true = currently bypassed
|
||||
let _loraNodeBypass = {};
|
||||
// { nodeId: { loraName, strModel, strClip } | { loras, text } } — pre-bypass values to
|
||||
// restore when toggled back off. Shape depends on isLoraManager at bypass-time.
|
||||
let _loraBypassSnapshot = {};
|
||||
// { id: string|null, selStart: number, selEnd: number }
|
||||
let _lastPromptFocus = { id: null, selStart: 0, selEnd: 0 };
|
||||
|
||||
@@ -742,6 +753,63 @@ export const comfyEditor = {
|
||||
_initTextEncoderSection(analysis, this.models.textEncoders || [], el).catch(() => {});
|
||||
},
|
||||
|
||||
// Toggles the GenerateUI Model tab's LoRA column-header bypass button: forces the
|
||||
// current target LoRA node's strength to 0 (both tabs' strength fields, whichever
|
||||
// is currently in currentWorkflow), or restores whatever was there before. Targets
|
||||
// the same node Single/Stack Apply default to (first LoraManager, else first LoRA
|
||||
// node) so one button covers both tabs without needing its own node picker.
|
||||
toggleLoraNodeBypass(analysis) {
|
||||
const loraNodes = analysis?.lora_nodes || [];
|
||||
let nodeId = (loraNodes.find((n) => n.is_lora_manager) || loraNodes[0])?.id;
|
||||
let isLoraManager = !!loraNodes.find((n) => String(n.id) === String(nodeId))?.is_lora_manager;
|
||||
if (nodeId == null) {
|
||||
nodeId = Object.keys(comfyUI.currentWorkflow || {}).find((id) => comfyUI.currentWorkflow[id]?.inputs && "lora_name" in comfyUI.currentWorkflow[id].inputs);
|
||||
isLoraManager = false;
|
||||
}
|
||||
const wfNode = nodeId != null ? comfyUI.currentWorkflow?.[nodeId] : null;
|
||||
if (!wfNode) {
|
||||
showToast(t("modelsGenUINoNode", "LoRA"), "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_loraNodeBypass[nodeId]) {
|
||||
// Restore whatever was set before bypassing
|
||||
const snap = _loraBypassSnapshot[nodeId];
|
||||
if (snap) {
|
||||
if (isLoraManager) {
|
||||
wfNode.inputs.loras = { __value__: JSON.parse(JSON.stringify(snap.loras || [])) };
|
||||
wfNode.inputs.text = snap.text ?? "";
|
||||
} else {
|
||||
wfNode.inputs.lora_name = snap.lora_name;
|
||||
wfNode.inputs.strength_model = snap.strength_model;
|
||||
wfNode.inputs.strength_clip = snap.strength_clip;
|
||||
}
|
||||
}
|
||||
delete _loraNodeBypass[nodeId];
|
||||
delete _loraBypassSnapshot[nodeId];
|
||||
} else {
|
||||
// Snapshot current values, then zero the strength (LoraManager: every
|
||||
// entry in its loras list, keeping the list itself intact so un-bypassing
|
||||
// restores the exact same Stack — not just a single-LoRA fallback).
|
||||
if (isLoraManager) {
|
||||
const loras = Array.isArray(wfNode.inputs.loras?.__value__) ? wfNode.inputs.loras.__value__ : [];
|
||||
_loraBypassSnapshot[nodeId] = { loras: JSON.parse(JSON.stringify(loras)), text: wfNode.inputs.text ?? "" };
|
||||
wfNode.inputs.loras = { __value__: loras.map((l) => ({ ...l, strength: 0, clipStrength: 0 })) };
|
||||
wfNode.inputs.text = "";
|
||||
} else {
|
||||
_loraBypassSnapshot[nodeId] = {
|
||||
lora_name: wfNode.inputs.lora_name,
|
||||
strength_model: wfNode.inputs.strength_model,
|
||||
strength_clip: wfNode.inputs.strength_clip,
|
||||
};
|
||||
wfNode.inputs.strength_model = 0;
|
||||
wfNode.inputs.strength_clip = 0;
|
||||
}
|
||||
_loraNodeBypass[nodeId] = true;
|
||||
}
|
||||
_syncRawJson();
|
||||
},
|
||||
|
||||
async renderLoraPane(analysis, containerId, opts = {}) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
@@ -751,6 +819,15 @@ export const comfyEditor = {
|
||||
const loras = this.models.loras || [];
|
||||
const defaultStackTarget = (loraNodes.find((n) => n.is_lora_manager) || loraNodes[0])?.id;
|
||||
const hasLoraManager = loraNodes.some((n) => n.is_lora_manager);
|
||||
|
||||
// Sync the column-header bypass button (lives outside this container's
|
||||
// innerHTML, in the static wfm-gen-lora-col-header) to whatever the target
|
||||
// node's current bypass state is.
|
||||
const bypassBtn = document.getElementById("wfm-lora-bypass-toggle");
|
||||
if (bypassBtn) {
|
||||
bypassBtn.classList.toggle("active", defaultStackTarget != null && !!_loraNodeBypass[defaultStackTarget]);
|
||||
bypassBtn.disabled = loraNodes.length === 0;
|
||||
}
|
||||
// Use Stack Group チェックボックスの現在状態を先読み(Stack表示の取得元判定に使う)。
|
||||
// resetStackMode(新規読み込み)時は常にOFF扱い。
|
||||
const stackModeChecked = hasLoraManager && !opts.resetStackMode && (el.querySelector("#wfm-lora-stack-mode-toggle")?.checked ?? false);
|
||||
@@ -759,6 +836,10 @@ export const comfyEditor = {
|
||||
// Stackグループ使用チェックボックスをOFFへリセットする(既存のGenUI状態への影響を避けるため
|
||||
// タブ切替やStack再読込などの単なる再描画では行わない)。
|
||||
if (opts.resetStackMode) {
|
||||
// New workflow loaded — bypass state/snapshots belonged to the previous
|
||||
// workflow's node IDs, so they can't be meaningfully carried forward.
|
||||
_loraNodeBypass = {};
|
||||
_loraBypassSnapshot = {};
|
||||
comfyUI.loraManagerSnapshots = {};
|
||||
for (const n of loraNodes) {
|
||||
if (!n.is_lora_manager) continue;
|
||||
@@ -1084,6 +1165,11 @@ export const comfyEditor = {
|
||||
const strModel = parseFloat(document.getElementById("wfm-lora-str-model")?.value) || 1.0;
|
||||
const strClip = parseFloat(document.getElementById("wfm-lora-str-clip")?.value) || 1.0;
|
||||
_applyLoraToNode(nodeId, select.value, strModel, strClip, node?.is_lora_manager);
|
||||
// An explicit Apply overwrites whatever bypass had zeroed — clear it so the
|
||||
// header button (and a later un-bypass) don't stomp this new value.
|
||||
delete _loraNodeBypass[nodeId];
|
||||
delete _loraBypassSnapshot[nodeId];
|
||||
document.getElementById("wfm-lora-bypass-toggle")?.classList.remove("active");
|
||||
_syncRawJson();
|
||||
|
||||
// Sync to Positive prompt
|
||||
@@ -1216,6 +1302,11 @@ export const comfyEditor = {
|
||||
comfyUI.currentWorkflow[nodeId].inputs.strength_clip = _stackStrengths[first]?.c ?? 1.0;
|
||||
}
|
||||
}
|
||||
// An explicit Apply overwrites whatever bypass had zeroed — clear it so the
|
||||
// header button (and a later un-bypass) don't stomp this new value.
|
||||
delete _loraNodeBypass[nodeId];
|
||||
delete _loraBypassSnapshot[nodeId];
|
||||
document.getElementById("wfm-lora-bypass-toggle")?.classList.remove("active");
|
||||
_syncRawJson();
|
||||
|
||||
// Sync Stack LORA SYNTAX + TRIGGER WORDS to Positive prompt
|
||||
@@ -1990,6 +2081,75 @@ export const comfyEditor = {
|
||||
if (!workflow) _syncRawJson();
|
||||
},
|
||||
|
||||
// Resolves a user/LLM-supplied aspect ratio (e.g. "3:2" or the full COMBO label
|
||||
// "3:2 (Photo)") against a ResolutionSelector-type node's actual options via
|
||||
// /object_info — the workflow analysis only knows the node's CURRENT value, not its
|
||||
// full option list, so a bare "3:2" needs matching against whatever labeled string
|
||||
// that node's aspect_ratio COMBO actually accepts. Falls back to the raw input if
|
||||
// the node type can't be queried or nothing matches, so ComfyUI's own validation
|
||||
// surfaces a clear error instead of this silently doing nothing.
|
||||
async resolveAspectRatioOption(nodeType, requested) {
|
||||
if (!requested) return null;
|
||||
const ratioMatch = String(requested).match(/(\d+)\s*:\s*(\d+)/);
|
||||
const wantedRatio = ratioMatch ? `${ratioMatch[1]}:${ratioMatch[2]}` : String(requested).trim();
|
||||
try {
|
||||
const info = await comfyUI.fetchObjectInfo(nodeType);
|
||||
const options = info?.[nodeType]?.input?.required?.aspect_ratio?.[1]?.options || [];
|
||||
if (options.includes(requested)) return requested;
|
||||
return options.find((o) => o.startsWith(wantedRatio)) || requested;
|
||||
} catch {
|
||||
return requested;
|
||||
}
|
||||
},
|
||||
|
||||
// AI TOOL Chat's generate_image tool call — writes whichever of KSampler
|
||||
// (steps/cfg/sampler_name/scheduler/denoise/seed), Latent Image (width/height/
|
||||
// batch_size), and Resolution Selector (resolvedAspectRatio) fields the LLM
|
||||
// supplied onto the given workflow/analysis (or comfyUI.currentWorkflow/
|
||||
// currentAnalysis by default). Mirrors lab-tab.js's _buildWorkflowForIteration
|
||||
// KSampler branch — same per-field node-id indirection (stepsNodeId/cfgNodeId/...
|
||||
// falling back to sampler.id) for Advanced-Sampling workflows (Flux.1/2, SD3.5,
|
||||
// Chroma...) that spread these across several nodes.
|
||||
// seed is returned rather than written directly — the caller passes it through to
|
||||
// comfyUI.generate()'s seedMode/seedValue instead, same as every other seed source.
|
||||
// resolvedAspectRatio must already be one of the node's actual option strings (see
|
||||
// resolveAspectRatioOption above) — passed in rather than resolved here because
|
||||
// that lookup is async and this stays a plain synchronous writer.
|
||||
setGenerationParams({ steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size, resolvedAspectRatio, workflow, analysis } = {}) {
|
||||
const wf = workflow || comfyUI.currentWorkflow;
|
||||
const an = analysis || comfyUI.currentAnalysis;
|
||||
if (!wf || !an) return { seed: null };
|
||||
|
||||
const sampler = an.sampler_nodes?.[0];
|
||||
if (sampler) {
|
||||
if (steps != null) { const id = sampler.stepsNodeId ?? sampler.id; if (wf[id]) wf[id].inputs.steps = steps; }
|
||||
if (cfg != null) { const id = sampler.cfgNodeId ?? sampler.id; if (wf[id]) wf[id].inputs.cfg = cfg; }
|
||||
if (sampler_name) { const id = sampler.samplerNodeId ?? sampler.id; if (wf[id]) wf[id].inputs.sampler_name = sampler_name; }
|
||||
if (scheduler && sampler.schedulerNodeId != null && wf[sampler.schedulerNodeId]) {
|
||||
wf[sampler.schedulerNodeId].inputs.scheduler = scheduler;
|
||||
}
|
||||
if (denoise != null && sampler.denoiseNodeId != null && wf[sampler.denoiseNodeId]) {
|
||||
wf[sampler.denoiseNodeId].inputs.denoise = denoise;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolution Selector takes priority over direct width/height — writing width/
|
||||
// height straight onto the EmptyLatentImage would just clobber whatever the
|
||||
// selector computed, silently ignoring the requested aspect ratio.
|
||||
const resSelector = an.resolution_selector_nodes?.[0];
|
||||
const latent = an.latent_nodes?.[0];
|
||||
if (resolvedAspectRatio && resSelector && wf[resSelector.id]) {
|
||||
wf[resSelector.id].inputs.aspect_ratio = resolvedAspectRatio;
|
||||
} else if (latent && wf[latent.id]) {
|
||||
if (width != null) wf[latent.id].inputs.width = width;
|
||||
if (height != null) wf[latent.id].inputs.height = height;
|
||||
}
|
||||
if (latent && wf[latent.id] && batch_size != null) wf[latent.id].inputs.batch_size = batch_size;
|
||||
|
||||
if (!workflow) _syncRawJson();
|
||||
return { seed: seed ?? null };
|
||||
},
|
||||
|
||||
syncToWorkflow() {
|
||||
// Sync prompt texts before generation
|
||||
const posSelect = document.getElementById("wfm-prompt-pos-target");
|
||||
|
||||
@@ -886,6 +886,7 @@ export const comfyWorkflow = {
|
||||
prompt_nodes: [],
|
||||
sampler_nodes: [],
|
||||
latent_nodes: [],
|
||||
resolution_selector_nodes: [],
|
||||
lora_nodes: [],
|
||||
vae_nodes: [],
|
||||
save_nodes: [],
|
||||
@@ -1349,6 +1350,20 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// ResolutionSelector (ComfyUI core, comfy_extras.nodes_resolution): typically
|
||||
// feeds an EmptyLatentImage's width/height rather than being a latent node
|
||||
// itself. aspect_ratio is a fixed-choice COMBO (e.g. "3:2 (Photo)") — its exact
|
||||
// option strings aren't known here, only the node's current value, so callers
|
||||
// that want to set a new one (AI TOOL Chat's generate_image) must resolve
|
||||
// against /object_info at call time (see comfyui-editor.js resolveAspectRatioOption).
|
||||
if (ct === "ResolutionSelector") {
|
||||
result.resolution_selector_nodes.push({
|
||||
id, type: ct, title,
|
||||
aspect_ratio: inputs.aspect_ratio,
|
||||
megapixels: inputs.megapixels,
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1443,14 +1443,17 @@ export async function _expandWildcardsInWorkflow(workflow) {
|
||||
// Generation (core — throws on error)
|
||||
// ============================================
|
||||
|
||||
async function _coreGenerate(silent = false, workflowOverride = null) {
|
||||
async function _coreGenerate(silent = false, workflowOverride = null, genOptions = {}) {
|
||||
const progressBar = document.getElementById("wfm-gen-progress-bar");
|
||||
const progressText = document.getElementById("wfm-gen-progress-text");
|
||||
const resultImg = document.getElementById("wfm-gen-result-img");
|
||||
const resultThumbs = document.getElementById("wfm-gen-result-thumbs");
|
||||
|
||||
const seedMode = document.getElementById("wfm-gen-seed-mode")?.value || "random";
|
||||
const seedValue = parseInt(document.getElementById("wfm-gen-seed-value")?.value) || -1;
|
||||
// genOptions.seedValue lets a caller (e.g. AI TOOL Chat's generate_image tool)
|
||||
// force a specific seed on an override workflow without touching the GenerateUI
|
||||
// seed fields — those still drive normal (non-override) generation as before.
|
||||
const seedMode = genOptions.seedValue != null ? "fixed" : (document.getElementById("wfm-gen-seed-mode")?.value || "random");
|
||||
const seedValue = genOptions.seedValue ?? (parseInt(document.getElementById("wfm-gen-seed-value")?.value) || -1);
|
||||
|
||||
if (progressBar) progressBar.style.width = "0%";
|
||||
if (progressText) progressText.textContent = "Starting...";
|
||||
@@ -1972,9 +1975,11 @@ async function _runBatchGenerate() {
|
||||
// ============================================
|
||||
|
||||
// workflowOverride: 明示的に渡された場合、comfyUI.currentWorkflow の代わりにこのワークフローで
|
||||
// 生成する(GenerateUIの表示状態には触れない。Image Edit「Inpaint」の専用ワークフロー実行から利用)。
|
||||
// 生成する(GenerateUIの表示状態には触れない。Image Edit「Inpaint」の専用ワークフロー実行や
|
||||
// AI TOOL Chatのgenerate_imageツールから利用)。
|
||||
// genOptions: 現状 seedValue のみ(指定時は_coreGenerateがseedMode "fixed"扱いにする)。
|
||||
// ボタンのclickハンドラから呼ばれる場合は第一引数がMouseEventになるため、その場合は無視する。
|
||||
async function handleGenerate(workflowOverride = null) {
|
||||
async function handleGenerate(workflowOverride = null, genOptions = {}) {
|
||||
if (workflowOverride instanceof Event) workflowOverride = null;
|
||||
|
||||
if (!workflowOverride && !comfyUI.currentWorkflow) {
|
||||
@@ -2003,7 +2008,7 @@ async function handleGenerate(workflowOverride = null) {
|
||||
await _runBatchGenerate();
|
||||
} else {
|
||||
try {
|
||||
return await _coreGenerate(false, workflowOverride);
|
||||
return await _coreGenerate(false, workflowOverride, genOptions);
|
||||
} catch (err) {
|
||||
const progressText = document.getElementById("wfm-gen-progress-text");
|
||||
if (progressText) progressText.textContent = "Error";
|
||||
@@ -2156,6 +2161,14 @@ export async function initGenerateTab() {
|
||||
}
|
||||
});
|
||||
|
||||
// Bypass button in LoRA column header — forces the target LoRA node's strength to
|
||||
// 0 (or restores it), independent of the Single/Stack tabs' own Apply buttons.
|
||||
document.getElementById("wfm-lora-bypass-toggle")?.addEventListener("click", () => {
|
||||
if (!comfyUI.currentAnalysis) return;
|
||||
comfyEditor.toggleLoraNodeBypass(comfyUI.currentAnalysis);
|
||||
comfyEditor.renderLoraPane(comfyUI.currentAnalysis, "wfm-gen-lora-fields");
|
||||
});
|
||||
|
||||
// Input inner tab (Prompt / Image)
|
||||
document.querySelectorAll(".wfm-input-inner-tab").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
|
||||
+30
-9
@@ -244,6 +244,10 @@ const LANGUAGES = {
|
||||
labRevertCheckboxLabel: "Revert to iteration #1's setting",
|
||||
labBypassed: "⏭ Bypassed",
|
||||
labBypassCheckboxLabel: "Bypass this setting (keep whatever was active before it)",
|
||||
labLoraNodeBypassed: "⛔ (node bypassed)",
|
||||
labLoraNodeBypassCheckboxLabel: "Bypass LoRA node (force strength to 0)",
|
||||
labLoraApplyToPromptCheckboxLabel: "Apply LoRA syntax + trigger words to Positive prompt",
|
||||
labLoraPromptAppliedTag: "→P",
|
||||
labSeedEmptyRandom: "Seed (empty = random)",
|
||||
labPause: "Pause",
|
||||
labResume: "Resume",
|
||||
@@ -783,6 +787,7 @@ const LANGUAGES = {
|
||||
helpGen22: "Stack tab — Use Stack Group checkbox (shown only when a Lora Manager node exists in the workflow): OFF (the default whenever a workflow is loaded) shows and edits the LoRAs actually configured in the workflow's Lora Manager node itself; ON shows and edits the \"Stack\" group registered in the Models tab instead; switching either direction clears the node's LoRA list first, then loads it from the newly selected source; switching back to OFF restores exactly the LoRA list that was present when the workflow was loaded (captured as a snapshot at load time)",
|
||||
helpGen23: "Stack tab controls: each row has an active checkbox and Str M / Str C fields; the header \"Stack\" checkbox toggles all rows at once; the +/- step buttons adjust the strength of all currently active rows together; a \"Showing: LoRAs currently in workflow\" / \"Showing: registered Stack group\" label indicates which source is currently displayed; Apply writes the full list back to the target node and also appends LoRA syntax + CivitAI trigger words to the Positive prompt",
|
||||
helpGen24: "Caution for CLIP Text Encode edit+ (CLIPTextEncodeEditPlus) nodes: when a Lora Manager node's trigger_words output feeds this node's text1 input (RAW/EDIT/front/back modes combine text1 with the editable text_edit field), trigger words are already included automatically at generation time; clicking a LoRA Apply button or turning on Use Stack Group additionally writes LoRA syntax/trigger words into text_edit, duplicating them — leave Use Stack Group off and avoid the LoRA Apply buttons if you want the workflow's original prompt and LoRA composition to stay unmodified",
|
||||
helpGen29: "Bypass button (⛔, in the LoRA column header, next to Refresh Stack group): forces the target LoRA node's strength to 0 — strength_model/strength_clip for a standard LoraLoader, or every entry's strength/clipStrength for a Lora Manager node (its LoRA list is kept intact, not replaced) — without touching the actual ComfyUI node's mode; click again to restore the exact values that were there before toggling; targets whichever node Single/Stack Apply default to (the workflow's Lora Manager node if it has one, else the first LoRA node); clicking a Single or Stack Apply button clears the bypass state so it can't silently reassert itself on a later toggle",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder subtab",
|
||||
helpFeederDesc: "Two modes switchable via the [Image Loop] / [Gallery] toggle at the top of the left pane.",
|
||||
helpFeederImgloopTitle: "Image Loop mode",
|
||||
@@ -821,8 +826,10 @@ const LANGUAGES = {
|
||||
helpLab11: "Prompt cell editor also has Clear (empties both Positive and Negative) and Get from Previous (copies both Positive and Negative from the keyframe immediately before this one in the same column — e.g. editing #3 copies #2's prompt wholesale; not shown on keyframe #1, which has no earlier row to copy from)",
|
||||
helpLab12: "Keyframe #1 in every column shows the currently loaded workflow's own live setting instead of a blank placeholder — for the Model column this covers Checkpoint and VAE only (LoRA is never live-reflected, since there's no reliable way to infer \"the\" active LoRA back out of a workflow node) — both in its grid cell (dashed border) and pre-filled into its edit modal, for as long as it hasn't been explicitly saved with an override; saving it (even without changing anything) locks that value in as a real override and the dashed/live styling goes away; switching to the Lab subtab always refreshes this against whatever is currently loaded in GenerateUI",
|
||||
helpLab13: "Bypass checkbox (every keyframe, including #1): skips that row entirely when picking which setting applies at a given iteration — as if the row didn't exist — so whatever was active before it (an earlier keyframe, or ultimately nothing) just keeps running; Lab-internal only, it never touches the actual ComfyUI node's mode; disables that row's value fields and its Revert checkbox while checked (the two are mutually exclusive); the cell shows an amber \"⏭ Bypassed\" label, which takes priority over both the Revert and live-value display",
|
||||
helpLab14: "Model column header's C / L / V buttons pick which single field (Checkpoint / LoRA / VAE — LoRA is Single-only, no Stack support here) its cells summarize and its edit modal shows; the two you're not currently viewing are left exactly as already saved when you Save. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it",
|
||||
helpLab15: "Model column fields carry forward independently: a keyframe that only sets a LoRA doesn't reset Checkpoint/VAE to the workflow default — each of the three keeps whatever an earlier keyframe last set until a later keyframe explicitly changes it. A field left blank in the edit modal shows \"(inherits: ...)\" as its placeholder instead of \"(workflow default)\" when there's something to inherit, so it's clear what actually applies. \"Revert to #1's setting\" resets all three fields at once back to keyframe #1's row, not just the one you're currently viewing",
|
||||
helpLab14: "Model column header's C / L / V buttons pick which independent keyframe list — Checkpoint, LoRA (Single-only, no Stack support here), or VAE — the shared cell list, its +/-, and its edit modal all currently operate on. Each of the three is its own timeline with its own keyframes, exactly like Prompt/KSampler; they only ever look merged because they share one column's worth of screen space. Adding, removing, or editing a keyframe while viewing one never touches the other two. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it",
|
||||
helpLab15: "Checkpoint / LoRA / VAE each carry forward independently, the same effective-value rule as every other column (see above): a keyframe in one never resets the other two to the workflow default. A field left blank in the edit modal shows \"(inherits: ...)\" as its placeholder instead of \"(workflow default)\" when an earlier keyframe in that same list already set something, so it's clear what actually applies. \"Revert to #1's setting\" and Bypass — like +/- — only ever affect whichever ONE of the three is currently selected by the C/L/V toggle, never the other two",
|
||||
helpLab16: "Bypass LoRA node checkbox (LoRA keyframe's own edit modal only): forces that keyframe's strength_model/strength_clip to 0 at apply-time, standing in for ComfyUI's own node Bypass (mode:4) — the workflow used here is already converted to API format, which has no mode concept, so zeroing strength is the behavioral equivalent. Disables the strength fields while checked; independent of the row-level Bypass checkbox above (see helpLab13), which skips the whole row instead of touching the node",
|
||||
helpLab17: "Apply LoRA syntax + trigger words to Positive prompt checkbox (LoRA column only): when checked, appends <lora:name:strengthModel:strengthClip> plus that LoRA's cached CivitAI trigger words to the Positive prompt, comma-separated, at apply-time — the same format GenerateUI's own Model tab Apply button writes. Switching keyframes (a different LoRA, this checkbox turned off, or the row Bypassed) always strips exactly what the previous iteration injected before adding anything new, so a batch never accumulates every LoRA it has used. Disabled automatically while Bypass LoRA node is checked",
|
||||
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",
|
||||
@@ -948,7 +955,7 @@ const LANGUAGES = {
|
||||
helpAiTitle: "AI Tab (A)",
|
||||
helpAi1: "4-pane layout — Translation | Chat | TOOLS | Settings — all panes are always visible simultaneously",
|
||||
helpAi2: "Translation pane: translate between Japanese, English, Chinese, or Free language using Ollama, LM Studio, Lemonade, or Unsloth; ⇄ 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)",
|
||||
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; generate_image also accepts optional generation parameters the model can fill in from the request — steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size (number of images produced in that one call), and aspect_ratio (a ratio preset like \"3:2\", applied only if the workflow has a Resolution Selector node feeding width/height — ignored otherwise); anything the model doesn't supply keeps whatever the loaded workflow already has set; 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",
|
||||
helpAi6: "Settings pane: choose backend (Ollama / LM Studio / Lemonade / Unsloth), set API URL, test connection, select model; Unsloth always requires an API key even for local access — set UNSLOTH_API_KEY in a .env file in the plugin folder (copy .env.example) and restart ComfyUI, the key is read server-side and never sent to the browser; Unsloth also requires \"Auto model switching (OpenAI API)\" to be turned ON in the Unsloth Desktop app itself (its own Settings screen) — without it, requests fail with HTTP 404 unless a model was already manually loaded in the Unsloth Studio UI; Unsloth reasoning models return their thinking in a separate reasoning_content field rather than inline <think> tags, so a low Max tokens can make the model spend its whole budget reasoning and return an empty reply — increase Max tokens (2048+) if replies come back blank; Generation section — Thinking mode toggle (off by default; Ollama receives a think flag, and regardless of backend any <think>...</think> block is stripped from the output when off, so reasoning models don't clutter Translation/Chat/TOOLS results) and Max tokens (Ollama: num_predict, LM Studio/Lemonade/Unsloth: max_tokens; 0 = unlimited); 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",
|
||||
@@ -1503,6 +1510,10 @@ const LANGUAGES = {
|
||||
labRevertCheckboxLabel: "1回目の設定に戻す",
|
||||
labBypassed: "⏭ バイパス中",
|
||||
labBypassCheckboxLabel: "この設定をバイパス(直前まで有効だった設定を維持)",
|
||||
labLoraNodeBypassed: "⛔(ノードをバイパス)",
|
||||
labLoraNodeBypassCheckboxLabel: "LoRAノードをバイパス(強度を0にする)",
|
||||
labLoraApplyToPromptCheckboxLabel: "LoRA構文とトリガーワードをポジティブプロンプトに適用",
|
||||
labLoraPromptAppliedTag: "→P",
|
||||
labSeedEmptyRandom: "Seed(空欄でランダム)",
|
||||
labPause: "一時停止",
|
||||
labResume: "再開",
|
||||
@@ -2042,6 +2053,7 @@ const LANGUAGES = {
|
||||
helpGen22: "Stackタブ — Use Stack Groupチェックボックス(ワークフロー内にLora Managerノードがある場合のみ表示):OFF(ワークフロー読み込み時の既定)はLora Managerノードに実際に設定されているLoRAをそのまま表示・編集;ONにするとModelsタブに登録済みの「Stack」グループを表示・編集;どちらへの切替もまずノードのLoRA一覧をクリアしてから切替先のソースを読み込む;OFFへ戻すとワークフロー読み込み時点のLoRA一覧(読み込み時にスナップショットとして保存済み)へ正確に復元される",
|
||||
helpGen23: "Stackタブの操作:各行に有効チェックボックスとStr M / Str Cフィールド;ヘッダーの「Stack」チェックボックスで全行を一括切替;上部の+/-ステップボタンで現在有効な全行の強度をまとめて増減;「Showing: LoRAs currently in workflow」/「Showing: registered Stack group」のラベルで現在どちらのソースを表示中か分かる;Applyで対象ノードへ一覧全体を書き込み、同時にLoRA構文とCivitAIトリガーワードをPositiveプロンプトへ追加",
|
||||
helpGen24: "CLIP Text Encode edit+(CLIPTextEncodeEditPlus)ノードを使う場合の注意:Lora Managerノードのtrigger_words出力がこのノードのtext1入力に接続されている構成(RAW/EDIT/front/backモードでtext1と編集可能なtext_editを結合)では、生成時にトリガーワードは既に自動的に反映済み;LoRAのApplyボタンをクリックしたりUse Stack GroupをONにしたりすると、LoRA構文・トリガーワードがtext_editへも追加で書き込まれ二重になる — ワークフロー本来のプロンプト・LoRA構成を変更せず維持したい場合は、Use Stack GroupをOFFのままにしLoRAのApply系ボタンには触れないこと",
|
||||
helpGen29: "バイパスボタン(⛔、LoRA列ヘッダー内、Refresh Stack groupボタンの隣):対象のLoRAノードの強度を強制的に0にします — 通常のLoraLoaderならstrength_model/strength_clip、Lora Managerノードならリスト内の全エントリのstrength/clipStrength(LoRAリスト自体は差し替えず維持)が対象で、実際のComfyUIノードのmodeには一切触れません;もう一度クリックすると、切り替え前の値に正確に戻ります;対象はSingle/Stack Applyがデフォルトで対象とするノード(ワークフローにLora Managerノードがあればそれ、無ければ最初のLoRAノード);SingleまたはStackのApplyボタンをクリックするとバイパス状態はクリアされ、後から無言で再適用されることはありません",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder サブタブ",
|
||||
helpFeederDesc: "左ペイン上部の [Image Loop] / [Gallery] トグルで2つのモードを切り替えられます。",
|
||||
helpFeederImgloopTitle: "Image Loop モード",
|
||||
@@ -2080,8 +2092,10 @@ const LANGUAGES = {
|
||||
helpLab11: "プロンプトセルの編集モーダルにはClear(Positive/Negative両方を空にする)とGet from Previous(同じカラムの直前のキーフレームからPositive/Negative両方をコピー — 例:#3を編集中に押すと#2のプロンプトをそのままコピー)も用意されています。1回目(#1)には直前の行が無いため表示されません",
|
||||
helpLab12: "各カラムの1回目(キーフレーム#1)は、明示的に上書き保存されていない間、現在GenerateUIで読み込まれているワークフロー本来の設定を、空欄プレースホルダの代わりにグリッドセル(破線枠)と編集モーダルの両方に表示します(Model列はCheckpointとVAEのみが対象 — LoRAはワークフローのノードから「どれが有効か」を安全に逆算する手段が無いため、ライブ表示の対象外です)。何も変更せずに保存しただけでも実際の上書き値として確定し、破線・ライブ表示は解除されます。Labサブタブに切り替えるたびに、その時点でGenerateUIに読み込まれている内容へ再同期されます",
|
||||
helpLab13: "Bypassチェックボックス(1回目を含む全キーフレームに搭載):そのキーフレームの回数目判定において、行自体が存在しないかのように完全にスキップします — その結果、直前まで有効だった設定(それ以前のキーフレーム、なければ何も適用されない状態)がそのまま使われ続けます。Labタブ内だけの概念で、実際のComfyUIノードのmodeには一切触れません。チェック中はその行の値フィールドと「1回目に戻す」チェックボックスが無効化されます(両者は同時に使えない排他的な関係)。セルには琥珀色の「⏭ Bypassed」ラベルが表示され、Revert表示・ライブ表示のどちらよりも優先されます",
|
||||
helpLab14: "Model列ヘッダーのC/L/Vボタンは、セル表示と編集モーダルがCheckpoint/LoRA/VAE(LoRAはSingleのみ、Stack非対応)のどれを表示・編集対象にするかを切り替えます。今見ていない残り2項目は、保存してもそれまでの保存値のまま変更されません。各ボタンは、読み込み中のワークフローに該当するノードが実際に存在する場合、GenerateUIのModelタブと同じハイライト色(Settingsで変更可)になります。LoRAを設定していてもワークフロー内にLoRAノードが1つも無い場合は、Run実行中に1回だけ警告し、その設定は適用されません — VAEと異なりLoRAノードの自動追加は行いません(CheckpointのModel/CLIP出力の配線を組み替える必要があるため)",
|
||||
helpLab15: "Model列の3項目(Checkpoint/LoRA/VAE)はそれぞれ独立して引き継がれます:LoRAだけを設定したキーフレームがあっても、Checkpoint/VAEはワークフロー本来の設定に戻るのではなく、それ以前のキーフレームで最後に設定された値がそのまま維持されます(さらに後のキーフレームで明示的に変更されるまで)。編集モーダルで未設定のままの項目は、引き継ぐ値がある場合プレースホルダが「(ワークフローの設定のまま)」ではなく「(引き継ぎ: ...)」と表示され、実際に適用される値が分かるようになっています。「1回目の設定に戻す」は、今見ている項目だけでなく3項目すべてを一括でキーフレーム#1の内容に戻します",
|
||||
helpLab14: "Model列ヘッダーのC/L/Vボタンは、Checkpoint・LoRA(Singleのみ、Stack非対応)・VAEという3つの独立したキーフレームリストのうち、今このセル表示・+/-・編集モーダルがどれを対象にしているかを切り替えます。3つはそれぞれPrompt・KSamplerと同じく完全に独立したタイムラインを持っており、1つの列にまとめて見えているのは表示スペースを共有しているだけです。1つを表示中にキーフレームを追加・削除・編集しても、残り2つには一切影響しません。各ボタンは、読み込み中のワークフローに該当するノードが実際に存在する場合、GenerateUIのModelタブと同じハイライト色(Settingsで変更可)になります。LoRAを設定していてもワークフロー内にLoRAノードが1つも無い場合は、Run実行中に1回だけ警告し、その設定は適用されません — VAEと異なりLoRAノードの自動追加は行いません(CheckpointのModel/CLIP出力の配線を組み替える必要があるため)",
|
||||
helpLab15: "Checkpoint/LoRA/VAEは、他の列と全く同じ実効値ルール(上記参照)でそれぞれ独立して引き継がれます:いずれか1つのキーフレームが、残り2つをワークフロー本来の設定に戻すことはありません。編集モーダルで未設定のままの項目は、同じリスト内の以前のキーフレームで何か設定済みの場合、プレースホルダが「(ワークフローの設定のまま)」ではなく「(引き継ぎ: ...)」と表示され、実際に適用される値が分かるようになっています。「1回目の設定に戻す」やBypassも+/-と同様、現在C/L/Vトグルで選択中の1項目だけに作用し、残り2つには影響しません",
|
||||
helpLab16: "LoRAノードをバイパスチェックボックス(LoRAキーフレーム自身の編集モーダルのみ):ComfyUI本来のノードBypass(mode:4)の代わりに、そのキーフレームのstrength_model/strength_clipを適用時に強制的に0にします — ここで使われるワークフローは既にAPI形式に変換済みでmodeの概念が無いため、強度をゼロにすることが実質的に同等の効果になります。チェック中は強度の入力欄が無効化されます。上記の行単位のBypassチェックボックス(helpLab13参照)とは独立しており、そちらは行全体をスキップするだけでノードには一切触れません",
|
||||
helpLab17: "LoRA構文とトリガーワードをポジティブプロンプトに適用チェックボックス(LoRA列のみ):チェックすると、適用時に <lora:name:strengthModel:strengthClip> とそのLoRAのキャッシュ済みCivitAIトリガーワードを、カンマ区切りでポジティブプロンプトへ追加します — GenerateUIのModelタブのApplyボタンが書き込むのと同じ書式です。キーフレームを切り替えた場合(別のLoRAへの変更、このチェックボックスをオフにした場合、または行自体をBypassした場合)は、新しい内容を追加する前に必ず直前のイテレーションが注入した分だけを正確に取り除くため、バッチ内で使われた全LoRAが蓄積されることはありません。「LoRAノードをバイパス」がチェックされている間は自動的に無効化されます",
|
||||
helpLabCardPlanTitle: "Settingサブタブ — 実行とプランファイル",
|
||||
helpLabPlan1: "右ペイン — Batch:実行する回数。Note:プランと一緒に保存される自由記述のメモ",
|
||||
helpLabPlan2: "Run/Pause/Cancel:Runでバッチループを開始、Pause/Resumeで回と回の間を一時停止・再開、Cancelで現在の生成を中断してループを止めます",
|
||||
@@ -2206,7 +2220,7 @@ const LANGUAGES = {
|
||||
helpAiTitle: "AI タブ(A)",
|
||||
helpAi1: "4ペインレイアウト — Translation | Chat | TOOLS | Settings — 全ペインが常時表示される",
|
||||
helpAi2: "翻訳ペイン:Ollama、LM Studio、Lemonade、または Unsloth を使って日本語・英語・中国語・カスタム言語(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)",
|
||||
helpAi3: "Chat ペイン:LLM とのマルチターン会話;Enter で送信、Shift+Enter で改行;Clear ボタンで会話履歴をリセット;モデルがtool callingに対応している場合、「猫の画像を作って」のように頼むとGenerateUIに読み込み中のワークフローで生成を実行し、結果をチャット内に表示する;ペインヘッダーの「画像生成を許可する」チェックボックスでこの機能のON/OFFを切り替えられる;画像が添付されている場合(TOOLSペインにドロップした画像)はLLMにもビジョン入力として送信され、generate_image呼び出し時はT2Iの代わりにI2I(image-to-image)の元画像として使われる;generate_image呼び出しにはsteps・cfg・sampler_name・scheduler・denoise・seed、width・height、batch_size(その1回の呼び出しで生成する枚数)、aspect_ratio(\"3:2\"のような比率プリセット。ワークフローにwidth/heightを供給するResolution Selectorノードがある場合のみ適用され、それ以外は無視される)といった任意の生成パラメータも指定できる;モデルが指定しなかった項目は読み込み中のワークフローの現在の設定がそのまま使われる;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生成に使用)、✕ボタンでクリアするまで保持される",
|
||||
helpAi6: "Settings ペイン:バックエンド(Ollama / LM Studio / Lemonade / Unsloth)の選択、APIのURL設定、接続テスト、モデル選択;Unslothはローカルアクセスでも常にAPIキー認証が必須——プラグインフォルダの .env.example を .env にコピーして UNSLOTH_API_KEY を設定し ComfyUI を再起動する(キーはサーバー側でのみ読み込まれ、ブラウザには一切送信されない);Unsloth Desktopアプリ側の設定画面で「モデル自動切り替え (OpenAI API)」もONにする必要がある——OFFのままだと、Unsloth Studio上で事前にモデルを手動ロードしていない限り HTTP 404 で接続に失敗する;Unslothのreasoningモデルは思考内容をインラインの<think>タグではなく別のreasoning_contentフィールドで返すため、Max tokensが少ないとモデルが思考だけでトークンを使い切り、空の返信になることがある——返信が空になる場合はMax tokensを増やす(2048以上を推奨);Generationセクション — Thinking modeトグル(デフォルトOFF;OllamaにはthinkフラグとしてOFF/ONを送信、バックエンドを問わずOFF時は出力中の<think>...</think>ブロックをクライアント側で除去するため、推論モデルの思考過程がTranslation/Chat/TOOLSの結果に混入しない)とMax tokens(Ollama: num_predict、LM Studio/Lemonade/Unsloth: max_tokens;0で無制限);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非対応のモデルでも動作する",
|
||||
@@ -2760,6 +2774,10 @@ const LANGUAGES = {
|
||||
labRevertCheckboxLabel: "恢复第1次的设置",
|
||||
labBypassed: "⏭ 已跳过",
|
||||
labBypassCheckboxLabel: "跳过此设置(保持它之前生效的设置)",
|
||||
labLoraNodeBypassed: "⛔(节点已绕过)",
|
||||
labLoraNodeBypassCheckboxLabel: "绕过LoRA节点(强度设为0)",
|
||||
labLoraApplyToPromptCheckboxLabel: "将LoRA语法和触发词应用到正面提示词",
|
||||
labLoraPromptAppliedTag: "→P",
|
||||
labSeedEmptyRandom: "Seed(留空为随机)",
|
||||
labPause: "暂停",
|
||||
labResume: "继续",
|
||||
@@ -3299,6 +3317,7 @@ const LANGUAGES = {
|
||||
helpGen22: "Stack标签 — Use Stack Group复选框(仅当工作流中存在Lora Manager节点时显示):OFF(加载工作流时的默认状态)显示并编辑Lora Manager节点中实际配置的LoRA;ON则改为显示并编辑Models标签中已注册的\"Stack\"分组;切换任一方向都会先清空节点的LoRA列表,再从新选择的来源加载;切回OFF会精确恢复为加载工作流那一刻的LoRA列表(加载时已保存快照)",
|
||||
helpGen23: "Stack标签的操作:每行都有启用复选框及Str M / Str C字段;表头的\"Stack\"复选框可一次性切换所有行;顶部的+/-步进按钮可同时调整所有当前启用行的强度;\"Showing: LoRAs currently in workflow\" / \"Showing: registered Stack group\"标签指示当前显示的是哪个来源;点击Apply会将完整列表写回目标节点,并同时将LoRA语法和CivitAI触发词追加到Positive提示词",
|
||||
helpGen24: "使用 CLIP Text Encode edit+(CLIPTextEncodeEditPlus)节点时的注意事项:当Lora Manager节点的trigger_words输出连接到该节点的text1输入时(RAW/EDIT/front/back模式会将text1与可编辑的text_edit字段组合),触发词在生成时已自动包含;此时点击LoRA的Apply按钮或开启Use Stack Group会额外把LoRA语法/触发词写入text_edit,造成重复 — 如果想保持工作流原始的提示词和LoRA配置不被修改,请保持Use Stack Group为OFF,并避免点击LoRA的Apply类按钮",
|
||||
helpGen29: "绕过按钮(⛔,位于LoRA列表头,紧邻Refresh Stack group):将目标LoRA节点的强度强制设为0——普通LoraLoader对应strength_model/strength_clip,Lora Manager节点则对应列表中每一项的strength/clipStrength(LoRA列表本身保持不变,不会被替换),不会改动实际ComfyUI节点的mode;再次点击可精确恢复切换前的数值;作用于Single/Stack Apply默认所用的节点(工作流中若有Lora Manager节点则用它,否则用第一个LoRA节点);点击Single或Stack的Apply按钮会清除绕过状态,避免其在之后被无声地重新生效",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder 子标签",
|
||||
helpFeederDesc: "通过左侧面板顶部的 [图片循环] / [图库] 切换按钮可在两种模式间切换。",
|
||||
helpFeederImgloopTitle: "图片循环模式",
|
||||
@@ -3337,8 +3356,10 @@ const LANGUAGES = {
|
||||
helpLab11: "Prompt单元格编辑弹窗中还提供Clear(将Positive和Negative都清空)和Get from Previous(从同一列紧邻的上一个关键帧复制Positive和Negative——例如编辑#3时会整段复制#2的提示词);关键帧#1没有更早的行可复制,因此不显示该按钮",
|
||||
helpLab12: "每一列的关键帧#1,只要还没有被显式保存覆盖,就会用当前GenerateUI中已加载工作流的实际设置代替空白占位显示(Model列仅覆盖Checkpoint和VAE两项——LoRA不会被实时反映,因为无法从工作流节点中安全地反推出\"当前生效的\"LoRA),同时体现在其网格单元格(虚线边框)和编辑弹窗的预填内容中;哪怕不做任何修改直接保存,也会将该值锁定为真正的覆盖值,虚线/实时显示随之消失;每次切换到Lab子标签页,都会重新根据当前GenerateUI中加载的内容刷新",
|
||||
helpLab13: "Bypass复选框(包括#1在内的每个关键帧都有):在判定某次迭代应使用哪一行设置时,会把该行完全跳过,如同它不存在——于是它之前生效的设置(更早的关键帧,或最终什么都不应用)会继续保持;这只是Lab内部的概念,绝不会改动实际ComfyUI节点的mode;勾选后会禁用该行的取值字段以及其Revert复选框(二者互斥,不能同时生效);单元格会显示琥珀色的「⏭ Bypassed」标签,其优先级高于Revert显示和实时显示",
|
||||
helpLab14: "Model列表头的C/L/V按钮用于切换单元格显示与编辑弹窗当前针对Checkpoint/LoRA/VAE(LoRA仅支持Single,不支持Stack)中的哪一项。未在查看的另外两项即使保存也会保持原值不变。当前加载的工作流中若确实存在对应类型的节点,相应按钮会变为与GenerateUI的Model标签页相同的高亮颜色(可在Settings中调整)。若设置了LoRA但工作流中完全没有LoRA节点,Run执行期间只会警告一次并跳过该项设置——与VAE不同,Lab不会自动插入LoRA节点,因为那需要重新连接Checkpoint的Model/CLIP输出线路",
|
||||
helpLab15: "Model列的三个字段(Checkpoint/LoRA/VAE)各自独立向后延续:即使某个关键帧只设置了LoRA,Checkpoint/VAE也不会恢复为工作流原始设置,而是保持更早的关键帧最后设置的值(直到之后的关键帧明确修改它为止)。在编辑弹窗中,若某字段留空且存在可继承的值,其占位符会显示「(继承: ...)」而不是「(保持工作流原设置)」,方便确认实际会应用的值。「恢复第1次的设置」会一次性将三个字段全部恢复为关键帧#1的内容,而不仅仅是当前正在查看的这一项",
|
||||
helpLab14: "Model列表头的C/L/V按钮用于切换Checkpoint、LoRA(仅支持Single,不支持Stack)、VAE这三个相互独立的关键帧列表中,当前单元格显示、+/-以及编辑弹窗针对的是哪一个。三者各自拥有完全独立的时间线,与Prompt、KSampler完全相同,只是共用同一列的显示空间,看起来像是合并在一起。查看其中一项时添加、删除或编辑关键帧,完全不会影响另外两项。当前加载的工作流中若确实存在对应类型的节点,相应按钮会变为与GenerateUI的Model标签页相同的高亮颜色(可在Settings中调整)。若设置了LoRA但工作流中完全没有LoRA节点,Run执行期间只会警告一次并跳过该项设置——与VAE不同,Lab不会自动插入LoRA节点,因为那需要重新连接Checkpoint的Model/CLIP输出线路",
|
||||
helpLab15: "Checkpoint/LoRA/VAE与其他列采用完全相同的实际生效值规则(见上文),各自独立向后延续:其中一项的关键帧不会使另外两项恢复为工作流原始设置。在编辑弹窗中,若某字段留空,且同一列表中更早的关键帧已设置过值,其占位符会显示「(继承: ...)」而不是「(保持工作流原设置)」,方便确认实际会应用的值。「恢复第1次的设置」和Bypass与+/-一样,只会作用于当前C/L/V切换选中的那一项,不会影响另外两项",
|
||||
helpLab16: "绕过LoRA节点复选框(仅LoRA关键帧自己的编辑弹窗中):在应用时强制将该关键帧的strength_model/strength_clip设为0,替代ComfyUI原生的节点Bypass(mode:4)——这里使用的工作流已转换为API格式,不带mode概念,因此将强度归零在效果上是等价的。勾选后会禁用强度输入框;与上方行级别的Bypass复选框(见helpLab13)相互独立,后者只是跳过整行而不会改动节点本身",
|
||||
helpLab17: "将LoRA语法和触发词应用到正面提示词复选框(仅LoRA列):勾选后,会在应用时以逗号分隔的形式,将<lora:name:strengthModel:strengthClip>及该LoRA缓存的CivitAI触发词追加到正面提示词——格式与GenerateUI的Model标签页Apply按钮写入的完全一致。切换关键帧时(更换为另一个LoRA、取消勾选此复选框,或该行被Bypass)总会先精确移除上一次迭代注入的内容,再添加新内容,因此批次不会累积用过的所有LoRA。勾选「绕过LoRA节点」时会自动禁用",
|
||||
helpLabCardPlanTitle: "Setting子标签 — 运行与方案文件",
|
||||
helpLabPlan1: "右侧面板 — Batch:要运行的迭代次数;Note:与方案一起保存的自由备注",
|
||||
helpLabPlan2: "Run/Pause/Cancel:Run开始批量循环;Pause/Resume在各次迭代之间暂停/继续;Cancel会中断当前生成并停止循环",
|
||||
@@ -3464,7 +3485,7 @@ const LANGUAGES = {
|
||||
helpAiTitle: "AI 标签页(A)",
|
||||
helpAi1: "4 面板布局 — Translation | Chat | TOOLS | Settings — 所有面板始终可见",
|
||||
helpAi2: "翻译面板:使用 Ollama、LM Studio、Lemonade 或 Unsloth 在日语、英语、中文或自定义语言(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)",
|
||||
helpAi3: "Chat 面板:与 LLM 进行多轮对话;Enter 发送消息,Shift+Enter 换行;Clear 按钮重置对话历史;如果模型支持 tool calling,当你要求生成图像(例如\"生成一张猫的图片\")时,会使用 GenerateUI 中当前加载的工作流执行生成,并在对话中显示结果;面板标题栏的\"允许生成图像\"复选框可开关此功能;如果附加了图像(在 TOOLS 面板中拖放的图像),该图像会作为视觉输入发送给 LLM,调用 generate_image 时会以该图像为基础执行图生图(I2I),而不是文生图;generate_image 还接受可选的生成参数——steps、cfg、sampler_name、scheduler、denoise、seed、width、height、batch_size(该次调用生成的图像数量),以及 aspect_ratio(形如\"3:2\"的比例预设,仅当工作流中存在向 width/height 供值的 Resolution Selector 节点时才会生效,否则忽略);模型未提供的参数将保留当前已加载工作流的设置;已验证可正常支持 tool calling 与视觉功能的模型:Gemma 4:e4b、Qwen 3.5:9b、Ministral 3:3b(Ollama)",
|
||||
helpAi4: "TOOLS 面板 — VLM:拖放图像(PNG/JPG/WebP)并选择任务(图像描述 / 提示词生成 / 标签生成),点击 Run;或选择「Create wildcards」,输入类别名称和数量生成纯文本通配符条目(每行一条);拖放的图像也会作为 Chat 面板的附件共享(在该处显示并用于图生图生成),直到点击 ✕ 按钮清除",
|
||||
helpAi6: "Settings 面板:选择后端(Ollama / LM Studio / Lemonade / Unsloth)、设置 API URL、测试连接、选择模型;Unsloth 即使在本地访问时也始终需要 API 密钥认证——将插件文件夹中的 .env.example 复制为 .env 并设置 UNSLOTH_API_KEY,然后重启 ComfyUI(密钥仅在服务器端读取,绝不会发送到浏览器);还需要在 Unsloth Desktop 应用自身的设置界面中开启「Auto model switching (OpenAI API)」——若保持关闭,除非已在 Unsloth Studio 中手动加载过模型,否则请求会以 HTTP 404 失败;Unsloth 的推理模型会将思考内容放在单独的 reasoning_content 字段中,而不是内联的 <think> 标签里,因此 Max tokens 太小会导致模型把全部配额都用在思考上,返回空白回复——如果回复为空,请调高 Max tokens(建议 2048 以上);Generation 部分 — Thinking mode 开关(默认关闭;Ollama会收到think标志,无论使用哪个后端,关闭时都会从输出中去除<think>...</think>思考块,避免推理模型的思考过程混入Translation/Chat/TOOLS的结果)以及 Max tokens(Ollama: num_predict,LM Studio/Lemonade/Unsloth: max_tokens;0为不限);配置 Free 语言名称;Chat 图像生成部分可让 Chat 触发的文生图使用专用工作流,而不是 GenerateUI 中当前加载的工作流;Chat I2I 生成部分提供相同设置,用于附加图像时的图生图生成;设置保存至 localStorage,并与 Library 面板的 A 标签共享",
|
||||
helpAi7: "Chat 面板 — SVG:LLM 还可以直接以文本形式生成 SVG 图形,完全不涉及 ComfyUI 工作流;只需要求它用 SVG 绘制内容,如果回复中包含 SVG 代码,会自动渲染为内嵌预览,并提供\"保存到图库\"按钮,将其保存为 .svg 文件到 Gallery 标签页的输出文件夹;该功能不使用 tool calling(函数调用),因此不支持 tool calling 的模型也可以使用",
|
||||
|
||||
+327
-209
@@ -17,70 +17,89 @@ import { extractAllMetadata, readAllPNGTextChunks } from "./metadata-tab.js";
|
||||
import { syncJsonHighlight, syncScroll } from "./json-highlight.js";
|
||||
import { _expandWildcardsInWorkflow } from "./generate-tab.js";
|
||||
|
||||
const COLUMN_KEYS = ["model", "prompt", "ksampler"];
|
||||
// Checkpoint/LoRA/VAE are three fully independent keyframe timelines — same as
|
||||
// Prompt/KSampler — each with its own +/-, its own atIteration sequence, its own
|
||||
// forward-fill resolution (_resolveKeyframe). The Model column header's C/L/V toggle
|
||||
// only picks which ONE of the three renders into the (single, shared) cell list and
|
||||
// which one +/- and the cell-click modal operate on; it never bundles them together.
|
||||
const COLUMN_KEYS = ["checkpoint", "lora", "vae", "prompt", "ksampler"];
|
||||
const MAX_RESULTS = 9;
|
||||
const RESULTS_GRID_COLS = 3;
|
||||
const LAB_PLAN_PREFIX = "ws_labplan_";
|
||||
const LAB_PLAN_PNG_KEY = "wfm_lab_plan";
|
||||
|
||||
function _defaultValueFor(col) {
|
||||
if (col === "model") return { checkpoint: "", lora: { name: "", strengthModel: 1.0, strengthClip: 1.0 }, vae: "" };
|
||||
if (col === "lora") return { name: "", strengthModel: 1.0, strengthClip: 1.0, nodeBypass: false, applyToPrompt: false };
|
||||
if (col === "prompt") return { positive: "", negative: "", styleApplied: false, styleName: "" };
|
||||
if (col === "ksampler") return { steps: null, cfg: null, sampler_name: "", scheduler: "", denoise: null, seed: null };
|
||||
return "";
|
||||
return ""; // checkpoint, vae
|
||||
}
|
||||
|
||||
// Fills in defaults for a possibly-partial "model" column value — both for legacy
|
||||
// plan data migrated by _migrateLabColumns (which never sets a lora) and defensively
|
||||
// for any hand-edited Plan JSON.
|
||||
function _normalizeModelValue(v) {
|
||||
// Fills in defaults for a possibly-partial LoRA value — both for legacy plan data
|
||||
// migrated by _migrateLabColumns and defensively for any hand-edited Plan JSON.
|
||||
function _normalizeLoraValue(v) {
|
||||
return {
|
||||
checkpoint: v?.checkpoint || "",
|
||||
lora: {
|
||||
name: v?.lora?.name || "",
|
||||
strengthModel: v?.lora?.strengthModel ?? 1.0,
|
||||
strengthClip: v?.lora?.strengthClip ?? 1.0,
|
||||
},
|
||||
vae: v?.vae || "",
|
||||
name: v?.name || "",
|
||||
strengthModel: v?.strengthModel ?? 1.0,
|
||||
strengthClip: v?.strengthClip ?? 1.0,
|
||||
nodeBypass: !!v?.nodeBypass,
|
||||
applyToPrompt: !!v?.applyToPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
function _emptyColumns() {
|
||||
return {
|
||||
model: [{ atIteration: 1, value: _defaultValueFor("model"), revertToBase: false, bypassed: false }],
|
||||
checkpoint: [{ atIteration: 1, value: _defaultValueFor("checkpoint"), revertToBase: false, bypassed: false }],
|
||||
lora: [{ atIteration: 1, value: _defaultValueFor("lora"), revertToBase: false, bypassed: false }],
|
||||
vae: [{ atIteration: 1, value: _defaultValueFor("vae"), revertToBase: false, bypassed: false }],
|
||||
prompt: [{ atIteration: 1, value: _defaultValueFor("prompt"), revertToBase: false, bypassed: false }],
|
||||
ksampler: [{ atIteration: 1, value: _defaultValueFor("ksampler"), revertToBase: false, bypassed: false }],
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy Plan JSON (pre "Model column" merge) stored separate checkpoint/vae columns.
|
||||
// Merges them into one "model" column keyed by atIteration so old .json/.png plans
|
||||
// still load correctly. LoRA is left empty since the old format never had it.
|
||||
// Two prior on-disk Plan JSON shapes need upgrading to today's independent
|
||||
// checkpoint/lora/vae arrays:
|
||||
// - Pre-v0.4.2: separate top-level `checkpoint`/`vae` columns, no `lora` at all.
|
||||
// - v0.4.2's brief "merged Model row" design: `columns.model` was a single array of
|
||||
// {atIteration, value:{checkpoint,lora,vae}, ...} rows bundling all three together.
|
||||
// That bundling caused its own bugs (+/- and the cell modal affected all three
|
||||
// fields at once) and was replaced the same release cycle, but a plan saved during
|
||||
// that window must still load. Values are moved into their own independent arrays;
|
||||
// iteration #1 is always kept (baseline row) even if a field was blank there.
|
||||
function _migrateLabColumns(columns) {
|
||||
if (!columns) return _emptyColumns();
|
||||
if (columns.model) return columns;
|
||||
if (!columns.model && Array.isArray(columns.lora)) return columns; // already current shape (independent lora array is the marker)
|
||||
|
||||
const ckptKfs = columns.checkpoint || [];
|
||||
const vaeKfs = columns.vae || [];
|
||||
const { checkpoint, vae, ...rest } = columns;
|
||||
const defaults = _emptyColumns();
|
||||
|
||||
if (ckptKfs.length === 0 && vaeKfs.length === 0) {
|
||||
return { ...rest, model: _emptyColumns().model };
|
||||
if (Array.isArray(columns.model)) {
|
||||
const checkpoint = [], lora = [], vae = [];
|
||||
for (const kf of columns.model) {
|
||||
const v = kf.value || {};
|
||||
const base = { atIteration: kf.atIteration, revertToBase: !!kf.revertToBase, bypassed: !!kf.bypassed };
|
||||
if (kf.atIteration === 1 || v.checkpoint) checkpoint.push({ ...base, value: v.checkpoint || "" });
|
||||
if (kf.atIteration === 1 || v.lora?.name) lora.push({ ...base, value: _normalizeLoraValue(v.lora) });
|
||||
if (kf.atIteration === 1 || v.vae) vae.push({ ...base, value: v.vae || "" });
|
||||
}
|
||||
const { model, ...rest } = columns;
|
||||
return {
|
||||
...rest,
|
||||
checkpoint: checkpoint.length ? checkpoint : defaults.checkpoint,
|
||||
lora: lora.length ? lora : defaults.lora,
|
||||
vae: vae.length ? vae : defaults.vae,
|
||||
};
|
||||
}
|
||||
|
||||
const iterations = [...new Set([1, ...ckptKfs.map((k) => k.atIteration), ...vaeKfs.map((k) => k.atIteration)])].sort((a, b) => a - b);
|
||||
const findAt = (kfs, iter) => kfs.find((k) => k.atIteration === iter);
|
||||
const model = iterations.map((iter) => {
|
||||
const ck = findAt(ckptKfs, iter);
|
||||
const vk = findAt(vaeKfs, iter);
|
||||
if (columns.checkpoint || columns.vae) {
|
||||
return {
|
||||
atIteration: iter,
|
||||
bypassed: !!(ck?.bypassed || vk?.bypassed),
|
||||
revertToBase: !!(ck?.revertToBase || vk?.revertToBase),
|
||||
value: { checkpoint: ck?.value || "", lora: { name: "", strengthModel: 1.0, strengthClip: 1.0 }, vae: vk?.value || "" },
|
||||
...columns,
|
||||
checkpoint: columns.checkpoint?.length ? columns.checkpoint : defaults.checkpoint,
|
||||
lora: defaults.lora,
|
||||
vae: columns.vae?.length ? columns.vae : defaults.vae,
|
||||
};
|
||||
});
|
||||
return { ...rest, model };
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function _emptyLabState() {
|
||||
@@ -343,10 +362,21 @@ function _initPlanButtons() {
|
||||
// Column keyframes: render / add / remove / edit
|
||||
// ============================================
|
||||
|
||||
// A click on +/- always targets whichever of checkpoint/lora/vae the Model column
|
||||
// header's C/L/V toggle currently has selected — "model" is a sentinel dataset.col
|
||||
// value on those two shared buttons (there's only one +/- pair in the DOM for the
|
||||
// merged-looking Model column), never a real key into _lab.columns. Resolving it here
|
||||
// means adding/removing a keyframe only ever touches the ONE field being viewed, never
|
||||
// the other two — Prompt/KSampler's own +/- buttons carry their real col directly and
|
||||
// pass through unchanged.
|
||||
function _resolveColumnKey(col) {
|
||||
return col === "model" ? _modelViewMode : col;
|
||||
}
|
||||
|
||||
function _initColumnButtons() {
|
||||
document.querySelectorAll(".wfm-lab-col-add").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const col = btn.dataset.col;
|
||||
const col = _resolveColumnKey(btn.dataset.col);
|
||||
const kfs = _lab.columns[col];
|
||||
const lastIter = kfs[kfs.length - 1].atIteration;
|
||||
if (lastIter >= _lab.batchCount) {
|
||||
@@ -354,42 +384,59 @@ function _initColumnButtons() {
|
||||
return;
|
||||
}
|
||||
kfs.push({ atIteration: lastIter + 1, value: _defaultValueFor(col), revertToBase: false, bypassed: false });
|
||||
_renderColumn(col);
|
||||
_renderColumnForKey(col);
|
||||
_openCellModal(col, kfs.length - 1);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".wfm-lab-col-remove").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const col = btn.dataset.col;
|
||||
const col = _resolveColumnKey(btn.dataset.col);
|
||||
const kfs = _lab.columns[col];
|
||||
if (kfs.length <= 1) return; // Row 1 (baseline) cannot be removed
|
||||
kfs.pop();
|
||||
_renderColumn(col);
|
||||
_renderColumnForKey(col);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Wires the Model column header's C/L/V buttons — switches which single field its cells
|
||||
// summarize (see _modelViewMode) and re-renders just that column. Purely a display
|
||||
// filter: the underlying keyframe data (all three fields) is untouched either way.
|
||||
// Wires the Model column header's C/L/V buttons — switches which independent field
|
||||
// (checkpoint/lora/vae) the shared cell list, +/-, and cell-click modal all operate on.
|
||||
// Purely a display/routing selector: none of the three keyframe arrays are touched by
|
||||
// switching it.
|
||||
function _initModelViewToggle() {
|
||||
document.querySelectorAll(".wfm-lab-model-view-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
_modelViewMode = btn.dataset.view;
|
||||
document.querySelectorAll(".wfm-lab-model-view-btn").forEach((b) => b.classList.toggle("active", b === btn));
|
||||
_renderColumn("model");
|
||||
_renderModelColumn();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _renderAllColumns() {
|
||||
COLUMN_KEYS.forEach(_renderColumn);
|
||||
_renderModelColumn();
|
||||
_renderColumn("prompt");
|
||||
_renderColumn("ksampler");
|
||||
}
|
||||
|
||||
function _renderColumn(col) {
|
||||
const container = document.getElementById(`wfm-lab-col-cells-${col}`);
|
||||
// Renders the Model column's shared cell list from whichever of checkpoint/lora/vae is
|
||||
// currently toggled (_modelViewMode) — the other two arrays exist in _lab.columns but
|
||||
// have no DOM of their own to render into; they're only ever shown when toggled to.
|
||||
function _renderModelColumn() {
|
||||
_updateModelViewHighlight();
|
||||
_renderColumn(_modelViewMode, "wfm-lab-col-cells-model");
|
||||
}
|
||||
|
||||
// Routes a keyframe-array key to whichever container renders it: checkpoint/lora/vae
|
||||
// all share the Model column's single cell list, everything else has its own.
|
||||
function _renderColumnForKey(col) {
|
||||
if (col === "checkpoint" || col === "lora" || col === "vae") _renderModelColumn();
|
||||
else _renderColumn(col);
|
||||
}
|
||||
|
||||
function _renderColumn(col, containerId = `wfm-lab-col-cells-${col}`) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
if (col === "model") _updateModelViewHighlight();
|
||||
const keyframes = _lab.columns[col];
|
||||
container.innerHTML = keyframes.map((kf, idx) => _cellHtml(col, kf, idx)).join("");
|
||||
container.querySelectorAll(".wfm-lab-cell").forEach((el) => {
|
||||
@@ -416,6 +463,7 @@ function _updateModelViewHighlight() {
|
||||
function _cellHtml(col, kf, idx) {
|
||||
const revertClass = kf.revertToBase ? " wfm-lab-cell-revert" : "";
|
||||
const bypassClass = kf.bypassed ? " wfm-lab-cell-bypassed" : "";
|
||||
const nodeBypassClass = (col === "lora" && kf.value?.nodeBypass) ? " wfm-lab-cell-node-bypassed" : "";
|
||||
if (idx === 0) {
|
||||
// Bypassed takes priority over the live-reflect display — an explicit "skip this
|
||||
// row" toggle shouldn't be masked by Setting 1's usual "show the workflow's
|
||||
@@ -423,13 +471,13 @@ function _cellHtml(col, kf, idx) {
|
||||
const live = !kf.bypassed && _isLiveDisplay(col, idx);
|
||||
const liveClass = live ? " wfm-lab-cell-live" : "";
|
||||
const label = live ? _cellLabel(col, { value: _liveValueFor(col), revertToBase: false, bypassed: false }) : _cellLabel(col, kf);
|
||||
return `<div class="wfm-lab-cell${revertClass}${bypassClass}${liveClass}" data-idx="${idx}">
|
||||
return `<div class="wfm-lab-cell${revertClass}${bypassClass}${nodeBypassClass}${liveClass}" data-idx="${idx}">
|
||||
<span class="wfm-lab-cell-index">${kf.atIteration}:</span>
|
||||
<span class="wfm-lab-cell-value">${label}</span>
|
||||
</div>`;
|
||||
}
|
||||
// Iteration #2 and beyond: value on its own line, applied-at iteration below it
|
||||
return `<div class="wfm-lab-cell${revertClass}${bypassClass}" data-idx="${idx}">
|
||||
return `<div class="wfm-lab-cell${revertClass}${bypassClass}${nodeBypassClass}" data-idx="${idx}">
|
||||
<div class="wfm-lab-cell-value">${_cellLabel(col, kf)}</div>
|
||||
<div class="wfm-lab-cell-applied">${t("labAppliedAt", kf.atIteration)}</div>
|
||||
</div>`;
|
||||
@@ -438,19 +486,15 @@ function _cellHtml(col, kf, idx) {
|
||||
function _cellLabel(col, kf) {
|
||||
if (kf.bypassed) return t("labBypassed");
|
||||
if (kf.revertToBase) return t("labRevertToBase");
|
||||
if (col === "model") {
|
||||
// The header's C/L/V toggle (_modelViewMode) picks which single field this cell
|
||||
// summarizes. Real keyframes (atIteration set) resolve the field's inherited/
|
||||
// effective value via _resolveModelKeyframe — e.g. a keyframe that only touches
|
||||
// LoRA still shows whatever Checkpoint is actually in effect when viewing "C",
|
||||
// not a blank. The synthetic live-preview object built for keyframe #1 (no
|
||||
// atIteration) already IS that resolved value, so it's used as-is.
|
||||
const resolved = (kf.atIteration != null) ? _resolveModelKeyframe(kf.atIteration) : _normalizeModelValue(kf.value);
|
||||
let display = "";
|
||||
if (_modelViewMode === "checkpoint") display = resolved.checkpoint;
|
||||
else if (_modelViewMode === "vae") display = resolved.vae;
|
||||
else if (_modelViewMode === "lora" && resolved.lora.name) display = `${resolved.lora.name} (${resolved.lora.strengthModel}/${resolved.lora.strengthClip})`;
|
||||
return display ? escapeHtml(display) : t("labUseWorkflowDefault");
|
||||
if (col === "checkpoint" || col === "vae") {
|
||||
return kf.value ? escapeHtml(kf.value) : t("labUseWorkflowDefault");
|
||||
}
|
||||
if (col === "lora") {
|
||||
const v = kf.value;
|
||||
if (!v?.name) return t("labUseWorkflowDefault");
|
||||
if (v.nodeBypass) return escapeHtml(`${v.name} ${t("labLoraNodeBypassed")}`);
|
||||
const promptTag = v.applyToPrompt ? ` ${t("labLoraPromptAppliedTag")}` : "";
|
||||
return escapeHtml(`${v.name} (${v.strengthModel}/${v.strengthClip})${promptTag}`);
|
||||
}
|
||||
if (col === "prompt") {
|
||||
const p = kf.value?.positive || "";
|
||||
@@ -471,10 +515,8 @@ function _cellLabel(col, kf) {
|
||||
}
|
||||
|
||||
function _isEmptyValue(col, value) {
|
||||
if (col === "model") {
|
||||
const v = _normalizeModelValue(value);
|
||||
return !v.checkpoint && !v.lora.name && !v.vae;
|
||||
}
|
||||
if (col === "checkpoint" || col === "vae") return !value;
|
||||
if (col === "lora") return !value?.name;
|
||||
if (col === "prompt") return !value || (!value.positive && !value.negative);
|
||||
if (col === "ksampler") {
|
||||
return !value || (value.steps == null && value.cfg == null && value.denoise == null &&
|
||||
@@ -492,14 +534,11 @@ function _isEmptyValue(col, value) {
|
||||
function _liveValueFor(col) {
|
||||
const analysis = comfyUI.currentAnalysis;
|
||||
if (!analysis) return null;
|
||||
if (col === "model") {
|
||||
const ckpt = analysis.checkpoint_nodes?.[0]?.ckpt_name || "";
|
||||
const vae = analysis.vae_nodes?.[0]?.vae_name || "";
|
||||
// LoRA is intentionally left out of the live-reflected value — there's no safe
|
||||
// way to pick "the" active LoRA back out of a workflow node to prefill with.
|
||||
if (!ckpt && !vae) return null;
|
||||
return { checkpoint: ckpt, lora: { name: "", strengthModel: 1.0, strengthClip: 1.0 }, vae };
|
||||
}
|
||||
if (col === "checkpoint") return analysis.checkpoint_nodes?.[0]?.ckpt_name || null;
|
||||
if (col === "vae") return analysis.vae_nodes?.[0]?.vae_name || null;
|
||||
// LoRA is intentionally never live-reflected — there's no safe way to pick "the"
|
||||
// active LoRA back out of a workflow node to prefill with.
|
||||
if (col === "lora") return null;
|
||||
if (col === "prompt") {
|
||||
const posNode = analysis.prompt_nodes?.find((n) => n.role === "positive");
|
||||
const negNode = analysis.prompt_nodes?.find((n) => n.role === "negative");
|
||||
@@ -544,53 +583,56 @@ function _openCellModal(col, idx) {
|
||||
const effectiveValue = live ? _liveValueFor(col) : kf.value;
|
||||
const hasPrevious = col === "prompt" && idx > 0;
|
||||
|
||||
// What this row would inherit if left blank (the latest earlier keyframe in this
|
||||
// SAME field's own array, not touching the other two) — shown as the blank option's
|
||||
// label so it's clear leaving it untouched still carries a value forward, rather
|
||||
// than resetting to the workflow's own default. Only meaningful past #1.
|
||||
const inherited = isFirst ? null : _resolveKeyframe(col, kf.atIteration - 1);
|
||||
|
||||
let valueFieldsHtml = "";
|
||||
if (col === "model") {
|
||||
// Shows ONLY the field matching the column header's C/L/V toggle (_modelViewMode)
|
||||
// — no in-modal tab switcher — so what's open in the modal always matches what the
|
||||
// cell/header are currently showing. To edit a different field, switch the header
|
||||
// toggle first, then reopen the cell; Save only touches the field being shown here
|
||||
// (see the save handler below), leaving the other two exactly as already stored.
|
||||
const v = _normalizeModelValue(effectiveValue);
|
||||
// What this row would inherit for the shown field if left blank (everything up to,
|
||||
// but not including, this keyframe's own atIteration) — shown as the blank option's
|
||||
// label so it's clear leaving it untouched still carries a value forward, rather
|
||||
// than resetting to the workflow's own default. Only meaningful past #1.
|
||||
const inherited = isFirst ? null : _resolveModelKeyframe(kf.atIteration - 1);
|
||||
const selectFieldHtml = (idPrefix, options, currentVal, inheritedVal) => `
|
||||
<div class="wfm-search-wrap" style="width:100%;">
|
||||
<input type="text" id="wfm-lab-modal-${idPrefix}-filter" class="wfm-input wfm-search-input" placeholder="Filter...">
|
||||
<button type="button" class="wfm-search-clear-btn" id="wfm-lab-modal-${idPrefix}-filter-clear" title="Clear search">✕</button>
|
||||
</div>
|
||||
<select id="wfm-lab-modal-${idPrefix}-select" class="wfm-select">
|
||||
<option value="">${escapeHtml(inheritedVal ? t("labInheritsFrom", inheritedVal) : t("labUseWorkflowDefault"))}</option>
|
||||
${options.map((m) => `<option value="${escapeHtml(m)}" ${m === currentVal ? "selected" : ""}>${escapeHtml(m)}</option>`).join("")}
|
||||
</select>`;
|
||||
if (_modelViewMode === "checkpoint") {
|
||||
valueFieldsHtml = `
|
||||
<div class="wfm-lab-modal-row">
|
||||
<label>Checkpoint</label>
|
||||
${selectFieldHtml("ckpt", comfyEditor.models.checkpoints || [], v.checkpoint, inherited?.checkpoint)}
|
||||
</div>`;
|
||||
} else if (_modelViewMode === "lora") {
|
||||
valueFieldsHtml = `
|
||||
<div class="wfm-lab-modal-row">
|
||||
<label>LoRA</label>
|
||||
${selectFieldHtml("lora", comfyEditor.models.loras || [], v.lora.name, inherited?.lora?.name)}
|
||||
<div class="wfm-lora-strength-single" style="margin-top:6px;">
|
||||
<span>M</span>
|
||||
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strmodel" value="${v.lora.strengthModel}" step="0.05" min="0" max="2">
|
||||
<span>C</span>
|
||||
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strclip" value="${v.lora.strengthClip}" step="0.05" min="0" max="2">
|
||||
</div>
|
||||
</div>`;
|
||||
} else if (_modelViewMode === "vae") {
|
||||
valueFieldsHtml = `
|
||||
<div class="wfm-lab-modal-row">
|
||||
<label>VAE</label>
|
||||
${selectFieldHtml("vae", comfyEditor.models.vaes || [], v.vae, inherited?.vae)}
|
||||
</div>`;
|
||||
}
|
||||
if (col === "checkpoint" || col === "vae") {
|
||||
const options = comfyEditor.models[col === "checkpoint" ? "checkpoints" : "vaes"] || [];
|
||||
valueFieldsHtml = `
|
||||
<div class="wfm-lab-modal-row">
|
||||
<label>${col === "checkpoint" ? "Checkpoint" : "VAE"}</label>
|
||||
<div class="wfm-search-wrap" style="margin-bottom:4px;width:100%;">
|
||||
<input type="text" id="wfm-lab-modal-value-filter" class="wfm-input wfm-search-input" placeholder="Filter...">
|
||||
<button type="button" class="wfm-search-clear-btn" id="wfm-lab-modal-value-filter-clear" title="Clear search">✕</button>
|
||||
</div>
|
||||
<select id="wfm-lab-modal-value" class="wfm-select">
|
||||
<option value="">${escapeHtml(inherited ? t("labInheritsFrom", inherited) : t("labUseWorkflowDefault"))}</option>
|
||||
${options.map((m) => `<option value="${escapeHtml(m)}" ${m === effectiveValue ? "selected" : ""}>${escapeHtml(m)}</option>`).join("")}
|
||||
</select>
|
||||
</div>`;
|
||||
} else if (col === "lora") {
|
||||
const v = _normalizeLoraValue(effectiveValue);
|
||||
const options = comfyEditor.models.loras || [];
|
||||
valueFieldsHtml = `
|
||||
<div class="wfm-lab-modal-row">
|
||||
<label>LoRA</label>
|
||||
<div class="wfm-search-wrap" style="width:100%;">
|
||||
<input type="text" id="wfm-lab-modal-lora-filter" class="wfm-input wfm-search-input" placeholder="Filter...">
|
||||
<button type="button" class="wfm-search-clear-btn" id="wfm-lab-modal-lora-filter-clear" title="Clear search">✕</button>
|
||||
</div>
|
||||
<select id="wfm-lab-modal-lora-select" class="wfm-select">
|
||||
<option value="">${escapeHtml(inherited?.name ? t("labInheritsFrom", inherited.name) : t("labUseWorkflowDefault"))}</option>
|
||||
${options.map((m) => `<option value="${escapeHtml(m)}" ${m === v.name ? "selected" : ""}>${escapeHtml(m)}</option>`).join("")}
|
||||
</select>
|
||||
<div class="wfm-lora-strength-single" style="margin-top:6px;">
|
||||
<span>M</span>
|
||||
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strmodel" value="${v.strengthModel}" step="0.05" min="0" max="2" ${v.nodeBypass ? "disabled" : ""}>
|
||||
<span>C</span>
|
||||
<input type="number" class="wfm-input" id="wfm-lab-modal-lora-strclip" value="${v.strengthClip}" step="0.05" min="0" max="2" ${v.nodeBypass ? "disabled" : ""}>
|
||||
</div>
|
||||
<div class="wfm-lab-modal-checkbox" style="margin-top:6px;">
|
||||
<input type="checkbox" id="wfm-lab-modal-lora-nodebypass" ${v.nodeBypass ? "checked" : ""}>
|
||||
<label for="wfm-lab-modal-lora-nodebypass">${t("labLoraNodeBypassCheckboxLabel")}</label>
|
||||
</div>
|
||||
<div class="wfm-lab-modal-checkbox">
|
||||
<input type="checkbox" id="wfm-lab-modal-lora-prompttoggle" ${v.applyToPrompt ? "checked" : ""} ${v.nodeBypass ? "disabled" : ""}>
|
||||
<label for="wfm-lab-modal-lora-prompttoggle">${t("labLoraApplyToPromptCheckboxLabel")}</label>
|
||||
</div>
|
||||
</div>`;
|
||||
} else if (col === "prompt") {
|
||||
const v = effectiveValue || {};
|
||||
valueFieldsHtml = `
|
||||
@@ -683,7 +725,8 @@ function _openCellModal(col, idx) {
|
||||
</div>`;
|
||||
openModal(`${col} #${idx + 1}`, html);
|
||||
if (col === "prompt") _wireLabPromptModalExtras(idx);
|
||||
if (col === "model") _wireLabModelFieldFilter();
|
||||
if (col === "checkpoint" || col === "vae") _wireLabValueFilter(col);
|
||||
if (col === "lora") _wireLabLoraFilter();
|
||||
|
||||
// Bypass disables everything else on the modal (value fields AND the Revert
|
||||
// checkbox) — the two are mutually exclusive since bypass already means "ignore
|
||||
@@ -692,6 +735,14 @@ function _openCellModal(col, idx) {
|
||||
const bypassCb = document.getElementById("wfm-lab-modal-bypass");
|
||||
const fieldsWrap = document.getElementById("wfm-lab-modal-fields");
|
||||
const revertRowEl = revertCb?.closest(".wfm-lab-modal-checkbox");
|
||||
// LoRA-only: the node-bypass checkbox (force strength to 0 at apply-time — see
|
||||
// _applyLabLoraToWorkflow) disables just the strength fields, independent of the
|
||||
// row-level bypass/revert above (which disable everything in fieldsWrap instead).
|
||||
const loraNodeBypassCb = document.getElementById("wfm-lab-modal-lora-nodebypass");
|
||||
const loraStrModelEl = document.getElementById("wfm-lab-modal-lora-strmodel");
|
||||
const loraStrClipEl = document.getElementById("wfm-lab-modal-lora-strclip");
|
||||
// Bypassing the LoRA node makes "apply its syntax/triggers to the prompt" moot too.
|
||||
const loraPromptCb = document.getElementById("wfm-lab-modal-lora-prompttoggle");
|
||||
const syncDisabled = () => {
|
||||
const bypassed = !!bypassCb?.checked;
|
||||
const fieldsDisabled = bypassed || !!revertCb?.checked;
|
||||
@@ -699,14 +750,20 @@ function _openCellModal(col, idx) {
|
||||
fieldsWrap?.querySelectorAll("input,select,textarea").forEach((el) => { el.disabled = fieldsDisabled; });
|
||||
if (revertCb) revertCb.disabled = bypassed;
|
||||
if (revertRowEl) revertRowEl.style.opacity = bypassed ? "0.4" : "1";
|
||||
if (!fieldsDisabled && loraNodeBypassCb?.checked) {
|
||||
if (loraStrModelEl) loraStrModelEl.disabled = true;
|
||||
if (loraStrClipEl) loraStrClipEl.disabled = true;
|
||||
if (loraPromptCb) loraPromptCb.disabled = true;
|
||||
}
|
||||
};
|
||||
revertCb?.addEventListener("change", syncDisabled);
|
||||
bypassCb?.addEventListener("change", syncDisabled);
|
||||
loraNodeBypassCb?.addEventListener("change", syncDisabled);
|
||||
syncDisabled();
|
||||
|
||||
document.getElementById("wfm-lab-modal-delete")?.addEventListener("click", () => {
|
||||
_lab.columns[col].splice(idx, 1);
|
||||
_renderColumn(col);
|
||||
_renderColumnForKey(col);
|
||||
closeModal();
|
||||
});
|
||||
|
||||
@@ -721,23 +778,17 @@ function _openCellModal(col, idx) {
|
||||
|
||||
let value = kf.value;
|
||||
if (!revert && !bypassed) {
|
||||
if (col === "model") {
|
||||
// Only the field the modal actually shows (_modelViewMode) gets updated —
|
||||
// the other two are carried over from this row's own currently-saved
|
||||
// value untouched, since their fields aren't in the DOM to read from.
|
||||
const next = _normalizeModelValue(kf.value);
|
||||
if (_modelViewMode === "checkpoint") {
|
||||
next.checkpoint = document.getElementById("wfm-lab-modal-ckpt-select")?.value || "";
|
||||
} else if (_modelViewMode === "lora") {
|
||||
next.lora = {
|
||||
name: document.getElementById("wfm-lab-modal-lora-select")?.value || "",
|
||||
strengthModel: parseFloat(document.getElementById("wfm-lab-modal-lora-strmodel")?.value) || 1.0,
|
||||
strengthClip: parseFloat(document.getElementById("wfm-lab-modal-lora-strclip")?.value) || 1.0,
|
||||
};
|
||||
} else if (_modelViewMode === "vae") {
|
||||
next.vae = document.getElementById("wfm-lab-modal-vae-select")?.value || "";
|
||||
}
|
||||
value = next;
|
||||
if (col === "checkpoint" || col === "vae") {
|
||||
value = document.getElementById("wfm-lab-modal-value")?.value || "";
|
||||
} else if (col === "lora") {
|
||||
const loraNodeBypassed = !!document.getElementById("wfm-lab-modal-lora-nodebypass")?.checked;
|
||||
value = {
|
||||
name: document.getElementById("wfm-lab-modal-lora-select")?.value || "",
|
||||
strengthModel: parseFloat(document.getElementById("wfm-lab-modal-lora-strmodel")?.value) || 1.0,
|
||||
strengthClip: parseFloat(document.getElementById("wfm-lab-modal-lora-strclip")?.value) || 1.0,
|
||||
nodeBypass: loraNodeBypassed,
|
||||
applyToPrompt: !loraNodeBypassed && !!document.getElementById("wfm-lab-modal-lora-prompttoggle")?.checked,
|
||||
};
|
||||
} else if (col === "prompt") {
|
||||
value = {
|
||||
positive: document.getElementById("wfm-lab-modal-positive")?.value || "",
|
||||
@@ -763,7 +814,7 @@ function _openCellModal(col, idx) {
|
||||
|
||||
_lab.columns[col][idx] = { atIteration, value, revertToBase: revert, bypassed };
|
||||
_lab.columns[col].sort((a, b) => a.atIteration - b.atIteration);
|
||||
_renderColumn(col);
|
||||
_renderColumnForKey(col);
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
@@ -784,28 +835,46 @@ function _applyStyleToText(positive, negative, style) {
|
||||
};
|
||||
}
|
||||
|
||||
// Wires the filter box above whichever single field the Model-column modal is
|
||||
// currently showing (_modelViewMode picks which one exists in the DOM — the other two
|
||||
// calls below are harmless no-ops since their elements aren't present). Same filter
|
||||
// behavior as GenerateUI's Model tab filter inputs (comfyui-editor.js renderModelTab).
|
||||
function _wireLabModelFieldFilter() {
|
||||
const wireFilter = (idPrefix, modelsKey) => {
|
||||
const filterInput = document.getElementById(`wfm-lab-modal-${idPrefix}-filter`);
|
||||
const select = document.getElementById(`wfm-lab-modal-${idPrefix}-select`);
|
||||
if (!filterInput || !select) return;
|
||||
const options = comfyEditor.models[modelsKey] || [];
|
||||
const applyFilter = () => {
|
||||
const filter = filterInput.value.toLowerCase();
|
||||
select.innerHTML = `<option value="">${t("labUseWorkflowDefault")}</option>` +
|
||||
options.filter((m) => m.toLowerCase().includes(filter))
|
||||
.map((m) => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join("");
|
||||
};
|
||||
filterInput.addEventListener("input", applyFilter);
|
||||
setupSearchClearBtn(filterInput.id, `${filterInput.id}-clear`, applyFilter);
|
||||
// Filter box above the Checkpoint/VAE modal's <select>, same behavior as the
|
||||
// Model tab's filter inputs (comfyui-editor.js renderModelTab): rebuild the
|
||||
// option list on every keystroke, always keeping the "workflow default" option.
|
||||
// The overlay ✕ clear button reuses the same setupSearchClearBtn() helper as the
|
||||
// Models/Nodes/Workflow/Gallery tab search boxes and the Raw JSON search bar.
|
||||
function _wireLabValueFilter(col) {
|
||||
const filterInput = document.getElementById("wfm-lab-modal-value-filter");
|
||||
const select = document.getElementById("wfm-lab-modal-value");
|
||||
if (!filterInput || !select) return;
|
||||
const options = comfyEditor.models[col === "checkpoint" ? "checkpoints" : "vaes"] || [];
|
||||
|
||||
const applyFilter = () => {
|
||||
const filter = filterInput.value.toLowerCase();
|
||||
select.innerHTML = `<option value="">${t("labUseWorkflowDefault")}</option>` +
|
||||
options.filter((m) => m.toLowerCase().includes(filter))
|
||||
.map((m) => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join("");
|
||||
};
|
||||
wireFilter("ckpt", "checkpoints");
|
||||
wireFilter("lora", "loras");
|
||||
wireFilter("vae", "vaes");
|
||||
|
||||
filterInput.addEventListener("input", applyFilter);
|
||||
setupSearchClearBtn("wfm-lab-modal-value-filter", "wfm-lab-modal-value-filter-clear", applyFilter);
|
||||
}
|
||||
|
||||
// Same filter behavior as _wireLabValueFilter, for the LoRA modal's own select (it
|
||||
// has separate element ids since a LoRA keyframe row also carries Strength M/C
|
||||
// inputs alongside it, unlike the plain single-select Checkpoint/VAE modal).
|
||||
function _wireLabLoraFilter() {
|
||||
const filterInput = document.getElementById("wfm-lab-modal-lora-filter");
|
||||
const select = document.getElementById("wfm-lab-modal-lora-select");
|
||||
if (!filterInput || !select) return;
|
||||
const options = comfyEditor.models.loras || [];
|
||||
|
||||
const applyFilter = () => {
|
||||
const filter = filterInput.value.toLowerCase();
|
||||
select.innerHTML = `<option value="">${t("labUseWorkflowDefault")}</option>` +
|
||||
options.filter((m) => m.toLowerCase().includes(filter))
|
||||
.map((m) => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join("");
|
||||
};
|
||||
|
||||
filterInput.addEventListener("input", applyFilter);
|
||||
setupSearchClearBtn("wfm-lab-modal-lora-filter", "wfm-lab-modal-lora-filter-clear", applyFilter);
|
||||
}
|
||||
|
||||
// Wires the Prompt-column modal's extra actions: fetch the live GenerateUI prompt,
|
||||
@@ -941,77 +1010,65 @@ function _resolveKeyframe(col, iteration) {
|
||||
return _isEmptyValue(col, applicable.value) ? null : applicable.value;
|
||||
}
|
||||
|
||||
// Unlike _resolveKeyframe (which swaps in one keyframe's whole value), the Model column
|
||||
// bundles three independent settings — Checkpoint/LoRA/VAE — into a single row, so a
|
||||
// keyframe that only sets a LoRA must NOT blank out a Checkpoint/VAE chosen by an earlier
|
||||
// keyframe. This resolves each of the three fields separately: walking keyframes up to
|
||||
// `iteration` in order, each field keeps whatever value it last saw set (non-empty),
|
||||
// carrying forward across keyframes that leave it blank. A `revertToBase` keyframe resets
|
||||
// all three fields at once to iteration #1's row (mirrors _resolveKeyframe's revert
|
||||
// semantics) rather than being treated as a per-field value of its own. Bypassed keyframes
|
||||
// are skipped entirely, as elsewhere.
|
||||
function _resolveModelKeyframe(iteration) {
|
||||
const kfs = _lab.columns.model;
|
||||
const row1 = kfs.find((k) => k.atIteration === 1);
|
||||
const row1Value = (row1 && !row1.bypassed) ? _normalizeModelValue(row1.value) : null;
|
||||
|
||||
let current = _defaultValueFor("model");
|
||||
for (const kf of kfs) {
|
||||
if (kf.atIteration > iteration) break;
|
||||
if (kf.bypassed) continue;
|
||||
if (kf.revertToBase) {
|
||||
current = row1Value ? _normalizeModelValue(row1Value) : _defaultValueFor("model");
|
||||
continue;
|
||||
}
|
||||
const v = _normalizeModelValue(kf.value);
|
||||
if (v.checkpoint) current.checkpoint = v.checkpoint;
|
||||
if (v.lora.name) current.lora = { ...v.lora };
|
||||
if (v.vae) current.vae = v.vae;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function _buildWorkflowForIteration(iteration, chainImageRef) {
|
||||
function _buildWorkflowForIteration(iteration, chainImageRef, prevLoraInjection) {
|
||||
const wf = JSON.parse(JSON.stringify(comfyUI.currentWorkflow || {}));
|
||||
const analysis = comfyUI.currentAnalysis;
|
||||
if (!analysis) return { workflow: wf, seed: null };
|
||||
if (!analysis) return { workflow: wf, seed: null, loraInjection: null };
|
||||
|
||||
const write = (nodeId, key, val) => {
|
||||
if (nodeId != null && wf[nodeId]) wf[nodeId].inputs[key] = val;
|
||||
};
|
||||
|
||||
const model = _resolveModelKeyframe(iteration);
|
||||
if (model.checkpoint && analysis.checkpoint_nodes?.[0]) {
|
||||
write(analysis.checkpoint_nodes[0].id, "ckpt_name", model.checkpoint);
|
||||
const ckpt = _resolveKeyframe("checkpoint", iteration);
|
||||
if (ckpt && analysis.checkpoint_nodes?.[0]) {
|
||||
write(analysis.checkpoint_nodes[0].id, "ckpt_name", ckpt);
|
||||
}
|
||||
|
||||
if (model.vae) {
|
||||
const vae = _resolveKeyframe("vae", iteration);
|
||||
if (vae) {
|
||||
const vaeNode = analysis.vae_nodes?.[0];
|
||||
if (vaeNode && wf[vaeNode.id]) {
|
||||
write(vaeNode.id, "vae_name", model.vae);
|
||||
write(vaeNode.id, "vae_name", vae);
|
||||
} else {
|
||||
// Workflow has no standalone VAELoader node (VAE baked into the checkpoint) —
|
||||
// inject one and repoint every "vae" input link at it so the override actually applies.
|
||||
const newId = String(Math.max(0, ...Object.keys(wf).map(Number)) + 1);
|
||||
wf[newId] = { class_type: "VAELoader", inputs: { vae_name: model.vae }, _meta: { title: "VAE Loader" } };
|
||||
wf[newId] = { class_type: "VAELoader", inputs: { vae_name: vae }, _meta: { title: "VAE Loader" } };
|
||||
for (const node of Object.values(wf)) {
|
||||
if (Array.isArray(node?.inputs?.vae)) node.inputs.vae = [newId, 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (model.lora.name) {
|
||||
_applyLabLoraToWorkflow(wf, analysis, model.lora);
|
||||
const lora = _resolveKeyframe("lora", iteration);
|
||||
if (lora?.name) {
|
||||
_applyLabLoraToWorkflow(wf, analysis, lora);
|
||||
}
|
||||
|
||||
const prompt = _resolveKeyframe("prompt", iteration);
|
||||
const posNode = analysis.prompt_nodes?.find((n) => n.role === "positive");
|
||||
const negNode = analysis.prompt_nodes?.find((n) => n.role === "negative");
|
||||
if (prompt) {
|
||||
const posNode = analysis.prompt_nodes?.find((n) => n.role === "positive");
|
||||
const negNode = analysis.prompt_nodes?.find((n) => n.role === "negative");
|
||||
if (posNode && prompt.positive) write(posNode.id, posNode.textKey || "text", prompt.positive);
|
||||
if (negNode && prompt.negative) write(negNode.id, negNode.textKey || "text", prompt.negative);
|
||||
}
|
||||
|
||||
// LoRA syntax/trigger-word injection into the Positive prompt (opt-in per keyframe
|
||||
// via lora.applyToPrompt). Always strip whatever the previous iteration injected
|
||||
// first, whether or not this iteration re-injects — so switching to a different
|
||||
// LoRA (or a keyframe with the option off) starts from a clean prompt instead of
|
||||
// accumulating every LoRA the batch has used so far. See _stripLoraPromptInjection.
|
||||
let loraInjection = null;
|
||||
if (posNode && wf[posNode.id]) {
|
||||
const textKey = posNode.textKey || "text";
|
||||
let posText = _stripLoraPromptInjection(wf[posNode.id].inputs[textKey] || "", prevLoraInjection);
|
||||
if (lora?.name && lora.applyToPrompt && !lora.nodeBypass) {
|
||||
loraInjection = _buildLoraPromptInjection(lora);
|
||||
posText = posText ? `${posText}, ${loraInjection}` : loraInjection;
|
||||
}
|
||||
write(posNode.id, textKey, posText);
|
||||
}
|
||||
|
||||
let seed = null;
|
||||
const ks = _resolveKeyframe("ksampler", iteration);
|
||||
const sampler = analysis.sampler_nodes?.[0];
|
||||
@@ -1029,7 +1086,7 @@ function _buildWorkflowForIteration(iteration, chainImageRef) {
|
||||
write(analysis.load_image_nodes[0].id, "image", imageRef);
|
||||
}
|
||||
|
||||
return { workflow: wf, seed };
|
||||
return { workflow: wf, seed, loraInjection };
|
||||
}
|
||||
|
||||
// True once a "no LoRA node in this workflow" warning has already been shown during the
|
||||
@@ -1037,6 +1094,58 @@ function _buildWorkflowForIteration(iteration, chainImageRef) {
|
||||
// run instead of once per iteration.
|
||||
let _labLoraWarned = false;
|
||||
|
||||
// metadata.json (sha256 lookup) + CivitAI cache (sha256 -> trainedWords), fetched once
|
||||
// per Run (see _runLabBatch) and used only to resolve trigger words for the Positive-
|
||||
// prompt LoRA injection below — same two endpoints comfyui-editor.js's renderLoraPane
|
||||
// uses for the GenerateUI Model tab's own LORA SYNTAX/TRIGGER WORDS display.
|
||||
let _labModelMetadata = {};
|
||||
let _labCivitaiCache = {};
|
||||
|
||||
async function _fetchLabLoraMetadataCache() {
|
||||
try {
|
||||
const [metaRes, civRes] = await Promise.all([
|
||||
fetch("/api/wfm/models/metadata"),
|
||||
fetch("/api/wfm/models/civitai/cache"),
|
||||
]);
|
||||
_labModelMetadata = metaRes.ok ? await metaRes.json() : {};
|
||||
_labCivitaiCache = civRes.ok ? await civRes.json() : {};
|
||||
} catch {
|
||||
_labModelMetadata = {};
|
||||
_labCivitaiCache = {};
|
||||
}
|
||||
}
|
||||
|
||||
function _labTriggerWordsFor(loraName) {
|
||||
const sha = (_labModelMetadata[loraName] || {}).sha256;
|
||||
const civInfo = sha && _labCivitaiCache[sha];
|
||||
return civInfo?.trainedWords || [];
|
||||
}
|
||||
|
||||
// Builds the exact "<lora:stem:m:c>[, trigger, words...]" string appended to the
|
||||
// Positive prompt for this LoRA keyframe — the same format comfyui-editor.js's Single
|
||||
// Apply writes. Returned as-is so _buildWorkflowForIteration can hand it back unchanged
|
||||
// to the next iteration for removal (see _stripLoraPromptInjection).
|
||||
function _buildLoraPromptInjection(lora) {
|
||||
const stem = lora.name.replace(/\\/g, "/").split("/").pop().replace(/\.[^.]+$/, "");
|
||||
const strModel = lora.strengthModel ?? 1.0;
|
||||
const strClip = lora.strengthClip ?? 1.0;
|
||||
const syntax = `<lora:${stem}:${strModel}:${strClip}>`;
|
||||
const triggers = _labTriggerWordsFor(lora.name);
|
||||
return triggers.length > 0 ? `${syntax}, ${triggers.join(", ")}` : syntax;
|
||||
}
|
||||
|
||||
// Removes an exact previously-injected string (as returned by _buildLoraPromptInjection)
|
||||
// from a prompt, plus the comma that joined it to whatever came before — so switching
|
||||
// keyframes/LoRAs mid-batch never accumulates every LoRA the batch has used so far.
|
||||
// A plain string search (no regex) is safe here since `injection` is always something
|
||||
// _buildLoraPromptInjection generated, never arbitrary user text.
|
||||
function _stripLoraPromptInjection(text, injection) {
|
||||
if (!injection || !text) return text || "";
|
||||
let out = text.split(`, ${injection}`).join("");
|
||||
out = out.split(injection).join("");
|
||||
return out.replace(/,\s*,/g, ",").replace(/^\s*,\s*/, "").replace(/\s*,\s*$/, "").trim();
|
||||
}
|
||||
|
||||
// Applies a Lab-configured LoRA (Single only — no Stack support here) to the given
|
||||
// per-iteration workflow clone. Mirrors models-tab.js's applyToGenUI LoRA branch: prefer
|
||||
// analysis.lora_nodes[0] (correctly picks up "Lora Loader (LoraManager)" nodes via
|
||||
@@ -1060,14 +1169,20 @@ function _applyLabLoraToWorkflow(wf, analysis, lora) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strModel = lora.strengthModel ?? 1.0;
|
||||
const strClip = lora.strengthClip ?? 1.0;
|
||||
// nodeBypass: user-requested "bypass the LoRA node" toggle (Lab keyframe-level,
|
||||
// independent of the row's own bypassed flag above). There's no real node to set
|
||||
// mode:4 on here — currentWorkflow is already-converted API format (see
|
||||
// comfyui-workflow.js convertUiToApi) where mode has no effect — so this forces
|
||||
// strength to 0 instead, which is behaviorally equivalent to bypassing the node.
|
||||
const bypass = !!lora.nodeBypass;
|
||||
const strModel = bypass ? 0 : (lora.strengthModel ?? 1.0);
|
||||
const strClip = bypass ? 0 : (lora.strengthClip ?? 1.0);
|
||||
if (isLoraManager) {
|
||||
const stem = lora.name.replace(/\\/g, "/").split("/").pop().replace(/\.[^.]+$/, "");
|
||||
wf[nodeId].inputs.loras = {
|
||||
__value__: [{ name: stem, strength: strModel, active: true, expanded: false, clipStrength: strClip, locked: false }],
|
||||
__value__: [{ name: stem, strength: strModel, active: !bypass, expanded: false, clipStrength: strClip, locked: false }],
|
||||
};
|
||||
wf[nodeId].inputs.text = `<lora:${stem}:${strModel}:${strClip}>`;
|
||||
wf[nodeId].inputs.text = bypass ? "" : `<lora:${stem}:${strModel}:${strClip}>`;
|
||||
} else {
|
||||
wf[nodeId].inputs.lora_name = comfyEditor.resolveLoraName(lora.name);
|
||||
wf[nodeId].inputs.strength_model = strModel;
|
||||
@@ -1120,12 +1235,14 @@ async function _runLabBatch() {
|
||||
_lab.results.images = [];
|
||||
_renderResultsGrid();
|
||||
_setRunUiState(true);
|
||||
await _fetchLabLoraMetadataCache();
|
||||
|
||||
const progressBar = document.getElementById("wfm-lab-progress-bar");
|
||||
const progressText = document.getElementById("wfm-lab-progress-text");
|
||||
const eagleAutoSave = getEagleSettings().autoSave;
|
||||
|
||||
let completed = 0, failed = 0;
|
||||
let prevLoraInjection = null;
|
||||
try {
|
||||
for (let i = 1; i <= _lab.batchCount; i++) {
|
||||
if (_aborted) break;
|
||||
@@ -1134,7 +1251,8 @@ async function _runLabBatch() {
|
||||
|
||||
if (progressText) progressText.textContent = `[${i}/${_lab.batchCount}] ...`;
|
||||
const chainRef = (!_lab.t2iMode && _lab.chainImage && i > 1) ? _lastGeneratedImageRef : null;
|
||||
const { workflow, seed } = _buildWorkflowForIteration(i, chainRef);
|
||||
const { workflow, seed, loraInjection } = _buildWorkflowForIteration(i, chainRef, prevLoraInjection);
|
||||
prevLoraInjection = loraInjection;
|
||||
const workflowExpanded = await _expandWildcardsInWorkflow(workflow);
|
||||
|
||||
try {
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
<div class="wfm-gen-lora-col">
|
||||
<div class="wfm-gen-lora-col-header">
|
||||
<span id="wfm-gen-lora-col-title">LoRA</span>
|
||||
<button class="wfm-btn wfm-btn-xs wfm-lora-bypass-btn" id="wfm-lora-bypass-toggle" title="Bypass LoRA node (force strength to 0)">⛔</button>
|
||||
<button class="wfm-btn wfm-btn-xs wfm-lora-refresh-btn" id="wfm-lora-stack-refresh" title="Refresh Stack group">🔄</button>
|
||||
</div>
|
||||
<div id="wfm-gen-lora-fields" class="wfm-gen-lora-col-body">
|
||||
@@ -1368,6 +1369,7 @@
|
||||
<li id="wfm-help-gen-22">Stack tab — Use Stack Group checkbox (shown only when a Lora Manager node exists in the workflow): <b>OFF</b> (the default whenever a workflow is loaded) shows and edits the LoRAs actually configured in the workflow's Lora Manager node itself; <b>ON</b> shows and edits the "Stack" group registered in the Models tab instead; switching either direction clears the node's LoRA list first, then loads it from the newly selected source; switching back to OFF restores exactly the LoRA list that was present when the workflow was loaded (captured as a snapshot at load time)</li>
|
||||
<li id="wfm-help-gen-23">Stack tab controls: each row has an active checkbox and Str M / Str C fields; the header "Stack" checkbox toggles all rows at once; the +/- step buttons adjust the strength of all currently active rows together; a "Showing: LoRAs currently in workflow" / "Showing: registered Stack group" label indicates which source is currently displayed; Apply writes the full list back to the target node and also appends LoRA syntax + CivitAI trigger words to the Positive prompt</li>
|
||||
<li id="wfm-help-gen-24">Caution for <b>CLIP Text Encode edit+</b> (CLIPTextEncodeEditPlus) nodes: when a Lora Manager node's trigger_words output feeds this node's text1 input (RAW/EDIT/front/back modes combine text1 with the editable text_edit field), trigger words are already included automatically at generation time; clicking a LoRA Apply button or turning on Use Stack Group additionally writes LoRA syntax/trigger words into text_edit, duplicating them — leave Use Stack Group off and avoid the LoRA Apply buttons if you want the workflow's original prompt and LoRA composition to stay unmodified</li>
|
||||
<li id="wfm-help-gen-29">Bypass button (⛔, in the LoRA column header, next to Refresh Stack group): forces the target LoRA node's strength to 0 — strength_model/strength_clip for a standard LoraLoader, or every entry's strength/clipStrength for a Lora Manager node (its LoRA list is kept intact, not replaced) — without touching the actual ComfyUI node's mode; click again to restore the exact values that were there before toggling; targets whichever node Single/Stack Apply default to (the workflow's Lora Manager node if it has one, else the first LoRA node); clicking a Single or Stack Apply button clears the bypass state so it can't silently reassert itself on a later toggle</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1432,8 +1434,10 @@
|
||||
<li id="wfm-help-lab-11">Prompt cell editor also has <b>Clear</b> (empties both Positive and Negative) and <b>Get from Previous</b> (copies both Positive and Negative from the keyframe immediately before this one in the same column — e.g. editing #3 copies #2's prompt wholesale; not shown on keyframe #1, which has no earlier row to copy from)</li>
|
||||
<li id="wfm-help-lab-12">Keyframe #1 in every column shows the currently loaded workflow's own live setting instead of a blank placeholder — for the Model column this covers Checkpoint and VAE only (LoRA is never live-reflected, since there's no reliable way to infer "the" active LoRA back out of a workflow node) — both in its grid cell (dashed border) and pre-filled into its edit modal, for as long as it hasn't been explicitly saved with an override; saving it (even without changing anything) locks that value in as a real override and the dashed/live styling goes away; switching to the Lab subtab always refreshes this against whatever is currently loaded in GenerateUI</li>
|
||||
<li id="wfm-help-lab-13"><b>Bypass</b> checkbox (every keyframe, including #1): skips that row entirely when picking which setting applies at a given iteration — as if the row didn't exist — so whatever was active before it (an earlier keyframe, or ultimately nothing) just keeps running; Lab-internal only, it never touches the actual ComfyUI node's mode; disables that row's value fields and its Revert checkbox while checked (the two are mutually exclusive); the cell shows an amber "⏭ Bypassed" label, which takes priority over both the Revert and live-value display</li>
|
||||
<li id="wfm-help-lab-14"><b>Model</b> column header's <b>C / L / V</b> buttons pick which single field (Checkpoint / LoRA / VAE — LoRA is Single-only, no Stack support here) its cells summarize and its edit modal shows; the two you're not currently viewing are left exactly as already saved when you Save. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it</li>
|
||||
<li id="wfm-help-lab-15">Model column fields carry forward independently: a keyframe that only sets a LoRA doesn't reset Checkpoint/VAE to the workflow default — each of the three keeps whatever an earlier keyframe last set until a later keyframe explicitly changes it. A field left blank in the edit modal shows "(inherits: ...)" as its placeholder instead of "(workflow default)" when there's something to inherit, so it's clear what actually applies. "Revert to #1's setting" resets all three fields at once back to keyframe #1's row, not just the one you're currently viewing</li>
|
||||
<li id="wfm-help-lab-14"><b>Model</b> column header's <b>C / L / V</b> buttons pick which independent keyframe list — Checkpoint, LoRA (Single-only, no Stack support here), or VAE — the shared cell list, its +/-, and its edit modal all currently operate on. Each of the three is its own timeline with its own keyframes, exactly like Prompt/KSampler; they only ever look merged because they share one column's worth of screen space. Adding, removing, or editing a keyframe while viewing one never touches the other two. Each button turns the same highlight color as GenerateUI's Model tab (configurable in Settings) when the loaded workflow actually contains that node type. If a LoRA is set but the workflow has no LoRA node at all, Lab warns once per Run and skips applying it — unlike VAE, it never auto-inserts a LoRA node, since that would require rewiring the Checkpoint's model/clip outputs through it</li>
|
||||
<li id="wfm-help-lab-15">Checkpoint / LoRA / VAE each carry forward independently, the same effective-value rule as every other column (see above): a keyframe in one never resets the other two to the workflow default. A field left blank in the edit modal shows "(inherits: ...)" as its placeholder instead of "(workflow default)" when an earlier keyframe in that same list already set something, so it's clear what actually applies. "Revert to #1's setting" and Bypass — like +/- — only ever affect whichever ONE of the three is currently selected by the C/L/V toggle, never the other two</li>
|
||||
<li id="wfm-help-lab-16">Bypass LoRA node checkbox (LoRA keyframe's own edit modal only): forces that keyframe's strength_model/strength_clip to 0 at apply-time, standing in for ComfyUI's own node Bypass (mode:4) — the workflow used here is already converted to API format, which has no mode concept, so zeroing strength is the behavioral equivalent. Disables the strength fields while checked; independent of the row-level Bypass checkbox above, which skips the whole row instead of touching the node</li>
|
||||
<li id="wfm-help-lab-17">Apply LoRA syntax + trigger words to Positive prompt checkbox (LoRA column only): when checked, appends <lora:name:strengthModel:strengthClip> plus that LoRA's cached CivitAI trigger words to the Positive prompt, comma-separated, at apply-time — the same format GenerateUI's own Model tab Apply button writes. Switching keyframes (a different LoRA, this checkbox turned off, or the row Bypassed) always strips exactly what the previous iteration injected before adding anything new, so a batch never accumulates every LoRA it has used. Disabled automatically while Bypass LoRA node is checked</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1782,7 +1786,7 @@
|
||||
</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>
|
||||
<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; generate_image also accepts optional generation parameters the model can fill in from the request — steps, cfg, sampler_name, scheduler, denoise, seed, width, height, batch_size (number of images produced in that one call), and aspect_ratio (a ratio preset like "3:2", applied only if the workflow has a Resolution Selector node feeding width/height — ignored otherwise); anything the model doesn't supply keeps whatever the loaded workflow already has set; 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>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Regenerate README.index.md / DEVLOG.index.md from README.md / DEVLOG.md.
|
||||
|
||||
README.md and DEVLOG.md are excluded from graphify's semantic extraction
|
||||
(.graphifyignore) because they are too large for reliable local-Ollama
|
||||
extraction: big chunks make the model summarize in prose instead of emitting
|
||||
JSON, and time out on slower models/hardware. These condensed index files are
|
||||
extracted instead. Run this whenever README.md or DEVLOG.md changes, then
|
||||
`graphify --update` to refresh the graph.
|
||||
|
||||
Usage:
|
||||
python tools/generate_doc_index.py [readme|devlog|all]
|
||||
(defaults to "all")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def generate_readme_index() -> Path:
|
||||
text = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
details_re = re.compile(
|
||||
r"<details>\s*<summary><h3>(.*?)</h3></summary>\s*(.*?)</details>",
|
||||
re.S,
|
||||
)
|
||||
bullet_re = re.compile(r"^- \*\*(.+?)\*\*", re.M)
|
||||
|
||||
feature_lines: list[str] = []
|
||||
for title, body in details_re.findall(text):
|
||||
names = bullet_re.findall(body)
|
||||
feature_lines.append(f"### {title.strip()}")
|
||||
if names:
|
||||
feature_lines.append(", ".join(names))
|
||||
feature_lines.append("")
|
||||
|
||||
tail_start = text.find("## Installation")
|
||||
tail = text[tail_start:] if tail_start != -1 else ""
|
||||
|
||||
intro_end = text.find("## Screenshots")
|
||||
intro = text[:intro_end].strip() if intro_end != -1 else ""
|
||||
|
||||
out = [
|
||||
"# README Index — ComfyUI-Workflow-Studio",
|
||||
"",
|
||||
"Condensed index of README.md: intro summary, feature names per tab (full "
|
||||
"descriptions live in the excluded README.md), and the installation/usage/"
|
||||
"requirements/project-structure sections verbatim. Exists so graphify can "
|
||||
"extract this without feeding the full README (with its screenshot table) to "
|
||||
"the local Ollama model. Regenerate with "
|
||||
"`python tools/generate_doc_index.py readme` whenever README.md changes.",
|
||||
"",
|
||||
intro,
|
||||
"",
|
||||
"## Features (tab -> feature names)",
|
||||
"",
|
||||
*feature_lines,
|
||||
tail,
|
||||
]
|
||||
|
||||
content = "\n".join(out)
|
||||
out_path = ROOT / "README.index.md"
|
||||
out_path.write_text(content, encoding="utf-8")
|
||||
return out_path
|
||||
|
||||
|
||||
def generate_devlog_index() -> Path:
|
||||
text = (ROOT / "DEVLOG.md").read_text(encoding="utf-8")
|
||||
|
||||
entries = re.split(r"^## ", text, flags=re.M)[1:]
|
||||
lines: list[str] = []
|
||||
for entry in entries:
|
||||
parts = entry.split("\n", 1)
|
||||
version_line = parts[0].strip()
|
||||
rest = parts[1] if len(parts) > 1 else ""
|
||||
m = re.search(r"^### (.+)$", rest, re.M)
|
||||
subtitle = m.group(1).strip() if m else ""
|
||||
lines.append(
|
||||
f"- **{version_line}** — {subtitle}" if subtitle else f"- **{version_line}**"
|
||||
)
|
||||
|
||||
out = [
|
||||
"# DEVLOG Index — ComfyUI-Workflow-Studio",
|
||||
"",
|
||||
"Condensed version-by-version index of DEVLOG.md (each line: version + "
|
||||
'one-line summary of what changed and why). Full rationale, code details, '
|
||||
'and "How to apply" lessons live in the excluded DEVLOG.md itself; this '
|
||||
"index exists so graphify can extract a queryable semantic node per release "
|
||||
"without feeding the full log to the local Ollama model. Regenerate with "
|
||||
"`python tools/generate_doc_index.py devlog` whenever DEVLOG.md changes.",
|
||||
"",
|
||||
*lines,
|
||||
"",
|
||||
]
|
||||
|
||||
content = "\n".join(out)
|
||||
out_path = ROOT / "DEVLOG.index.md"
|
||||
out_path.write_text(content, encoding="utf-8")
|
||||
return out_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "all"
|
||||
written: list[Path] = []
|
||||
if target in ("readme", "all"):
|
||||
written.append(generate_readme_index())
|
||||
if target in ("devlog", "all"):
|
||||
written.append(generate_devlog_index())
|
||||
if not written:
|
||||
print(f"unknown target {target!r}; use readme|devlog|all", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
for p in written:
|
||||
print(f"wrote {p.relative_to(ROOT)} ({p.stat().st_size:,} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user