Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd4b721d51 | ||
|
|
46c789c492 | ||
|
|
ede39ec715 | ||
|
|
ad3b380929 | ||
|
|
c589e562e4 | ||
|
|
cb61dc61a7 | ||
|
|
ad9d06d3e1 | ||
|
|
ea408690ff | ||
|
|
cbb096ec85 | ||
|
|
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
|
||||
|
||||
@@ -200,6 +200,8 @@ os.environ["HF_HOME"] = "D:\\hf_cache"
|
||||
|
||||
`pyproject.toml` のバージョン変更が GitHub Actions のトリガー(`paths: pyproject.toml`)になっており、ComfyUI Registry への自動公開もここで行われる。バージョン更新を忘れると Registry に反映されない。
|
||||
|
||||
<!-- graphify運用は一時停止中(2026-08-23〜、ユーザー指示により凍結)。再開時はこのコメントブロックを解除すること。graphify-out/ 自体は削除しておらずそのまま残っている。
|
||||
|
||||
## graphify
|
||||
|
||||
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
|
||||
@@ -209,3 +211,17 @@ 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 .`を実行してグラフに反映する
|
||||
|
||||
-->
|
||||
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# 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.5.2** — 機能追加: GenerateUI/Labタブの動画ワークフロー対応 + VideoタブにFrame抽出/GIF変換機能 + データ管理監査とZIPフルバックアップ + Send to LI node統一
|
||||
- **v0.5.1** — 機能追加: mp4動画の埋め込みメタデータ対応(Gallery Prompt/JSON・Metadataサブタブ・サイドパネルIタブ)+ Gallery Infoタブに寸法/再生時間表示 + MiniMax H3等オールインワン動画ノードのプロンプト抽出バグ修正
|
||||
- **v0.5.0** — 機能追加: mp4動画Gallery対応 + Videoタブ新設(MiniMax H3専用) + バグ修正: Galleryのタグ/メモが再起動後に消える
|
||||
- **v0.4.8** — 機能追加: LabタブにPromptグループ一括適用+キーフレーム並べ替え + バグ修正: デフォルトワークフローの上書き保存が反映されない + Styleテーブルの「このファイルへ追加」対応
|
||||
- **v0.4.7** — 機能追加: Prompt Tab内部リファクタ + Gallery Promptタブ新設 + Send to Canvas改善 + Presetsソート
|
||||
- **v0.4.6** — 機能追加: MetadataタブをGalleryタブへ統合 + Promptタブに一覧編集可能なTable表示を新設
|
||||
- **v0.4.5** — 調査: トップバー保存ワークフローのRMBG不具合報告(原因なし) + バグ修正: LabタブModel列の無意味な空キーフレーム表示を解消
|
||||
- **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 統一** — 概要
|
||||
@@ -1,9 +1,263 @@
|
||||
異性
|
||||
|
||||
# DEVLOG - ComfyUI-Workflow-Studio
|
||||
|
||||
---
|
||||
|
||||
## v0.5.2
|
||||
|
||||
### 機能追加: GenerateUI/Labタブの動画ワークフロー対応 + VideoタブにFrame抽出/GIF変換機能 + データ管理監査とZIPフルバックアップ + Send to LI node統一
|
||||
|
||||
v0.5.0/v0.5.1で専用Videoタブを整備したMiniMax H3動画生成ワークフローを、GenerateUI/Labタブ本体でも扱えるようにする要望を起点に着手。1セッションで複数の要望・バグ報告・監査依頼を順に実装した一連の作業。
|
||||
|
||||
**1. GenerateUI/Labタブの動画ワークフロー対応**: 「MiniMax H3の動画生成ワークフローを生成UIタブ、Labタブでも生成できるようにしたい」との要望。実際には`comfyWorkflow.convertUiToApi()`+`comfyUI.generate()`が既に汎用的で生成自体は動いていたが、`analyzeWorkflow()`が`MiniMaxH3ImageToVideo`のようなCLIPTextEncodeを介さないオールインワン動画ノードのpromptを検出できず、GenerateUIのInput>Promptタブが空になっていた。クラス名が`ImageToVideo`/`TextToVideo`を含み`inputs.prompt`が文字列のノードを汎用的に拾う分岐を追加(`metadata-tab.js`/`node_sets_menu.js`に既にあったプロンプト抽出と同じ判定パターン)。GenerateUIとLabタブは共に同じ`comfyUI.currentAnalysis`を参照する設計だったため、この一箇所の修正でPromptタブとLabのキーフレーム書き込みの両方が動画ワークフローに対応した。画像(first_frame)は既存のLoadImage検出がそのまま拾う。あわせて、動画生成は静止画より大幅に時間がかかるため`comfyWorkflow.isVideoWorkflow(analysis)`ヘルパーを新設し、動画ワークフロー検出時はGenerateUI/Labとも生成タイムアウトを30分に延長。GenerateUIの結果プレビューは`<img>`固定だったため動画結果が正しく表示できなかった問題も、`<video>`要素を追加し出力ファイルの拡張子で出し分けるよう修正した。
|
||||
|
||||
**2. バグ修正: Labタブのindex imageが真っ黒になる**: 実機確認で発覚。コンタクトシート生成(`_buildIndexImageDataUrl`)が各結果を`<img>`要素として読み込もうとしており、mp4は画像として読み込めず失敗、そのセルはcanvasの黒背景がそのまま残っていた。動画結果は既存のchain image機能で使っていた最終フレーム抽出ロジック(`extractLastFrameBlob`、`<video>`+`<canvas>`でクライアント側完結)を再利用してフレームを取り出してから描画するよう修正。
|
||||
|
||||
**3. バグ修正: LabタブKsampler列が動画ワークフローでも表示される**: 「Model同様に該当ノードがなければ非表示にしたい」との要望に対し当初`sampler_nodes.length > 0`で判定したが、実機確認で「まだ表示される」との報告。原因はMiniMax H3のサブグラフ内部に実際に`SamplerCustomAdvanced`が配線されており(内部プリセットの一部で、ユーザーが調整すべきものではない)、単純な有無判定では引っかからなかったこと。判定を`comfyWorkflow.isVideoWorkflow(analysis)`ベースに切り替え、動画ワークフロー全体ではKsampler列自体を常に非表示にするよう修正した。
|
||||
|
||||
**4. Videoタブ拡張: Frame抽出 / アニメーションGIF変換**: Eagleプラグイン(video-to-frame, video2gif)を参考に要望された2機能。Eagle版はNode.js+ffmpegバイナリ直接呼び出しの実装だったため、ComfyUI-Workflow-Studioの構成(Python/aiohttpバックエンド)に合わせて設計し直した。フレーム抽出は`<video>`+`<canvas>`でブラウザ完結(サーバー往復・追加依存なし)。GIF変換はユーザー確認の上サーバー側実装を選択:PyAV(デコード)+ Pillow(GIFエンコード)— どちらもComfyUI本体が既に要求している依存のため、ffmpegバイナリや新規ライブラリの追加インストールは不要。`py/services/video_service.py` / `py/routes/video_routes.py`を新規追加し、`folder_paths.get_save_image_path()`でComfyUI標準の自動採番保存パターンをそのまま踏襲。合成テスト動画(PyAVで生成)を使いvenv環境でデコード→GIFエンコードの実処理を検証済み。
|
||||
- あわせてVideoタブの中央パネルを再設計:「結果」見出し/「まだ結果がありません」表示を廃止し、生成結果とドロップされた任意のローカル動画を同じプレビュー枠に統合、横に**Video Source**ドロップ/クリック選択エリア(Clearボタン付き)を新設。右パネルの「Properties」プレースホルダーを**Frame**/**GIF**の2タブに置き換え。生成結果に限らず任意の動画に対してFrame/GIF機能が使える設計にした。
|
||||
|
||||
**5. データ管理の監査とZIPフルバックアップ機能の新規実装**: 「設定のデータ管理を現在のバージョンで対応できているか確認してほしい」との依頼を受け、既存の`_DATA_FILES`(単一JSONバンドル方式)と実際のデータディレクトリを突き合わせ監査。**`civitai_cache.json`(3.6MB、CivitAI連携キャッシュ)がエクスポート対象から漏れていた**ことを発見し即修正。また、ヘルプが謳う「export all plugin data」に反し、フォルダ形式のデータ(`ai_skills/`、`lab_plan/`、`style/`)とSQLite DB(`tagger.db`)が単一JSONバンドル方式では原理的に扱えず完全に対象外になっていた点をユーザーに報告、ZIP形式のフルバックアップ機能を新規実装する方針で合意。`DATA_DIR`配下を丸ごとZIP化する`GET /api/wfm/settings/export-full`/`POST /api/wfm/settings/import-full`を追加(一時ファイル系`gmic_temp`/`thumb_cache`と、他custom_node(comfyui-impact-pack)へのシンボリックリンクである`wildcard/`のみ除外、`settings.json`はAPIキーを除外してから含める)。復元側はZip Slip対策(`../`等でDATA_DIR外に書き込もうとするエントリは`relative_to()`チェックでスキップ)を実装し、悪意あるZIPでの検証済み。
|
||||
- 追加要望「デフォルトワークフローフォルダー(`user/default/workflows`)とwildcardフォルダー(`comfyui-impact-pack/wildcards`)もオプションでバックアップに含めたいが、インポートは除外し手動対応としたい」に対応:エクスポート画面にオプトインのチェックボックスを2つ追加、有効時はZIP内に`_external/`プレフィックスで格納し、復元処理は`_external/`配下のエントリを常にスキップする実装にした(他プラグイン/ComfyUI本体が管理する領域を自動上書きしないため)。実データ(ワークフロー535件・wildcard 34件)で検証済み。
|
||||
|
||||
**6. Send to LI node機能をGalleryタブにも追加 + ボタン名統一**: Image Editタブに既にあった「選択画像をComfyUIキャンバス上の選択中ノードのimageウィジェットへ送る」機能(`window.opener.wfmSendImageToSelectedNode`、`node_sets_menu.js`で定義されたクロスウィンドウ橋渡し)を、Galleryタブの選択画像にも使えるよう追加。Galleryの画像は任意のフォルダにあり得るため、既存の「Send GenUI Image」と異なりLoadImageノードの有無に依存せず、`/upload/image`でinputフォルダへアップロードしてからファイル名を書き込む方式にした。あわせてImage Edit側の既存ボタン名「Send to Workflow」を「Send to LI node」に統一(トースト文言・コメント・ヘルプも合わせて更新)。
|
||||
|
||||
**7. Galleryタブ ✕クリアボタンの位置移動**: 「表示形式切替ボタン左隣に移動したい」との要望どおり、`wfm-gallery-clear-filters-btn`をSort selectとView切替ボタンの間から、View切替ボタン(Thumbnail/Table)の左隣へ移動。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、Video Tab(Video Source/Frame/GIFの3項目)、Lab subtab(Ksampler非表示・chain image動画対応の項目文言修正)、Settings(Data Managementのフルバックアップ・オプション外部フォルダの2項目)、Gallery Tab(Send to LI nodeボタンの項目)のヘルプ本文を英語・日本語・中国語の3言語で追加・更新。README.mdはVideo Tab(Frame/GIFタブ、Video Sourceパネル)、GenerateUI Tab(動画ワークフロー対応)、Lab subtab(動画ワークフロー対応・chain image動画対応、バージョン見出し範囲更新)、Settings Tab(Data Management更新)、Gallery Tab(Send to LI node・✕クリアボタン位置)、Image Edit Tab(ボタン名統一)を更新。
|
||||
|
||||
**検証**: 全JS/Pythonファイルを`node --check`/`ast.parse()`で構文確認。GIF変換・フルバックアップZIP・Zip Slipガードはvenv Python環境で実データ・合成テストデータに対する実処理検証を実施(PyAVで生成した合成テスト動画のフレーム抽出→Pillow GIFエンコード、実際のデータディレクトリ(civitai_cache.json含む36ファイル・約2.6MB)のZIP化、悪意あるパストラバーサルZIPでのスキップ確認、外部フォルダオプション有効時のワークフロー535件・wildcard 34件の`_external/`格納とインポート時全skipの確認)。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更・新規ファイルを都度両フォルダへ同期し改行コード差異を除いた`diff`で完全一致を確認、Pythonファイル追加時は`__pycache__`を都度クリア。動画ワークフロー対応・Labバグ修正・Videoタブ新機能・Send to LI node機能はユーザー自身の実機確認で動作を確認済み。
|
||||
|
||||
**How to apply**: `analyzeWorkflow()`のような中央解析関数に新しいノードパターンの検出を1箇所追加するだけで、それを参照する複数タブ(GenerateUI/Lab)に同時に効果が及ぶ設計は、機能追加のコストを大きく下げる — 逆に言えば、新しいワークフロータイプに対応する際はまずこの中央解析関数を疑うべき。「該当ノードが無ければ列を非表示にする」系の判定は、`sampler_nodes.length`のような表面的なノード種別の有無だけでなく、そのノードが「ユーザーが調整すべき設定」を表しているか(動画生成モデル内部に固定配線されたサンプラーのように、単に存在するだけでは判定材料にならない場合がある)を意識する必要がある。「all plugin data」のような謳い文句を持つ機能は、実装が実際にその範囲をカバーしているか定期的に実データと突き合わせて監査する価値がある — 今回`civitai_cache.json`の漏れは3.6MBというサイズながら発見されていなかった。
|
||||
|
||||
関連: [[project_v050_video_tab]], [[project_v051_mp4_metadata]], [[project_dev_deploy_sync]], [[project_v0336_conventions]]
|
||||
|
||||
---
|
||||
|
||||
## v0.5.1
|
||||
|
||||
### 機能追加: mp4動画の埋め込みメタデータ対応(Gallery Prompt/JSON・Metadataサブタブ・サイドパネルIタブ)+ Gallery Infoタブに寸法/再生時間表示 + MiniMax H3等オールインワン動画ノードのプロンプト抽出バグ修正
|
||||
|
||||
v0.5.0で追加したmp4 Gallery対応(一覧・サムネイル・再生のみ)に続き、「ギャラリータブでmp4のメタデータのプロンプトを表示できるようにしたい。メタデータタブでもプロンプト、表示可能ならモデル等も表示させたい。またInfoタブで画像サイズ、再生時間も表示したい」との要望を起点に着手。Eagleでmp4のプロンプト/Steps/Sampler/Seed/Modelが見えている実例(MiniMax H3で生成した`.mp4`)を手がかりに、ComfyUIの`SaveVideo`がmp4にどう埋め込んでいるかを`ffprobe`/実PyAVで直接確認した上で実装した。
|
||||
|
||||
**1. 埋め込み形式の実地調査**: `ffprobe -show_format`で対象mp4を調べたところ、PNGのtEXtチャンクと全く同じキー名(`workflow`=UI形式寄りの内容、`prompt`=API形式)でJSON文字列がコンテナレベルのformatタグとして書き込まれていた。バイト単位で追うと`moov > udta > meta > (hdlr, keys, ilst)`という、iTunes系メタデータ(`mdta`名前空間の`keys`/`ilst`)の構造になっており、FFmpegの`movflags=+use_metadata_tags`相当の書き込み方だと判明。ComfyUI側は`comfy_api/latest/_input_impl/video_types.py`の`write_output_metadata()`がPyAV経由で`container.metadata[key] = json文字列`する実装。
|
||||
|
||||
**2. バックエンド(`py/services/gallery_service.py`)**: 既存の`serve_thumbnail()`が動画フレーム抽出に使っていた`av`(PyAV)依存を再利用し、`_read_mp4_metadata()`(`container.metadata`からタグ辞書を取得)と`_read_media_dimensions()`(画像はPillowの`Image.size`、動画はPyAVの`stream.width/height`+`container.duration`)を追加。`get_image_metadata()`・`_extract_embedded_workflow()`にmp4分岐を追加し、既存のPNG向けワークフロー抽出ロジック(`workflow`/`prompt`優先順位の切り替え)をそのままmp4にも適用した。venv Pythonで`GalleryService`を直接呼び出すE2E検証、および実mp4に対する`ffprobe`/PyAVの値の突き合わせ(幅高さ608×352, 再生時間3.042秒, workflow/promptタグ長一致)で正しさを確認済み。
|
||||
|
||||
**3. Galleryタブ Infoサブタブ**: 幅×高さ(画像)または再生時間(動画)を表示する行を追加(`gallery-tab.js` `renderDimensionInfo()`)。既存のPrompt/JSONタブは`extract_workflow_from_metadata`系のAPIをそのまま叩いているため、バックエンド対応だけでmp4でも自動的に動くようになった。
|
||||
|
||||
**4. Metadataサブタブ(Iタブ、`metadata-tab.js`)とサイドパネルIタブ(`web/comfyui/node_sets_menu.js`)**: どちらもドラッグ&ドロップされたFile自体(サーバー未経由のバイト列)を直接パースする設計のため、MP4のボックス構造(`moov→udta→meta→keys/ilst`)をブラウザ上で直接読む`readMP4MetaTags()`/`_readMP4MetaTags()`をゼロから実装(`findMP4Box`でサイズ/タイプ解決→ `keys`ボックスからキー名一覧 → `ilst`ボックスの`data`アトムから値を取り出し名前に対応付け)。Node.jsで実mp4のバイト列に対して単体テストし、`workflow`/`prompt`タグの内容がPyAV/`ffprobe`の結果と完全一致することを確認した。サイドパネルIタブは`metadata-tab.js`と同一ロジックを保持する独立した複製実装(ComfyUIネイティブ拡張のため別ファイル)のため、同じ変更を両方に適用。
|
||||
|
||||
**5. バグ発見・修正: オールインワン動画ノードのプロンプトが抽出されない**: 上記の実装後、実際のMiniMax H3出力mp4でPromptタブを確認したところ「プロンプトなし」と表示される問題を発見。原因は`MiniMaxH3ImageToVideo`のような、`CLIPTextEncode`系ノードを介さずノード自身の`prompt`入力に直接テキストを持つ「動画生成オールインワンノード」を、既存の`isTextEncoderNode()`判定(クラス名に`TextEncode`を含むかで判定)が拾えていなかったこと。`extractPromptsAPI()`(`metadata-tab.js`・`node_sets_menu.js`の両方)に、クラス名が`ImageToVideo`/`TextToVideo`を含み`inputs.prompt`が文字列のノードをtextMapへ直接投入する分岐を追加して解決(positive/negativeの区別が無いノードのため、フォールバックの`texts`バケットに載る)。
|
||||
|
||||
**6. ヘルプ更新**: Gallery Tab(Infoタブの寸法/再生時間表示、Formats行のMP4追加)、Metadata Tab(Supported Formatsカード)、Workflow Studio Library / Iタブ(サイドパネル)のヘルプ本文を英語・日本語・中国語の3言語(`i18n.js`)+ `templates/index.html`で更新。
|
||||
|
||||
**検証**: Python側は`GalleryService`をvenv Pythonで直接呼び出すE2Eテストと実mp4への`ffprobe`/PyAVでの突き合わせ、JS側のMP4パーサーとプロンプト抽出ロジックはNode.jsで実バイト列/実JSONに対する単体テストで検証。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイル(`gallery_service.py`, `gallery-tab.js`, `metadata-tab.js`, `node_sets_menu.js`, `i18n.js`, `templates/index.html`, `README.md`, `pyproject.toml`)を両フォルダへ同期し改行コード差異を除いた`diff`で完全一致を確認。ComfyUI実機でのブラウザ操作確認はユーザー側で実施済み(Gallery Prompt/JSON表示、サイドパネルIタブとも問題なしとの回答)。
|
||||
|
||||
関連: [[project_v050_video_tab]]
|
||||
|
||||
---
|
||||
|
||||
## v0.5.0
|
||||
|
||||
### 機能追加: mp4動画Gallery対応 + Videoタブ新設(MiniMax H3専用) + バグ修正: Galleryのタグ/メモが再起動後に消える
|
||||
|
||||
動画生成ワークフロー対応の第一弾。「1. 動画ファイルmp4対応 2. 動画生成に対応していきたい。まずはminimax_H3動画生成ワークフローに対応させたい」との要望を起点に、実機フィードバックを重ねながら仕上げた一連の作業。ブランチ`feature/mp4-video-gallery-and-video-tab`で実施。
|
||||
|
||||
**1. Galleryタブのmp4動画対応**: `.mp4`を`IMAGE_EXTENSIONS`に追加し、一覧・お気に入り・タグ・グループ・移動・削除・エクスポートなど既存の画像操作全てにmp4も対象として乗るようにした。サムネイルはPillowで開けないため、PyAV (`av`) で先頭フレームを抽出しJPEGとしてディスクキャッシュする専用ロジックを追加(ComfyUI本体が既に`av>=17.0.0`を要求しており追加インストール不要。未導入環境ではサムネイル生成のみ失敗しプレースホルダー表示にフォールバックする設計)。フロントエンドは詳細パネル・lightbox・Compareビューで拡張子に応じて`<img>`/`<video controls>`を出し分け、サムネイルグリッドには▶バッジを重ねて動画であることを示す。
|
||||
|
||||
**2. 新規Videoタブ(MiniMax H3専用、GenerateUIタブとは完全に独立)**: `C:\Users\...\ComfyUI_5\user\default\workflows\minimax_test.json`をリファレンスに、first_frame/last_frame/prompt/aspect_ratio/megapixels/multiple/duration/noise_seedのフォームを持つ新規タブ`video-tab.js`を作成。モデル選択(UNet/CLIP/VAE/Audio VAE)はワークフローの既定値に固定し編集不可とした(ユーザー確認済み)。`comfyWorkflow.convertUiToApi()`が既にサブグラフのflatten処理を内蔵していたため、Videoタブ独自のグラフ→プロンプト変換は不要で、生成そのものは既存の`comfyUI.generate()`をそのまま再利用できた(ComfyUI本体の`SaveVideo`/`PreviewVideo`が`{"images":[...], "animated":true}`という画像と同じレスポンス形式で動画を返すため)。
|
||||
|
||||
**3. 実機フィードバックへの追随(ボタン配置・色・レイアウト)**: 一連の実機確認を経て、(a) Galleryの詳細パネルのダウンロードオーバーレイが動画の再生コントロール操作をブロックするため廃止し、Taggerボタン右隣に常時表示の「Download」ボタン(識別しやすいようオレンジ色)を新設、「Load GenUI」ボタンは「GenUI」に短縮、(b) Workflowタブのモーダルにあった「生成UIに読み込み」「Videoに読み込み」「キャンバスへ送る」の3ボタンを廃止し、一覧選択時のサイドパネルツールバー(生成UIに読み込みの右隣に新規追加した「Videoに読み込み」)へ統合、(c) Videoタブの結果パネルの位置・サイズを繰り返し調整 — 最終的に**3ペイン構成**(左:フォーム/中央:プレビュー+将来の編集機能用プレースホルダー/右:将来のプロパティ機能用プレースホルダー)に落ち着いた。各ペインを独立したflexカラムにしたことで、中央の生成結果の高さがどれだけ変わっても左右のペインの位置が動かなくなる(`max-height`だけで高さを抑えていた前段の実装では、動画のコントロールバー分だけ実高さが伸び、下のフォームパネルを押し下げてしまっていた)。Kapture経由で実際のブラウザDOM(`getBoundingClientRect()`)とスクリーンショットを直接確認しながら調整した。
|
||||
|
||||
**4. First Frame/Last Frameを両方optionalに、LoadImageノードが無いワークフローにも対応**: 「First Frameなしでも動作させたい(ノードをバイパスで処理)。またノードのないワークフローにも対応させたい」との要望。`minimax_test1.json`(first_frame/last_frame双方とも未接続、対応するLoadImageノードがトップレベルに存在しない構成)を新たに確認し、`locateMiniMaxNodes()`のLoadImageノード必須チェックを撤廃。first_frame未指定時はリンクなしのまま実行(バイパス)し、画像が指定された場合のみその場でLoadImageノードを動的に注入する設計に変更(ノードID/リンクIDはclone内の実際の最大値+1で採番するため重複の心配がない)。first_frameスロットにもlast_frameと同様のClearボタンを追加した。
|
||||
|
||||
**5. バグ修正: mp4のタグ/メモがComfyUI再起動後に消える**: 「タグ、メモadd、保存操作はできるが記録されていない。画像ファイルもできていない。エラー表示はなし、ブラウザもなし」との報告を受け、`comfyui_8189.log`を直接確認したところ`Cleaned up 189 stale metadata entries from ...\Text2Img`という決定的なログを発見。**根本原因**: `cleanup_stale_images()`は「現在表示中のフォルダ配下」のメタデータキーを全階層(サブフォルダ含む)対象にstale判定するが、Galleryがルートフォルダを非再帰スキャンした場合、`existing_paths`(現在のファイル一覧)にはルート直下のファイルしか含まれない。Videoタブで生成した動画は`SaveVideo`の`filename_prefix`設定で`video/MiniMax_H3/`というサブフォルダに保存されるため、ルートフォルダを開くたびに「サブフォルダ内ファイルは非再帰スキャンには映らない→存在しないと誤判定→メタデータ削除」が発生し、189件ものタグ/メモ/お気に入り情報が消えていた。`cleanup_stale_images()`に`recursive`引数を追加し、非再帰スキャン時はfolder_path直下のみを対象にするよう修正(`gallery_service.py`の`list_images()`から`recursive`をそのまま伝播)。既に削除済みのデータはバックアップが無く復元不可能だった旨をユーザーに伝えた。
|
||||
- 副次的に発見したフロントエンドのバグも合わせて修正: `saveMetaField()`がサーバーのレスポンス(`{"ok": bool}`)を一切チェックせず常に成功したかのようにローカル状態を楽観的更新していたため、サーバー側で保存が拒否されてもUI上はエラーなく成功したように見えてしまう作りになっていた。レスポンスの`ok`フィールドを検証し失敗時は例外を投げるよう変更、`addTag`/`removeTag`/メモ保存ハンドラは戻り値を見て成功時のみローカル状態を更新するよう修正した。保険的措置として`save_image_meta`(バックエンド)の保存キーも`get_image_metadata`と同じ`resolve()`後のパスに統一した。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、新規Video Tabヘルプページを追加。README.mdはバージョンバッジ更新、Workflow Tab(サイドパネルツールバーの3ボタン構成)、Gallery Tab(Downloadボタンの新配置・mp4対応・GenUIボタンへの改名)、新規Video Tabセクション(3ペイン構成・optional First/Last Frame・ノードなしワークフロー対応)を追加した。
|
||||
|
||||
**検証**: 全JS/Pythonファイルを`node --check`/`ast.parse()`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更した全11ファイルを都度同期しmd5ハッシュ比較で完全一致を確認、Pythonファイル変更時は`__pycache__`を全クリア。Kapture(ブラウザDevTools連携)で実際のブラウザに接続し、DOM要素の`getComputedStyle()`/`getBoundingClientRect()`とスクリーンショットで3ペインレイアウトの実測検証、生成後状態はダミー画像をDOMに直接注入して左右ペインが動かないことを確認。mp4のGallery表示・再生、タグ/メモ保存とComfyUI再起動後の永続性、Workflowタブの新規ボタン導線はユーザー自身の実機で確認済み。
|
||||
|
||||
**How to apply**: サーバーAPIが常にHTTP 200を返し成否をレスポンスボディの`ok`フィールドで表す設計(このプロジェクト全体で頻出するパターン)では、呼び出し側で必ずそのフィールドをチェックする — でないと「操作はできるがエラーも出ず、実は保存されていない」というサイレント障害が発生し得る。複数のパネルを横並びに配置するレイアウトで「あるパネルの内容量に応じて他のパネルの位置がずれる」不具合が出た場合、`max-height`のような上限指定では実コンテンツが小さいときに縮んでしまい根本解決にならない — 各パネルを独立したflexカラムに分離し、パネル間で高さを共有させない構造にするのが確実。「フォルダ配下」を対象にした一括処理(今回のstale cleanup)を書く際は、その処理が使う「現在のファイル一覧」がフォルダの全階層をカバーしているか(非再帰スキャンならサブフォルダは含まれない)を必ず確認し、範囲の不一致がないか意識する。ブラウザで実際に何が起きているか切り分けが必要な場面では、コードを読むだけでなくKapture等でDOMの実測値を直接取得するのが最も確実。
|
||||
|
||||
関連: [[project_dev_deploy_sync]], [[project_v0336_conventions]]
|
||||
|
||||
---
|
||||
|
||||
## v0.4.8
|
||||
|
||||
### 機能追加: LabタブにPromptグループ一括適用+キーフレーム並べ替え + バグ修正: デフォルトワークフローの上書き保存が反映されない + Styleテーブルの「このファイルへ追加」対応
|
||||
|
||||
ユーザーからの複数の要望・不具合報告を1セッションで順に実装した一連の作業。
|
||||
|
||||
**1. LabタブPrompt列に「PG」ボタン — Promptタブのグループをキーフレームへ一括変換**: 「生成UIタブのLabのプロンプトにプロンプトタブのグループを使用できるようにしたい。'PG'ボタンを配置、モーダルでプロンプトのグループを選択、決定するとプロンプトのキーフレームが#1から自動で作成されます(#1は上書き)」との要望。`prompt-presets.js`の`pmGroups`(グループ名→プリセットID配列)を参照する`getPresetsInGroup()`を新設してexportし、`lab-tab.js`側にPresetManagerの一覧UI(`.wfm-pm-item`)を再利用したグループ選択モーダルを実装。選択→適用で`_lab.columns.prompt`をグループ内プリセット順に#1から全置換(#1は上書き、以降は#2,#3...)する。グループのプリセット数がBatch数を超える場合はBatch数を自動引き上げ(Batch入力欄自体の上限50でキャップ)、それでも収まらない分は既存の「Batchを超えたキーフレーム」警告トースト(`_warnIfKeyframesExceedBatch`)をそのまま再利用して通知する形にした。
|
||||
|
||||
**2. Lab: Model/Prompt/KSampler キーフレームの並べ替え**: 続けて「モデル、プロンプト、Ksamplerそれぞれでキーフレームの順変更を可能にしたい。キーフレームのモーダル展開をWクリックに変更しシングルクリックでキーフレームを選択、PGボタン左隣に上下ボタンを設置し操作可能にしたい」との要望。キーフレームセルのクリック挙動を分離し、シングルクリックは選択状態(列ごとに`_selectedIdx[col]`で管理、青枠でハイライト)、ダブルクリックで従来の編集モーダルを開くように変更。各列ヘッダーに▲/▼ボタンを追加し(Promptは要望通りPGボタンの左隣)、選択中のキーフレームと隣接するキーフレームの`atIteration`を入れ替えることで並び順を変更する実装にした。キーフレーム#1(ベースライン — GenerateUIのライブ値を表示する特別な行)は移動不可、かつ他のキーフレームが#1の位置に入れ替わることもないよう制約した。Prompt Group適用・Plan読込・Plan Clearなど列を丸ごと置き換える操作では選択状態を自動リセットするようにし、配列外を指す不整合を防いだ。
|
||||
|
||||
**3. バグ修正: デフォルトワークフローに設定した状態で同名上書き保存すると変更が反映されない**: 「生成UIタブで読み込んだデフォルトワークフローのモデルを変更しワークフローを同名で上書き保存した。ワークフロータブでJSONでモデルが変更していることを確認。ブラウザハード再読み込みを行うと以前のモデルが表示される」との不具合報告(スクリーンショット添付、旧モデル名がインストール済みモデルの一覧に無く「見つかりません」警告が出ている状態)。調査の結果、「デフォルトワークフローに設定」機能(`workflow-tab.js`)は設定した時点のワークフローJSON全体を`localStorage`(`wfm_settings.defaultWorkflowData`)へスナップショットとして保存し、起動時の自動読込(`generate-tab.js`)はこのスナップショットを読む仕様であることが判明。GenerateUIタブでの同名上書き保存はファイル自体は更新するが、このスナップショットまでは更新していなかったため、リロードのたびに保存前の古い内容が復活していた。`saveCurrentWorkflow()`の保存成功後処理に、保存したファイル名が現在の`defaultWorkflow`と一致する場合は`defaultWorkflowData`も同時に更新する処理を追加して修正した。
|
||||
|
||||
**4. Prompt Tab Tableビュー Styleタブに「+ Add to this file」対応**: 「プロンプトタブ、テーブルのスタイルにForm同様に既存のjsonファイルに追加する方法が欲しい(custom.json以外の場合)。該当jsonのスタイルを選択状態で+ Add to this fileボタンで追加できるようにしたい」との要望。Formタブのスタイルマネージャー(`prompt-styles.js`)には既にこの機能があったが、Tableビュー(`prompt-table.js`)の新規スタイル作成は常に`custom.json`固定(`styleApiCreate(..., null)`)だった。行選択が1件かつそのスタイルの`file`が`custom.json`以外の場合のみ有効化される「+ Add to this file」ボタンをツールバーに追加し、クリックすると保存先ファイルを保持したまま空の新規行を追加(File列に保存前からバッジ表示)、保存時に`styleApiCreate(name, pos, neg, pendingNewStyleFile)`で対象ファイルへ追記する実装にした。バックエンド(`settings_routes.py`)は元々`file`パラメータでの追記に対応済みだったため、フロントエンドの導線を追加するだけで実現できた。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、Lab subtab(Setting/Columnsカードへ選択・並べ替えとPGボタンの説明2項目)とPrompt Tab(Table Viewカードへ「+ Add to this file」の説明1項目)を追加。README.mdはGenerateUI Tab(Saveボタン説明にデフォルトワークフロースナップショット修正の注記)、Lab subtab(見出しのバージョン範囲をv0.4.8へ更新、選択/並べ替えとPGボタンの新規項目)、Prompt Tab(Styleテーブルの項目に「+ Add to this file」のTableビュー対応を追記)を更新した。
|
||||
|
||||
**検証**: 全JSファイルを`node --check`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルごとに改行コード(CRLF/LF)の一致を確認してから同期し、都度`diff`で完全一致を検証した。デフォルトワークフローのバグ修正はユーザー自身の実機(GenerateUIでの上書き保存→ハード再読み込み)で確認済み。PGボタン・キーフレーム並べ替え・Styleの「+ Add to this file」もユーザー自身の実機確認で動作を確認済み。
|
||||
|
||||
**How to apply**: 開発元リポジトリと実行時`custom_nodes`フォルダへの同期前には必ず`file`コマンドで両ファイルの改行コードが一致しているか確認する(過去に一部ファイルだけLF化していた事例があるため — [[project_dev_deploy_sync]]参照)。一致していれば単純な`cp`で安全にデプロイでき、`diff`で完全一致を最終確認する。キーフレームや行の「並べ替え」機能を実装する際、内部的にソート順を保持する配列(今回は`atIteration`昇順)に対しては、配列そのものの並べ替えではなく「隣接要素同士でソートキーの値を交換する」実装にすると、他のロジック(`_resolveKeyframe`等)が前提とする「配列は常にソート済み」という不変条件を崩さずに済む。
|
||||
|
||||
関連: [[project_dev_deploy_sync]], [[project_v0336_conventions]]
|
||||
|
||||
---
|
||||
|
||||
## v0.4.7
|
||||
|
||||
### 機能追加: Prompt Tab内部リファクタ + Gallery Promptタブ新設 + Send to Canvas改善 + Presetsソート
|
||||
|
||||
ユーザーからの複数の要望を1セッションで順に実装した一連の作業。
|
||||
|
||||
**1. graphifyでの掘り下げを起点にprompt-tab.jsを分割**: graphifyの知識グラフでコードベースを掘り下げた結果、`prompt-tab.js`(1545行)がPreset管理・AI Chat・Wildcard管理・Style管理の4つの独立したクラスタで構成されていることが判明。`prompt-presets.js`/`prompt-ai-chat.js`/`prompt-wildcards.js`/`prompt-styles.js`の4ファイルに分割し、`prompt-tab.js`自体は各モジュールを結線するだけの薄いオーケストレーターにした。`prompt-table.js`/`app.js`は無変更(`prompt-tab.js`からのre-export経由で既存のimportをそのまま維持)。
|
||||
|
||||
**2. テーブルタブのWildcard入力支援ツールバー、実は配置ミス**: 「テーブルタブのワイルドカードのファイルがtxtの場合、モーダルにフォームの入力支援ボタンを表示したい」との要望を受け実装したが、直後にユーザーから「ワイルドカード構文はワイルドカードのモーダルに追加するのは誤りでプロンプトに追加するのが正しい気がします」との指摘。Form側の既存設計(構文ボタンはWildcard Composerにのみ存在し、ファイル中身のエディタには無い — ファイルの中身は単なる候補リストという前提)に照らして誤りだったと確認し、Presetsのプロンプト編集モーダルへ移設した。ワイルドカードツールバーは`prompt-wildcards.js`の`createWildcardToolbar()`として再利用可能な形に切り出し、Form側の静的ツールバーとテーブル側の動的モーダルの両方から使えるようにした。Styleモーダルには適用しない(Form側でもWildcard ComposerとStyleの連携導線は存在しないため)。
|
||||
|
||||
**3. Style×Wildcard展開順序のバグ修正**: 「スタイルにワイルドカード構文を使用しても使用時プロンプトに挿入されるので動作しますか?」との確認依頼から調査を開始。`generate-tab.js`の`_coreGenerate`内でワイルドカード展開(`_expandWildcardsInWorkflow`)がStyle適用(`_applyStyleToWorkflow`)より*先*に実行されていたため、Style文字列内に書いた`__file__`参照が展開されないまま生テキストのままComfyUIへ送信される不具合を発見。実行順序をStyle適用→ワイルドカード展開に入れ替えて修正。実際にComfyUIで生成を実行し、`comfyUI.generate`を一時的にモンキーパッチして送信直前のペイロードを確認、Style由来の`__flower__`が`"flower, gardenia scent"`のように正しく展開されることを実証した。
|
||||
|
||||
**4. yamlワイルドカードを使うサンプルワークフロー**: 「yamlファイルのワイルドカードを使用するワークフローを作成してもらうことはできますか」との依頼。調査の結果、このプラグイン自体のワイルドカード展開(SPA側の`_expandWildcardsInWorkflow`)は`.txt`拡張子固定でyaml非対応であり、yaml形式は`ImpactWildcardEncode`/`ImpactWildcardProcessor`ノード(Impact Pack、サーバー側処理)でのみ解決できることが判明。プラグインのwildcardディレクトリはjunction経由でImpact Packの`wildcards/`フォルダへ直結しているため、通常のWildcard File Manager経由で保存したyamlファイルがそのままImpactWildcardEncodeから参照可能。サンプルyamlワイルドカードファイル(`ws_sample_season_lighting.yaml`)とImpactWildcardEncodeノードを使うサンプルワークフロー(`ws_yaml_wildcard_sample.json`)を作成し、実際にComfyUIで生成→出力PNGのメタデータで`populated_text`が階層キー参照から正しく展開されていることを確認した。
|
||||
|
||||
**5. Galleryタブ詳細パネルに「Prompt」タブ新設**: 「メタデータでも確認できるが頻度を考えるとマウス操作が多いため、プロンプトだけはこちらでも表示としたい。ボタンは右端でデフォルトで選択状態としたい」との要望。当初は単純な全文連結表示で実装したが、「メタデータタブの表示の様に複数プロンプトがあるケースも考慮したい」とのフィードバックを受け、Metadataタブの`extractPrompts`/`buildPromptItem`をexportし、完全に同じPOS/NEGバッジ付きリスト→クリックで全文表示のUIに作り直した。
|
||||
- さらに実機確認の中で、同一画像でもMetadataタブとGalleryタブでpositive数が食い違うケース(`Krea2_turbo_00133_.png`、Metadataでは2件のPositiveが見える)を発見。原因は埋め込みデータの優先順位の違い — Metadataタブは`prompt`(API形式、サブグラフ展開済みでフラット)を優先するのに対し、Gallery側サーバーAPIは`workflow`(UI形式、ノード位置等を保持)を優先していた。トップレベルとサブグラフに独立した2系統のサンプリングを持つワークフローでは、UI形式のトップレベルnodes配列に一方の系統しか含まれず、`extractPromptsLiteGraph`の早期returnロジックがその系統しか拾えないため。既存のLoad GenUI/JSON表示/Copy & Send Canvas等には一切影響しないよう、`gallery_service.py`に優先順位を選べる`_extract_embedded_workflow`ヘルパーを追加し、新規`extract_prompt_workflow_from_metadata`(API形式優先)を`/wfm/gallery/image/workflow`のレスポンスへ`prompt_workflow`として追加、Galleryの Promptタブの抽出だけこちらを使うようにした。Python側の変更を反映するため、ユーザー自身の手でComfyUIを再起動していただいた。
|
||||
|
||||
**6. Send to Canvasのフォールバック方式を刷新**: 「このワークフローを生成UIタブに読み込むと...またキャンバスへ送るでAPI形式のためタイトルをキャンバスへドラッグのメッセージが表示されますがこの動作を変更したい...UI形式のサムネイルがないパターンのワークフローも同様にしたい」との要望。`window.opener`経由の直接ロードに失敗した場合の従来のフォールバック(`localStorage`+パネルタイトルのドラッグ、API形式のワークフローはそもそも非対応でエラーのみ)を、Workflow Studio LibraryのW(Workflows)タブを該当ワークフローで絞り込み表示する方式に統一した。`node_sets_menu.js`に`showWorkflowInLibrary()`をexportし、`top_menu_extension.js`が新規localStorageキー`wfm_library_filter_request`への`storage`イベントを監視して呼び出す(同一オリジンの別タブでのみ発火する仕様を利用)。フォーマット(UI形式・API形式)を問わず同じ導線になり、サムネイル付きの実物のワークフローカードからユーザー自身がドラッグできるようになった。Galleryタブの「Copy & Send Canvas」(画像埋め込みのその場限りのワークフローが対象で、そもそもLibrary一覧=保存済みファイルには存在しない)は今回のスコープ外として変更していない。
|
||||
|
||||
**7. Presetsテーブルに★/Bソート機能**: 「プロンプトタブのテーブル(プリセット)で上部メニューに★とBボタンによるソート機能を追加したい」との要望。ツールバーに★(favorite)/B(Batch選択済み)のトグルボタンを追加し、該当する行を先頭にソートする機能を実装。両方同時にONにでき、★を主キー・Bを副キーとした複合ソートに対応、該当しない項目同士は安定ソートで元の相対順序を維持する。ソート有効時に行内の★/Bボタンで状態を変更すると即座に再ソートされる。
|
||||
|
||||
**8. graphifyの運用を一時停止**: ユーザー指示によりCLAUDE.md内の「graphify」セクション全体をコメントアウトし、明示的な指示があるまでコード変更後の`graphify update`は実行しないことにした(`graphify-out/`自体は削除せず保持、再開時にコメントを解除するだけで元に戻せる)。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、Prompt Tab(Table Viewカードに★/Bソートボタンの新規項目)を更新。README.mdはGenerateUI Tab(Style×Wildcard展開順序修正)、Prompt Tab(Presetsモーダルのワイルドカードツールバー、★/Bソート)、Gallery Tab(新規Promptタブ)、Workflow Studio Library(Send to CanvasのWタブフィルタ方式)を更新。
|
||||
|
||||
**検証**: 全JS/Pythonファイルを`node --check`/`ast.parse()`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルを都度両フォルダへ同期。Playwright MCP経由で実機ブラウザ操作を機能追加・修正ごとに実施: (1) prompt-tab.js分割後のPreset/AI Chat/Wildcard/Styleパネル全機能の回帰確認、(2) Wildcardツールバー移設前後でのモーダル表示・非表示の確認、(3) Style×Wildcard展開の実生成確認、(4) yamlサンプルワークフローの実生成とメタデータ確認、(5) Gallery Promptタブの複数プロンプト表示とprompt_workflow切替の確認、(6) Send to Canvasの別タブ間storage連携の実機確認、(7) Presetsテーブルの★/Bソート・複合ソート・安定ソートの確認。テスト用に作成したデータ(一時Style、yamlワイルドカードファイル等)はテストワークフロー用途のものを除き削除・復元済み。
|
||||
|
||||
**How to apply**: 開発時は `graphify query "<question>"` を使わず通常の Grep/Read/Explore で調査する(graphify運用停止中、明示的な再開指示があるまで)。ワイルドカード構文(`{|}`, `__|__`, `<lora>`等)は「プロンプトを書く場所」に対する入力支援であり、「ファイルの中身を編集する場所」には付けない、という区別を今後の入力支援UI追加でも踏襲する。ワークフローの実行順序に依存するバグ(Style適用とワイルドカード展開の順序等)は、実際にComfyUIへ送信される直前のペイロードをモンキーパッチや`/history`のPNGメタデータで確認するのが最も確実な検証方法。
|
||||
|
||||
---
|
||||
|
||||
## v0.4.6
|
||||
|
||||
### 機能追加: MetadataタブをGalleryタブへ統合 + Promptタブに一覧編集可能なTable表示を新設
|
||||
|
||||
ユーザーからの複数の要望を1セッションで順に実装した一連の作業。
|
||||
|
||||
**1. MetadataタブをGalleryタブのサブタブへ移動**: 「Metadataタブをギャラリータブ内に移動したい。Style_Catalogタブ右隣としたい」との要望。トップナビの独立タブを廃止し、Galleryタブのサブタブ列(Output/ImagePrompt/Style_Catalog)の右端に追加。外部から画像をMetadata表示させる`loadFileIntoMetadataTab()`(`metadata-tab.js`)や、Helpタブの「Open tab →」ジャンプボタンなど、旧トップレベルタブへの参照箇所(Gallery側の「Open in Metadata Tab」ボタン、Style_Catalog側の同名ボタン)はすべて「Galleryタブへ切替→Metadataサブタブへ切替」の2段階クリックへ置き換えた。
|
||||
|
||||
**2. GenerateUIタブ: Save ボタンをBatch/Labサブタブで非表示化**: 「ワークフローのSaveボタンをBatch、Labサブタブ選択時、非表示にしたい」との要望。両サブタブは現在ロード中のワークフロー本体ではなく、ワークフローの複製やLab独自のPlanファイルを操作するため、Saveボタンが誤解を招く(クリックしても実際に触っているワークフローとは別物が保存される)ことへの対処。サブタブ切替ハンドラ(`generate-tab.js`)にワンライナーの表示切替を追加。
|
||||
|
||||
**3. Promptタブに「Table」表示を新設**: 「現在のプロンプトタブをタブ化してテーブルタブを追加したい(既存はフォームタブとする)。テーブルタブではプロンプトのプリセット、ワイルドカード、スタイルがそれぞれ表形式で一覧表示、編集可能としたい」との要望を起点に、複数回のフィードバックを経て以下の形に発展した:
|
||||
- 既存の3カラムレイアウト(AI Assistant / Presets・Preset Manager / Wildcard・Style)をまるごと「Form」パネルとしてラップし、新設の「Table」パネルと上部のトップレベルタブで切り替える構成にした。
|
||||
- Tableパネル内にPresets・Wildcards・Styleの3つの編集可能テーブルを新設(`prompt-table.js`新規ファイル)。既存の`prompt-tab.js`が保持するAPIヘルパー(`fetchPresets`/`apiUpdatePreset`/`wcSaveFile`/`styleApiUpdate`等)とリフレッシュ関数を`export`して再利用し、Table側での編集は即座にForm側にも反映されるようにした。
|
||||
- **「Presets右隣にPresets Groupを追加したい」**: グループ管理専用の4つ目のサブタブを新設。グループの作成・リネーム・削除・メンバー一覧(バッジ表示・×で削除)に対応。
|
||||
- **「共通で左端に番号(自動)列を追加したい。個別の削除ボタンは誤操作が予想されるため上部にボタンを設置し番号列で該当番号選択、削除という手順にしたい」**: 4テーブル共通で自動採番の`#`列を新設し、番号クリックで行選択(複数可・赤ハイライト)→ツールバーの「Delete」ボタンで一括削除、という手順に統一。個別の行削除ボタンは全廃した。新規行追加時は番号セルの代わりに小さな✓(Add)/✕(Cancel)アイコンを表示する設計にした。
|
||||
- **「バッチグループの登録方法を変更したい」**: PresetsテーブルにForm側と同じ仕組みの「B」列(クリックでグレー⇔黄色にトグルしBatchグループへ登録/解除)と、Batchグループを一括クリアする「BC」ボタンをツールバー上部に追加。Presets Groupテーブルからは予約グループ「Batch」を非表示にし、Batch管理をPresetsテーブル側のB列/BCボタンへ一本化した。
|
||||
- **「PresetsのコピーボタンとグループドロップダウンをテーブルのPresetsタブに配置したい。これによりPresets GroupタブのAdd Presetsをなくしたい(プリセットが多くなった際に選択が困難なため)」**: Presets Groupテーブルの「Add Preset」列(プリセットが多いと使いづらい長大なドロップダウン)を廃止。代わりにPresetsテーブルのツールバーに「PP Copy」「NP Copy」(選択中1件のPositive/Negativeをクリップボードへコピー)と、グループ選択+「Add to Group」(選択中の複数プリセットを一括で選んだグループへ追加)を新設した。プリセット側を検索・選択してから短いグループ一覧から選ぶ流れにしたことで、プリセット数が多くても操作しやすい設計になった。
|
||||
- **「選択クリアボタンも追加したい」**: 4テーブル共通で「Deselect」ボタンをツールバーへ追加し、選択状態を一括解除できるようにした。
|
||||
- **「プリセット、ワイルドカード、スタイルで番号をWクリックでモーダルでプロンプトの表示、編集したい」**: `#`列のダブルクリックで、既存の共有モーダル基盤(`app.js`の`openModal`/`closeModal`)を再利用したプロンプト編集モーダルを開くようにした。Presets/Styleは正負プロンプト切り替えボタン+共有の大型テキストエリア(切り替えても両方の編集内容を保持)、Wildcardsはファイル内容のみのテキストエリアで、Save/Closeボタンを備える。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習(index.html + i18n.js 3言語 + app.jsのhelpIdMapの3点セット)に従い、GenerateUI Tab(Saveボタン非表示の説明)・Prompt Tab(新規カード「Form / Table」「Table View」10項目)・Gallery Tab(Metadataサブタブの説明)・Metadata Tab(Galleryタブ内へ移動した旨の注記、`metadata-tab.js`側の独自i18n適用ロジックへ追加)を更新。README.mdはGenerateUI TabのSaveボタン説明を拡張、Prompt Tabセクションに新規ブロックを追加、「Metadata Tab」セクションをGalleryタブの4番目のサブタブとして「Metadata Gallery subtab」に改称の上、Style_Catalog Gallery subtabの直後へ移設した。
|
||||
|
||||
**検証**: 全JSファイルを`node --check`で構文確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイルを都度両フォルダへ同期。Playwright MCP経由で実機ブラウザ操作を機能追加ごとに実施: (1) Gallery→Metadataサブタブの表示とHelpジャンプボタンの遷移、(2) Batch/LabサブタブでのSaveボタン非表示、(3) Presets/Wildcards/Styleテーブルの実データ表示・インライン編集の永続化・行追加・削除、(4) 番号列での選択トグルとDelete/Deselectボタンの有効化、(5) Presets GroupタブでのグループCRUD・メンバー追加削除・予約グループBatchの削除保護、(6) B列/BCボタンでのBatchグループ登録・一括クリア(既存の"pony"データが誤って変更されないことも確認)、(7) PP/NP CopyとAdd to Groupの単一選択/複数選択それぞれの動作、(8) ダブルクリックモーダルの表示・Positive/Negative切り替え時のテキスト保持・保存→サーバー反映→モーダル自動クローズ、(9) ヘルプ全項目の日本語UIでの表示、を一通り確認。テスト用に作成したデータはすべて削除・復元済み。
|
||||
|
||||
**How to apply**:
|
||||
|
||||
1. 1つの機能要望(テーブル表示の追加)が実装後の実運用フィードバックを経て、削除方式・グループ登録方式・選択解除・詳細表示方式と段階的に発展していくケースでは、各段階で「既存の確立済みパターン(今回は#列クリック選択→ツールバー操作、共有モーダル基盤)を後続の要望にも一貫して適用できないか」を優先的に検討すると、機能ごとにUIパターンがバラつかず学習コストが低いまま拡張できる。
|
||||
2. 一覧が長大になりうるデータ(プリセット)をドロップダウンで選ばせるUIは、件数が増えると事実上使えなくなる。件数が少ないことが保証されている別の軸(今回はグループ)を主語にした選択UIへ倒すと、データ量に強い設計になる。
|
||||
3. 複数テーブルにまたがる共通パターン(番号列・選択トグル・ダブルクリックモーダル)は、テーブルごとに実装をコピーするのではなく共通ヘルパー関数(`buildNumCell`/`openPositiveNegativeModal`/`openSingleTextModal`)に切り出しておくと、後続の類似要望(今回で言えば選択クリアボタン・ダブルクリックモーダル)を全テーブルへ一度に反映しやすい。
|
||||
|
||||
関連: [[project_dev_deploy_sync]], [[project_v0336_conventions]]
|
||||
|
||||
---
|
||||
|
||||
## v0.4.5
|
||||
|
||||
### 調査: トップバー保存ワークフローのRMBG不具合報告(原因なし) + バグ修正: LabタブModel列の無意味な空キーフレーム表示を解消
|
||||
|
||||
ユーザーから「トップバーで保存した2つのワークフロー(I2I_rmbg.json/I2I_rmbg_2.json)を生成UIタブで読み込んで生成するとRemove Background(RMBG)ノードが機能せず透過画像が作成されない。ComfyUIのExport(API)で保存した3つ目のファイルは正しく動く」との報告を受けて調査した一連のセッション。
|
||||
|
||||
**1. ワークフロー解析の調査**: RMBGノードは`background_color`という「ウィジェットを持たない特殊な入力(COLORCODE型、`model`より前の位置)」を持つ特徴的な構造で、UI形式→API形式変換時に`widgets_values`の対応がずれる可能性を最初に疑った。`comfyui-workflow.js`の`convertUiToApi()`を段階的に検証:
|
||||
- `/object_info/RMBG`を実機から取得し、`_getWidgetInputNames()`が`forceInput`相当(widgetキーを持たない入力)を正しく除外し、`background_color`がウィジェット名リストから漏れることを確認。
|
||||
- ユーザー提供の`I2I_rmbg_2.json`をブラウザ内(Playwright MCP)で実際に`convertUiToApi()`にかけ、変換結果を確認 — `model`/`sensitivity`/`background`など全フィールドが正しくマッピングされていた。
|
||||
- その変換結果を実際に`/prompt`へキューイングして実行、さらにGenerateUIタブの本物のGenerateボタンをクリックする経路でも実行 — いずれも出力PNGはRGBA・アルファチャンネル0〜255の**正しい透過画像**だった。
|
||||
- 結論: 現在デプロイされているv0.4.3のワークフロー解析ロジックに該当バグは再現せず、コード変更は行わなかった。報告時点でより古いバージョンを使っていた可能性が高いと判断し、ユーザーに調査結果と再検証手順を報告した。
|
||||
|
||||
**2. Labタブ: モデルの無いワークフローで無意味なキーフレームが作成される問題**: 調査の副産物として、RMBGのみ(Checkpoint/LoRA/VAEいずれのノードも無い)ワークフローをLabタブで開くと、Model列に「1: (ワークフローの設定のまま)」という空のキーフレーム行が常に表示されてしまうことを実機で発見。ユーザーから「不要なため作成しない形にしたい」との要望を受けて修正。`_buildWorkflowForIteration()`は対象ノードIDが存在しない場合、書き込みを黙ってスキップするだけ(`write()`の`wf[nodeId]`ガード)なので、この行はRun時に何の効果も持たない死んだコントロールだった。`_hasNodeForColumn(col)`を新設し、`_renderColumn()`と`_initColumnButtons()`の+ボタンで判定 — 対応ノードが無い列は「このワークフローに該当ノードがありません」というプレースホルダーを表示し、キーフレーム追加もブロックする。VAEだけは特殊扱いで、専用VAELoaderが無くてもCheckpointノードが存在すれば内蔵VAEの上書き注入が意味を持つため(既存の`_buildWorkflowForIteration`のVAELoader自動注入ロジック)、Checkpoint存在時はVAE列も通常表示のままにした。
|
||||
|
||||
**ヘルプ・README更新**: `project_v0336_conventions`の慣習に従い、`i18n.js`に新規メッセージキー`labNoNodeForColumn`を日本語/英語/中国語すべてに追加(このケースは既存キーワードで賄えないUI状態のため新設。ヘルプページ本文の更新は対象外の細部挙動のため省略)。README.mdのLab subtabセクションに「Empty state when no matching node exists (v0.4.5)」の項目を追記し、見出しのバージョン範囲を`v0.3.87 – v0.4.5`に更新した。
|
||||
|
||||
**検証**: Playwright MCP経由で実機ブラウザ操作: (1) RMBGのみのモデル無しワークフローを読み込みLabタブへ切り替え、Model列(Checkpoint/LoRA/VAEいずれのビューでも)が空プレースホルダーになることを確認、(2) Checkpoint+VAELoaderを含む通常のワークフローでは従来通りライブ値付きキーフレームが表示され回帰が無いことを確認、(3) 同じworkflow内でLoRAノードだけが無いケースではLoRAビューのみ正しく空プレースホルダーになることを確認。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更ファイル(`lab-tab.js`/`i18n.js`/`lab-tab.css`)を両フォルダへ同期しdiffで完全一致を確認した。
|
||||
|
||||
**How to apply**:
|
||||
|
||||
1. 「機能しない」という不具合報告を受けた際は、コードを読んで理屈上正しそうに見えても、実機(今回はPlaywright MCP経由の実ブラウザ+実ComfyUI)で変換結果と生成画像の中身(アルファチャンネルの実値など)まで直接検証してから結論を出す。理屈の検証だけでは「今のバージョンでは再現しない」という事実に辿り着けなかった。
|
||||
2. 「機能しない」の調査中に見つかった別の実害あるバグ(今回のLabタブの死んだキーフレーム行)は、本題の調査がクローズしても見逃さず、その場でユーザーに報告し必要なら合わせて修正する。
|
||||
3. UIに「設定できるが実行時には何の効果も持たない」コントロールを残すと、ユーザーは正常に設定できていると誤認する。対象データが存在しないなら、コントロール自体を出さない(無効化ではなく非表示)のが最も誤解を生まない。
|
||||
|
||||
関連: [[project_v042_lab_model_column]], [[project_dev_deploy_sync]]
|
||||
|
||||
---
|
||||
|
||||
## 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表示切替
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
# 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, Side panel toolbar buttons, 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, Video workflow support, 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.5.2)
|
||||
Setting / Results / Plan JSON sub-panels, Per-column keyframes, Model column — Checkpoint / LoRA Single / VAE, Empty state when no matching node exists, 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, Select vs. edit, and keyframe reordering, PG — apply a Prompt tab preset group, Video workflow support, Use generated image for next, now video-aware
|
||||
|
||||
### 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, Form / Table toggle, Table view — Presets / Presets Group / Wildcards / Style
|
||||
|
||||
### 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 / ImagePrompt / Style_Catalog / Metadata sub-tabs, Image browser, Folder tree root label, Thumbnail / Table views, Folder management, File operations, Download, MP4 video support, MP4 embedded metadata, Info tab: dimensions & duration, Multi-select, Image Compare, Prompt search, Server-side filtering, Group management, Thumbnail F button, Favorites, Detail panel, Prompt tab, Workflow viewer, GenUI button, Image Edit button, Send GenUI Image button, Send to LI node 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
|
||||
|
||||
### Metadata Gallery subtab (v0.3.8, moved from a top-level tab in v0.4.6)
|
||||
Now inside Gallery, 3-column layout, File drop, MP4 embedded metadata, Model extraction, LoRA extraction, Prompt extraction, Prompt actions, Format support, Format note
|
||||
|
||||
### 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
|
||||
|
||||
### Video Tab (v0.5.0 – v0.5.2)
|
||||
Dedicated MiniMax H3 video generation UI, independent of GenerateUI, 3-pane layout, First Frame / Last Frame are both optional, Node lookup instead of hardcoded IDs, Load in Video, Preview frame + Video Source panel, Frame tab, GIF tab
|
||||
|
||||
### 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
|
||||
|
||||
@@ -69,6 +69,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- **Badge management** — add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)
|
||||
- **AI summary** — generate workflow descriptions using Ollama
|
||||
- **Import / Export** — import workflows from files or clipboard; **Send to Canvas** button sends the selected workflow directly to the ComfyUI canvas (UI and API formats both supported)
|
||||
- **Side panel toolbar buttons** (v0.5.0) — selecting a card shows three buttons in the top toolbar: **Load in GenerateUI**, **Load in Video** (new — loads a MiniMax H3 Image-to-Video workflow into the Video tab; shows an error toast for workflows that don't contain that node), and **Send to Canvas**; the detail modal's duplicate copies of these three buttons were removed in favor of this single toolbar location
|
||||
- **Default view setting** — persist your preferred view mode (Thumbnail / Table)
|
||||
- **Search clear (✕)** — inline ✕ button appears inside the search box whenever text is entered; click to clear immediately
|
||||
- **Clear all filters (✕ Clear)** — rightmost toolbar button; resets search text, group filter, and badge filter (ALL) in one click
|
||||
@@ -89,7 +90,8 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
<summary><h3>GenerateUI Tab (v0.3.5)</h3></summary>
|
||||
|
||||
- **6-tab layout** — Input / Model / Settings / Feeder / Batch / **Lab** tabs; Input, Model, and Settings each include a Raw JSON column on the right for instant preview and direct editing
|
||||
- **Save button** — located at the right end of the subtab row; opens a filename dialog (default: current workflow name) and saves the current workflow as a `.json` file to the Workflow tab via the import API
|
||||
- **Save button** — located at the right end of the subtab row; opens a filename dialog (default: current workflow name) and saves the current workflow as a `.json` file to the Workflow tab via the import API; hidden while the Batch or Lab subtab is active (v0.4.6), since those work on workflow copies / their own Plan files rather than the currently loaded workflow
|
||||
- **Default-workflow snapshot fix** (v0.4.8) — overwriting the loaded workflow under a filename that's currently set as the auto-load **Default workflow** (Settings tab, see below) now also refreshes that cached snapshot; previously only the file on disk was updated, so the old (pre-edit) version — including any checkpoint no longer installed — kept reappearing after every page reload until the default was re-set by hand
|
||||
- **Input tab** — Prompt and Image inner tabs (drag-and-drop upload); Prompt tab shows Positive Prompt and Negative Prompt textareas, plus an **Embeddings selector** at the bottom (Filter + Select + Weight input + Paste button); Paste inserts `(embedding:Name:weight)` at the cursor position of the last focused textarea (defaults to Positive when neither is focused); Raw JSON (540px) in the right column
|
||||
- **Revert button** (v0.3.70) — a ↺ button next to each Apply button reverts the textarea to the node's currently applied value (the reverse of Apply)
|
||||
- **Emphasis weight editing** (v0.3.70) — select text (or just place the cursor on a word/parenthesis block) and press Ctrl+↑/↓ to adjust `(text:weight)` by ±0.05, A1111/native-ComfyUI style; parentheses are removed automatically when the weight returns to 1.0
|
||||
@@ -98,12 +100,14 @@ 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
|
||||
- **One-click generation** — queue prompts to ComfyUI without leaving the studio
|
||||
- **Seed control** — randomize, lock, or manually set seeds; seed input and mode selector stacked vertically for readability
|
||||
- **Style selector** — checkbox and dropdown next to the Reset Workflow button; enable to apply a Fooocus-style JSON to positive and negative prompts at generation time; style files (`*.json`) are loaded from `user/default/Workflow-Studio/style/`; the style's `{prompt}` placeholder is replaced with the original prompt text, or the style is appended if no placeholder exists; `negative_prompt` is appended to the existing negative prompt; styles are applied to a per-generation copy and do not modify the loaded workflow
|
||||
- **Wildcards inside a Style now expand** (v0.4.7) — an A1111-style `__wildcard__` reference written into a Style's Positive/Negative prompt is expanded at generation time just like one typed directly into GenerateUI; the Style text is merged into the prompt *before* wildcard expansion runs (previously the order was reversed, so a Style's own wildcard references were sent to ComfyUI unexpanded)
|
||||
- **Create Catalog / Catalog buttons** (v0.3.95) — next to the Style dropdown. **Create Catalog** generates one preview image per Style checked in the Batch tab's Style sub-tab (using the currently loaded workflow, typically T2I), then saves each result into a chosen `ws_style_catalog` subfolder named after the Style (existing files with the same name are overwritten); a modal lets you create a new destination folder or pick an existing one — separate folders are useful for grouping catalogs by checkpoint or by purpose. **Catalog** jumps straight to the Gallery tab's Style_Catalog sub-tab to browse the results (see below)
|
||||
- **Don't keep a copy in the Output folder** (v0.3.96) — checkbox in the Create Catalog modal; each generation still writes a normal file to the Output folder first (the workflow's own Save Image node always does that), but once its copy has been saved into the catalog folder, checking this deletes that Output-folder original right away — keeps a large catalog run (e.g. every Style at once) from flooding the Output folder with near-duplicate files; if a style's catalog copy fails to save, its Output file is left alone rather than deleted
|
||||
- **Batch type selector** — check one of the column header checkboxes in the Batch Queue pane (Checkpoint / Lora / Prompt / Workflow / Sampler / Scheduler / Style) to activate that batch type; only one type can be active at a time; the Batch panel below Generate shows the active type, progress, and Pause/Resume/Stop controls
|
||||
@@ -113,6 +117,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- **Right pane (Batch Queue)** — 7 columns: Checkpoint / Lora / Prompt / Workflow / Sampler / Scheduler / **Style**; each column header has an enable checkbox (radio behavior: only one at a time); count shown per column; Style batch applies each selected style sequentially to a workflow copy
|
||||
- **Lora batch prompt sync** (v0.3.69) — for each selected LoRA, its syntax (`<lora:name:strength_model:strength_clip>`) and CivitAI trained words are automatically appended to the Positive prompt (rebuilt from the original prompt each time, not accumulated), then restored to the pre-batch state when the batch completes or is stopped; if a selected LoRA is not yet recognized by ComfyUI's model list, a warning toast suggests a Refresh or restart
|
||||
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion; improved analysis covers SDXL multi-hop CONDITIONING chains, CLIPTextEncodeSDXL, SDXLPromptStyler, KSamplerAdvanced, and Guider-based "Advanced Sampling" workflows (`SamplerCustomAdvanced`/`SamplerCustom` + `CFGGuider`/`DualCFGGuider`/`BasicGuider` + `RandomNoise` + `KSamplerSelect` + `*Scheduler` — Flux.2 Klein, LongCat, Boogu, HiDream E1, etc.)
|
||||
- **Video workflow support** (v0.5.2) — loading a MiniMax H3 (or other all-in-one video-generation) workflow now works from GenerateUI too, not just the dedicated Video tab: the Input tab's Prompt field picks up its `prompt` widget, the Image tab picks up its LoadImage-based `first_frame` the same way any other I2I workflow does, and generation runs with a longer timeout since video generation takes much longer than a still image; the result preview shows the generated mp4 inline (`<video>`) instead of trying to render it as an image
|
||||
- **Eagle integration** — auto-save generated images to [Eagle](https://eagle.cool/) with metadata
|
||||
|
||||
</details>
|
||||
@@ -146,17 +151,19 @@ 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.5.2)</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
|
||||
- **Empty state when no matching node exists** (v0.4.5) — Checkpoint/LoRA/VAE each show "No matching node in this workflow" instead of a keyframe list when the loaded workflow has no node that column could actually override, and the column's +/− is disabled too, so no dead "(workflow default)" keyframe row can be created in the first place; VAE still counts as usable whenever a Checkpoint node exists, since its baked-in VAE can still be overridden via auto-injected VAELoader as above
|
||||
- **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)
|
||||
@@ -166,6 +173,10 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **Save index image to Output on Run** (v0.3.90, on by default since v0.3.93) — checkbox in the run controls; when checked, every completed Run also builds the same contact-sheet index image used for Plan files and saves it directly into ComfyUI's own Output folder as `Lab_index_<counter>_.png` (auto-numbered like a normal generated image, right alongside the images that Run just generated); independent of Plan Save, which always writes its own copy next to the plan file regardless of this checkbox; saved with the plan as `save_index_on_run`
|
||||
- **Index images are self-contained plan files** (v0.3.91) — every index-image PNG, whether from Plan Save/Save As or from Save index image to Output on Run, carries the entire plan embedded directly in the file as a PNG `wfm_lab_plan` chunk; this is what lets the Output-folder copy above work as a Plan Load source even though it has no matching `.json` anywhere
|
||||
- **Eagle integration** — every image Lab generates is auto-saved to Eagle the same way as GenerateUI's own Generate button, if enabled in the Settings tab; the Output-folder index image above is included too (v0.3.90) once saved
|
||||
- **Select vs. edit, and keyframe reordering** (v0.4.8) — a single click on a keyframe cell now only selects it (blue outline); double-click to open its edit modal as before. Each column's header (Model / Prompt / KSampler) also gets **▲ / ▼** buttons that swap the selected keyframe with its immediate neighbor, reordering keyframes without retyping them; keyframe #1 (the live-reflecting baseline) can never move, and nothing can be swapped into its slot
|
||||
- **PG — apply a Prompt tab preset group** (v0.4.8) — a **PG** button on the Prompt column header opens a picker for the Prompt tab's Table-view preset groups (the same groups the Batch tab's checklist uses); choosing one replaces the entire Prompt column with one keyframe per preset in that group's order, starting at #1 (#1 is overwritten, not appended after). If the group has more presets than the current Batch count, Batch count is raised automatically to fit (capped at 50, the Batch input's own limit); anything still beyond that shows the existing "keyframes beyond Batch count" warning instead of being dropped
|
||||
- **Video workflow support** (v0.5.2) — loading a MiniMax H3 (or other all-in-one video-generation) workflow into GenerateUI and running Lab now works end to end: the Prompt column writes to its `prompt` widget, the KSampler column auto-hides (same "no matching node" treatment as Model sub-columns) since the sampler wired internally is a fixed part of the model's own preset rather than something meant to be tuned per keyframe, the Results grid plays mp4 results inline, and the contact-sheet index image draws each video result's last frame instead of failing silently onto a black square
|
||||
- **Use generated image for next, now video-aware** (v0.5.2) — when a Run's output is a video, the "Use generated image for next" chain extracts its last frame client-side (canvas) and uploads it as the next iteration's source image, instead of trying to feed the raw mp4 into a LoadImage node
|
||||
|
||||
</details>
|
||||
|
||||
@@ -189,21 +200,18 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **New styles go to a dedicated file** — created via **+ New**, a style is appended to its own `custom.json` inside the style folder, so the bundled style packs are never modified directly
|
||||
- **+ Add to this file** — while editing an existing style, this button clears the form for a brand-new entry that gets appended to that *same* file instead of the default `custom.json` — the way to add a style into one of the bundled packs
|
||||
- Saving, renaming, or deleting a style immediately refreshes GenerateUI's Style dropdown, the Batch tab's checklist, and the Style Catalog's name list — no reload needed
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Metadata Tab (v0.3.8)</h3></summary>
|
||||
|
||||
- **3-column layout** — Drop zone (left) | Model info (center) | LoRA + Prompt (right)
|
||||
- **File drop** — drop a ComfyUI-generated PNG / WebP or workflow JSON onto the drop zone (or click to open a file picker); PNG/WebP images are shown as a preview
|
||||
- **Model extraction** — automatically extracts Checkpoint, VAE, Diffusion Model, and Text Encoder names from the workflow; supports both standard and subgraph-based workflows (Flux.2 Dev/Klein, Qwen-Image-Edit/2511/Layered, Z-Image Base/Turbo, Ernie Image, WAN2.2, Mage-Flow, LongCat, Boogu, HiDream E1, FireRed, Krea-2 Turbo); node types covered: UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, QuadrupleCLIPLoader (e.g. HiDream 4-CLIP)
|
||||
- **LoRA extraction** — lists all LoRA models with `strength_model / strength_clip` values
|
||||
- **Prompt extraction** — lists prompts with POS / NEG badges when positive/negative can be determined; when distinction is not possible (e.g. `SamplerCustomAdvanced`, intermediate nodes, cross-level connections), prompts are shown without a badge as plain **Text**; click any entry to view the full text below
|
||||
- **CLIP Text Encode edit+ support** (v0.3.92) — `CLIPTextEncodeEditPlus` nodes are resolved using the same RAW / EDIT / front / back combination rule as the node itself, correctly merging the linked `text1` (e.g. a Lora Manager's trigger words) with the editable `text_edit` field instead of showing only one side
|
||||
- **Prompt actions** — Copy to clipboard, **GenUI:P/N** (set GenerateUI positive/negative prompt), **Prompt:P/N** (set Prompt tab preset positive/negative)
|
||||
- **Format support** — ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Ernie Image / WAN2.2 / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Krea-2 Turbo subgraph workflows), SD WebUI, SD Forge, Fooocus
|
||||
- **Format note** — supported formats and covered model types are always shown in the left column
|
||||
- **Form / Table toggle** (v0.4.6) — two top-level tabs at the top of Prompt Tab: **Form** is the one-item-at-a-time editor described above; **Table** shows the same Presets / Wildcards / Style data all at once as editable spreadsheets, useful once there are more than a handful of items to manage
|
||||
- **Table view — Presets / Presets Group / Wildcards / Style** (v0.4.6) — four sub-tabs, each an editable table built from the exact same data as the Form tab above (changes on either side stay in sync)
|
||||
- **Row selection & bulk delete** — every table shares a left-most auto-numbered **#** column; click a number to select that row (multiple at once), then use the toolbar's **Delete** button to remove every selected row in one confirm, or **Deselect** to clear the selection — replacing per-row delete buttons, which were too easy to misclick
|
||||
- **Double-click to edit in a modal** — double-clicking a row's number opens a larger prompt-editing modal instead of the small in-table textarea; Presets/Style show Positive/Negative toggle buttons above one shared textarea (switching preserves unsaved text on the other side), Wildcards shows just the file content; Save writes back and closes the modal, Close discards changes
|
||||
- **Wildcard syntax toolbar in the Presets modal** (v0.4.7) — the same `{|}` / `__|__` / `<lora>` / file-picker toolbar as the Form tab's Wildcard composer appears above the Presets modal's textarea, since that's where a wildcard reference typically ends up; not shown in the Wildcards or Style modals (the Wildcard modal edits the raw file content itself, and Style prompts have no such wildcard-composer connection)
|
||||
- **+ New row** — the toolbar's **+ New ...** button adds a blank editable row with small ✓ (Add) / ✕ (Cancel) icons in place of its number
|
||||
- **Presets table** — **★** (favorite) and **B** (Batch group) columns work as toggle switches, same as the Form tab's Preset Manager; the toolbar's **BC** button clears the whole Batch group at once; **PP Copy** / **NP Copy** copy the single selected row's positive/negative prompt to the clipboard; a **group select + Add to Group** control bulk-adds every selected preset to a chosen group in one click — searching/selecting from the table itself (rather than a long per-group preset picker) keeps group assignment usable once there are many presets
|
||||
- **★ / B sort toggles** (v0.4.7) — toolbar buttons next to the search box bring favorited and/or batch-selected presets to the top of the list; both can be enabled together (★ takes priority, B breaks ties), and everything else keeps its original relative order; toggling a row's own ★/B while a sort is active re-sorts immediately
|
||||
- **Presets Group table** — create, rename, and delete groups, with each group's members shown as removable badges; the reserved **Batch** group is not listed here — it's managed entirely through the Presets table's B column and BC button instead
|
||||
- **Wildcards table** — same files as the Form tab's Wildcard file manager above; editing the File path or the Ext dropdown renames the file (saves the content under the new name, then removes the old file)
|
||||
- **Style table** — same styles as the Form tab's Style manager above, editable inline the same way as Presets
|
||||
- **+ Add to this file** (v0.4.8) — select exactly one row whose File badge isn't `custom.json` to enable this toolbar button, bringing the Form tab's same-file-add capability (above) to the Table view too; it adds a blank row targeting that same file (shown as a badge in the File column even before saving); custom.json entries and multi-row selections leave it disabled — use the regular + New Style button for those instead
|
||||
|
||||
</details>
|
||||
|
||||
@@ -222,7 +230,8 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **CivitAI API Key** — optional Bearer token for authenticated CivitAI access; stored in `settings.json` (excluded from data exports); environment variable `CIVITAI_API_KEY` takes priority if set
|
||||
- **Default workflow** — set a workflow to auto-load on startup
|
||||
- **Default Checkpoint** (v0.3.73) — optional checkbox to force every checkpoint loader node's model to a chosen default whenever a workflow is loaded into GenerateUI; a safeguard against generation errors or garbled images from a stale/mismatched checkpoint saved in the workflow file
|
||||
- **Data Management** — export all plugin data (settings, metadata, prompts, etc.) to a single JSON file; import to restore data (useful when migrating or reinstalling); API keys are excluded from exports for security
|
||||
- **Data Management** — export core settings/metadata/prompts/CivitAI cache to a single JSON file; import to restore data (useful when migrating or reinstalling); API keys are excluded from exports for security
|
||||
- **Full Backup (ZIP)** (v0.5.2) — additionally covers what the JSON bundle can't represent: folder-based data (AI Skills, Lab Plans, Style presets) and the Tagger SQLite database; two opt-in checkboxes can also include ComfyUI's default workflows folder (`user/default/workflows`) and the Impact Pack's wildcard folder in the export — both of those are export-only and stored under an `_external/` prefix in the ZIP that Restore always skips, since auto-overwriting files a different plugin/ComfyUI core manages would be unsafe; copy them back by hand if needed. Restoring overwrites existing files; restart ComfyUI afterward for either restore path
|
||||
- **Text Size** — one slider (10–28 px) adjusts font size for all prompt and chat textareas at once: Generate UI positive/negative prompts, AI Assistant chat input, Preset prompts, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings
|
||||
- **RAW JSON Colors** — customize the 6 syntax highlight colors for the Raw JSON editor in Generate UI: Default Text (base), Name/Scheduler (yellow), Title (pink), Width/Height (green), Prompt/Text (cyan), Image/File (red); changes apply immediately on color pick; Reset Defaults restores the original scheme; saved to `localStorage` under `wfm_settings.jsonColors` and applied on startup
|
||||
- **GenerateUI Model Tab Highlight** (v0.3.98) — pick the color used to mark Checkpoint/VAE/Diffusion Model/Text Encoder/ControlNet/Hypernetwork labels and the LoRA column header in GenerateUI's Model tab when the loaded workflow actually contains that type of node; default bright green; changes apply immediately, Reset Defaults restores it; saved to `localStorage` under `wfm_settings.modelTabActiveColor`
|
||||
@@ -235,13 +244,16 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
<details>
|
||||
<summary><h3>Gallery Tab (v0.3.44)</h3></summary>
|
||||
|
||||
- **Output / Style-Prompt sub-tabs** (v0.3.94) — the Gallery tab has two sub-tabs at the top: **Output** (the image browser described below) and **Style/Prompt** (a separate visual prompt-building library — see its own section below)
|
||||
- **Output / ImagePrompt / Style_Catalog / Metadata sub-tabs** (v0.3.94, v0.3.95, v0.4.6) — four sub-tabs at the top of the Gallery tab: **Output** (the image browser described below), **ImagePrompt** and **Style_Catalog** (separate visual libraries — see their own sections below), and **Metadata** (embeds the full Metadata tab — see its own section below)
|
||||
- **Image browser** — browse ComfyUI output images (Thumbnail / Table views) with server-side scanning optimized for 6,000+ image libraries
|
||||
- **Folder tree root label** (v0.3.90) — the root entry in the folder tree is labeled simply **[root]**, no longer suffixed with the scanned folder's own directory name (which could be a misleading, environment-specific name — e.g. StabilityMatrix installs where the output folder is a symlink named `Text2Img` — even though the folder holds every kind of generated output, not just text-to-image results)
|
||||
- **Thumbnail / Table views** — switch view modes; Favorites column shown leftmost in Table view
|
||||
- **Folder management** — create subfolders ("+ New") or delete the selected folder with all contents ("Del") from the folder tree header
|
||||
- **File operations** — move or delete individual images from the detail panel's Info tab; bulk Move To..., Export, and Delete File from the multi-select bar
|
||||
- **Download** — hover over the image preview in the detail panel to reveal a download icon (⬇); click to download the single image
|
||||
- **Download** (v0.5.0) — a dedicated orange **Download** button in the detail panel's action-button row (next to Tagger); replaces the old preview-image hover overlay, which blocked video playback controls
|
||||
- **MP4 video support** (v0.5.0) — `.mp4` files are browsable, previewable (detail panel with native `<video controls>`, lightbox, and Compare), and included in favorites/tags/groups/move/delete/export alongside images; server-side thumbnails are generated from the first video frame via PyAV (falls back to a placeholder if PyAV/ffmpeg decoding isn't available); a small ▶ badge overlays video thumbnails in the grid
|
||||
- **MP4 embedded metadata** (v0.5.1) — the Prompt and JSON tabs now also work for `.mp4` files: reads the `workflow`/`prompt` container tags ComfyUI's `SaveVideo` writes into the mp4 (via PyAV server-side), including all-in-one video-generation nodes that carry their prompt directly on a `prompt` input rather than a separate `CLIPTextEncode`-style node (e.g. `MiniMaxH3ImageToVideo`)
|
||||
- **Info tab: dimensions & duration** (v0.5.1) — the detail panel's Info tab now also shows image dimensions (width×height) or video duration, in addition to filename, size, date, tags, and memo
|
||||
- **Multi-select** — Ctrl+click to select multiple images; Bulk Bar appears for batch operations: select group → "Add to Group" / "Remove from Group"; Favorite All / Unfavorite All; Compare (2–4 images); Move To...; **Export** (downloads all selected images as a ZIP file); Delete File
|
||||
- **Image Compare** — select 2–4 images with Ctrl+click, then click "Compare" in the Bulk Bar to open a side-by-side lightbox; grid adapts to the number of selected images
|
||||
- **Prompt search** — text search covers filename, tags, memo, and prompt text (A1111 `parameters` field cached on first detail panel open)
|
||||
@@ -249,14 +261,16 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **Group management** — create, rename, delete groups and assign/remove images using the same 4-section panel as Models tab; **`__Feeder__`** is a reserved group (🔒 prefix) that cannot be renamed or deleted; used by the Feeder Gallery mode to define the generation queue; **FC** button in the toolbar clears all members without deleting the group
|
||||
- **Thumbnail F button** — cyan overlay button (top-left of each image card) toggles the image's membership in the `__Feeder__` group; cyan and always-visible when the image is in the group; visible on hover only when inactive
|
||||
- **Favorites** — star images inline without reopening the detail panel
|
||||
- **Detail panel** — view filename, path, tags, groups, and metadata in a slide-out panel
|
||||
- **Detail panel** — view filename, path, tags, groups, and metadata in a slide-out panel; four tabs — **Info** / **JSON** / **Groups** / **Prompt**
|
||||
- **Prompt tab** (v0.4.7) — selected by default (the most frequently checked piece of info shouldn't need an extra click); lists every positive/negative prompt candidate found in the embedded workflow with POS/NEG badges, reusing the exact same list→click→full-text UI as the Metadata tab; extracts from the API-format embedding when available rather than the UI-format one, since a workflow with independent top-level and subgraph sampling chains (e.g. Krea-2) can otherwise have some of its prompts missed
|
||||
- **Workflow viewer** — Metadata tab displays workflow JSON from PNG embedded data (`prompt` / `workflow` keys) or from workflow saved by the Generate UI tab; **Copy & Send Canvas** button copies the JSON to the clipboard and sends the workflow directly to the ComfyUI canvas (UI and API formats both supported)
|
||||
- **Load GenUI button** — loads the embedded ComfyUI workflow from the selected image directly into the GenerateUI tab; shows a warning toast if no workflow is embedded or the format is unsupported; Metadata button is styled green, Load GenUI button uses the primary accent color
|
||||
- **GenUI button** (renamed from "Load GenUI" in v0.5.0 for readability) — loads the embedded ComfyUI workflow from the selected image directly into the GenerateUI tab; shows a warning toast if no workflow is embedded or the format is unsupported; Metadata button is styled green, GenUI button uses the primary accent color
|
||||
- **Image Edit button** — toolbar button; sends the selected image directly to the Image Edit tab as the base layer
|
||||
- **Send GenUI Image button** — toolbar button (next to Image Edit); uploads the selected image to ComfyUI and sets it as the input for the first LoadImage node in the GenerateUI tab; automatically switches to GenerateUI → Input → Image; requires a workflow with at least one LoadImage node to be loaded
|
||||
- **Send to LI node button** (v0.5.2) — toolbar button (next to Send GenUI Image); uploads the selected image and writes its filename into the `image` widget of the node currently selected on the ComfyUI canvas (falling back to the first LoadImage-type node found) — same destination and cross-window bridge as the Image Edit tab's **Send to LI node** button (renamed from "Send to Workflow" in this same release), so both entry points now share one consistent name; requires Workflow Studio to have been opened from ComfyUI's top menu button
|
||||
- **Send CC button** (v0.3.69) — toolbar button, shown only when this Gallery tab is embedded as an iframe inside ComfyUI Comic Creator (a separate custom node); sends the selected image directly into Comic Creator's currently selected panel/overlay
|
||||
- **Search clear (✕)** — inline ✕ button appears inside the search box whenever text is entered; click to clear immediately
|
||||
- **Clear all filters (✕ Clear)** — rightmost toolbar button; resets search text, tag filter, group filter, and favorites filter in one click (sort order is preserved)
|
||||
- **Clear all filters (✕ Clear)** — toolbar button next to the view-mode (Thumbnail/Table) switch (v0.5.2, moved from the toolbar's rightmost position so it sits with the other display-affecting controls); resets search text, tag filter, group filter, and favorites filter in one click (sort order is preserved)
|
||||
- **Workflow auto-save** — images generated from the Generate UI tab have their workflow automatically saved to gallery metadata
|
||||
- **Output folder configurable** — set the scanned output folder from Settings tab
|
||||
- **SVG file support** (v0.3.77) — `.svg` files are browsable, previewable (detail panel and lightbox), and included in favorites/tags/groups/move/delete/export alongside raster formats; served as-is with no rasterized thumbnail, since `<img>` scales vector graphics natively at any size
|
||||
@@ -288,6 +302,23 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Metadata Gallery subtab (v0.3.8, moved from a top-level tab in v0.4.6)</h3></summary>
|
||||
|
||||
- **Now inside Gallery** (v0.4.6) — this used to be its own top-level tab; it's now the fourth Gallery sub-tab, to the right of Style_Catalog — everything below works exactly the same as before
|
||||
- **3-column layout** — Drop zone (left) | Model info (center) | LoRA + Prompt (right)
|
||||
- **File drop** — drop a ComfyUI-generated PNG / WebP / MP4 or workflow JSON onto the drop zone (or click to open a file picker); PNG/WebP images are shown as a preview
|
||||
- **MP4 embedded metadata** (v0.5.1) — reads the `workflow`/`prompt` container tags ComfyUI's `SaveVideo` writes into the mp4's `moov/udta/meta` box (parsed client-side, no server round-trip); also recognizes all-in-one video-generation nodes that carry their prompt directly on a `prompt` input instead of a separate `CLIPTextEncode`-style node (e.g. `MiniMaxH3ImageToVideo`)
|
||||
- **Model extraction** — automatically extracts Checkpoint, VAE, Diffusion Model, and Text Encoder names from the workflow; supports both standard and subgraph-based workflows (Flux.2 Dev/Klein, Qwen-Image-Edit/2511/Layered, Z-Image Base/Turbo, Ernie Image, WAN2.2, Mage-Flow, LongCat, Boogu, HiDream E1, FireRed, Krea-2 Turbo); node types covered: UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, QuadrupleCLIPLoader (e.g. HiDream 4-CLIP)
|
||||
- **LoRA extraction** — lists all LoRA models with `strength_model / strength_clip` values
|
||||
- **Prompt extraction** — lists prompts with POS / NEG badges when positive/negative can be determined; when distinction is not possible (e.g. `SamplerCustomAdvanced`, intermediate nodes, cross-level connections), prompts are shown without a badge as plain **Text**; click any entry to view the full text below
|
||||
- **CLIP Text Encode edit+ support** (v0.3.92) — `CLIPTextEncodeEditPlus` nodes are resolved using the same RAW / EDIT / front / back combination rule as the node itself, correctly merging the linked `text1` (e.g. a Lora Manager's trigger words) with the editable `text_edit` field instead of showing only one side
|
||||
- **Prompt actions** — Copy to clipboard, **GenUI:P/N** (set GenerateUI positive/negative prompt), **Prompt:P/N** (set Prompt tab preset positive/negative)
|
||||
- **Format support** — ComfyUI PNG/WebP/MP4/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Ernie Image / WAN2.2 / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Krea-2 Turbo subgraph workflows), SD WebUI, SD Forge, Fooocus
|
||||
- **Format note** — supported formats and covered model types are always shown in the left column
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Nodes Tab (v0.1.7)</h3></summary>
|
||||
|
||||
@@ -357,7 +388,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **Draw** — freehand brush; options: color, brush size, blend mode; paints directly onto the active draw layer while all other layers remain visible; brush cursor shown as a size-accurate circle; strokes can start from outside the canvas margin and continue past the edge
|
||||
- **Text** — click to place; configure font, size, bold/italic, align, and color; placed as an exact-size text layer sized to the measured bounding box; double-click to re-edit
|
||||
- **Shape** — drag to draw geometric shapes (Rect / Ellipse / Line / FreeLine); options: shape type, Rounded toggle (Rect/Ellipse), Fill color, Stroke color + width, Opacity; each committed shape becomes an independent draw layer; Stroke None hidden for Line/FreeLine (stroke always active)
|
||||
- **Mask** — 7 sub-tools for painting masks onto a dedicated mask layer; click **M** in the Layers header to add a mask layer (Mask tool activates automatically); clicking a mask layer in the list also switches to the Mask tool; **Tool Options bar**: sub-tool buttons (Paint / Color / Alpha / Text / Vector / Shape / SAM3), Invert checkbox, Overlay color picker (default red), Blur slider (0–50 px); the overlay is only shown for the currently active mask layer, other mask layers stay hidden until selected (v0.3.70); SAM3 requires Mask Editor One (disabled when not installed); Color / Alpha / Text / Vector / Shape work without Mask Editor One; **Properties pane — Paint**: Mode (Add / Erase), Size (1–200 px), Hardness (0–100%); **MASK EDITOR ONE section** (Paint sub-tool, visible when installed): Select opens the ABR stamp-brush library; ✕ clears back to Circle; when an image brush is active, Hardness is disabled and Spacing (5–100%), Angle (0–359°), Sz Jitter (0–100%), Rot. Jitter (on/off) appear; **Edit in Mask Editor One →** (v0.4.0) — sends the current canvas composite (plus the active mask layer's content, if one is selected) to a MaskEditorOne node on the ComfyUI canvas — the currently selected node if it is one, otherwise the first MaskEditorOne node found in the graph (same selection logic as **Send to Workflow**) — and opens its full interactive editor (paint, SAM3, BiRefNet, shapes, etc.) in the ComfyUI browser tab; clicking Apply there imports the result back here as a new mask layer; requires Workflow Studio to have been launched from ComfyUI's top-menu button and at least one MaskEditorOne node to exist on the ComfyUI canvas; **Properties pane — Color**: click canvas to flood-select pixels by color similarity; Mode (Add / Subtract), Tolerance (0–255), Feather % (0–100); **Properties pane — Alpha**: extract the active layer's transparency as a mask; Threshold, Invert checkbox, Extract Alpha button; **Properties pane — Text**: click canvas to open a stamp overlay — enter text, set Font / Size / Bold / Italic / Align / Mode (Add / Erase), Stamp or Ctrl+Enter to apply; multi-line supported; **Properties pane — Vector**: click to add Catmull-Rom spline points; Enter (or click first point) closes and fills the polygon; Backspace removes the last point; Esc resets; Mode (Add / Erase); **Properties pane — Shape**: drag to draw Rect or Ellipse; hold Shift for square/circle; blue dashed outline preview while dragging; Mode (Add / Erase); **Properties pane — SAM3**: enter a text prompt, set Max candidates (3/6/9/12), click Segment; select candidate thumbnails (multi-select), Apply Selected (N) applies with current Mode; **Mask layer A/S operation**: A/S toggle — Add (green) composites normally, Subtract (red) cuts the mask from below; **✂ clipping mask button** clips the layer below to the painted area; brush cursor shown as size-accurate circle; strokes can start from outside the canvas margin and continue past the edge
|
||||
- **Mask** — 7 sub-tools for painting masks onto a dedicated mask layer; click **M** in the Layers header to add a mask layer (Mask tool activates automatically); clicking a mask layer in the list also switches to the Mask tool; **Tool Options bar**: sub-tool buttons (Paint / Color / Alpha / Text / Vector / Shape / SAM3), Invert checkbox, Overlay color picker (default red), Blur slider (0–50 px); the overlay is only shown for the currently active mask layer, other mask layers stay hidden until selected (v0.3.70); SAM3 requires Mask Editor One (disabled when not installed); Color / Alpha / Text / Vector / Shape work without Mask Editor One; **Properties pane — Paint**: Mode (Add / Erase), Size (1–200 px), Hardness (0–100%); **MASK EDITOR ONE section** (Paint sub-tool, visible when installed): Select opens the ABR stamp-brush library; ✕ clears back to Circle; when an image brush is active, Hardness is disabled and Spacing (5–100%), Angle (0–359°), Sz Jitter (0–100%), Rot. Jitter (on/off) appear; **Edit in Mask Editor One →** (v0.4.0) — sends the current canvas composite (plus the active mask layer's content, if one is selected) to a MaskEditorOne node on the ComfyUI canvas — the currently selected node if it is one, otherwise the first MaskEditorOne node found in the graph (same selection logic as **Send to LI node**) — and opens its full interactive editor (paint, SAM3, BiRefNet, shapes, etc.) in the ComfyUI browser tab; clicking Apply there imports the result back here as a new mask layer; requires Workflow Studio to have been launched from ComfyUI's top-menu button and at least one MaskEditorOne node to exist on the ComfyUI canvas; **Properties pane — Color**: click canvas to flood-select pixels by color similarity; Mode (Add / Subtract), Tolerance (0–255), Feather % (0–100); **Properties pane — Alpha**: extract the active layer's transparency as a mask; Threshold, Invert checkbox, Extract Alpha button; **Properties pane — Text**: click canvas to open a stamp overlay — enter text, set Font / Size / Bold / Italic / Align / Mode (Add / Erase), Stamp or Ctrl+Enter to apply; multi-line supported; **Properties pane — Vector**: click to add Catmull-Rom spline points; Enter (or click first point) closes and fills the polygon; Backspace removes the last point; Esc resets; Mode (Add / Erase); **Properties pane — Shape**: drag to draw Rect or Ellipse; hold Shift for square/circle; blue dashed outline preview while dragging; Mode (Add / Erase); **Properties pane — SAM3**: enter a text prompt, set Max candidates (3/6/9/12), click Segment; select candidate thumbnails (multi-select), Apply Selected (N) applies with current Mode; **Mask layer A/S operation**: A/S toggle — Add (green) composites normally, Subtract (red) cuts the mask from below; **✂ clipping mask button** clips the layer below to the painted area; brush cursor shown as size-accurate circle; strokes can start from outside the canvas margin and continue past the edge
|
||||
- **Blur** — **Whole Blur**: Gaussian blur to the entire active layer with intensity slider (1–50 px); **Whole Mosaic**: pixelation mosaic with block-size slider (5–100 px); **Rect Blur / Rect Mosaic**: enable the toggle then drag a rectangle on the canvas to apply blur or mosaic to that region only (blue preview for blur, orange for mosaic); Rect Blur and Rect Mosaic are mutually exclusive; all operations support Undo
|
||||
- **BG Remove** — remove the background from the active layer; **Lightweight (@imgly)**: runs entirely in the browser via CDN (no server required; ~40 MB model downloaded on first use); **BiRefNet**: high-quality removal via Mask Editor One's Python backend (requires Mask Editor One installed and `birefnet.safetensors` in `ComfyUI/models/background_removal/`); **New Layer** option (default on) adds the result as a new layer above the original; when off, replaces the active layer in-place
|
||||
- **Filter (G'MIC)** — apply G'MIC-Qt filter effects to the active image layer; click **Edit with G'MIC** to launch G'MIC-Qt GUI; select a filter and click OK — a "G'MIC-Qt filter output" window appears; close it to save the result back as the active layer; requires G'MIC-Qt Standalone installed and path configured in Settings → G'MIC-Qt Executable Path
|
||||
@@ -365,12 +396,26 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **Layer panel** — layer list with visibility (👁/🚫), lock (🔒/🔓), and clipping-mask (✂) toggles; opacity slider; type icons (🖼 image / T text / ✏ draw / ⬚ mask); Layer 1 is automatically locked on first image load
|
||||
- **Layer lock** — locked layers show an orange bounding box and 🔒 icon on the canvas; move/resize/rotate are disabled while locked; click the 🔓 button in the layer row to unlock
|
||||
- **Text quality** — text layers are rendered at their measured bounding-box size; resizing with the Select tool regenerates the canvas at the new display resolution so text stays sharp
|
||||
- **Export** — Save PNG (download composite locally); **Save to Gallery** (saves to Gallery root folder with a timestamped default name `wfs-image-YYYYMMDDHHmmss`); **Save to Input** (uploads to ComfyUI input folder for use in Load Image nodes); **Send to Workflow** (v0.4.0) — uploads the same way, then writes the resulting filename directly into the `image` widget of the node currently selected on the ComfyUI canvas (falls back to the first LoadImage-type node found); requires Workflow Studio to have been launched from ComfyUI's top-menu button so the ComfyUI tab is reachable
|
||||
- **Export** — Save PNG (download composite locally); **Save to Gallery** (saves to Gallery root folder with a timestamped default name `wfs-image-YYYYMMDDHHmmss`); **Send to LI node** (renamed from "Send to Workflow" in v0.5.2) — uploads to ComfyUI's input folder via `/upload/image`, then writes the resulting filename directly into the `image` widget of the node currently selected on the ComfyUI canvas (falls back to the first LoadImage-type node found); requires Workflow Studio to have been launched from ComfyUI's top-menu button so the ComfyUI tab is reachable; the Gallery tab's toolbar has the same button (v0.5.2, see Gallery Tab below) so a selected Gallery image can be sent the same way without going through Image Edit first
|
||||
- **Canvas navigation** — scroll-wheel zoom, Space + drag to pan; zoom indicator in the bottom bar
|
||||
- **Undo / Redo** — Ctrl+Z / Ctrl+Y (or toolbar buttons); keyboard shortcuts: V (Select), B (Draw), T (Text), S (Shape), Delete (remove selected layer when 2+ exist)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Video Tab (v0.5.0 – v0.5.2)</h3></summary>
|
||||
|
||||
- **Dedicated MiniMax H3 video generation UI, independent of GenerateUI** — a purpose-built tab for the MiniMax H3 (Image to Video) workflow; does not share state with the GenerateUI tab, and model selection (UNet/CLIP/VAE/Audio VAE) is fixed to the loaded workflow's own defaults rather than editable here
|
||||
- **3-pane layout** — left: form (First Frame / Last Frame / Prompt / Aspect Ratio / Megapixels / Multiple / Duration / Noise Seed / Generate); center: preview frame + Video Source panel, plus an Editing pane reserved for future video-editing tools; right: **Frame** / **GIF** property tabs (v0.5.2, see below)
|
||||
- **First Frame / Last Frame are both optional** — leaving First Frame empty runs the workflow text-only (the node is simply left unconnected — a "bypass"); workflows that don't even include a LoadImage node for First Frame are supported out of the box, and a LoadImage node is only injected into the graph on the fly if an image is actually supplied
|
||||
- **Node lookup instead of hardcoded IDs** — locates the MiniMax H3 subgraph instance, its linked Resolution Selector, and (if present) its linked LoadImage node by walking the workflow's subgraph definitions and links, so a workflow doesn't need to match the reference `minimax_test.json`'s exact node IDs
|
||||
- **Load in Video** (Workflow tab) — load any workflow containing a MiniMax H3 Image-to-Video node straight into this tab from the Workflow tab's side-panel toolbar (next to Load in GenerateUI); unsupported workflows show an error toast instead of loading
|
||||
- **Preview frame + Video Source panel** (v0.5.2, replaces the old Result panel) — the center pane's preview shows either a just-generated video or one loaded independently of generation; a **Video Source** drop zone next to it accepts any local video file (drag-and-drop or click to pick), with a **Clear** button to reset back to empty — the Frame/GIF tools below work on whatever is currently shown here, generated or not; generated results are still saved to Gallery metadata (embedded workflow JSON) automatically, so the Gallery tab's Workflow/Prompt panels work on generated videos the same way they do for images
|
||||
- **Frame tab** (v0.5.2) — captures the preview video's current playback position as a still frame: play/seek to the position you want, click **Capture Frame** (client-side canvas, no server round-trip), then **Save to Output** to write it as a PNG into ComfyUI's own Output folder (auto-numbered, same pattern as other generated images)
|
||||
- **GIF tab** (v0.5.2) — converts the preview video to an animated GIF; set Start/End (seconds), FPS, and an optional Max Width, then **Convert to GIF** — the server decodes the video (PyAV) and encodes an animated GIF (Pillow), both dependencies ComfyUI itself already requires, so no ffmpeg binary or extra install is needed; the result is saved into ComfyUI's Output folder and previewed inline
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>AI TOOL Tab (v0.3.14)</h3></summary>
|
||||
|
||||
@@ -379,6 +424,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
|
||||
@@ -407,7 +453,8 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **Package sub-tab** — dropdown to filter nodes by custom node package name
|
||||
- **M — Models tab** — browse installed models (All / ★ Favorites / Groups / By Type sub-tabs); LoRA groups show an **All N LoRAs** item — drag to canvas to place a `Lora Loader (LoraManager)` node with all LoRAs pre-loaded
|
||||
- **P — Prompts tab** — browse prompt presets with All / ★ Favorites / Categories sub-tabs; **Groups sub-tab** (row 2) — view presets by group (shared with the Batch tab's `wfm_prompt_preset_groups`)
|
||||
- **I — Information tab** — drop a ComfyUI-generated PNG/WebP or workflow JSON in the side panel to view its metadata; detects LoRAs from `LoraLoader`, `LoraLoaderModelOnly`, and `Lora Loader (LoraManager)` nodes (API format supported); supports `UnetLoaderGGUF` and `QuadrupleCLIPLoader` node types; preview area fixed at 110px
|
||||
- **I — Information tab** — drop a ComfyUI-generated PNG/WebP/MP4 or workflow JSON in the side panel to view its metadata; detects LoRAs from `LoraLoader`, `LoraLoaderModelOnly`, and `Lora Loader (LoraManager)` nodes (API format supported); supports `UnetLoaderGGUF` and `QuadrupleCLIPLoader` node types; preview area fixed at 110px
|
||||
- **MP4 embedded metadata** (v0.5.1) — reads the `workflow`/`prompt` container tags ComfyUI's `SaveVideo` writes into the mp4's `moov/udta/meta` box, parsed entirely client-side (same box-walking logic as the Gallery tab's Metadata sub-tab); also recognizes all-in-one video-generation nodes that carry their prompt directly on a `prompt` input rather than a separate `CLIPTextEncode`-style node (e.g. `MiniMaxH3ImageToVideo`)
|
||||
- **A — AI TOOL tab** — Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama, LM Studio, Lemonade, or Unsloth; Chat supports multi-turn conversations (full history sent each turn); TOOLS includes VLM image analysis and wildcard generation; settings (backend, URL, model) shared with the SPA AI TOOL tab via `localStorage`
|
||||
- **model sub-tab** — Checkpoint, VAE, Diffusion Model, and Text Encoder; drag items to canvas to place the corresponding loader node (Checkpoint → `CheckpointLoaderSimple`, VAE → `VAELoader`, Diffusion Model → `UNETLoader`, Text Encoder → `CLIPLoader`); double-click also places at canvas center
|
||||
- **lora sub-tab** — detects LoRAs from `LoraLoader`, `LoraLoaderModelOnly`, and `Lora Loader (LoraManager)` nodes (API format `inputs.loras.__value__` supported); shows `strength_model / strength_clip` values; drag individual LoRA to place `LoraLoader`; **Multiple LORA** section (appears for 1+ LoRAs) drags all LoRAs into a single `Lora Loader (LoraManager)` node with LoRA syntax pre-filled
|
||||
@@ -415,7 +462,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
- **Drag & drop workflows** — drag a workflow onto the canvas to load it
|
||||
- **Drag & drop nodes** — drag nodes/node sets onto the canvas to place them
|
||||
- **Drag & drop prompts** — drag a preset onto the canvas to create a WFS_PromptText node with positive/negative prompts
|
||||
- **Send to Canvas** — clicking "Send to Canvas" (Workflow tab toolbar / detail modal) or "Copy & Send Canvas" (Gallery tab JSON panel) sends the workflow directly to the ComfyUI canvas via `window.opener` (UI and API formats both supported); if Workflow Studio was opened from a bookmark instead of the ComfyUI toolbar, UI-format workflows fall back to title drag (panel title highlights blue with a green ● — drag it onto the canvas to load)
|
||||
- **Send to Canvas** — clicking "Send to Canvas" (Workflow tab toolbar / detail modal) sends the workflow directly to the ComfyUI canvas via `window.opener` when available (UI and API formats both supported); if Workflow Studio was opened from a bookmark instead of the ComfyUI toolbar (or in another tab), it filters the Library's **W** tab down to that workflow's card instead (v0.4.7) — drag the card onto the canvas yourself; this replaced the old title-drag fallback, which had no thumbnail and didn't work for API-format workflows at all. "Copy & Send Canvas" (Gallery tab JSON panel) still uses only `window.opener` + title-drag for UI-format workflows, since the embedded workflow it sends isn't a saved Library file to filter down to
|
||||
- **Copy prompts** — copy individual positive (P) or negative (N) prompts from sidebar items
|
||||
- **Double-click** — load workflows or place nodes without dragging
|
||||
- **Search** — search within each sub-tab to quickly find items
|
||||
|
||||
File diff suppressed because one or more lines are too long
+361
-560
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+86194
-86171
File diff suppressed because it is too large
Load Diff
+570
-546
File diff suppressed because it is too large
Load Diff
@@ -163,9 +163,15 @@ async def get_image_workflow(request: web.Request) -> web.Response:
|
||||
return web.json_response({"error": "path required"}, status=400)
|
||||
try:
|
||||
wf = _service.extract_workflow_from_metadata(path)
|
||||
if wf is None:
|
||||
return web.json_response({"workflow": None, "has_workflow": False})
|
||||
return web.json_response({"workflow": wf, "has_workflow": True})
|
||||
# prompt_workflow: プロンプト抽出専用(PNG[prompt]優先)。トップレベルとサブグラフに
|
||||
# 独立した複数系統を持つワークフローではUI形式(workflow)からの抽出がトップレベル
|
||||
# 系統しか拾えないことがあるため、Promptタブはこちらを使う(Metadataタブと揃える)。
|
||||
prompt_wf = _service.extract_prompt_workflow_from_metadata(path)
|
||||
return web.json_response({
|
||||
"workflow": wf,
|
||||
"has_workflow": wf is not None,
|
||||
"prompt_workflow": prompt_wf,
|
||||
})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Settings API routes."""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from aiohttp import web
|
||||
@@ -24,6 +27,7 @@ _DATA_FILES = [
|
||||
"model_metadata.json",
|
||||
"gallery_metadata.json",
|
||||
"tagger_settings.json",
|
||||
"civitai_cache.json",
|
||||
]
|
||||
|
||||
|
||||
@@ -37,6 +41,8 @@ def setup_routes(app: web.Application):
|
||||
app.router.add_post("/api/wfm/settings/output-dir", handle_set_output_dir)
|
||||
app.router.add_get("/api/wfm/settings/export", handle_export)
|
||||
app.router.add_post("/api/wfm/settings/import", handle_import)
|
||||
app.router.add_get("/api/wfm/settings/export-full", handle_export_full)
|
||||
app.router.add_post("/api/wfm/settings/import-full", handle_import_full)
|
||||
app.router.add_get("/api/wfm/styles", handle_get_styles)
|
||||
app.router.add_post("/api/wfm/styles", handle_create_style)
|
||||
app.router.add_put("/api/wfm/styles/{name}", handle_update_style)
|
||||
@@ -248,6 +254,152 @@ async def handle_import(request: web.Request) -> web.Response:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
# Full-backup (ZIP) export/import: unlike the flat _DATA_FILES JSON bundle above, this
|
||||
# covers the whole DATA_DIR — directory-shaped data (ai_skills/, lab_plan/, style/) and
|
||||
# the SQLite tagger.db that the JSON bundle can't represent. Excludes transient/cache
|
||||
# dirs, and wildcard/ specifically because it's typically a symlink into a *different*
|
||||
# custom_node's folder (comfyui-impact-pack) rather than this plugin's own data.
|
||||
_FULL_BACKUP_EXCLUDE_NAMES = {"gmic_temp", "thumb_cache", "wildcard"}
|
||||
|
||||
|
||||
def _add_file_to_zip(zf: zipfile.ZipFile, path: Path, base: Path):
|
||||
rel = str(path.relative_to(base)).replace("\\", "/")
|
||||
if path.name == "settings.json":
|
||||
# Same API-key scrubbing as the JSON-bundle export — fall through to a raw
|
||||
# copy if the file turns out to be unreadable/corrupt rather than skipping it.
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data = {k: v for k, v in data.items() if k not in _SETTINGS_EXPORT_EXCLUDE}
|
||||
zf.writestr(rel, json.dumps(data, ensure_ascii=False, indent=2))
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
zf.write(path, rel)
|
||||
|
||||
|
||||
# Two folders this plugin doesn't own but users often want backed up alongside their
|
||||
# own data: ComfyUI's own default workflows dir, and the Impact Pack's wildcard
|
||||
# library (a separate custom_node — the plugin's own wildcard/ is usually just a
|
||||
# symlink into this same folder, see _FULL_BACKUP_EXCLUDE_NAMES above). Opt-in only
|
||||
# (see handle_export_full's include_workflows/include_wildcard query params), and
|
||||
# stored under an "_external/" prefix so _apply_full_backup_zip can recognize and
|
||||
# skip them on restore — see that function's docstring for why.
|
||||
def _default_workflows_dir() -> Path:
|
||||
from ..config import COMFYUI_ROOT
|
||||
return COMFYUI_ROOT / "user" / "default" / "workflows"
|
||||
|
||||
|
||||
def _impact_pack_wildcard_dir() -> Path:
|
||||
from ..config import COMFYUI_ROOT
|
||||
return COMFYUI_ROOT / "custom_nodes" / "comfyui-impact-pack" / "wildcards"
|
||||
|
||||
|
||||
def _add_external_dir_to_zip(zf: zipfile.ZipFile, src_dir: Path, zip_prefix: str):
|
||||
if not src_dir.is_dir() or src_dir.is_symlink():
|
||||
return
|
||||
for path in src_dir.rglob("*"):
|
||||
if path.is_dir() or path.is_symlink():
|
||||
continue
|
||||
rel = f"{zip_prefix}/{path.relative_to(src_dir)}".replace("\\", "/")
|
||||
zf.write(path, rel)
|
||||
|
||||
|
||||
def _build_full_backup_zip(include_workflows: bool = False, include_wildcard: bool = False) -> bytes:
|
||||
"""Zips everything under DATA_DIR except the excluded names above. Walks
|
||||
top-level entries first so an excluded symlinked directory (wildcard/) is never
|
||||
even opened, rather than filtering after a recursive walk has already followed it.
|
||||
Optionally also includes the two external folders above under "_external/"."""
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for top in sorted(DATA_DIR.iterdir()):
|
||||
if top.name in _FULL_BACKUP_EXCLUDE_NAMES or top.is_symlink():
|
||||
continue
|
||||
if top.is_file():
|
||||
_add_file_to_zip(zf, top, DATA_DIR)
|
||||
elif top.is_dir():
|
||||
for path in top.rglob("*"):
|
||||
if path.is_dir() or path.is_symlink():
|
||||
continue
|
||||
_add_file_to_zip(zf, path, DATA_DIR)
|
||||
|
||||
if include_workflows:
|
||||
_add_external_dir_to_zip(zf, _default_workflows_dir(), "_external/default_workflows")
|
||||
if include_wildcard:
|
||||
_add_external_dir_to_zip(zf, _impact_pack_wildcard_dir(), "_external/wildcard")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _apply_full_backup_zip(zip_bytes: bytes) -> dict:
|
||||
"""Extracts a full-backup ZIP into DATA_DIR, overwriting existing files. Guards
|
||||
against Zip Slip (entries whose path resolves outside DATA_DIR via '..' or an
|
||||
absolute path) by checking each entry before writing it.
|
||||
|
||||
Entries under "_external/" (default workflows dir, Impact Pack wildcards — see
|
||||
_build_full_backup_zip) are always skipped here rather than restored: they live
|
||||
outside DATA_DIR and are managed by ComfyUI core / a different custom_node, so
|
||||
auto-restoring them could silently overwrite files this plugin doesn't own. They
|
||||
stay in the ZIP for the user to copy back manually if they want them."""
|
||||
summary = {"extracted": [], "skipped": []}
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
base = DATA_DIR.resolve()
|
||||
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
||||
for info in zf.infolist():
|
||||
if info.is_dir():
|
||||
continue
|
||||
if info.filename.startswith("_external/"):
|
||||
summary["skipped"].append(info.filename)
|
||||
continue
|
||||
target = (base / info.filename).resolve()
|
||||
try:
|
||||
target.relative_to(base)
|
||||
except ValueError:
|
||||
summary["skipped"].append(info.filename)
|
||||
continue
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zf.open(info) as src, open(target, "wb") as dst:
|
||||
shutil.copyfileobj(src, dst)
|
||||
summary["extracted"].append(info.filename)
|
||||
return summary
|
||||
|
||||
|
||||
async def handle_export_full(request: web.Request) -> web.Response:
|
||||
"""GET /api/wfm/settings/export-full - Download a ZIP of the entire data
|
||||
directory, including data the flat JSON bundle (handle_export) can't represent.
|
||||
Optional query params include_workflows=1 / include_wildcard=1 additionally bundle
|
||||
ComfyUI's default workflows dir / the Impact Pack's wildcard dir (see
|
||||
_build_full_backup_zip) — both restore-excluded, export-only, manual-copy-back."""
|
||||
try:
|
||||
include_workflows = request.rel_url.query.get("include_workflows") == "1"
|
||||
include_wildcard = request.rel_url.query.get("include_wildcard") == "1"
|
||||
zip_bytes = await asyncio.to_thread(_build_full_backup_zip, include_workflows, include_wildcard)
|
||||
return web.Response(
|
||||
body=zip_bytes,
|
||||
content_type="application/zip",
|
||||
headers={"Content-Disposition": 'attachment; filename="wfm-full-backup.zip"'},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error building full backup: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_import_full(request: web.Request) -> web.Response:
|
||||
"""POST /api/wfm/settings/import-full - Upload and restore a full-backup ZIP."""
|
||||
try:
|
||||
reader = await request.multipart()
|
||||
field = await reader.next()
|
||||
if field is None or field.name != "file":
|
||||
return web.json_response({"error": "file field required"}, status=400)
|
||||
zip_bytes = await field.read(decode=False)
|
||||
summary = await asyncio.to_thread(_apply_full_backup_zip, zip_bytes)
|
||||
return web.json_response({"status": "ok", **summary})
|
||||
except zipfile.BadZipFile:
|
||||
return web.json_response({"error": "Not a valid ZIP file"}, status=400)
|
||||
except Exception as e:
|
||||
logger.error("Error importing full backup: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
def _style_files() -> list:
|
||||
style_dir = DATA_DIR / "style"
|
||||
if not style_dir.is_dir():
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Video tab utility API routes: last-frame capture, animated GIF conversion."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..services.video_service import VideoService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_service = VideoService()
|
||||
|
||||
|
||||
def setup_routes(app: web.Application):
|
||||
app.router.add_post("/api/wfm/video/frame/save-to-output", handle_save_frame)
|
||||
app.router.add_post("/api/wfm/video/to-gif", handle_to_gif)
|
||||
|
||||
|
||||
async def handle_save_frame(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
image_base64 = body.get("image_base64", "")
|
||||
if not image_base64:
|
||||
return web.json_response({"error": "image_base64 required"}, status=400)
|
||||
result = await asyncio.to_thread(_service.save_frame_to_output, image_base64)
|
||||
return web.json_response({"status": "ok", **result})
|
||||
except Exception as e:
|
||||
logger.error("Error saving video frame to output: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
async def handle_to_gif(request: web.Request) -> web.Response:
|
||||
try:
|
||||
body = await request.json()
|
||||
filename = body.get("filename", "")
|
||||
subfolder = body.get("subfolder", "")
|
||||
type_ = body.get("type", "output")
|
||||
start_time = float(body.get("start_time") or 0)
|
||||
end_time_raw = body.get("end_time")
|
||||
end_time = float(end_time_raw) if end_time_raw not in (None, "") else None
|
||||
fps = float(body.get("fps") or 10)
|
||||
max_width_raw = body.get("max_width")
|
||||
max_width = int(max_width_raw) if max_width_raw not in (None, "") else None
|
||||
|
||||
if not filename:
|
||||
return web.json_response({"error": "filename required"}, status=400)
|
||||
|
||||
result = await asyncio.to_thread(
|
||||
_service.convert_to_gif, filename, subfolder, type_, start_time, end_time, fps, max_width
|
||||
)
|
||||
return web.json_response({"status": "ok", **result})
|
||||
except FileNotFoundError as e:
|
||||
return web.json_response({"error": str(e)}, status=404)
|
||||
except ValueError as e:
|
||||
return web.json_response({"error": str(e)}, status=400)
|
||||
except Exception as e:
|
||||
logger.error("Error converting video to GIF: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
@@ -191,17 +191,33 @@ class GalleryMetadataStore:
|
||||
return self._save_to_disk()
|
||||
return True
|
||||
|
||||
def cleanup_stale_images(self, folder_path: str, existing_paths: set) -> int:
|
||||
def cleanup_stale_images(self, folder_path: str, existing_paths: set, recursive: bool = True) -> int:
|
||||
"""folder_path 配下の存在しないファイルのメタデータを削除する。
|
||||
existing_paths: そのフォルダの現在のファイルパス集合(_normalize_path 済み)
|
||||
recursive: False の場合、folder_path 直下のファイルのみを対象にする。
|
||||
呼び出し元(list_images)が非recursiveスキャン(フォルダ直下のみ列挙)の
|
||||
結果を existing_paths として渡してくる場合、サブフォルダ内のファイルは
|
||||
existing_paths に含まれない。それにもかかわらず対象範囲を「folder_path配下
|
||||
全階層」のままにすると、サブフォルダ内画像のメタデータ(tags/memo等)を
|
||||
"存在しない"と誤判定して全削除してしまう(実際に発生した不具合: サブフォルダ
|
||||
に動画を保存する運用で、ルートフォルダを非recursiveで開くたびに
|
||||
サブフォルダ内の全メタデータが失われていた)。
|
||||
"""
|
||||
folder_key = self._normalize_path(folder_path)
|
||||
if not folder_key.endswith("/"):
|
||||
folder_key += "/"
|
||||
|
||||
def _in_scope(key: str) -> bool:
|
||||
if not key.startswith(folder_key):
|
||||
return False
|
||||
if recursive:
|
||||
return True
|
||||
return "/" not in key[len(folder_key):]
|
||||
|
||||
with self._lock:
|
||||
stale = [
|
||||
key for key in self._data["images"]
|
||||
if key.startswith(folder_key) and key not in existing_paths
|
||||
if _in_scope(key) and key not in existing_paths
|
||||
]
|
||||
if not stale:
|
||||
return 0
|
||||
|
||||
+113
-27
@@ -16,8 +16,8 @@ from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# サポートする画像拡張子
|
||||
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"}
|
||||
# サポートする画像拡張子(.mp4は動画だがGalleryでは静止画と同じ一覧・配信経路を共有する)
|
||||
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".mp4"}
|
||||
|
||||
# data URL の MIME タイプ -> 拡張子(save_image_to_gallery / save_image_to_folder共用)
|
||||
_SAVE_EXT_BY_MIME = {
|
||||
@@ -385,9 +385,12 @@ class GalleryService:
|
||||
# os.scandir() でファイル情報を一括取得(stat()の個別呼び出しを排除)
|
||||
raw_entries = self._scan_folder_recursive(folder) if recursive else self._scan_folder(folder)
|
||||
|
||||
# 孤立メタデータを自動クリーンアップ(移動・削除されたファイルの残骸を除去)
|
||||
# 孤立メタデータを自動クリーンアップ(移動・削除されたファイルの残骸を除去)。
|
||||
# recursive=False(非再帰スキャン)の場合、existing_pathsにはfolder直下の
|
||||
# ファイルしか含まれないため、cleanup側にもrecursive=Falseを伝えて対象範囲を
|
||||
# 直下のみに限定する(でないとサブフォルダ内ファイルのメタデータを誤削除する)。
|
||||
existing_paths = {abs_path for _, abs_path, _, _ in raw_entries}
|
||||
self.metadata_store.cleanup_stale_images(str(folder), existing_paths)
|
||||
self.metadata_store.cleanup_stale_images(str(folder), existing_paths, recursive=recursive)
|
||||
|
||||
results = []
|
||||
for name, abs_path, size, mtime in raw_entries:
|
||||
@@ -584,7 +587,10 @@ class GalleryService:
|
||||
embedded = self._read_png_metadata(path)
|
||||
elif ext in {".jpg", ".jpeg"}:
|
||||
embedded = self._read_jpeg_metadata(path)
|
||||
elif ext == ".mp4":
|
||||
embedded = self._read_mp4_metadata(path)
|
||||
|
||||
dims = self._read_media_dimensions(path, ext)
|
||||
saved = self.metadata_store.get(str(path))
|
||||
|
||||
# workflow キーを除く文字列フィールドをプロンプトキャッシュとして保存(検索用)
|
||||
@@ -603,6 +609,9 @@ class GalleryService:
|
||||
"mtime": stat.st_mtime,
|
||||
"ext": ext,
|
||||
"embedded": embedded,
|
||||
"width": dims["width"],
|
||||
"height": dims["height"],
|
||||
"duration": dims["duration"],
|
||||
"favorite": saved.get("favorite", False),
|
||||
"tags": saved.get("tags", []),
|
||||
"memo": saved.get("memo", ""),
|
||||
@@ -682,45 +691,102 @@ class GalleryService:
|
||||
logger.debug("JPEG metadata read error: %s", e)
|
||||
return result
|
||||
|
||||
def extract_workflow_from_metadata(self, image_path: str) -> dict | None:
|
||||
"""ワークフローを抽出する。
|
||||
優先順位: PNG[workflow] > PNG[prompt] > gallery_metadata.json[workflow]
|
||||
def _read_mp4_metadata(self, path: Path) -> dict:
|
||||
"""MP4コンテナのformat-levelメタデータタグ(workflow/prompt等)をPyAVで抽出する。
|
||||
ComfyUIのSaveVideoはPNGのtEXtチャンクと同じキー名(workflow=UI形式, prompt=API形式)で
|
||||
moov/udta/meta(mdta)にJSON文字列を書き込む。"""
|
||||
result = {}
|
||||
try:
|
||||
import av
|
||||
with av.open(str(path)) as container:
|
||||
for k, v in (container.metadata or {}).items():
|
||||
result[k] = v
|
||||
except Exception as e:
|
||||
logger.debug("MP4 metadata read error: %s", e)
|
||||
return result
|
||||
|
||||
def _read_media_dimensions(self, path: Path, ext: str) -> dict:
|
||||
"""画像は幅高さ、動画は幅高さ+再生時間(秒)を返す。取得できない項目はNone。"""
|
||||
info: dict = {"width": None, "height": None, "duration": None}
|
||||
try:
|
||||
if ext == ".mp4":
|
||||
import av
|
||||
with av.open(str(path)) as container:
|
||||
if container.streams.video:
|
||||
stream = container.streams.video[0]
|
||||
info["width"] = stream.width
|
||||
info["height"] = stream.height
|
||||
if container.duration:
|
||||
info["duration"] = container.duration / 1_000_000
|
||||
elif ext != ".svg":
|
||||
from PIL import Image
|
||||
with Image.open(path) as img:
|
||||
info["width"], info["height"] = img.size
|
||||
except Exception as e:
|
||||
logger.debug("dimension read error for %s: %s", path, e)
|
||||
return info
|
||||
|
||||
def _extract_embedded_workflow(self, image_path: str, prefer: str = "workflow") -> dict | None:
|
||||
"""埋め込みワークフローを抽出する(PNG/MP4の[workflow]/[prompt]の優先順位を選べる)。
|
||||
|
||||
prefer="workflow": UI形式(ノード位置・グループ等を保持)優先。GenerateUIへの
|
||||
ロードやJSON表示などエディタ復元が必要な用途向け。
|
||||
prefer="prompt": API形式(サブグラフ展開済みでフラット)優先。トップレベルと
|
||||
サブグラフに独立した複数系統を持つワークフロー(例: Krea2)ではUI形式の
|
||||
extractPromptsLiteGraphがトップレベル系統しか拾えないことがあるため、
|
||||
プロンプト抽出専用にAPI形式を優先する(Metadataタブのプロンプト抽出と同じ優先順位)。
|
||||
"""
|
||||
path = Path(image_path).resolve()
|
||||
if not self._check_path_allowed(path):
|
||||
return None
|
||||
|
||||
if path.suffix.lower() == ".png":
|
||||
ext = path.suffix.lower()
|
||||
embedded = None
|
||||
if ext == ".png":
|
||||
embedded = self._read_png_metadata(path)
|
||||
# 1. workflow キー (ComfyUI UI形式)
|
||||
for key in ("workflow", "Workflow"):
|
||||
s = embedded.get(key)
|
||||
if s:
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
# 2. prompt キー (ComfyUI API形式 — 大多数の生成画像)
|
||||
for key in ("prompt", "Prompt"):
|
||||
s = embedded.get(key)
|
||||
if s:
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
elif ext == ".mp4":
|
||||
embedded = self._read_mp4_metadata(path)
|
||||
|
||||
# 3. gallery_metadata.json に保存されたworkflow
|
||||
if embedded:
|
||||
key_groups = (("workflow", "Workflow"), ("prompt", "Prompt"))
|
||||
if prefer == "prompt":
|
||||
key_groups = tuple(reversed(key_groups))
|
||||
for keys in key_groups:
|
||||
for key in keys:
|
||||
s = embedded.get(key)
|
||||
if s:
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# gallery_metadata.json に保存されたworkflow
|
||||
saved = self.metadata_store.get(str(path))
|
||||
return saved.get("workflow") or None
|
||||
|
||||
def extract_workflow_from_metadata(self, image_path: str) -> dict | None:
|
||||
"""ワークフローを抽出する。
|
||||
優先順位: PNG[workflow] > PNG[prompt] > gallery_metadata.json[workflow]
|
||||
"""
|
||||
return self._extract_embedded_workflow(image_path, prefer="workflow")
|
||||
|
||||
def extract_prompt_workflow_from_metadata(self, image_path: str) -> dict | None:
|
||||
"""プロンプト抽出専用: PNG[prompt] > PNG[workflow] > gallery_metadata.json[workflow]"""
|
||||
return self._extract_embedded_workflow(image_path, prefer="prompt")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# メタデータ保存
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
|
||||
def save_image_meta(self, image_path: str, data: dict) -> bool:
|
||||
if not self._check_path_allowed(Path(image_path).resolve()):
|
||||
resolved = Path(image_path).resolve()
|
||||
if not self._check_path_allowed(resolved):
|
||||
logger.warning("save_image_meta: path not allowed: %s", resolved)
|
||||
return False
|
||||
return self.metadata_store.save(image_path, data)
|
||||
# get_image_metadata() 側は resolve() 済みパスをキーにして取得するため、
|
||||
# ここでも resolve() 後の文字列で統一して保存する(キー不一致による
|
||||
# 「保存はできるが表示されない」というサイレントな食い違いを防ぐ)。
|
||||
return self.metadata_store.save(str(resolved), data)
|
||||
|
||||
def toggle_favorite(self, image_path: str) -> bool:
|
||||
if not self._check_path_allowed(Path(image_path).resolve()):
|
||||
@@ -923,6 +989,8 @@ class GalleryService:
|
||||
ディスクキャッシュがあればそれを返し、なければPillowで生成して保存する。
|
||||
GIFはアニメーション保持のため元ファイルをそのまま返す。
|
||||
Pillowが使えない場合は元ファイルにフォールバック。
|
||||
MP4はPillowで開けないためPyAVで先頭フレームを抽出しJPEGとしてキャッシュする
|
||||
(元ファイルへのフォールバックはできないため、失敗時はNoneを返す)。
|
||||
"""
|
||||
p = Path(image_path).resolve()
|
||||
if not p.is_file():
|
||||
@@ -955,6 +1023,24 @@ class GalleryService:
|
||||
if thumb_path.exists():
|
||||
return thumb_path
|
||||
|
||||
# MP4は動画のためPillowで開けない。PyAV(av)で先頭フレームを抽出しJPEGとしてキャッシュする。
|
||||
# avが未導入/デコード失敗の場合、元mp4ファイルは<img>的な経路に渡せないため
|
||||
# (GIF/SVGと違い元ファイルへフォールバックできない) Noneを返しプレースホルダー表示に委ねる。
|
||||
if p.suffix.lower() == ".mp4":
|
||||
try:
|
||||
import av
|
||||
from PIL import Image
|
||||
with av.open(str(p)) as container:
|
||||
stream = container.streams.video[0]
|
||||
frame = next(container.decode(stream))
|
||||
img = frame.to_image().convert("RGB")
|
||||
img.thumbnail((width, width), Image.LANCZOS)
|
||||
img.save(thumb_path, "JPEG", quality=85, optimize=True)
|
||||
return thumb_path
|
||||
except Exception as e:
|
||||
logger.warning("serve_thumbnail: mp4 frame extraction failed for %s: %s", p, e)
|
||||
return None
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(p) as img:
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Video tab utilities: last-frame capture and animated GIF conversion.
|
||||
|
||||
Runs entirely on dependencies ComfyUI itself already requires — PyAV for
|
||||
decoding (ComfyUI core already needs av>=17.0.0) and Pillow for GIF encoding
|
||||
— so no ffmpeg binary and no new pip installs, unlike a typical ffmpeg-based
|
||||
implementation (e.g. Eagle's video2gif/video-to-frame plugins).
|
||||
"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
_RESAMPLE = getattr(getattr(Image, "Resampling", Image), "LANCZOS")
|
||||
except Exception: # pragma: no cover - Pillow is a hard ComfyUI dependency
|
||||
_RESAMPLE = None
|
||||
|
||||
|
||||
class VideoService:
|
||||
def _resolve_media_path(self, filename: str, subfolder: str, type_: str) -> Path:
|
||||
"""Resolves a ComfyUI-style {filename, subfolder, type} reference to an
|
||||
absolute path, guarding against path traversal outside the matching
|
||||
input/output/temp directory."""
|
||||
import folder_paths # type: ignore
|
||||
|
||||
base_dir = folder_paths.get_directory_by_type(type_)
|
||||
if not base_dir:
|
||||
raise ValueError(f"Invalid type: {type_}")
|
||||
base = Path(base_dir).resolve()
|
||||
target = (base / (subfolder or "") / filename).resolve()
|
||||
try:
|
||||
target.relative_to(base)
|
||||
except ValueError:
|
||||
raise ValueError("Access denied: path outside allowed directory")
|
||||
if not target.is_file():
|
||||
raise FileNotFoundError(f"File not found: {filename}")
|
||||
return target
|
||||
|
||||
def save_frame_to_output(self, image_base64: str, filename_prefix: str = "video/Frame") -> dict:
|
||||
"""Saves a client-captured video frame (PNG data URL) into ComfyUI's own
|
||||
output folder — same auto-numbered pattern as Lab's index-image save
|
||||
(see LabPlanService.save_index_image_to_output)."""
|
||||
import folder_paths # type: ignore
|
||||
|
||||
b64 = image_base64
|
||||
if "," in b64 and b64.strip().lower().startswith("data:"):
|
||||
b64 = b64.split(",", 1)[1]
|
||||
image_bytes = base64.b64decode(b64)
|
||||
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(filename_prefix, output_dir)
|
||||
save_name = f"{filename}_{counter:05}_.png"
|
||||
save_path = Path(full_output_folder) / save_name
|
||||
save_path.write_bytes(image_bytes)
|
||||
return {"filename": save_name, "subfolder": subfolder}
|
||||
|
||||
def convert_to_gif(
|
||||
self,
|
||||
filename: str,
|
||||
subfolder: str,
|
||||
type_: str,
|
||||
start_time: float = 0.0,
|
||||
end_time=None,
|
||||
fps: float = 10.0,
|
||||
max_width=None,
|
||||
filename_prefix: str = "video/GIF",
|
||||
) -> dict:
|
||||
"""Decodes the given video (PyAV), samples it down to `fps`, optionally
|
||||
downscales to `max_width`, and encodes the result as an animated GIF
|
||||
(Pillow) into ComfyUI's own output folder."""
|
||||
import av # type: ignore
|
||||
import folder_paths # type: ignore
|
||||
|
||||
if _RESAMPLE is None:
|
||||
raise RuntimeError("Pillow is not available")
|
||||
|
||||
src_path = self._resolve_media_path(filename, subfolder, type_)
|
||||
fps = max(0.1, float(fps))
|
||||
|
||||
container = av.open(str(src_path))
|
||||
try:
|
||||
stream = container.streams.video[0]
|
||||
source_fps = float(stream.average_rate) if stream.average_rate else 24.0
|
||||
step = max(1, round(source_fps / fps))
|
||||
|
||||
pil_frames = []
|
||||
idx = 0
|
||||
for frame in container.decode(stream):
|
||||
t = float(frame.pts * stream.time_base) if frame.pts is not None else None
|
||||
if t is not None:
|
||||
if t < start_time:
|
||||
idx += 1
|
||||
continue
|
||||
if end_time is not None and t > end_time:
|
||||
break
|
||||
if idx % step == 0:
|
||||
im = frame.to_image()
|
||||
if max_width and im.width > max_width:
|
||||
ratio = max_width / im.width
|
||||
im = im.resize((max_width, max(1, round(im.height * ratio))), _RESAMPLE)
|
||||
pil_frames.append(im.convert("RGB"))
|
||||
idx += 1
|
||||
finally:
|
||||
container.close()
|
||||
|
||||
if not pil_frames:
|
||||
raise ValueError("No frames found in the specified range")
|
||||
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
full_output_folder, out_filename, counter, out_subfolder, _ = folder_paths.get_save_image_path(filename_prefix, output_dir)
|
||||
save_name = f"{out_filename}_{counter:05}_.gif"
|
||||
save_path = Path(full_output_folder) / save_name
|
||||
|
||||
duration_ms = max(20, round(1000 / fps))
|
||||
pil_frames[0].save(
|
||||
save_path,
|
||||
format="GIF",
|
||||
save_all=True,
|
||||
append_images=pil_frames[1:],
|
||||
duration=duration_ms,
|
||||
loop=0,
|
||||
optimize=True,
|
||||
)
|
||||
return {"filename": save_name, "subfolder": out_subfolder, "frame_count": len(pil_frames)}
|
||||
@@ -80,7 +80,7 @@ class WorkflowStudio:
|
||||
app.router.add_get("/wfm", serve_index_page)
|
||||
|
||||
# API routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, unsloth_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes, tagger_routes, gmic_routes, skill_routes, lab_routes
|
||||
from .routes import workflow_routes, settings_routes, ollama_routes, unsloth_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes, tagger_routes, gmic_routes, skill_routes, lab_routes, video_routes
|
||||
|
||||
workflow_routes.setup_routes(app)
|
||||
settings_routes.setup_routes(app)
|
||||
@@ -96,5 +96,6 @@ class WorkflowStudio:
|
||||
gmic_routes.setup_routes(app)
|
||||
skill_routes.setup_routes(app)
|
||||
lab_routes.setup_routes(app)
|
||||
video_routes.setup_routes(app)
|
||||
|
||||
logger.info("Workflow Studio: Routes registered successfully")
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-workflow-studio"
|
||||
description = "A comprehensive workflow, asset management, and generation UI plugin for ComfyUI. Manage workflows, models, images, and prompts; batch generation and Image Feeder in the GenerateUI tab; library side panel with canvas drag & drop and AI tools."
|
||||
version = "0.4.2"
|
||||
version = "0.5.2"
|
||||
license = "MIT"
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
@@ -22,3 +22,10 @@ piexif>=1.1.3
|
||||
|
||||
# -- Required to load UNSLOTH_API_KEY from a local .env file (Unsloth backend, AI TOOL tab) --
|
||||
python-dotenv>=1.0,<2.0
|
||||
|
||||
# -- MP4 thumbnail generation (Gallery tab) --
|
||||
# Uses PyAV (av) to decode the first frame. ComfyUI core already requires av>=17.0.0,
|
||||
# so it is normally pre-installed in python_embeded and no action is needed here.
|
||||
# If missing, mp4 thumbnail generation simply fails and falls back to a placeholder
|
||||
# (Gallery itself keeps working). Not pinned here on purpose — see torch note above.
|
||||
# av>=17.0.0
|
||||
|
||||
+28
-37
@@ -290,6 +290,24 @@
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
/* 動画ファイル(mp4)の再生アイコンバッジ(サムネイル中央) */
|
||||
.wfm-gallery-thumb-video-badge {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,0.55);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* テーブルビューのお気に入りボタン */
|
||||
.wfm-gallery-table-fav-btn {
|
||||
background: none;
|
||||
@@ -465,43 +483,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wfm-gallery-preview-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.wfm-gallery-preview-wrapper:hover .wfm-gallery-preview-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wfm-gallery-download-btn {
|
||||
background: var(--wfm-accent, #4f8ef7);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.wfm-gallery-download-btn:hover {
|
||||
background: var(--wfm-accent-hover, #3f7fe7);
|
||||
}
|
||||
|
||||
/* タブコンテンツ */
|
||||
.wfm-gallery-detail-tab-content {
|
||||
flex: 1;
|
||||
@@ -836,6 +817,16 @@
|
||||
border-color: #6a4de0;
|
||||
}
|
||||
|
||||
.wfm-gallery-action-btn-orange {
|
||||
background: #f59e0b;
|
||||
border-color: #f59e0b;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.wfm-gallery-action-btn-orange:hover {
|
||||
background: #d97f06;
|
||||
border-color: #d97f06;
|
||||
}
|
||||
|
||||
/* ── ファイル操作ボタン (詳細パネル) ─────────────────────────── */
|
||||
|
||||
.wfm-gallery-file-actions {
|
||||
|
||||
@@ -270,6 +270,23 @@
|
||||
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);
|
||||
}
|
||||
|
||||
/* Single-click selection target for the column header's up/down reorder buttons —
|
||||
layered as a box-shadow (not border-color) so it never conflicts with the
|
||||
revert/live/bypassed/node-bypassed border-color signaling above. */
|
||||
.wfm-lab-cell.wfm-lab-cell-selected {
|
||||
box-shadow: 0 0 0 2px var(--wfm-primary);
|
||||
}
|
||||
|
||||
.wfm-lab-cell-applied {
|
||||
margin-top: 3px;
|
||||
padding-top: 3px;
|
||||
@@ -278,6 +295,16 @@
|
||||
color: var(--wfm-text-secondary);
|
||||
}
|
||||
|
||||
/* Shown instead of a keyframe list when the loaded workflow has no node this Model
|
||||
sub-column (checkpoint/lora/vae) could override — see _hasNodeForColumn in lab-tab.js. */
|
||||
.wfm-lab-cell-empty {
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
color: var(--wfm-text-secondary);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wfm-lab-cell-prompt-line {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
+258
-6
@@ -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;
|
||||
@@ -2374,6 +2387,248 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Prompt タブ: Form / Table 切り替え */
|
||||
.wfm-prompt-toplevel-nav {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wfm-prompt-toplevel-tab {
|
||||
padding: 6px 16px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--wfm-text-secondary);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.wfm-prompt-toplevel-tab:hover { color: var(--wfm-text-primary); }
|
||||
.wfm-prompt-toplevel-tab.active { color: var(--wfm-primary); border-bottom-color: var(--wfm-primary); }
|
||||
|
||||
.wfm-prompt-panel {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wfm-prompt-panel.active { display: flex; }
|
||||
|
||||
/* Table パネル内サブタブ (Presets / Wildcards / Style) */
|
||||
.wfm-prompt-table-subnav {
|
||||
display: flex;
|
||||
background: var(--wfm-surface);
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wfm-prompt-table-subtab {
|
||||
padding: 8px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.wfm-prompt-table-subtab:hover { color: var(--wfm-text); }
|
||||
.wfm-prompt-table-subtab.active { color: var(--wfm-text); border-bottom-color: var(--wfm-primary); }
|
||||
|
||||
.wfm-prompt-table-pane {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wfm-prompt-table-pane.active { display: flex; }
|
||||
|
||||
.wfm-prompt-table-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
}
|
||||
|
||||
.wfm-prompt-table-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 編集可能テーブル (Presets / Wildcards / Style 共通) */
|
||||
.wfm-prompt-edit-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.wfm-prompt-edit-table th,
|
||||
.wfm-prompt-edit-table td {
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
padding: 4px 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.wfm-prompt-edit-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--wfm-surface);
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
color: var(--wfm-text-secondary);
|
||||
z-index: 1;
|
||||
}
|
||||
.wfm-prompt-edit-table tbody tr:hover td {
|
||||
background: var(--wfm-surface);
|
||||
}
|
||||
.wfm-prompt-edit-input,
|
||||
.wfm-prompt-edit-textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
color: var(--wfm-text);
|
||||
font-size: 12px;
|
||||
padding: 3px 5px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.wfm-prompt-edit-textarea {
|
||||
min-height: 40px;
|
||||
resize: vertical;
|
||||
}
|
||||
.wfm-prompt-edit-input:hover,
|
||||
.wfm-prompt-edit-textarea:hover {
|
||||
border-color: var(--wfm-border);
|
||||
}
|
||||
.wfm-prompt-edit-input:focus,
|
||||
.wfm-prompt-edit-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--wfm-primary);
|
||||
background: var(--wfm-bg);
|
||||
}
|
||||
@keyframes wfm-prompt-cell-flash {
|
||||
0% { background: rgba(74, 158, 255, 0.3); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
.wfm-prompt-edit-cell-saved {
|
||||
animation: wfm-prompt-cell-flash 0.7s ease;
|
||||
}
|
||||
.wfm-prompt-edit-fav-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--wfm-text-secondary);
|
||||
padding: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
.wfm-prompt-edit-fav-btn.active { color: #ffd700; }
|
||||
.wfm-prompt-edit-batch-btn.active { color: #f0c040; }
|
||||
.wfm-prompt-edit-row-new td { background: rgba(74, 158, 255, 0.06); }
|
||||
.wfm-prompt-edit-file-badge {
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
.wfm-prompt-edit-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ダブルクリックで開くプロンプト編集モーダル (Presets / Wildcards / Style 共通) */
|
||||
.wfm-prompt-modal-toggle {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.wfm-prompt-modal-textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 320px;
|
||||
resize: vertical;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.wfm-prompt-modal-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* #(自動採番)列: クリックで行選択 → 上部の Delete ボタンで一括削除 */
|
||||
.wfm-prompt-edit-num-cell {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wfm-prompt-edit-num-cell:hover {
|
||||
color: var(--wfm-text);
|
||||
background: var(--wfm-surface-2, var(--wfm-surface));
|
||||
}
|
||||
.wfm-prompt-edit-table tbody tr.wfm-prompt-edit-row-selected td {
|
||||
background: rgba(255, 69, 58, 0.14);
|
||||
}
|
||||
.wfm-prompt-edit-table tbody tr.wfm-prompt-edit-row-selected .wfm-prompt-edit-num-cell {
|
||||
color: var(--wfm-danger);
|
||||
font-weight: bold;
|
||||
}
|
||||
.wfm-prompt-edit-num-cell-new {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
}
|
||||
.wfm-prompt-edit-inline-add-btn,
|
||||
.wfm-prompt-edit-inline-cancel-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 2px 3px;
|
||||
line-height: 1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.wfm-prompt-edit-inline-add-btn { color: var(--wfm-primary); }
|
||||
.wfm-prompt-edit-inline-add-btn:hover { background: var(--wfm-primary); color: #fff; }
|
||||
.wfm-prompt-edit-inline-cancel-btn { color: var(--wfm-text-secondary); }
|
||||
.wfm-prompt-edit-inline-cancel-btn:hover { color: var(--wfm-danger); }
|
||||
|
||||
/* Presets Group テーブル: プリセットバッジ */
|
||||
.wfm-prompt-edit-preset-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: 10px;
|
||||
padding: 2px 4px 2px 8px;
|
||||
font-size: 11px;
|
||||
margin: 2px 3px 2px 0;
|
||||
}
|
||||
.wfm-prompt-edit-badge-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 10px;
|
||||
padding: 1px 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
.wfm-prompt-edit-badge-remove:hover { color: var(--wfm-danger); }
|
||||
|
||||
/* Ollama Assistant */
|
||||
.wfm-ollama-assistant {
|
||||
display: flex;
|
||||
@@ -5018,15 +5273,12 @@ body {
|
||||
Metadata Tab
|
||||
============================================ */
|
||||
|
||||
#wfm-tab-metadata {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-metadata-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr 1fr;
|
||||
gap: 0;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/* =============================================
|
||||
Video Tab (MiniMax H3専用・GenerateUIタブとは独立)
|
||||
3ペイン構成: 左=フォーム / 中央=プレビュー+編集 / 右=プロパティ
|
||||
各ペインは独立したカラムとして高さ100%を占め、内部でスクロールする。
|
||||
中央ペインのコンテンツ量(生成結果の有無・動画の縦横比)が変わっても
|
||||
左右のペインの位置が動かないよう、意図的に高さを共有させない構造にしている。
|
||||
============================================= */
|
||||
|
||||
#wfm-tab-video {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wfm-video-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── 左ペイン: フォーム ── */
|
||||
.wfm-video-form-panel {
|
||||
width: 380px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--wfm-border);
|
||||
}
|
||||
|
||||
.wfm-video-form-panel label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wfm-video-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wfm-video-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wfm-video-wf-name {
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wfm-video-frame-slots {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wfm-video-frame-slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wfm-video-resolution-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.wfm-video-seed-randomize-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
/* ── 中央ペイン: プレビュー + 編集 ── */
|
||||
.wfm-video-center-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
border-right: 1px solid var(--wfm-border);
|
||||
}
|
||||
|
||||
/* 生成結果 or ソース選択エリアで読み込んだ動画を横並びで表示する行。
|
||||
プレビュー枠が主役、ソースパネルは選択UIのみの固定幅。 */
|
||||
.wfm-video-preview-row {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.wfm-video-preview-frame {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed var(--wfm-border);
|
||||
border-radius: 6px;
|
||||
background: var(--wfm-bg-hover, #111);
|
||||
}
|
||||
|
||||
.wfm-video-preview-el {
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.wfm-video-source-panel {
|
||||
width: 220px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wfm-video-pane-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wfm-video-prop-hint {
|
||||
font-size: 12px;
|
||||
color: var(--wfm-text-secondary);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
/* 将来の動画編集機能(トリミング等)用のプレースホルダー領域 */
|
||||
.wfm-video-edit-panel {
|
||||
flex: 1;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed var(--wfm-border);
|
||||
border-radius: 6px;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── 右ペイン: プロパティ (Frame抽出 / GIF変換タブ) ── */
|
||||
.wfm-video-properties-panel {
|
||||
width: 240px;
|
||||
flex: none;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wfm-video-prop-tabnav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
}
|
||||
|
||||
.wfm-video-prop-tab {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 6px 4px;
|
||||
cursor: pointer;
|
||||
color: var(--wfm-text-secondary);
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wfm-video-prop-tab.active {
|
||||
color: var(--wfm-text);
|
||||
border-bottom-color: var(--wfm-primary);
|
||||
}
|
||||
|
||||
.wfm-video-prop-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
+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");
|
||||
|
||||
+62
-4
@@ -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";
|
||||
@@ -91,7 +112,7 @@ function initTabs() {
|
||||
|
||||
function applyI18nToHtml() {
|
||||
// Tab labels
|
||||
const tabMap = { workflow: "tabWorkflow", nodes: "tabNodes", models: "tabModels", generate: "tabGenerate", prompt: "tabPrompt", metadata: "tabMetadata", gallery: "tabGallery", "image-edit": "tabImageEdit", settings: "tabSettings", help: "tabHelp", ai: "tabAi", tagger: "tabTagger" };
|
||||
const tabMap = { workflow: "tabWorkflow", nodes: "tabNodes", models: "tabModels", generate: "tabGenerate", prompt: "tabPrompt", gallery: "tabGallery", "image-edit": "tabImageEdit", video: "tabVideo", settings: "tabSettings", help: "tabHelp", ai: "tabAi", tagger: "tabTagger" };
|
||||
document.querySelectorAll(".wfm-tab").forEach((tab) => {
|
||||
const key = tabMap[tab.dataset.tab];
|
||||
if (key) tab.textContent = t(key);
|
||||
@@ -113,6 +134,12 @@ function applyI18nToHtml() {
|
||||
if (listLoadBtn.disabled) listLoadBtn.title = t("selectCardFirst");
|
||||
}
|
||||
|
||||
const listLoadVideoBtn = document.getElementById("wfm-list-load-video-btn");
|
||||
if (listLoadVideoBtn) {
|
||||
listLoadVideoBtn.textContent = t("loadInVideo");
|
||||
if (listLoadVideoBtn.disabled) listLoadVideoBtn.title = t("selectCardFirst");
|
||||
}
|
||||
|
||||
const listOpenComfyBtn = document.getElementById("wfm-list-open-comfyui-btn");
|
||||
if (listOpenComfyBtn) {
|
||||
listOpenComfyBtn.textContent = t("sendToCanvas");
|
||||
@@ -195,6 +222,7 @@ function applyI18nToHtml() {
|
||||
if (btn.dataset.detailTab === "info") btn.textContent = t("modelsSideInfo");
|
||||
if (btn.dataset.detailTab === "meta") btn.textContent = t("gallerySideMetadata");
|
||||
if (btn.dataset.detailTab === "group") btn.textContent = t("modelsSideGroup");
|
||||
if (btn.dataset.detailTab === "prompt") btn.textContent = t("gallerySidePrompt");
|
||||
});
|
||||
const galleryBulkDeselect = document.getElementById("wfm-gallery-bulk-deselect");
|
||||
if (galleryBulkDeselect) galleryBulkDeselect.textContent = t("galleryBulkDeselectAll");
|
||||
@@ -229,6 +257,8 @@ function applyI18nToHtml() {
|
||||
if (galleryImagePromptSubtab) galleryImagePromptSubtab.textContent = t("galleryTabImagePrompt");
|
||||
const galleryStyleCatalogSubtab = document.getElementById("wfm-gallery-subtab-styleCatalog");
|
||||
if (galleryStyleCatalogSubtab) galleryStyleCatalogSubtab.textContent = t("galleryTabStyleCatalog");
|
||||
const galleryMetadataSubtab = document.getElementById("wfm-gallery-subtab-metadata");
|
||||
if (galleryMetadataSubtab) galleryMetadataSubtab.textContent = t("galleryTabMetadata");
|
||||
|
||||
// ImagePrompt ギャラリーパネル
|
||||
const imagePromptSearch = document.getElementById("wfm-imageprompt-search");
|
||||
@@ -455,6 +485,8 @@ 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-gen-30": "helpGen30",
|
||||
"wfm-help-feeder-title": "helpFeederTitle",
|
||||
"wfm-help-feeder-desc": "helpFeederDesc",
|
||||
"wfm-help-feeder-imgloop-title": "helpFeederImgloopTitle",
|
||||
@@ -480,6 +512,8 @@ 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-18": "helpLab18", "wfm-help-lab-19": "helpLab19",
|
||||
"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",
|
||||
@@ -499,6 +533,15 @@ function applyI18nToHtml() {
|
||||
"wfm-help-prompt-10": "helpPrompt10", "wfm-help-prompt-11": "helpPrompt11",
|
||||
"wfm-help-prompt-12": "helpPrompt12", "wfm-help-prompt-13": "helpPrompt13",
|
||||
"wfm-help-prompt-14": "helpPrompt14",
|
||||
"wfm-help-prompt-card-toplevel-title": "helpPromptCardToplevel",
|
||||
"wfm-help-prompt-15": "helpPrompt15",
|
||||
"wfm-help-prompt-card-table-title": "helpPromptCardTable",
|
||||
"wfm-help-prompt-16": "helpPrompt16", "wfm-help-prompt-17": "helpPrompt17",
|
||||
"wfm-help-prompt-18": "helpPrompt18", "wfm-help-prompt-19": "helpPrompt19",
|
||||
"wfm-help-prompt-20": "helpPrompt20", "wfm-help-prompt-21": "helpPrompt21",
|
||||
"wfm-help-prompt-22": "helpPrompt22", "wfm-help-prompt-23": "helpPrompt23",
|
||||
"wfm-help-prompt-24": "helpPrompt24", "wfm-help-prompt-25": "helpPrompt25",
|
||||
"wfm-help-prompt-26": "helpPrompt26",
|
||||
"wfm-help-settings-1": "helpSettings1", "wfm-help-settings-2": "helpSettings2",
|
||||
"wfm-help-settings-3": "helpSettings3", "wfm-help-settings-4": "helpSettings4",
|
||||
"wfm-help-settings-5": "helpSettings5", "wfm-help-settings-6": "helpSettings6",
|
||||
@@ -506,6 +549,7 @@ function applyI18nToHtml() {
|
||||
"wfm-help-settings-9": "helpSettings9", "wfm-help-settings-10": "helpSettings10",
|
||||
"wfm-help-settings-11": "helpSettings11", "wfm-help-settings-12": "helpSettings12",
|
||||
"wfm-help-settings-13": "helpSettings13", "wfm-help-settings-14": "helpSettings14",
|
||||
"wfm-help-settings-15": "helpSettings15", "wfm-help-settings-16": "helpSettings16",
|
||||
"wfm-help-nodes-1": "helpNodes1", "wfm-help-nodes-2": "helpNodes2",
|
||||
"wfm-help-nodes-3": "helpNodes3", "wfm-help-nodes-4": "helpNodes4",
|
||||
"wfm-help-nodes-5": "helpNodes5", "wfm-help-nodes-6": "helpNodes6",
|
||||
@@ -525,11 +569,12 @@ function applyI18nToHtml() {
|
||||
"wfm-help-gallery-11": "helpGallery11", "wfm-help-gallery-12": "helpGallery12",
|
||||
"wfm-help-gallery-13": "helpGallery13", "wfm-help-gallery-14": "helpGallery14",
|
||||
"wfm-help-gallery-15": "helpGallery15", "wfm-help-gallery-16": "helpGallery16",
|
||||
"wfm-help-gallery-17": "helpGallery17",
|
||||
"wfm-help-gallery-17": "helpGallery17", "wfm-help-gallery-27": "helpGallery27",
|
||||
"wfm-help-gallery-18": "helpGallery18", "wfm-help-gallery-19": "helpGallery19",
|
||||
"wfm-help-gallery-20": "helpGallery20", "wfm-help-gallery-21": "helpGallery21",
|
||||
"wfm-help-gallery-22": "helpGallery22", "wfm-help-gallery-23": "helpGallery23",
|
||||
"wfm-help-gallery-24": "helpGallery24", "wfm-help-gallery-25": "helpGallery25",
|
||||
"wfm-help-gallery-26": "helpGallery26",
|
||||
"wfm-help-gallery-card-styleprompt-title": "helpGalleryCardStyleprompt",
|
||||
"wfm-help-gallery-card-stylecatalog-title": "helpGalleryCardStylecatalog",
|
||||
"wfm-help-tagger-title": "helpTaggerTitle",
|
||||
@@ -577,7 +622,7 @@ function applyI18nToHtml() {
|
||||
"wfm-help-imageedit-12": "helpImageEdit12", "wfm-help-imageedit-13": "helpImageEdit13",
|
||||
"wfm-help-imageedit-14": "helpImageEdit14", "wfm-help-imageedit-15": "helpImageEdit15",
|
||||
"wfm-help-imageedit-16": "helpImageEdit16", "wfm-help-imageedit-17": "helpImageEdit17",
|
||||
"wfm-help-imageedit-18": "helpImageEdit18", "wfm-help-imageedit-19": "helpImageEdit19",
|
||||
"wfm-help-imageedit-18": "helpImageEdit18",
|
||||
"wfm-help-imageedit-19b": "helpImageEdit19b",
|
||||
"wfm-help-imageedit-20": "helpImageEdit20", "wfm-help-imageedit-21": "helpImageEdit21",
|
||||
"wfm-help-imageedit-22": "helpImageEdit22", "wfm-help-imageedit-23": "helpImageEdit23",
|
||||
@@ -686,6 +731,12 @@ function applyI18nToHtml() {
|
||||
"wfm-help-trouble-card-connection-title": "helpTroubleCardConnection",
|
||||
"wfm-help-trouble-card-workflow-title": "helpTroubleCardWorkflow",
|
||||
"wfm-help-trouble-card-tagger-title": "helpTroubleCardTagger",
|
||||
"wfm-help-video-title": "helpVideoTitle",
|
||||
"wfm-help-video-desc": "helpVideoDesc",
|
||||
"wfm-help-video-1": "helpVideo1", "wfm-help-video-2": "helpVideo2",
|
||||
"wfm-help-video-3": "helpVideo3", "wfm-help-video-4": "helpVideo4",
|
||||
"wfm-help-video-5": "helpVideo5", "wfm-help-video-6": "helpVideo6",
|
||||
"wfm-help-video-7": "helpVideo7",
|
||||
};
|
||||
for (const [id, key] of Object.entries(helpIdMap)) {
|
||||
const el = document.getElementById(id);
|
||||
@@ -768,6 +819,7 @@ import { initWorkflowTab } from "./workflow-tab.js";
|
||||
import { initNodesTab } from "./nodes-tab.js";
|
||||
import { initGenerateTab } from "./generate-tab.js";
|
||||
import { initPromptTab } from "./prompt-tab.js";
|
||||
import { initPromptTableTab } from "./prompt-table.js";
|
||||
import { initSettingsTab, applyTheme, getSavedTheme, applyTextareaFontSize, applyJsonColors, applyModelTabActiveColor } from "./settings-tab.js";
|
||||
import { initModelsTab } from "./models-tab.js";
|
||||
import { initGalleryTab } from "./gallery-tab.js";
|
||||
@@ -775,6 +827,7 @@ import { initMetadataTab } from "./metadata-tab.js";
|
||||
import { initAiTab } from "./ai-tab.js";
|
||||
import { initTaggerTab } from "./tagger-tab.js";
|
||||
import { imageEditTab } from "./image-edit-tab.js";
|
||||
import { initVideoTab } from "./video-tab.js";
|
||||
|
||||
// Apply saved theme immediately to prevent flash of default theme
|
||||
applyTheme(getSavedTheme());
|
||||
@@ -802,6 +855,9 @@ function initHelpTab() {
|
||||
document.querySelectorAll(".wfm-help-jump-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelector(`.wfm-tab[data-tab="${btn.dataset.jumpTab}"]`)?.click();
|
||||
if (btn.dataset.jumpSubtab) {
|
||||
document.querySelector(`.wfm-gallery-subtab-btn[data-gallery-subtab="${btn.dataset.jumpSubtab}"]`)?.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -817,11 +873,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initModelsTab();
|
||||
initGenerateTab();
|
||||
initPromptTab();
|
||||
initPromptTableTab();
|
||||
initMetadataTab();
|
||||
initGalleryTab();
|
||||
initAiTab();
|
||||
initTaggerTab();
|
||||
imageEditTab.init();
|
||||
initVideoTab();
|
||||
|
||||
console.log("Workflow Studio: App initialized");
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
@@ -1335,6 +1336,19 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// Video all-in-one nodes (e.g. MiniMaxH3ImageToVideo) — hold their own "prompt"
|
||||
// widget directly instead of going through a separate CLIPTextEncode, and have
|
||||
// no negative counterpart. Matched generically by class name (ImageToVideo /
|
||||
// TextToVideo) rather than a fixed list so future models sharing this pattern
|
||||
// (WAN, HunyuanVideo, etc.) work without a code change — same detection metadata-tab.js
|
||||
// and node_sets_menu.js already use for prompt extraction (see extractPromptsAPI).
|
||||
if ((ct.includes("ImageToVideo") || ct.includes("TextToVideo")) && typeof inputs.prompt === "string") {
|
||||
result.prompt_nodes.push({
|
||||
id, type: ct, title, role: "positive",
|
||||
text: inputs.prompt, textKey: "prompt",
|
||||
});
|
||||
}
|
||||
|
||||
// --- Latent nodes ---
|
||||
// EmptyFlux2LatentImage (Flux.2 templates) often has width/height wired to a
|
||||
// GetImageSize node (auto-follows the reference image) rather than a direct number —
|
||||
@@ -1349,6 +1363,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
|
||||
@@ -1504,6 +1532,14 @@ export const comfyWorkflow = {
|
||||
return result;
|
||||
},
|
||||
|
||||
// True if this analysis found a video all-in-one node (see analyzeWorkflow's
|
||||
// ImageToVideo/TextToVideo prompt_nodes branch). Callers use this to pick a longer
|
||||
// generation timeout (video generation runs far longer than a still image) — see
|
||||
// generate-tab.js _coreGenerate and lab-tab.js _runLabBatch.
|
||||
isVideoWorkflow(analysis) {
|
||||
return (analysis?.prompt_nodes || []).some((n) => /ImageToVideo|TextToVideo/.test(n.type));
|
||||
},
|
||||
|
||||
applyParams(workflow, params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
// Parse key format: nodeId.paramName
|
||||
|
||||
+156
-18
@@ -5,7 +5,7 @@
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { loadFileIntoMetadataTab } from "./metadata-tab.js";
|
||||
import { loadFileIntoMetadataTab, extractPrompts, buildPromptItem } from "./metadata-tab.js";
|
||||
import { loadWorkflowIntoEditor } from "./generate-tab.js";
|
||||
import { escapeHtml, setupSearchClearBtn, getAiBackendDefaultUrl } from "./util.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
@@ -46,6 +46,13 @@ const API = {
|
||||
|
||||
export const FEEDER_GROUP = "__Feeder__";
|
||||
|
||||
// 拡張子から動画ファイル(mp4)かどうかを判定する。
|
||||
// 一覧アイテムは path/filename、詳細メタ(get_image_metadata)は ext フィールドを持つため両対応。
|
||||
function isVideoFile(img) {
|
||||
const source = img?.ext || img?.filename || img?.path || "";
|
||||
return source.toLowerCase().endsWith(".mp4");
|
||||
}
|
||||
|
||||
// ComfyUI Comic Creater からiframe越しに画像を受け取り、Generate UIのImage入力スロットへ直接セットする(I2I連携)。
|
||||
// Comic Creater側の「I2Iへ送る」ボタンから
|
||||
// iframe.contentWindow._wfmReceiveImageForI2I(blob, name, workflowData?, workflowFilename?) として呼ばれる。
|
||||
@@ -253,6 +260,13 @@ function formatDate(mtime) {
|
||||
return new Date(mtime * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const total = Math.round(seconds);
|
||||
const m = Math.floor(total / 60);
|
||||
const s = total % 60;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
async function openImageInMetadataTab(img) {
|
||||
try {
|
||||
const res = await fetch(API.serveImage(img.path));
|
||||
@@ -578,6 +592,14 @@ function createThumbCard(img) {
|
||||
};
|
||||
card.appendChild(imgEl);
|
||||
|
||||
// 動画ファイルは再生アイコンバッジを重ねて表示(サムネイル自体は先頭フレームのJPEG)
|
||||
if (isVideoFile(img)) {
|
||||
const playBadge = document.createElement("div");
|
||||
playBadge.className = "wfm-gallery-thumb-video-badge";
|
||||
playBadge.textContent = "▶";
|
||||
card.appendChild(playBadge);
|
||||
}
|
||||
|
||||
// お気に入りトグルボタン(カード右上)
|
||||
const favBtn = document.createElement("button");
|
||||
favBtn.className = `wfm-gallery-thumb-fav-btn${img.favorite ? " active" : ""}`;
|
||||
@@ -877,18 +899,18 @@ async function loadImageDetail(img) {
|
||||
if (moveBtn) moveBtn.disabled = false;
|
||||
if (delBtn) delBtn.disabled = false;
|
||||
|
||||
// プレビュー
|
||||
// プレビュー(mp4は<video controls>、それ以外は<img>)
|
||||
const preview = document.getElementById("wfm-gallery-detail-preview");
|
||||
const isVideo = isVideoFile(img);
|
||||
const mediaHtml = isVideo
|
||||
? `<video src="${API.serveImage(img.path)}" class="wfm-gallery-detail-img" controls title="Double-click to enlarge"></video>`
|
||||
: `<img src="${API.serveImage(img.path)}" class="wfm-gallery-detail-img" alt="${escapeHtml(img.filename)}" title="Double-click to enlarge">`;
|
||||
preview.innerHTML = `
|
||||
<div class="wfm-gallery-preview-wrapper">
|
||||
<img src="${API.serveImage(img.path)}" class="wfm-gallery-detail-img" alt="${escapeHtml(img.filename)}" title="Double-click to enlarge">
|
||||
<div class="wfm-gallery-preview-overlay">
|
||||
<button class="wfm-gallery-download-btn" title="Download image">⬇</button>
|
||||
</div>
|
||||
${mediaHtml}
|
||||
</div>
|
||||
`;
|
||||
preview.querySelector("img").addEventListener("dblclick", () => openLightbox(img));
|
||||
preview.querySelector(".wfm-gallery-download-btn").addEventListener("click", () => downloadImage(img));
|
||||
preview.querySelector(isVideo ? "video" : "img").addEventListener("dblclick", () => openLightbox(img));
|
||||
|
||||
// ファイル名
|
||||
document.getElementById("wfm-gallery-detail-filename").textContent = img.filename;
|
||||
@@ -914,16 +936,32 @@ async function loadImageDetail(img) {
|
||||
renderWorkflowJson(state.embeddedWorkflow);
|
||||
_updateCopyCanvasBtn();
|
||||
renderImagePromptSection(metaRes.image_prompt);
|
||||
renderDimensionInfo(metaRes);
|
||||
// Promptタブは prompt_workflow (API形式優先) を使う。トップレベルとサブグラフに
|
||||
// 独立した複数系統を持つワークフローでは workflow (UI形式) からの抽出だと
|
||||
// トップレベル系統しか拾えないことがあるため(Metadataタブと同じ優先順位に揃える)。
|
||||
renderPromptTab(wfRes.prompt_workflow ?? state.embeddedWorkflow);
|
||||
} catch (e) {
|
||||
renderWorkflowJson(null);
|
||||
_updateCopyCanvasBtn();
|
||||
renderImagePromptSection(null);
|
||||
renderDimensionInfo(null);
|
||||
renderPromptTab(null);
|
||||
}
|
||||
|
||||
// グループタブ更新
|
||||
renderDetailGroup(img);
|
||||
}
|
||||
|
||||
function renderDimensionInfo(metaRes) {
|
||||
const el = document.getElementById("wfm-gallery-info-dim");
|
||||
if (!el) return;
|
||||
const parts = [];
|
||||
if (metaRes?.width && metaRes?.height) parts.push(`${metaRes.width}×${metaRes.height}`);
|
||||
if (metaRes?.duration) parts.push(formatDuration(metaRes.duration));
|
||||
el.textContent = parts.length ? parts.join(" · ") : "";
|
||||
}
|
||||
|
||||
function renderImagePromptSection(imagePrompt) {
|
||||
const section = document.getElementById("wfm-gallery-image-prompt-section");
|
||||
const text = document.getElementById("wfm-gallery-image-prompt-text");
|
||||
@@ -964,6 +1002,44 @@ function renderWorkflowJson(workflow) {
|
||||
}
|
||||
}
|
||||
|
||||
// 埋め込みワークフローからpositive/negativeプロンプトを抽出してPromptタブに表示する。
|
||||
// 複数候補があるケース(PromptStyler等で正負が複数生成される場合)に対応するため、
|
||||
// Metadataタブと同じ POS/NEGリスト → クリックで全文表示のUI(buildPromptItem)をそのまま
|
||||
// 再利用する。選択画像を切り替えるたびにマウス操作無しで読めるよう、先頭項目を自動選択する。
|
||||
function renderPromptTab(workflow) {
|
||||
const listEl = document.getElementById("wfm-gallery-prompt-list");
|
||||
const fullArea = document.getElementById("wfm-gallery-prompt-full");
|
||||
const fullLabel = document.getElementById("wfm-gallery-prompt-full-label");
|
||||
if (!listEl || !fullArea || !fullLabel) return;
|
||||
|
||||
listEl.innerHTML = "";
|
||||
fullArea.value = "";
|
||||
fullLabel.textContent = "";
|
||||
|
||||
if (!workflow) {
|
||||
listEl.innerHTML = `<div class="wfm-meta-item" style="opacity:0.6;">No workflow embedded in this image.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const { positives, negatives, texts } = extractPrompts(workflow);
|
||||
const allPrompts = [
|
||||
...positives.map(p => ({ type: "positive", text: p })),
|
||||
...negatives.map(p => ({ type: "negative", text: p })),
|
||||
...(texts ?? []).map(p => ({ type: "text", text: p })),
|
||||
];
|
||||
|
||||
if (allPrompts.length === 0) {
|
||||
listEl.innerHTML = `<div class="wfm-meta-item" style="opacity:0.6;">No prompt found.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { type, text } of allPrompts) {
|
||||
listEl.appendChild(buildPromptItem(text, type, text, fullArea, fullLabel, listEl));
|
||||
}
|
||||
// Auto-select first item (positive prompts come first in allPrompts)
|
||||
listEl.querySelector(".wfm-meta-item-clickable")?.click();
|
||||
}
|
||||
|
||||
// ── タグ操作 ─────────────────────────────────────────────────
|
||||
|
||||
async function addTag(tag) {
|
||||
@@ -971,7 +1047,8 @@ async function addTag(tag) {
|
||||
const tags = [...(state.selectedImage.tags || [])];
|
||||
if (tags.includes(tag.trim())) return;
|
||||
tags.push(tag.trim());
|
||||
await saveMetaField({ tags });
|
||||
const ok = await saveMetaField({ tags });
|
||||
if (!ok) return;
|
||||
state.selectedImage.tags = tags;
|
||||
renderTagsDisplay(tags);
|
||||
updateTagFilter(state.images);
|
||||
@@ -980,24 +1057,38 @@ async function addTag(tag) {
|
||||
async function removeTag(tag) {
|
||||
if (!state.selectedImage) return;
|
||||
const tags = (state.selectedImage.tags || []).filter(t => t !== tag);
|
||||
await saveMetaField({ tags });
|
||||
const ok = await saveMetaField({ tags });
|
||||
if (!ok) return;
|
||||
state.selectedImage.tags = tags;
|
||||
renderTagsDisplay(tags);
|
||||
updateTagFilter(state.images);
|
||||
}
|
||||
|
||||
// 戻り値: 保存が実際にサーバーへ永続化されたか(true/false)。呼び出し元はこれを見て
|
||||
// ローカル状態(state.selectedImage等)の更新可否を判断すること — でないと、サーバーが
|
||||
// 保存を拒否した場合にもUI上は成功したように見えてしまう(リロードすると消える不具合の原因)。
|
||||
async function saveMetaField(fields) {
|
||||
if (!state.selectedImage) return;
|
||||
if (!state.selectedImage) return false;
|
||||
try {
|
||||
await fetch(API.saveImageMeta, {
|
||||
const res = await fetch(API.saveImageMeta, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path: state.selectedImage.path, ...fields }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
// サーバー側は常にHTTP 200を返し、成否は body.ok (bool) で表す設計
|
||||
// (gallery_routes.py save_image_meta)。ここをチェックしないと、
|
||||
// 許可パス外などでサーバーが保存を拒否してもUI上は成功したように見えてしまう。
|
||||
if (json.error || json.ok === false) {
|
||||
throw new Error(json.error || "Save rejected by server");
|
||||
}
|
||||
const idx = state.images.findIndex(i => i.path === state.selectedImage.path);
|
||||
if (idx >= 0) Object.assign(state.images[idx], fields);
|
||||
return true;
|
||||
} catch (e) {
|
||||
showToast(t("saveFailed", e.message), "error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1540,9 +1631,12 @@ async function bulkSetFavorite(favoriteValue) {
|
||||
function openLightbox(img) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "wfm-gallery-lightbox";
|
||||
const mediaHtml = isVideoFile(img)
|
||||
? `<video src="${API.serveImage(img.path)}" class="wfm-gallery-lightbox-img" controls autoplay></video>`
|
||||
: `<img src="${API.serveImage(img.path)}" class="wfm-gallery-lightbox-img" alt="${escapeHtml(img.filename)}">`;
|
||||
overlay.innerHTML = `
|
||||
<div class="wfm-gallery-lightbox-inner">
|
||||
<img src="${API.serveImage(img.path)}" class="wfm-gallery-lightbox-img" alt="${escapeHtml(img.filename)}">
|
||||
${mediaHtml}
|
||||
<div class="wfm-gallery-lightbox-footer">${escapeHtml(img.filename)}</div>
|
||||
<button class="wfm-gallery-lightbox-close">×</button>
|
||||
</div>
|
||||
@@ -1564,12 +1658,17 @@ function openCompare(paths) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "wfm-gallery-lightbox wfm-lightbox-compare";
|
||||
|
||||
const itemsHtml = imgs.map(img => `
|
||||
const itemsHtml = imgs.map(img => {
|
||||
const mediaHtml = isVideoFile(img)
|
||||
? `<video src="${API.serveImage(img.path)}" class="wfm-lightbox-compare-img" controls></video>`
|
||||
: `<img src="${API.serveImage(img.path)}" class="wfm-lightbox-compare-img" alt="${escapeHtml(img.filename)}" loading="lazy">`;
|
||||
return `
|
||||
<div class="wfm-lightbox-compare-item">
|
||||
<img src="${API.serveImage(img.path)}" class="wfm-lightbox-compare-img" alt="${escapeHtml(img.filename)}" loading="lazy">
|
||||
${mediaHtml}
|
||||
<div class="wfm-lightbox-compare-label">${escapeHtml(img.filename)}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="wfm-lightbox-compare-inner">
|
||||
@@ -1636,7 +1735,7 @@ function onGalleryTabActivated() {
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
// ── サブタブ切替 (Output / ImagePrompt / Style_Catalog) ────────────────────────
|
||||
// ── サブタブ切替 (Output / ImagePrompt / Style_Catalog / Metadata) ────────────
|
||||
|
||||
function _initGallerySubtabToggle() {
|
||||
document.querySelectorAll(".wfm-gallery-subtab-btn").forEach((btn) => {
|
||||
@@ -1647,6 +1746,7 @@ function _initGallerySubtabToggle() {
|
||||
document.getElementById("wfm-gallery-panel-output")?.classList.toggle("active", target === "output");
|
||||
document.getElementById("wfm-gallery-panel-imageprompt")?.classList.toggle("active", target === "imageprompt");
|
||||
document.getElementById("wfm-gallery-panel-styleCatalog")?.classList.toggle("active", target === "styleCatalog");
|
||||
document.getElementById("wfm-gallery-panel-metadata")?.classList.toggle("active", target === "metadata");
|
||||
if (target === "imageprompt") activateImagePromptTab();
|
||||
if (target === "styleCatalog") activateStyleCatalogTab();
|
||||
});
|
||||
@@ -1707,6 +1807,34 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
// 選択画像をComfyUIキャンバス上で選択中のノード(LoadImage系)のimageウィジェットへ送信。
|
||||
// Image Editタブの「Send to LI node」と同じ橋渡し(window.opener.wfmSendImageToSelectedNode、
|
||||
// node_sets_menu.js側で定義)を使う。Galleryの画像はGalleryルート配下の任意フォルダにあり得るため、
|
||||
// GenUI送信と違いLoadImageノードの有無に依存しない — /upload/imageでinputフォルダへ
|
||||
// アップロードしてから、そのファイル名を書き込む。
|
||||
document.getElementById("wfm-gallery-send-li-node-btn")?.addEventListener("click", async () => {
|
||||
if (!state.selectedImage) {
|
||||
showToast(t("gallerySelectImageFirst"), "info");
|
||||
return;
|
||||
}
|
||||
if (!window.opener || typeof window.opener.wfmSendImageToSelectedNode !== "function") {
|
||||
showToast(t("sendToLiNodeNoOpener"), "error");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(API.serveImage(state.selectedImage.path));
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], state.selectedImage.filename || "gallery-image.png", { type: blob.type || "image/png" });
|
||||
const result = await comfyUI.uploadImage(file, file.name);
|
||||
const filename = result.subfolder ? `${result.subfolder}/${result.name}` : result.name;
|
||||
window.opener.wfmSendImageToSelectedNode(filename);
|
||||
showToast(t("sendToLiNodeSuccess", filename), "success");
|
||||
} catch (e) {
|
||||
showToast(t("errorWithMsg", e.message), "error");
|
||||
}
|
||||
});
|
||||
|
||||
// 選択画像を ComfyUI Comic Creator の選択コマ/オーバーレイへ送信
|
||||
// (ComfyUI Comic CreaterからこのGalleryタブがiframe埋め込みされている場合のみ表示・動作する)
|
||||
const sendCcBtn = document.getElementById("wfm-gallery-send-cc-btn");
|
||||
@@ -1840,7 +1968,7 @@ function bindEvents() {
|
||||
// メモ保存
|
||||
document.getElementById("wfm-gallery-memo-save-btn")?.addEventListener("click", () => {
|
||||
const memo = document.getElementById("wfm-gallery-memo").value;
|
||||
saveMetaField({ memo }).then(() => showToast(t("memoSaved"), "success"));
|
||||
saveMetaField({ memo }).then((ok) => { if (ok) showToast(t("memoSaved"), "success"); });
|
||||
});
|
||||
|
||||
// ワークフローコピー&キャンバスへ送る
|
||||
@@ -1973,6 +2101,16 @@ function bindEvents() {
|
||||
openImageInTaggerTab(state.selectedImage);
|
||||
});
|
||||
|
||||
// Downloadボタン: プレビューのオーバーレイだと動画の再生コントロール操作を妨げるため、
|
||||
// アクションボタン行に常時表示のボタンとして配置している
|
||||
document.getElementById("wfm-gallery-download-btn")?.addEventListener("click", () => {
|
||||
if (!state.selectedImage) {
|
||||
showToast(t("gallerySelectImageFirst"), "error");
|
||||
return;
|
||||
}
|
||||
downloadImage(state.selectedImage);
|
||||
});
|
||||
|
||||
// Load in GenerateUI ボタン: 埋め込みワークフローをGenerateUIタブに読み込む
|
||||
document.getElementById("wfm-gallery-load-genui-btn")?.addEventListener("click", async () => {
|
||||
if (!state.selectedImage) {
|
||||
|
||||
+67
-16
@@ -11,6 +11,7 @@ import { syncJsonHighlight, syncScroll } from "./json-highlight.js";
|
||||
import { initFeederTab, refreshFeederNodeList } from "./feeder-tab.js";
|
||||
import { initLabTab, refreshLabLiveDefaults } from "./lab-tab.js";
|
||||
import { getSettings, readJsonStorage, escapeHtml, getEagleSettings, saveToEagle } from "./util.js";
|
||||
import { isVideoFilename } from "./video-utils.js";
|
||||
|
||||
// ============================================
|
||||
// Gallery Metadata - ワークフロー保存
|
||||
@@ -164,6 +165,17 @@ async function saveCurrentWorkflow() {
|
||||
nameEl.dataset.filename = filename;
|
||||
}
|
||||
}
|
||||
|
||||
// このファイル名が「デフォルトワークフロー」(workflow-tab.jsの「デフォルトに設定」で
|
||||
// localStorageにスナップショットされたAPI形式JSON、ページ起動時に自動読込される)に
|
||||
// 一致する場合、保存内容と食い違ったまま残らないようスナップショットも更新する。
|
||||
// 更新しないと、ここで上書き保存しても次回リロード時に古いモデル等が復活する。
|
||||
const settings = getSettings();
|
||||
if (settings.defaultWorkflow === filename) {
|
||||
settings.defaultWorkflowData = workflow;
|
||||
localStorage.setItem("wfm_settings", JSON.stringify(settings));
|
||||
}
|
||||
|
||||
showToast(t("savedAs", filename), "success");
|
||||
} catch (err) {
|
||||
showToast(t("saveFailed", err.message), "error");
|
||||
@@ -1443,26 +1455,38 @@ 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 resultVideo = document.getElementById("wfm-gen-result-video");
|
||||
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...";
|
||||
|
||||
// Style must be applied *before* wildcard expansion so a __name__ reference written
|
||||
// inside a Style's prompt/negative_prompt also gets expanded, not just wildcards
|
||||
// already present in the base workflow's prompt nodes.
|
||||
const baseWorkflow = workflowOverride || comfyUI.currentWorkflow;
|
||||
const workflowExpanded = await _expandWildcardsInWorkflow({ ...baseWorkflow });
|
||||
const workflowForGenerate = _applyStyleToWorkflow(workflowExpanded);
|
||||
const workflowStyled = _applyStyleToWorkflow({ ...baseWorkflow });
|
||||
const workflowForGenerate = await _expandWildcardsInWorkflow(workflowStyled);
|
||||
// Video workflows (e.g. MiniMax H3) run far longer than a still image — use a longer
|
||||
// timeout so a multi-minute generation doesn't get force-rejected mid-run (see
|
||||
// comfyWorkflow.isVideoWorkflow and video-tab.js's own 30-minute timeout).
|
||||
const isVideo = comfyWorkflow.isVideoWorkflow(comfyUI.currentAnalysis);
|
||||
const { images, seed, svgOutputs } = await comfyUI.generate(
|
||||
workflowForGenerate,
|
||||
{
|
||||
seedMode,
|
||||
seedValue,
|
||||
timeoutMs: isVideo ? 30 * 60 * 1000 : undefined,
|
||||
onProgress: (pct) => {
|
||||
if (progressBar) progressBar.style.width = `${(pct * 100).toFixed(1)}%`;
|
||||
if (progressText) progressText.textContent = `${(pct * 100).toFixed(0)}%`;
|
||||
@@ -1508,23 +1532,36 @@ async function _coreGenerate(silent = false, workflowOverride = null) {
|
||||
});
|
||||
}
|
||||
} else if (outputImages.length > 0) {
|
||||
// Video outputs (SaveVideo/PreviewVideo, e.g. MiniMax H3) come back through the
|
||||
// same {"images":[...]} shape as still images — dispatch to <video> vs <img>
|
||||
// per-item by extension rather than assuming the whole batch is one media type.
|
||||
const showMedia = (blob, filename) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
if (isVideoFilename(filename)) {
|
||||
if (resultVideo) { resultVideo.src = url; resultVideo.style.display = "block"; }
|
||||
if (resultImg) resultImg.style.display = "none";
|
||||
} else {
|
||||
if (resultImg) { resultImg.src = url; resultImg.style.display = "block"; }
|
||||
if (resultVideo) { resultVideo.pause(); resultVideo.style.display = "none"; }
|
||||
}
|
||||
};
|
||||
|
||||
const blob = await comfyUI.getImageBlob(outputImages[0]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
if (resultImg) {
|
||||
resultImg.src = url;
|
||||
resultImg.style.display = "block";
|
||||
}
|
||||
showMedia(blob, outputImages[0].filename);
|
||||
|
||||
if (resultThumbs && outputImages.length > 1) {
|
||||
resultThumbs.innerHTML = "";
|
||||
for (let i = 0; i < outputImages.length; i++) {
|
||||
const b = i === 0 ? blob : await comfyUI.getImageBlob(outputImages[i]);
|
||||
const u = i === 0 ? url : URL.createObjectURL(b);
|
||||
const thumb = document.createElement("img");
|
||||
const filename = outputImages[i].filename;
|
||||
const u = URL.createObjectURL(b);
|
||||
const isVid = isVideoFilename(filename);
|
||||
const thumb = document.createElement(isVid ? "video" : "img");
|
||||
thumb.src = u;
|
||||
if (isVid) thumb.muted = true;
|
||||
thumb.className = `wfm-gen-thumb ${i === 0 ? "active" : ""}`;
|
||||
thumb.addEventListener("click", () => {
|
||||
resultImg.src = u;
|
||||
showMedia(b, filename);
|
||||
resultThumbs.querySelectorAll(".wfm-gen-thumb").forEach((t) => t.classList.remove("active"));
|
||||
thumb.classList.add("active");
|
||||
});
|
||||
@@ -1972,9 +2009,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 +2042,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";
|
||||
@@ -2146,6 +2185,10 @@ export async function initGenerateTab() {
|
||||
// Lab has its own Run/Results panel — hide the shared Generate/Seed/Batch column
|
||||
const rightCol = document.querySelector(".wfm-gen-right");
|
||||
if (rightCol) rightCol.style.display = (target === "lab") ? "none" : "";
|
||||
// Save button writes the currently loaded workflow as-is — not meaningful while
|
||||
// browsing Batch/Lab (which operate on workflow copies / their own Plan files)
|
||||
const saveBtn = document.getElementById("wfm-gen-save-btn");
|
||||
if (saveBtn) saveBtn.style.display = (target === "batch" || target === "lab") ? "none" : "";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2156,6 +2199,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", () => {
|
||||
|
||||
+348
-42
@@ -93,6 +93,7 @@ const LANGUAGES = {
|
||||
copyAndSendCanvas: "Copy & Send Canvas",
|
||||
workflowSentToCanvas: "Workflow ready — drag 'Workflow Studio Library' title to canvas",
|
||||
workflowSentToCanvasDirect: "Workflow sent to canvas",
|
||||
workflowFilteredInLibrary: "Filtered in Workflow Studio Library (W tab) — drag the card onto the canvas",
|
||||
comfyUILoadTimeout: "ComfyUI load timeout",
|
||||
popupBlocked: "Popup blocked. Please allow popups for this site.",
|
||||
setAsDefault: "Set as Default",
|
||||
@@ -236,6 +237,7 @@ const LANGUAGES = {
|
||||
labBatchCountTooSmall: "Increase the Batch count first to add a keyframe beyond it",
|
||||
labKeyframesExceedBatch: (maxIter, batchCount) => `Some keyframes are set up to iteration #${maxIter}, beyond the current Batch count (${batchCount}) — they won't be applied until Batch count is raised again`,
|
||||
labUseWorkflowDefault: "(workflow default)",
|
||||
labNoNodeForColumn: "No matching node in this workflow",
|
||||
labInheritsFrom: (val) => `(inherits: ${val})`,
|
||||
labLoraNoNode: "No LoRA node found in the workflow — LoRA setting was skipped",
|
||||
labAppliedAt: (n) => `Applied: ${n}`,
|
||||
@@ -244,12 +246,17 @@ 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",
|
||||
labNoWorkflowLoaded: "Load a workflow into GenerateUI first",
|
||||
labNoSourceImage: "Drop a source image first",
|
||||
labNoResultsYet: "No results yet",
|
||||
labLastFrameExtractFailed: (msg) => `Failed to extract last frame from generated video: ${msg}`,
|
||||
labResultPreview: "Preview",
|
||||
labEnterPlanName: "Plan name:",
|
||||
labPlanSaved: "Plan saved",
|
||||
@@ -281,6 +288,15 @@ const LANGUAGES = {
|
||||
labT2IImageIgnored: "T2I mode is on — image upload is not needed and was skipped",
|
||||
labIndexImageSaved: (filename) => `Index image saved to Output: ${filename}`,
|
||||
labIndexImageSaveFailed: "Failed to save the index image to Output",
|
||||
labPromptGroupBtnTitle: "Apply a Prompt Group",
|
||||
labPromptGroupModalTitle: "Prompt Group",
|
||||
labPromptGroupEmpty: "No groups yet. Create groups in the Prompt tab's group panel.",
|
||||
labPromptGroupCount: (n) => `${n} preset(s)`,
|
||||
labPromptGroupSelectFirst: "Select a group first",
|
||||
labPromptGroupNoPresets: "This group has no presets",
|
||||
labPromptGroupApplied: (name, n) => `Applied "${name}" (${n} keyframes) to Prompt`,
|
||||
labPromptGroupBatchRaised: (n) => `Batch count raised to ${n} to fit the group`,
|
||||
labSelectKeyframeFirst: "Select a keyframe first",
|
||||
|
||||
// -- GenerateUI Input tab: Image sub-panel placeholder --
|
||||
i2iPlaceholderTitle: "Placeholder Image",
|
||||
@@ -547,6 +563,7 @@ const LANGUAGES = {
|
||||
modelsSideGroup: "Group",
|
||||
modelsSideCivitai: "CivitAI",
|
||||
gallerySideMetadata: "Metadata",
|
||||
gallerySidePrompt: "Prompt",
|
||||
|
||||
// -- Gallery --
|
||||
galleryAllTags: "All Tags",
|
||||
@@ -560,12 +577,15 @@ const LANGUAGES = {
|
||||
gallerySelectImageFirst: "Please select an image first",
|
||||
galleryNoLoadImageNode: "No LoadImage node in current workflow",
|
||||
gallerySentGenUI: "Sent to GenerateUI Image tab",
|
||||
sendToLiNodeNoOpener: "Open Workflow Studio from ComfyUI's top menu to use this feature",
|
||||
sendToLiNodeSuccess: (filename) => `Sent "${filename}" to LI node`,
|
||||
galleryCCNotAvailable: "Open this Gallery from ComfyUI Comic Creator to use Send CC",
|
||||
gallerySentCC: "Sent to ComfyUI Comic Creator",
|
||||
galleryNoEmbeddedWorkflow: "No ComfyUI workflow embedded in this image.",
|
||||
galleryTabOutput: "Output",
|
||||
galleryTabImagePrompt: "ImagePrompt",
|
||||
galleryTabStyleCatalog: "Style_Catalog",
|
||||
galleryTabMetadata: "Metadata",
|
||||
galleryNoSelection: "No selection",
|
||||
imagePromptSelectedPrompt: "Prompt",
|
||||
imagePromptNoPrompt: "No prompt data for this image.",
|
||||
@@ -783,6 +803,8 @@ 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",
|
||||
helpGen30: "The Save button (right end of the subtab bar) is hidden while the Batch or Lab subtab is active, since those work on workflow copies / their own Plan files rather than the currently loaded workflow; it reappears on Input/Model/Settings/Feeder",
|
||||
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 +843,12 @@ 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",
|
||||
helpLab18: "A single click on a keyframe cell selects it (blue outline) instead of opening it — double-click to open the edit modal as before. Each column's header (Model / Prompt / Ksampler) also has ▲ / ▼ buttons that swap the selected keyframe with its immediate neighbor to reorder them without retyping anything; keyframe #1 (the baseline) can never move, and nothing can be swapped into its slot",
|
||||
helpLab19: "Prompt column header's PG button: pick one of the Prompt tab's preset groups (Table view's Presets Group, the same groups used by the Batch tab's checklist) and replace the entire Prompt column with one keyframe per preset in that group's order, starting at #1 (#1 is overwritten, not appended after). If the group has more presets than the current Batch count, Batch count is raised automatically to fit (capped at 50, the Batch input's own limit); anything still beyond that shows the usual \"keyframes beyond Batch count\" warning instead of being silently dropped",
|
||||
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",
|
||||
@@ -855,6 +881,18 @@ const LANGUAGES = {
|
||||
helpPrompt12: "Renaming a style keeps its position in whichever file it was originally defined in; new styles are saved into a dedicated custom.json inside the style folder so the bundled style packs are never modified directly; a name must be unique across all styles",
|
||||
helpPrompt13: "Saving, renaming, or deleting a style immediately refreshes GenerateUI's Style dropdown, the Batch tab's style checklist, and the Style Catalog's name list — no reload needed",
|
||||
helpPrompt14: "Each entry in the list shows its source file as a small badge next to the name (e.g. sdxl_styles_diva.json); while editing an existing style, that filename also appears above the Positive prompt field, and a \"+ Add to this file\" button appears next to Save — click it to clear the form for a brand-new style that gets appended to that same file instead of the default custom.json",
|
||||
helpPrompt15: "Two top-level tabs: Form is the original one-item-at-a-time editor described below (AI Assistant / Presets / Wildcard / Style panes); Table shows the same underlying data all at once as editable spreadsheets — useful once there are more than a handful of presets, wildcard files, or styles to manage",
|
||||
helpPrompt16: "The Table tab has four sub-tabs of its own: Presets, Presets Group, Wildcards, and Style — each an editable table built from the exact same data as the Form tab, so changes on either side stay in sync",
|
||||
helpPrompt17: "Every table shares a left-most auto-numbered # column: click a number to select that row (highlighted red) — multiple rows can be selected at once; the toolbar's Delete button removes every selected row in one confirm, and Deselect clears the selection. This replaced per-row delete buttons, which were too easy to misclick",
|
||||
helpPrompt18: "Double-click a row's number to open a larger prompt-editing modal instead of the small in-table textarea. Presets and Style show Positive/Negative toggle buttons above one shared textarea (switching preserves whatever you typed on the other side); Wildcards shows just the file's content. Save writes the change back and closes the modal; Close discards it",
|
||||
helpPrompt19: "The toolbar's + New ... button adds a blank row at the top with small ✓ (Add) / ✕ (Cancel) icons in place of its number; fill in the fields and click ✓ to create it, or ✕ to discard the row without saving",
|
||||
helpPrompt20: "Presets table — the ★ (favorite) and B (Batch group) columns work like toggle switches: click to turn them gold/yellow, click again to clear; the toolbar's BC button clears every preset out of the Batch group at once, same as the Form tab's Preset Manager",
|
||||
helpPrompt21: "Presets table toolbar — PP Copy / NP Copy copy the single selected row's positive/negative prompt to the clipboard; the group select + Add to Group adds every currently selected preset to the chosen group in one click. Doing the group assignment from here (search/select presets, then pick from a short group list) replaced the old per-group preset picker, which became unusable once there were many presets",
|
||||
helpPrompt22: "Presets Group table — create, rename, and delete groups; each group's members are shown as removable badges (✕). The reserved Batch group is not listed here — it's managed entirely through the Presets table's B column and BC button instead",
|
||||
helpPrompt23: "Wildcards table — same files as the Form tab's Wildcard file manager; editing the File path or the Ext dropdown renames the file (saves the content under the new name, then removes the old file)",
|
||||
helpPrompt24: "Style table — same styles as the Form tab's Style manager, editable inline the same way as Presets",
|
||||
helpPrompt25: "Presets table toolbar — the ★ and B sort buttons (next to the search box) bring favorited / batch-selected presets to the top of the list; both can be enabled together (★ takes priority, B breaks ties), and everything else keeps its original relative order",
|
||||
helpPrompt26: "Style table toolbar — select exactly one row whose File badge isn't custom.json to enable + Add to this file: it adds a blank row targeting that same file (shown as a badge in the File column even before saving) — the same capability as the Form tab's Style manager (see above); custom.json entries and multi-row selections leave the button disabled, use the regular + New Style button for those instead",
|
||||
helpSettings1: "UI language and summary language",
|
||||
helpSettings2: "Theme selection (13 presets) with customization: color overrides, background patterns (stripes, polka dot, checkerboard, SVG tiling), and font selection (16 Google Fonts including Japanese display fonts)",
|
||||
helpSettings3: "Workflow data folder path",
|
||||
@@ -862,7 +900,9 @@ const LANGUAGES = {
|
||||
helpSettings5: "AI Assistant (Prompt Tab): backend (Ollama / LM Studio / Lemonade), API URL, default model, and connection test",
|
||||
helpSettings6: "Default workflow setting",
|
||||
helpSettings7: "Eagle integration URL and auto-save toggle",
|
||||
helpSettings8: "Data Management: export all plugin data (settings, metadata, prompts, etc.) to a JSON file, or import to restore. After importing, restart ComfyUI.",
|
||||
helpSettings8: "Data Management: export core settings, metadata, and prompts to a JSON file, or import to restore. After importing, restart ComfyUI.",
|
||||
helpSettings15: "Full Backup (ZIP): unlike the JSON export above, also includes folder-based data (AI Skills, Lab Plans, Style presets) and the Tagger database — everything the JSON export can't represent. Restoring overwrites existing files; restart ComfyUI afterward.",
|
||||
helpSettings16: "Two opt-in checkboxes above the Full Backup buttons additionally include ComfyUI's default workflows folder and the Impact Pack's wildcard folder in the export — both export-only, never restored automatically; copy the ZIP's _external/ folder back by hand if you want them.",
|
||||
helpSettings9: "All sections are collapsible accordion panels",
|
||||
helpSettings10: "Wildcard Integration: link WFS wildcard dir to ComfyUI-Impact-Pack wildcards (directory junction on Windows / symlink on other OS); existing files migrated automatically",
|
||||
helpSettings11: "Text Size: one slider (10–28 px) adjusts font size for all prompt and chat textareas at once — Generate UI positive/negative prompts, AI Assistant chat input, Preset positive/negative, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings",
|
||||
@@ -895,17 +935,18 @@ const LANGUAGES = {
|
||||
helpGallery4: "Toolbar filters: favorites (★), text search (filename / tag / prompt), tag filter, group filter, sort by date/name",
|
||||
helpGallery5: "Folder tree: click to load images, expand with ▶; '+ New' creates a subfolder inside the selected folder, 'Del' deletes it with all contents",
|
||||
helpGallery6: "Multi-select: Ctrl+click to toggle individual images; Shift+click to range-select from the last clicked item — bulk bar: select group → 'Add to Group' / 'Remove from Group'; 'Favorite All' / 'Unfavorite All'; 'Compare' (2–4 images); 'Move To...'; 'Delete File'",
|
||||
helpGallery7: "Detail panel — Info tab: filename, size, date, tags (add/remove), memo; 'Move To...' moves the image to another folder, 'Delete File' removes it permanently",
|
||||
helpGallery7: "Detail panel — Info tab: filename, size, dimensions (image width×height) or video duration, date, tags (add/remove), memo; 'Move To...' moves the image to another folder, 'Delete File' removes it permanently",
|
||||
helpGallery8: "Detail panel — JSON tab: displays workflow JSON from PNG embedded data (prompt / workflow keys) or from metadata saved by the Generate UI tab; 'Copy & Send Canvas' button copies to clipboard and sends the workflow directly to the ComfyUI canvas (requires Workflow Studio to be opened from the ComfyUI toolbar; UI-format only falls back to title drag if opened from a bookmark)",
|
||||
helpGallery9: "Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image",
|
||||
helpGallery10: "Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the JSON tab",
|
||||
helpGallery11: "Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata",
|
||||
helpGallery11: "Supports PNG (tEXt/iTXt), JPEG, WebP, GIF, MP4; workflow extraction reads ComfyUI prompt/workflow keys embedded in PNG metadata or in MP4 container tags (as written by SaveVideo, e.g. MiniMax H3 output)",
|
||||
helpGallery12: "Metadata button (green, in the detail panel tab bar): opens the selected image in the Metadata tab for full embedded-data inspection; Alt+click on any image thumbnail has the same effect",
|
||||
helpGallery13: "Load GenUI button (blue, in the detail panel tab bar): loads the embedded ComfyUI workflow from the selected image directly into the GenerateUI tab and switches to it; shows a warning toast if no workflow is embedded or the format is not supported",
|
||||
helpGallery14: "Tagger button (purple, in the detail panel action row): opens the selected image directly in the Tagger tab's Single mode for instant tagging",
|
||||
helpGallery15: "F button (cyan overlay, top-left of each thumbnail): toggle the image in/out of the __Feeder__ group with a single click; cyan and always visible when in group, visible on hover only when not; quickly queue images for the Feeder tab Gallery mode",
|
||||
helpGallery16: "Image Edit button (toolbar, next to Refresh): sends the currently selected image to the Image Edit tab; if layers already exist the image is added as a new layer scaled to fit the canvas, otherwise it becomes Layer 1",
|
||||
helpGallery17: "Send GenUI Image button (toolbar, next to Image Edit): uploads the selected image to ComfyUI and sets it as the input for the first LoadImage node in the GenerateUI tab; automatically switches to GenerateUI → Input → Image; requires a workflow with at least one LoadImage node to be loaded first",
|
||||
helpGallery27: "Send to LI node button (toolbar, next to Send GenUI Image): uploads the selected image and writes its filename into the image widget of the currently selected node on the ComfyUI canvas (falling back to the first LoadImage-type node found) — same destination as Image Edit tab's Send to LI node; requires Workflow Studio to have been opened from ComfyUI's top menu button",
|
||||
helpGallery18: "Second sub-tab next to Output (top of the Gallery tab) for browsing a visual prompt library — manages the ws_image_prompt folder inside the ComfyUI output directory (created automatically)",
|
||||
helpGallery19: "Same 3-column layout: folder tree (left) | thumbnails (center) | prompt builder (right). Click a thumbnail to preview it and its prompt text — this does not add it to the prompt yet",
|
||||
helpGallery20: "\"+ Add\" appends the previewed image's prompt as a chip under \"Selected Prompts\"; chips can be removed individually (×) or all at once (\"Clear All\"); \"Save\" edits and persists the prompt text for that image",
|
||||
@@ -914,6 +955,7 @@ const LANGUAGES = {
|
||||
helpGallery23: "Same 3-column layout: folder tree (left) | thumbnails (center) | Positive/Negative prompt panel (right). Click a thumbnail to read the Positive/Negative prompt actually embedded in that generated image, each with its own Copy button",
|
||||
helpGallery24: "Select as Style button: matches the selected image's filename (without extension) against a registered Style name and switches the GenerateUI tab's Style dropdown to it — a visual shortcut into the existing named-Style system, not a one-off apply of the embedded prompt (if the Style was renamed or deleted since the catalog image was created, a \"no matching style\" toast is shown instead)",
|
||||
helpGallery25: "Load in GenerateUI / Open in Metadata Tab buttons: read the workflow embedded in the catalog image itself (the catalog copy carries the same PNG metadata as the original output image) and either load it straight into the GenerateUI editor or open the image in the Metadata tab for inspection — no need to hunt down the original file in the Output folder",
|
||||
helpGallery26: "Four sub-tabs at the top of the Gallery tab: Output (this browser), ImagePrompt, Style_Catalog, and Metadata — the last one embeds the full Metadata Tab (see its own help page) so you can drop an image there to inspect its embedded model/prompt data without leaving Gallery",
|
||||
|
||||
// -- Tagger Tab --
|
||||
helpTaggerTitle: "Tagger Tab",
|
||||
@@ -940,7 +982,7 @@ const LANGUAGES = {
|
||||
helpSidepanel10: "Search within each sub-tab to quickly find items",
|
||||
helpSidepanel11: "Models tab: All / ★ Favorites / Groups / By Type sub-tabs; Prompts tab: All / ★ Favorites / Categories sub-tabs + Groups sub-tab (row 2); Models Groups — LoRA groups show an \"All N LoRAs\" item to drag all into a single Lora Loader (LoraManager) node",
|
||||
helpSidepanel12: "⚙ Theme button (header): customize panel background, sub-header background, text, border, and secondary text colors — changes are saved to localStorage and applied on next open",
|
||||
helpSidepanel13: "I tab (Information): drop a ComfyUI PNG/WebP/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF, QuadrupleCLIPLoader, and Lora Loader (LoraManager) node types",
|
||||
helpSidepanel13: "I tab (Information): drop a ComfyUI PNG/WebP/MP4/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF, QuadrupleCLIPLoader, and Lora Loader (LoraManager) node types; MP4 reads the workflow/prompt tags embedded by SaveVideo, including all-in-one video nodes like MiniMax H3 ImageToVideo",
|
||||
helpSidepanel14: "Information tab — drag items to canvas: model sub-tab places the loader node (Checkpoint → CheckpointLoaderSimple, VAE → VAELoader, Diffusion Model → UNETLoader, Text Encoder → CLIPLoader); double-click also places at canvas center",
|
||||
helpSidepanel15: "Information tab — LoRA sub-tab: detects LoRAs from LoraLoader, LoraLoaderModelOnly, and Lora Loader (LoraManager) nodes (API format supported); drag individual LoRA to place LoraLoader; \"Multiple LORA\" section places all LoRAs in a single Lora Loader (LoraManager) node with LoRA syntax pre-filled; Prompts sub-tab: drag a prompt to place CLIPTextEncode node with text pre-filled; click prompt to view full text and copy",
|
||||
helpSidepanel16: "A tab (AI Tools): Translation, Chat, TOOLS, and Settings sub-tabs powered by Ollama, LM Studio, Lemonade, or Unsloth; Chat supports multi-turn conversations; TOOLS includes VLM image analysis and wildcard generation; settings shared with the SPA AI TOOL tab via localStorage",
|
||||
@@ -948,7 +990,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",
|
||||
@@ -976,10 +1018,12 @@ const LANGUAGES = {
|
||||
helpGenCardBatchTypes: "Batch Tab — Batch Types",
|
||||
helpGenCardLora: "Model Tab — LoRA Details",
|
||||
helpPromptTitle: "Prompt Tab",
|
||||
helpPromptCardToplevel: "Form / Table",
|
||||
helpPromptCardAi: "AI Assistant & Translation",
|
||||
helpPromptCardPresets: "Presets",
|
||||
helpPromptCardWildcards: "Wildcards",
|
||||
helpPromptCardStyleTitle: "Style",
|
||||
helpPromptCardTable: "Table View",
|
||||
helpMetadataCardOverview: "Overview",
|
||||
helpMetadataCardExtracted: "Extracted Information",
|
||||
helpMetadataCardFormats: "Supported Formats",
|
||||
@@ -1043,8 +1087,7 @@ const LANGUAGES = {
|
||||
helpImageEditExportTitle: "Export & Navigation",
|
||||
helpImageEdit17: "Save PNG: composites all visible layers and downloads the result as a PNG file",
|
||||
helpImageEdit18: "Save to Gallery: prompts for a filename (default: wfs-image-YYYYMMDDHHmmss) and saves the composite PNG directly to the Gallery root folder (configured in Settings > Gallery Output Folder); requires the Gallery tab to have been opened at least once to set the root",
|
||||
helpImageEdit19: "Save to Input: uploads the composite PNG to ComfyUI's input folder via /upload/image, making it selectable in Load Image nodes",
|
||||
helpImageEdit19b: "Send to Workflow: uploads the composite PNG (same as Save to Input) and additionally writes the resulting filename into the image widget of the currently selected node in the ComfyUI graph, falling back to the first LoadImage-type node found; requires Workflow Studio to have been opened from ComfyUI's top menu button so the ComfyUI tab is reachable",
|
||||
helpImageEdit19b: "Send to LI node: uploads the composite PNG to ComfyUI's input folder via /upload/image and writes the resulting filename into the image widget of the currently selected node in the ComfyUI graph, falling back to the first LoadImage-type node found; requires Workflow Studio to have been opened from ComfyUI's top menu button so the ComfyUI tab is reachable",
|
||||
helpImageEdit20: "Canvas navigation: scroll wheel to zoom, Space + drag to pan",
|
||||
helpImageEdit21: "Undo / Redo: Ctrl+Z / Ctrl+Y (or toolbar buttons ↩ ↪)",
|
||||
helpImageEditKbdTitle: "Image Edit Keyboard Shortcuts",
|
||||
@@ -1166,13 +1209,20 @@ const LANGUAGES = {
|
||||
eagleUrl: "Eagle URL",
|
||||
eagleAutoSave: "Auto-save generated images to Eagle",
|
||||
dataManagement: "Data Management",
|
||||
dataManagementHint: "Export all plugin data (settings, metadata, prompts, etc.) to a JSON file, or import a previously exported file to restore data. After importing, restart ComfyUI to apply changes.",
|
||||
dataManagementHint: "Export core settings, metadata, and prompts to a JSON file, or import a previously exported file to restore them. After importing, restart ComfyUI to apply changes.",
|
||||
exportData: "Export",
|
||||
importData: "Import",
|
||||
exportSuccess: "Export complete",
|
||||
exportError: "Export failed",
|
||||
importSuccess: "Import complete",
|
||||
importError: "Import failed",
|
||||
exportFullBackup: "Full Backup (ZIP)",
|
||||
importFullBackup: "Restore Full Backup (ZIP)",
|
||||
fullBackupHint: "Full Backup (ZIP) additionally includes folder-based data (AI Skills, Lab Plans, Style presets) and the Tagger database — everything the JSON export above can't represent. Restoring overwrites existing files; restart ComfyUI afterward.",
|
||||
fullBackupFilesUnit: "files",
|
||||
fullBackupIncludeWorkflows: "Include default workflows folder (user/default/workflows)",
|
||||
fullBackupIncludeWildcard: "Include Impact Pack wildcard folder (if present)",
|
||||
fullBackupExternalHint: "These are export-only — restoring a backup never touches them; copy the _external/ folder back manually if needed.",
|
||||
saveSettings: "Save Settings",
|
||||
settingsSaved: "Settings saved",
|
||||
ollamaSaved: "AI Assistant settings saved",
|
||||
@@ -1241,8 +1291,8 @@ const LANGUAGES = {
|
||||
metaSectionTe: "Text Encoder",
|
||||
metaSectionLora: "LoRA",
|
||||
metaSectionPrompt: "Prompt",
|
||||
metaDropMain: "Drop image / JSON",
|
||||
metaDropSub: "PNG · WebP · JSON",
|
||||
metaDropMain: "Drop image / video / JSON",
|
||||
metaDropSub: "PNG · WebP · MP4 · JSON",
|
||||
metaParsing: "Parsing...",
|
||||
metaFileTooLarge: "File too large (max 50 MB)",
|
||||
metaNoMetadata: "No metadata found",
|
||||
@@ -1252,7 +1302,7 @@ const LANGUAGES = {
|
||||
metaPromptText: "Text",
|
||||
metaSelectPrompt: "Select a prompt above to view full text...",
|
||||
metaFormatNoteTitle: "Supported formats",
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / MP4 / JSON",
|
||||
metaFmtSdwebui: "SD WebUI / SD Forge — PNG (parameters)",
|
||||
metaFmtFooocus: "Fooocus — PNG (fooocus_scheme)",
|
||||
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image subgraph workflows supported",
|
||||
@@ -1262,7 +1312,59 @@ const LANGUAGES = {
|
||||
helpMetadata2: "Center column: Checkpoint, VAE, Diffusion Model, and Text Encoder lists extracted from the workflow",
|
||||
helpMetadata3: "Right column: LoRA list (with strength values) and Prompt list (click to view full text below)",
|
||||
helpMetadata4: "Prompt actions — Copy: copy to clipboard; GenUI:P/N: set GenerateUI positive/negative prompt; Prompt:P/N: set Prompt tab preset positive/negative",
|
||||
helpMetadata5: "Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image subgraph workflows), SD WebUI, SD Forge, Fooocus.",
|
||||
helpMetadata5: "Supported: ComfyUI PNG/WebP/MP4/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image subgraph workflows; MP4 reads the workflow/prompt container tags written by SaveVideo, including all-in-one video nodes like MiniMax H3 ImageToVideo), SD WebUI, SD Forge, Fooocus.",
|
||||
helpMetadata6: "This is now a sub-tab inside the Gallery tab (to the right of Style_Catalog), rather than its own top-level tab — everything else about it works exactly the same",
|
||||
|
||||
// -- Video Tab (MiniMax H3, independent of GenerateUI) --
|
||||
tabVideo: "Video",
|
||||
loadInVideo: "Load in Video",
|
||||
videoTitle: "Video (MiniMax H3)",
|
||||
videoFirstFrame: "First Frame (optional)",
|
||||
videoLastFrame: "Last Frame (optional)",
|
||||
videoDropOrClick: "Drop image or click to select",
|
||||
videoPrompt: "Prompt",
|
||||
videoAspectRatio: "Aspect Ratio",
|
||||
videoMegapixels: "Megapixels",
|
||||
videoMultiple: "Multiple",
|
||||
videoDuration: "Duration (seconds)",
|
||||
videoNoiseSeed: "Noise Seed",
|
||||
videoRandomizeSeed: "Randomize",
|
||||
videoGenerate: "Generate",
|
||||
videoEditPlaceholder: "Editing tools coming soon",
|
||||
videoNoWorkflowLoaded: "No workflow loaded",
|
||||
videoUnsupportedWorkflow: "This workflow does not contain a MiniMax H3 Image-to-Video node",
|
||||
videoWorkflowLoaded: (name) => `Workflow loaded: ${name}`,
|
||||
videoGenerateSuccess: "Video generated",
|
||||
videoDoneCount: (n) => `Done (${n} file${n === 1 ? "" : "s"})`,
|
||||
videoSourceLabel: "Video Source",
|
||||
videoSourceHint: "Load any video here to use Frame/GIF tools without generating one.",
|
||||
videoSourceDropLabel: "Drop video or click to select",
|
||||
videoNoVideo: "No video",
|
||||
videoPropFrame: "Frame",
|
||||
videoPropGif: "GIF",
|
||||
videoFrameHint: "Play/seek the preview video to the position you want, then capture.",
|
||||
videoCaptureFrame: "Capture Frame",
|
||||
videoSaveToOutput: "Save to Output",
|
||||
videoFrameSaved: (name) => `Frame saved: ${name}`,
|
||||
videoFrameCaptureFailed: (msg) => `Failed to capture frame: ${msg}`,
|
||||
videoGifStart: "Start (s)",
|
||||
videoGifEnd: "End (s)",
|
||||
videoGifFps: "FPS",
|
||||
videoGifWidth: "Max Width",
|
||||
videoConvertToGif: "Convert to GIF",
|
||||
videoGifConverting: "Converting...",
|
||||
videoGifDoneCount: (n) => `Done (${n} frame${n === 1 ? "" : "s"})`,
|
||||
videoGifSaved: (name) => `GIF saved: ${name}`,
|
||||
videoNoSourceLoaded: "Load a video first (generate one, or drop a file into Video Source)",
|
||||
helpVideoTitle: "Video Tab",
|
||||
helpVideoDesc: "A dedicated video generation tab for the MiniMax H3 (Image to Video) workflow, independent of the GenerateUI tab.",
|
||||
helpVideo1: "Set first_frame (optional) / last_frame (optional) images, prompt, aspect_ratio / megapixels / multiple (resolution), duration (seconds), and noise_seed, then click Generate. Leaving first_frame empty runs the workflow text-only (bypassed), and workflows with no LoadImage node at all are also supported.",
|
||||
helpVideo2: "Model selection (UNet / CLIP / VAE / Audio VAE) is fixed to the loaded workflow's defaults and cannot be changed from this tab.",
|
||||
helpVideo3: "Use the \"Load in Video\" button in the Workflow tab's detail modal to load a workflow containing a MiniMax H3 Image-to-Video node. Unsupported workflows show an error instead.",
|
||||
helpVideo4: "Generated videos (mp4) can also be browsed and played back in the Gallery tab.",
|
||||
helpVideo5: "The Video Source panel next to the preview lets you drop or pick any local video (not just a generated one) to use the Frame/GIF tools below without running MiniMax H3 first; Clear resets it back to empty.",
|
||||
helpVideo6: "Frame tab (right panel): play/seek the preview video to the position you want, click Capture Frame to grab that frame client-side, then Save to Output to write it as a PNG into ComfyUI's output folder.",
|
||||
helpVideo7: "GIF tab (right panel): set Start/End (seconds), FPS, and an optional Max Width, then Convert to GIF — the server decodes the video and encodes an animated GIF into ComfyUI's output folder.",
|
||||
},
|
||||
|
||||
ja: {
|
||||
@@ -1352,6 +1454,7 @@ const LANGUAGES = {
|
||||
copyAndSendCanvas: "コピー&キャンバスへ送る",
|
||||
workflowSentToCanvas: "ワークフロー準備完了 — タイトルをキャンバスへドラッグ",
|
||||
workflowSentToCanvasDirect: "ワークフローをキャンバスへ送信しました",
|
||||
workflowFilteredInLibrary: "Workflow Studio Library(Wタブ)で絞り込みました — カードをキャンバスへドラッグしてください",
|
||||
comfyUILoadTimeout: "ComfyUIの読み込みがタイムアウトしました",
|
||||
popupBlocked: "ポップアップがブロックされました。このサイトのポップアップを許可してください。",
|
||||
setAsDefault: "デフォルトに設定",
|
||||
@@ -1495,6 +1598,7 @@ const LANGUAGES = {
|
||||
labBatchCountTooSmall: "先にBatch数を増やしてからキーフレームを追加してください",
|
||||
labKeyframesExceedBatch: (maxIter, batchCount) => `${maxIter}回目まで設定されたキーフレームがありますが、現在のBatch数(${batchCount})を超えているため、Batch数を増やすまで適用されません`,
|
||||
labUseWorkflowDefault: "(ワークフローの設定のまま)",
|
||||
labNoNodeForColumn: "このワークフローに該当ノードがありません",
|
||||
labInheritsFrom: (val) => `(引き継ぎ: ${val})`,
|
||||
labLoraNoNode: "ワークフロー内にLoRAノードが見つかりません — LoRA設定はスキップされました",
|
||||
labAppliedAt: (n) => `適用:${n}`,
|
||||
@@ -1503,12 +1607,17 @@ const LANGUAGES = {
|
||||
labRevertCheckboxLabel: "1回目の設定に戻す",
|
||||
labBypassed: "⏭ バイパス中",
|
||||
labBypassCheckboxLabel: "この設定をバイパス(直前まで有効だった設定を維持)",
|
||||
labLoraNodeBypassed: "⛔(ノードをバイパス)",
|
||||
labLoraNodeBypassCheckboxLabel: "LoRAノードをバイパス(強度を0にする)",
|
||||
labLoraApplyToPromptCheckboxLabel: "LoRA構文とトリガーワードをポジティブプロンプトに適用",
|
||||
labLoraPromptAppliedTag: "→P",
|
||||
labSeedEmptyRandom: "Seed(空欄でランダム)",
|
||||
labPause: "一時停止",
|
||||
labResume: "再開",
|
||||
labNoWorkflowLoaded: "先にGenerateUIでワークフローを読み込んでください",
|
||||
labNoSourceImage: "先に元になる画像をドロップしてください",
|
||||
labNoResultsYet: "まだ結果がありません",
|
||||
labLastFrameExtractFailed: (msg) => `生成された動画から最終フレームの抽出に失敗しました: ${msg}`,
|
||||
labResultPreview: "プレビュー",
|
||||
labEnterPlanName: "プラン名:",
|
||||
labPlanSaved: "プランを保存しました",
|
||||
@@ -1540,6 +1649,15 @@ const LANGUAGES = {
|
||||
labT2IImageIgnored: "T2Iモードが有効なため、画像のアップロードは不要としてスキップしました",
|
||||
labIndexImageSaved: (filename) => `インデックス画像をOutputへ保存しました: ${filename}`,
|
||||
labIndexImageSaveFailed: "インデックス画像のOutputへの保存に失敗しました",
|
||||
labPromptGroupBtnTitle: "プロンプトグループを適用",
|
||||
labPromptGroupModalTitle: "プロンプトグループ",
|
||||
labPromptGroupEmpty: "グループがまだありません。Promptタブのグループパネルで作成してください。",
|
||||
labPromptGroupCount: (n) => `プリセット${n}件`,
|
||||
labPromptGroupSelectFirst: "先にグループを選択してください",
|
||||
labPromptGroupNoPresets: "このグループにはプリセットがありません",
|
||||
labPromptGroupApplied: (name, n) => `「${name}」(${n}件のキーフレーム)をPromptに適用しました`,
|
||||
labPromptGroupBatchRaised: (n) => `グループに合わせてBatch数を${n}に増やしました`,
|
||||
labSelectKeyframeFirst: "先にキーフレームを選択してください",
|
||||
|
||||
// -- GenerateUIタブ Inputサブタブ: Image内プレースホルダ --
|
||||
i2iPlaceholderTitle: "プレースホルダ画像",
|
||||
@@ -1806,6 +1924,7 @@ const LANGUAGES = {
|
||||
modelsSideGroup: "グループ",
|
||||
modelsSideCivitai: "CivitAI",
|
||||
gallerySideMetadata: "メタデータ",
|
||||
gallerySidePrompt: "プロンプト",
|
||||
|
||||
// -- Gallery --
|
||||
galleryAllTags: "すべてのタグ",
|
||||
@@ -1819,12 +1938,15 @@ const LANGUAGES = {
|
||||
gallerySelectImageFirst: "先に画像を選択してください",
|
||||
galleryNoLoadImageNode: "現在のワークフローにLoadImageノードがありません",
|
||||
gallerySentGenUI: "GenerateUI Imageタブへ送信しました",
|
||||
sendToLiNodeNoOpener: "この機能を使うにはComfyUIのトップメニューからWorkflow Studioを起動してください",
|
||||
sendToLiNodeSuccess: (filename) => `"${filename}" をLIノードへ送信しました`,
|
||||
galleryCCNotAvailable: "Send CCはComfyUI Comic Creator内のギャラリーから開いた場合のみ使用できます",
|
||||
gallerySentCC: "ComfyUI Comic Creatorへ送信しました",
|
||||
galleryNoEmbeddedWorkflow: "この画像にはComfyUIワークフローが埋め込まれていません。",
|
||||
galleryTabOutput: "Output",
|
||||
galleryTabImagePrompt: "ImagePrompt",
|
||||
galleryTabStyleCatalog: "Style_Catalog",
|
||||
galleryTabMetadata: "メタデータ",
|
||||
galleryNoSelection: "選択なし",
|
||||
imagePromptSelectedPrompt: "プロンプト",
|
||||
imagePromptNoPrompt: "この画像にはプロンプトデータがありません。",
|
||||
@@ -2042,6 +2164,8 @@ 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ボタンをクリックするとバイパス状態はクリアされ、後から無言で再適用されることはありません",
|
||||
helpGen30: "Saveボタン(サブタブバー右端)は、BatchまたはLabサブタブがアクティブな間は非表示になります — これらは現在読み込み中のワークフロー本体ではなく、ワークフローの複製や独自のPlanファイルを操作するためです。Input/Model/Settings/Feederでは再表示されます",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder サブタブ",
|
||||
helpFeederDesc: "左ペイン上部の [Image Loop] / [Gallery] トグルで2つのモードを切り替えられます。",
|
||||
helpFeederImgloopTitle: "Image Loop モード",
|
||||
@@ -2080,8 +2204,12 @@ 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ノードをバイパス」がチェックされている間は自動的に無効化されます",
|
||||
helpLab18: "キーフレームセルはシングルクリックで選択(青い枠線)に変わり、開くにはダブルクリックが必要になりました。各カラムのヘッダー(Model/Prompt/Ksampler)には▲/▼ボタンもあり、選択中のキーフレームを隣接するものと入れ替えて、入力し直すことなく並び順を変更できます。キーフレーム#1(ベースライン)は移動できず、他のキーフレームが#1の位置に入れ替わることもありません",
|
||||
helpLab19: "Promptカラムヘッダーの PG ボタン:Promptタブのプリセットグループ(Tableビューの Presets Group、Batchタブのチェックリストと同じグループ)を1つ選ぶと、Prompt列全体をそのグループの並び順で1プリセットにつき1キーフレームへ置き換えます(#1から開始、#1は上書きで追加ではありません)。グループのプリセット数が現在のBatch数を超える場合はBatch数を自動的に引き上げます(上限50、Batch入力欄自体の上限と同じ)。それでも収まらない分は静かに削除されず、通常の「Batch数を超えたキーフレーム」警告が表示されます",
|
||||
helpLabCardPlanTitle: "Settingサブタブ — 実行とプランファイル",
|
||||
helpLabPlan1: "右ペイン — Batch:実行する回数。Note:プランと一緒に保存される自由記述のメモ",
|
||||
helpLabPlan2: "Run/Pause/Cancel:Runでバッチループを開始、Pause/Resumeで回と回の間を一時停止・再開、Cancelで現在の生成を中断してループを止めます",
|
||||
@@ -2114,6 +2242,18 @@ const LANGUAGES = {
|
||||
helpPrompt12: "リネームしても、元々定義されていたファイル内での位置は保たれる。新規作成したStyleはstyleフォルダ内の専用custom.jsonへ保存されるため、同梱のスタイルパックファイルは直接変更されない。名前は全スタイル中で一意である必要がある",
|
||||
helpPrompt13: "Styleの保存・リネーム・削除は、GenerateUIのStyleドロップダウン、Batchタブのスタイルチェックリスト、Style Catalogの名前一覧に即座に反映される(再読み込み不要)",
|
||||
helpPrompt14: "一覧の各項目には、名前の横に定義元ファイル名が小さなバッジで表示される(例: sdxl_styles_diva.json)。既存スタイルを編集中は、そのファイル名がPositive promptフィールドの上にも表示され、Saveの隣に「+ Add to this file」ボタンが現れる。クリックするとフォームが空になり、デフォルトのcustom.jsonではなく同じファイルへ追加される新規スタイルを作成できる",
|
||||
helpPrompt15: "上部の2つのトップレベルタブ:Formは下記で説明する従来の一件ずつ編集する画面(AI Assistant / Presets / Wildcard / Style の各パネル)。Tableは同じデータを一覧編集可能な表形式で全件表示 — プリセット・ワイルドカードファイル・スタイルが多くなってきた場合に便利",
|
||||
helpPrompt16: "Tableタブにはさらに4つのサブタブがあります:Presets・Presets Group・Wildcards・Style — いずれもFormタブと全く同じデータを参照する編集可能なテーブルなので、どちらで変更しても両方に反映されます",
|
||||
helpPrompt17: "各テーブルは左端に自動採番の#列を共有します。番号をクリックするとその行が選択(赤くハイライト)され、複数行を同時選択可能。ツールバーのDeleteボタンで選択した行をまとめて1回の確認で削除、Deselectで選択解除。誤操作しやすかった行ごとの削除ボタンを廃止して置き換えたものです",
|
||||
helpPrompt18: "行番号をダブルクリックすると、テーブル内の小さなテキストエリアの代わりに、より大きなプロンプト編集モーダルが開きます。Presets・Styleは共通の1つのテキストエリアの上にPositive/Negative切り替えボタンが表示され(切り替えても入力済みのテキストは両方とも保持されます)、Wildcardsはファイルの内容のみを表示します。Saveで保存してモーダルを閉じ、Closeは変更を破棄します",
|
||||
helpPrompt19: "ツールバーの「+ New ...」ボタンで先頭に空の編集可能な行が追加され、番号の代わりに小さな✓(Add)/✕(Cancel)アイコンが表示されます。入力してから✓をクリックすると作成、✕で保存せずに破棄します",
|
||||
helpPrompt20: "Presetsテーブル — ★(お気に入り)とB(Batchグループ)の列はトグルスイッチのように動作します。クリックで金色/黄色に変化、もう一度クリックで解除。ツールバーのBCボタンでBatchグループに登録された全プリセットを一括クリアします(Formタブのプリセットマネージャーと同じ)",
|
||||
helpPrompt21: "Presetsテーブルのツールバー — PP Copy/NP Copyは選択中の1行のPositive/Negativeプロンプトをクリップボードへコピー。グループ選択+Add to Groupは選択中の全プリセットを選んだグループへワンクリックで一括追加します。ここで(プリセット側を検索・選択してから短いグループ一覧から選ぶ形で)グループ割り当てを行うようにしたことで、プリセット数が多くなると使いづらかった旧来のグループごとのプリセット選択方式を置き換えました",
|
||||
helpPrompt22: "Presets Groupテーブル — グループの作成・リネーム・削除。各グループのメンバーは削除可能なバッジ(✕)として表示されます。予約グループのBatchはここには表示されません — PresetsテーブルのB列とBCボタンで専用管理されます",
|
||||
helpPrompt23: "Wildcardsテーブル — Formタブのワイルドカードファイルマネージャーと同じファイル一覧です。File列のパスやExtドロップダウンを編集するとファイル名が変更されます(新しい名前で内容を保存してから、古いファイルを削除します)",
|
||||
helpPrompt24: "Styleテーブル — Formタブのスタイル管理と同じスタイル一覧で、Presetsと同様にインライン編集できます",
|
||||
helpPrompt25: "Presetsテーブルのツールバー — 検索ボックス横の★とBソートボタンで、お気に入り/Batch登録済みのプリセットを一覧の先頭に並べ替えます。両方同時にONにでき、その場合は★が優先、Bが次点の基準になり、それ以外は元の相対順序を維持します",
|
||||
helpPrompt26: "Styleテーブルのツールバー — Fileバッジがcustom.jsonではない行を1件だけ選択すると「+ Add to this file」が有効になります。同じファイルを追加先とする空の行を追加します(保存前からFile列にバッジで表示されます)— Formタブのスタイルマネージャーと同じ機能です(上記参照)。custom.jsonのスタイルや複数選択時はボタンが無効のままです。その場合は通常の「+ New Style」を使ってください",
|
||||
helpSettings1: "UI言語と要約言語の設定",
|
||||
helpSettings2: "テーマ選択(13種プリセット)+カスタマイズ:カラー調整、背景パターン(ストライプ、ポルカドット、チェック、SVGタイリング)、フォント選択(日本語デザインフォント含む16種のGoogle Fonts)",
|
||||
helpSettings3: "ワークフローデータフォルダのパス変更・リセット",
|
||||
@@ -2121,7 +2261,9 @@ const LANGUAGES = {
|
||||
helpSettings5: "AI Assistant(プロンプトタブ):バックエンド(Ollama / LM Studio / Lemonade)・API URL・デフォルトモデル・接続テスト",
|
||||
helpSettings6: "デフォルトワークフローの設定・解除",
|
||||
helpSettings7: "Eagle連携URL、自動保存の有効/無効",
|
||||
helpSettings8: "データ管理:設定・メタデータ・プロンプトなど全データをJSONにエクスポート、またはインポートして復元。インポート後はComfyUIを再起動してください。",
|
||||
helpSettings8: "データ管理:設定・メタデータ・プロンプトなど主要データをJSONにエクスポート、またはインポートして復元。インポート後はComfyUIを再起動してください。",
|
||||
helpSettings15: "フルバックアップ (ZIP):上記のJSONエクスポートと異なり、フォルダ形式のデータ(AI Skills、Lab Plan、Styleプリセット)とTaggerデータベースも含みます。復元すると既存ファイルは上書きされます。復元後はComfyUIを再起動してください。",
|
||||
helpSettings16: "フルバックアップのボタン上にある2つのチェックボックスで、デフォルトワークフローフォルダーとImpact Packのwildcardフォルダーもエクスポートに含められます。どちらもエクスポート専用で自動復元はされません。必要な場合はZIP内の_external/フォルダから手動でコピーしてください。",
|
||||
helpSettings9: "各セクションはアコーディオン式で折りたたみ可能",
|
||||
helpSettings10: "Wildcard Integration:WFSのワイルドカードディレクトリをComfyUI-Impact-Packのwildcardsディレクトリへリンク(Windowsはジャンクション、他OSはシンボリックリンク)。既存ファイルは自動で移行されます",
|
||||
helpSettings11: "テキストサイズ:スライダー1本(10〜28px)でプロンプト・チャット全テキストエリアのフォントサイズを一括調整 — GenerateUI ポジティブ/ネガティブプロンプト、AIアシスタントチャット入力、プリセット ポジティブ/ネガティブ、ワイルドカードプロンプト・ファイルエディタ、メタデータプロンプト全文プレビュー;即時反映・保存で確定",
|
||||
@@ -2154,17 +2296,18 @@ const LANGUAGES = {
|
||||
helpGallery4: "ツールバーフィルタ:お気に入り(★)、テキスト検索(ファイル名・タグ・プロンプト)、タグフィルタ、グループフィルタ、日付・名前ソート",
|
||||
helpGallery5: "フォルダツリー:クリックで画像を読み込み;▶で展開;「+ New」で選択フォルダ内にサブフォルダを作成;「Del」でそのフォルダとすべての内容を削除",
|
||||
helpGallery6: "複数選択:Ctrl+クリックで個別トグル;Shift+クリックで最後にクリックした位置から範囲選択 — 一括操作バー:グループを選択して「グループに追加」/「グループから削除」;「お気に入り追加(全件)」/「お気に入り解除(全件)」;「比較」(2〜4枚選択時);「移動先...」;「ファイル削除」",
|
||||
helpGallery7: "詳細パネル — Infoタブ:ファイル名、サイズ、日付、タグ(追加/削除)、メモ;「Move To...」で他フォルダへ移動;「ファイル削除」で完全削除",
|
||||
helpGallery7: "詳細パネル — Infoタブ:ファイル名、サイズ、寸法(画像は幅×高さ、動画は再生時間)、日付、タグ(追加/削除)、メモ;「Move To...」で他フォルダへ移動;「ファイル削除」で完全削除",
|
||||
helpGallery8: "詳細パネル — JSONタブ:PNGの埋め込みデータ(prompt / workflow キー)またはGenerateUIタブで保存したメタデータからワークフローJSONを表示;「Copy & Send Canvas」ボタンでクリップボードにコピーしてComfyUIキャンバスへ直接送信(UI形式・API形式どちらも対応);ComfyUIツールバーから開いていない場合はUI形式のみタイトルドラッグのフォールバック",
|
||||
helpGallery9: "詳細パネル — Groupsタブ:画像ごとのグループ作成、割り当て、解除、名前変更、削除",
|
||||
helpGallery10: "ワークフロー自動保存:GenerateUIタブで生成した画像はワークフローが自動でギャラリーメタデータに保存され、JSONタブで確認可能",
|
||||
helpGallery11: "PNG (tEXt/iTXt)、JPEG、WebP、GIF対応;PNG形式はComfyUIの埋め込みJSON(prompt / workflow キー)を読み込んでワークフロー抽出が可能",
|
||||
helpGallery11: "PNG (tEXt/iTXt)、JPEG、WebP、GIF、MP4対応;PNG形式はComfyUIの埋め込みJSON(prompt / workflow キー)、MP4形式はSaveVideoが書き込むコンテナタグ(同キー、MiniMax H3出力等)からワークフロー抽出が可能",
|
||||
helpGallery12: "Metadataボタン(緑、詳細パネルのタブバー内):選択画像をMetadataタブで開いて埋め込みデータを詳細確認。画像サムネイルのAlt+クリックでも同じ操作が可能",
|
||||
helpGallery13: "Load GenUIボタン(青、詳細パネルのタブバー内):選択画像に埋め込まれたComfyUIワークフローをGenerateUIタブへ直接読み込んで切り替え。ワークフローが埋め込まれていない場合や非対応形式の場合は警告トーストを表示",
|
||||
helpGallery14: "Taggerボタン(紫、詳細パネルのアクション行):選択画像をTaggerタブのSingleモードで直接開き、即座にタグ付けを開始できる",
|
||||
helpGallery15: "Fボタン(シアン色オーバーレイ、各サムネイル左上):クリック一つで画像を __Feeder__ グループに追加/削除;グループ内の場合は常にシアン色で表示、非グループ時はホバー時のみ表示;FeederタブのGalleryモードへ素早く画像を追加できる",
|
||||
helpGallery16: "Image Editボタン(ツールバー、Refreshボタン横):選択中の画像をImage Editタブに送信;既にレイヤーが存在する場合はキャンバスに合わせてスケールして新規レイヤーとして追加、存在しない場合はLayer 1になる",
|
||||
helpGallery17: "Send GenUI Imageボタン(ツールバー、Image Editボタン横):選択画像をComfyUIにアップロードし、GenerateUIタブの最初のLoadImageノードへ入力として設定;GenerateUI → Input → Imageへ自動切り替え;事前にLoadImageノードを含むワークフローの読み込みが必要",
|
||||
helpGallery27: "Send to LI nodeボタン(ツールバー、Send GenUI Imageボタン横):選択画像をアップロードし、そのファイル名をComfyUIキャンバス上で選択中のノード(無ければ最初に見つかったLoadImage系ノード)のimageウィジェットへ書き込む — Image EditタブのSend to LI nodeと同じ送信先;ComfyUIのトップメニューボタンからWorkflow Studioを起動している必要がある",
|
||||
helpGallery18: "GalleryタブのOutputと並ぶ2つ目のサブタブ。視覚的なプロンプトライブラリを閲覧できる — ComfyUI outputフォルダ内のws_image_promptフォルダを管理(未作成時は自動生成)",
|
||||
helpGallery19: "同じ3カラムレイアウト:フォルダツリー(左)|サムネイル一覧(中央)|プロンプトビルダー(右)。サムネイルクリックでプレビューとプロンプトテキストを表示 — この時点ではまだプロンプトに追加されない",
|
||||
helpGallery20: "「+ 追加」で、プレビュー中の画像のプロンプトを「選択済みプロンプト」にチップとして追加;チップは個別(×)または一括(「全クリア」)で削除可能;「Save」でその画像のプロンプトテキストを編集・保存",
|
||||
@@ -2172,6 +2315,7 @@ const LANGUAGES = {
|
||||
helpGallery22: "Galleryタブ先頭にある3つ目のサブタブ。登録済みStyleをプレビュー画像から視覚的に選べる — ComfyUI outputフォルダ内のws_style_catalogフォルダを管理(未作成時は自動生成)。GenerateUIタブの「カタログ作成」ボタンで生成される(GenerateUIタブのヘルプ参照)",
|
||||
helpGallery23: "同じ3カラムレイアウト:フォルダツリー(左)|サムネイル一覧(中央)|Positive/Negativeプロンプトパネル(右)。サムネイルクリックで、その生成画像に実際に埋め込まれているPositive/Negativeプロンプトを表示、それぞれ個別のCopyボタン付き",
|
||||
helpGallery24: "Select as Styleボタン:選択中の画像のファイル名(拡張子除く)と同名の登録済みStyleを探し、GenerateUIタブのStyleドロップダウンをそれに切り替える — 既存Styleシステムへの視覚的なショートカットであり、埋め込みプロンプトをその場限りで適用するものではない(カタログ画像作成後にStyleがリネーム・削除されていた場合は「一致するスタイルが見つかりません」トーストを表示)",
|
||||
helpGallery26: "Galleryタブ上部の4つのサブタブ:Output(このブラウザ)・ImagePrompt・Style_Catalog・Metadata — 最後のMetadataは独立したMetadataタブ(該当ヘルプページ参照)をそのまま内包しているため、Galleryを離れずに画像をドロップして埋め込みモデル/プロンプト情報を確認できます",
|
||||
|
||||
// -- Tagger Tab --
|
||||
helpTaggerTitle: "Taggerタブ",
|
||||
@@ -2198,7 +2342,7 @@ const LANGUAGES = {
|
||||
helpSidepanel10: "各サブタブ内で検索してアイテムをすばやく検索",
|
||||
helpSidepanel11: "Modelsタブ:All / ★ Favorites / Groups / By Type サブタブ;Promptsタブ:All / ★ Favorites / Categories サブタブ+Groupsサブタブ(下段);Models Groupsの LoRA グループには「All N LoRAs」アイテムがあり、ドラッグで全 LoRA を Lora Loader (LoraManager) ノードに一括配置できる",
|
||||
helpSidepanel12: "⚙ テーマボタン(ヘッダー):パネル背景・サブヘッダー背景・文字色・ボーダー・補助テキスト色をカスタマイズ可能、設定はlocalStorageに保存され次回起動時も反映",
|
||||
helpSidepanel13: "I タブ(Information):ComfyUI の PNG / WebP / JSON をドロップするとサイドパネル内でモデル・LoRA・プロンプトのメタデータを確認可能 — サブタブ: model / lora / Prompts;UnetLoaderGGUF・QuadrupleCLIPLoader・Lora Loader (LoraManager) ノードタイプに対応",
|
||||
helpSidepanel13: "I タブ(Information):ComfyUI の PNG / WebP / MP4 / JSON をドロップするとサイドパネル内でモデル・LoRA・プロンプトのメタデータを確認可能 — サブタブ: model / lora / Prompts;UnetLoaderGGUF・QuadrupleCLIPLoader・Lora Loader (LoraManager) ノードタイプに対応;MP4はSaveVideoが埋め込むworkflow/promptタグを読み取り、MiniMax H3 ImageToVideoのようなオールインワン動画ノードのプロンプトにも対応",
|
||||
helpSidepanel14: "Information タブ — キャンバスへドラッグ:model サブタブのアイテムをドラッグすると対応ローダーノードを配置(Checkpoint → CheckpointLoaderSimple、VAE → VAELoader、Diffusion Model → UNETLoader、Text Encoder → CLIPLoader);ダブルクリックでもキャンバス中央に配置可能",
|
||||
helpSidepanel15: "Information タブ — lora サブタブ:LoraLoader・LoraLoaderModelOnly・Lora Loader (LoraManager) ノード(APIフォーマット含む)から LoRA を検出;個別 LoRA をドラッグで LoraLoader を配置;「Multiple LORA」セクションで全 LoRA を Lora Loader (LoraManager) ノードに一括配置(LoRA構文自動入力);Prompts サブタブ:プロンプトをドラッグするとテキスト入力済みの CLIPTextEncode ノードを配置;クリックで全文表示・コピー可能",
|
||||
helpSidepanel16: "A タブ(AI ツール):翻訳・Chat・TOOLS・設定のサブタブ。Ollama、LM Studio、Lemonade、Unsloth に対応;Chat は LLM とのマルチターン会話;TOOLS は VLM 画像解析とワイルドカード生成に対応;設定は SPA の AI タブと localStorage で共有される",
|
||||
@@ -2206,7 +2350,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非対応のモデルでも動作する",
|
||||
@@ -2234,10 +2378,12 @@ const LANGUAGES = {
|
||||
helpGenCardBatchTypes: "Batchタブ — バッチ種別",
|
||||
helpGenCardLora: "Modelタブ — LoRA詳細",
|
||||
helpPromptTitle: "プロンプトタブ",
|
||||
helpPromptCardToplevel: "Form / Table",
|
||||
helpPromptCardAi: "AIアシスタント・翻訳",
|
||||
helpPromptCardPresets: "プリセット",
|
||||
helpPromptCardWildcards: "ワイルドカード",
|
||||
helpPromptCardStyleTitle: "Style",
|
||||
helpPromptCardTable: "テーブル表示",
|
||||
helpMetadataCardOverview: "概要",
|
||||
helpMetadataCardExtracted: "抽出情報",
|
||||
helpMetadataCardFormats: "対応フォーマット",
|
||||
@@ -2301,8 +2447,7 @@ const LANGUAGES = {
|
||||
helpImageEditExportTitle: "エクスポートとナビゲーション",
|
||||
helpImageEdit17: "Save PNG:全可視レイヤーを合成してPNGファイルとしてダウンロード",
|
||||
helpImageEdit18: "Save to Gallery:ファイル名を入力(デフォルト: wfs-image-YYYYMMDDHHmmss)してギャラリーのルートフォルダに合成PNGを保存(設定 > Gallery Output Folderで設定);Galleryタブを一度開いてルートフォルダを設定する必要あり",
|
||||
helpImageEdit19: "Save to Input:合成PNGをComfyUIのinputフォルダに /upload/image でアップロードし、Load Imageノードで選択可能にする",
|
||||
helpImageEdit19b: "Send to Workflow:合成PNGをアップロードする点はSave to Inputと同じだが、さらにアップロード結果のファイル名をComfyUIグラフ内で選択中のノード(無ければ最初に見つかったLoadImage系ノード)のimageウィジェットへ書き込む;ComfyUIのトップメニューボタンからWorkflow Studioを起動している必要がある",
|
||||
helpImageEdit19b: "Send to LI node:合成PNGをComfyUIのinputフォルダに /upload/image でアップロードし、さらにそのファイル名をComfyUIグラフ内で選択中のノード(無ければ最初に見つかったLoadImage系ノード)のimageウィジェットへ書き込む;ComfyUIのトップメニューボタンからWorkflow Studioを起動している必要がある",
|
||||
helpImageEdit20: "キャンバスナビゲーション:スクロールホイールでズーム、Space + ドラッグでパン",
|
||||
helpImageEdit21: "Undo / Redo:Ctrl+Z / Ctrl+Y(またはツールバーの ↩ ↪ ボタン)",
|
||||
helpImageEditKbdTitle: "Image Edit キーボードショートカット",
|
||||
@@ -2423,13 +2568,20 @@ const LANGUAGES = {
|
||||
eagleUrl: "Eagle URL",
|
||||
eagleAutoSave: "生成画像をEagleに自動保存",
|
||||
dataManagement: "データ管理",
|
||||
dataManagementHint: "設定・メタデータ・プロンプトなど全データをJSONファイルにエクスポート、またはインポートして復元できます。インポート後はComfyUIを再起動してください。",
|
||||
dataManagementHint: "設定・メタデータ・プロンプトなど主要データをJSONファイルにエクスポート、またはインポートして復元できます。インポート後はComfyUIを再起動してください。",
|
||||
exportData: "エクスポート",
|
||||
importData: "インポート",
|
||||
exportSuccess: "エクスポート完了",
|
||||
exportError: "エクスポート失敗",
|
||||
importSuccess: "インポート完了",
|
||||
importError: "インポート失敗",
|
||||
exportFullBackup: "フルバックアップ (ZIP)",
|
||||
importFullBackup: "フルバックアップを復元 (ZIP)",
|
||||
fullBackupHint: "フルバックアップ(ZIP)は上記のJSONエクスポートでは表現できないフォルダ形式のデータ(AI Skills、Lab Plan、Styleプリセット)とTaggerデータベースも含みます。復元すると既存ファイルは上書きされます。復元後はComfyUIを再起動してください。",
|
||||
fullBackupFilesUnit: "件",
|
||||
fullBackupIncludeWorkflows: "デフォルトワークフローフォルダーを含める (user/default/workflows)",
|
||||
fullBackupIncludeWildcard: "Impact Packのwildcardフォルダーを含める(存在する場合)",
|
||||
fullBackupExternalHint: "これらはエクスポート専用です。復元時には一切触れません。必要な場合はZIP内の_external/フォルダから手動でコピーしてください。",
|
||||
saveSettings: "設定を保存",
|
||||
settingsSaved: "設定を保存しました",
|
||||
ollamaSaved: "AI Assistant設定を保存しました",
|
||||
@@ -2498,8 +2650,8 @@ const LANGUAGES = {
|
||||
metaSectionTe: "Text Encoder",
|
||||
metaSectionLora: "LoRA",
|
||||
metaSectionPrompt: "プロンプト",
|
||||
metaDropMain: "画像 / JSON をドロップ",
|
||||
metaDropSub: "PNG · WebP · JSON",
|
||||
metaDropMain: "画像 / 動画 / JSON をドロップ",
|
||||
metaDropSub: "PNG · WebP · MP4 · JSON",
|
||||
metaParsing: "解析中...",
|
||||
metaFileTooLarge: "ファイルが大きすぎます(最大 50 MB)",
|
||||
metaNoMetadata: "メタデータが見つかりません",
|
||||
@@ -2509,7 +2661,7 @@ const LANGUAGES = {
|
||||
metaPromptText: "テキスト",
|
||||
metaSelectPrompt: "上のプロンプトを選択すると全文を表示します...",
|
||||
metaFormatNoteTitle: "対応フォーマット",
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / MP4 / JSON",
|
||||
metaFmtSdwebui: "SD WebUI / SD Forge — PNG(parameters チャンク)",
|
||||
metaFmtFooocus: "Fooocus — PNG(fooocus_scheme チャンク)",
|
||||
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image サブグラフ形式に対応済み",
|
||||
@@ -2519,7 +2671,59 @@ const LANGUAGES = {
|
||||
helpMetadata2: "中列: ワークフローから抽出した Checkpoint・VAE・Diffusion Model・Text Encoder の一覧",
|
||||
helpMetadata3: "右列: LoRA 一覧(強度値付き)+プロンプト一覧(クリックで下部に全文表示)",
|
||||
helpMetadata4: "プロンプトアクション — Copy: クリップボードにコピー; GenUI:P/N: GenerateUI のポジティブ/ネガティブプロンプトにセット; Prompt:P/N: Prompt タブ Presets のポジティブ/ネガティブにセット",
|
||||
helpMetadata5: "対応フォーマット: ComfyUI PNG/WebP/JSON(標準形式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image サブグラフ形式)・SD WebUI・SD Forge・Fooocus。",
|
||||
helpMetadata5: "対応フォーマット: ComfyUI PNG/WebP/MP4/JSON(標準形式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image サブグラフ形式;MP4はSaveVideoが書き込むworkflow/promptコンテナタグを読み取り、MiniMax H3 ImageToVideoのようなオールインワン動画ノードにも対応)・SD WebUI・SD Forge・Fooocus。",
|
||||
helpMetadata6: "現在は Gallery タブ内のサブタブ(Style_Catalog の右隣)になっており、独立したトップレベルタブではありません — それ以外の動作は全く同じです",
|
||||
|
||||
// -- Video タブ(MiniMax H3専用、GenerateUIタブとは独立) --
|
||||
tabVideo: "Video",
|
||||
loadInVideo: "Videoに読み込み",
|
||||
videoTitle: "Video (MiniMax H3)",
|
||||
videoFirstFrame: "First Frame(任意)",
|
||||
videoLastFrame: "Last Frame(任意)",
|
||||
videoDropOrClick: "画像をドロップまたはクリックして選択",
|
||||
videoPrompt: "Prompt",
|
||||
videoAspectRatio: "Aspect Ratio",
|
||||
videoMegapixels: "Megapixels",
|
||||
videoMultiple: "Multiple",
|
||||
videoDuration: "Duration(秒)",
|
||||
videoNoiseSeed: "Noise Seed",
|
||||
videoRandomizeSeed: "ランダム化",
|
||||
videoGenerate: "Generate",
|
||||
videoEditPlaceholder: "編集機能は今後追加予定",
|
||||
videoNoWorkflowLoaded: "ワークフローが読み込まれていません",
|
||||
videoUnsupportedWorkflow: "このワークフローにはMiniMax H3のImage to Videoノードが含まれていません",
|
||||
videoWorkflowLoaded: (name) => `ワークフローを読み込みました: ${name}`,
|
||||
videoGenerateSuccess: "動画を生成しました",
|
||||
videoDoneCount: (n) => `完了(${n}件)`,
|
||||
videoSourceLabel: "動画ソース",
|
||||
videoSourceHint: "生成せずにFrame/GIF機能を使いたい場合はここに任意の動画を読み込めます。",
|
||||
videoSourceDropLabel: "動画をドロップまたはクリックして選択",
|
||||
videoNoVideo: "動画なし",
|
||||
videoPropFrame: "Frame",
|
||||
videoPropGif: "GIF",
|
||||
videoFrameHint: "プレビュー動画を再生・シークして止めたい位置で「Capture Frame」を押してください。",
|
||||
videoCaptureFrame: "Capture Frame",
|
||||
videoSaveToOutput: "Outputに保存",
|
||||
videoFrameSaved: (name) => `フレームを保存しました: ${name}`,
|
||||
videoFrameCaptureFailed: (msg) => `フレームのキャプチャに失敗しました: ${msg}`,
|
||||
videoGifStart: "開始(秒)",
|
||||
videoGifEnd: "終了(秒)",
|
||||
videoGifFps: "FPS",
|
||||
videoGifWidth: "最大幅",
|
||||
videoConvertToGif: "GIFに変換",
|
||||
videoGifConverting: "変換中...",
|
||||
videoGifDoneCount: (n) => `完了(${n}フレーム)`,
|
||||
videoGifSaved: (name) => `GIFを保存しました: ${name}`,
|
||||
videoNoSourceLoaded: "先に動画を用意してください(生成するか、動画ソースにファイルをドロップ)",
|
||||
helpVideoTitle: "Video タブ",
|
||||
helpVideoDesc: "MiniMax H3 (Image to Video) ワークフロー専用の動画生成タブ。GenerateUIタブとは独立して動作します。",
|
||||
helpVideo1: "first_frame(任意)/last_frame(任意)の画像、prompt、aspect_ratio/megapixels/multiple(解像度)、duration(秒)、noise_seedを指定してGenerateをクリックします。first_frameを未指定にするとテキストのみでバイパス実行され、LoadImageノード自体が無いワークフローにも対応しています。",
|
||||
helpVideo2: "モデル選択(UNet/CLIP/VAE/Audio VAE)は読み込んだワークフローの既定値に固定され、このタブでは変更できません。",
|
||||
helpVideo3: "Workflowタブの詳細モーダルにある「Videoに読み込み」ボタンから、MiniMax H3のImage to Videoノードを含むワークフローを読み込めます。対応していないワークフローはエラー表示になります。",
|
||||
helpVideo4: "生成した動画(mp4)はGalleryタブでも閲覧・再生できます。",
|
||||
helpVideo5: "プレビューの横にある「Video Source」パネルでは、生成した動画に限らず任意のローカル動画をドロップ/選択して、下のFrame/GIF機能をMiniMax H3を実行せずに使えます。Clearで選択をリセットできます。",
|
||||
helpVideo6: "Frameタブ(右パネル): プレビュー動画を再生・シークして止めたい位置で「Capture Frame」を押すとその場でフレームをキャプチャし、「Save to Output」でPNGとしてComfyUIのOutputフォルダに保存します。",
|
||||
helpVideo7: "GIFタブ(右パネル): Start/End(秒)・FPS・Max Width(任意)を指定して「Convert to GIF」を押すと、サーバー側で動画をデコードしアニメーションGIFとしてComfyUIのOutputフォルダに保存します。",
|
||||
},
|
||||
|
||||
zh: {
|
||||
@@ -2609,6 +2813,7 @@ const LANGUAGES = {
|
||||
copyAndSendCanvas: "复制并发送到画布",
|
||||
workflowSentToCanvas: "工作流已就绪 — 将标题拖拽到画布",
|
||||
workflowSentToCanvasDirect: "工作流已发送到画布",
|
||||
workflowFilteredInLibrary: "已在Workflow Studio Library(W标签)中筛选 — 请将卡片拖拽到画布",
|
||||
comfyUILoadTimeout: "ComfyUI加载超时",
|
||||
popupBlocked: "弹窗被阻止。请允许此站点的弹窗。",
|
||||
setAsDefault: "设为默认",
|
||||
@@ -2752,6 +2957,7 @@ const LANGUAGES = {
|
||||
labBatchCountTooSmall: "请先增加Batch数量再添加关键帧",
|
||||
labKeyframesExceedBatch: (maxIter, batchCount) => `存在设置到第${maxIter}次的关键帧,超过了当前Batch数量(${batchCount}),在增加Batch数量之前不会被应用`,
|
||||
labUseWorkflowDefault: "(保持工作流原设置)",
|
||||
labNoNodeForColumn: "此工作流中没有对应节点",
|
||||
labInheritsFrom: (val) => `(继承: ${val})`,
|
||||
labLoraNoNode: "工作流中未找到LoRA节点 — 已跳过LoRA设置",
|
||||
labAppliedAt: (n) => `应用:${n}`,
|
||||
@@ -2760,12 +2966,17 @@ const LANGUAGES = {
|
||||
labRevertCheckboxLabel: "恢复第1次的设置",
|
||||
labBypassed: "⏭ 已跳过",
|
||||
labBypassCheckboxLabel: "跳过此设置(保持它之前生效的设置)",
|
||||
labLoraNodeBypassed: "⛔(节点已绕过)",
|
||||
labLoraNodeBypassCheckboxLabel: "绕过LoRA节点(强度设为0)",
|
||||
labLoraApplyToPromptCheckboxLabel: "将LoRA语法和触发词应用到正面提示词",
|
||||
labLoraPromptAppliedTag: "→P",
|
||||
labSeedEmptyRandom: "Seed(留空为随机)",
|
||||
labPause: "暂停",
|
||||
labResume: "继续",
|
||||
labNoWorkflowLoaded: "请先在GenerateUI中加载工作流",
|
||||
labNoSourceImage: "请先拖入源图片",
|
||||
labNoResultsYet: "暂无结果",
|
||||
labLastFrameExtractFailed: (msg) => `从生成的视频提取最后一帧失败: ${msg}`,
|
||||
labResultPreview: "预览",
|
||||
labEnterPlanName: "方案名称:",
|
||||
labPlanSaved: "方案已保存",
|
||||
@@ -2797,6 +3008,15 @@ const LANGUAGES = {
|
||||
labT2IImageIgnored: "T2I模式已开启,无需上传图片,已跳过",
|
||||
labIndexImageSaved: (filename) => `索引图片已保存到Output:${filename}`,
|
||||
labIndexImageSaveFailed: "索引图片保存到Output失败",
|
||||
labPromptGroupBtnTitle: "应用提示词组",
|
||||
labPromptGroupModalTitle: "提示词组",
|
||||
labPromptGroupEmpty: "尚无分组。请在Prompt标签的分组面板中创建。",
|
||||
labPromptGroupCount: (n) => `${n} 个预设`,
|
||||
labPromptGroupSelectFirst: "请先选择一个分组",
|
||||
labPromptGroupNoPresets: "该分组没有预设",
|
||||
labPromptGroupApplied: (name, n) => `已将"${name}"(${n}个关键帧)应用到Prompt`,
|
||||
labPromptGroupBatchRaised: (n) => `已将Batch数量提高到${n}以适应该分组`,
|
||||
labSelectKeyframeFirst: "请先选择一个关键帧",
|
||||
|
||||
// -- GenerateUI标签页 Input子标签: Image内占位图 --
|
||||
i2iPlaceholderTitle: "占位图片",
|
||||
@@ -3063,6 +3283,7 @@ const LANGUAGES = {
|
||||
modelsSideGroup: "分组",
|
||||
modelsSideCivitai: "CivitAI",
|
||||
gallerySideMetadata: "元数据",
|
||||
gallerySidePrompt: "提示词",
|
||||
|
||||
// -- Gallery --
|
||||
galleryAllTags: "所有标签",
|
||||
@@ -3076,12 +3297,15 @@ const LANGUAGES = {
|
||||
gallerySelectImageFirst: "请先选择图像",
|
||||
galleryNoLoadImageNode: "当前工作流中没有LoadImage节点",
|
||||
gallerySentGenUI: "已发送至GenerateUI图像标签",
|
||||
sendToLiNodeNoOpener: "请通过ComfyUI顶部菜单打开Workflow Studio以使用此功能",
|
||||
sendToLiNodeSuccess: (filename) => `已将"${filename}"发送到LI节点`,
|
||||
galleryCCNotAvailable: "仅当从ComfyUI Comic Creator内的图库打开时才能使用Send CC",
|
||||
gallerySentCC: "已发送至ComfyUI Comic Creator",
|
||||
galleryNoEmbeddedWorkflow: "此图像中未嵌入ComfyUI工作流。",
|
||||
galleryTabOutput: "Output",
|
||||
galleryTabImagePrompt: "ImagePrompt",
|
||||
galleryTabStyleCatalog: "Style_Catalog",
|
||||
galleryTabMetadata: "元数据",
|
||||
galleryNoSelection: "未选择",
|
||||
imagePromptSelectedPrompt: "提示词",
|
||||
imagePromptNoPrompt: "此图像没有提示词数据。",
|
||||
@@ -3299,6 +3523,8 @@ 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按钮会清除绕过状态,避免其在之后被无声地重新生效",
|
||||
helpGen30: "Save按钮(子标签栏右端)在Batch或Lab子标签处于激活状态时会隐藏,因为它们操作的是工作流副本或各自的Plan文件,而不是当前加载的工作流本身;切换回Input/Model/Settings/Feeder时会重新显示",
|
||||
helpFeederTitle: "GenerateUI Tab — Feeder 子标签",
|
||||
helpFeederDesc: "通过左侧面板顶部的 [图片循环] / [图库] 切换按钮可在两种模式间切换。",
|
||||
helpFeederImgloopTitle: "图片循环模式",
|
||||
@@ -3337,8 +3563,12 @@ 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节点」时会自动禁用",
|
||||
helpLab18: "单击关键帧单元格现在只是选中它(蓝色描边),不再直接打开编辑弹窗——需要双击才能打开编辑弹窗。每列的表头(Model/Prompt/Ksampler)新增了▲/▼按钮,可将选中的关键帧与相邻的关键帧互换位置,从而在不重新输入内容的情况下调整顺序;关键帧#1(基准行)无法移动,也不会有其他关键帧被换到#1的位置",
|
||||
helpLab19: "Prompt列表头的PG按钮:选择Prompt标签中的一个预设分组(Table视图中的Presets Group,与Batch标签复选列表使用的是同一批分组),即可将整个Prompt列替换为该分组内每个预设对应一个关键帧、按分组顺序排列(从#1开始,#1会被覆盖而不是在其后追加)。如果分组中的预设数量超过当前Batch数量,会自动提高Batch数量以容纳(上限为50,与Batch输入框本身的上限一致);仍然超出的部分不会被静默丢弃,而是照常显示「关键帧超出Batch数量」的提示",
|
||||
helpLabCardPlanTitle: "Setting子标签 — 运行与方案文件",
|
||||
helpLabPlan1: "右侧面板 — Batch:要运行的迭代次数;Note:与方案一起保存的自由备注",
|
||||
helpLabPlan2: "Run/Pause/Cancel:Run开始批量循环;Pause/Resume在各次迭代之间暂停/继续;Cancel会中断当前生成并停止循环",
|
||||
@@ -3371,6 +3601,18 @@ const LANGUAGES = {
|
||||
helpPrompt12: "重命名后仍保留其在原定义文件中的位置;新建的Style会保存到style文件夹内专用的custom.json中,因此内置的风格包文件不会被直接修改;名称在所有Style中必须唯一",
|
||||
helpPrompt13: "保存、重命名或删除Style会立即刷新GenerateUI的Style下拉菜单、Batch标签的风格勾选列表以及Style Catalog的名称列表 — 无需重新加载",
|
||||
helpPrompt14: "列表中每个条目的名称旁都会以小徽章显示其来源文件(例如sdxl_styles_diva.json);编辑现有Style时,该文件名也会显示在Positive prompt字段上方,并且Save按钮旁会出现「+ Add to this file」按钮 — 点击后表单会清空,可创建一个新Style并追加到同一文件,而不是默认的custom.json",
|
||||
helpPrompt15: "两个顶层标签:Form是下文所述的传统单项逐一编辑界面(AI助手/Presets/Wildcard/Style各面板);Table将同一份数据一次性以可编辑表格形式全部显示 — 当预设、通配符文件或Style数量较多时更实用",
|
||||
helpPrompt16: "Table标签内还有四个子标签:Presets、Presets Group、Wildcards、Style — 均为与Form标签完全相同数据的可编辑表格,任一侧的修改都会同步到另一侧",
|
||||
helpPrompt17: "每个表格左侧共用一个自动编号的#列:点击编号可选中该行(红色高亮),可同时选中多行;工具栏的Delete按钮会在一次确认后删除所有选中行,Deselect用于清除选择。此设计取代了容易误触的逐行删除按钮",
|
||||
helpPrompt18: "双击行号会打开更大的提示词编辑弹窗,取代表格内较小的文本框。Presets和Style在共用的一个文本框上方显示Positive/Negative切换按钮(切换时会保留另一侧已输入的内容);Wildcards仅显示文件内容。Save会写回并关闭弹窗,Close则放弃更改",
|
||||
helpPrompt19: "工具栏的「+ New ...」按钮会在顶部添加一个空白可编辑行,编号位置显示小图标✓(Add)/✕(Cancel);填写内容后点击✓即可创建,点击✕则放弃该行且不保存",
|
||||
helpPrompt20: "Presets表格 — ★(收藏)和B(Batch分组)两列如同开关:点击变为金色/黄色,再次点击取消;工具栏的BC按钮可一次性清空Batch分组中的所有预设,与Form标签的Preset Manager效果相同",
|
||||
helpPrompt21: "Presets表格工具栏 — PP Copy/NP Copy用于将当前选中的单行的正/负向提示词复制到剪贴板;分组下拉框+Add to Group可一键将所有已选中的预设添加到所选分组。在此处进行分组分配(先在Presets表格中搜索/选择,再从简短的分组列表中挑选)取代了旧版按分组选择预设的方式,后者在预设较多时已难以使用",
|
||||
helpPrompt22: "Presets Group表格 — 创建、重命名、删除分组;每个分组的成员以可删除的徽章(✕)形式显示。保留分组Batch不会显示在这里 — 它完全通过Presets表格的B列和BC按钮进行管理",
|
||||
helpPrompt23: "Wildcards表格 — 与Form标签的通配符文件管理器使用相同的文件列表;编辑File列的路径或Ext下拉框会重命名文件(先以新名称保存内容,再删除旧文件)",
|
||||
helpPrompt24: "Style表格 — 与Form标签的Style管理器使用相同的Style列表,可像Presets一样进行内联编辑",
|
||||
helpPrompt25: "Presets表格工具栏 — 搜索框旁边的★和B排序按钮,可将收藏/已加入Batch的预设排到列表顶部;两者可同时启用(★优先,B为次要条件),其余项保持原有的相对顺序",
|
||||
helpPrompt26: "Style表格工具栏——只选中一行且其File徽标不是custom.json时,「+ Add to this file」按钮才会启用:会添加一个以该文件为目标的空白行(保存前File列就会显示该文件的徽标)——与Form标签中Style管理器的同一功能(见上文)。custom.json条目或多选时按钮保持禁用,此时请改用常规的「+ New Style」按钮",
|
||||
helpSettings1: "UI语言和摘要语言设置",
|
||||
helpSettings2: "主题选择(13种预设)+ 自定义:颜色调整、背景图案(条纹、波点、棋盘格、SVG平铺)、字体选择(含日文显示字体在内的16种Google Fonts)",
|
||||
helpSettings3: "工作流数据文件夹路径更改/重置",
|
||||
@@ -3378,7 +3620,9 @@ const LANGUAGES = {
|
||||
helpSettings5: "AI助手(提示词标签页):后端(Ollama / LM Studio / Lemonade)、API URL、默认模型、连接测试",
|
||||
helpSettings6: "默认工作流设置/取消",
|
||||
helpSettings7: "Eagle集成URL、自动保存开关",
|
||||
helpSettings8: "数据管理:将所有插件数据(设置、元数据、提示词等)导出为JSON文件,或导入以恢复。导入后请重启ComfyUI。",
|
||||
helpSettings8: "数据管理:将设置、元数据、提示词等主要数据导出为JSON文件,或导入以恢复。导入后请重启ComfyUI。",
|
||||
helpSettings15: "完整备份 (ZIP):与上方的JSON导出不同,还包含文件夹型数据(AI Skills、Lab Plan、Style预设)以及Tagger数据库。恢复会覆盖现有文件,恢复后请重启ComfyUI。",
|
||||
helpSettings16: "完整备份按钮上方的两个复选框可将ComfyUI默认工作流文件夹和Impact Pack的wildcard文件夹一并导出。两者均仅用于导出,不会自动恢复。如需要,请从ZIP中的_external/文件夹手动复制回去。",
|
||||
helpSettings9: "各部分可折叠展开",
|
||||
helpSettings10: "Wildcard Integration:将WFS通配符目录链接到ComfyUI-Impact-Pack的wildcards目录(Windows为目录联结,其他OS为符号链接);现有文件自动迁移",
|
||||
helpSettings11: "文字大小:单个滑块(10–28 px)可一次性调整所有提示词和聊天文本框的字体大小 — GenerateUI正/负向提示词、AI助手聊天输入、预设正/负向提示、通配符提示和文件编辑器、元数据提示词全文预览;即时生效并通过「保存设置」保存",
|
||||
@@ -3411,17 +3655,18 @@ const LANGUAGES = {
|
||||
helpGallery4: "工具栏过滤器:收藏 (★)、文本搜索(文件名・标签・提示词)、标签过滤、分组过滤、按日期/名称排序",
|
||||
helpGallery5: "文件夹树:点击加载图像,▶展开;'+ New'在选定文件夹内创建子文件夹;'Del'删除该文件夹及其所有内容",
|
||||
helpGallery6: "多选:Ctrl+点击切换单张图像;Shift+点击从上次点击位置进行范围选择 — 批量操作栏:选择分组 → 「添加到分组」/「从分组中删除」;全部收藏/取消收藏;「对比」(2–4张);移动到...;删除文件",
|
||||
helpGallery7: "详情面板 — 信息(Info)标签:文件名、大小、日期、标签(添加/移除)、备注;'移动到...'将图像移至另一文件夹;'删除文件'永久删除",
|
||||
helpGallery7: "详情面板 — 信息(Info)标签:文件名、大小、尺寸(图片为宽×高,视频为时长)、日期、标签(添加/移除)、备注;'移动到...'将图像移至另一文件夹;'删除文件'永久删除",
|
||||
helpGallery8: "详情面板 — JSON标签:显示来自PNG嵌入数据(prompt / workflow键)或Generate UI标签保存的工作流JSON;「Copy & Send Canvas」按钮将工作流复制到剪贴板并直接发送到ComfyUI画布(UI和API格式均支持);若Workflow Studio不是从ComfyUI工具栏打开,UI格式将回退到标题拖拽",
|
||||
helpGallery9: "详情面板 — 分组(Groups)标签:对每个图像进行组的创建、分配、移除、重命名和删除",
|
||||
helpGallery10: "工作流自动保存:在Generate UI标签生成的图像,其工作流会自动保存到图库元数据,并在JSON标签中显示",
|
||||
helpGallery11: "支持PNG(tEXt/iTXt)、JPEG、WebP、GIF;PNG格式读取ComfyUI嵌入的JSON(prompt / workflow键)提取工作流",
|
||||
helpGallery11: "支持PNG(tEXt/iTXt)、JPEG、WebP、GIF、MP4;PNG格式读取ComfyUI嵌入的JSON(prompt / workflow键)提取工作流,MP4格式读取SaveVideo写入的容器标签(同名键,如MiniMax H3输出)",
|
||||
helpGallery12: "Metadata按钮(绿色,位于详情面板标签栏):在Metadata标签中打开所选图像以全面查看嵌入数据;Alt+点击任意图像缩略图效果相同",
|
||||
helpGallery13: "Load GenUI按钮(蓝色,位于详情面板标签栏):将所选图像中嵌入的ComfyUI工作流直接加载到GenerateUI标签并切换过去;如果没有嵌入工作流或格式不受支持,则显示警告提示",
|
||||
helpGallery14: "Tagger按钮(紫色,位于详情面板操作行):将所选图像直接在Tagger标签的Single模式中打开,即可立即进行标注",
|
||||
helpGallery15: "F按钮(青色悬浮按钮,每个缩略图左上角):单击即可将图像添加到/从__Feeder__组中移除;图像在组中时始终显示青色,不在组中时仅悬停时可见;可快速为Feeder标签Gallery模式添加队列图像",
|
||||
helpGallery16: "Image Edit按钮(工具栏,Refresh按钮旁):将当前选中的图像发送到Image Edit标签;如果已有图层则按比例缩放添加为新图层,否则将成为Layer 1",
|
||||
helpGallery17: "Send GenUI Image按钮(工具栏,Image Edit按钮旁):将选中图像上传至ComfyUI并设置为GenerateUI标签中第一个LoadImage节点的输入;自动切换至GenerateUI → Input → Image;需要预先加载包含至少一个LoadImage节点的工作流",
|
||||
helpGallery27: "Send to LI node按钮(工具栏,Send GenUI Image按钮旁):上传选中图像并将其文件名写入ComfyUI画布上当前选中节点(若无选中节点则回退到找到的第一个LoadImage类节点)的image部件 — 与Image Edit标签的Send to LI node发送到同一目标;需要通过ComfyUI顶部菜单按钮打开Workflow Studio",
|
||||
helpGallery18: "Gallery标签页中与Output并列的第二个子标签,用于浏览可视化提示词库 — 管理ComfyUI output文件夹内的ws_image_prompt文件夹(未创建时自动生成)",
|
||||
helpGallery19: "相同的三栏布局:文件夹树(左)|缩略图(中)|提示词构建器(右)。点击缩略图可预览图像及其提示词文本 — 此时尚未添加到提示词中",
|
||||
helpGallery20: "「+ 添加」将预览中图像的提示词作为标签添加到「已选提示词」;标签可单独删除(×)或一次性清除(「全部清除」);「Save」用于编辑并保存该图像的提示词文本",
|
||||
@@ -3430,6 +3675,7 @@ const LANGUAGES = {
|
||||
helpGallery23: "相同的三栏布局:文件夹树(左)|缩略图(中)|Positive/Negative提示词面板(右)。点击缩略图可查看该生成图像中实际嵌入的Positive/Negative提示词,各自带有独立的Copy按钮",
|
||||
helpGallery24: "Select as Style按钮:以选中图像的文件名(不含扩展名)匹配已注册的Style名称,并将GenerateUI标签的Style下拉框切换为该风格 — 这是通往现有命名Style系统的可视化快捷方式,而非一次性应用嵌入的提示词(如果目录图像生成后该Style已被重命名或删除,则会显示「未找到匹配的风格」提示)",
|
||||
helpGallery25: "载入GenerateUI / 在Metadata标签中打开按钮:读取目录图像本身内嵌的工作流(目录副本携带与原始输出图像相同的PNG元数据),可直接载入GenerateUI编辑器,或在Metadata标签中打开该图像以供查看 — 无需在Output文件夹中寻找原始文件",
|
||||
helpGallery26: "Gallery标签页顶部有四个子标签:Output(此浏览器)、ImagePrompt、Style_Catalog、Metadata — 最后一个直接内嵌完整的Metadata标签页(参见其专属帮助页),因此无需离开Gallery即可拖入图像查看其内嵌的模型/提示词数据",
|
||||
|
||||
// -- Tagger Tab --
|
||||
helpTaggerTitle: "Tagger标签",
|
||||
@@ -3456,7 +3702,7 @@ const LANGUAGES = {
|
||||
helpSidepanel10: "在各子标签内搜索快速查找项目",
|
||||
helpSidepanel11: "Models标签:All / ★ Favorites / Groups / By Type子标签;Prompts标签:All / ★ Favorites / Categories子标签+Groups子标签(第二行);Models Groups的LoRA组显示「All N LoRAs」条目,可拖拽将全部LoRA放入单个Lora Loader (LoraManager)节点",
|
||||
helpSidepanel12: "⚙ 主题按钮(标题栏):可自定义面板背景、子标题背景、文字颜色、边框及辅助文字颜色,设置保存至localStorage并在下次打开时生效",
|
||||
helpSidepanel13: "I 标签(Information):拖放 ComfyUI PNG/WebP/JSON 即可在侧面板内查看模型、LoRA 和提示词元数据 — 子标签:model / lora / Prompts;支持 UnetLoaderGGUF、QuadrupleCLIPLoader 和 Lora Loader (LoraManager) 节点类型",
|
||||
helpSidepanel13: "I 标签(Information):拖放 ComfyUI PNG/WebP/MP4/JSON 即可在侧面板内查看模型、LoRA 和提示词元数据 — 子标签:model / lora / Prompts;支持 UnetLoaderGGUF、QuadrupleCLIPLoader 和 Lora Loader (LoraManager) 节点类型;MP4会读取SaveVideo写入的workflow/prompt标签,也支持MiniMax H3 ImageToVideo等一体化视频节点的提示词",
|
||||
helpSidepanel14: "Information 标签 — 拖拽到画布:model 子标签拖拽可放置对应加载节点(Checkpoint → CheckpointLoaderSimple、VAE → VAELoader、Diffusion Model → UNETLoader、Text Encoder → CLIPLoader);双击也可放置到画布中央",
|
||||
helpSidepanel15: "Information 标签 — lora 子标签:可检测 LoraLoader、LoraLoaderModelOnly 及 Lora Loader (LoraManager) 节点(含API格式)的 LoRA;拖拽单个 LoRA 放置 LoraLoader;「Multiple LORA」区域可将全部 LoRA 一次性放入 Lora Loader (LoraManager) 节点并自动填入LoRA语法;Prompts 子标签:拖拽提示词放置预填文本的 CLIPTextEncode 节点;点击可查看全文并复制",
|
||||
helpSidepanel16: "A 标签(AI 工具):翻译、Chat、TOOLS 和设置子标签,支持 Ollama、LM Studio、Lemonade 或 Unsloth;Chat 支持与 LLM 的多轮对话;TOOLS 包含 VLM 图像分析和通配符生成;设置通过 localStorage 与 SPA 的 AI 标签共享",
|
||||
@@ -3464,7 +3710,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 的模型也可以使用",
|
||||
@@ -3492,10 +3738,12 @@ const LANGUAGES = {
|
||||
helpGenCardBatchTypes: "Batch 标签 — 批量类型",
|
||||
helpGenCardLora: "Model 标签 — LoRA 详情",
|
||||
helpPromptTitle: "提示词标签页",
|
||||
helpPromptCardToplevel: "Form / Table",
|
||||
helpPromptCardAi: "AI 助手与翻译",
|
||||
helpPromptCardPresets: "预设",
|
||||
helpPromptCardWildcards: "通配符",
|
||||
helpPromptCardStyleTitle: "Style",
|
||||
helpPromptCardTable: "表格视图",
|
||||
helpMetadataCardOverview: "概述",
|
||||
helpMetadataCardExtracted: "提取的信息",
|
||||
helpMetadataCardFormats: "支持的格式",
|
||||
@@ -3559,8 +3807,7 @@ const LANGUAGES = {
|
||||
helpImageEditExportTitle: "导出与导航",
|
||||
helpImageEdit17: "Save PNG:合并所有可见图层并下载为PNG文件",
|
||||
helpImageEdit18: "Save to Gallery:输入文件名(默认:wfs-image-YYYYMMDDHHmmss)将合成PNG保存到Gallery根文件夹(在设置 > Gallery Output Folder中配置);需要至少打开过一次Gallery标签以设置根目录",
|
||||
helpImageEdit19: "Save to Input:通过/upload/image将合成PNG上传到ComfyUI的输入文件夹,使其可在Load Image节点中选择",
|
||||
helpImageEdit19b: "Send to Workflow:与 Save to Input 一样会上传合成PNG,但还会将上传结果的文件名写入 ComfyUI 图中当前选中节点(若无选中节点则回退到找到的第一个 LoadImage 类节点)的 image 部件;需要通过 ComfyUI 顶部菜单按钮打开 Workflow Studio",
|
||||
helpImageEdit19b: "Send to LI node:通过 /upload/image 将合成PNG上传到ComfyUI的输入文件夹,并将上传结果的文件名写入 ComfyUI 图中当前选中节点(若无选中节点则回退到找到的第一个 LoadImage 类节点)的 image 部件;需要通过 ComfyUI 顶部菜单按钮打开 Workflow Studio",
|
||||
helpImageEdit20: "画布导航:滚轮缩放,Space+拖动平移",
|
||||
helpImageEdit21: "撤销/重做:Ctrl+Z / Ctrl+Y(或工具栏的 ↩ ↪ 按钮)",
|
||||
helpImageEditKbdTitle: "图像编辑键盘快捷键",
|
||||
@@ -3681,13 +3928,20 @@ const LANGUAGES = {
|
||||
eagleUrl: "Eagle URL",
|
||||
eagleAutoSave: "自动保存生成图片到Eagle",
|
||||
dataManagement: "数据管理",
|
||||
dataManagementHint: "将所有插件数据(设置、元数据、提示词等)导出为JSON文件,或导入之前导出的文件以恢复数据。导入后请重启ComfyUI以使更改生效。",
|
||||
dataManagementHint: "将设置、元数据、提示词等主要数据导出为JSON文件,或导入之前导出的文件以恢复数据。导入后请重启ComfyUI以使更改生效。",
|
||||
exportData: "导出",
|
||||
importData: "导入",
|
||||
exportSuccess: "导出完成",
|
||||
exportError: "导出失败",
|
||||
importSuccess: "导入完成",
|
||||
importError: "导入失败",
|
||||
exportFullBackup: "完整备份 (ZIP)",
|
||||
importFullBackup: "恢复完整备份 (ZIP)",
|
||||
fullBackupHint: "完整备份(ZIP)还包含上方JSON导出无法表示的文件夹型数据(AI Skills、Lab Plan、Style预设)以及Tagger数据库。恢复会覆盖现有文件,恢复后请重启ComfyUI。",
|
||||
fullBackupFilesUnit: "个文件",
|
||||
fullBackupIncludeWorkflows: "包含默认工作流文件夹 (user/default/workflows)",
|
||||
fullBackupIncludeWildcard: "包含Impact Pack的wildcard文件夹(如存在)",
|
||||
fullBackupExternalHint: "这些仅用于导出,恢复备份时不会处理它们。如需要,请从ZIP中的_external/文件夹手动复制回去。",
|
||||
saveSettings: "保存设置",
|
||||
settingsSaved: "设置已保存",
|
||||
ollamaSaved: "AI助手设置已保存",
|
||||
@@ -3756,8 +4010,8 @@ const LANGUAGES = {
|
||||
metaSectionTe: "文本编码器",
|
||||
metaSectionLora: "LoRA",
|
||||
metaSectionPrompt: "提示词",
|
||||
metaDropMain: "拖放图片 / JSON",
|
||||
metaDropSub: "PNG · WebP · JSON",
|
||||
metaDropMain: "拖放图片 / 视频 / JSON",
|
||||
metaDropSub: "PNG · WebP · MP4 · JSON",
|
||||
metaParsing: "解析中...",
|
||||
metaFileTooLarge: "文件过大(最大 50 MB)",
|
||||
metaNoMetadata: "未找到元数据",
|
||||
@@ -3767,7 +4021,7 @@ const LANGUAGES = {
|
||||
metaPromptText: "文本",
|
||||
metaSelectPrompt: "选择上方提示词以查看全文...",
|
||||
metaFormatNoteTitle: "支持格式",
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
|
||||
metaFmtComfyui: "ComfyUI — PNG / WebP / MP4 / JSON",
|
||||
metaFmtSdwebui: "SD WebUI / SD Forge — PNG(parameters)",
|
||||
metaFmtFooocus: "Fooocus — PNG(fooocus_scheme)",
|
||||
metaFormatTodo: "已支持 Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 子图工作流",
|
||||
@@ -3777,7 +4031,59 @@ const LANGUAGES = {
|
||||
helpMetadata2: "中列:从工作流中提取的 Checkpoint、VAE、扩散模型、文本编码器列表",
|
||||
helpMetadata3: "右列:LoRA 列表(含强度值)和提示词列表(点击在下方显示全文)",
|
||||
helpMetadata4: "提示词操作 — Copy:复制到剪贴板;GenUI:P/N:设置 GenerateUI 正/负向提示;Prompt:P/N:设置 Prompt 标签页预设正/负向提示",
|
||||
helpMetadata5: "支持格式:ComfyUI PNG/WebP/JSON(标准格式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 子图工作流)、SD WebUI、SD Forge、Fooocus。",
|
||||
helpMetadata5: "支持格式:ComfyUI PNG/WebP/MP4/JSON(标准格式 + Flux.2 / Qwen-Image / Z-Image / Mage-Flow / LongCat / Boogu / HiDream E1 / FireRed / Ernie Image 子图工作流;MP4读取SaveVideo写入的workflow/prompt容器标签,也支持MiniMax H3 ImageToVideo等一体化视频节点)、SD WebUI、SD Forge、Fooocus。",
|
||||
helpMetadata6: "现在它是 Gallery 标签页内的一个子标签(位于 Style_Catalog 右侧),不再是独立的顶层标签页 — 其余操作方式完全相同",
|
||||
|
||||
// -- Video 标签页(MiniMax H3 专用,独立于 GenerateUI) --
|
||||
tabVideo: "Video",
|
||||
loadInVideo: "加载到Video",
|
||||
videoTitle: "Video (MiniMax H3)",
|
||||
videoFirstFrame: "First Frame(可选)",
|
||||
videoLastFrame: "Last Frame(可选)",
|
||||
videoDropOrClick: "拖放图片或点击选择",
|
||||
videoPrompt: "Prompt",
|
||||
videoAspectRatio: "Aspect Ratio",
|
||||
videoMegapixels: "Megapixels",
|
||||
videoMultiple: "Multiple",
|
||||
videoDuration: "Duration(秒)",
|
||||
videoNoiseSeed: "Noise Seed",
|
||||
videoRandomizeSeed: "随机化",
|
||||
videoGenerate: "Generate",
|
||||
videoEditPlaceholder: "编辑功能即将推出",
|
||||
videoNoWorkflowLoaded: "尚未加载工作流",
|
||||
videoUnsupportedWorkflow: "该工作流不包含 MiniMax H3 的 Image to Video 节点",
|
||||
videoWorkflowLoaded: (name) => `已加载工作流: ${name}`,
|
||||
videoGenerateSuccess: "视频已生成",
|
||||
videoDoneCount: (n) => `完成(${n} 个文件)`,
|
||||
videoSourceLabel: "视频来源",
|
||||
videoSourceHint: "无需生成,也可在此加载任意视频以使用Frame/GIF工具。",
|
||||
videoSourceDropLabel: "拖放视频或点击选择",
|
||||
videoNoVideo: "暂无视频",
|
||||
videoPropFrame: "Frame",
|
||||
videoPropGif: "GIF",
|
||||
videoFrameHint: "播放/拖动预览视频到想要的位置,然后点击截取。",
|
||||
videoCaptureFrame: "截取当前帧",
|
||||
videoSaveToOutput: "保存到Output",
|
||||
videoFrameSaved: (name) => `已保存帧: ${name}`,
|
||||
videoFrameCaptureFailed: (msg) => `截取帧失败: ${msg}`,
|
||||
videoGifStart: "开始(秒)",
|
||||
videoGifEnd: "结束(秒)",
|
||||
videoGifFps: "FPS",
|
||||
videoGifWidth: "最大宽度",
|
||||
videoConvertToGif: "转换为GIF",
|
||||
videoGifConverting: "转换中...",
|
||||
videoGifDoneCount: (n) => `完成(${n} 帧)`,
|
||||
videoGifSaved: (name) => `已保存GIF: ${name}`,
|
||||
videoNoSourceLoaded: "请先准备视频(生成一个,或将文件拖入视频来源)",
|
||||
helpVideoTitle: "Video 标签页",
|
||||
helpVideoDesc: "MiniMax H3(Image to Video)工作流专用的视频生成标签页,独立于 GenerateUI 标签页运行。",
|
||||
helpVideo1: "设置 first_frame(可选)/last_frame(可选)图片、prompt、aspect_ratio/megapixels/multiple(分辨率)、duration(秒)和 noise_seed,然后点击 Generate。不指定 first_frame 时将以纯文本方式绕过执行,也支持完全没有 LoadImage 节点的工作流。",
|
||||
helpVideo2: "模型选择(UNet/CLIP/VAE/Audio VAE)固定为所加载工作流的默认值,无法在此标签页中修改。",
|
||||
helpVideo3: "可通过 Workflow 标签页详情弹窗中的「加载到Video」按钮,加载包含 MiniMax H3 Image to Video 节点的工作流。不支持的工作流会显示错误。",
|
||||
helpVideo4: "生成的视频(mp4)也可以在 Gallery 标签页中浏览和播放。",
|
||||
helpVideo5: "预览旁边的「Video Source」面板可以拖放或选择任意本地视频(不仅限于生成的视频),无需先运行MiniMax H3即可使用下方的Frame/GIF工具。点击Clear可重置选择。",
|
||||
helpVideo6: "Frame标签页(右侧面板):播放/拖动预览视频到想要的位置,点击「Capture Frame」在客户端截取该帧,再点击「Save to Output」将其保存为PNG到ComfyUI的输出文件夹。",
|
||||
helpVideo7: "GIF标签页(右侧面板):设置Start/End(秒)、FPS和可选的Max Width,点击「Convert to GIF」后,服务器会解码视频并将动画GIF保存到ComfyUI的输出文件夹。",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1112,7 +1112,6 @@ class ImageEditTab {
|
||||
document.getElementById("ie-redo-btn")?.addEventListener("click", () => this._redo());
|
||||
document.getElementById("ie-save-btn")?.addEventListener("click", () => this._fileExport.savePng());
|
||||
document.getElementById("ie-save-gallery-btn")?.addEventListener("click", () => this._fileExport.saveToGallery());
|
||||
document.getElementById("ie-upload-comfy-btn")?.addEventListener("click", () => this._fileExport.uploadToComfyUI());
|
||||
document.getElementById("ie-send-workflow-btn")?.addEventListener("click", () => this._fileExport.sendToWorkflow());
|
||||
document.getElementById("ie-zoom-fit")?.addEventListener("click", () => this._fitToView());
|
||||
document.getElementById("ie-zoom-100")?.addEventListener("click", () => {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* Image Edit Tab - File Export
|
||||
* Composites the visible layers (respecting mask-apply clipping) into a
|
||||
* flat canvas and offers three destinations: local PNG download, WFS
|
||||
* Gallery, and ComfyUI's own /upload/image endpoint.
|
||||
* Gallery, and the selected node's image widget on the ComfyUI canvas
|
||||
* (Send to LI node).
|
||||
*/
|
||||
|
||||
import { showToast } from "../app.js";
|
||||
@@ -104,24 +105,7 @@ export class FileExport {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadToComfyUI() {
|
||||
if (!this._cb.getLayerManager()) { showToast("No image loaded", "error"); return; }
|
||||
const canvas = this._buildCompositeCanvas();
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, "image/png"));
|
||||
const file = new File([blob], (this._cb.getBaseName() || "wfs-edit") + "-output.png", { type: "image/png" });
|
||||
const form = new FormData();
|
||||
form.append("image", file);
|
||||
form.append("overwrite", "true");
|
||||
try {
|
||||
const r = await fetch("/upload/image", { method: "POST", body: form });
|
||||
const data = await r.json();
|
||||
showToast(`Uploaded: ${data.name}`, "success");
|
||||
} catch {
|
||||
showToast("Upload failed", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Uploads the composite like uploadToComfyUI(), then (via window.opener,
|
||||
// Uploads the composite via /upload/image, then (via window.opener,
|
||||
// since WFS runs in its own window — same cross-window pattern as
|
||||
// wfmReceiveWorkflow/wfmOpenMaskEditorForNode) writes the resulting
|
||||
// filename into the "image" widget of the currently selected ComfyUI
|
||||
@@ -144,9 +128,9 @@ export class FileExport {
|
||||
const data = await r.json();
|
||||
const filename = data.subfolder ? `${data.subfolder}/${data.name}` : data.name;
|
||||
window.opener.wfmSendImageToSelectedNode(filename);
|
||||
showToast(`Sent "${filename}" to workflow`, "success");
|
||||
showToast(`Sent "${filename}" to LI node`, "success");
|
||||
} catch (err) {
|
||||
showToast(`Send to workflow failed: ${err.message}`, "error");
|
||||
showToast(`Send to LI node failed: ${err.message}`, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+577
-223
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,81 @@ export async function readAllPNGTextChunks(file) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// ── MP4 container metadata (moov/udta/meta の mdta keys/ilst) ──
|
||||
// ComfyUIのSaveVideoはPNGのtEXtチャンクと同じキー名(workflow=UI形式, prompt=API形式)で
|
||||
// JSON文字列をisobmff(mp4)のudta/meta(mdta)にコンテナレベルタグとして書き込む(FFmpegの
|
||||
// movflags=+use_metadata_tags)。ここではPyAV等に頼らずブラウザ側でボックスを直接辿って読む。
|
||||
function findMP4Box(view, bytes, start, end, targetType) {
|
||||
let offset = start;
|
||||
while (offset + 8 <= end) {
|
||||
const size = view.getUint32(offset);
|
||||
const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
|
||||
let boxSize = size;
|
||||
let bodyStart = offset + 8;
|
||||
if (size === 1) {
|
||||
if (offset + 16 > end) break;
|
||||
const hi = view.getUint32(offset + 8);
|
||||
const lo = view.getUint32(offset + 12);
|
||||
boxSize = hi * 4294967296 + lo;
|
||||
bodyStart = offset + 16;
|
||||
} else if (size === 0) {
|
||||
boxSize = end - offset;
|
||||
}
|
||||
if (boxSize < 8) break;
|
||||
if (type === targetType) return { bodyStart, end: offset + boxSize };
|
||||
offset += boxSize;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function readMP4MetaTags(file) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const view = new DataView(buffer);
|
||||
if (bytes.length < 12) return null;
|
||||
const ftyp = String.fromCharCode(bytes[4], bytes[5], bytes[6], bytes[7]);
|
||||
if (ftyp !== "ftyp") return null;
|
||||
|
||||
const moov = findMP4Box(view, bytes, 0, bytes.length, "moov");
|
||||
if (!moov) return null;
|
||||
const udta = findMP4Box(view, bytes, moov.bodyStart, moov.end, "udta");
|
||||
let meta = udta ? findMP4Box(view, bytes, udta.bodyStart, udta.end, "meta") : null;
|
||||
if (!meta) meta = findMP4Box(view, bytes, moov.bodyStart, moov.end, "meta");
|
||||
if (!meta) return null;
|
||||
const metaBody = meta.bodyStart + 4; // meta は FullBox (version+flags 4バイト)
|
||||
const keysBox = findMP4Box(view, bytes, metaBody, meta.end, "keys");
|
||||
const ilstBox = findMP4Box(view, bytes, metaBody, meta.end, "ilst");
|
||||
if (!keysBox || !ilstBox) return null;
|
||||
|
||||
const utf8 = new TextDecoder("utf-8", { fatal: false });
|
||||
let off = keysBox.bodyStart + 4; // version+flags をスキップ
|
||||
const count = view.getUint32(off); off += 4;
|
||||
const keyNames = [];
|
||||
for (let i = 0; i < count && off + 8 <= keysBox.end; i++) {
|
||||
const ksize = view.getUint32(off);
|
||||
if (ksize < 8) break;
|
||||
keyNames.push(utf8.decode(bytes.slice(off + 8, off + ksize)));
|
||||
off += ksize;
|
||||
}
|
||||
|
||||
const result = {};
|
||||
let ioff = ilstBox.bodyStart;
|
||||
while (ioff + 8 <= ilstBox.end) {
|
||||
const itemSize = view.getUint32(ioff);
|
||||
const keyIndex = view.getUint32(ioff + 4);
|
||||
if (itemSize < 8) break;
|
||||
const itemEnd = Math.min(ioff + itemSize, ilstBox.end);
|
||||
const data = findMP4Box(view, bytes, ioff + 8, itemEnd, "data");
|
||||
if (data) {
|
||||
const payload = bytes.slice(data.bodyStart + 8, data.end); // type(4)+locale(4)をスキップ
|
||||
const name = keyNames[keyIndex - 1];
|
||||
if (name) { try { result[name] = utf8.decode(payload); } catch { /* skip */ } }
|
||||
}
|
||||
ioff = itemEnd;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Workflow extraction helpers ───────────────────────────────
|
||||
function collectUnique(arr) {
|
||||
const seen = new Set(), out = [];
|
||||
@@ -203,7 +278,7 @@ function resolveEditPlusText(mode, textEdit, text1, text2) {
|
||||
}
|
||||
}
|
||||
|
||||
function extractPrompts(wf) {
|
||||
export function extractPrompts(wf) {
|
||||
if (!wf || typeof wf !== "object") return { positives: [], negatives: [] };
|
||||
return Array.isArray(wf.nodes) ? extractPromptsLiteGraph(wf) : extractPromptsAPI(wf);
|
||||
}
|
||||
@@ -459,6 +534,13 @@ function extractPromptsAPI(wf) {
|
||||
if (combined) textMap.set(id, combined);
|
||||
continue;
|
||||
}
|
||||
// MiniMaxH3ImageToVideo等の動画生成オールインワンノード。CLIPTextEncode等を経由せず
|
||||
// 自ノードの"prompt"入力に直接テキストを持つ(positive/negativeの区別も無い)ため、
|
||||
// isTextEncoderNode判定を通さずtextMapへ直接投入する(fallbackのtextsに載る)。
|
||||
if (typeof n.inputs?.prompt === "string" && /ImageToVideo|TextToVideo/i.test(ct)) {
|
||||
textMap.set(id, n.inputs.prompt);
|
||||
continue;
|
||||
}
|
||||
if (!isTextEncoderNode(ct)) continue;
|
||||
// "prompt" — TextEncodeQwenImageEdit(Plus)/TextEncodeBooguEdit and similar Image Edit
|
||||
// model text encoders use this key instead of "text"/"text_g".
|
||||
@@ -607,6 +689,7 @@ export async function extractAllMetadata(file) {
|
||||
const name = file.name.toLowerCase();
|
||||
const isJSON = file.type === "application/json" || name.endsWith(".json");
|
||||
const isWebP = file.type === "image/webp" || name.endsWith(".webp");
|
||||
const isMP4 = file.type === "video/mp4" || name.endsWith(".mp4");
|
||||
|
||||
async function fromWorkflow(originalWf, source) {
|
||||
let wf = originalWf;
|
||||
@@ -644,6 +727,17 @@ export async function extractAllMetadata(file) {
|
||||
const wf = extractWorkflowFromEXIF(exif);
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
if (isMP4) {
|
||||
let tags; try { tags = await readMP4MetaTags(file); } catch { return null; }
|
||||
if (!tags) return null;
|
||||
for (const key of ["prompt", "workflow"]) {
|
||||
const raw = tags[key];
|
||||
if (!raw) continue;
|
||||
let wf; try { wf = JSON.parse(sanitizeJSON(raw)); } catch { continue; }
|
||||
if (wf) return await fromWorkflow(wf, "comfyui");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// PNG
|
||||
const chunks = await readAllPNGTextChunks(file);
|
||||
if (!chunks) return null;
|
||||
@@ -693,7 +787,7 @@ function buildLoRAItem(lora) {
|
||||
return el;
|
||||
}
|
||||
|
||||
function buildPromptItem(label, type, full, fullArea, fullLabel, listEl) {
|
||||
export function buildPromptItem(label, type, full, fullArea, fullLabel, listEl) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "wfm-meta-item wfm-meta-item-clickable";
|
||||
const snippet = label.length > 60 ? label.slice(0, 60) + "…" : label;
|
||||
@@ -723,7 +817,8 @@ function renderSection(sectionEl, listEl, items, buildFn) {
|
||||
let _externalHandleFile = null;
|
||||
|
||||
export async function loadFileIntoMetadataTab(file) {
|
||||
document.querySelector('.wfm-tab[data-tab="metadata"]')?.click();
|
||||
document.querySelector('.wfm-tab[data-tab="gallery"]')?.click();
|
||||
document.querySelector('.wfm-gallery-subtab-btn[data-gallery-subtab="metadata"]')?.click();
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
if (_externalHandleFile) await _externalHandleFile(file);
|
||||
}
|
||||
@@ -790,6 +885,7 @@ export function initMetadataTab() {
|
||||
"wfm-help-metadata-3": "helpMetadata3",
|
||||
"wfm-help-metadata-4": "helpMetadata4",
|
||||
"wfm-help-metadata-5": "helpMetadata5",
|
||||
"wfm-help-metadata-6": "helpMetadata6",
|
||||
};
|
||||
for (const [id, key] of Object.entries(helpIds)) {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* Prompt Tab - AI Assistant (Ollama / LM Studio chat, translate, apply to GenerateUI)
|
||||
*/
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { getAiBackendDefaultUrl } from "./util.js";
|
||||
|
||||
// ============================================
|
||||
// State
|
||||
// ============================================
|
||||
|
||||
const ollamaState = {
|
||||
chatHistory: [],
|
||||
attachedImage: null,
|
||||
};
|
||||
|
||||
const PROMPT_AI_KEY = "wfm_prompt_ai_settings";
|
||||
|
||||
function loadPromptAiSettings() {
|
||||
try { return JSON.parse(localStorage.getItem(PROMPT_AI_KEY) || "{}"); } catch { return {}; }
|
||||
}
|
||||
|
||||
// バックエンド変更検知用: モデル一覧を取得した時点のバックエンド名を記録
|
||||
let _promptAiBackendForModels = null;
|
||||
|
||||
export function getPromptAiConfig() {
|
||||
const s = loadPromptAiSettings();
|
||||
const backend = s.backend || "ollama";
|
||||
const url = (s.backendUrl || getAiBackendDefaultUrl(backend)).replace(/\/$/, "");
|
||||
return { backend, url };
|
||||
}
|
||||
|
||||
export async function fetchAiModels() {
|
||||
const { backend, url } = getPromptAiConfig();
|
||||
if (backend === "ollama") {
|
||||
const res = await fetch(`${url}/api/tags`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return (await res.json()).models?.map(m => m.name) || [];
|
||||
} else {
|
||||
const res = await fetch(`${url}/v1/models`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return ((await res.json()).data || []).map(m => m.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function chatWithAi(model, messages) {
|
||||
const { backend, url } = getPromptAiConfig();
|
||||
if (backend === "ollama") {
|
||||
const res = await fetch(`${url}/api/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model, messages, stream: false }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return (await res.json()).message?.content || "";
|
||||
} else {
|
||||
// LM Studio / Lemonade (OpenAI-compatible): convert Ollama-style messages (with .images) to OpenAI content arrays
|
||||
const openAiMessages = messages.map(msg => {
|
||||
if (msg.images?.length) {
|
||||
return {
|
||||
role: msg.role,
|
||||
content: [
|
||||
{ type: "text", text: msg.content },
|
||||
...msg.images.map(b64 => ({ type: "image_url", image_url: { url: `data:image/jpeg;base64,${b64}` } })),
|
||||
],
|
||||
};
|
||||
}
|
||||
return { role: msg.role, content: msg.content };
|
||||
});
|
||||
const res = await fetch(`${url}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model, messages: openAiMessages, stream: false }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return (await res.json()).choices?.[0]?.message?.content || "";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Chat UI
|
||||
// ============================================
|
||||
|
||||
function addChatMessage(role, content) {
|
||||
const chat = document.getElementById("wfm-ollama-chat");
|
||||
if (!chat) return;
|
||||
|
||||
const welcome = chat.querySelector(".wfm-ollama-welcome");
|
||||
if (welcome) welcome.remove();
|
||||
|
||||
const msg = document.createElement("div");
|
||||
msg.className = `wfm-ollama-msg ${role}`;
|
||||
|
||||
const roleLabel = document.createElement("div");
|
||||
roleLabel.className = "wfm-ollama-msg-role";
|
||||
roleLabel.textContent = role === "user" ? t("you") : "AI";
|
||||
msg.appendChild(roleLabel);
|
||||
|
||||
const contentDiv = document.createElement("div");
|
||||
contentDiv.className = "wfm-ollama-msg-content";
|
||||
contentDiv.textContent = content;
|
||||
msg.appendChild(contentDiv);
|
||||
|
||||
// Apply button for assistant messages
|
||||
if (role === "assistant") {
|
||||
const applyRow = document.createElement("div");
|
||||
applyRow.className = "wfm-ollama-apply-row";
|
||||
|
||||
const applyBtn = document.createElement("button");
|
||||
applyBtn.className = "wfm-btn wfm-btn-sm wfm-btn-primary";
|
||||
applyBtn.textContent = t("applyToGenerateUI");
|
||||
applyBtn.addEventListener("click", () => applyToGenerateUI(content));
|
||||
applyRow.appendChild(applyBtn);
|
||||
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "wfm-btn wfm-btn-sm";
|
||||
copyBtn.textContent = t("copy");
|
||||
copyBtn.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
copyBtn.textContent = t("copied");
|
||||
setTimeout(() => { copyBtn.textContent = t("copy"); }, 1500);
|
||||
});
|
||||
});
|
||||
applyRow.appendChild(copyBtn);
|
||||
|
||||
msg.appendChild(applyRow);
|
||||
}
|
||||
|
||||
chat.appendChild(msg);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
ollamaState.chatHistory = [];
|
||||
ollamaState.attachedImage = null;
|
||||
updateAttachmentDisplay();
|
||||
|
||||
const chat = document.getElementById("wfm-ollama-chat");
|
||||
if (chat) {
|
||||
chat.innerHTML = `
|
||||
<div class="wfm-ollama-welcome">
|
||||
<p>AI Assistant</p>
|
||||
<p>${t("assistantWelcome")}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Image Attachment
|
||||
// ============================================
|
||||
|
||||
function attachFile(file) {
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
ollamaState.attachedImage = { name: file.name, dataUrl: e.target.result };
|
||||
updateAttachmentDisplay();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function updateAttachmentDisplay() {
|
||||
const container = document.getElementById("wfm-ollama-attachments");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
|
||||
if (!ollamaState.attachedImage) return;
|
||||
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-ollama-attachment";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = ollamaState.attachedImage.dataUrl;
|
||||
item.appendChild(img);
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "wfm-ollama-attachment-name";
|
||||
name.textContent = ollamaState.attachedImage.name;
|
||||
item.appendChild(name);
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "wfm-ollama-attachment-remove";
|
||||
removeBtn.textContent = "×";
|
||||
removeBtn.addEventListener("click", () => {
|
||||
ollamaState.attachedImage = null;
|
||||
updateAttachmentDisplay();
|
||||
});
|
||||
item.appendChild(removeBtn);
|
||||
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Send Message
|
||||
// ============================================
|
||||
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById("wfm-ollama-input");
|
||||
const modelSelect = document.getElementById("wfm-ollama-model");
|
||||
if (!input || !modelSelect) return;
|
||||
|
||||
const message = input.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// バックエンドが変更されていたらモデル一覧を自動更新
|
||||
const { backend } = getPromptAiConfig();
|
||||
if (backend !== _promptAiBackendForModels) {
|
||||
const status = document.getElementById("wfm-ollama-status");
|
||||
if (status) { status.textContent = `Switching to ${backend}...`; status.className = "wfm-ollama-status"; }
|
||||
try {
|
||||
const models = await fetchAiModels();
|
||||
const savedModel = loadPromptAiSettings().model || "";
|
||||
modelSelect.innerHTML = models.length
|
||||
? models.map(name => `<option value="${name}" ${name === savedModel ? "selected" : ""}>${name}</option>`).join("")
|
||||
: `<option value="">${t("noModelsFound")}</option>`;
|
||||
_promptAiBackendForModels = backend;
|
||||
if (status) { status.textContent = `${models.length} models [${backend}]`; status.className = "wfm-ollama-status connected"; }
|
||||
} catch {
|
||||
if (status) { status.textContent = t("failedConnect"); status.className = "wfm-ollama-status error"; }
|
||||
}
|
||||
}
|
||||
|
||||
const model = modelSelect.value;
|
||||
if (!model) {
|
||||
showToast(t("selectModelFirst"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
addChatMessage("user", message);
|
||||
input.value = "";
|
||||
|
||||
const userMsg = { role: "user", content: message };
|
||||
if (ollamaState.attachedImage) {
|
||||
userMsg.images = [ollamaState.attachedImage.dataUrl.split(",")[1]];
|
||||
}
|
||||
ollamaState.chatHistory.push(userMsg);
|
||||
|
||||
const sendBtn = document.getElementById("wfm-ollama-send-btn");
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const reply = await chatWithAi(model, ollamaState.chatHistory);
|
||||
addChatMessage("assistant", reply);
|
||||
ollamaState.chatHistory.push({ role: "assistant", content: reply });
|
||||
ollamaState.attachedImage = null;
|
||||
updateAttachmentDisplay();
|
||||
} catch (err) {
|
||||
showToast(t("error") + ": " + err.message, "error");
|
||||
} finally {
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Translation
|
||||
// ============================================
|
||||
|
||||
async function sendTranslate(direction) {
|
||||
const input = document.getElementById("wfm-ollama-input");
|
||||
const modelSelect = document.getElementById("wfm-ollama-model");
|
||||
if (!input || !modelSelect) return;
|
||||
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
const model = modelSelect.value;
|
||||
if (!model) {
|
||||
showToast(t("selectModelFirst"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = { ja2en: "JA→EN", en2ja: "EN→JA", zh2en: "ZH→EN", en2zh: "EN→ZH" };
|
||||
addChatMessage("user", `[${labels[direction] || direction}]\n${text}`);
|
||||
input.value = "";
|
||||
|
||||
const btns = ["wfm-ollama-send-btn", "wfm-ollama-ja2en-btn", "wfm-ollama-en2ja-btn", "wfm-ollama-zh2en-btn", "wfm-ollama-en2zh-btn"]
|
||||
.map((id) => document.getElementById(id));
|
||||
btns.forEach((b) => { if (b) b.disabled = true; });
|
||||
|
||||
try {
|
||||
const prompts = {
|
||||
ja2en: `Translate the following Japanese text into English. Output only the translated text, nothing else.\n\n${text}`,
|
||||
en2ja: `以下の英語のテキストを日本語に翻訳してください。翻訳結果のみを出力し、他の説明は不要です。\n\n${text}`,
|
||||
zh2en: `Translate the following Chinese text into English. Output only the translated text, nothing else.\n\n${text}`,
|
||||
en2zh: `请将以下英文翻译成中文。只输出翻译结果,不需要其他说明。\n\n${text}`,
|
||||
};
|
||||
const prompt = prompts[direction];
|
||||
|
||||
const reply = await chatWithAi(model, [{ role: "user", content: prompt }]);
|
||||
addChatMessage("assistant", reply);
|
||||
} catch (err) {
|
||||
showToast(t("error") + ": " + err.message, "error");
|
||||
} finally {
|
||||
btns.forEach((b) => { if (b) b.disabled = false; });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Apply to GenerateUI
|
||||
// ============================================
|
||||
|
||||
function applyToGenerateUI(text) {
|
||||
const textareas = document.querySelectorAll("#wfm-gen-prompt-fields textarea");
|
||||
if (textareas.length === 0) {
|
||||
showToast(t("noPromptFields"), "error");
|
||||
return;
|
||||
}
|
||||
textareas[0].value = text;
|
||||
textareas[0].dispatchEvent(new Event("input", { bubbles: true }));
|
||||
textareas[0].dispatchEvent(new Event("change", { bubbles: true }));
|
||||
showToast(t("appliedToGenerateUI"), "success");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI wiring (called from prompt-tab.js's initPromptTab)
|
||||
// ============================================
|
||||
|
||||
export function initAiChatUI() {
|
||||
// AI model refresh
|
||||
async function refreshModels() {
|
||||
const select = document.getElementById("wfm-ollama-model");
|
||||
const status = document.getElementById("wfm-ollama-status");
|
||||
const { backend } = getPromptAiConfig();
|
||||
try {
|
||||
const models = await fetchAiModels();
|
||||
const savedModel = loadPromptAiSettings().model || "";
|
||||
if (select) {
|
||||
select.innerHTML = models.length
|
||||
? models.map(name => `<option value="${name}" ${name === savedModel ? "selected" : ""}>${name}</option>`).join("")
|
||||
: `<option value="">${t("noModelsFound")}</option>`;
|
||||
}
|
||||
_promptAiBackendForModels = backend;
|
||||
if (status) {
|
||||
status.textContent = `${models.length} models [${backend}]`;
|
||||
status.className = "wfm-ollama-status connected";
|
||||
}
|
||||
} catch {
|
||||
if (status) {
|
||||
status.textContent = t("failedConnect");
|
||||
status.className = "wfm-ollama-status error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-load models on init
|
||||
refreshModels();
|
||||
|
||||
document.getElementById("wfm-ollama-refresh-btn")?.addEventListener("click", refreshModels);
|
||||
|
||||
// Test connection
|
||||
document.getElementById("wfm-ollama-test-btn")?.addEventListener("click", async () => {
|
||||
const status = document.getElementById("wfm-ollama-status");
|
||||
if (status) { status.textContent = "Testing..."; status.className = "wfm-ollama-status"; }
|
||||
try {
|
||||
await fetchAiModels();
|
||||
if (status) { status.textContent = t("connectedCheck"); status.className = "wfm-ollama-status connected"; }
|
||||
await refreshModels();
|
||||
} catch {
|
||||
if (status) { status.textContent = t("failedConnect"); status.className = "wfm-ollama-status error"; }
|
||||
}
|
||||
});
|
||||
|
||||
// Send message
|
||||
document.getElementById("wfm-ollama-send-btn")?.addEventListener("click", sendMessage);
|
||||
|
||||
// Enter to send
|
||||
document.getElementById("wfm-ollama-input")?.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// Clear chat
|
||||
document.getElementById("wfm-ollama-clear-btn")?.addEventListener("click", clearChat);
|
||||
|
||||
// File attachment
|
||||
const fileInput = document.getElementById("wfm-ollama-file-input");
|
||||
document.getElementById("wfm-ollama-attach-btn")?.addEventListener("click", () => fileInput?.click());
|
||||
fileInput?.addEventListener("change", () => {
|
||||
if (fileInput.files.length > 0) {
|
||||
attachFile(fileInput.files[0]);
|
||||
fileInput.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
// Translation
|
||||
document.getElementById("wfm-ollama-ja2en-btn")?.addEventListener("click", () => sendTranslate("ja2en"));
|
||||
document.getElementById("wfm-ollama-en2ja-btn")?.addEventListener("click", () => sendTranslate("en2ja"));
|
||||
document.getElementById("wfm-ollama-zh2en-btn")?.addEventListener("click", () => sendTranslate("zh2en"));
|
||||
document.getElementById("wfm-ollama-en2zh-btn")?.addEventListener("click", () => sendTranslate("en2zh"));
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* Prompt Tab - Presets (API CRUD, groups, Preset Manager panel)
|
||||
*/
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { readJsonStorage } from "./util.js";
|
||||
|
||||
// ============================================
|
||||
// State
|
||||
// ============================================
|
||||
|
||||
// Preset data (loaded from API, with localStorage migration)
|
||||
let promptPresets = [];
|
||||
let pmActiveTab = "all"; // "all" | "favorites" | "groups"
|
||||
let pmSearchText = "";
|
||||
let pmSelectedId = null; // currently selected preset id
|
||||
export let pmGroups = {}; // { groupName: [presetId, ...] }
|
||||
const PM_GROUPS_KEY = "wfm_prompt_preset_groups";
|
||||
export const PROMPT_RESERVED_GROUPS = ["Batch"];
|
||||
|
||||
// ============================================
|
||||
// Preset API helpers
|
||||
// ============================================
|
||||
|
||||
export async function fetchPresets() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/prompts");
|
||||
return res.ok ? await res.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
export async function apiCreatePreset(data) {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/prompts", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await res.json();
|
||||
return result.status === "ok" ? result.prompt : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function apiUpdatePreset(id, updates) {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/prompts/update", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, ...updates }),
|
||||
});
|
||||
const result = await res.json();
|
||||
return result.status === "ok" ? result.prompt : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function apiDeletePreset(id) {
|
||||
try {
|
||||
await fetch("/api/wfm/prompts/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Migrate localStorage presets to API
|
||||
// ============================================
|
||||
|
||||
const PRESETS_KEY = "wfm_prompt_presets";
|
||||
|
||||
async function migrateLocalStoragePresets() {
|
||||
try {
|
||||
const raw = localStorage.getItem(PRESETS_KEY);
|
||||
if (!raw) return;
|
||||
const local = JSON.parse(raw);
|
||||
if (!Array.isArray(local) || local.length === 0) return;
|
||||
|
||||
// Only migrate if API has no presets yet
|
||||
const existing = await fetchPresets();
|
||||
if (existing.length > 0) {
|
||||
localStorage.removeItem(PRESETS_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const p of local) {
|
||||
await apiCreatePreset({
|
||||
name: p.name || "Untitled",
|
||||
text: p.posText || p.text || "",
|
||||
negText: p.negText || "",
|
||||
category: "",
|
||||
tags: [],
|
||||
favorite: false,
|
||||
});
|
||||
}
|
||||
localStorage.removeItem(PRESETS_KEY);
|
||||
} catch { /* ignore migration errors */ }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Preset data management
|
||||
// ============================================
|
||||
|
||||
export async function loadAllPresets() {
|
||||
await migrateLocalStoragePresets();
|
||||
promptPresets = await fetchPresets();
|
||||
|
||||
// Load groups from localStorage
|
||||
pmGroups = readJsonStorage(PM_GROUPS_KEY);
|
||||
|
||||
// Clean stale entries from groups (preserve reserved groups even if empty)
|
||||
const validIds = new Set(promptPresets.map(p => p.id));
|
||||
for (const g of Object.keys(pmGroups)) {
|
||||
pmGroups[g] = (pmGroups[g] || []).filter(id => validIds.has(id));
|
||||
if (pmGroups[g].length === 0 && !PROMPT_RESERVED_GROUPS.includes(g)) delete pmGroups[g];
|
||||
}
|
||||
// Ensure reserved groups always exist
|
||||
for (const g of PROMPT_RESERVED_GROUPS) {
|
||||
if (!pmGroups[g]) pmGroups[g] = [];
|
||||
}
|
||||
saveGroups();
|
||||
|
||||
renderPresetSelect();
|
||||
renderGroupSelect();
|
||||
renderPresetManager();
|
||||
}
|
||||
|
||||
export function saveGroups() {
|
||||
localStorage.setItem(PM_GROUPS_KEY, JSON.stringify(pmGroups));
|
||||
renderGroupSelect();
|
||||
}
|
||||
|
||||
function renderGroupSelect() {
|
||||
const select = document.getElementById("wfm-preset-group-select");
|
||||
if (!select) return;
|
||||
const prevVal = select.value;
|
||||
select.innerHTML = `<option value="">Select group...</option>`;
|
||||
for (const g of Object.keys(pmGroups).sort()) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = g;
|
||||
opt.textContent = g;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
if (prevVal && pmGroups[prevVal]) select.value = prevVal;
|
||||
}
|
||||
|
||||
function renderPresetSelect() {
|
||||
const select = document.getElementById("wfm-preset-select");
|
||||
if (!select) return;
|
||||
const prevVal = select.value;
|
||||
select.innerHTML = `<option value="">${t("newPreset")}</option>`;
|
||||
promptPresets.forEach((p) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
// Restore selection if still valid
|
||||
if (prevVal && promptPresets.find(p => p.id === prevVal)) {
|
||||
select.value = prevVal;
|
||||
}
|
||||
}
|
||||
|
||||
function selectPresetInEditor(preset) {
|
||||
const presetSelect = document.getElementById("wfm-preset-select");
|
||||
const presetName = document.getElementById("wfm-preset-name");
|
||||
const presetCategory = document.getElementById("wfm-preset-category");
|
||||
const presetPos = document.getElementById("wfm-preset-pos");
|
||||
const presetNeg = document.getElementById("wfm-preset-neg");
|
||||
|
||||
if (presetSelect) presetSelect.value = preset ? preset.id : "";
|
||||
if (presetName) presetName.value = preset ? preset.name : "";
|
||||
if (presetCategory) presetCategory.value = preset ? (preset.category || "") : "";
|
||||
if (presetPos) presetPos.value = preset ? (preset.text || preset.posText || "") : "";
|
||||
if (presetNeg) presetNeg.value = preset ? (preset.negText || "") : "";
|
||||
|
||||
pmSelectedId = preset ? preset.id : null;
|
||||
renderPresetManager();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Preset Manager rendering
|
||||
// ============================================
|
||||
|
||||
export function esc(s) {
|
||||
return s ? String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") : "";
|
||||
}
|
||||
|
||||
export function renderPresetManager() {
|
||||
const container = document.getElementById("wfm-pm-list");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
|
||||
switch (pmActiveTab) {
|
||||
case "all": renderPmAll(container); break;
|
||||
case "favorites": renderPmFavorites(container); break;
|
||||
case "groups": renderPmGroups(container); break;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesSearch(p) {
|
||||
if (!pmSearchText) return true;
|
||||
const s = pmSearchText.toLowerCase();
|
||||
return (p.name || "").toLowerCase().includes(s) ||
|
||||
(p.text || "").toLowerCase().includes(s) ||
|
||||
(p.category || "").toLowerCase().includes(s) ||
|
||||
(p.tags || []).some(t => t.toLowerCase().includes(s));
|
||||
}
|
||||
|
||||
// Ordered list of actual preset objects for a group (stale ids already filtered out by
|
||||
// loadAllPresets, but this is defensive for callers that read pmGroups directly too).
|
||||
export function getPresetsInGroup(groupName) {
|
||||
const ids = pmGroups[groupName] || [];
|
||||
return ids.map(id => promptPresets.find(p => p.id === id)).filter(Boolean);
|
||||
}
|
||||
|
||||
export function isInBatchPreset(id) {
|
||||
return (pmGroups["Batch"] || []).includes(id);
|
||||
}
|
||||
|
||||
export function toggleBatchPreset(id) {
|
||||
const batch = pmGroups["Batch"] || [];
|
||||
const idx = batch.indexOf(id);
|
||||
if (idx >= 0) { batch.splice(idx, 1); } else { batch.push(id); }
|
||||
pmGroups["Batch"] = batch;
|
||||
saveGroups();
|
||||
}
|
||||
|
||||
export function clearBatchPresets() {
|
||||
pmGroups["Batch"] = [];
|
||||
saveGroups();
|
||||
renderPresetManager();
|
||||
showToast(t("promptBatchClear"), "success");
|
||||
}
|
||||
|
||||
function createPmItem(preset) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "wfm-pm-item" + (pmSelectedId === preset.id ? " active" : "");
|
||||
|
||||
const previewText = (preset.text || "").length > 50
|
||||
? preset.text.substring(0, 50) + "..."
|
||||
: (preset.text || "");
|
||||
|
||||
const catBadge = preset.category
|
||||
? `<span style="font-size:9px;color:var(--wfm-primary);margin-left:4px;">[${esc(preset.category)}]</span>`
|
||||
: "";
|
||||
|
||||
const inBatch = isInBatchPreset(preset.id);
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="wfm-pm-item-body">
|
||||
<div class="wfm-pm-item-name">${preset.favorite ? '<span style="color:#ffd700;">★</span> ' : ""}${esc(preset.name)}${catBadge}</div>
|
||||
<div class="wfm-pm-item-sub">${esc(previewText)}</div>
|
||||
</div>
|
||||
<div class="wfm-pm-item-actions">
|
||||
<button class="wfm-pm-action-btn pm-batch-btn${inBatch ? " batch-active" : ""}" title="Batch">B</button>
|
||||
<button class="wfm-pm-action-btn pm-fav-btn${preset.favorite ? " fav-active" : ""}" title="Favorite">★</button>
|
||||
<button class="wfm-pm-action-btn pm-del-btn" title="Delete" style="color:var(--wfm-danger);">✕</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Click to select in editor
|
||||
el.addEventListener("click", (e) => {
|
||||
if (e.target.closest(".wfm-pm-item-actions")) return;
|
||||
selectPresetInEditor(preset);
|
||||
});
|
||||
|
||||
// Batch toggle
|
||||
el.querySelector(".pm-batch-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleBatchPreset(preset.id);
|
||||
renderPresetManager();
|
||||
});
|
||||
|
||||
// Favorite toggle
|
||||
el.querySelector(".pm-fav-btn").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const updated = await apiUpdatePreset(preset.id, { favorite: !preset.favorite });
|
||||
if (updated) {
|
||||
const idx = promptPresets.findIndex(p => p.id === preset.id);
|
||||
if (idx >= 0) promptPresets[idx] = updated;
|
||||
renderPresetManager();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete
|
||||
el.querySelector(".pm-del-btn").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Delete "${preset.name}"?`)) return;
|
||||
await apiDeletePreset(preset.id);
|
||||
promptPresets = promptPresets.filter(p => p.id !== preset.id);
|
||||
if (pmSelectedId === preset.id) {
|
||||
pmSelectedId = null;
|
||||
selectPresetInEditor(null);
|
||||
}
|
||||
renderPresetSelect();
|
||||
renderPresetManager();
|
||||
showToast(t("deleted"), "success");
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderPmAll(container) {
|
||||
const items = promptPresets.filter(matchesSearch);
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = `<div class="wfm-pm-empty">${promptPresets.length === 0 ? "No presets yet.<br><small>Create one in the Presets panel</small>" : "No matches"}</div>`;
|
||||
return;
|
||||
}
|
||||
for (const p of items) {
|
||||
container.appendChild(createPmItem(p));
|
||||
}
|
||||
}
|
||||
|
||||
function renderPmFavorites(container) {
|
||||
const favs = promptPresets.filter(p => p.favorite);
|
||||
const items = favs.filter(matchesSearch);
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = `<div class="wfm-pm-empty">${favs.length === 0 ? "No favorites yet.<br><small>Star presets to add them here</small>" : "No matches"}</div>`;
|
||||
return;
|
||||
}
|
||||
for (const p of items) {
|
||||
container.appendChild(createPmItem(p));
|
||||
}
|
||||
}
|
||||
|
||||
function renderPmGroups(container) {
|
||||
const groupNames = Object.keys(pmGroups).sort();
|
||||
|
||||
if (groupNames.length === 0) {
|
||||
container.innerHTML = `<div class="wfm-pm-empty">No groups yet.<br><small>Create groups from the Presets panel below</small></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const groupName of groupNames) {
|
||||
const ids = pmGroups[groupName] || [];
|
||||
const presets = ids.map(id => promptPresets.find(p => p.id === id)).filter(Boolean);
|
||||
|
||||
const section = document.createElement("div");
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "wfm-pm-group-header collapsed";
|
||||
header.innerHTML = `<span>${esc(groupName)}</span> <span class="wfm-pm-badge">${presets.length}</span>`;
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.style.display = "none";
|
||||
|
||||
header.addEventListener("click", () => {
|
||||
list.style.display = list.style.display === "none" ? "block" : "none";
|
||||
header.classList.toggle("collapsed");
|
||||
});
|
||||
|
||||
for (const p of presets) {
|
||||
const item = createPmItem(p);
|
||||
// Add remove-from-group button
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "wfm-pm-action-btn";
|
||||
removeBtn.title = "Remove from group";
|
||||
removeBtn.textContent = "➖";
|
||||
removeBtn.style.fontSize = "10px";
|
||||
removeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
pmGroups[groupName] = pmGroups[groupName].filter(id => id !== p.id);
|
||||
// 予約グループ (Batch) は空になってもキーを保持する
|
||||
if (pmGroups[groupName].length === 0 && !PROMPT_RESERVED_GROUPS.includes(groupName)) delete pmGroups[groupName];
|
||||
saveGroups();
|
||||
renderPresetManager();
|
||||
});
|
||||
item.querySelector(".wfm-pm-item-actions").prepend(removeBtn);
|
||||
list.appendChild(item);
|
||||
}
|
||||
section.appendChild(header);
|
||||
section.appendChild(list);
|
||||
container.appendChild(section);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI wiring (called from prompt-tab.js's initPromptTab)
|
||||
// ============================================
|
||||
|
||||
export function initPresetsUI() {
|
||||
loadAllPresets();
|
||||
|
||||
const presetSelect = document.getElementById("wfm-preset-select");
|
||||
const presetName = document.getElementById("wfm-preset-name");
|
||||
const presetCategory = document.getElementById("wfm-preset-category");
|
||||
const presetPos = document.getElementById("wfm-preset-pos");
|
||||
const presetNeg = document.getElementById("wfm-preset-neg");
|
||||
|
||||
// Load preset on selection
|
||||
presetSelect?.addEventListener("change", () => {
|
||||
const id = presetSelect.value;
|
||||
const p = promptPresets.find(pp => pp.id === id);
|
||||
selectPresetInEditor(p || null);
|
||||
});
|
||||
|
||||
// Copy positive prompt to clipboard
|
||||
document.getElementById("wfm-preset-copy-pos-btn")?.addEventListener("click", () => {
|
||||
const text = presetPos?.value || "";
|
||||
if (!text.trim()) {
|
||||
showToast(t("noTextToCopy"), "error");
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast(t("copiedToClipboard"), "success");
|
||||
});
|
||||
});
|
||||
|
||||
// Copy negative prompt to clipboard
|
||||
document.getElementById("wfm-preset-copy-neg-btn")?.addEventListener("click", () => {
|
||||
const text = presetNeg?.value || "";
|
||||
if (!text.trim()) {
|
||||
showToast(t("noTextToCopy"), "error");
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast(t("copiedToClipboard"), "success");
|
||||
});
|
||||
});
|
||||
|
||||
// Save preset (create or update via API)
|
||||
document.getElementById("wfm-preset-save-btn")?.addEventListener("click", async () => {
|
||||
const name = presetName?.value.trim();
|
||||
const category = presetCategory?.value.trim() || "";
|
||||
const pos = presetPos?.value || "";
|
||||
const neg = presetNeg?.value || "";
|
||||
|
||||
if (!name) {
|
||||
showToast(t("enterPresetName"), "error");
|
||||
return;
|
||||
}
|
||||
if (!pos.trim() && !neg.trim()) {
|
||||
showToast(t("noPromptToSave"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedId = presetSelect?.value;
|
||||
const existing = selectedId ? promptPresets.find(p => p.id === selectedId) : null;
|
||||
|
||||
if (existing) {
|
||||
// Update
|
||||
const updated = await apiUpdatePreset(existing.id, {
|
||||
name, text: pos, negText: neg, category,
|
||||
});
|
||||
if (updated) {
|
||||
const idx = promptPresets.findIndex(p => p.id === existing.id);
|
||||
if (idx >= 0) promptPresets[idx] = updated;
|
||||
showToast(t("presetSaved"), "success");
|
||||
}
|
||||
} else {
|
||||
// Create
|
||||
const created = await apiCreatePreset({
|
||||
name, text: pos, negText: neg, category,
|
||||
tags: [], favorite: false,
|
||||
});
|
||||
if (created) {
|
||||
promptPresets.push(created);
|
||||
showToast(t("presetSaved"), "success");
|
||||
}
|
||||
}
|
||||
|
||||
pmSelectedId = null;
|
||||
renderPresetSelect();
|
||||
renderPresetManager();
|
||||
if (presetSelect) presetSelect.value = "";
|
||||
});
|
||||
|
||||
// Apply preset to GenerateUI
|
||||
document.getElementById("wfm-preset-apply-btn")?.addEventListener("click", () => {
|
||||
const pos = presetPos?.value || "";
|
||||
const neg = presetNeg?.value || "";
|
||||
const textareas = document.querySelectorAll("#wfm-gen-prompt-fields textarea");
|
||||
if (textareas.length === 0) {
|
||||
showToast(t("noPromptFields"), "error");
|
||||
return;
|
||||
}
|
||||
let applied = false;
|
||||
if (textareas.length > 0 && pos) {
|
||||
textareas[0].value = pos;
|
||||
textareas[0].dispatchEvent(new Event("input", { bubbles: true }));
|
||||
applied = true;
|
||||
}
|
||||
if (textareas.length > 1 && neg) {
|
||||
textareas[1].value = neg;
|
||||
textareas[1].dispatchEvent(new Event("input", { bubbles: true }));
|
||||
applied = true;
|
||||
}
|
||||
if (applied) showToast(t("appliedToGenerateUI"), "success");
|
||||
});
|
||||
|
||||
// --- Group management (Presets side) ---
|
||||
document.getElementById("wfm-preset-new-group-btn")?.addEventListener("click", () => {
|
||||
const name = prompt("Group name:");
|
||||
if (!name || !name.trim()) return;
|
||||
const key = name.trim();
|
||||
if (pmGroups[key]) {
|
||||
showToast(t("groupExists"), "error");
|
||||
return;
|
||||
}
|
||||
pmGroups[key] = [];
|
||||
saveGroups();
|
||||
renderPresetManager();
|
||||
showToast(t("groupCreated", key), "success");
|
||||
});
|
||||
|
||||
document.getElementById("wfm-preset-add-to-group-btn")?.addEventListener("click", () => {
|
||||
const groupSelect = document.getElementById("wfm-preset-group-select");
|
||||
const groupName = groupSelect?.value;
|
||||
if (!groupName) {
|
||||
showToast(t("selectGroupFirst"), "error");
|
||||
return;
|
||||
}
|
||||
const id = presetSelect?.value;
|
||||
if (!id) {
|
||||
showToast(t("selectPresetFirst"), "error");
|
||||
return;
|
||||
}
|
||||
if (!pmGroups[groupName]) pmGroups[groupName] = [];
|
||||
if (pmGroups[groupName].includes(id)) {
|
||||
showToast(t("alreadyInGroup"), "info");
|
||||
return;
|
||||
}
|
||||
pmGroups[groupName].push(id);
|
||||
saveGroups();
|
||||
renderPresetManager();
|
||||
showToast(t("addedToGroup"), "success");
|
||||
});
|
||||
|
||||
document.getElementById("wfm-preset-del-group-btn")?.addEventListener("click", () => {
|
||||
const groupSelect = document.getElementById("wfm-preset-group-select");
|
||||
const groupName = groupSelect?.value;
|
||||
if (!groupName) {
|
||||
showToast(t("selectGroupFirst"), "error");
|
||||
return;
|
||||
}
|
||||
if (PROMPT_RESERVED_GROUPS.includes(groupName)) {
|
||||
showToast(t("modelsGroupReserved"), "warning");
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Delete group "${groupName}"?`)) return;
|
||||
delete pmGroups[groupName];
|
||||
saveGroups();
|
||||
renderPresetManager();
|
||||
showToast(t("groupDeleted", groupName), "success");
|
||||
});
|
||||
|
||||
// --- Preset Manager ---
|
||||
document.querySelectorAll(".wfm-pm-tab").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
pmActiveTab = btn.dataset.pmtab;
|
||||
document.querySelectorAll(".wfm-pm-tab").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
renderPresetManager();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("wfm-pm-search-input")?.addEventListener("input", (e) => {
|
||||
pmSearchText = e.target.value.trim();
|
||||
renderPresetManager();
|
||||
});
|
||||
|
||||
document.getElementById("wfm-pm-batch-clear-btn")?.addEventListener("click", () => {
|
||||
clearBatchPresets();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Prompt Tab - Style manager (Col3 "Style" タブ)
|
||||
*/
|
||||
// Wildcardファイルマネージャーと同じCRUDパターン(一覧→エディタ→保存/削除/キャンセル)。
|
||||
// 保存・削除後は generate-tab.js の refreshStylesList() を呼び、GenerateUIのStyleドロップダウンや
|
||||
// Batchタブのチェックリストを再読み込み無しで最新状態へ同期する。
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { setupSearchClearBtn } from "./util.js";
|
||||
import { refreshStylesList } from "./generate-tab.js";
|
||||
import { esc } from "./prompt-presets.js";
|
||||
|
||||
let styleListData = [];
|
||||
let styleSearchText = "";
|
||||
let styleEditingOriginalName = null; // null = 新規作成
|
||||
let styleEditingFile = null; // 編集中/追加先スタイルの定義元ファイル名(style/配下、null = デフォルトのcustom.json)
|
||||
|
||||
export async function styleFetchList() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/styles");
|
||||
return res.ok ? await res.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
export async function styleApiCreate(name, prompt, negativePrompt, targetFile) {
|
||||
try {
|
||||
const body = { name, prompt, negative_prompt: negativePrompt };
|
||||
if (targetFile) body.file = targetFile;
|
||||
const res = await fetch("/api/wfm/styles", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return res.ok ? { ok: true } : { ok: false, error: data.error || `HTTP ${res.status}` };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function styleApiUpdate(originalName, name, prompt, negativePrompt) {
|
||||
try {
|
||||
const res = await fetch(`/api/wfm/styles/${encodeURIComponent(originalName)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, prompt, negative_prompt: negativePrompt }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return res.ok ? { ok: true } : { ok: false, error: data.error || `HTTP ${res.status}` };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function styleApiDelete(name) {
|
||||
try {
|
||||
const res = await fetch(`/api/wfm/styles/${encodeURIComponent(name)}`, { method: "DELETE" });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return res.ok ? { ok: true } : { ok: false, error: data.error || `HTTP ${res.status}` };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
function styleRenderList() {
|
||||
const list = document.getElementById("wfm-style-list");
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
|
||||
const q = styleSearchText.trim().toLowerCase();
|
||||
const filtered = q ? styleListData.filter(s => s.name.toLowerCase().includes(q)) : styleListData;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = styleListData.length === 0
|
||||
? `<div class="wfm-pm-empty" style="font-size:12px;">No styles yet.<br><small>Click "+ New" to create one.</small></div>`
|
||||
: `<div class="wfm-pm-empty" style="font-size:12px;">No matching styles.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const s of filtered) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-wc-file-item";
|
||||
item.innerHTML = `
|
||||
<div class="wfm-wc-file-item-info">
|
||||
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(s.name)}</span>
|
||||
${s.file ? `<span class="wfm-wc-file-ext" title="${esc(s.file)}">${esc(s.file)}</span>` : ""}
|
||||
</div>
|
||||
<div class="wfm-wc-file-item-actions">
|
||||
<button class="wfm-pm-action-btn wfm-style-edit-btn" title="Edit style">✎</button>
|
||||
</div>
|
||||
`;
|
||||
item.querySelector(".wfm-style-edit-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
styleOpenEditor(s);
|
||||
});
|
||||
item.addEventListener("click", () => styleOpenEditor(s));
|
||||
list.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
// forcedFile: 「+ Add to this file」から呼ばれた場合に、既存スタイルではなく
|
||||
// 特定ファイルを追加先として指定するために使う(styleは渡さずnullにする)
|
||||
function styleOpenEditor(style, forcedFile) {
|
||||
styleEditingOriginalName = style ? style.name : null;
|
||||
styleEditingFile = style ? (style.file || null) : (forcedFile || null);
|
||||
|
||||
const editor = document.getElementById("wfm-style-editor");
|
||||
const nameInput = document.getElementById("wfm-style-editor-name");
|
||||
const promptTA = document.getElementById("wfm-style-editor-prompt");
|
||||
const negativeTA = document.getElementById("wfm-style-editor-negative");
|
||||
const deleteBtn = document.getElementById("wfm-style-editor-delete-btn");
|
||||
const addFileBtn = document.getElementById("wfm-style-editor-addfile-btn");
|
||||
const fileLabel = document.getElementById("wfm-style-editor-file-label");
|
||||
|
||||
if (!editor) return;
|
||||
nameInput.value = style?.name || "";
|
||||
promptTA.value = style?.prompt || "";
|
||||
negativeTA.value = style?.negative_prompt || "";
|
||||
deleteBtn.style.display = style ? "" : "none";
|
||||
// 「このファイルへ追加」は既存スタイルを編集中(=対象ファイルが分かっている)時だけ表示
|
||||
addFileBtn.style.display = style ? "" : "none";
|
||||
if (styleEditingFile) {
|
||||
fileLabel.textContent = `File: ${styleEditingFile}`;
|
||||
fileLabel.style.display = "";
|
||||
} else {
|
||||
fileLabel.style.display = "none";
|
||||
}
|
||||
editor.style.display = "";
|
||||
nameInput.focus();
|
||||
}
|
||||
|
||||
function styleCloseEditor() {
|
||||
styleEditingOriginalName = null;
|
||||
styleEditingFile = null;
|
||||
const editor = document.getElementById("wfm-style-editor");
|
||||
if (editor) editor.style.display = "none";
|
||||
}
|
||||
|
||||
export async function styleRefreshList() {
|
||||
styleListData = await styleFetchList();
|
||||
styleRenderList();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI wiring (called from prompt-tab.js's initPromptTab)
|
||||
// ============================================
|
||||
|
||||
export function initStylesUI() {
|
||||
document.getElementById("wfm-style-new-btn")?.addEventListener("click", () => {
|
||||
styleOpenEditor(null);
|
||||
});
|
||||
|
||||
document.getElementById("wfm-style-refresh-btn")?.addEventListener("click", () => {
|
||||
styleRefreshList();
|
||||
});
|
||||
|
||||
// 編集中の既存スタイルと同じファイルへ、新しい空のスタイルを追加する
|
||||
document.getElementById("wfm-style-editor-addfile-btn")?.addEventListener("click", () => {
|
||||
if (!styleEditingFile) return;
|
||||
styleOpenEditor(null, styleEditingFile);
|
||||
});
|
||||
|
||||
setupSearchClearBtn("wfm-style-search", "wfm-style-search-clear-btn", () => {
|
||||
styleSearchText = "";
|
||||
styleRenderList();
|
||||
});
|
||||
document.getElementById("wfm-style-search")?.addEventListener("input", (e) => {
|
||||
styleSearchText = e.target.value;
|
||||
styleRenderList();
|
||||
});
|
||||
|
||||
document.getElementById("wfm-style-editor-save-btn")?.addEventListener("click", async () => {
|
||||
const nameInput = document.getElementById("wfm-style-editor-name");
|
||||
const promptTA = document.getElementById("wfm-style-editor-prompt");
|
||||
const negativeTA = document.getElementById("wfm-style-editor-negative");
|
||||
|
||||
const name = (nameInput?.value || "").trim();
|
||||
const prompt = promptTA?.value || "";
|
||||
const negativePrompt = negativeTA?.value || "";
|
||||
|
||||
if (!name) { showToast(t("pleaseEnterStyleName"), "error"); return; }
|
||||
|
||||
const result = styleEditingOriginalName
|
||||
? await styleApiUpdate(styleEditingOriginalName, name, prompt, negativePrompt)
|
||||
: await styleApiCreate(name, prompt, negativePrompt, styleEditingFile);
|
||||
|
||||
if (result.ok) {
|
||||
showToast(t("savedAs", name), "success");
|
||||
styleCloseEditor();
|
||||
await styleRefreshList();
|
||||
await refreshStylesList();
|
||||
} else {
|
||||
showToast(t("errorWithMsg", result.error), "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Editor: delete
|
||||
document.getElementById("wfm-style-editor-delete-btn")?.addEventListener("click", async () => {
|
||||
if (!styleEditingOriginalName) return;
|
||||
if (!confirm(`Delete "${styleEditingOriginalName}"?`)) return;
|
||||
const result = await styleApiDelete(styleEditingOriginalName);
|
||||
if (result.ok) {
|
||||
showToast(t("deletedName", styleEditingOriginalName), "success");
|
||||
styleCloseEditor();
|
||||
await styleRefreshList();
|
||||
await refreshStylesList();
|
||||
} else {
|
||||
showToast(t("errorWithMsg", result.error), "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Editor: cancel
|
||||
document.getElementById("wfm-style-editor-cancel-btn")?.addEventListener("click", () => {
|
||||
styleCloseEditor();
|
||||
});
|
||||
|
||||
// Initial style list load
|
||||
styleRefreshList();
|
||||
}
|
||||
+30
-1509
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* Prompt Tab - Wildcard file manager (list, editor, insert-at-cursor helpers)
|
||||
*/
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { esc } from "./prompt-presets.js";
|
||||
|
||||
// ============================================
|
||||
// Wildcard API helpers
|
||||
// ============================================
|
||||
|
||||
export async function wcFetchFiles() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/wildcards");
|
||||
return res.ok ? await res.json() : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
export async function wcFetchContent(filename) {
|
||||
try {
|
||||
const res = await fetch(`/api/wfm/wildcards/content?filename=${encodeURIComponent(filename)}`);
|
||||
const data = await res.json();
|
||||
return data.content ?? null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function wcSaveFile(filename, content) {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/wildcards/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, content }),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.status === "ok" ? data.file : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function wcDeleteFile(filename) {
|
||||
try {
|
||||
await fetch("/api/wfm/wildcards/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard state & helpers
|
||||
// ============================================
|
||||
|
||||
let wcFiles = [];
|
||||
let wcEditingFilename = null; // null = new file
|
||||
|
||||
// Insert text at cursor; if open+close provided wraps selection
|
||||
function wcInsertAtCursor(textarea, open, close = "") {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selected = textarea.value.substring(start, end);
|
||||
let inserted, cursorStart, cursorEnd;
|
||||
|
||||
if (selected && close) {
|
||||
inserted = open + selected + close;
|
||||
cursorStart = start + open.length;
|
||||
cursorEnd = cursorStart + selected.length;
|
||||
} else if (close) {
|
||||
inserted = open + close;
|
||||
cursorStart = start + open.length;
|
||||
cursorEnd = cursorStart;
|
||||
} else {
|
||||
inserted = open;
|
||||
cursorStart = start + open.length;
|
||||
cursorEnd = cursorStart;
|
||||
}
|
||||
|
||||
textarea.value =
|
||||
textarea.value.substring(0, start) +
|
||||
inserted +
|
||||
textarea.value.substring(end);
|
||||
textarea.setSelectionRange(cursorStart, cursorEnd);
|
||||
textarea.focus();
|
||||
textarea.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard file list rendering
|
||||
// ============================================
|
||||
|
||||
function wcRenderFileList() {
|
||||
const list = document.getElementById("wfm-wc-file-list");
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
|
||||
if (wcFiles.length === 0) {
|
||||
list.innerHTML = `<div class="wfm-pm-empty" style="font-size:12px;">No wildcard files yet.<br><small>Click "+ New" to create one.</small></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by directory; root ("") first, then alphabetical
|
||||
const grouped = {};
|
||||
for (const f of wcFiles) {
|
||||
const dir = f.dir || "";
|
||||
if (!grouped[dir]) grouped[dir] = [];
|
||||
grouped[dir].push(f);
|
||||
}
|
||||
const dirs = Object.keys(grouped).sort((a, b) => {
|
||||
if (a === "") return -1;
|
||||
if (b === "") return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
function addFileItem(f, indented) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-wc-file-item" + (indented ? " wfm-wc-file-item--sub" : "");
|
||||
item.innerHTML = `
|
||||
<div class="wfm-wc-file-item-info">
|
||||
<span class="wfm-wc-file-name">${esc(f.name)}</span>
|
||||
<span class="wfm-wc-file-ext">.${esc(f.ext)}</span>
|
||||
</div>
|
||||
<div class="wfm-wc-file-item-actions">
|
||||
<button class="wfm-pm-action-btn wfm-wc-use-btn" title="Insert __${esc(f.wc_name)}__ at cursor">Use</button>
|
||||
<button class="wfm-pm-action-btn wfm-wc-edit-btn" title="Edit file">✎</button>
|
||||
</div>
|
||||
`;
|
||||
item.querySelector(".wfm-wc-use-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
|
||||
});
|
||||
item.querySelector(".wfm-wc-edit-btn").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const content = await wcFetchContent(f.filename);
|
||||
wcOpenEditor(f.wc_name, f.ext, content ?? "", f.filename);
|
||||
});
|
||||
item.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
|
||||
});
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (dir) {
|
||||
const header = document.createElement("div");
|
||||
header.className = "wfm-wc-dir-header";
|
||||
header.textContent = dir + "/";
|
||||
list.appendChild(header);
|
||||
}
|
||||
for (const f of grouped[dir]) {
|
||||
addFileItem(f, dir !== "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard file editor
|
||||
// ============================================
|
||||
|
||||
function wcOpenEditor(name, ext, content, existingFilename) {
|
||||
wcEditingFilename = existingFilename || null;
|
||||
|
||||
const editor = document.getElementById("wfm-wc-editor");
|
||||
const nameInput = document.getElementById("wfm-wc-editor-name");
|
||||
const extSelect = document.getElementById("wfm-wc-editor-ext");
|
||||
const contentTA = document.getElementById("wfm-wc-editor-content");
|
||||
const deleteBtn = document.getElementById("wfm-wc-editor-delete-btn");
|
||||
|
||||
if (!editor) return;
|
||||
nameInput.value = name || "";
|
||||
extSelect.value = ext || "txt";
|
||||
contentTA.value = content || "";
|
||||
deleteBtn.style.display = existingFilename ? "" : "none";
|
||||
editor.style.display = "";
|
||||
nameInput.focus();
|
||||
}
|
||||
|
||||
function wcCloseEditor() {
|
||||
wcEditingFilename = null;
|
||||
const editor = document.getElementById("wfm-wc-editor");
|
||||
if (editor) editor.style.display = "none";
|
||||
}
|
||||
|
||||
export async function wcRefreshFiles() {
|
||||
wcFiles = await wcFetchFiles();
|
||||
wcRenderFileList();
|
||||
wcUpdateFilePicker();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Wildcard file picker popup
|
||||
// ============================================
|
||||
|
||||
// Renders the wildcard-file-reference list into `picker`, inserting into `targetTa` on click.
|
||||
// Shared by the Form tab's fixed file-picker popup and any dynamically built toolbar
|
||||
// (see createWildcardToolbar) so both stay in sync with a single implementation.
|
||||
function renderFilePicker(picker, targetTa) {
|
||||
picker.innerHTML = "";
|
||||
if (wcFiles.length === 0) {
|
||||
picker.innerHTML = `<div style="padding:8px;font-size:11px;color:var(--wfm-text-secondary);">No wildcard files</div>`;
|
||||
return;
|
||||
}
|
||||
// Group by directory same as file list
|
||||
const grouped = {};
|
||||
for (const f of wcFiles) {
|
||||
const dir = f.dir || "";
|
||||
if (!grouped[dir]) grouped[dir] = [];
|
||||
grouped[dir].push(f);
|
||||
}
|
||||
const dirs = Object.keys(grouped).sort((a, b) => {
|
||||
if (a === "") return -1;
|
||||
if (b === "") return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
for (const dir of dirs) {
|
||||
if (dir) {
|
||||
const sep = document.createElement("div");
|
||||
sep.className = "wfm-wc-picker-dir";
|
||||
sep.textContent = dir + "/";
|
||||
picker.appendChild(sep);
|
||||
}
|
||||
for (const f of grouped[dir]) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "wfm-wc-picker-item" + (dir ? " wfm-wc-picker-item--sub" : "");
|
||||
btn.textContent = `__${f.wc_name}__`;
|
||||
btn.addEventListener("click", () => {
|
||||
wcInsertAtCursor(targetTa, `__${f.wc_name}__`);
|
||||
picker.style.display = "none";
|
||||
});
|
||||
picker.appendChild(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wcUpdateFilePicker() {
|
||||
const picker = document.getElementById("wfm-wc-file-picker");
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (!picker || !ta) return;
|
||||
renderFilePicker(picker, ta);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Reusable wildcard syntax toolbar (buttons + file-reference picker)
|
||||
// ============================================
|
||||
|
||||
const WC_SYNTAX_BUTTONS = [
|
||||
{ open: "{", close: "}", label: "{ }", title: "Random pick set e.g. {cat|dog}" },
|
||||
{ open: "|", close: "", label: "|", title: "Separator" },
|
||||
{ open: "__", close: "__", label: "__", title: "Wildcard file reference e.g. __animals__" },
|
||||
{ open: ":", close: "", label: ":", title: "Colon" },
|
||||
{ open: ";", close: "", label: ";", title: "Semicolon" },
|
||||
{ open: "$$", close: "", label: "$$", title: "Multi-pick e.g. {2$$cat|dog|bird}" },
|
||||
{ open: "[", close: "]", label: "[ ]", title: "Bracket set e.g. [cat|dog]" },
|
||||
];
|
||||
|
||||
// Builds a standalone wildcard-syntax toolbar (syntax buttons, LoRA/n$${} helpers, and the
|
||||
// file-reference picker) wired to insert into the given textarea. The Form tab's Col3 panel
|
||||
// uses the static toolbar markup in index.html instead (wired in initWildcardsUI below); this
|
||||
// is for callers that need the same toolbar attached to a textarea built at runtime, such as
|
||||
// the Table tab's wildcard edit modal (see prompt-table.js).
|
||||
export function createWildcardToolbar(textareaEl) {
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "wfm-wc-toolbar";
|
||||
|
||||
for (const b of WC_SYNTAX_BUTTONS) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "wfm-wc-btn";
|
||||
btn.title = b.title;
|
||||
btn.textContent = b.label;
|
||||
btn.addEventListener("click", () => wcInsertAtCursor(textareaEl, b.open, b.close));
|
||||
toolbar.appendChild(btn);
|
||||
}
|
||||
|
||||
const loraBtn = document.createElement("button");
|
||||
loraBtn.className = "wfm-wc-btn";
|
||||
loraBtn.title = "LoRA with LBW e.g. <lora:name:1:LBW=...;...>";
|
||||
loraBtn.textContent = "<lora>";
|
||||
loraBtn.addEventListener("click", () => {
|
||||
wcInsertAtCursor(textareaEl, "<lora::1:LBW=;>");
|
||||
// Move cursor to after "lora:" for the name
|
||||
const pos = textareaEl.selectionStart - "<lora::1:LBW=;>".length + 6;
|
||||
textareaEl.setSelectionRange(pos, pos);
|
||||
textareaEl.focus();
|
||||
});
|
||||
toolbar.appendChild(loraBtn);
|
||||
|
||||
const nsetBtn = document.createElement("button");
|
||||
nsetBtn.className = "wfm-wc-btn";
|
||||
nsetBtn.title = "Multi-pick set e.g. {2$$cat|dog|bird}";
|
||||
nsetBtn.textContent = "n$${ }";
|
||||
nsetBtn.addEventListener("click", () => {
|
||||
const n = prompt("Number of items to pick (n):", "2");
|
||||
if (n === null) return;
|
||||
const num = parseInt(n, 10);
|
||||
if (isNaN(num) || num < 1) { showToast(t("enterPositiveInt"), "error"); return; }
|
||||
const template = "{" + num + "$$|}";
|
||||
const start = textareaEl.selectionStart;
|
||||
wcInsertAtCursor(textareaEl, template);
|
||||
// Move cursor between $$ and |
|
||||
const inner = start + ("{" + num + "$$").length;
|
||||
textareaEl.setSelectionRange(inner, inner);
|
||||
textareaEl.focus();
|
||||
});
|
||||
toolbar.appendChild(nsetBtn);
|
||||
|
||||
const pickerWrap = document.createElement("div");
|
||||
pickerWrap.className = "wfm-wc-file-picker-wrap";
|
||||
const pickBtn = document.createElement("button");
|
||||
pickBtn.className = "wfm-wc-btn";
|
||||
pickBtn.title = "Insert wildcard file reference";
|
||||
pickBtn.innerHTML = "📂 File";
|
||||
const picker = document.createElement("div");
|
||||
picker.className = "wfm-wc-file-picker";
|
||||
picker.style.display = "none";
|
||||
pickBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = picker.style.display !== "none";
|
||||
picker.style.display = isOpen ? "none" : "";
|
||||
if (!isOpen) renderFilePicker(picker, textareaEl);
|
||||
});
|
||||
document.addEventListener("click", () => { picker.style.display = "none"; });
|
||||
pickerWrap.appendChild(pickBtn);
|
||||
pickerWrap.appendChild(picker);
|
||||
toolbar.appendChild(pickerWrap);
|
||||
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI wiring (called from prompt-tab.js's initPromptTab)
|
||||
// ============================================
|
||||
|
||||
export function initWildcardsUI() {
|
||||
// ── Wildcard syntax buttons ──────────────────────────────
|
||||
document.querySelectorAll(".wfm-wc-btn[data-wc-open]").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (!ta) return;
|
||||
const open = btn.dataset.wcOpen || "";
|
||||
const close = btn.dataset.wcClose || "";
|
||||
wcInsertAtCursor(ta, open, close);
|
||||
});
|
||||
});
|
||||
|
||||
// LoRA LBW template button
|
||||
document.getElementById("wfm-wc-lora-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (!ta) return;
|
||||
wcInsertAtCursor(ta, "<lora::1:LBW=;>");
|
||||
// Move cursor to after "lora:" for the name
|
||||
const pos = ta.selectionStart - "<lora::1:LBW=;>".length + 6;
|
||||
ta.setSelectionRange(pos, pos);
|
||||
ta.focus();
|
||||
});
|
||||
|
||||
// n$${ } button — asks for n then inserts multi-pick template
|
||||
document.getElementById("wfm-wc-nset-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (!ta) return;
|
||||
const n = prompt("Number of items to pick (n):", "2");
|
||||
if (n === null) return;
|
||||
const num = parseInt(n, 10);
|
||||
if (isNaN(num) || num < 1) { showToast(t("enterPositiveInt"), "error"); return; }
|
||||
const template = "{" + num + "$$|}";
|
||||
const start = ta.selectionStart;
|
||||
wcInsertAtCursor(ta, template);
|
||||
// Move cursor between $$ and |
|
||||
const inner = start + ("{" + num + "$$").length;
|
||||
ta.setSelectionRange(inner, inner);
|
||||
ta.focus();
|
||||
});
|
||||
|
||||
// File picker toggle
|
||||
const filePickBtn = document.getElementById("wfm-wc-filepick-btn");
|
||||
const filePicker = document.getElementById("wfm-wc-file-picker");
|
||||
filePickBtn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (!filePicker) return;
|
||||
const isOpen = filePicker.style.display !== "none";
|
||||
filePicker.style.display = isOpen ? "none" : "";
|
||||
if (!isOpen) wcUpdateFilePicker();
|
||||
});
|
||||
document.addEventListener("click", () => {
|
||||
if (filePicker) filePicker.style.display = "none";
|
||||
});
|
||||
|
||||
// ── Wildcard prompt actions ──────────────────────────────
|
||||
document.getElementById("wfm-wc-copy-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
const text = ta?.value || "";
|
||||
if (!text.trim()) { showToast(t("noTextToCopy"), "error"); return; }
|
||||
navigator.clipboard.writeText(text).then(() => showToast(t("copiedToClipboard"), "success"));
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-to-pos-btn")?.addEventListener("click", () => {
|
||||
const src = document.getElementById("wfm-wc-prompt");
|
||||
const dst = document.getElementById("wfm-preset-pos");
|
||||
if (!src || !dst) return;
|
||||
dst.value = src.value;
|
||||
dst.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
showToast(t("sentToPositive"), "success");
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-to-neg-btn")?.addEventListener("click", () => {
|
||||
const src = document.getElementById("wfm-wc-prompt");
|
||||
const dst = document.getElementById("wfm-preset-neg");
|
||||
if (!src || !dst) return;
|
||||
dst.value = src.value;
|
||||
dst.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
showToast(t("sentToNegative"), "success");
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-clear-btn")?.addEventListener("click", () => {
|
||||
const ta = document.getElementById("wfm-wc-prompt");
|
||||
if (ta) { ta.value = ""; ta.focus(); }
|
||||
});
|
||||
|
||||
// ── Wildcard file manager ────────────────────────────────
|
||||
document.getElementById("wfm-wc-new-file-btn")?.addEventListener("click", () => {
|
||||
wcOpenEditor("", "txt", "", null);
|
||||
});
|
||||
|
||||
document.getElementById("wfm-wc-files-refresh-btn")?.addEventListener("click", () => {
|
||||
wcRefreshFiles();
|
||||
});
|
||||
|
||||
// Editor: save
|
||||
document.getElementById("wfm-wc-editor-save-btn")?.addEventListener("click", async () => {
|
||||
const nameInput = document.getElementById("wfm-wc-editor-name");
|
||||
const extSelect = document.getElementById("wfm-wc-editor-ext");
|
||||
const contentTA = document.getElementById("wfm-wc-editor-content");
|
||||
|
||||
// Normalize separators and strip leading/trailing slashes
|
||||
const rawName = (nameInput?.value || "").trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
||||
const ext = extSelect?.value || "txt";
|
||||
const content = contentTA?.value || "";
|
||||
|
||||
if (!rawName) { showToast(t("pleaseEnterFilename"), "error"); return; }
|
||||
// Validate each path component (allow letters, numbers, -, _, ., space)
|
||||
const nameParts = rawName.split("/");
|
||||
if (nameParts.some(p => !p || !/^[\w\-. ]+$/.test(p))) {
|
||||
showToast(t("invalidPathFormat"), "error");
|
||||
return;
|
||||
}
|
||||
const name = rawName;
|
||||
const filename = `${name}.${ext}`;
|
||||
const saved = await wcSaveFile(filename, content);
|
||||
if (saved) {
|
||||
showToast(t("savedAs", filename), "success");
|
||||
wcCloseEditor();
|
||||
wcRefreshFiles();
|
||||
} else {
|
||||
showToast(t("saveFailed"), "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Editor: delete
|
||||
document.getElementById("wfm-wc-editor-delete-btn")?.addEventListener("click", async () => {
|
||||
if (!wcEditingFilename) return;
|
||||
if (!confirm(`Delete "${wcEditingFilename}"?`)) return;
|
||||
await wcDeleteFile(wcEditingFilename);
|
||||
showToast(t("deletedName", wcEditingFilename), "success");
|
||||
wcCloseEditor();
|
||||
wcRefreshFiles();
|
||||
});
|
||||
|
||||
// Editor: cancel
|
||||
document.getElementById("wfm-wc-editor-cancel-btn")?.addEventListener("click", () => {
|
||||
wcCloseEditor();
|
||||
});
|
||||
|
||||
// Initial wildcard file load
|
||||
wcRefreshFiles();
|
||||
}
|
||||
@@ -824,6 +824,25 @@ export async function initSettingsTab() {
|
||||
<input type="file" id="wfm-settings-import-file" accept=".json" style="display:none;">
|
||||
</label>
|
||||
</div>
|
||||
<p style="font-size:12px;color:var(--wfm-text-secondary);margin:12px 0 8px 0;">${t("fullBackupHint")}</p>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;margin-bottom:8px;">
|
||||
<label class="wfm-lab-checkbox-label" style="font-size:12px;">
|
||||
<input type="checkbox" id="wfm-settings-backup-include-workflows">
|
||||
${t("fullBackupIncludeWorkflows")}
|
||||
</label>
|
||||
<label class="wfm-lab-checkbox-label" style="font-size:12px;">
|
||||
<input type="checkbox" id="wfm-settings-backup-include-wildcard">
|
||||
${t("fullBackupIncludeWildcard")}
|
||||
</label>
|
||||
<span style="font-size:11px;color:var(--wfm-text-secondary);">${t("fullBackupExternalHint")}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-export-full">${t("exportFullBackup")}</button>
|
||||
<label class="wfm-btn wfm-btn-sm" style="cursor:pointer;margin:0;">
|
||||
${t("importFullBackup")}
|
||||
<input type="file" id="wfm-settings-import-full-file" accept=".zip" style="display:none;">
|
||||
</label>
|
||||
</div>
|
||||
<div id="wfm-settings-data-status" style="font-size:12px;margin-top:8px;"></div>
|
||||
</details>
|
||||
|
||||
@@ -1532,8 +1551,53 @@ export async function initSettingsTab() {
|
||||
statusEl.textContent = t("importError") + ": " + e.message;
|
||||
statusEl.style.color = "var(--wfm-error, #f44336)";
|
||||
}
|
||||
// Reset file input so same file can be re-selected
|
||||
e.target.value = "";
|
||||
});
|
||||
|
||||
// --- Full Backup Export (ZIP) ---
|
||||
document.getElementById("wfm-settings-export-full")?.addEventListener("click", async () => {
|
||||
const statusEl = document.getElementById("wfm-settings-data-status");
|
||||
try {
|
||||
const includeWorkflows = document.getElementById("wfm-settings-backup-include-workflows")?.checked ? "1" : "0";
|
||||
const includeWildcard = document.getElementById("wfm-settings-backup-include-wildcard")?.checked ? "1" : "0";
|
||||
const res = await fetch(`/api/wfm/settings/export-full?include_workflows=${includeWorkflows}&include_wildcard=${includeWildcard}`);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "wfm-full-backup.zip";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
statusEl.textContent = t("exportSuccess");
|
||||
statusEl.style.color = "var(--wfm-success, #4caf50)";
|
||||
} catch (e) {
|
||||
statusEl.textContent = t("exportError") + ": " + e.message;
|
||||
statusEl.style.color = "var(--wfm-error, #f44336)";
|
||||
}
|
||||
});
|
||||
|
||||
// --- Full Backup Import (ZIP) ---
|
||||
document.getElementById("wfm-settings-import-full-file")?.addEventListener("change", async (e) => {
|
||||
const statusEl = document.getElementById("wfm-settings-data-status");
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file, file.name);
|
||||
const res = await fetch("/api/wfm/settings/import-full", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) throw new Error(result.error || res.statusText);
|
||||
statusEl.textContent = `${t("importSuccess")}: ${result.extracted?.length ?? 0} ${t("fullBackupFilesUnit")}`;
|
||||
statusEl.style.color = "var(--wfm-success, #4caf50)";
|
||||
} catch (e) {
|
||||
statusEl.textContent = t("importError") + ": " + e.message;
|
||||
statusEl.style.color = "var(--wfm-error, #f44336)";
|
||||
} finally {
|
||||
e.target.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
// Save G'MIC path setting
|
||||
|
||||
@@ -0,0 +1,725 @@
|
||||
/**
|
||||
* Video Tab - MiniMax H3 (Image to Video) 専用の動画生成UI
|
||||
* GenerateUIタブとは完全に独立。UI形式ワークフローJSONを直接編集し、
|
||||
* comfyWorkflow.convertUiToApi() でAPI形式に変換して実行する。
|
||||
* 状態は「テンプレートJSON+フォーム値」のみ保持し、生成の都度
|
||||
* structuredClone() したコピーに書き込む(複数回生成をまたいだID不整合を防ぐ)。
|
||||
*/
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
import { escapeHtml } from "./util.js";
|
||||
|
||||
const DEFAULT_WORKFLOW_FILENAME = "minimax_test.json";
|
||||
|
||||
const state = {
|
||||
templateWorkflow: null,
|
||||
templateFilename: null,
|
||||
firstFrameFilename: null,
|
||||
lastFrameFilename: null,
|
||||
generating: false,
|
||||
// Whatever's currently shown in the center preview frame — a just-generated
|
||||
// result, or a video dropped/picked in the Video Source panel. Used by the
|
||||
// Frame/GIF property tabs so they work on any video, not just generated ones.
|
||||
// { kind: "output"|"input", filename, subfolder, type } for anything already on
|
||||
// the server, or { kind: "local", file } for a picked file not yet uploaded.
|
||||
previewSource: null,
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ワークフロー取得
|
||||
// ============================================
|
||||
|
||||
async function getRawWorkflow(filename) {
|
||||
const res = await fetch(`/api/wfm/workflows/raw?filename=${encodeURIComponent(filename)}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MiniMax H3ノードの動的特定(ノードIDをハードコードしない)
|
||||
// ============================================
|
||||
|
||||
// ノードのinputs[]のうちwidgetキーを持つものだけを抽出した並び順で、名前からインデックスを引く。
|
||||
// widgets_values配列はこの並び順と1:1対応する(link接続されていてもwidgetキーを持つ入力は
|
||||
// widgets_valuesにスロットを持つ「legacy full」形式のため)。
|
||||
function _widgetIndex(node, name) {
|
||||
const widgetInputs = (node?.inputs || []).filter((inp) => inp.widget);
|
||||
return widgetInputs.findIndex((inp) => inp.name === name);
|
||||
}
|
||||
|
||||
function locateMiniMaxNodes(workflow) {
|
||||
if (!Array.isArray(workflow?.nodes) || !Array.isArray(workflow?.links)) return null;
|
||||
|
||||
const sgDef = (workflow.definitions?.subgraphs || [])
|
||||
.find((sg) => (sg.nodes || []).some((n) => n.type === "MiniMaxH3ImageToVideo"));
|
||||
if (!sgDef) return null;
|
||||
|
||||
const instanceNode = workflow.nodes.find((n) => n.type === sgDef.id);
|
||||
if (!instanceNode || !Array.isArray(instanceNode.widgets_values)) return null;
|
||||
|
||||
const firstFrameSlot = instanceNode.inputs.findIndex((inp) => inp.name === "first_frame");
|
||||
const lastFrameSlot = instanceNode.inputs.findIndex((inp) => inp.name === "last_frame");
|
||||
const widthSlot = instanceNode.inputs.findIndex((inp) => inp.name === "width");
|
||||
if (firstFrameSlot === -1 || lastFrameSlot === -1 || widthSlot === -1) return null;
|
||||
|
||||
const findLinkedNode = (targetSlot) => {
|
||||
const link = workflow.links.find((l) => l[3] === instanceNode.id && l[4] === targetSlot);
|
||||
if (!link) return null;
|
||||
return workflow.nodes.find((n) => n.id === link[1]) || null;
|
||||
};
|
||||
|
||||
// first_frameはoptional入力のため、LoadImageノードが接続されていないワークフロー
|
||||
// (テキストのみで動作するT2V構成など)もサポート対象とする。未接続の場合は
|
||||
// loadImageNode: null のまま返し、必要ならrunGeneration側で動的にノードを注入する。
|
||||
const rawLoadImageNode = findLinkedNode(firstFrameSlot);
|
||||
const loadImageNode = (rawLoadImageNode && rawLoadImageNode.type === "LoadImage") ? rawLoadImageNode : null;
|
||||
const resolutionNode = findLinkedNode(widthSlot);
|
||||
if (!resolutionNode || resolutionNode.type !== "ResolutionSelector") return null;
|
||||
|
||||
const promptIdx = _widgetIndex(instanceNode, "prompt");
|
||||
const durationIdx = _widgetIndex(instanceNode, "value_1");
|
||||
if (promptIdx === -1 || durationIdx === -1) return null;
|
||||
|
||||
return {
|
||||
sgDef, instanceNode, loadImageNode, resolutionNode,
|
||||
firstFrameSlot, lastFrameSlot, promptIdx, durationIdx,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ワークフロー読み込み
|
||||
// ============================================
|
||||
|
||||
function _showFramePreview(which, filename) {
|
||||
const previewImg = document.getElementById(`wfm-video-${which}-frame-preview`);
|
||||
const wrap = document.getElementById(`wfm-video-${which}-frame-wrap`);
|
||||
if (!previewImg || !wrap || !filename) return;
|
||||
previewImg.src = `${comfyUI.baseUrl}/view?filename=${encodeURIComponent(filename)}&type=input`;
|
||||
wrap.style.display = "";
|
||||
}
|
||||
|
||||
export async function loadWorkflowIntoVideoEditor(workflow, filename) {
|
||||
const nodes = locateMiniMaxNodes(workflow);
|
||||
if (!nodes) {
|
||||
showToast(t("videoUnsupportedWorkflow"), "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
state.templateWorkflow = workflow;
|
||||
state.templateFilename = filename || "";
|
||||
state.firstFrameFilename = null;
|
||||
state.lastFrameFilename = null;
|
||||
|
||||
const nameEl = document.getElementById("wfm-video-wf-name");
|
||||
if (nameEl) nameEl.textContent = filename || "Loaded Workflow";
|
||||
|
||||
const { instanceNode, resolutionNode, loadImageNode, promptIdx, durationIdx } = nodes;
|
||||
|
||||
const promptEl = document.getElementById("wfm-video-prompt");
|
||||
if (promptEl) promptEl.value = instanceNode.widgets_values[promptIdx] ?? "";
|
||||
|
||||
const durationEl = document.getElementById("wfm-video-duration");
|
||||
if (durationEl) durationEl.value = instanceNode.widgets_values[durationIdx] ?? 2;
|
||||
|
||||
const arIdx = _widgetIndex(resolutionNode, "aspect_ratio");
|
||||
const mpIdx = _widgetIndex(resolutionNode, "megapixels");
|
||||
const multIdx = _widgetIndex(resolutionNode, "multiple");
|
||||
const aspectEl = document.getElementById("wfm-video-aspect-ratio");
|
||||
if (aspectEl && arIdx !== -1 && resolutionNode.widgets_values[arIdx] != null) {
|
||||
aspectEl.value = resolutionNode.widgets_values[arIdx];
|
||||
}
|
||||
const mpEl = document.getElementById("wfm-video-megapixels");
|
||||
if (mpEl && mpIdx !== -1) mpEl.value = resolutionNode.widgets_values[mpIdx] ?? mpEl.value;
|
||||
const multEl = document.getElementById("wfm-video-multiple");
|
||||
if (multEl && multIdx !== -1) multEl.value = resolutionNode.widgets_values[multIdx] ?? multEl.value;
|
||||
|
||||
const fnIdx = _widgetIndex(loadImageNode, "image");
|
||||
if (fnIdx !== -1 && loadImageNode.widgets_values[fnIdx]) {
|
||||
state.firstFrameFilename = loadImageNode.widgets_values[fnIdx];
|
||||
_showFramePreview("first", state.firstFrameFilename);
|
||||
}
|
||||
|
||||
showToast(t("videoWorkflowLoaded", filename || ""), "success");
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadDefaultWorkflow() {
|
||||
try {
|
||||
const wf = await getRawWorkflow(DEFAULT_WORKFLOW_FILENAME);
|
||||
await loadWorkflowIntoVideoEditor(wf, DEFAULT_WORKFLOW_FILENAME);
|
||||
} catch {
|
||||
// 初回自動ロード失敗は静かに無視する(環境によってファイルが存在しないこともある)。
|
||||
// Workflowタブの「Load in Video」ボタンから明示的に読み込める。
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Aspect Ratio 選択肢(object_infoから動的取得、ハードコードしない)
|
||||
// ============================================
|
||||
|
||||
async function _populateAspectRatioOptions() {
|
||||
const select = document.getElementById("wfm-video-aspect-ratio");
|
||||
if (!select) return;
|
||||
try {
|
||||
const info = await comfyUI.fetchObjectInfo("ResolutionSelector");
|
||||
const inputDef = info?.ResolutionSelector?.input?.required?.aspect_ratio;
|
||||
if (!inputDef) return;
|
||||
const first = inputDef[0];
|
||||
let values = null;
|
||||
if (Array.isArray(first)) values = first;
|
||||
else if (typeof first === "string" && Array.isArray(inputDef[1]?.values)) values = inputDef[1].values;
|
||||
else if (typeof first === "string" && Array.isArray(inputDef[1]?.options)) values = inputDef[1].options;
|
||||
if (!values?.length) return;
|
||||
select.innerHTML = values.map((v) => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join("");
|
||||
} catch {
|
||||
// ResolutionSelectorが未導入の環境などは選択肢が空のまま(手入力不可のselectなので実質何もできないが、
|
||||
// Videoタブ自体の初期化は継続させる)。
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// first_frame / last_frame アップロード
|
||||
// ============================================
|
||||
|
||||
function _wireDropZone(dropZoneId, fileInputId, onFile) {
|
||||
const dropZone = document.getElementById(dropZoneId);
|
||||
const fileInput = document.getElementById(fileInputId);
|
||||
if (!dropZone || !fileInput) return;
|
||||
fileInput.addEventListener("change", () => {
|
||||
if (fileInput.files.length > 0) onFile(fileInput.files[0]);
|
||||
});
|
||||
dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("drag-over"); });
|
||||
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag-over"));
|
||||
dropZone.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove("drag-over");
|
||||
if (e.dataTransfer.files.length > 0) onFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
async function _handleFrameUpload(which, file) {
|
||||
if (!file || !file.type.startsWith("image/")) return;
|
||||
const statusEl = document.getElementById(`wfm-video-${which}-frame-status`);
|
||||
const previewImg = document.getElementById(`wfm-video-${which}-frame-preview`);
|
||||
const wrap = document.getElementById(`wfm-video-${which}-frame-wrap`);
|
||||
if (previewImg) previewImg.src = URL.createObjectURL(file);
|
||||
if (wrap) wrap.style.display = "";
|
||||
if (statusEl) statusEl.textContent = "Uploading...";
|
||||
try {
|
||||
const result = await comfyUI.uploadImage(file, file.name);
|
||||
if (which === "first") state.firstFrameFilename = result.name;
|
||||
else state.lastFrameFilename = result.name;
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `✓ ${result.name}`;
|
||||
statusEl.style.color = "var(--wfm-success)";
|
||||
}
|
||||
} catch (err) {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `✗ ${err.message}`;
|
||||
statusEl.style.color = "var(--wfm-danger)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _clearFrame(which) {
|
||||
if (which === "first") state.firstFrameFilename = null;
|
||||
else state.lastFrameFilename = null;
|
||||
const wrap = document.getElementById(`wfm-video-${which}-frame-wrap`);
|
||||
const previewImg = document.getElementById(`wfm-video-${which}-frame-preview`);
|
||||
const statusEl = document.getElementById(`wfm-video-${which}-frame-status`);
|
||||
const fileInput = document.getElementById(`wfm-video-${which}-frame-file`);
|
||||
if (wrap) wrap.style.display = "none";
|
||||
if (previewImg) previewImg.src = "";
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
if (fileInput) fileInput.value = "";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// フォーム値の読み取り・ワークフローへの反映
|
||||
// ============================================
|
||||
|
||||
function _readFormValues() {
|
||||
return {
|
||||
prompt: document.getElementById("wfm-video-prompt")?.value || "",
|
||||
aspectRatio: document.getElementById("wfm-video-aspect-ratio")?.value || "",
|
||||
megapixels: Number(document.getElementById("wfm-video-megapixels")?.value) || 0.2,
|
||||
multiple: Number(document.getElementById("wfm-video-multiple")?.value) || 32,
|
||||
duration: Number(document.getElementById("wfm-video-duration")?.value) || 2,
|
||||
};
|
||||
}
|
||||
|
||||
function _applyFormToWorkflow(nodes, form) {
|
||||
const { instanceNode, resolutionNode, loadImageNode, promptIdx, durationIdx } = nodes;
|
||||
|
||||
instanceNode.widgets_values[promptIdx] = form.prompt;
|
||||
instanceNode.widgets_values[durationIdx] = form.duration;
|
||||
|
||||
const arIdx = _widgetIndex(resolutionNode, "aspect_ratio");
|
||||
const mpIdx = _widgetIndex(resolutionNode, "megapixels");
|
||||
const multIdx = _widgetIndex(resolutionNode, "multiple");
|
||||
if (arIdx !== -1) resolutionNode.widgets_values[arIdx] = form.aspectRatio;
|
||||
if (mpIdx !== -1) resolutionNode.widgets_values[mpIdx] = form.megapixels;
|
||||
if (multIdx !== -1) resolutionNode.widgets_values[multIdx] = form.multiple;
|
||||
|
||||
// first_frameは既存のLoadImageノードが接続されている場合のみここで書き換える。
|
||||
// 接続が無い場合(loadImageNode: null)は、first_frameが新規指定された時だけ
|
||||
// _injectFrameNode()でノードごと注入する(未指定ならバイパス=未接続のまま実行)。
|
||||
if (loadImageNode) {
|
||||
const fnIdx = _widgetIndex(loadImageNode, "image");
|
||||
if (fnIdx !== -1) loadImageNode.widgets_values[fnIdx] = state.firstFrameFilename;
|
||||
}
|
||||
}
|
||||
|
||||
// first_frame/last_frameが未接続のワークフローに対して、画像が指定された時だけ
|
||||
// LoadImageノードを新規注入する共通ヘルパー。ノードID/リンクIDはclone内の実際の
|
||||
// 最大値+1で採番するため、重複ID/リンクID不整合は起きない
|
||||
// (CLAUDE.md記載の既知の落とし穴への対策)。未指定時は何もしない
|
||||
// (テンプレート由来のlink:nullのまま=クリーンな状態を毎回cloneするので残留の心配がない=バイパス扱い)。
|
||||
function _injectFrameNode(clone, instanceNode, targetSlot, filename) {
|
||||
const maxNodeId = Math.max(clone.last_node_id || 0, ...clone.nodes.map((n) => n.id));
|
||||
const maxLinkId = Math.max(clone.last_link_id || 0, ...clone.links.map((l) => l[0]));
|
||||
const newNodeId = maxNodeId + 1;
|
||||
const newLinkId = maxLinkId + 1;
|
||||
|
||||
clone.nodes.push({
|
||||
id: newNodeId,
|
||||
type: "LoadImage",
|
||||
pos: [(instanceNode.pos?.[0] || 0) - 260, (instanceNode.pos?.[1] || 0) + 40],
|
||||
size: [280, 314],
|
||||
flags: {},
|
||||
order: clone.nodes.length,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{ localized_name: "image", name: "image", type: "COMBO", widget: { name: "image" }, link: null },
|
||||
{ localized_name: "choose file to upload", name: "upload", type: "IMAGEUPLOAD", widget: { name: "upload" }, link: null },
|
||||
],
|
||||
outputs: [
|
||||
{ localized_name: "IMAGE", name: "IMAGE", type: "IMAGE", links: [newLinkId] },
|
||||
{ localized_name: "MASK", name: "MASK", type: "MASK", links: null },
|
||||
],
|
||||
properties: { "Node name for S&R": "LoadImage" },
|
||||
widgets_values: [filename, "image"],
|
||||
});
|
||||
|
||||
clone.links.push([newLinkId, newNodeId, 0, instanceNode.id, targetSlot, "IMAGE"]);
|
||||
const targetInput = instanceNode.inputs[targetSlot];
|
||||
if (targetInput) targetInput.link = newLinkId;
|
||||
|
||||
clone.last_node_id = newNodeId;
|
||||
clone.last_link_id = newLinkId;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 実行
|
||||
// ============================================
|
||||
|
||||
async function runGeneration() {
|
||||
if (state.generating) return;
|
||||
if (!state.templateWorkflow) {
|
||||
showToast(t("videoNoWorkflowLoaded"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const clone = structuredClone(state.templateWorkflow);
|
||||
const nodes = locateMiniMaxNodes(clone);
|
||||
if (!nodes) {
|
||||
showToast(t("videoUnsupportedWorkflow"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
_applyFormToWorkflow(nodes, _readFormValues());
|
||||
// first_frame: 既存のLoadImageノードが無いワークフロー(T2V構成等)で、
|
||||
// ユーザーが新たに画像を指定した場合のみノードを注入する。未指定ならバイパス(未接続のまま実行)。
|
||||
if (!nodes.loadImageNode && state.firstFrameFilename) {
|
||||
_injectFrameNode(clone, nodes.instanceNode, nodes.firstFrameSlot, state.firstFrameFilename);
|
||||
}
|
||||
if (state.lastFrameFilename) {
|
||||
_injectFrameNode(clone, nodes.instanceNode, nodes.lastFrameSlot, state.lastFrameFilename);
|
||||
}
|
||||
|
||||
const genBtn = document.getElementById("wfm-video-generate-btn");
|
||||
const progressBar = document.getElementById("wfm-video-progress-bar");
|
||||
const progressText = document.getElementById("wfm-video-progress-text");
|
||||
|
||||
state.generating = true;
|
||||
if (genBtn) genBtn.disabled = true;
|
||||
if (progressBar) progressBar.style.width = "0%";
|
||||
if (progressText) progressText.textContent = "Converting workflow...";
|
||||
|
||||
try {
|
||||
const apiWorkflow = await comfyWorkflow.convertUiToApi(clone);
|
||||
|
||||
const seedValue = Number(document.getElementById("wfm-video-noise-seed")?.value) || 0;
|
||||
const seedMode = document.getElementById("wfm-video-seed-randomize")?.checked ? "random" : "fixed";
|
||||
|
||||
const { images, seed } = await comfyUI.generate(apiWorkflow, {
|
||||
seedMode,
|
||||
seedValue,
|
||||
timeoutMs: 30 * 60 * 1000, // 動画生成は静止画より長時間になりうる
|
||||
onProgress: (pct) => {
|
||||
if (progressBar) progressBar.style.width = `${(pct * 100).toFixed(1)}%`;
|
||||
if (progressText) progressText.textContent = `${(pct * 100).toFixed(0)}%`;
|
||||
},
|
||||
});
|
||||
|
||||
const seedEl = document.getElementById("wfm-video-noise-seed");
|
||||
if (seedEl) seedEl.value = seed;
|
||||
|
||||
const outputMedia = images.filter((img) => img.type !== "temp");
|
||||
if (progressText) progressText.textContent = t("videoDoneCount", outputMedia.length);
|
||||
if (progressBar) progressBar.style.width = "100%";
|
||||
|
||||
_renderResult(outputMedia);
|
||||
await _saveGeneratedVideoMeta(outputMedia, apiWorkflow);
|
||||
showToast(t("videoGenerateSuccess"), "success");
|
||||
} catch (err) {
|
||||
showToast(t("errorWithMsg", err.message), "error");
|
||||
if (progressText) progressText.textContent = "Error";
|
||||
} finally {
|
||||
state.generating = false;
|
||||
if (genBtn) genBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 結果表示
|
||||
// ============================================
|
||||
|
||||
// Sets (or clears) the center preview frame's video and records where it came from —
|
||||
// `source` is what the Frame/GIF property tabs act on (see state.previewSource above).
|
||||
// Revokes the previous blob: URL (if any) so picking a series of local files doesn't
|
||||
// leak memory.
|
||||
let _previewObjectUrl = null;
|
||||
|
||||
function _setPreviewMedia(url, source) {
|
||||
state.previewSource = source;
|
||||
const video = document.getElementById("wfm-video-preview-video");
|
||||
const placeholder = document.getElementById("wfm-video-preview-placeholder");
|
||||
if (!video || !placeholder) return;
|
||||
|
||||
if (_previewObjectUrl) {
|
||||
URL.revokeObjectURL(_previewObjectUrl);
|
||||
_previewObjectUrl = null;
|
||||
}
|
||||
if (url && url.startsWith("blob:")) _previewObjectUrl = url;
|
||||
|
||||
if (url) {
|
||||
video.src = url;
|
||||
video.style.display = "block";
|
||||
placeholder.style.display = "none";
|
||||
} else {
|
||||
video.pause();
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
video.style.display = "none";
|
||||
placeholder.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function _renderResult(images) {
|
||||
// MiniMax H3 always emits a single VIDEO output — but guard generically (prefer
|
||||
// an mp4 if present) in case a future video workflow's SaveVideo/PreviewVideo
|
||||
// ordering differs.
|
||||
const media = (images || []).find((img) => img.filename.toLowerCase().endsWith(".mp4")) || images?.[0];
|
||||
if (!media) {
|
||||
_setPreviewMedia(null, null);
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams({ filename: media.filename, subfolder: media.subfolder || "", type: media.type || "output" });
|
||||
const src = `${comfyUI.baseUrl}/view?${params}`;
|
||||
_setPreviewMedia(src, {
|
||||
kind: media.type === "input" ? "input" : "output",
|
||||
filename: media.filename,
|
||||
subfolder: media.subfolder || "",
|
||||
type: media.type || "output",
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Gallery連携(生成結果のワークフローをサイドカーメタとして保存)
|
||||
// ============================================
|
||||
|
||||
let _outputDir = "";
|
||||
|
||||
async function _fetchOutputDir() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/settings/output-dir");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
_outputDir = (data.current || "").replace(/\\/g, "/").replace(/\/$/, "");
|
||||
}
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
async function _saveGeneratedVideoMeta(images, workflow) {
|
||||
if (!_outputDir) await _fetchOutputDir();
|
||||
if (!_outputDir) return;
|
||||
for (const img of images) {
|
||||
if (img.type !== "output") continue;
|
||||
const parts = [_outputDir];
|
||||
if (img.subfolder) parts.push(img.subfolder);
|
||||
parts.push(img.filename);
|
||||
const path = parts.join("/");
|
||||
try {
|
||||
await fetch("/wfm/gallery/image/meta", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path, workflow }),
|
||||
});
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// i18nの適用(app.jsのapplyI18nToHtml()と同じ「個別要素にt()を適用」方式)
|
||||
// ============================================
|
||||
|
||||
function _applyVideoI18n() {
|
||||
const setText = (id, key) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = t(key);
|
||||
};
|
||||
setText("wfm-video-title-label", "videoTitle");
|
||||
setText("wfm-video-first-frame-label", "videoFirstFrame");
|
||||
setText("wfm-video-last-frame-label", "videoLastFrame");
|
||||
setText("wfm-video-first-frame-drop-label", "videoDropOrClick");
|
||||
setText("wfm-video-last-frame-drop-label", "videoDropOrClick");
|
||||
setText("wfm-video-first-frame-clear", "clear");
|
||||
setText("wfm-video-last-frame-clear", "clear");
|
||||
setText("wfm-video-prompt-label", "videoPrompt");
|
||||
setText("wfm-video-aspect-ratio-label", "videoAspectRatio");
|
||||
setText("wfm-video-megapixels-label", "videoMegapixels");
|
||||
setText("wfm-video-multiple-label", "videoMultiple");
|
||||
setText("wfm-video-duration-label", "videoDuration");
|
||||
setText("wfm-video-noise-seed-label", "videoNoiseSeed");
|
||||
setText("wfm-video-randomize-label", "videoRandomizeSeed");
|
||||
setText("wfm-video-generate-btn", "videoGenerate");
|
||||
setText("wfm-video-edit-placeholder-label", "videoEditPlaceholder");
|
||||
setText("wfm-video-source-label", "videoSourceLabel");
|
||||
setText("wfm-video-source-hint", "videoSourceHint");
|
||||
setText("wfm-video-source-drop-label", "videoSourceDropLabel");
|
||||
setText("wfm-video-source-clear", "clear");
|
||||
setText("wfm-video-preview-placeholder", "videoNoVideo");
|
||||
setText("wfm-video-prop-tab-frame", "videoPropFrame");
|
||||
setText("wfm-video-prop-tab-gif", "videoPropGif");
|
||||
setText("wfm-video-frame-hint", "videoFrameHint");
|
||||
setText("wfm-video-frame-capture-btn", "videoCaptureFrame");
|
||||
setText("wfm-video-frame-save-btn", "videoSaveToOutput");
|
||||
setText("wfm-video-gif-start-label", "videoGifStart");
|
||||
setText("wfm-video-gif-end-label", "videoGifEnd");
|
||||
setText("wfm-video-gif-fps-label", "videoGifFps");
|
||||
setText("wfm-video-gif-width-label", "videoGifWidth");
|
||||
setText("wfm-video-gif-convert-btn", "videoConvertToGif");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Video Source panel — loads any local video into the preview frame, independent of
|
||||
// generation, so Frame/GIF tools work without running MiniMax H3 first.
|
||||
// ============================================
|
||||
|
||||
function _wireVideoSourcePanel() {
|
||||
_wireDropZone("wfm-video-source-drop", "wfm-video-source-file", (file) => {
|
||||
if (!file.type.startsWith("video/")) return;
|
||||
const statusEl = document.getElementById("wfm-video-source-status");
|
||||
_setPreviewMedia(URL.createObjectURL(file), { kind: "local", file });
|
||||
if (statusEl) { statusEl.textContent = file.name; statusEl.style.color = ""; }
|
||||
});
|
||||
|
||||
document.getElementById("wfm-video-source-clear")?.addEventListener("click", () => {
|
||||
_setPreviewMedia(null, null);
|
||||
const statusEl = document.getElementById("wfm-video-source-status");
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
const fileInput = document.getElementById("wfm-video-source-file");
|
||||
if (fileInput) fileInput.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Properties panel tabs (Frame / GIF)
|
||||
// ============================================
|
||||
|
||||
function _initPropTabs() {
|
||||
const tabs = document.querySelectorAll(".wfm-video-prop-tab");
|
||||
tabs.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
tabs.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
const target = btn.dataset.propTab;
|
||||
document.querySelectorAll(".wfm-video-prop-content").forEach((c) => {
|
||||
c.style.display = c.id === `wfm-video-prop-${target}` ? "" : "none";
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Frame tab — captures the preview video's current playback position as a PNG,
|
||||
// client-side (canvas), and saves it into ComfyUI's own output folder.
|
||||
// ============================================
|
||||
|
||||
let _capturedFrameBlob = null;
|
||||
|
||||
function _blobToDataUrl(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(new Error("Failed to read blob"));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
function _initFrameTab() {
|
||||
const captureBtn = document.getElementById("wfm-video-frame-capture-btn");
|
||||
const saveBtn = document.getElementById("wfm-video-frame-save-btn");
|
||||
const previewWrap = document.getElementById("wfm-video-frame-preview-wrap");
|
||||
const previewImg = document.getElementById("wfm-video-frame-preview-img");
|
||||
const statusEl = document.getElementById("wfm-video-frame-status");
|
||||
|
||||
captureBtn?.addEventListener("click", () => {
|
||||
const video = document.getElementById("wfm-video-preview-video");
|
||||
if (!video || video.style.display === "none" || !video.videoWidth) {
|
||||
showToast(t("videoNoSourceLoaded"), "error");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas.getContext("2d").drawImage(video, 0, 0);
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return;
|
||||
_capturedFrameBlob = blob;
|
||||
if (previewImg) previewImg.src = URL.createObjectURL(blob);
|
||||
if (previewWrap) previewWrap.style.display = "";
|
||||
if (saveBtn) saveBtn.disabled = false;
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
}, "image/png");
|
||||
} catch (err) {
|
||||
showToast(t("videoFrameCaptureFailed", err.message), "error");
|
||||
}
|
||||
});
|
||||
|
||||
saveBtn?.addEventListener("click", async () => {
|
||||
if (!_capturedFrameBlob) return;
|
||||
saveBtn.disabled = true;
|
||||
if (statusEl) statusEl.textContent = "Saving...";
|
||||
try {
|
||||
const dataUrl = await _blobToDataUrl(_capturedFrameBlob);
|
||||
const res = await fetch("/api/wfm/video/frame/save-to-output", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ image_base64: dataUrl }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
|
||||
if (statusEl) { statusEl.textContent = `✓ ${json.filename}`; statusEl.style.color = "var(--wfm-success)"; }
|
||||
showToast(t("videoFrameSaved", json.filename), "success");
|
||||
} catch (err) {
|
||||
if (statusEl) { statusEl.textContent = `✗ ${err.message}`; statusEl.style.color = "var(--wfm-danger)"; }
|
||||
showToast(t("errorWithMsg", err.message), "error");
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GIF tab — sends the preview video (uploading it first if it's a local file not yet
|
||||
// on the server) to the server, which decodes it with PyAV and encodes an animated
|
||||
// GIF with Pillow (both already required by ComfyUI itself — no ffmpeg dependency).
|
||||
// ============================================
|
||||
|
||||
function _initGifTab() {
|
||||
const convertBtn = document.getElementById("wfm-video-gif-convert-btn");
|
||||
const progressBar = document.getElementById("wfm-video-gif-progress-bar");
|
||||
const progressText = document.getElementById("wfm-video-gif-progress-text");
|
||||
const previewWrap = document.getElementById("wfm-video-gif-preview-wrap");
|
||||
const previewImg = document.getElementById("wfm-video-gif-preview-img");
|
||||
const statusEl = document.getElementById("wfm-video-gif-status");
|
||||
|
||||
convertBtn?.addEventListener("click", async () => {
|
||||
if (!state.previewSource) {
|
||||
showToast(t("videoNoSourceLoaded"), "error");
|
||||
return;
|
||||
}
|
||||
convertBtn.disabled = true;
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
if (progressBar) progressBar.style.width = "20%";
|
||||
if (progressText) progressText.textContent = "Uploading...";
|
||||
|
||||
try {
|
||||
let ref = state.previewSource;
|
||||
if (ref.kind === "local") {
|
||||
const result = await comfyUI.uploadImage(ref.file, ref.file.name);
|
||||
ref = { kind: "input", filename: result.name, subfolder: result.subfolder || "", type: "input" };
|
||||
// Cache the uploaded reference so a repeat conversion doesn't re-upload.
|
||||
state.previewSource = ref;
|
||||
}
|
||||
|
||||
const start = Number(document.getElementById("wfm-video-gif-start")?.value) || 0;
|
||||
const endRaw = document.getElementById("wfm-video-gif-end")?.value;
|
||||
const fps = Number(document.getElementById("wfm-video-gif-fps")?.value) || 10;
|
||||
const widthRaw = document.getElementById("wfm-video-gif-width")?.value;
|
||||
|
||||
if (progressBar) progressBar.style.width = "60%";
|
||||
if (progressText) progressText.textContent = t("videoGifConverting");
|
||||
|
||||
const res = await fetch("/api/wfm/video/to-gif", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
filename: ref.filename,
|
||||
subfolder: ref.subfolder || "",
|
||||
type: ref.type,
|
||||
start_time: start,
|
||||
end_time: endRaw ? Number(endRaw) : null,
|
||||
fps,
|
||||
max_width: widthRaw ? Number(widthRaw) : null,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
|
||||
|
||||
if (progressBar) progressBar.style.width = "100%";
|
||||
if (progressText) progressText.textContent = t("videoGifDoneCount", json.frame_count ?? 0);
|
||||
|
||||
const params = new URLSearchParams({ filename: json.filename, subfolder: json.subfolder || "", type: "output" });
|
||||
if (previewImg) previewImg.src = `${comfyUI.baseUrl}/view?${params}`;
|
||||
if (previewWrap) previewWrap.style.display = "";
|
||||
if (statusEl) { statusEl.textContent = `✓ ${json.filename}`; statusEl.style.color = "var(--wfm-success)"; }
|
||||
showToast(t("videoGifSaved", json.filename), "success");
|
||||
} catch (err) {
|
||||
if (statusEl) { statusEl.textContent = `✗ ${err.message}`; statusEl.style.color = "var(--wfm-danger)"; }
|
||||
if (progressText) progressText.textContent = "Error";
|
||||
showToast(t("errorWithMsg", err.message), "error");
|
||||
} finally {
|
||||
convertBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 初期化
|
||||
// ============================================
|
||||
|
||||
export function initVideoTab() {
|
||||
_applyVideoI18n();
|
||||
_populateAspectRatioOptions();
|
||||
|
||||
_wireDropZone("wfm-video-first-frame-drop", "wfm-video-first-frame-file", (file) => _handleFrameUpload("first", file));
|
||||
_wireDropZone("wfm-video-last-frame-drop", "wfm-video-last-frame-file", (file) => _handleFrameUpload("last", file));
|
||||
|
||||
document.getElementById("wfm-video-first-frame-clear")?.addEventListener("click", () => _clearFrame("first"));
|
||||
document.getElementById("wfm-video-last-frame-clear")?.addEventListener("click", () => _clearFrame("last"));
|
||||
document.getElementById("wfm-video-generate-btn")?.addEventListener("click", runGeneration);
|
||||
|
||||
_wireVideoSourcePanel();
|
||||
_initPropTabs();
|
||||
_initFrameTab();
|
||||
_initGifTab();
|
||||
|
||||
loadDefaultWorkflow();
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Video helper utilities shared across tabs that deal with generated mp4 output
|
||||
* (Lab tab's "use generated image for next" chaining, and the Video tab's future
|
||||
* batch features). Kept dependency-free (no comfyUI/comfyEditor imports) so it can
|
||||
* be reused from either tab without pulling in unrelated state.
|
||||
*/
|
||||
|
||||
export function isVideoFilename(filename) {
|
||||
return /\.(mp4|webm|mov)$/i.test(filename || "");
|
||||
}
|
||||
|
||||
// Extracts the last frame of a video at `videoUrl` as a PNG Blob, by seeking a
|
||||
// detached <video> element to just before its end and drawing that frame onto a
|
||||
// canvas. Runs entirely client-side (no server round-trip / PyAV dependency).
|
||||
//
|
||||
// Seeks to (duration - 0.05s) rather than duration itself: seeking exactly to the
|
||||
// end is unreliable across browsers (some clamp it back to the last keyframe's
|
||||
// timestamp, which can fire a "seeked" event before the frame is actually decoded).
|
||||
// The "seeked" listener also ignores events that don't land near that target time,
|
||||
// since loading a video can fire an initial seeked-to-0 before the real seek lands.
|
||||
export function extractLastFrameBlob(videoUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement("video");
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.preload = "auto";
|
||||
|
||||
let target = null;
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
};
|
||||
const finish = (fn, arg) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
fn(arg);
|
||||
};
|
||||
|
||||
video.addEventListener("error", () => {
|
||||
finish(reject, new Error("Failed to load video for last-frame extraction"));
|
||||
});
|
||||
|
||||
video.addEventListener("loadedmetadata", () => {
|
||||
if (!Number.isFinite(video.duration) || video.duration <= 0) {
|
||||
finish(reject, new Error("Video has no readable duration"));
|
||||
return;
|
||||
}
|
||||
target = Math.max(0, video.duration - 0.05);
|
||||
video.currentTime = target;
|
||||
});
|
||||
|
||||
video.addEventListener("seeked", () => {
|
||||
if (target == null || Math.abs(video.currentTime - target) > 0.5) return;
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas.getContext("2d").drawImage(video, 0, 0);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) finish(resolve, blob);
|
||||
else finish(reject, new Error("Failed to encode last frame"));
|
||||
}, "image/png");
|
||||
} catch (err) {
|
||||
finish(reject, err);
|
||||
}
|
||||
});
|
||||
|
||||
video.src = videoUrl;
|
||||
});
|
||||
}
|
||||
+27
-35
@@ -7,6 +7,7 @@
|
||||
import { showToast, openModal, closeModal } from "./app.js";
|
||||
import { comfyWorkflow } from "./comfyui-workflow.js";
|
||||
import { loadWorkflowIntoEditor } from "./generate-tab.js";
|
||||
import { loadWorkflowIntoVideoEditor } from "./video-tab.js";
|
||||
import { t, getSummaryPrompt } from "./i18n.js";
|
||||
import { highlightJSON } from "./json-highlight.js";
|
||||
import { openBadgeEditModal } from "./models-tab.js";
|
||||
@@ -46,7 +47,7 @@ function saveBadgePalette(palette) {
|
||||
// Open in ComfyUI / Send to Canvas
|
||||
// ============================================
|
||||
|
||||
function sendToCanvas(workflowData) {
|
||||
function sendToCanvas(workflowData, filename) {
|
||||
try {
|
||||
// window.opener経由でComfyUIキャンバスに直接ロード(推奨)
|
||||
if (window.opener && typeof window.opener.wfmReceiveWorkflow === "function") {
|
||||
@@ -54,14 +55,13 @@ function sendToCanvas(workflowData) {
|
||||
showToast(t("workflowSentToCanvasDirect"), "success");
|
||||
return;
|
||||
}
|
||||
// フォールバック: localStorage + タイトルドラッグ(UI形式のみ)
|
||||
const fmt = comfyWorkflow.detectFormat(workflowData);
|
||||
if (fmt === "api") {
|
||||
showToast(t("apiFormatCanvasNoOpener"), "error");
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("wfm_pending_workflow", JSON.stringify(workflowData));
|
||||
showToast(t("workflowSentToCanvas"), "success");
|
||||
// フォールバック: Workflow Studio LibraryのWorkflows(W)タブを当該ワークフローで
|
||||
// 絞り込み表示する。以前はUI形式限定で「タイトルをドラッグ」させていたが、
|
||||
// サムネイルが見えず分かりにくい上、API形式のワークフローはそもそも非対応だった。
|
||||
// 絞り込み表示なら実物のワークフローカード(サムネイル付き)が出るので、
|
||||
// フォーマットを問わずユーザー自身がそこからドラッグできる。
|
||||
localStorage.setItem("wfm_library_filter_request", filename || "");
|
||||
showToast(t("workflowFilteredInLibrary"), "success");
|
||||
} catch (err) {
|
||||
showToast(t("errorWithMsg", err.message), "error");
|
||||
}
|
||||
@@ -616,6 +616,8 @@ async function showSidePanel(wf, cardEl) {
|
||||
// Enable toolbar load buttons
|
||||
const listLoadBtn = document.getElementById("wfm-list-load-btn");
|
||||
if (listLoadBtn) { listLoadBtn.disabled = false; listLoadBtn.title = ""; }
|
||||
const listLoadVideoBtn = document.getElementById("wfm-list-load-video-btn");
|
||||
if (listLoadVideoBtn) { listLoadVideoBtn.disabled = false; listLoadVideoBtn.title = ""; }
|
||||
const listOpenComfyBtn = document.getElementById("wfm-list-open-comfyui-btn");
|
||||
if (listOpenComfyBtn) { listOpenComfyBtn.disabled = false; listOpenComfyBtn.title = ""; }
|
||||
|
||||
@@ -661,6 +663,8 @@ function closeSidePanel() {
|
||||
if (titleEl) titleEl.textContent = "";
|
||||
const listLoadBtn = document.getElementById("wfm-list-load-btn");
|
||||
if (listLoadBtn) { listLoadBtn.disabled = true; listLoadBtn.title = t("selectCardFirst"); }
|
||||
const listLoadVideoBtn = document.getElementById("wfm-list-load-video-btn");
|
||||
if (listLoadVideoBtn) { listLoadVideoBtn.disabled = true; listLoadVideoBtn.title = t("selectCardFirst"); }
|
||||
const listOpenComfyBtn = document.getElementById("wfm-list-open-comfyui-btn");
|
||||
if (listOpenComfyBtn) { listOpenComfyBtn.disabled = true; listOpenComfyBtn.title = t("selectCardFirst"); }
|
||||
renderSideGroup(null);
|
||||
@@ -957,8 +961,6 @@ function openDetailModal(wf) {
|
||||
<textarea class="wfm-textarea" id="wfm-detail-memo" rows="4" placeholder="${t("memoPlaceholder")}">${meta.memo || ""}</textarea>
|
||||
</section>
|
||||
<div class="wfm-modal-actions">
|
||||
<button class="wfm-btn wfm-btn-primary" id="wfm-detail-load">${t("loadInGenerate")}</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-detail-open-comfyui">${t("sendToCanvas")}</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-detail-set-default">${t("setAsDefault")}</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-detail-change-thumb">${t("changeThumbnail")}</button>
|
||||
<input type="file" id="wfm-detail-thumb-file" accept="image/png,image/webp,image/jpeg" style="display:none">
|
||||
@@ -1095,29 +1097,6 @@ function openDetailModal(wf) {
|
||||
}
|
||||
});
|
||||
|
||||
// Load in GenerateUI
|
||||
document.getElementById("wfm-detail-load").addEventListener("click", async () => {
|
||||
try {
|
||||
const wfData = await getRawWorkflow(wf.filename);
|
||||
const loaded = await loadWorkflowIntoEditor(wfData, wf.filename);
|
||||
if (loaded === false) return;
|
||||
document.querySelector('.wfm-tab[data-tab="generate"]')?.click();
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
showToast(t("loadError") + ": " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Send to Canvas
|
||||
document.getElementById("wfm-detail-open-comfyui")?.addEventListener("click", async () => {
|
||||
try {
|
||||
const wfData = await getRawWorkflow(wf.filename);
|
||||
sendToCanvas(wfData);
|
||||
} catch (err) {
|
||||
showToast(t("loadError") + ": " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Set as default workflow
|
||||
document.getElementById("wfm-detail-set-default")?.addEventListener("click", async () => {
|
||||
try {
|
||||
@@ -1399,12 +1378,25 @@ export function initWorkflowTab() {
|
||||
}
|
||||
});
|
||||
|
||||
// Toolbar: Load in Video button (MiniMax H3専用。非対応ワークフローはloadWorkflowIntoVideoEditor内でエラートースト表示済み)
|
||||
document.getElementById("wfm-list-load-video-btn")?.addEventListener("click", async () => {
|
||||
if (!state.selectedWf) return;
|
||||
try {
|
||||
const wfData = await getRawWorkflow(state.selectedWf.filename);
|
||||
const loaded = await loadWorkflowIntoVideoEditor(wfData, state.selectedWf.filename);
|
||||
if (loaded === false) return;
|
||||
document.querySelector('.wfm-tab[data-tab="video"]')?.click();
|
||||
} catch (err) {
|
||||
showToast(t("loadError") + ": " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Toolbar: Send to Canvas button
|
||||
document.getElementById("wfm-list-open-comfyui-btn")?.addEventListener("click", async () => {
|
||||
if (!state.selectedWf) return;
|
||||
try {
|
||||
const wfData = await getRawWorkflow(state.selectedWf.filename);
|
||||
sendToCanvas(wfData);
|
||||
sendToCanvas(wfData, state.selectedWf.filename);
|
||||
} catch (err) {
|
||||
showToast(t("loadError") + ": " + err.message, "error");
|
||||
}
|
||||
|
||||
+465
-77
@@ -10,6 +10,7 @@
|
||||
<link rel="stylesheet" href="/wfm_static/css/tagger-tab.css">
|
||||
<link rel="stylesheet" href="/wfm_static/css/image-edit-tab.css">
|
||||
<link rel="stylesheet" href="/wfm_static/css/lab-tab.css">
|
||||
<link rel="stylesheet" href="/wfm_static/css/video-tab.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
@@ -23,9 +24,9 @@
|
||||
<button class="wfm-tab" data-tab="models">Models</button>
|
||||
<button class="wfm-tab" data-tab="generate">GenerateUI</button>
|
||||
<button class="wfm-tab" data-tab="prompt">Prompt</button>
|
||||
<button class="wfm-tab" data-tab="metadata">Metadata</button>
|
||||
<button class="wfm-tab" data-tab="gallery">Gallery</button>
|
||||
<button class="wfm-tab" data-tab="image-edit">Image Edit</button>
|
||||
<button class="wfm-tab" data-tab="video">Video</button>
|
||||
<button class="wfm-tab" data-tab="tagger">Tagger</button>
|
||||
<button class="wfm-tab" data-tab="settings">Settings</button>
|
||||
<button class="wfm-tab" data-tab="help">Help</button>
|
||||
@@ -55,6 +56,7 @@
|
||||
<option value="">All Groups</option>
|
||||
</select>
|
||||
<button id="wfm-list-load-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary" disabled title="Select a card first">Load in GenerateUI</button>
|
||||
<button id="wfm-list-load-video-btn" class="wfm-btn wfm-btn-sm" disabled title="Select a card first">Load in Video</button>
|
||||
<button id="wfm-list-open-comfyui-btn" class="wfm-btn wfm-btn-sm" disabled title="Select a card first">Send to Canvas</button>
|
||||
<div class="wfm-view-btns">
|
||||
<button class="wfm-view-btn active" data-view="thumb" title="Thumbnail">⊞</button>
|
||||
@@ -351,6 +353,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">
|
||||
@@ -567,6 +570,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-lab-col-btns">
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-up" data-col="model" title="Move selected keyframe up">▲</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-down" data-col="model" title="Move selected keyframe down">▼</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-add" data-col="model" title="Add keyframe">+</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-remove" data-col="model" title="Remove last keyframe">-</button>
|
||||
</div>
|
||||
@@ -577,6 +582,9 @@
|
||||
<div class="wfm-lab-col-header">
|
||||
<span>プロンプト</span>
|
||||
<div class="wfm-lab-col-btns">
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-up" data-col="prompt" title="Move selected keyframe up">▲</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-down" data-col="prompt" title="Move selected keyframe down">▼</button>
|
||||
<button type="button" id="wfm-lab-prompt-group-btn" class="wfm-btn wfm-btn-xs" title="Apply Prompt Group">PG</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-add" data-col="prompt" title="Add keyframe">+</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-remove" data-col="prompt" title="Remove last keyframe">-</button>
|
||||
</div>
|
||||
@@ -587,6 +595,8 @@
|
||||
<div class="wfm-lab-col-header">
|
||||
<span>Ksampler</span>
|
||||
<div class="wfm-lab-col-btns">
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-up" data-col="ksampler" title="Move selected keyframe up">▲</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-down" data-col="ksampler" title="Move selected keyframe down">▼</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-add" data-col="ksampler" title="Add keyframe">+</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-xs wfm-lab-col-remove" data-col="ksampler" title="Remove last keyframe">-</button>
|
||||
</div>
|
||||
@@ -898,6 +908,7 @@
|
||||
<!-- Result Display -->
|
||||
<div class="wfm-gen-result">
|
||||
<img id="wfm-gen-result-img" style="display:none;max-width:100%;border-radius:6px;">
|
||||
<video id="wfm-gen-result-video" controls style="display:none;max-width:100%;border-radius:6px;"></video>
|
||||
<div id="wfm-gen-result-thumbs" class="wfm-gen-thumbs"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -927,6 +938,13 @@
|
||||
|
||||
<!-- Prompt Tab -->
|
||||
<section id="wfm-tab-prompt" class="wfm-tab-content">
|
||||
<div class="wfm-prompt-toplevel-nav">
|
||||
<button id="wfm-prompt-toplevel-form" class="wfm-prompt-toplevel-tab active" data-prompt-toplevel="form">Form</button>
|
||||
<button id="wfm-prompt-toplevel-table" class="wfm-prompt-toplevel-tab" data-prompt-toplevel="table">Table</button>
|
||||
</div>
|
||||
|
||||
<!-- Form パネル -->
|
||||
<div id="wfm-prompt-panel-form" class="wfm-prompt-panel active">
|
||||
<div class="wfm-prompt-split wfm-prompt-split-3col">
|
||||
<!-- Col 1: AI Assistant -->
|
||||
<div class="wfm-prompt-split-col">
|
||||
@@ -1152,76 +1170,144 @@
|
||||
<!-- /Style タブ -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- /Form パネル -->
|
||||
|
||||
<!-- Metadata Tab -->
|
||||
<section id="wfm-tab-metadata" class="wfm-tab-content">
|
||||
<div class="wfm-metadata-layout">
|
||||
<!-- Col1: Drop zone + preview -->
|
||||
<div class="wfm-metadata-col1">
|
||||
<div class="wfm-metadata-drop-zone" id="wfm-meta-drop">
|
||||
<div class="wfm-meta-drop-label" id="wfm-meta-drop-label">
|
||||
<div class="wfm-meta-drop-main">Drop image / JSON</div>
|
||||
<div class="wfm-meta-drop-sub">PNG · WebP · JSON</div>
|
||||
</div>
|
||||
<img id="wfm-meta-preview-img" class="wfm-meta-preview-img" style="display:none;" alt="preview">
|
||||
</div>
|
||||
<div class="wfm-meta-file-info" id="wfm-meta-file-info"></div>
|
||||
<div class="wfm-meta-format-note" id="wfm-meta-format-note">
|
||||
<div class="wfm-meta-format-note-title" id="wfm-meta-format-note-title">Supported formats</div>
|
||||
<ul class="wfm-meta-format-list">
|
||||
<li id="wfm-meta-fmt-comfyui">ComfyUI — PNG / WebP / JSON</li>
|
||||
<li id="wfm-meta-fmt-sdwebui">SD WebUI / SD Forge — PNG (parameters)</li>
|
||||
<li id="wfm-meta-fmt-fooocus">Fooocus — PNG (fooocus_scheme)</li>
|
||||
</ul>
|
||||
<div class="wfm-meta-format-todo" id="wfm-meta-format-todo">Flux.2 / Qwen-Image / Z-Image subgraph workflows supported</div>
|
||||
</div>
|
||||
<input type="file" id="wfm-meta-file-input" accept="image/png,image/webp,.json,application/json" style="display:none;">
|
||||
<!-- Table パネル -->
|
||||
<div id="wfm-prompt-panel-table" class="wfm-prompt-panel">
|
||||
<div class="wfm-prompt-table-subnav">
|
||||
<button class="wfm-prompt-table-subtab active" data-ptable-subtab="presets">Presets</button>
|
||||
<button class="wfm-prompt-table-subtab" data-ptable-subtab="presetsGroup">Presets Group</button>
|
||||
<button class="wfm-prompt-table-subtab" data-ptable-subtab="wildcards">Wildcards</button>
|
||||
<button class="wfm-prompt-table-subtab" data-ptable-subtab="style">Style</button>
|
||||
</div>
|
||||
|
||||
<!-- Col2: Model info -->
|
||||
<div class="wfm-metadata-col2">
|
||||
<div class="wfm-meta-section" id="wfm-meta-ckpt-section">
|
||||
<div class="wfm-meta-section-title">Checkpoint</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-ckpt-list"></div>
|
||||
<!-- Presets テーブル -->
|
||||
<div id="wfm-ptable-presets" class="wfm-prompt-table-pane active">
|
||||
<div class="wfm-prompt-table-toolbar">
|
||||
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
|
||||
<input type="text" id="wfm-ptable-presets-search" class="wfm-search-input" placeholder="Search presets...">
|
||||
<button class="wfm-search-clear-btn" id="wfm-ptable-presets-search-clear-btn" title="Clear search">✕</button>
|
||||
</div>
|
||||
<button id="wfm-ptable-presets-sort-fav-btn" class="wfm-btn wfm-btn-sm" title="Sort favorites to top">★</button>
|
||||
<button id="wfm-ptable-presets-sort-batch-btn" class="wfm-btn wfm-btn-sm" title="Sort batch-selected to top">B</button>
|
||||
<span id="wfm-ptable-presets-count" class="wfm-nodes-count"></span>
|
||||
<span style="flex:1;"></span>
|
||||
<button id="wfm-ptable-presets-copy-pos-btn" class="wfm-btn wfm-btn-sm" title="Copy selected preset's positive prompt">PP Copy</button>
|
||||
<button id="wfm-ptable-presets-copy-neg-btn" class="wfm-btn wfm-btn-sm" title="Copy selected preset's negative prompt">NP Copy</button>
|
||||
<select id="wfm-ptable-presets-group-select" class="wfm-select" style="font-size:12px;padding:3px 6px;width:160px;flex:0 0 auto;">
|
||||
<option value="">Select group...</option>
|
||||
</select>
|
||||
<button id="wfm-ptable-presets-add-to-group-btn" class="wfm-btn wfm-btn-sm">Add to Group</button>
|
||||
<button id="wfm-ptable-presets-batch-clear-btn" class="wfm-btn wfm-btn-sm" title="Clear all batch presets">BC</button>
|
||||
<button id="wfm-ptable-presets-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
|
||||
<button id="wfm-ptable-presets-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
|
||||
<button id="wfm-ptable-presets-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New Preset</button>
|
||||
</div>
|
||||
<div class="wfm-meta-section" id="wfm-meta-vae-section">
|
||||
<div class="wfm-meta-section-title">VAE</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-vae-list"></div>
|
||||
</div>
|
||||
<div class="wfm-meta-section" id="wfm-meta-diff-section">
|
||||
<div class="wfm-meta-section-title">Diffusion Model</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-diff-list"></div>
|
||||
</div>
|
||||
<div class="wfm-meta-section" id="wfm-meta-te-section">
|
||||
<div class="wfm-meta-section-title">Text Encoder</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-te-list"></div>
|
||||
<div class="wfm-prompt-table-scroll">
|
||||
<table class="wfm-table wfm-prompt-edit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:56px;">#</th>
|
||||
<th style="width:28px;"></th>
|
||||
<th style="width:160px;">Name</th>
|
||||
<th style="width:110px;">Category</th>
|
||||
<th>Positive</th>
|
||||
<th>Negative</th>
|
||||
<th style="width:28px;">B</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wfm-ptable-presets-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Col3: LoRA + Prompt -->
|
||||
<div class="wfm-metadata-col3">
|
||||
<div class="wfm-meta-section" id="wfm-meta-lora-section">
|
||||
<div class="wfm-meta-section-title">LoRA</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-lora-list"></div>
|
||||
</div>
|
||||
<div class="wfm-meta-section wfm-meta-prompt-section" id="wfm-meta-prompt-section">
|
||||
<div class="wfm-meta-section-title">Prompt</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-prompt-list"></div>
|
||||
<div class="wfm-meta-prompt-full-wrap">
|
||||
<div class="wfm-meta-prompt-full-label" id="wfm-meta-prompt-full-label"></div>
|
||||
<textarea class="wfm-meta-prompt-full" id="wfm-meta-prompt-full" readonly placeholder="Select a prompt above to view full text..."></textarea>
|
||||
<div class="wfm-meta-prompt-actions">
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-copy-btn">Copy</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-pos-btn">GenUI:P</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-neg-btn">GenUI:N</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-pos-btn">Prompt:P</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-neg-btn">Prompt:N</button>
|
||||
</div>
|
||||
<!-- Presets Group テーブル -->
|
||||
<div id="wfm-ptable-presetsGroup" class="wfm-prompt-table-pane">
|
||||
<div class="wfm-prompt-table-toolbar">
|
||||
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
|
||||
<input type="text" id="wfm-ptable-presetsgroup-search" class="wfm-search-input" placeholder="Search groups...">
|
||||
<button class="wfm-search-clear-btn" id="wfm-ptable-presetsgroup-search-clear-btn" title="Clear search">✕</button>
|
||||
</div>
|
||||
<span id="wfm-ptable-presetsgroup-count" class="wfm-nodes-count"></span>
|
||||
<span style="flex:1;"></span>
|
||||
<button id="wfm-ptable-presetsgroup-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
|
||||
<button id="wfm-ptable-presetsgroup-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
|
||||
<button id="wfm-ptable-presetsgroup-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New Group</button>
|
||||
</div>
|
||||
<div class="wfm-prompt-table-scroll">
|
||||
<table class="wfm-table wfm-prompt-edit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:56px;">#</th>
|
||||
<th style="width:160px;">Group Name</th>
|
||||
<th>Presets</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wfm-ptable-presetsgroup-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wildcards テーブル -->
|
||||
<div id="wfm-ptable-wildcards" class="wfm-prompt-table-pane">
|
||||
<div class="wfm-prompt-table-toolbar">
|
||||
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
|
||||
<input type="text" id="wfm-ptable-wildcards-search" class="wfm-search-input" placeholder="Search wildcard files...">
|
||||
<button class="wfm-search-clear-btn" id="wfm-ptable-wildcards-search-clear-btn" title="Clear search">✕</button>
|
||||
</div>
|
||||
<span id="wfm-ptable-wildcards-count" class="wfm-nodes-count"></span>
|
||||
<span style="flex:1;"></span>
|
||||
<button id="wfm-ptable-wildcards-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
|
||||
<button id="wfm-ptable-wildcards-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
|
||||
<button id="wfm-ptable-wildcards-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New File</button>
|
||||
</div>
|
||||
<div class="wfm-prompt-table-scroll">
|
||||
<table class="wfm-table wfm-prompt-edit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:56px;">#</th>
|
||||
<th style="width:220px;">File (folder/name)</th>
|
||||
<th style="width:70px;">Ext</th>
|
||||
<th>Content (one item per line, or YAML)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wfm-ptable-wildcards-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Style テーブル -->
|
||||
<div id="wfm-ptable-style" class="wfm-prompt-table-pane">
|
||||
<div class="wfm-prompt-table-toolbar">
|
||||
<div class="wfm-search-wrap" style="flex:1;max-width:280px;">
|
||||
<input type="text" id="wfm-ptable-style-search" class="wfm-search-input" placeholder="Search styles...">
|
||||
<button class="wfm-search-clear-btn" id="wfm-ptable-style-search-clear-btn" title="Clear search">✕</button>
|
||||
</div>
|
||||
<span id="wfm-ptable-style-count" class="wfm-nodes-count"></span>
|
||||
<span style="flex:1;"></span>
|
||||
<button id="wfm-ptable-style-deselect-btn" class="wfm-btn wfm-btn-sm" disabled title="Clear row selection">Deselect</button>
|
||||
<button id="wfm-ptable-style-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" disabled>Delete</button>
|
||||
<button id="wfm-ptable-style-addfile-btn" class="wfm-btn wfm-btn-sm" disabled title="Select a style whose file is not custom.json to add a new style to that same file">+ Add to this file</button>
|
||||
<button id="wfm-ptable-style-add-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New Style</button>
|
||||
</div>
|
||||
<div class="wfm-prompt-table-scroll">
|
||||
<table class="wfm-table wfm-prompt-edit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:56px;">#</th>
|
||||
<th style="width:160px;">Name</th>
|
||||
<th style="width:110px;">File</th>
|
||||
<th>Positive</th>
|
||||
<th>Negative</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wfm-ptable-style-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Table パネル -->
|
||||
</section>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
@@ -1254,6 +1340,7 @@
|
||||
<button class="wfm-help-nav-item" data-help-page="aitool">AI TOOL Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="tagger">Tagger Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="imageedit">Image Edit Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="video">Video Tab</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="shortcuts">Keyboard Shortcuts</button>
|
||||
<button class="wfm-help-nav-item" data-help-page="trouble">Troubleshooting</button>
|
||||
</div>
|
||||
@@ -1331,6 +1418,7 @@
|
||||
<li id="wfm-help-gen-28">Model tab — Highlight color: the label of each model type (Checkpoint, VAE, Diffusion Model, Text Encoder, ControlNet, Hypernetwork) and the LoRA column header turn a distinct color (default bright green) when the loaded workflow actually contains that type of node; this stays visible even while ControlNet/Hypernetwork are collapsed, so you can tell at a glance whether they're in use without expanding them; the color is customizable in the Settings tab</li>
|
||||
<li id="wfm-help-gen-6">Settings tab: KSampler and Latent Image side by side, each at 50% width</li>
|
||||
<li id="wfm-help-gen-7">Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow; if the loaded workflow has Bypass or Mute nodes, a note listing them appears above the search bar (Bypass nodes are excluded with their wires reconnected around them; Mute nodes are excluded with no reconnection, matching ComfyUI itself)</li>
|
||||
<li id="wfm-help-gen-30">The <b>Save</b> button (right end of the subtab bar) is hidden while the <b>Batch</b> or <b>Lab</b> subtab is active, since those work on workflow copies / their own Plan files rather than the currently loaded workflow; it reappears on Input/Model/Settings/Feeder</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
@@ -1368,6 +1456,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 +1521,12 @@
|
||||
<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>
|
||||
<li id="wfm-help-lab-18">A single click on a keyframe cell selects it (blue outline) instead of opening it — double-click to open the edit modal as before. Each column's header (Model / Prompt / Ksampler) also has <b>▲ / ▼</b> buttons that swap the selected keyframe with its immediate neighbor to reorder them without retyping anything; keyframe #1 (the baseline) can never move, and nothing can be swapped into its slot</li>
|
||||
<li id="wfm-help-lab-19">Prompt column header's <b>PG</b> button: pick one of the Prompt tab's preset groups (Table view's Presets Group, the same groups used by the Batch tab's checklist) and replace the entire Prompt column with one keyframe per preset in that group's order, starting at #1 (#1 is overwritten, not appended after). If the group has more presets than the current Batch count, Batch count is raised automatically to fit (capped at 50, the Batch input's own limit); anything still beyond that shows the usual "keyframes beyond Batch count" warning instead of being silently dropped</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1469,6 +1562,12 @@
|
||||
<h2 id="wfm-help-prompt-title">Prompt Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="prompt">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-toplevel-title">Form / Table</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-15">Two top-level tabs: <b>Form</b> is the original one-item-at-a-time editor described below (AI Assistant / Presets / Wildcard / Style panes); <b>Table</b> shows the same underlying data all at once as editable spreadsheets — useful once there are more than a handful of presets, wildcard files, or styles to manage</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-ai-title">AI Assistant & Translation</h3>
|
||||
<ul>
|
||||
@@ -1503,18 +1602,35 @@
|
||||
<li id="wfm-help-prompt-14">Each entry in the list shows its source file as a small badge next to the name (e.g. <code>sdxl_styles_diva.json</code>); while editing an existing style, that filename also appears above the Positive prompt field, and a <b>+ Add to this file</b> button appears next to Save — click it to clear the form for a brand-new style that gets appended to that same file instead of the default <code>custom.json</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-prompt-card-table-title">Table View</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-prompt-16">The Table tab has four sub-tabs of its own: <b>Presets</b>, <b>Presets Group</b>, <b>Wildcards</b>, and <b>Style</b> — each an editable table built from the exact same data as the Form tab, so changes on either side stay in sync</li>
|
||||
<li id="wfm-help-prompt-17">Every table shares a left-most auto-numbered <b>#</b> column: click a number to select that row (highlighted red) — multiple rows can be selected at once; the toolbar's <b>Delete</b> button removes every selected row in one confirm, and <b>Deselect</b> clears the selection. This replaced per-row delete buttons, which were too easy to misclick</li>
|
||||
<li id="wfm-help-prompt-18"><b>Double-click</b> a row's number to open a larger prompt-editing modal instead of the small in-table textarea. Presets and Style show <b>Positive</b>/<b>Negative</b> toggle buttons above one shared textarea (switching preserves whatever you typed on the other side); Wildcards shows just the file's content. <b>Save</b> writes the change back and closes the modal; <b>Close</b> discards it</li>
|
||||
<li id="wfm-help-prompt-19">The toolbar's <b>+ New ...</b> button adds a blank row at the top with small ✓ (Add) / ✕ (Cancel) icons in place of its number; fill in the fields and click ✓ to create it, or ✕ to discard the row without saving</li>
|
||||
<li id="wfm-help-prompt-20">Presets table — the <b>★</b> (favorite) and <b>B</b> (Batch group) columns work like toggle switches: click to turn them gold/yellow, click again to clear; the toolbar's <b>BC</b> button clears every preset out of the Batch group at once, same as the Form tab's Preset Manager</li>
|
||||
<li id="wfm-help-prompt-21">Presets table toolbar — <b>PP Copy</b> / <b>NP Copy</b> copy the single selected row's positive/negative prompt to the clipboard; the <b>group select + Add to Group</b> adds every currently selected preset to the chosen group in one click. Doing the group assignment from here (search/select presets, then pick from a short group list) replaced the old per-group preset picker, which became unusable once there were many presets</li>
|
||||
<li id="wfm-help-prompt-22">Presets Group table — create, rename, and delete groups; each group's members are shown as removable badges (✕). The reserved <b>Batch</b> group is not listed here — it's managed entirely through the Presets table's B column and BC button instead</li>
|
||||
<li id="wfm-help-prompt-23">Wildcards table — same files as the Form tab's Wildcard file manager; editing the <b>File</b> path or the <b>Ext</b> dropdown renames the file (saves the content under the new name, then removes the old file)</li>
|
||||
<li id="wfm-help-prompt-24">Style table — same styles as the Form tab's Style manager, editable inline the same way as Presets</li>
|
||||
<li id="wfm-help-prompt-25">Presets table toolbar — the ★ and B sort buttons (next to the search box) bring favorited / batch-selected presets to the top of the list; both can be enabled together (★ takes priority, B breaks ties), and everything else keeps its original relative order</li>
|
||||
<li id="wfm-help-prompt-26">Style table toolbar — select exactly one row whose <b>File</b> badge isn't <code>custom.json</code> to enable <b>+ Add to this file</b>: it adds a blank row targeting that same file (shown as a badge in the File column even before saving) — the same capability as the Form tab's Style manager (see above); custom.json entries and multi-row selections leave the button disabled, use the regular <b>+ New Style</b> button for those instead</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-metadata">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-metadata-title">Metadata Tab</h2>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="metadata">Open tab →</button>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm wfm-help-jump-btn" data-jump-tab="gallery" data-jump-subtab="metadata">Open tab →</button>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-card-overview-title">Overview</h3>
|
||||
<p id="wfm-help-metadata-desc">Drop a ComfyUI-generated PNG/WebP or workflow JSON to instantly extract and view the embedded model and prompt information.</p>
|
||||
<ul>
|
||||
<li id="wfm-help-metadata-1">Left column: drop zone (image preview for PNG/WebP) and file info; click or drop to load</li>
|
||||
<li id="wfm-help-metadata-6">This is now a sub-tab inside the <b>Gallery</b> tab (to the right of Style_Catalog), rather than its own top-level tab — everything else about it works exactly the same</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
@@ -1528,7 +1644,7 @@
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-metadata-card-formats-title">Supported Formats</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-metadata-5">Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow subgraph workflows), SD WebUI, SD Forge, Fooocus. Model extraction covers UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, and QuadrupleCLIPLoader (e.g. HiDream 4-CLIP).</li>
|
||||
<li id="wfm-help-metadata-5">Supported: ComfyUI PNG/WebP/MP4/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Mage-Flow subgraph workflows; MP4 reads the workflow/prompt container tags written by SaveVideo, including all-in-one video nodes like MiniMax H3 ImageToVideo), SD WebUI, SD Forge, Fooocus. Model extraction covers UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, and QuadrupleCLIPLoader (e.g. HiDream 4-CLIP).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1573,7 +1689,9 @@
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-settings-card-misc-title">Data & Misc</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-settings-8">Data Management: export all plugin data to JSON / import to restore</li>
|
||||
<li id="wfm-help-settings-8">Data Management: export core settings/metadata/prompts to JSON / import to restore</li>
|
||||
<li id="wfm-help-settings-15">Full Backup (ZIP): unlike the JSON export above, also includes folder-based data (AI Skills, Lab Plans, Style presets) and the Tagger database; Restore overwrites existing files — restart ComfyUI afterward</li>
|
||||
<li id="wfm-help-settings-16">Two opt-in checkboxes above the Full Backup buttons additionally include ComfyUI's default workflows folder and the Impact Pack's wildcard folder in the export — both export-only, never restored automatically; copy the ZIP's _external/ folder back by hand if you want them</li>
|
||||
<li id="wfm-help-settings-13">CivitAI Host: choose between civitai.com (SFW only) and civitai.red (unrestricted) — determines which site opens when clicking a model link in the Models tab CivitAI panel</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1620,6 +1738,7 @@
|
||||
<li id="wfm-help-gallery-2">3-column layout: folder tree (left) | image grid (center) | detail panel (right)</li>
|
||||
<li id="wfm-help-gallery-3">Thumbnail and Table view modes; double-click any image to open fullscreen lightbox</li>
|
||||
<li id="wfm-help-gallery-4">Toolbar filters: favorites (★), text search (filename / tag / prompt), tag filter, group filter, sort by date/name</li>
|
||||
<li id="wfm-help-gallery-26">Four sub-tabs at the top of the Gallery tab: <b>Output</b> (this browser), <b>ImagePrompt</b>, <b>Style_Catalog</b>, and <b>Metadata</b> — the last one embeds the full Metadata Tab (see its own help page) so you can drop an image there to inspect its embedded model/prompt data without leaving Gallery</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
@@ -1651,7 +1770,7 @@
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-detail-title">Detail Panel</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-7">Detail panel — Info tab: filename, size, date, tags (add/remove), memo; <b>download icon</b> (on image hover) to download the single image; "Move To..." moves the image to another folder, "Delete File" removes it permanently</li>
|
||||
<li id="wfm-help-gallery-7">Detail panel — Info tab: filename, size, dimensions (image width×height) or video duration, date, tags (add/remove), memo; <b>download icon</b> (on image hover) to download the single image; "Move To..." moves the image to another folder, "Delete File" removes it permanently</li>
|
||||
<li id="wfm-help-gallery-8">Detail panel — JSON tab: displays workflow JSON from PNG embedded data (prompt / workflow keys) or from metadata saved by the Generate UI tab; 'Copy & Send Canvas' button copies to clipboard and stores the workflow for title-drag to canvas</li>
|
||||
<li id="wfm-help-gallery-9">Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image; the <b>__Feeder__</b> group is reserved (shown with 🔒) and cannot be renamed or deleted — rename and delete buttons are disabled when it is selected</li>
|
||||
</ul>
|
||||
@@ -1664,13 +1783,14 @@
|
||||
<li id="wfm-help-gallery-14"><b>Tagger</b> button (purple, in the detail panel action row): opens the selected image directly in the Tagger tab's Single mode for instant tagging</li>
|
||||
<li id="wfm-help-gallery-16"><b>Image Edit</b> button (toolbar, next to Refresh): sends the currently selected image to the Image Edit tab; if layers already exist the image is added as a new layer scaled to fit the canvas, otherwise it becomes Layer 1</li>
|
||||
<li id="wfm-help-gallery-17"><b>Send GenUI Image</b> button (toolbar, next to Image Edit): uploads the selected image to ComfyUI and sets it as the input for the first LoadImage node in the GenerateUI tab; automatically switches to GenerateUI → Input → Image; requires a workflow with at least one LoadImage node to be loaded first</li>
|
||||
<li id="wfm-help-gallery-27"><b>Send to LI node</b> button (toolbar, next to Send GenUI Image): uploads the selected image and writes its filename into the image widget of the currently selected node on the ComfyUI canvas (falling back to the first LoadImage-type node found) — same destination as Image Edit tab's Send to LI node; requires Workflow Studio to have been opened from ComfyUI's top menu button</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-gallery-card-formats-title">Formats & Auto-save</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-gallery-10">Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the JSON tab</li>
|
||||
<li id="wfm-help-gallery-11">Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata</li>
|
||||
<li id="wfm-help-gallery-11">Supports PNG (tEXt/iTXt), JPEG, WebP, GIF, MP4; workflow extraction reads ComfyUI prompt/workflow keys embedded in PNG metadata or in MP4 container tags (as written by SaveVideo, e.g. MiniMax H3 output)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1747,7 +1867,7 @@
|
||||
<div class="wfm-help-card">
|
||||
<h3 id="wfm-help-sidepanel-card-info-title">Information (I) tab</h3>
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-13">I tab (Information): drop a ComfyUI PNG/WebP/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF and QuadrupleCLIPLoader node types</li>
|
||||
<li id="wfm-help-sidepanel-13">I tab (Information): drop a ComfyUI PNG/WebP/MP4/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF and QuadrupleCLIPLoader node types; MP4 reads the workflow/prompt tags embedded by SaveVideo, including all-in-one video nodes like MiniMax H3 ImageToVideo</li>
|
||||
<li id="wfm-help-sidepanel-14">Information tab — drag items to canvas: model sub-tab places the loader node (Checkpoint → CheckpointLoaderSimple, VAE → VAELoader, Diffusion Model → UNETLoader, Text Encoder → CLIPLoader); double-click also places at canvas center</li>
|
||||
<li id="wfm-help-sidepanel-15">Information tab — LoRA sub-tab: drag individual LoRA to place LoraLoader; "Multiple LORA" section at bottom places all LoRAs in a single Lora Loader (LoraManager) node; Prompts sub-tab: drag a prompt to place CLIPTextEncode node with text pre-filled; click prompt to view full text and copy</li>
|
||||
</ul>
|
||||
@@ -1782,7 +1902,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>
|
||||
@@ -1950,8 +2070,7 @@
|
||||
<ul>
|
||||
<li id="wfm-help-imageedit-17">Save PNG: composites all visible layers and downloads the result as a PNG file</li>
|
||||
<li id="wfm-help-imageedit-18">Save to Gallery: prompts for a filename (default: wfs-image-YYYYMMDDHHmmss) and saves the composite PNG directly to the Gallery root folder (configured in Settings > Gallery Output Folder); requires the Gallery tab to have been opened at least once to set the root</li>
|
||||
<li id="wfm-help-imageedit-19">Save to Input: uploads the composite PNG to ComfyUI's input folder via /upload/image, making it selectable in Load Image nodes</li>
|
||||
<li id="wfm-help-imageedit-19b">Send to Workflow: uploads the composite PNG (same as Save to Input) and additionally writes the resulting filename into the image widget of the currently selected node in the ComfyUI graph, falling back to the first LoadImage-type node found; requires Workflow Studio to have been opened from ComfyUI's top menu button so the ComfyUI tab is reachable</li>
|
||||
<li id="wfm-help-imageedit-19b">Send to Workflow: uploads the composite PNG to ComfyUI's input folder via /upload/image and writes the resulting filename into the image widget of the currently selected node in the ComfyUI graph, falling back to the first LoadImage-type node found; requires Workflow Studio to have been opened from ComfyUI's top menu button so the ComfyUI tab is reachable</li>
|
||||
<li id="wfm-help-imageedit-20">Canvas navigation: scroll wheel to zoom, Space + drag to pan</li>
|
||||
<li id="wfm-help-imageedit-21">Undo / Redo: Ctrl+Z / Ctrl+Y (or toolbar buttons ↩ ↪)</li>
|
||||
</ul>
|
||||
@@ -1972,6 +2091,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-video">
|
||||
<h2 id="wfm-help-video-title">Video Tab</h2>
|
||||
<p id="wfm-help-video-desc">A dedicated video generation tab for the MiniMax H3 (Image to Video) workflow, independent of the GenerateUI tab.</p>
|
||||
<div class="wfm-help-card">
|
||||
<ul>
|
||||
<li id="wfm-help-video-1">Set first_frame (optional) / last_frame (optional) images, prompt, aspect_ratio / megapixels / multiple (resolution), duration (seconds), and noise_seed, then click Generate. Leaving first_frame empty runs the workflow text-only (bypassed), and workflows with no LoadImage node at all are also supported.</li>
|
||||
<li id="wfm-help-video-2">Model selection (UNet / CLIP / VAE / Audio VAE) is fixed to the loaded workflow's defaults and cannot be changed from this tab.</li>
|
||||
<li id="wfm-help-video-3">Use the "Load in Video" button in the Workflow tab's detail modal to load a workflow containing a MiniMax H3 Image-to-Video node. Unsupported workflows show an error instead.</li>
|
||||
<li id="wfm-help-video-4">Generated videos (mp4) can also be browsed and played back in the Gallery tab.</li>
|
||||
<li id="wfm-help-video-5">The Video Source panel next to the preview lets you drop or pick any local video (not just a generated one) to use the Frame/GIF tools below without running MiniMax H3 first; Clear resets it back to empty.</li>
|
||||
<li id="wfm-help-video-6">Frame tab (right panel): play/seek the preview video to the position you want, click Capture Frame to grab that frame client-side, then Save to Output to write it as a PNG into ComfyUI's output folder.</li>
|
||||
<li id="wfm-help-video-7">GIF tab (right panel): set Start/End (seconds), FPS, and an optional Max Width, then Convert to GIF — the server decodes the video and encodes an animated GIF into ComfyUI's output folder.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="wfm-help-jump-btn wfm-btn wfm-btn-sm" data-jump-tab="video">Go to Video Tab</button>
|
||||
</div>
|
||||
|
||||
<div class="wfm-help-page" id="wfm-help-page-shortcuts">
|
||||
<div class="wfm-help-article-header">
|
||||
<h2 id="wfm-help-shortcuts-title">Keyboard Shortcuts</h2>
|
||||
@@ -2078,6 +2214,7 @@
|
||||
<button id="wfm-gallery-subtab-output" class="wfm-gallery-subtab-btn active" data-gallery-subtab="output">Output</button>
|
||||
<button id="wfm-gallery-subtab-imageprompt" class="wfm-gallery-subtab-btn" data-gallery-subtab="imageprompt">ImagePrompt</button>
|
||||
<button id="wfm-gallery-subtab-styleCatalog" class="wfm-gallery-subtab-btn" data-gallery-subtab="styleCatalog">Style_Catalog</button>
|
||||
<button id="wfm-gallery-subtab-metadata" class="wfm-gallery-subtab-btn" data-gallery-subtab="metadata">Metadata</button>
|
||||
</div>
|
||||
|
||||
<!-- Output ギャラリーパネル -->
|
||||
@@ -2103,6 +2240,7 @@
|
||||
<option value="name_asc">Name A-Z</option>
|
||||
<option value="name_desc">Name Z-A</option>
|
||||
</select>
|
||||
<button id="wfm-gallery-clear-filters-btn" class="wfm-btn wfm-btn-sm" title="Clear all filters"></button>
|
||||
<div class="wfm-view-btns">
|
||||
<button class="wfm-view-btn active" data-gallery-view="thumb" title="Thumbnail">⊞</button>
|
||||
<button class="wfm-view-btn" data-gallery-view="table" title="Table">≡</button>
|
||||
@@ -2111,8 +2249,8 @@
|
||||
<button id="wfm-gallery-refresh-btn" class="wfm-btn wfm-btn-sm">Refresh</button>
|
||||
<button id="wfm-gallery-send-image-edit-btn" class="wfm-btn wfm-btn-sm" title="Send selected image to Image Edit tab">Image Edit</button>
|
||||
<button id="wfm-gallery-send-genui-image-btn" class="wfm-btn wfm-btn-sm" title="Send selected image to GenerateUI Image input">Send GenUI Image</button>
|
||||
<button id="wfm-gallery-send-li-node-btn" class="wfm-btn wfm-btn-sm" title="Upload and write selected image's filename into the selected node's image widget (e.g. LoadImage)">Send to LI node</button>
|
||||
<button id="wfm-gallery-send-cc-btn" class="wfm-btn wfm-btn-sm" title="Send selected image to ComfyUI Comic Creator's selected panel" style="display:none;">Send CC</button>
|
||||
<button id="wfm-gallery-clear-filters-btn" class="wfm-btn wfm-btn-sm" title="Clear all filters"></button>
|
||||
</div>
|
||||
<!-- 複数選択時の一括操作バー -->
|
||||
<div id="wfm-gallery-bulk-bar" class="wfm-gallery-bulk-bar" style="display:none;">
|
||||
@@ -2167,20 +2305,23 @@
|
||||
</div>
|
||||
<!-- タブナビ -->
|
||||
<div class="wfm-side-tab-nav">
|
||||
<button class="wfm-gallery-detail-tab-btn active" data-detail-tab="info">Info</button>
|
||||
<button class="wfm-gallery-detail-tab-btn" data-detail-tab="info">Info</button>
|
||||
<button class="wfm-gallery-detail-tab-btn" data-detail-tab="meta">JSON</button>
|
||||
<button class="wfm-gallery-detail-tab-btn" data-detail-tab="group">Groups</button>
|
||||
<button class="wfm-gallery-detail-tab-btn active" data-detail-tab="prompt">Prompt</button>
|
||||
</div>
|
||||
<div class="wfm-gallery-detail-action-row">
|
||||
<button id="wfm-gallery-open-metadata-btn" class="wfm-gallery-action-btn wfm-gallery-action-btn-green" title="Open in Metadata tab (or Alt+click image)">Metadata</button>
|
||||
<button id="wfm-gallery-load-genui-btn" class="wfm-gallery-action-btn wfm-gallery-action-btn-primary" title="Load workflow into GenerateUI">Load GenUI</button>
|
||||
<button id="wfm-gallery-load-genui-btn" class="wfm-gallery-action-btn wfm-gallery-action-btn-primary" title="Load workflow into GenerateUI">GenUI</button>
|
||||
<button id="wfm-gallery-open-tagger-btn" class="wfm-gallery-action-btn wfm-gallery-action-btn-tagger" title="Open in Tagger"></button>
|
||||
<button id="wfm-gallery-download-btn" class="wfm-gallery-action-btn wfm-gallery-action-btn-orange" title="Download">Download</button>
|
||||
</div>
|
||||
<!-- Info タブ -->
|
||||
<div id="wfm-gallery-detail-info" class="wfm-gallery-detail-tab-content active">
|
||||
<div id="wfm-gallery-detail-info" class="wfm-gallery-detail-tab-content" style="display:none;">
|
||||
<div class="wfm-gallery-info-section">
|
||||
<div class="wfm-gallery-info-row" id="wfm-gallery-info-name"></div>
|
||||
<div class="wfm-gallery-info-row" id="wfm-gallery-info-size"></div>
|
||||
<div class="wfm-gallery-info-row" id="wfm-gallery-info-dim"></div>
|
||||
<div class="wfm-gallery-info-row" id="wfm-gallery-info-date"></div>
|
||||
</div>
|
||||
<!-- ImagePromptサイドカー ( .txt / ponyxlWildcardsVault 由来のプロンプトがある画像のみ表示) -->
|
||||
@@ -2226,6 +2367,21 @@
|
||||
<div id="wfm-gallery-detail-group" class="wfm-gallery-detail-tab-content" style="display:none;overflow-y:auto;">
|
||||
<!-- JS動的生成: renderDetailGroup() -->
|
||||
</div>
|
||||
|
||||
<!-- Prompt タブ(埋め込みワークフローから抽出したpositive/negative。Metadataタブより
|
||||
手数少なく確認できるよう、頻度の高いプロンプト確認用に既定選択タブとしている。
|
||||
複数プロンプト候補がある場合に備え、Metadataタブと同じPOS/NEGリスト
|
||||
→ クリックで全文表示のUIをそのまま再利用している(見た目・挙動を統一)。 -->
|
||||
<div id="wfm-gallery-detail-prompt" class="wfm-gallery-detail-tab-content">
|
||||
<div class="wfm-meta-section wfm-meta-prompt-section" id="wfm-gallery-prompt-section">
|
||||
<div class="wfm-meta-section-title">Prompt</div>
|
||||
<div class="wfm-meta-list" id="wfm-gallery-prompt-list"></div>
|
||||
<div class="wfm-meta-prompt-full-wrap">
|
||||
<div class="wfm-meta-prompt-full-label" id="wfm-gallery-prompt-full-label"></div>
|
||||
<textarea class="wfm-meta-prompt-full" id="wfm-gallery-prompt-full" readonly placeholder="Select a prompt above to view full text..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2371,6 +2527,77 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Style_Catalog ギャラリーパネル -->
|
||||
|
||||
<!-- Metadata ギャラリーパネル -->
|
||||
<div id="wfm-gallery-panel-metadata" class="wfm-gallery-panel">
|
||||
<div class="wfm-metadata-layout">
|
||||
<!-- Col1: Drop zone + preview -->
|
||||
<div class="wfm-metadata-col1">
|
||||
<div class="wfm-metadata-drop-zone" id="wfm-meta-drop">
|
||||
<div class="wfm-meta-drop-label" id="wfm-meta-drop-label">
|
||||
<div class="wfm-meta-drop-main">Drop image / video / JSON</div>
|
||||
<div class="wfm-meta-drop-sub">PNG · WebP · MP4 · JSON</div>
|
||||
</div>
|
||||
<img id="wfm-meta-preview-img" class="wfm-meta-preview-img" style="display:none;" alt="preview">
|
||||
</div>
|
||||
<div class="wfm-meta-file-info" id="wfm-meta-file-info"></div>
|
||||
<div class="wfm-meta-format-note" id="wfm-meta-format-note">
|
||||
<div class="wfm-meta-format-note-title" id="wfm-meta-format-note-title">Supported formats</div>
|
||||
<ul class="wfm-meta-format-list">
|
||||
<li id="wfm-meta-fmt-comfyui">ComfyUI — PNG / WebP / MP4 / JSON</li>
|
||||
<li id="wfm-meta-fmt-sdwebui">SD WebUI / SD Forge — PNG (parameters)</li>
|
||||
<li id="wfm-meta-fmt-fooocus">Fooocus — PNG (fooocus_scheme)</li>
|
||||
</ul>
|
||||
<div class="wfm-meta-format-todo" id="wfm-meta-format-todo">Flux.2 / Qwen-Image / Z-Image subgraph workflows supported</div>
|
||||
</div>
|
||||
<input type="file" id="wfm-meta-file-input" accept="image/png,image/webp,video/mp4,.mp4,.json,application/json" style="display:none;">
|
||||
</div>
|
||||
|
||||
<!-- Col2: Model info -->
|
||||
<div class="wfm-metadata-col2">
|
||||
<div class="wfm-meta-section" id="wfm-meta-ckpt-section">
|
||||
<div class="wfm-meta-section-title">Checkpoint</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-ckpt-list"></div>
|
||||
</div>
|
||||
<div class="wfm-meta-section" id="wfm-meta-vae-section">
|
||||
<div class="wfm-meta-section-title">VAE</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-vae-list"></div>
|
||||
</div>
|
||||
<div class="wfm-meta-section" id="wfm-meta-diff-section">
|
||||
<div class="wfm-meta-section-title">Diffusion Model</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-diff-list"></div>
|
||||
</div>
|
||||
<div class="wfm-meta-section" id="wfm-meta-te-section">
|
||||
<div class="wfm-meta-section-title">Text Encoder</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-te-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Col3: LoRA + Prompt -->
|
||||
<div class="wfm-metadata-col3">
|
||||
<div class="wfm-meta-section" id="wfm-meta-lora-section">
|
||||
<div class="wfm-meta-section-title">LoRA</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-lora-list"></div>
|
||||
</div>
|
||||
<div class="wfm-meta-section wfm-meta-prompt-section" id="wfm-meta-prompt-section">
|
||||
<div class="wfm-meta-section-title">Prompt</div>
|
||||
<div class="wfm-meta-list" id="wfm-meta-prompt-list"></div>
|
||||
<div class="wfm-meta-prompt-full-wrap">
|
||||
<div class="wfm-meta-prompt-full-label" id="wfm-meta-prompt-full-label"></div>
|
||||
<textarea class="wfm-meta-prompt-full" id="wfm-meta-prompt-full" readonly placeholder="Select a prompt above to view full text..."></textarea>
|
||||
<div class="wfm-meta-prompt-actions">
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-copy-btn">Copy</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-pos-btn">GenUI:P</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-neg-btn">GenUI:N</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-pos-btn">Prompt:P</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-neg-btn">Prompt:N</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Metadata ギャラリーパネル -->
|
||||
</section>
|
||||
|
||||
<!-- Image Edit Tab -->
|
||||
@@ -2403,8 +2630,7 @@
|
||||
<button class="wfm-btn wfm-btn-sm" id="ie-redo-btn" title="Redo (Ctrl+Y)">↪</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="ie-save-btn">Save PNG</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="ie-save-gallery-btn" title="Save to Gallery root folder">Save to Gallery</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="ie-upload-comfy-btn">Save to Input</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="ie-send-workflow-btn" title="Upload and write this image's filename into the selected node's image widget (e.g. LoadImage)">Send to Workflow</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="ie-send-workflow-btn" title="Upload and write this image's filename into the selected node's image widget (e.g. LoadImage)">Send to LI node</button>
|
||||
</div>
|
||||
|
||||
<!-- Tool options -->
|
||||
@@ -2461,6 +2687,168 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Video Tab (MiniMax H3専用・GenerateUIタブとは独立) -->
|
||||
<section id="wfm-tab-video" class="wfm-tab-content">
|
||||
<div class="wfm-video-layout">
|
||||
<div class="wfm-video-form-panel">
|
||||
<div class="wfm-video-header">
|
||||
<h3 id="wfm-video-title-label">Video (MiniMax H3)</h3>
|
||||
<span id="wfm-video-wf-name" class="wfm-video-wf-name">No workflow loaded</span>
|
||||
</div>
|
||||
|
||||
<div class="wfm-video-frame-slots">
|
||||
<div class="wfm-video-frame-slot">
|
||||
<label id="wfm-video-first-frame-label">First Frame (optional)</label>
|
||||
<div class="wfm-i2i-preview-wrap" id="wfm-video-first-frame-wrap" style="display:none;">
|
||||
<img class="wfm-i2i-preview-img" id="wfm-video-first-frame-preview">
|
||||
</div>
|
||||
<div class="wfm-i2i-drop-zone" id="wfm-video-first-frame-drop">
|
||||
<label class="wfm-i2i-drop-label">
|
||||
<span id="wfm-video-first-frame-drop-label">Drop image or click to select</span>
|
||||
<input type="file" accept="image/*" id="wfm-video-first-frame-file" style="display:none;">
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
<span class="wfm-i2i-status" id="wfm-video-first-frame-status" style="flex:1;"></span>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-first-frame-clear">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-video-frame-slot">
|
||||
<label id="wfm-video-last-frame-label">Last Frame (optional)</label>
|
||||
<div class="wfm-i2i-preview-wrap" id="wfm-video-last-frame-wrap" style="display:none;">
|
||||
<img class="wfm-i2i-preview-img" id="wfm-video-last-frame-preview">
|
||||
</div>
|
||||
<div class="wfm-i2i-drop-zone" id="wfm-video-last-frame-drop">
|
||||
<label class="wfm-i2i-drop-label">
|
||||
<span id="wfm-video-last-frame-drop-label">Drop image or click to select</span>
|
||||
<input type="file" accept="image/*" id="wfm-video-last-frame-file" style="display:none;">
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
<span class="wfm-i2i-status" id="wfm-video-last-frame-status" style="flex:1;"></span>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-last-frame-clear">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="wfm-video-prompt" id="wfm-video-prompt-label">Prompt</label>
|
||||
<textarea id="wfm-video-prompt" class="wfm-textarea" rows="6"></textarea>
|
||||
|
||||
<div class="wfm-video-resolution-row">
|
||||
<div>
|
||||
<label for="wfm-video-aspect-ratio" id="wfm-video-aspect-ratio-label">Aspect Ratio</label>
|
||||
<select id="wfm-video-aspect-ratio" class="wfm-input"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wfm-video-megapixels" id="wfm-video-megapixels-label">Megapixels</label>
|
||||
<input type="number" id="wfm-video-megapixels" class="wfm-input" step="0.01" min="0.01" value="0.2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wfm-video-multiple" id="wfm-video-multiple-label">Multiple</label>
|
||||
<input type="number" id="wfm-video-multiple" class="wfm-input" step="8" min="8" value="32">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-video-resolution-row">
|
||||
<div>
|
||||
<label for="wfm-video-duration" id="wfm-video-duration-label">Duration (seconds)</label>
|
||||
<input type="number" id="wfm-video-duration" class="wfm-input" step="0.5" min="0.5" value="2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wfm-video-noise-seed" id="wfm-video-noise-seed-label">Noise Seed</label>
|
||||
<input type="number" id="wfm-video-noise-seed" class="wfm-input" value="0">
|
||||
</div>
|
||||
<div class="wfm-video-seed-randomize-wrap">
|
||||
<label><input type="checkbox" id="wfm-video-seed-randomize" checked> <span id="wfm-video-randomize-label">Randomize</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-top:10px;">
|
||||
<button id="wfm-video-generate-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">Generate</button>
|
||||
</div>
|
||||
<div class="wfm-gen-progress-container">
|
||||
<div class="wfm-gen-progress-track">
|
||||
<div id="wfm-video-progress-bar" class="wfm-gen-progress-bar"></div>
|
||||
</div>
|
||||
<span id="wfm-video-progress-text" class="wfm-gen-progress-text">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 中央ペイン: プレビュー(生成結果 or ドロップされた任意の動画) + 編集(将来機能用プレースホルダー) -->
|
||||
<div class="wfm-video-center-panel">
|
||||
<div class="wfm-video-preview-row">
|
||||
<div class="wfm-video-preview-frame" id="wfm-video-preview-frame">
|
||||
<span class="wfm-placeholder" id="wfm-video-preview-placeholder">No video</span>
|
||||
<video id="wfm-video-preview-video" class="wfm-video-preview-el" controls style="display:none;"></video>
|
||||
</div>
|
||||
<div class="wfm-video-source-panel">
|
||||
<div class="wfm-video-pane-header" id="wfm-video-source-label">Video Source</div>
|
||||
<p class="wfm-video-prop-hint" id="wfm-video-source-hint">Load any video here to use Frame/GIF tools without generating one.</p>
|
||||
<div class="wfm-i2i-drop-zone" id="wfm-video-source-drop">
|
||||
<label class="wfm-i2i-drop-label">
|
||||
<span id="wfm-video-source-drop-label">Drop video or click to select</span>
|
||||
<input type="file" accept="video/*" id="wfm-video-source-file" style="display:none;">
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center;margin-top:6px;">
|
||||
<span class="wfm-i2i-status" id="wfm-video-source-status" style="flex:1;"></span>
|
||||
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-source-clear">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-video-edit-panel">
|
||||
<span id="wfm-video-edit-placeholder-label" class="wfm-placeholder">Editing tools coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右ペイン: プロパティ(Frame抽出 / GIF変換の2タブ) -->
|
||||
<div class="wfm-video-properties-panel">
|
||||
<div class="wfm-video-prop-tabnav">
|
||||
<button type="button" class="wfm-video-prop-tab active" data-prop-tab="frame" id="wfm-video-prop-tab-frame">Frame</button>
|
||||
<button type="button" class="wfm-video-prop-tab" data-prop-tab="gif" id="wfm-video-prop-tab-gif">GIF</button>
|
||||
</div>
|
||||
|
||||
<div id="wfm-video-prop-frame" class="wfm-video-prop-content active">
|
||||
<p class="wfm-video-prop-hint" id="wfm-video-frame-hint">Play/seek the preview video to the position you want, then capture.</p>
|
||||
<button type="button" id="wfm-video-frame-capture-btn" class="wfm-btn wfm-btn-sm" style="width:100%;">Capture Frame</button>
|
||||
<div class="wfm-i2i-preview-wrap" id="wfm-video-frame-preview-wrap" style="display:none;">
|
||||
<img class="wfm-i2i-preview-img" id="wfm-video-frame-preview-img">
|
||||
</div>
|
||||
<button type="button" id="wfm-video-frame-save-btn" class="wfm-btn wfm-btn-primary wfm-btn-sm" style="width:100%;margin-top:6px;" disabled>Save to Output</button>
|
||||
<span class="wfm-i2i-status" id="wfm-video-frame-status"></span>
|
||||
</div>
|
||||
|
||||
<div id="wfm-video-prop-gif" class="wfm-video-prop-content" style="display:none;">
|
||||
<div class="wfm-lab-field-row">
|
||||
<label id="wfm-video-gif-start-label">Start (s)</label>
|
||||
<input type="number" id="wfm-video-gif-start" class="wfm-input" value="0" min="0" step="0.1">
|
||||
</div>
|
||||
<div class="wfm-lab-field-row">
|
||||
<label id="wfm-video-gif-end-label">End (s)</label>
|
||||
<input type="number" id="wfm-video-gif-end" class="wfm-input" placeholder="end" min="0" step="0.1">
|
||||
</div>
|
||||
<div class="wfm-lab-field-row">
|
||||
<label id="wfm-video-gif-fps-label">FPS</label>
|
||||
<input type="number" id="wfm-video-gif-fps" class="wfm-input" value="10" min="1" max="30">
|
||||
</div>
|
||||
<div class="wfm-lab-field-row">
|
||||
<label id="wfm-video-gif-width-label">Max Width</label>
|
||||
<input type="number" id="wfm-video-gif-width" class="wfm-input" placeholder="original" min="16">
|
||||
</div>
|
||||
<button type="button" id="wfm-video-gif-convert-btn" class="wfm-btn wfm-btn-primary wfm-btn-sm" style="width:100%;margin-top:8px;">Convert to GIF</button>
|
||||
<div class="wfm-gen-progress-container" style="margin-top:8px;">
|
||||
<div class="wfm-gen-progress-track">
|
||||
<div id="wfm-video-gif-progress-bar" class="wfm-gen-progress-bar"></div>
|
||||
</div>
|
||||
<span id="wfm-video-gif-progress-text" class="wfm-gen-progress-text"></span>
|
||||
</div>
|
||||
<div class="wfm-i2i-preview-wrap" id="wfm-video-gif-preview-wrap" style="display:none;">
|
||||
<img class="wfm-i2i-preview-img" id="wfm-video-gif-preview-img">
|
||||
</div>
|
||||
<span class="wfm-i2i-status" id="wfm-video-gif-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tagger Tab -->
|
||||
<section id="wfm-tab-tagger" class="wfm-tab-content">
|
||||
<div class="wfm-tagger-subtab-nav">
|
||||
|
||||
@@ -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()
|
||||
@@ -673,9 +673,9 @@ window.wfmOpenMaskEditorForNode = async (imageDataUrl, maskDataUrl) => {
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Cross-window bridge for SPA "Send to Workflow"
|
||||
// Cross-window bridge for SPA "Send to LI node"
|
||||
// ============================================
|
||||
// WFS Image Edit タブから呼ばれる。アップロード済みの画像ファイル名を、現在ComfyUIキャンバス上で
|
||||
// WFS Image Edit / Galleryタブから呼ばれる。アップロード済みの画像ファイル名を、現在ComfyUIキャンバス上で
|
||||
// 選択中のノードの "image" ウィジェット(LoadImage / LoadImage 互換ノード)へ書き込む。
|
||||
// 選択ノードに該当ウィジェットが無ければグラフ内の最初の該当ノードにフォールバックする。
|
||||
// chat_TE カスタムノードの "Send to workflow" ボタンと同じロジック。
|
||||
@@ -2376,6 +2376,35 @@ const createDraggableItem = (label, type, data, subtitle) => {
|
||||
// Toggle & exports
|
||||
// ============================================
|
||||
|
||||
// Opens the Library panel on the Workflows (W) tab, filtered down to a single
|
||||
// workflow by filename. Used as the "Send to Canvas" fallback when window.opener
|
||||
// isn't available (or the workflow is API-format, which the title-drag mechanism
|
||||
// can't handle at all) — the user gets the actual workflow card, thumbnail
|
||||
// included, to drag onto the canvas themselves instead of a bare title element.
|
||||
export const showWorkflowInLibrary = async (filename) => {
|
||||
const panel = createPanel();
|
||||
state.visible = true;
|
||||
panel.style.display = "flex";
|
||||
|
||||
state.topTab = "workflows";
|
||||
panel.querySelectorAll(".wfm-nlp-top-tab").forEach(b => b.classList.toggle("active", b.dataset.toptab === "workflows"));
|
||||
state.wfSubTab = "wf-all";
|
||||
state.wfSubTab2 = null;
|
||||
|
||||
if (!state.wfLoaded) await loadWfData();
|
||||
|
||||
state.searchText = (filename || "").toLowerCase().trim();
|
||||
const searchInput = panel.querySelector(".wfm-nlp-search-input");
|
||||
if (searchInput) {
|
||||
searchInput.value = filename || "";
|
||||
searchInput.placeholder = "Search workflows...";
|
||||
}
|
||||
|
||||
rebuildSubTabs();
|
||||
renderContent();
|
||||
installCanvasDropHandler();
|
||||
};
|
||||
|
||||
export const togglePanel = async () => {
|
||||
const panel = createPanel();
|
||||
state.visible = !state.visible;
|
||||
@@ -2524,6 +2553,81 @@ async function _readAllPNGTextChunks(file) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// ── MP4 container metadata (moov/udta/meta の mdta keys/ilst) ──
|
||||
// ComfyUIのSaveVideoはPNGのtEXtチャンクと同じキー名(workflow=UI形式, prompt=API形式)で
|
||||
// JSON文字列をisobmff(mp4)のudta/meta(mdta)にコンテナレベルタグとして書き込む(FFmpegの
|
||||
// movflags=+use_metadata_tags)。static/js/metadata-tab.js の readMP4MetaTags() と同一ロジック。
|
||||
function _findMP4Box(view, bytes, start, end, targetType) {
|
||||
let offset = start;
|
||||
while (offset + 8 <= end) {
|
||||
const size = view.getUint32(offset);
|
||||
const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
|
||||
let boxSize = size;
|
||||
let bodyStart = offset + 8;
|
||||
if (size === 1) {
|
||||
if (offset + 16 > end) break;
|
||||
const hi = view.getUint32(offset + 8);
|
||||
const lo = view.getUint32(offset + 12);
|
||||
boxSize = hi * 4294967296 + lo;
|
||||
bodyStart = offset + 16;
|
||||
} else if (size === 0) {
|
||||
boxSize = end - offset;
|
||||
}
|
||||
if (boxSize < 8) break;
|
||||
if (type === targetType) return { bodyStart, end: offset + boxSize };
|
||||
offset += boxSize;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function _readMP4MetaTags(file) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const view = new DataView(buffer);
|
||||
if (bytes.length < 12) return null;
|
||||
const ftyp = String.fromCharCode(bytes[4], bytes[5], bytes[6], bytes[7]);
|
||||
if (ftyp !== "ftyp") return null;
|
||||
|
||||
const moov = _findMP4Box(view, bytes, 0, bytes.length, "moov");
|
||||
if (!moov) return null;
|
||||
const udta = _findMP4Box(view, bytes, moov.bodyStart, moov.end, "udta");
|
||||
let meta = udta ? _findMP4Box(view, bytes, udta.bodyStart, udta.end, "meta") : null;
|
||||
if (!meta) meta = _findMP4Box(view, bytes, moov.bodyStart, moov.end, "meta");
|
||||
if (!meta) return null;
|
||||
const metaBody = meta.bodyStart + 4; // meta は FullBox (version+flags 4バイト)
|
||||
const keysBox = _findMP4Box(view, bytes, metaBody, meta.end, "keys");
|
||||
const ilstBox = _findMP4Box(view, bytes, metaBody, meta.end, "ilst");
|
||||
if (!keysBox || !ilstBox) return null;
|
||||
|
||||
const utf8 = new TextDecoder("utf-8", { fatal: false });
|
||||
let off = keysBox.bodyStart + 4; // version+flags をスキップ
|
||||
const count = view.getUint32(off); off += 4;
|
||||
const keyNames = [];
|
||||
for (let i = 0; i < count && off + 8 <= keysBox.end; i++) {
|
||||
const ksize = view.getUint32(off);
|
||||
if (ksize < 8) break;
|
||||
keyNames.push(utf8.decode(bytes.slice(off + 8, off + ksize)));
|
||||
off += ksize;
|
||||
}
|
||||
|
||||
const result = {};
|
||||
let ioff = ilstBox.bodyStart;
|
||||
while (ioff + 8 <= ilstBox.end) {
|
||||
const itemSize = view.getUint32(ioff);
|
||||
const keyIndex = view.getUint32(ioff + 4);
|
||||
if (itemSize < 8) break;
|
||||
const itemEnd = Math.min(ioff + itemSize, ilstBox.end);
|
||||
const data = _findMP4Box(view, bytes, ioff + 8, itemEnd, "data");
|
||||
if (data) {
|
||||
const payload = bytes.slice(data.bodyStart + 8, data.end); // type(4)+locale(4)をスキップ
|
||||
const name = keyNames[keyIndex - 1];
|
||||
if (name) { try { result[name] = utf8.decode(payload); } catch { /* skip */ } }
|
||||
}
|
||||
ioff = itemEnd;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI→API変換(static/js/comfyui-workflow.js の convertUiToApi() 相当ロジックの複製)
|
||||
// web/comfyui/ は static/js/ と配信URLが異なりESモジュールをimportできないため、
|
||||
@@ -3301,6 +3405,13 @@ function _extractPromptsAPI(wf) {
|
||||
if (combined) textMap.set(id, combined);
|
||||
continue;
|
||||
}
|
||||
// MiniMaxH3ImageToVideo等の動画生成オールインワンノード。CLIPTextEncode等を経由せず
|
||||
// 自ノードの"prompt"入力に直接テキストを持つ(positive/negativeの区別も無い)ため、
|
||||
// _isTextEncoderNode判定を通さずtextMapへ直接投入する(fallbackのtextsに載る)。
|
||||
if (typeof n.inputs?.prompt === "string" && /ImageToVideo|TextToVideo/i.test(ct)) {
|
||||
textMap.set(id, n.inputs.prompt);
|
||||
continue;
|
||||
}
|
||||
if (!_isTextEncoderNode(ct)) continue;
|
||||
// "prompt" — TextEncodeQwenImageEdit(Plus)/TextEncodeBooguEdit and similar Image Edit
|
||||
// model text encoders use this key instead of "text"/"text_g".
|
||||
@@ -3428,6 +3539,7 @@ async function _extractAllMetadata(file) {
|
||||
const name = file.name.toLowerCase();
|
||||
const isJSON = file.type === "application/json" || name.endsWith(".json");
|
||||
const isWebP = file.type === "image/webp" || name.endsWith(".webp");
|
||||
const isMP4 = file.type === "video/mp4" || name.endsWith(".mp4");
|
||||
|
||||
async function fromWorkflow(originalWf, source) {
|
||||
let wf = originalWf;
|
||||
@@ -3463,6 +3575,17 @@ async function _extractAllMetadata(file) {
|
||||
const wf = _extractWorkflowFromEXIF(exif);
|
||||
return wf ? await fromWorkflow(wf, "comfyui") : null;
|
||||
}
|
||||
if (isMP4) {
|
||||
let tags; try { tags = await _readMP4MetaTags(file); } catch { return null; }
|
||||
if (!tags) return null;
|
||||
for (const key of ["prompt", "workflow"]) {
|
||||
const raw = tags[key];
|
||||
if (!raw) continue;
|
||||
let wf; try { wf = JSON.parse(_sanitizeJSON(raw)); } catch { continue; }
|
||||
if (wf) return await fromWorkflow(wf, "comfyui");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// PNG
|
||||
const chunks = await _readAllPNGTextChunks(file);
|
||||
if (!chunks) return null;
|
||||
@@ -3572,7 +3695,7 @@ const setupInfoDropHandlers = (container) => {
|
||||
const renderInfoModels = (container, meta) => {
|
||||
container.innerHTML = "";
|
||||
if (!meta) {
|
||||
container.innerHTML = `<div class="wfm-nlp-info-empty">Drop a PNG/WebP/JSON file to view model info</div>`;
|
||||
container.innerHTML = `<div class="wfm-nlp-info-empty">Drop a PNG/WebP/MP4/JSON file to view model info</div>`;
|
||||
return;
|
||||
}
|
||||
const sections = [
|
||||
@@ -3611,7 +3734,7 @@ const renderInfoModels = (container, meta) => {
|
||||
const renderInfoLoras = (container, meta) => {
|
||||
container.innerHTML = "";
|
||||
if (!meta || !meta.loras || meta.loras.length === 0) {
|
||||
container.innerHTML = `<div class="wfm-nlp-info-empty">${meta ? "No LoRA found" : "Drop a PNG/WebP/JSON file to view LoRA info"}</div>`;
|
||||
container.innerHTML = `<div class="wfm-nlp-info-empty">${meta ? "No LoRA found" : "Drop a PNG/WebP/MP4/JSON file to view LoRA info"}</div>`;
|
||||
return;
|
||||
}
|
||||
for (const lora of meta.loras) {
|
||||
@@ -3660,7 +3783,7 @@ const renderInfoPrompts = (container, meta) => {
|
||||
const promptFull = panelEl?.querySelector("#wfm-nlp-info-prompt-full");
|
||||
const promptFullLabel = panelEl?.querySelector("#wfm-nlp-info-prompt-full-label");
|
||||
if (!meta) {
|
||||
container.innerHTML = `<div class="wfm-nlp-info-empty">Drop a PNG/WebP/JSON file to view prompts</div>`;
|
||||
container.innerHTML = `<div class="wfm-nlp-info-empty">Drop a PNG/WebP/MP4/JSON file to view prompts</div>`;
|
||||
return;
|
||||
}
|
||||
const allPrompts = [
|
||||
@@ -3725,8 +3848,8 @@ const renderInfoTab = (container) => {
|
||||
<div class="wfm-nlp-info-layout">
|
||||
<div class="wfm-nlp-info-drop" id="wfm-nlp-info-drop">
|
||||
<img id="wfm-nlp-info-preview-img" style="display:none;max-width:100%;max-height:100%;object-fit:contain;pointer-events:none;">
|
||||
<span id="wfm-nlp-info-drop-label" style="font-size:11px;color:var(--descrip-text,#999);pointer-events:none;">Drop PNG / WebP / JSON</span>
|
||||
<input type="file" id="wfm-nlp-info-file-input" accept=".png,.webp,.json,image/png,image/webp,application/json" style="display:none;">
|
||||
<span id="wfm-nlp-info-drop-label" style="font-size:11px;color:var(--descrip-text,#999);pointer-events:none;">Drop PNG / WebP / MP4 / JSON</span>
|
||||
<input type="file" id="wfm-nlp-info-file-input" accept=".png,.webp,.mp4,.json,image/png,image/webp,video/mp4,application/json" style="display:none;">
|
||||
</div>
|
||||
<div id="wfm-nlp-info-fileinfo" class="wfm-nlp-info-fileinfo">—</div>
|
||||
<div id="wfm-nlp-info-subcontent" class="wfm-nlp-info-subcontent"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { togglePanel, getNodeSetsIcon, NODE_SETS_TOOLTIP, saveSelectedAsNodeSet } from "./node_sets_menu.js";
|
||||
import { togglePanel, getNodeSetsIcon, NODE_SETS_TOOLTIP, saveSelectedAsNodeSet, showWorkflowInLibrary } from "./node_sets_menu.js";
|
||||
|
||||
const BUTTON_TOOLTIP = "Launch Workflow Studio (Shift+Click opens in new window)";
|
||||
const SNAPSHOT_TOOLTIP = "Save workflow canvas image as thumbnail";
|
||||
@@ -572,6 +572,23 @@ app.registerExtension({
|
||||
};
|
||||
},
|
||||
async setup() {
|
||||
// Send to Canvas fallback: when the Workflow Studio SPA can't reach this page
|
||||
// directly via window.opener (e.g. it isn't the opener that spawned it, or
|
||||
// the workflow is API-format and window.opener.wfmReceiveWorkflow can't
|
||||
// handle it either), the SPA writes the workflow's filename here instead of
|
||||
// the old title-drag mechanism. localStorage "storage" events fire in every
|
||||
// same-origin tab except the one that wrote the value, so this listener
|
||||
// picks it up here and opens the Library panel pre-filtered to that
|
||||
// workflow's card for the user to drag onto the canvas themselves.
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key === "wfm_library_filter_request" && e.newValue != null) {
|
||||
showWorkflowInLibrary(e.newValue);
|
||||
// Consume it so a future page load (which reads the stale value but
|
||||
// never sees a "storage" event for it) doesn't re-trigger the filter.
|
||||
localStorage.removeItem("wfm_library_filter_request");
|
||||
}
|
||||
});
|
||||
|
||||
const injectStyles = () => {
|
||||
const styleId = "wfm-top-menu-button-styles";
|
||||
if (document.getElementById(styleId)) return;
|
||||
|
||||
@@ -0,0 +1,627 @@
|
||||
{
|
||||
"id": "8e2c1a4f-6b3d-4a9e-9c7f-1d5e8f3a2b6c",
|
||||
"revision": 0,
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 11,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
413,
|
||||
389
|
||||
],
|
||||
"size": [
|
||||
425.27801513671875,
|
||||
180.6060791015625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"localized_name": "text",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.8",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"text, watermark"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
863,
|
||||
186
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
474
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "model",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"localized_name": "positive",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"localized_name": "negative",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"localized_name": "latent_image",
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"localized_name": "seed",
|
||||
"name": "seed",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "seed"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "steps",
|
||||
"name": "steps",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "steps"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "cfg",
|
||||
"name": "cfg",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "cfg"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "sampler_name",
|
||||
"name": "sampler_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "sampler_name"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "scheduler",
|
||||
"name": "scheduler",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "scheduler"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "denoise",
|
||||
"name": "denoise",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "denoise"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.8",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
378124453179007,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1209,
|
||||
188
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "samples",
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"localized_name": "vae",
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.8",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1451,
|
||||
189
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
},
|
||||
{
|
||||
"localized_name": "filename_prefix",
|
||||
"name": "filename_prefix",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filename_prefix"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveImage",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.8",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
473,
|
||||
609
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "width",
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "width"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "height",
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "height"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "batch_size",
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "batch_size"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.8",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
1024,
|
||||
1024,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
51.3,
|
||||
182.49999999999997
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "ckpt_name",
|
||||
"name": "ckpt_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "ckpt_name"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "MODEL",
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1,
|
||||
11
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "CLIP",
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
3,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "VAE",
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.8",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly-fp16.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "VAELoader",
|
||||
"pos": [
|
||||
61.897062649999256,
|
||||
347.8818042399999
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "vae_name",
|
||||
"name": "vae_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "vae_name"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "VAE",
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {}
|
||||
},
|
||||
"Node name for S&R": "VAELoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"vae-ft-mse-840000-ema-pruned.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "ImpactWildcardEncode",
|
||||
"pos": [
|
||||
415,
|
||||
150
|
||||
],
|
||||
"size": [
|
||||
420,
|
||||
322
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "model",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 11
|
||||
},
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "model",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"localized_name": "conditioning",
|
||||
"name": "conditioning",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "populated_text",
|
||||
"name": "populated_text",
|
||||
"type": "STRING",
|
||||
"slot_index": 3,
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImpactWildcardEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"a photo of __sample_season/spring__, with __sample_lighting/natural__, highly detailed",
|
||||
"",
|
||||
"populate",
|
||||
"Select the LoRA to add to the text",
|
||||
"Select the Wildcard to add to the text",
|
||||
123456789,
|
||||
"randomize"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
4,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
2,
|
||||
5,
|
||||
0,
|
||||
3,
|
||||
3,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
6,
|
||||
1,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
4,
|
||||
6,
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
5,
|
||||
4,
|
||||
1,
|
||||
7,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
3,
|
||||
2,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
7,
|
||||
3,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
9,
|
||||
8,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
0,
|
||||
8,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
11,
|
||||
4,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
"MODEL"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scale": 1
|
||||
},
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": []
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
Reference in New Issue
Block a user