Compare commits

...
8 Commits
Author SHA1 Message Date
ketle-manandClaude Opus 5.5 6016ac1fcf docs: manual v0.9.9 (ja/en) — prompt translation
- New section 5-10: writing prompts in your own language (Prompt
  Translation) — setup, PLaMo 3 Translate sizes, GPU vs CPU-resident,
  the globe/Original buttons, automatic translation for Generate, Lab Run
  and Video Plan Run.
- Notes in 7-5 and 10-2 (PLaMo in the translate buttons), 11-4 (Video Plan
  globe button and the v0.9.9 Plan-values fix), 11-5 settings table, 12-3
  troubleshooting table, appendix glossary and links.
- Rebuilt the combined MD and ZIP for v0.9.9 (v0.9.6 files removed);
  README points to the v0.9.9 ZIPs and the v0.9.9 EPUB edition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 11:56:50 +09:00
ketle-manandClaude Opus 5.5 b82e93a431 release: v0.9.9 — prompt translation (PLaMo 3 Translate), Video Plan values fix
Prompt translation (native language -> English), off by default:
- Settings -> Prompt Translation: Ollama model (PLaMo 3 Translate
  0.8B/2B/8B/31B presets with Download, or any Ollama model), source
  language, GPU or CPU-resident mode, and "translate automatically when
  running" for GenerateUI Generate, Lab Run and Video Plan Run.
- 🌐 / Original buttons on the GenerateUI prompts, the Lab prompt modal and
  the Video Plan block prompt. Only parts with non-ASCII letters are
  translated; prompt syntax (weights, <lora:>, wildcards, BREAK) is kept.
- PLaMo is driven with a raw PFML3 prompt on /api/generate; CPU mode keeps
  it loaded (num_gpu 0, keep_alive -1) and preloads it when GenerateUI opens.
- Lab/Video runs translate only what is sent; failures stop the batch.

Fixes:
- Translate buttons in the Prompt tab AI Assistant, AI TOOL tab and side
  panel A tab sent an instruction prompt, which PLaMo translates or treats
  as context; PLaMo models now get a [plamo-translate] system header.
- Video Plan wrote widgets_values only, but convertUiToApi() prefers the
  newer widgets_values_named copy, so the plan's prompt/duration/turbo
  values were replaced by the template's own (7 of 8 local templates).

Also adds the missing v0.9.8 DEVLOG entry and README badge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 11:27:56 +09:00
ketle-manandClaude Fable 5 f39227a95b security: harden path validation (NT device forms, Eagle SVG path)
Follow-up to ea44cf4, from the automated security review of that commit:

- resolve_untrusted (py/config.py) now allowlists the accepted shapes:
  a drive-letter absolute path on Windows (or '/'-rooted elsewhere), or a
  UNC path under a UNC allowed root. Rooted NT device forms (\??\...,
  \.\...), root-relative (\foo), drive-relative (C:foo) and relative
  values are rejected before any filesystem access.
- eagle_routes _resolve_absolute_svg_path called os.path.isfile on the
  raw client path before the containment check — on Windows a UNC value
  makes that isfile() connect to the host with the account's credentials.
  The containment check (string-only abspath/commonpath) now runs first
  and isfile() only on paths under a ComfyUI-managed folder.
- eagle_routes _resolve_local_path rejects UNC/drive/absolute/.. subfolder
  values as strings before the join (commonpath raised ValueError for
  those instead of returning None).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-11-14 13:29:16 +09:00
ketle-manandClaude Fable 5 ea44cf41fe security: validate client paths as strings before resolve()/exists()
On Windows, resolving (or exists()-checking) a UNC path (\host\share)
makes the server connect to that host with the account's credentials
before any containment check runs. Client-supplied paths were resolved
first and checked against their base folder afterwards.

Add is_safe_rel/resolve_untrusted (py/config.py): string-only processing
(normpath + unresolved relative_to) that rejects UNC, drive-relative and
embedded-NUL values before the first resolve(); a UNC path is allowed
only under an allowed root that is itself UNC (NAS gallery roots keep
working). Applied to:
- gmic result (routes/gmic_routes.py): also moved the raw os.path.exists
  after the check
- gallery service: all client path/folder entries now go through
  _resolve_untrusted before _check_path_allowed; .gltf external resource
  URIs must be relative (is_safe_rel) before joining
- semantic index/similar, tagger write_meta_to_file
- video media refs ({filename, subfolder}) and settings export/import
  (project media refs, backup-ZIP entry names): is_safe_rel before join
- wildcard _safe_path: also reject drive-relative segments (C:name)

The existing resolve()+relative_to containment checks are kept as defense
in depth. Same issue as ComfyUI-VRM-Pose-Studio 2be32ea.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-11-14 13:25:49 +09:00
ketle-manandClaude Opus 5.5 f6ce112ade release: v0.9.6 — MiniMax-H3-Image, Settings Switches column
- GenerateUI/Metadata/I tab: MiniMax H3 Image (T2I and Edit) support
- GenerateUI Settings tab: Switches column for true/false and two-choice
  on/off inputs, toggled instantly
- Fix ImageCompare failing validation (compare_view missing)
- Manual v0.9.6 (ja/en) and EPUB edition on Google Drive

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 18:28:10 +09:00
ketle-manandClaude Opus 5.5 48198eac2b docs: manual v0.9.6 (ja/en) and EPUB edition link
- Settings tab Switches column (5-4), MiniMax H3 Image and Compare Images
  fix (5-9), Apply exception for switches (12-3); target version v0.9.6
- Rebuilt combined MD/ZIP as v0.9.6 and removed the v0.9.5 files
- README: link to the EPUB edition with screenshots on Google Drive

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 18:22:33 +09:00
ketle-manandClaude Opus 5.5 f0ae2253f4 feat: GenerateUI Settings tab — Switches column for on/off values
- New third column lists every literal true/false input in the workflow
  and every two-choice enable/disable, on/off, true/false or yes/no combo
  (e.g. KSamplerAdvanced add_noise, DWPreprocessor detect_*), with node
  and input names; a click toggles it at once and syncs Raw JSON
- Combos are detected from per-type object_info and added once it loads;
  DynamicCombo selectors (TextGenerate sampling_mode) are left out
- Help (en/ja/zh) and DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 17:57:11 +09:00
ketle-manandClaude Opus 5.5 27066918ef feat: MiniMax-H3-Image support; fix ImageCompare compare_view missing
- GenerateUI/Metadata/I tab: support MiniMaxH3ImageConditioning (MiniMax
  H3 Image T2I and Edit templates), handled like PH3ImageConditioning
- Fix "required_input_missing" for ImageCompare: its display-only
  compare_view widget is not stored in widgets_values; send the same
  {"__value__": ["", ""]} the frontend does
- README and DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 17:47:14 +09:00
59 changed files with 2395 additions and 119 deletions
+3
View File
@@ -2,6 +2,9 @@
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.9.9(2026-11-15)** — プロンプト翻訳(自国語 → 英語、PLaMo 3 Translate)
- **v0.9.8(2026-11-14)** — セキュリティ: クライアントから受け取るパスを解決前に検証
- **v0.9.6(2026-11-12)** — マニュアル v0.9.6版(ja/en)
- **v0.9.5(2026-11-11)** — AI TOOL Chat: スキル v2(metadata・同梱スキルの版管理・プロンプトボタン)
- **v0.9.4(2026-11-07)** — Gallery: 3Dモデル対応(GLB / glTF / VRM / PLY / SPZ)
- **v0.9.3(2026-11-05)** — Image Edit: Drawツールのカラーパレット・ショートカット・スポイト(Comic Creator v1.67.0 から移植)
+60
View File
@@ -2,6 +2,66 @@
---
## v0.9.9(2026-11-15)
### プロンプト翻訳(自国語 → 英語、PLaMo 3 Translate)
- 設定タブに「プロンプト翻訳」セクションを新設(既定オフ)。A1111 の prompt translator 拡張と同じく、英数字以外の文字を含む部分だけを翻訳し、カンマ・`(語句:1.2)`・`[ ]`・`{a|b}`・`<lora:...>`・`__wildcard__`・`embedding:`・`BREAK`/`AND` は保護する(`static/js/prompt-translate.js` の `splitPrompt`)。
- 🌐ボタン(+「原文」ボタン、Ctrl+Z も可)を GenerateUI の Positive/Negative、Lab プロンプトモーダル、Video Plan のブロックプロンプトに追加。
- モデルは Ollama 経由。PLaMo は `/api/generate` に `raw:true` で PFML3 プロンプトを直接送る(モデルカード推奨、stop `<|plamo:tag|>`/`<|plamo:bos|>`)。PLaMo 以外の Ollama モデルは `/api/chat` +翻訳指示。PLaMo の4サイズはプリセットとして表示し、未取得なら「ダウンロード」(`/api/wfm/translate/pull`、プリセットのみ許可)。
- 実行場所: GPU(1語句約0.1秒、翻訳後アンロード可)/CPU常駐(`num_gpu:0`+`keep_alive:-1`、1語句約0.3秒、VRAM 0、GenerateUI表示時に `/warmup` で裏読み込み、デバイス・モデル切替や無効化時は `/unload`)。実測でコールド約4秒の大半は Ollama ランナー起動(CPUでも4.4秒)だったため、常駐でほぼ解消。
- 「実行時に自動で翻訳する」: GenerateUI Generate(欄の文を置き換え)/Lab Run(各回のAPIワークフローのコピーを翻訳)/Video Plan Run(送信するプロンプトだけ翻訳)。Lab/Video はプランを原文のまま残す。失敗時は生成せず中断し、理由を進行状況欄に残す(終了トーストが上書きするため)。
- サーバー側に翻訳結果の上限付きキャッシュ(1024件)。設定は `settings.json` の `prompt_translate_*`。
- 実機確認: Z-Image で Generate 時自動翻訳と Lab Run(2枚)を実生成、Video Plan Run は `/prompt` 送信をブラウザで横取りして英訳の送信を確認。
### 修正: PLaMo を選んだときの各翻訳ボタン
- Prompt タブ AI アシスタント・AI TOOL タブ・サイドパネル A タブの翻訳は、指示文+本文(または指示の system メッセージ)で送っていたため、PLaMo では指示文まで翻訳される/日本語⇔英語の自動切替になり ZH→EN・EN→ZH・自由入力が誤った言語で出ていた。PLaMo 系モデルのときは先頭 system に `[plamo-translate]` と `target_lang`/`source_lang` を書き本文だけを送る(モデルカードの Ollama/LM Studio 向け方式)。言語名(French・フランス語・français 等)を PLaMo の言語コードに変換する `static/js/plamo-chat.js` を追加(サイドパネルは import できないためインラインコピー)。
- Prompt タブ AI アシスタントに応答待ちの表示を追加(初回はモデル読み込みで十数秒かかることがある)。
### 修正: Video Plan のプロンプト等がテンプレートの値のまま送られていた
- 新しいフロントエンドで保存したワークフローは、サブグラフのインスタンスノードに名前付きの値のコピー `widgets_values_named` を持ち、`convertUiToApi()` はそちらを優先する。`applyBlockToWorkflow` は `widgets_values` だけを書いていたため、Plan のプロンプト・Duration・Turbo 等が無視されていた(手元の動画テンプレート8本中7本)。書き込み後に `widgets_values_named` へ反映する `_syncNamedWidgetValues` を追加。修正後は12本中11本で反映(残る `minimaax_h3_fast.json` はプロンプトを LiveChatStream から配線で受けるため対象外)。
### ヘルプ
- 設定タブ(helpSettings26)と GenerateUI(helpGen33)にプロンプト翻訳を追加(en/ja/zh)。
### マニュアル v0.9.9版(ja/en)
- 新節 5-10「日本語のままプロンプトを書く(プロンプト翻訳)」(準備・モデルの大きさ・GPU/CPU常駐・🌐と原文・実行時の自動翻訳・Lab/Video Plan)。7-5・10-2 に PLaMo の注記、11-4 に Video Plan の🌐と v0.9.9 修正の注意、11-5 の表・12-3 のトラブル表・付録の用語集と参考リンクを追記。対象バージョンを v0.9.9 に更新し、全章MD・ZIPを `manual/rebuild_repo_edition.py` で再生成(v0.9.6版は削除)。
- EPUB(画像付き、75枚=新規3枚: 設定のプロンプト翻訳・生成UIで翻訳後・Labモーダル)を Google ドライブで公開。
## v0.9.8(2026-11-14)
### セキュリティ: クライアントから受け取るパスを解決前に検証
- `py/config.py` に `is_safe_rel` / `resolve_untrusted` を追加。文字列だけで正規化・配下チェックし、UNC・ドライブ相対・NUL 混入を最初の `resolve()`/`exists()` より前に拒否(Windows では UNC を解決した時点でアカウント資格情報付きで相手ホストへ接続するため)。UNC は許可ルート自体が UNC の場合のみ許可。G'MIC 結果・Gallery のパス受け取りに適用(ea44cf4)。
- 追加の強化: 受け付ける形をドライブレター絶対パス(Windows)または許可された UNC に限定し、NT デバイス形式(`\??\...`、`\.\...`)等を拒否。Eagle の SVG パスで配下チェック前に `os.path.isfile` していた箇所を修正(f39227a)。
- ※v0.9.8 はリリース時に README バッジと DEVLOG の更新が漏れていたため、v0.9.9 で追記。
## v0.9.6(2026-11-12)
### マニュアル v0.9.6版(ja/en)
- 5-4 Settingsタブを3列(スイッチ列の説明を追加)、5-9 対応表に MiniMax H3 Image と Compare Images 修正の注記、12-3 の Apply 注意にスイッチ列の例外を追記。対象バージョンを v0.9.6 に更新し、全章MD・ZIPを `manual/rebuild_repo_edition.py` で再生成(v0.9.5版は削除)。
### GenerateUI Settingsタブ: スイッチ列
- KSampler・Latent Imageの右に3列目「スイッチ」を追加。APIワークフロー内のリテラルな true/false 入力(ComfySwitchNode の switch、PrimitiveBoolean の value、TextGenerateV2 の thinking など)をノード名・入力名付きで一覧し、チェックボックスで即時切り替え(Apply不要、Raw JSONへ同期)。リンクされた入力は配列なので対象外で、配線元のPrimitiveBooleanが出る。
- 選択肢がちょうど enable/disable・enabled/disabled・on/off・true/false・yes/no の2択のCOMBO(KSamplerAdvanced add_noise、DWPreprocessor detect_* など)も対象。object_info を型別取得してから追加描画し、値はそのノード自身の綴りの文字列で書き戻す。DynamicCombo(TextGenerate の sampling_mode)は子入力が変わるため対象外。
- 保存済みワークフロー347本(App形式5本は読込対象外)をブラウザで順に読み込み、128本・426項目でオン→オフ→元に戻すの往復と Raw JSON 同期を自動検証、失敗・コンソールエラー0。
- MiniMax H3 Image のT2I(サブグラフ内のプロンプト強化スイッチ)とEdit(PrimitiveBoolean「Enable PE?」)でブラウザ実操作を確認。
### MiniMax-H3-Image対応
- `MiniMaxH3ImageConditioning`(Comfy-Org の MiniMax H3 Image T2I/Edit テンプレート)をGenerateUI/Metadata/Iタブに追加。入力は `prompt`/`width`/`height`/`images.image_N` で `PH3ImageConditioning` と同形のため同じ扱い(negativeなし、promptはPreviewAny→プロンプト強化スイッチ経由で配線元のユーザープロンプトをpositiveに)。width/height=0は image_1 のサイズ、画像なしなら2048。
- 修正前はプロンプト欄が空で、Metadataタブもプロンプトを読めなかった。T2I(プロンプト強化オフ、2048x2048)とEdit(プロンプト強化オン、2720x1536)で実生成確認。
### 修正: ImageCompareノードを含むワークフローで検証エラー
- `ImageCompare` の `compare_view`(表示専用の socketless ウィジェット、`widgets_values` に保存されない)がAPI変換で欠落し、`required_input_missing` になっていた。フロントエンドと同じ `{"__value__": ["", ""]}` を補う(`_DISPLAY_WIDGET_DEFAULTS`)。MiniMax H3 Image Edit テンプレートで発覚。
## v0.9.5(2026-11-11)
### AI TOOL Chat: スキル v2(metadata・同梱スキルの版管理・プロンプトボタン)
+9 -7
View File
@@ -27,7 +27,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.9.5-green)
![Version](https://img.shields.io/badge/version-0.9.9-green)
## Features (tab -> feature names)
@@ -38,7 +38,7 @@ Thumbnail / Table views, Thumbnail side panel, Badge filtering, Search, Side pan
One-click capture, Auto-save as thumbnail, Embedded workflow metadata, Auto-import
### GenerateUI Tab (v0.3.5)
6-tab layout, Save button, Input tab, Multi-image I2I for Comic Creator, 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, Qwen Image 2.1 support, Ming Image support, P-H3 Image support
6-tab layout, Save button, Input tab, Multi-image I2I for Comic Creator, 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, Qwen Image 2.1 support, Ming Image support, P-H3 Image support, MiniMax-H3-Image support
### 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
@@ -50,7 +50,7 @@ Setting / Results / Plan JSON sub-panels, Per-column keyframes, Model column —
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, CivitAI Host, CivitAI API Key, Default workflow, Default Checkpoint, VRAM Management (Ollama), Video Playback Volume, Data Management, Text Size, RAW JSON Colors, GenerateUI Model Tab Highlight, Wildcard Integration, G'MIC-Qt Integration, Language
2-column layout, Collapsible sections, Theme selection, Theme customization, Workflows directory, Gallery output directory, Eagle connection, CivitAI Host, CivitAI API Key, Default workflow, Default Checkpoint, VRAM Management (Ollama), Video Playback Volume, Data Management, Text Size, RAW JSON Colors, GenerateUI Model Tab Highlight, Wildcard Integration, G'MIC-Qt Integration, Prompt Translation (native language → English), 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, WebM support, Embedded metadata reading fixes, Audio support, File conversion, Move To folder filter, Semantic Search, Audio waveform colors, Info tab: dimensions & duration, Multi-select, Image Compare, Prompt search, Server-side filtering, Group management, Thumbnail F button, Favorites, Detail panel, Prompt tab, Nanobanana tab, Gemini / Nano Banana nodes, MOSS-Image in GenerateUI, Faster GenerateUI Check / conversion, Partner API nodes in GenerateUI, Workflow viewer, GenUI button, Image Edit button, Send GenUI Image button, Send to LI node button, Send to LV node button, Send CC Layout / Send CC Image buttons, Search clear (✕), Clear all filters (✕ Clear), Workflow auto-save, Output folder configurable, SVG file support, PSD file support, 3D model support, Performance
@@ -128,10 +128,12 @@ Samples for the Feeder tab: `ws_sdxl_image_feeder_i2i` / `ws_dwpose_image_feeder
## Manual
A beginner-oriented manual (covers v0.9.5, text-only edition without screenshots) is available in [`docs/manual/`](docs/manual/):
A beginner-oriented manual (covers v0.9.9, text-only edition without screenshots) is available in [`docs/manual/`](docs/manual/):
- English: [`docs/manual/en/`](docs/manual/en/) — ZIP: `Workflow_Studio_Manual_v0.9.5_en.zip`
- 日本語: [`docs/manual/`](docs/manual/) — ZIP: `Workflow_Studio_Manual_v0.9.5.zip`
- English: [`docs/manual/en/`](docs/manual/en/) — ZIP: `Workflow_Studio_Manual_v0.9.9_en.zip`
- 日本語: [`docs/manual/`](docs/manual/) — ZIP: `Workflow_Studio_Manual_v0.9.9.zip`
An EPUB edition with screenshots (Japanese and English, v0.9.9) is available on [Google Drive](https://drive.google.com/drive/folders/14mOHcnLYrFgNrOo6fy7ELyFjjWrAoU-_?usp=sharing).
---
@@ -169,7 +171,7 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
### Optional
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features; also runs PLaMo 3 Translate for Settings → Prompt Translation (`ollama pull hf.co/pfnet/plamo-3-translate-2b-GGUF:Q4_K_M`, or the Download button there)
- **[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); needs an API key set via `UNSLOTH_API_KEY` in a `.env` file, or Unsloth's Keyless API access → "Chat and inference" turned on — `pip install -r requirements.txt` for `python-dotenv`
+9 -5
View File
@@ -23,7 +23,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.9.5-green)
![Version](https://img.shields.io/badge/version-0.9.9-green)
## Screenshots
@@ -134,6 +134,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- **Qwen Image 2.1 support** (v0.7.3) — `TextEncodeQwenImage21` (the unified T2I/Edit encoder used by Comfy-Org's Qwen-Image-2.1 workflow templates) is now recognized: its `prompt`/`negative_prompt` widgets populate the Input tab's Positive/Negative Prompt textareas and are editable like any other prompt node; its `resolution` (a pixel-budget int, not width/height) is exposed for Raw JSON editing. No extra work was needed for the model's up to 10 reference images (`image_1`…`image_10`, used for image editing) — each one arrives as an ordinary LoadImage node, so it already gets its own card in the Input tab's Image sub-panel via the existing generic LoadImage detection
- **Ming Image support** (v0.7.5) — `TextEncodeMingImageEdit` (single `prompt` widget, up to 8 optional `images.image_N` reference inputs; role taken from the sampler/guider wiring, so the empty-prompt negative instance in layer-decomposition workflows shows as Negative) and `EmptyQwenImageLayeredLatentImage` (Latent panel; `layers` kept as-is) are recognized in GenerateUI, and Gallery/Metadata/sidepanel I tab now show their prompts as Positive. The Sampler field is disabled (N/A) when the SAMPLER input comes from a node without `sampler_name` (e.g. `SamplerLCM`). **Fix:** top-level nodes whose widgets are converted to wired inputs (e.g. `EmptyLatentImage` width/height fed by `GetImageSize`) keep those values in `widgets_values`; they were not stripped outside subgraphs, shifting later values (`batch_size` became `1024`, KSampler `steps`/`cfg` misread). Also: `BasicGuider`'s `conditioning` is treated as positive, and a linked text input wins over the stale widget text in UI-format metadata
- **P-H3 Image support** (v0.9.4) — the `PH3ImageConditioning` templates (Image Edit and T2I) load and run from GenerateUI, with the prompt shown on the Input tab and read back by the Metadata tab. Width and height follow the workflow's wiring, and the prompt upsampler runs as set in the workflow
- **MiniMax-H3-Image support** (v0.9.6) — the `MiniMaxH3ImageConditioning` templates (Image Edit and T2I) load and run from GenerateUI the same way, with the prompt shown on the Input tab and read back by the Metadata tab and the sidepanel I tab
</details>
@@ -256,6 +257,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
- **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`; the same color and mechanism also drives the Video tab's field highlighting (v0.5.4, see above)
- **Wildcard Integration** — link the WFS wildcard directory to ComfyUI-Impact-Pack's `wildcards/` directory (directory junction on Windows, symlink on other OS); existing WFS wildcard files are automatically migrated; requires [ComfyUI-Impact-Pack](https://github.com/ltdrdata/ComfyUI-Impact-Pack)
- **G'MIC-Qt Integration** — set the path to `gmic_qt.exe` for the Image Edit Filter tool; download G'MIC-Qt Standalone from `gmic.eu/download.html` (Windows: `gmic_qt-win64.zip`), extract anywhere, then enter the full path to `gmic_qt.exe` and click Save
- **Prompt Translation (native language → English)** (v0.9.9) — off by default. Adds a 🌐 button to the GenerateUI Positive/Negative prompts, the Lab prompt modal and the Video Plan block prompt that translates only the parts written in your own language into English with a local Ollama model, keeping prompt syntax as it is (commas, `(word:1.2)`, `[ ]`, `{a|b}`, `<lora:...>`, `__wildcards__`, `embedding:name`, `BREAK`); "Original" restores the text. Model: [PLaMo 3 Translate](https://huggingface.co/pfnet/plamo-3-translate-2b-GGUF) 0.8B / 2B / 8B / 31B (Download button pulls it into Ollama; free for personal and non-commercial use under the PLaMo Community License) or any other Ollama model. Source language auto-detect or one of 51 languages; run on GPU (about 0.1 s per phrase, optionally unloaded after each translation) or **CPU kept loaded** (about 0.3 s per phrase, no VRAM, preloaded when GenerateUI opens). **Translate automatically when running**: GenerateUI Generate (replaces the prompt text), Lab Run and Video Plan Run (translate only what is sent; the plan keeps your text); if translation fails nothing is generated. Server routes: `POST /api/wfm/translate`, `GET /api/wfm/translate/config|models`, `POST /api/wfm/translate/pull|warmup|unload`
- **Language** — English / Japanese / Chinese
</details>
@@ -606,10 +608,12 @@ Samples for the Feeder tab: `ws_sdxl_image_feeder_i2i` / `ws_dwpose_image_feeder
## Manual
A beginner-oriented manual (covers v0.9.5, text-only edition without screenshots) is available in [`docs/manual/`](docs/manual/):
A beginner-oriented manual (covers v0.9.9, text-only edition without screenshots) is available in [`docs/manual/`](docs/manual/):
- English: [`docs/manual/en/`](docs/manual/en/) — ZIP: `Workflow_Studio_Manual_v0.9.5_en.zip`
- 日本語: [`docs/manual/`](docs/manual/) — ZIP: `Workflow_Studio_Manual_v0.9.5.zip`
- English: [`docs/manual/en/`](docs/manual/en/) — ZIP: `Workflow_Studio_Manual_v0.9.9_en.zip`
- 日本語: [`docs/manual/`](docs/manual/) — ZIP: `Workflow_Studio_Manual_v0.9.9.zip`
An EPUB edition with screenshots (Japanese and English, v0.9.9) is available on [Google Drive](https://drive.google.com/drive/folders/14mOHcnLYrFgNrOo6fy7ELyFjjWrAoU-_?usp=sharing).
---
@@ -647,7 +651,7 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
### Optional
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features; also runs PLaMo 3 Translate for Settings → Prompt Translation (`ollama pull hf.co/pfnet/plamo-3-translate-2b-GGUF:Q4_K_M`, or the Download button there)
- **[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); needs an API key set via `UNSLOTH_API_KEY` in a `.env` file, or Unsloth's Keyless API access → "Chat and inference" turned on — `pip install -r requirements.txt` for `python-dotenv`
+3 -3
View File
@@ -2,12 +2,12 @@
ノードが苦手でも大丈夫 ― 画像生成をもっと手軽に、もっと整理して
- 対象バージョン:Workflow Studio v0.9.5
- 対象バージョン:Workflow Studio v0.9.9
- 著者:ketle-man
- © 2026 ketle-man
このマニュアルは、Kindle版をもとにした **画像なしのテキスト版** です。
章ごとのファイルは `chapters/` フォルダ、全章をまとめたファイルは `Workflow_Studio_Manual_v0.9.5.md` です。
章ごとのファイルは `chapters/` フォルダ、全章をまとめたファイルは `Workflow_Studio_Manual_v0.9.9.md` です。
画面の例は省略しているため、実際の画面はWorkflow Studioのヘルプタブとあわせてご覧ください。
## 目次
@@ -29,4 +29,4 @@
- [13_付録](chapters/13_付録.md)
- [14_おわりに](chapters/14_おわりに.md)
全章まとめ:[Workflow_Studio_Manual_v0.9.5.md](Workflow_Studio_Manual_v0.9.5.md)
全章まとめ:[Workflow_Studio_Manual_v0.9.9.md](Workflow_Studio_Manual_v0.9.9.md)
Binary file not shown.
@@ -2,12 +2,12 @@
ノードが苦手でも大丈夫 ― 画像生成をもっと手軽に、もっと整理して
- 対象バージョン:Workflow Studio v0.9.5
- 対象バージョン:Workflow Studio v0.9.9
- 著者:ketle-man
- © 2026 ketle-man
このマニュアルは、Kindle版をもとにした **画像なしのテキスト版** です。
章ごとのファイルは `chapters/` フォルダ、全章をまとめたファイルは `Workflow_Studio_Manual_v0.9.5.md` です。
章ごとのファイルは `chapters/` フォルダ、全章をまとめたファイルは `Workflow_Studio_Manual_v0.9.9.md` です。
画面の例は省略しているため、実際の画面はWorkflow Studioのヘルプタブとあわせてご覧ください。
---
@@ -143,7 +143,7 @@ ComfyUIの「自由さ」はそのままに、「分かりやすさ」と「手
## 本書の対象バージョンと環境
- **Workflow Studio**:v0.9.5
- **Workflow Studio**:v0.9.9
- **ComfyUI**:本書執筆時点の最新版
- **OS**:Windows(画面の例はWindowsのものです)
@@ -1222,12 +1222,13 @@ Inputサブタブでは、Raw JSONの上端にある **»** ボタンで、Raw J
1枚目が生成できたら、次は **Settings** サブタブで、画像の仕上がりを左右する設定を見てみましょう。
Settingsタブは、左右2つのブロックに分かれています。
Settingsタブは、3つの列に分かれています。
- **KSampler(左)** - 画像をどのように描くかの設定
- **Latent Image(右)** - 画像の大きさと枚数の設定
- **Latent Image(中央)** - 画像の大きさと枚数の設定
- **スイッチ(右、v0.9.6〜)** - ワークフローの中のオン・オフの切り替え
ここでも、値を変えたら各ブロックの **Apply** を押すのを忘れないようにしましょう。
ここでも、KSamplerとLatent Imageは、値を変えたら各ブロックの **Apply** を押すのを忘れないようにしましょう(スイッチの列はApply不要です)。
### KSampler の項目
| 項目 | 意味 | 初心者向けの目安 |
@@ -1254,6 +1255,23 @@ Settingsタブは、左右2つのブロックに分かれています。
> モデルが学習したサイズから大きく外れると、人物が二重になったり、構図が崩れたりしやすくなります。
> 縦長・横長にしたいときも、合計のピクセル数がおおよそ同じになるように調整しましょう(例:SDXLなら 832×1216 など)。
### スイッチの項目(v0.9.6〜)
ワークフローの中にある「オン・オフ」の設定が、ノード名と項目名つきで一覧になります。
チェックボックスをクリックすると、その場で切り替わります。**Apply を押す必要はありません**。切り替えた値は右側の Raw JSON にもすぐ反映されます。
一覧に出るのは、次の2種類です。
- 値が true/false の項目 - プロンプト強化(PE)のオン・オフを切り替えるスイッチや「Boolean」ノード、TextGenerate の thinking など
- 選択肢が2つだけのオン・オフの項目 - enable/disable、on/off、yes/no など。KSamplerAdvanced の add_noise、DWPreprocessor の detect_hand(手を検出するか)など
各行の右端には、いまの値(true/false、enable/disable など)が表示されます。
行にマウスを乗せると、ノードの番号と種類を確認できます。
> **ポイント**
> 別のノードから線でつながっている項目は、一覧に出ません。代わりに、つながっている元のノード(たとえば「Enable PE?」という名前の Boolean ノード)が出るので、そちらを切り替えてください。
> ノードに名前を付けていないワークフローでは、ノードの種類名(ComfySwitchNode など)で表示されます。
> オン・オフの項目が1つも無いワークフローでは「このワークフローに true/false の項目はありません」と表示されます。
### シードの管理(生成パネル)
シードは、生成パネルの **Seed** 欄と、その下のプルダウンで管理します。
@@ -1286,7 +1304,7 @@ Settingsタブは、左右2つのブロックに分かれています。
> **この章(前半)のまとめ**
> - ワークフロータブで選んで「生成UIに読み込み」を押すと、生成UIで編集できる
> - 基本は Input(プロンプト)・Model(モデル)・Settings(細かい設定)の3つのサブタブ
> - モデルやSettingsの値を変えたら必ず **Apply**。押し忘れると変更前の値で生成される(プロンプトは自動で反映)
> - モデルやSettingsの値を変えたら必ず **Apply**。押し忘れると変更前の値で生成される(プロンプトとSettingsのスイッチ列は自動で反映)
> - Generate で生成。Alt+Apply なら反映と生成を一度に行える
> - シードを Fixed にすると、気に入った絵をもとに条件を変えて比べられる。ワークフローのシードをそのまま使うなら Workflow
@@ -1516,21 +1534,140 @@ v0.8.6〜は、こうしたノードを含むワークフローも生成UIで実
| FastH3 OmniRef/Pika-H3-Flex(v0.8.14〜) | MiniMax H3の「参照画像から動画」(Reference to Video)。動画のワークフローとして扱われ、Videoタブ(第11章)でも使える |
| ネイティブLoRAスタック | ComfyUI v0.40のLoRAをまとめて読み込むノード(LoadLoraModel/LoadLoraTextEncoder)。Apply・バイパス・Metadata表示に対応 |
| P-H3 Image(v0.9.4〜) | 画像編集とT2Iのテンプレート。プロンプトはInputタブに表示され、Metadataタブでも読める。ネガティブの欄は無い。幅・高さはワークフローのつなぎ方に従い、プロンプトを補う機能(プロンプトアップサンプラー)もワークフローの設定どおりに動く |
| MiniMax H3 Image(v0.9.6〜) | ComfyUI公式の画像編集とT2Iのテンプレート。P-H3 Imageと同じく、プロンプトはInputタブに表示され、MetadataタブとサイドパネルのIタブでも読める。ネガティブの欄は無い。プロンプト強化(PE)のオン・オフは、Settingsタブのスイッチの列(5-4)で切り替えられる |
> **注意**
> ネイティブLoRAスタックのノードは、保存形式が **API形式のワークフローのみ** 対応です。
> ComfyUI本体の画面がこのノードの行追加にまだ対応していないためで、UI形式のワークフローは対象外です。
> **ポイント**
> 2枚の画像をスライダーで見比べる **Compare Images**(ImageCompare)ノードを含むワークフローは、以前は生成UIから実行すると、このノードだけエラーになっていました(v0.9.6で修正)。
> **ポイント**
> 画像のメタデータを見るMetadataタブとサイドパネルの **I** タブでも、Nano Bananaで作った画像を読めます(v0.8.5〜)。
> プロンプトは、Positive/Negativeに分けずに全文が1件で表示されます。
## 5-10 日本語のままプロンプトを書く(プロンプト翻訳、v0.9.9〜)
多くの画像生成モデルは、英語のプロンプトで学習されています。
**プロンプト翻訳** をオンにすると、プロンプトを日本語で書き、ボタン1つで英語に直せます。
Stable Diffusion web UI(A1111)の翻訳拡張機能と同じように、プロンプトの中の **日本語の部分だけ** を翻訳します。
翻訳には、パソコンで動くAI(**Ollama**)と翻訳専用のモデル **PLaMo 3 Translate** を使います。
インターネット上の翻訳サービスにプロンプトを送ることはありません。
> **事前に必要なもの**
> **Ollama** をインストールして起動しておきます(第10章 10-1)。翻訳モデルのダウンロードは、設定画面からできます(下記)。
### 準備:設定でオンにする
1. **設定** タブの **プロンプト翻訳(自国語 → 英語)** を開きます。
2. **生成UIでプロンプト翻訳を使う** にチェックを入れます。
3. **翻訳モデル** で、使うモデルを選びます。まだOllamaに入っていないモデルには **(未取得)** と付いています。選んで **ダウンロード** を押すと、Ollamaに取り込まれます。
4. **テスト** を押して、見本のプロンプトが英語になれば準備完了です。
PLaMo 3 Translate には4つの大きさがあります。
| モデル | 大きさ | 特徴 |
| --- | --- | --- |
| 0.8B | 約0.6GB | 最速。短いタグの翻訳なら十分 |
| 2B | 約1.8GB | おすすめ。文章の翻訳も正確 |
| 8B | 約5.3GB | より高品質 |
| 31B | 約19.5GB | 最高品質。メモリに余裕がある人向け |
> **ポイント**
> 0.8Bは、文章の翻訳で意味がずれることがあります(例:「カメラは後ろから追いかける」が「her camera following behind」=「彼女のカメラが後ろについてくる」になった)。
> 動画のプロンプトのように文章で書くことが多いなら、2B以上を選びましょう。
設定のほかの項目は、次のとおりです。
| 項目 | 内容 |
| --- | --- |
| 入力する言語 | ふつうは **自動判定** のままでよい。日本語だけで書くなら「日本語」を選ぶと確実 |
| Ollama URL | Ollamaのアドレス。空欄なら `http://localhost:11434` |
| 翻訳モデルの実行場所 | **GPU** か **CPUで常駐** か(下記) |
| 実行時に自動で翻訳する | Generateなどを押したときに自動で翻訳する(下記) |
PLaMo以外のOllamaのモデル(ふだんチャットに使っているモデルなど)も、**その他のOllamaモデル** から選べます。その場合は「英語に翻訳して」という指示を付けて送ります。
### 翻訳モデルをどこで動かすか
| 実行場所 | 1語句あたりの時間 | 向いている人 |
| --- | --- | --- |
| GPU(1回の翻訳が最速・VRAMを使う) | 約0.1秒 | GPUのメモリに余裕がある |
| CPUで常駐(VRAMを使わず、再読み込みの待ちなし) | 約0.2〜0.3秒 | 画像生成にGPUのメモリをすべて使いたい |
GPUのときは、**翻訳後に翻訳モデルをVRAMから解放する** をオンにしておくと、画像生成の前にGPUのメモリを空けられます(初期値はオン)。
ただし、翻訳のたびにモデルの読み込み直しで **約4秒** 待つことになります。
**CPUで常駐** にすると、モデルをメインメモリに置いたままにします(2Bで約2.7GB)。
GPUのメモリを使わず、生成UIにワークフローを読み込んだときに裏で読み込んでおくので、最初の翻訳から待たされません。
> **ポイント**
> Ollamaは、ほかのモデル(AI TOOLのチャット用など)を読み込むときに、常駐させた翻訳モデルを外すことがあります。
> その場合は、次の翻訳のときに自動で読み込み直します(その1回だけ数秒かかります)。
### 使い方:🌐ボタンで翻訳する
プロンプト翻訳をオンにすると、InputタブのPositive Prompt・Negative Promptの **↺** ボタンの右に **🌐** ボタンが表示されます。
1. プロンプト欄に、日本語で書きます。英語のタグと混ぜてもかまいません。
```
masterpiece, 夕焼けの海辺に立つ少女、(長い黒髪:1.2), <lora:test_lora:0.8>, {猫耳|狐耳}, 水彩画風
```
2. **🌐** を押します。
3. 日本語の部分だけが英語になります。
```
masterpiece, A girl standing by the seaside at sunset, (long black hair:1.2), <lora:test_lora:0.8>, {cat ears|fox ears}, watercolor style
```
次のようなプロンプトの書き方(記法)は、翻訳せずにそのまま残します。
- カンマ・改行
- 重み付け `(語句:1.2)`、`[ ]`
- ランダムに選ぶ `{A|B}`、ワイルドカード `__名前__`
- LoRAの指定 `<lora:名前:0.8>`、`embedding:名前`
- `BREAK`、`AND`
翻訳したあとに **原文** ボタンが表示されます。押すと、翻訳する前の文に戻ります(Ctrl+Zでも戻せます)。
> **注意**
> 翻訳されるのは、英数字以外の文字を含む部分だけです。英語だけの部分は、そのまま残ります。
> 反対に、é などのアクセント付きの文字を含む英単語は、翻訳の対象になります。
### 実行時に自動で翻訳する
設定の **実行時に自動で翻訳する** で、どの実行のときに自動で翻訳するかを、チェックボックスで選べます。
| チェックボックス | 動き |
| --- | --- |
| 生成UI - Generate | Generateを押すと、Positive/Negativeの欄の日本語を英語に置き換えてから生成する |
| 生成UI Lab - Run | Labの実行で、送る内容だけを翻訳する。Labのプランには日本語が残る |
| Video Plan - Run | Videoタブ Planの実行で、送るプロンプトだけを翻訳する。プランには日本語が残る |
LabとVideo Planでは、プランを日本語のまま残すので、あとから日本語で書き直せます。
生成した画像や動画の情報(メタデータ)には、実際に送った英語のプロンプトが記録されます。
> **注意**
> 自動翻訳がオンのときに翻訳に失敗すると(Ollamaが起動していないなど)、日本語のまま生成しないよう、生成を止めます。
> LabとVideo Planでは、止まった理由が進行状況の欄に表示されます。
### Labのモーダルと、Video Planでも使える
🌐ボタンは、次の欄にもあります。使い方は同じです。
- Lab(5-7)のプロンプト列のセルをダブルクリックして開く画面の、Positive・Negativeの欄
- Videoタブ Planサブタブの、ブロックの **Prompt** の欄(第11章 11-4)
> **ポイント**
> PLaMo 3 Translate は、個人の利用や趣味、研究などの非商用の利用なら無料で使えます(PLaMoコミュニティライセンス)。
> 商用で使う場合は、ライセンスの条件を確認してください。
> **この章(後半)のまとめ**
> - Batchサブタブで、モデル・プロンプト・サンプラーなどを1種類ずつ変えながらまとめて生成できる
> - Batch Queueの列のチェックで種類を選び、Generateで開始。終わったらチェックを外す
> - Feederは、たくさんの画像を1枚ずつワークフローに送り込む機能。専用ノードが必要
> - Labは、複数の設定を別々のタイミングで切り替えられる上級者向けの機能
> - パートナーAPIノード(Nano Bananaなど)は、`.env` の `COMFY_API_KEY` を設定すれば実行できる。クレジットが消費される
> - プロンプト翻訳をオンにすると、日本語で書いたプロンプトを🌐ボタンで英語にできる。記法はそのまま残り、Generate・Lab・Video Planの実行時に自動で翻訳することもできる
---
@@ -1950,6 +2087,10 @@ Workflow Studioにはたくさんのスタイルが付属しています。
英語のプロンプトの意味を知りたいときは、反対に **EN→JA** で日本語に訳せます。
> **ポイント**
> 生成UIのプロンプト欄で直接翻訳したいときは、第5章 5-10 の **プロンプト翻訳**(🌐ボタン)が便利です。重み付けやLoRAの指定を残したまま、日本語の部分だけを英語にできます。
> AI Assistantの翻訳にも、翻訳専用のモデル **PLaMo 3 Translate** を選べます(v0.9.9〜。以前は指示文まで翻訳されたり、ZH→ENで日本語が出たりしていました)。
### AIと相談しながらプロンプトを作る
入力欄に質問を書いて **Send**(または Enter キー)を押すと、AIとチャットできます。
@@ -2599,6 +2740,11 @@ Workflow Studio本体の **AI TOOL** タブを開きます。
> 小さなモデルは、翻訳せずに元の文章をそのまま返してしまうことがあります。
> その場合は「モデルが原文をそのまま返しました」と表示されるので、別のモデルを試してください。
> **ポイント**
> 翻訳専用のモデル **PLaMo 3 Translate**(第5章 5-10)を、設定のモデルに選ぶこともできます(v0.9.9〜)。
> 日本語・英語・中国語のほか、Free(自由入力)では French・フランス語・français のような言語名で、PLaMoが対応する言語を指定できます。PLaMoが対応していない言語(ポルトガル語など)を翻訳先にすると、エラーになります。
> WS LibraryのAタブの翻訳も同じです。
### Chat
入力欄に話しかけて、**Enter** で送信します(**Shift+Enter** で改行)。
会話の流れを覚えているので、「もっと明るい雰囲気にして」のような続けての依頼もできます。
@@ -3085,6 +3231,14 @@ Image Editのいちばん便利な使い方が、**インペイント** です
そのほか、動画の好きな場面を静止画として保存する **Frame** や、動画をGIFアニメに変換する **GIF** の機能もあります。
> **ポイント**
> Planサブタブのブロックの **Prompt** の欄にも、プロンプト翻訳の **🌐** ボタンがあります(第5章 5-10)。
> 設定の **Video Plan - Run** をオンにすると、日本語のプロンプトのまま実行して、送る内容だけを英語にできます。
> **注意**
> v0.9.8までは、新しいComfyUIで保存したワークフローを使うと、Planで入力したプロンプト・長さ(Duration)・Turboの設定が反映されず、ワークフローに保存されている値のまま生成されていました(v0.9.9で修正)。
> 以前と同じPlanを実行しても、Planの値が反映されるようになったため、結果が変わることがあります。
### アセット登録と音声変換(v0.8.9〜)
- **アセット登録** - 左側の動画ソースパネルのボタンです。パソコン内の動画を、outputフォルダ(`output/video`)へコピーして「Video Assets」グループに登録します。
登録すると、Assetサブタブの一覧に出て、Editなどで使えるようになります。
@@ -3135,6 +3289,7 @@ Image Editのいちばん便利な使い方が、**インペイント** です
| CivitAI APIキー | Civitaiの情報取得用(省略可) | 第6章 |
| Eagle 連携 (オプション) | 生成画像をEagleに自動保存 | 第8章 |
| VRAM調整(Ollama) | 生成前にAIモデルをメモリから降ろす | 第10章 |
| プロンプト翻訳(自国語 → 英語) | 日本語のプロンプトを🌐ボタンや実行時の自動翻訳で英語にする | 第5章 5-10 |
| データ管理 | 設定やデータのバックアップ・復元 | 下記 |
### デフォルトワークフロー
@@ -3358,6 +3513,7 @@ Workflow Studioを更新(アップデート)したあとは、必ずComfyUI
### 設定を変えたのに、結果が変わらない
いちばん多い原因は、**Apply の押し忘れ** です。
生成UIでは、モデルやSettings(ステップ数・サイズなど)の値を変えたあと、**Apply** を押さないとワークフローに反映されません(第5章 5-3)。
ただし、Settingsタブの右端のスイッチの列は、クリックした時点で反映されます(第5章 5-4)。
右側の **Raw JSON** を見ると、実際に反映されている値を確認できます。
### 読み込んだワークフローとは違う内容で生成される
@@ -3373,6 +3529,15 @@ Batchサブタブを開き、Batch Queueの列のチェックをすべてオフ
いつも同じモデルを使いたい場合は、設定タブの **デフォルトCheckpoint** を使うと便利です(第11章 11-5)。
### プロンプト翻訳の🌐ボタンが出ない・翻訳できない
| 症状 | 原因 | 対処 |
| --- | --- | --- |
| 🌐ボタンが表示されない | プロンプト翻訳がオフになっている | 設定タブの **プロンプト翻訳(自国語 → 英語)** で **生成UIでプロンプト翻訳を使う** にチェックを入れる(第5章 5-10) |
| 「翻訳エラー」と出る | Ollamaが起動していない、または翻訳モデルがOllamaに無い | Ollamaを起動する。設定の **翻訳モデル** に **(未取得)** と出ていれば **ダウンロード** を押す |
| 押しても何も変わらない(「翻訳する部分がありません」) | 英語(英数字)だけのプロンプトになっている | 翻訳されるのは英数字以外の文字を含む部分だけ。英語の部分はそのまま残る |
| 自動翻訳がオンのとき、生成が始まらない | 翻訳に失敗したので、日本語のまま生成しないよう止めた | 上の「翻訳エラー」と同じ対処をする。LabとVideo Planでは、理由が進行状況の欄に出る |
| 訳がおかしい | 小さいモデル(0.8B)は、文章で意味がずれることがある | 設定の **翻訳モデル** で2B以上を選ぶ |
### 同じ設定なのに、同じ画像にならない
生成パネルのシードのモードが **Random** になっていませんか?
同じ画像を作るには、シードを **Workflow**(ワークフローのシードをそのまま使う)か **Fixed**(Seed欄の値で固定)にする必要があります。
@@ -3572,6 +3737,7 @@ Workflow Studioの **ヘルプ** タブにも、トラブルシューティン
| LoRA | 絵柄やキャラクターなどを追加で覚えさせる小さなモデル | 第6章 |
| OCR | 画像に写っている文字を読み取ること | 第10章 |
| Ollama | ローカルAIを手軽に動かせるソフト | 第10章 |
| PLaMo 3 Translate | Preferred Networks(PFN)の翻訳専用AIモデル。プロンプト翻訳で使う | 第5章 |
| T2I(Text to Image) | 文章(プロンプト)から画像を作ること | 第5章 |
| TagSpace | タグ・プロンプト・生成画像を意味の近さで3Dの空間に並べて表示するタブ | 第11章 |
| VAE | 画像の色合いや細部を仕上げるモデル | 第6章 |
@@ -3645,6 +3811,7 @@ Workflow Studioの **ヘルプ** タブにも、トラブルシューティン
| --- | --- | --- |
| Civitai(https://civitai.com/) | モデルの入手・情報取得 | 第6章 |
| Ollama(https://ollama.com/) | ローカルAI | 第10章 |
| PLaMo 3 Translate(https://huggingface.co/pfnet/plamo-3-translate-2b-GGUF) | プロンプト翻訳の翻訳モデル | 第5章 |
| LM Studio(https://lmstudio.ai/) | ローカルAI | 第10章 |
| Lemonade Server(https://lemonade-server.ai/) | ローカルAI | 第10章 |
| Eagle(https://eagle.cool/) | 画像管理ソフト | 第8章 |
Binary file not shown.
+1 -1
View File
@@ -129,7 +129,7 @@ ComfyUIの「自由さ」はそのままに、「分かりやすさ」と「手
## 本書の対象バージョンと環境
- **Workflow Studio**:v0.9.5
- **Workflow Studio**:v0.9.9
- **ComfyUI**:本書執筆時点の最新版
- **OS**:Windows(画面の例はWindowsのものです)
@@ -168,12 +168,13 @@ Inputサブタブでは、Raw JSONの上端にある **»** ボタンで、Raw J
1枚目が生成できたら、次は **Settings** サブタブで、画像の仕上がりを左右する設定を見てみましょう。
Settingsタブは、左右2つのブロックに分かれています。
Settingsタブは、3つの列に分かれています。
- **KSampler(左)** - 画像をどのように描くかの設定
- **Latent Image(右)** - 画像の大きさと枚数の設定
- **Latent Image(中央)** - 画像の大きさと枚数の設定
- **スイッチ(右、v0.9.6〜)** - ワークフローの中のオン・オフの切り替え
ここでも、値を変えたら各ブロックの **Apply** を押すのを忘れないようにしましょう。
ここでも、KSamplerとLatent Imageは、値を変えたら各ブロックの **Apply** を押すのを忘れないようにしましょう(スイッチの列はApply不要です)。
### KSampler の項目
| 項目 | 意味 | 初心者向けの目安 |
@@ -200,6 +201,23 @@ Settingsタブは、左右2つのブロックに分かれています。
> モデルが学習したサイズから大きく外れると、人物が二重になったり、構図が崩れたりしやすくなります。
> 縦長・横長にしたいときも、合計のピクセル数がおおよそ同じになるように調整しましょう(例:SDXLなら 832×1216 など)。
### スイッチの項目(v0.9.6〜)
ワークフローの中にある「オン・オフ」の設定が、ノード名と項目名つきで一覧になります。
チェックボックスをクリックすると、その場で切り替わります。**Apply を押す必要はありません**。切り替えた値は右側の Raw JSON にもすぐ反映されます。
一覧に出るのは、次の2種類です。
- 値が true/false の項目 - プロンプト強化(PE)のオン・オフを切り替えるスイッチや「Boolean」ノード、TextGenerate の thinking など
- 選択肢が2つだけのオン・オフの項目 - enable/disable、on/off、yes/no など。KSamplerAdvanced の add_noise、DWPreprocessor の detect_hand(手を検出するか)など
各行の右端には、いまの値(true/false、enable/disable など)が表示されます。
行にマウスを乗せると、ノードの番号と種類を確認できます。
> **ポイント**
> 別のノードから線でつながっている項目は、一覧に出ません。代わりに、つながっている元のノード(たとえば「Enable PE?」という名前の Boolean ノード)が出るので、そちらを切り替えてください。
> ノードに名前を付けていないワークフローでは、ノードの種類名(ComfySwitchNode など)で表示されます。
> オン・オフの項目が1つも無いワークフローでは「このワークフローに true/false の項目はありません」と表示されます。
### シードの管理(生成パネル)
シードは、生成パネルの **Seed** 欄と、その下のプルダウンで管理します。
@@ -232,6 +250,6 @@ Settingsタブは、左右2つのブロックに分かれています。
> **この章(前半)のまとめ**
> - ワークフロータブで選んで「生成UIに読み込み」を押すと、生成UIで編集できる
> - 基本は Input(プロンプト)・Model(モデル)・Settings(細かい設定)の3つのサブタブ
> - モデルやSettingsの値を変えたら必ず **Apply**。押し忘れると変更前の値で生成される(プロンプトは自動で反映)
> - モデルやSettingsの値を変えたら必ず **Apply**。押し忘れると変更前の値で生成される(プロンプトとSettingsのスイッチ列は自動で反映)
> - Generate で生成。Alt+Apply なら反映と生成を一度に行える
> - シードを Fixed にすると、気に入った絵をもとに条件を変えて比べられる。ワークフローのシードをそのまま使うなら Workflow
@@ -222,18 +222,137 @@ v0.8.6〜は、こうしたノードを含むワークフローも生成UIで実
| FastH3 OmniRef/Pika-H3-Flex(v0.8.14〜) | MiniMax H3の「参照画像から動画」(Reference to Video)。動画のワークフローとして扱われ、Videoタブ(第11章)でも使える |
| ネイティブLoRAスタック | ComfyUI v0.40のLoRAをまとめて読み込むノード(LoadLoraModel/LoadLoraTextEncoder)。Apply・バイパス・Metadata表示に対応 |
| P-H3 Image(v0.9.4〜) | 画像編集とT2Iのテンプレート。プロンプトはInputタブに表示され、Metadataタブでも読める。ネガティブの欄は無い。幅・高さはワークフローのつなぎ方に従い、プロンプトを補う機能(プロンプトアップサンプラー)もワークフローの設定どおりに動く |
| MiniMax H3 Image(v0.9.6〜) | ComfyUI公式の画像編集とT2Iのテンプレート。P-H3 Imageと同じく、プロンプトはInputタブに表示され、MetadataタブとサイドパネルのIタブでも読める。ネガティブの欄は無い。プロンプト強化(PE)のオン・オフは、Settingsタブのスイッチの列(5-4)で切り替えられる |
> **注意**
> ネイティブLoRAスタックのノードは、保存形式が **API形式のワークフローのみ** 対応です。
> ComfyUI本体の画面がこのノードの行追加にまだ対応していないためで、UI形式のワークフローは対象外です。
> **ポイント**
> 2枚の画像をスライダーで見比べる **Compare Images**(ImageCompare)ノードを含むワークフローは、以前は生成UIから実行すると、このノードだけエラーになっていました(v0.9.6で修正)。
> **ポイント**
> 画像のメタデータを見るMetadataタブとサイドパネルの **I** タブでも、Nano Bananaで作った画像を読めます(v0.8.5〜)。
> プロンプトは、Positive/Negativeに分けずに全文が1件で表示されます。
## 5-10 日本語のままプロンプトを書く(プロンプト翻訳、v0.9.9〜)
多くの画像生成モデルは、英語のプロンプトで学習されています。
**プロンプト翻訳** をオンにすると、プロンプトを日本語で書き、ボタン1つで英語に直せます。
Stable Diffusion web UI(A1111)の翻訳拡張機能と同じように、プロンプトの中の **日本語の部分だけ** を翻訳します。
翻訳には、パソコンで動くAI(**Ollama**)と翻訳専用のモデル **PLaMo 3 Translate** を使います。
インターネット上の翻訳サービスにプロンプトを送ることはありません。
> **事前に必要なもの**
> **Ollama** をインストールして起動しておきます(第10章 10-1)。翻訳モデルのダウンロードは、設定画面からできます(下記)。
### 準備:設定でオンにする
1. **設定** タブの **プロンプト翻訳(自国語 → 英語)** を開きます。
2. **生成UIでプロンプト翻訳を使う** にチェックを入れます。
3. **翻訳モデル** で、使うモデルを選びます。まだOllamaに入っていないモデルには **(未取得)** と付いています。選んで **ダウンロード** を押すと、Ollamaに取り込まれます。
4. **テスト** を押して、見本のプロンプトが英語になれば準備完了です。
PLaMo 3 Translate には4つの大きさがあります。
| モデル | 大きさ | 特徴 |
| --- | --- | --- |
| 0.8B | 約0.6GB | 最速。短いタグの翻訳なら十分 |
| 2B | 約1.8GB | おすすめ。文章の翻訳も正確 |
| 8B | 約5.3GB | より高品質 |
| 31B | 約19.5GB | 最高品質。メモリに余裕がある人向け |
> **ポイント**
> 0.8Bは、文章の翻訳で意味がずれることがあります(例:「カメラは後ろから追いかける」が「her camera following behind」=「彼女のカメラが後ろについてくる」になった)。
> 動画のプロンプトのように文章で書くことが多いなら、2B以上を選びましょう。
設定のほかの項目は、次のとおりです。
| 項目 | 内容 |
| --- | --- |
| 入力する言語 | ふつうは **自動判定** のままでよい。日本語だけで書くなら「日本語」を選ぶと確実 |
| Ollama URL | Ollamaのアドレス。空欄なら `http://localhost:11434` |
| 翻訳モデルの実行場所 | **GPU** か **CPUで常駐** か(下記) |
| 実行時に自動で翻訳する | Generateなどを押したときに自動で翻訳する(下記) |
PLaMo以外のOllamaのモデル(ふだんチャットに使っているモデルなど)も、**その他のOllamaモデル** から選べます。その場合は「英語に翻訳して」という指示を付けて送ります。
### 翻訳モデルをどこで動かすか
| 実行場所 | 1語句あたりの時間 | 向いている人 |
| --- | --- | --- |
| GPU(1回の翻訳が最速・VRAMを使う) | 約0.1秒 | GPUのメモリに余裕がある |
| CPUで常駐(VRAMを使わず、再読み込みの待ちなし) | 約0.2〜0.3秒 | 画像生成にGPUのメモリをすべて使いたい |
GPUのときは、**翻訳後に翻訳モデルをVRAMから解放する** をオンにしておくと、画像生成の前にGPUのメモリを空けられます(初期値はオン)。
ただし、翻訳のたびにモデルの読み込み直しで **約4秒** 待つことになります。
**CPUで常駐** にすると、モデルをメインメモリに置いたままにします(2Bで約2.7GB)。
GPUのメモリを使わず、生成UIにワークフローを読み込んだときに裏で読み込んでおくので、最初の翻訳から待たされません。
> **ポイント**
> Ollamaは、ほかのモデル(AI TOOLのチャット用など)を読み込むときに、常駐させた翻訳モデルを外すことがあります。
> その場合は、次の翻訳のときに自動で読み込み直します(その1回だけ数秒かかります)。
### 使い方:🌐ボタンで翻訳する
プロンプト翻訳をオンにすると、InputタブのPositive Prompt・Negative Promptの **↺** ボタンの右に **🌐** ボタンが表示されます。
1. プロンプト欄に、日本語で書きます。英語のタグと混ぜてもかまいません。
```
masterpiece, 夕焼けの海辺に立つ少女、(長い黒髪:1.2), <lora:test_lora:0.8>, {猫耳|狐耳}, 水彩画風
```
2. **🌐** を押します。
3. 日本語の部分だけが英語になります。
```
masterpiece, A girl standing by the seaside at sunset, (long black hair:1.2), <lora:test_lora:0.8>, {cat ears|fox ears}, watercolor style
```
次のようなプロンプトの書き方(記法)は、翻訳せずにそのまま残します。
- カンマ・改行
- 重み付け `(語句:1.2)`、`[ ]`
- ランダムに選ぶ `{A|B}`、ワイルドカード `__名前__`
- LoRAの指定 `<lora:名前:0.8>`、`embedding:名前`
- `BREAK`、`AND`
翻訳したあとに **原文** ボタンが表示されます。押すと、翻訳する前の文に戻ります(Ctrl+Zでも戻せます)。
> **注意**
> 翻訳されるのは、英数字以外の文字を含む部分だけです。英語だけの部分は、そのまま残ります。
> 反対に、é などのアクセント付きの文字を含む英単語は、翻訳の対象になります。
### 実行時に自動で翻訳する
設定の **実行時に自動で翻訳する** で、どの実行のときに自動で翻訳するかを、チェックボックスで選べます。
| チェックボックス | 動き |
| --- | --- |
| 生成UI - Generate | Generateを押すと、Positive/Negativeの欄の日本語を英語に置き換えてから生成する |
| 生成UI Lab - Run | Labの実行で、送る内容だけを翻訳する。Labのプランには日本語が残る |
| Video Plan - Run | Videoタブ Planの実行で、送るプロンプトだけを翻訳する。プランには日本語が残る |
LabとVideo Planでは、プランを日本語のまま残すので、あとから日本語で書き直せます。
生成した画像や動画の情報(メタデータ)には、実際に送った英語のプロンプトが記録されます。
> **注意**
> 自動翻訳がオンのときに翻訳に失敗すると(Ollamaが起動していないなど)、日本語のまま生成しないよう、生成を止めます。
> LabとVideo Planでは、止まった理由が進行状況の欄に表示されます。
### Labのモーダルと、Video Planでも使える
🌐ボタンは、次の欄にもあります。使い方は同じです。
- Lab(5-7)のプロンプト列のセルをダブルクリックして開く画面の、Positive・Negativeの欄
- Videoタブ Planサブタブの、ブロックの **Prompt** の欄(第11章 11-4)
> **ポイント**
> PLaMo 3 Translate は、個人の利用や趣味、研究などの非商用の利用なら無料で使えます(PLaMoコミュニティライセンス)。
> 商用で使う場合は、ライセンスの条件を確認してください。
> **この章(後半)のまとめ**
> - Batchサブタブで、モデル・プロンプト・サンプラーなどを1種類ずつ変えながらまとめて生成できる
> - Batch Queueの列のチェックで種類を選び、Generateで開始。終わったらチェックを外す
> - Feederは、たくさんの画像を1枚ずつワークフローに送り込む機能。専用ノードが必要
> - Labは、複数の設定を別々のタイミングで切り替えられる上級者向けの機能
> - パートナーAPIノード(Nano Bananaなど)は、`.env` の `COMFY_API_KEY` を設定すれば実行できる。クレジットが消費される
> - プロンプト翻訳をオンにすると、日本語で書いたプロンプトを🌐ボタンで英語にできる。記法はそのまま残り、Generate・Lab・Video Planの実行時に自動で翻訳することもできる
@@ -227,6 +227,10 @@ Workflow Studioにはたくさんのスタイルが付属しています。
英語のプロンプトの意味を知りたいときは、反対に **EN→JA** で日本語に訳せます。
> **ポイント**
> 生成UIのプロンプト欄で直接翻訳したいときは、第5章 5-10 の **プロンプト翻訳**(🌐ボタン)が便利です。重み付けやLoRAの指定を残したまま、日本語の部分だけを英語にできます。
> AI Assistantの翻訳にも、翻訳専用のモデル **PLaMo 3 Translate** を選べます(v0.9.9〜。以前は指示文まで翻訳されたり、ZH→ENで日本語が出たりしていました)。
### AIと相談しながらプロンプトを作る
入力欄に質問を書いて **Send**(または Enter キー)を押すと、AIとチャットできます。
@@ -110,6 +110,11 @@ Workflow Studio本体の **AI TOOL** タブを開きます。
> 小さなモデルは、翻訳せずに元の文章をそのまま返してしまうことがあります。
> その場合は「モデルが原文をそのまま返しました」と表示されるので、別のモデルを試してください。
> **ポイント**
> 翻訳専用のモデル **PLaMo 3 Translate**(第5章 5-10)を、設定のモデルに選ぶこともできます(v0.9.9〜)。
> 日本語・英語・中国語のほか、Free(自由入力)では French・フランス語・français のような言語名で、PLaMoが対応する言語を指定できます。PLaMoが対応していない言語(ポルトガル語など)を翻訳先にすると、エラーになります。
> WS LibraryのAタブの翻訳も同じです。
### Chat
入力欄に話しかけて、**Enter** で送信します(**Shift+Enter** で改行)。
会話の流れを覚えているので、「もっと明るい雰囲気にして」のような続けての依頼もできます。
@@ -155,6 +155,14 @@ Image Editのいちばん便利な使い方が、**インペイント** です
そのほか、動画の好きな場面を静止画として保存する **Frame** や、動画をGIFアニメに変換する **GIF** の機能もあります。
> **ポイント**
> Planサブタブのブロックの **Prompt** の欄にも、プロンプト翻訳の **🌐** ボタンがあります(第5章 5-10)。
> 設定の **Video Plan - Run** をオンにすると、日本語のプロンプトのまま実行して、送る内容だけを英語にできます。
> **注意**
> v0.9.8までは、新しいComfyUIで保存したワークフローを使うと、Planで入力したプロンプト・長さ(Duration)・Turboの設定が反映されず、ワークフローに保存されている値のまま生成されていました(v0.9.9で修正)。
> 以前と同じPlanを実行しても、Planの値が反映されるようになったため、結果が変わることがあります。
### アセット登録と音声変換(v0.8.9〜)
- **アセット登録** - 左側の動画ソースパネルのボタンです。パソコン内の動画を、outputフォルダ(`output/video`)へコピーして「Video Assets」グループに登録します。
登録すると、Assetサブタブの一覧に出て、Editなどで使えるようになります。
@@ -205,6 +213,7 @@ Image Editのいちばん便利な使い方が、**インペイント** です
| CivitAI APIキー | Civitaiの情報取得用(省略可) | 第6章 |
| Eagle 連携 (オプション) | 生成画像をEagleに自動保存 | 第8章 |
| VRAM調整(Ollama) | 生成前にAIモデルをメモリから降ろす | 第10章 |
| プロンプト翻訳(自国語 → 英語) | 日本語のプロンプトを🌐ボタンや実行時の自動翻訳で英語にする | 第5章 5-10 |
| データ管理 | 設定やデータのバックアップ・復元 | 下記 |
### デフォルトワークフロー
@@ -72,6 +72,7 @@ Workflow Studioを更新(アップデート)したあとは、必ずComfyUI
### 設定を変えたのに、結果が変わらない
いちばん多い原因は、**Apply の押し忘れ** です。
生成UIでは、モデルやSettings(ステップ数・サイズなど)の値を変えたあと、**Apply** を押さないとワークフローに反映されません(第5章 5-3)。
ただし、Settingsタブの右端のスイッチの列は、クリックした時点で反映されます(第5章 5-4)。
右側の **Raw JSON** を見ると、実際に反映されている値を確認できます。
### 読み込んだワークフローとは違う内容で生成される
@@ -87,6 +88,15 @@ Batchサブタブを開き、Batch Queueの列のチェックをすべてオフ
いつも同じモデルを使いたい場合は、設定タブの **デフォルトCheckpoint** を使うと便利です(第11章 11-5)。
### プロンプト翻訳の🌐ボタンが出ない・翻訳できない
| 症状 | 原因 | 対処 |
| --- | --- | --- |
| 🌐ボタンが表示されない | プロンプト翻訳がオフになっている | 設定タブの **プロンプト翻訳(自国語 → 英語)** で **生成UIでプロンプト翻訳を使う** にチェックを入れる(第5章 5-10) |
| 「翻訳エラー」と出る | Ollamaが起動していない、または翻訳モデルがOllamaに無い | Ollamaを起動する。設定の **翻訳モデル** に **(未取得)** と出ていれば **ダウンロード** を押す |
| 押しても何も変わらない(「翻訳する部分がありません」) | 英語(英数字)だけのプロンプトになっている | 翻訳されるのは英数字以外の文字を含む部分だけ。英語の部分はそのまま残る |
| 自動翻訳がオンのとき、生成が始まらない | 翻訳に失敗したので、日本語のまま生成しないよう止めた | 上の「翻訳エラー」と同じ対処をする。LabとVideo Planでは、理由が進行状況の欄に出る |
| 訳がおかしい | 小さいモデル(0.8B)は、文章で意味がずれることがある | 設定の **翻訳モデル** で2B以上を選ぶ |
### 同じ設定なのに、同じ画像にならない
生成パネルのシードのモードが **Random** になっていませんか?
同じ画像を作るには、シードを **Workflow**(ワークフローのシードをそのまま使う)か **Fixed**(Seed欄の値で固定)にする必要があります。
+2
View File
@@ -54,6 +54,7 @@
| LoRA | 絵柄やキャラクターなどを追加で覚えさせる小さなモデル | 第6章 |
| OCR | 画像に写っている文字を読み取ること | 第10章 |
| Ollama | ローカルAIを手軽に動かせるソフト | 第10章 |
| PLaMo 3 Translate | Preferred Networks(PFN)の翻訳専用AIモデル。プロンプト翻訳で使う | 第5章 |
| T2I(Text to Image) | 文章(プロンプト)から画像を作ること | 第5章 |
| TagSpace | タグ・プロンプト・生成画像を意味の近さで3Dの空間に並べて表示するタブ | 第11章 |
| VAE | 画像の色合いや細部を仕上げるモデル | 第6章 |
@@ -127,6 +128,7 @@
| --- | --- | --- |
| Civitai(https://civitai.com/) | モデルの入手・情報取得 | 第6章 |
| Ollama(https://ollama.com/) | ローカルAI | 第10章 |
| PLaMo 3 Translate(https://huggingface.co/pfnet/plamo-3-translate-2b-GGUF) | プロンプト翻訳の翻訳モデル | 第5章 |
| LM Studio(https://lmstudio.ai/) | ローカルAI | 第10章 |
| Lemonade Server(https://lemonade-server.ai/) | ローカルAI | 第10章 |
| Eagle(https://eagle.cool/) | 画像管理ソフト | 第8章 |
+3 -3
View File
@@ -2,12 +2,12 @@
Don't Be Afraid of Nodes - Generate Images More Easily and Keep Them Organized
- Covers: Workflow Studio v0.9.5
- Covers: Workflow Studio v0.9.9
- Author: ketle-man
- (c) 2026 ketle-man
This is the **text-only edition** (without screenshots) of the Kindle book.
Chapter files are in the `chapters/` folder; the whole book in one file is `Workflow_Studio_Manual_v0.9.5_en.md`.
Chapter files are in the `chapters/` folder; the whole book in one file is `Workflow_Studio_Manual_v0.9.9_en.md`.
Screen examples are omitted, so please use the book together with Workflow Studio's Help tab.
## Contents
@@ -29,4 +29,4 @@ Screen examples are omitted, so please use the book together with Workflow Studi
- [13_Appendix](chapters/13_Appendix.md)
- [14_Afterword](chapters/14_Afterword.md)
Whole book: [Workflow_Studio_Manual_v0.9.5_en.md](Workflow_Studio_Manual_v0.9.5_en.md)
Whole book: [Workflow_Studio_Manual_v0.9.9_en.md](Workflow_Studio_Manual_v0.9.9_en.md)
Binary file not shown.
@@ -2,12 +2,12 @@
Don't Be Afraid of Nodes - Generate Images More Easily and Keep Them Organized
- Covers: Workflow Studio v0.9.5
- Covers: Workflow Studio v0.9.9
- Author: ketle-man
- (c) 2026 ketle-man
This is the **text-only edition** (without screenshots) of the Kindle book.
Chapter files are in the `chapters/` folder; the whole book in one file is `Workflow_Studio_Manual_v0.9.5_en.md`.
Chapter files are in the `chapters/` folder; the whole book in one file is `Workflow_Studio_Manual_v0.9.9_en.md`.
Screen examples are omitted, so please use the book together with Workflow Studio's Help tab.
---
@@ -143,7 +143,7 @@ It is written so that you can follow it if you know basic computer operations (c
## Version and environment covered
- **Workflow Studio**: v0.9.5
- **Workflow Studio**: v0.9.9
- **ComfyUI**: the latest version at the time of writing
- **OS**: Windows (screen examples are from Windows)
@@ -1220,12 +1220,13 @@ If you save it with a name, it is added as a new workflow in the Workflow tab.
Once the first image is out, next look at the **Settings** subtab, where settings that decide how the result turns out are found.
The Settings tab is divided into two blocks, left and right.
The Settings tab is divided into three columns.
- **KSampler (left)** - settings for how the image is drawn
- **Latent Image (right)** - settings for the size and number of images
- **Latent Image (middle)** - settings for the size and number of images
- **Switches (right, v0.9.6+)** - the on/off settings inside the workflow
Here too, don't forget to press **Apply** on each block after changing values.
Here too, for KSampler and Latent Image, don't forget to press **Apply** on each block after changing values (the Switches column needs no Apply).
### KSampler items
| Item | Meaning | Rule of thumb for beginners |
@@ -1252,6 +1253,23 @@ Here too, don't forget to press **Apply** on each block after changing values.
> If you stray far from the size the model learned, people tend to come out doubled or the composition falls apart.
> Even when you want portrait or landscape, adjust so the total pixel count stays about the same (for example 832x1216 for SDXL).
### Switches items (v0.9.6+)
The "on/off" settings inside the workflow are listed with their node name and item name.
Clicking a checkbox switches it right away. **You don't need to press Apply**. The new value also shows up in the Raw JSON on the right at once.
Two kinds of items are listed:
- Items whose value is true/false - for example the switch that turns the prompt enhancer (PE) on and off, "Boolean" nodes, and TextGenerate's thinking
- On/off items with only two choices - enable/disable, on/off, yes/no and so on. For example KSamplerAdvanced's add_noise and DWPreprocessor's detect_hand (whether to detect hands)
The right end of each row shows the current value (true/false, enable/disable and so on).
Hover over a row to see the node's number and type.
> **Tip**
> Items wired from another node are not listed. Instead, the source node they are wired from (for example a Boolean node named "Enable PE?") is listed, so switch that one.
> In workflows where nodes have no names, the node type name (such as ComfySwitchNode) is shown.
> If the workflow has no on/off items at all, "No true/false settings in this workflow" is shown.
### Managing the seed (generate panel)
The seed is managed with the **Seed** field in the generate panel and the pull-down below it.
@@ -1284,7 +1302,7 @@ This is the shortest path to getting used to image generation.
> **Summary of this chapter (first half)**
> - Select in the Workflow tab and press "Load in GenerateUI" to edit in GenerateUI
> - The basics are the three subtabs Input (prompt), Model (model) and Settings (detailed settings)
> - After changing the model or Settings values, always press **Apply**. If you forget, it generates with the values from before the change (prompts are reflected automatically)
> - After changing the model or Settings values, always press **Apply**. If you forget, it generates with the values from before the change (prompts and the Settings Switches column are reflected automatically)
> - Generate to produce. Alt + Apply reflects and generates in one go
> - If you set the seed to Fixed, you can keep a picture you like and compare by changing conditions. To use the workflow's seed as it is, use Workflow
@@ -1511,21 +1529,138 @@ The workflows you can use in GenerateUI have increased as follows. They are used
- **FastH3 OmniRef / Pika-H3-Flex** (v0.8.14 and later) - MiniMax H3 reference to video. Treated as video workflows, and also usable in the Video tab (Chapter 11).
- **Native LoRA stack** - ComfyUI v0.40's nodes that load several LoRAs at once (LoadLoraModel / LoadLoraTextEncoder). Apply, bypass and the Metadata display are supported.
- **P-H3 Image** (v0.9.4+) - the image-edit and T2I templates. The prompt is shown on the Input tab and can be read by the Metadata tab; there is no negative prompt field. Width and height follow the workflow's wiring, and the prompt upsampler runs as set in the workflow.
- **MiniMax H3 Image** (v0.9.6+) - ComfyUI's official image-edit and T2I templates. As with P-H3 Image, the prompt is shown on the Input tab and can be read by the Metadata tab and the sidepanel I tab; there is no negative prompt field. The prompt enhancer (PE) can be turned on and off in the Settings tab's Switches column (5-4).
> **Caution**
> The native LoRA stack nodes are supported **only in API-format workflows**.
> ComfyUI's own screen cannot yet add rows to this node, so UI-format workflows are not covered.
> **Tip**
> Workflows containing the **Compare Images** (ImageCompare) node, which compares two images with a slider, used to fail on that node when run from GenerateUI (fixed in v0.9.6).
> **Tip**
> The Metadata tab and the side panel's **I** tab can also read images made with Nano Banana (v0.8.5 and later).
> The prompt appears as one whole entry, not split into Positive/Negative.
## 5-10 Writing prompts in your own language (Prompt Translation, v0.9.9 and later)
Most image generation models were trained on English prompts.
With **Prompt Translation** turned on, you can write a prompt in your own language and turn it into English with one button.
Like the translation extensions for Stable Diffusion web UI (A1111), it translates **only the parts written in your own language**.
Translation uses AI running on your PC (**Ollama**) and the translation model **PLaMo 3 Translate**, which supports 51 languages besides English (Japanese, Chinese, Korean, French, German, Spanish, Russian, Thai, Vietnamese and more).
Your prompts are not sent to an online translation service.
> **What you need first**
> Install and start **Ollama** (Chapter 10, section 10-1). The translation model can be downloaded from the Settings screen (see below).
### Preparation: turn it on in Settings
1. In the **Settings** tab, open **Prompt Translation (native language -> English)**.
2. Check **Enable prompt translation in GenerateUI**.
3. Choose the model in **Translation model**. Models not yet in Ollama are marked **(not downloaded)**; choose one and press **Download** to pull it into Ollama.
4. Press **Test**. When the sample prompt comes back in English, you are ready.
PLaMo 3 Translate comes in four sizes.
| Model | Size | Notes |
| --- | --- | --- |
| 0.8B | about 0.6 GB | Fastest. Enough for short tags |
| 2B | about 1.8 GB | Recommended. Translates sentences accurately too |
| 8B | about 5.3 GB | Higher quality |
| 31B | about 19.5 GB | Highest quality, for PCs with plenty of memory |
> **Tip**
> 0.8B sometimes shifts the meaning of a sentence (for example, "the camera follows from behind" in Japanese became "her camera following behind").
> If you often write prompts as sentences, such as video prompts, choose 2B or larger.
The other settings are:
| Setting | What it does |
| --- | --- |
| Source language | Usually leave it at **Auto-detect**. If you always write in one language, choosing it makes detection certain |
| Ollama URL | Ollama's address. Blank means `http://localhost:11434` |
| Where to run the translation model | **GPU** or **CPU, kept loaded** (see below) |
| Translate automatically when running | Translates automatically when you press Generate and so on (see below) |
Other Ollama models (such as the one you use for chat) can also be chosen under **Other Ollama models**. They are sent the text with a "translate into English" instruction.
### Where to run the translation model
| Where | Time per phrase | Good for |
| --- | --- | --- |
| GPU (fastest per translation, uses VRAM) | about 0.1 s | GPUs with VRAM to spare |
| CPU, kept loaded (no VRAM, no reload wait) | about 0.2-0.3 s | Keeping all VRAM for image generation |
On GPU, turning on **Unload the translation model from VRAM after translating** frees VRAM before image generation (on by default).
However, every translation then waits **about 4 seconds** while the model loads again.
**CPU, kept loaded** keeps the model in system RAM (about 2.7 GB for 2B).
It uses no VRAM and is loaded in the background when a workflow is loaded into GenerateUI, so even the first translation does not wait.
> **Tip**
> When Ollama loads another model (for example the AI TOOL chat model), it may drop the resident translation model.
> It then loads again automatically at the next translation (that one translation takes a few seconds).
### Using it: translate with the globe button
With Prompt Translation on, a **globe (🌐)** button appears to the right of the **↺** button of Positive Prompt and Negative Prompt on the Input tab.
1. Write the prompt in your own language. Mixing in English tags is fine.
```
masterpiece, 夕焼けの海辺に立つ少女、(長い黒髪:1.2), <lora:test_lora:0.8>, {猫耳|狐耳}, 水彩画風
```
2. Press the globe button.
3. Only the parts in your language become English.
```
masterpiece, A girl standing by the seaside at sunset, (long black hair:1.2), <lora:test_lora:0.8>, {cat ears|fox ears}, watercolor style
```
The following prompt syntax is kept as it is, without translating:
- Commas and line breaks
- Weights `(word:1.2)` and `[ ]`
- Random choice `{A|B}` and wildcards `__name__`
- LoRA `<lora:name:0.8>` and `embedding:name`
- `BREAK` and `AND`
After translating, an **Original** button appears. Press it to go back to the text before translation (Ctrl+Z also works).
> **Note**
> Only parts that contain letters outside ASCII are translated; English-only parts stay as they are.
> Conversely, an English word that contains an accented letter such as é is translated too.
### Translating automatically when running
Under **Translate automatically when running** in Settings, checkboxes choose when to translate automatically.
- **GenerateUI - Generate**: pressing Generate replaces the text of the Positive/Negative fields with the translation, then generates
- **GenerateUI Lab - Run**: a Lab run translates only what is sent. The Lab plan keeps your text
- **Video Plan - Run**: a Video tab Plan run translates only the prompts that are sent. The plan keeps your text
Because Lab and Video Plan keep your text, you can keep editing the plan in your own language.
The information saved with the generated images and videos (metadata) records the English prompt that was actually sent.
> **Note**
> If translation fails while automatic translation is on (for example, Ollama is not running), generation stops so that untranslated text is not sent.
> For Lab and Video Plan, the reason is shown in the progress line.
### Also in the Lab modal and Video Plan
The globe button is also on these fields, and works the same way:
- The Positive and Negative fields of the screen that opens when you double-click a cell in Lab's prompt column (5-7)
- The **Prompt** field of a block in the Video tab's Plan subtab (Chapter 11, section 11-4)
> **Tip**
> PLaMo 3 Translate is free for personal use, hobbies, research and other non-commercial use (PLaMo Community License).
> Check the license terms before using it commercially.
> **Summary of this chapter (second half)**
> - With the Batch subtab, you can generate in bulk while changing the model, prompt, sampler and so on, one kind at a time
> - Choose the kind with the Batch Queue column checkbox and start with Generate. Turn the check off when done
> - Feeder is a feature that feeds many images one at a time into a workflow. A dedicated node is required
> - Lab is an advanced feature that can switch several settings at separate timings
> - Partner API nodes (Nano Banana and others) run once `COMFY_API_KEY` is set in `.env`. They consume credits
> - With Prompt Translation on, the globe button turns a prompt written in your own language into English. Prompt syntax is kept, and Generate, Lab and Video Plan runs can translate automatically
---
@@ -1941,6 +2076,10 @@ With the buttons shown below the translation result, you can use the result.
When you want to know the meaning of an English prompt, you can conversely translate it with **EN->JA**.
> **Tip**
> To translate directly in GenerateUI's prompt fields, **Prompt Translation** (the globe button) in Chapter 5, section 5-10 is handy. It turns only the parts in your language into English and keeps weights and LoRA tags.
> The AI Assistant's translation can also use the translation model **PLaMo 3 Translate** (v0.9.9 and later; before that, the instruction text was translated too, and ZH->EN came out in Japanese).
### Making prompts while consulting the AI
Write a question in the input box and press **Send** (or the Enter key) to chat with the AI.
@@ -2589,6 +2728,11 @@ The screen is divided into four parts, all shown at the same time.
> Small models sometimes return the original text as it is without translating.
> In that case a message says the model returned the original text, so try another model.
> **Tip**
> You can also choose the translation model **PLaMo 3 Translate** (Chapter 5, section 5-10) as the model in Settings (v0.9.9 and later).
> Besides Japanese, English and Chinese, **Free** accepts language names such as French, フランス語 or français for any language PLaMo supports. A target language PLaMo does not support (such as Portuguese) gives an error.
> The same applies to the translation in WS Library's A tab.
### Chat
Talk in the input box and send with **Enter** (**Shift + Enter** for a line break).
It remembers the flow of the conversation, so follow-up requests such as "make it a brighter mood" also work.
@@ -3074,6 +3218,14 @@ You load a matching workflow with **Load in Video** in the Workflow tab and use
There are also the **Frame** feature, which saves a favorite scene of a video as a still image, and **GIF**, which converts a video to an animated GIF.
> **Tip**
> The **Prompt** field of a Plan block also has the Prompt Translation globe button (Chapter 5, section 5-10).
> With **Video Plan - Run** turned on in Settings, you can run the plan with prompts in your own language and send only English.
> **Note**
> Up to v0.9.8, with workflows saved by newer ComfyUI versions, the prompt, length (Duration) and Turbo settings entered in Plan were not applied, and the values saved in the workflow were used instead (fixed in v0.9.9).
> Because Plan values are applied now, running the same plan as before may give different results.
### Register as Asset and audio conversion (v0.8.9 and later)
- **Register as Asset** - a button in the Video Source panel on the left. It copies a video from your PC into the output folder (`output/video`) and registers it in the "Video Assets" group.
Once registered, it appears in the Asset subtab list and can be used in Edit and elsewhere.
@@ -3122,6 +3274,7 @@ Here we mainly introduce items not covered in earlier chapters.
| CivitAI API Key | For fetching Civitai information (optional) | Chapter 6 |
| Eagle integration (optional) | Automatically save generated images to Eagle | Chapter 8 |
| VRAM Management (Ollama) | Unload AI models from memory before generation | Chapter 10 |
| Prompt Translation (native language -> English) | Turn prompts in your own language into English with the globe button or automatic translation when running | Chapter 5, 5-10 |
| Data Management | Backing up and restoring settings and data | Below |
### Default Workflow
@@ -3341,6 +3494,7 @@ This means **the custom nodes the workflow needs are not installed**.
### You changed a setting but the result did not change
The most common cause is **forgetting to press Apply**.
In GenerateUI, after changing the model or Settings values (steps, size and so on), the change is not reflected in the workflow unless you press **Apply** (Chapter 5, section 5-3).
The Switches column at the right of the Settings tab is the exception: it takes effect the moment you click (Chapter 5, section 5-4).
Looking at the **Raw JSON** on the right lets you check the values actually reflected.
### It generates something different from the loaded workflow
@@ -3356,6 +3510,15 @@ In the **Model** subtab of GenerateUI, choose the model you want to use again an
If you always want to use the same model, the **Default Checkpoint** in the Settings tab is handy (Chapter 11, section 11-5).
### The Prompt Translation globe button does not appear or translation fails
| Symptom | Cause | Fix |
| --- | --- | --- |
| No globe button | Prompt Translation is off | In the Settings tab, open **Prompt Translation (native language -> English)** and check **Enable prompt translation in GenerateUI** (Chapter 5, section 5-10) |
| "Translation error" appears | Ollama is not running, or the translation model is not in Ollama | Start Ollama. If **Translation model** shows **(not downloaded)**, press **Download** |
| Nothing changes ("Nothing to translate") | The prompt is English (ASCII) only | Only parts with letters outside ASCII are translated; English parts stay as they are |
| Generation does not start with automatic translation on | Translation failed, so generation stopped instead of sending untranslated text | Same fix as "Translation error". In Lab and Video Plan the reason is shown in the progress line |
| The translation is off | Small models (0.8B) can shift the meaning of sentences | Choose 2B or larger in **Translation model** |
### It does not become the same image even with the same settings
Is the seed mode in the generate panel set to **Random**?
To make the same image, you need to set the seed to **Workflow** (use the workflow's seed as it is) or **Fixed** (fix it to the value in the Seed field).
@@ -3535,6 +3698,7 @@ The main terms that appear in this book, in alphabetical order.
| Node set | Several nodes and their connections saved together | Chapter 9 |
| OCR | Reading the text shown in an image | Chapter 10 |
| Ollama | Software for running local AI easily | Chapter 10 |
| PLaMo 3 Translate | A translation AI model by Preferred Networks (PFN), used by Prompt Translation | Chapter 5 |
| Port number | A number for distinguishing which software to communicate with inside one computer | Chapter 2 |
| Positive prompt | A prompt where you write what you want drawn | Chapter 7 |
| Preset | A pair of positive and negative prompts saved together | Chapter 7 |
@@ -3623,6 +3787,7 @@ The main terms that appear in this book, in alphabetical order.
| --- | --- | --- |
| Civitai (https://civitai.com/) | Getting models, fetching information | Chapter 6 |
| Ollama (https://ollama.com/) | Local AI | Chapter 10 |
| PLaMo 3 Translate (https://huggingface.co/pfnet/plamo-3-translate-2b-GGUF) | Translation model for Prompt Translation | Chapter 5 |
| LM Studio (https://lmstudio.ai/) | Local AI | Chapter 10 |
| Lemonade Server (https://lemonade-server.ai/) | Local AI | Chapter 10 |
| Eagle (https://eagle.cool/) | Image-management software | Chapter 8 |
Binary file not shown.
+1 -1
View File
@@ -129,7 +129,7 @@ It is written so that you can follow it if you know basic computer operations (c
## Version and environment covered
- **Workflow Studio**: v0.9.5
- **Workflow Studio**: v0.9.9
- **ComfyUI**: the latest version at the time of writing
- **OS**: Windows (screen examples are from Windows)
@@ -168,12 +168,13 @@ If you save it with a name, it is added as a new workflow in the Workflow tab.
Once the first image is out, next look at the **Settings** subtab, where settings that decide how the result turns out are found.
The Settings tab is divided into two blocks, left and right.
The Settings tab is divided into three columns.
- **KSampler (left)** - settings for how the image is drawn
- **Latent Image (right)** - settings for the size and number of images
- **Latent Image (middle)** - settings for the size and number of images
- **Switches (right, v0.9.6+)** - the on/off settings inside the workflow
Here too, don't forget to press **Apply** on each block after changing values.
Here too, for KSampler and Latent Image, don't forget to press **Apply** on each block after changing values (the Switches column needs no Apply).
### KSampler items
| Item | Meaning | Rule of thumb for beginners |
@@ -200,6 +201,23 @@ Here too, don't forget to press **Apply** on each block after changing values.
> If you stray far from the size the model learned, people tend to come out doubled or the composition falls apart.
> Even when you want portrait or landscape, adjust so the total pixel count stays about the same (for example 832x1216 for SDXL).
### Switches items (v0.9.6+)
The "on/off" settings inside the workflow are listed with their node name and item name.
Clicking a checkbox switches it right away. **You don't need to press Apply**. The new value also shows up in the Raw JSON on the right at once.
Two kinds of items are listed:
- Items whose value is true/false - for example the switch that turns the prompt enhancer (PE) on and off, "Boolean" nodes, and TextGenerate's thinking
- On/off items with only two choices - enable/disable, on/off, yes/no and so on. For example KSamplerAdvanced's add_noise and DWPreprocessor's detect_hand (whether to detect hands)
The right end of each row shows the current value (true/false, enable/disable and so on).
Hover over a row to see the node's number and type.
> **Tip**
> Items wired from another node are not listed. Instead, the source node they are wired from (for example a Boolean node named "Enable PE?") is listed, so switch that one.
> In workflows where nodes have no names, the node type name (such as ComfySwitchNode) is shown.
> If the workflow has no on/off items at all, "No true/false settings in this workflow" is shown.
### Managing the seed (generate panel)
The seed is managed with the **Seed** field in the generate panel and the pull-down below it.
@@ -232,6 +250,6 @@ This is the shortest path to getting used to image generation.
> **Summary of this chapter (first half)**
> - Select in the Workflow tab and press "Load in GenerateUI" to edit in GenerateUI
> - The basics are the three subtabs Input (prompt), Model (model) and Settings (detailed settings)
> - After changing the model or Settings values, always press **Apply**. If you forget, it generates with the values from before the change (prompts are reflected automatically)
> - After changing the model or Settings values, always press **Apply**. If you forget, it generates with the values from before the change (prompts and the Settings Switches column are reflected automatically)
> - Generate to produce. Alt + Apply reflects and generates in one go
> - If you set the seed to Fixed, you can keep a picture you like and compare by changing conditions. To use the workflow's seed as it is, use Workflow
@@ -219,18 +219,135 @@ The workflows you can use in GenerateUI have increased as follows. They are used
- **FastH3 OmniRef / Pika-H3-Flex** (v0.8.14 and later) - MiniMax H3 reference to video. Treated as video workflows, and also usable in the Video tab (Chapter 11).
- **Native LoRA stack** - ComfyUI v0.40's nodes that load several LoRAs at once (LoadLoraModel / LoadLoraTextEncoder). Apply, bypass and the Metadata display are supported.
- **P-H3 Image** (v0.9.4+) - the image-edit and T2I templates. The prompt is shown on the Input tab and can be read by the Metadata tab; there is no negative prompt field. Width and height follow the workflow's wiring, and the prompt upsampler runs as set in the workflow.
- **MiniMax H3 Image** (v0.9.6+) - ComfyUI's official image-edit and T2I templates. As with P-H3 Image, the prompt is shown on the Input tab and can be read by the Metadata tab and the sidepanel I tab; there is no negative prompt field. The prompt enhancer (PE) can be turned on and off in the Settings tab's Switches column (5-4).
> **Caution**
> The native LoRA stack nodes are supported **only in API-format workflows**.
> ComfyUI's own screen cannot yet add rows to this node, so UI-format workflows are not covered.
> **Tip**
> Workflows containing the **Compare Images** (ImageCompare) node, which compares two images with a slider, used to fail on that node when run from GenerateUI (fixed in v0.9.6).
> **Tip**
> The Metadata tab and the side panel's **I** tab can also read images made with Nano Banana (v0.8.5 and later).
> The prompt appears as one whole entry, not split into Positive/Negative.
## 5-10 Writing prompts in your own language (Prompt Translation, v0.9.9 and later)
Most image generation models were trained on English prompts.
With **Prompt Translation** turned on, you can write a prompt in your own language and turn it into English with one button.
Like the translation extensions for Stable Diffusion web UI (A1111), it translates **only the parts written in your own language**.
Translation uses AI running on your PC (**Ollama**) and the translation model **PLaMo 3 Translate**, which supports 51 languages besides English (Japanese, Chinese, Korean, French, German, Spanish, Russian, Thai, Vietnamese and more).
Your prompts are not sent to an online translation service.
> **What you need first**
> Install and start **Ollama** (Chapter 10, section 10-1). The translation model can be downloaded from the Settings screen (see below).
### Preparation: turn it on in Settings
1. In the **Settings** tab, open **Prompt Translation (native language -> English)**.
2. Check **Enable prompt translation in GenerateUI**.
3. Choose the model in **Translation model**. Models not yet in Ollama are marked **(not downloaded)**; choose one and press **Download** to pull it into Ollama.
4. Press **Test**. When the sample prompt comes back in English, you are ready.
PLaMo 3 Translate comes in four sizes.
| Model | Size | Notes |
| --- | --- | --- |
| 0.8B | about 0.6 GB | Fastest. Enough for short tags |
| 2B | about 1.8 GB | Recommended. Translates sentences accurately too |
| 8B | about 5.3 GB | Higher quality |
| 31B | about 19.5 GB | Highest quality, for PCs with plenty of memory |
> **Tip**
> 0.8B sometimes shifts the meaning of a sentence (for example, "the camera follows from behind" in Japanese became "her camera following behind").
> If you often write prompts as sentences, such as video prompts, choose 2B or larger.
The other settings are:
| Setting | What it does |
| --- | --- |
| Source language | Usually leave it at **Auto-detect**. If you always write in one language, choosing it makes detection certain |
| Ollama URL | Ollama's address. Blank means `http://localhost:11434` |
| Where to run the translation model | **GPU** or **CPU, kept loaded** (see below) |
| Translate automatically when running | Translates automatically when you press Generate and so on (see below) |
Other Ollama models (such as the one you use for chat) can also be chosen under **Other Ollama models**. They are sent the text with a "translate into English" instruction.
### Where to run the translation model
| Where | Time per phrase | Good for |
| --- | --- | --- |
| GPU (fastest per translation, uses VRAM) | about 0.1 s | GPUs with VRAM to spare |
| CPU, kept loaded (no VRAM, no reload wait) | about 0.2-0.3 s | Keeping all VRAM for image generation |
On GPU, turning on **Unload the translation model from VRAM after translating** frees VRAM before image generation (on by default).
However, every translation then waits **about 4 seconds** while the model loads again.
**CPU, kept loaded** keeps the model in system RAM (about 2.7 GB for 2B).
It uses no VRAM and is loaded in the background when a workflow is loaded into GenerateUI, so even the first translation does not wait.
> **Tip**
> When Ollama loads another model (for example the AI TOOL chat model), it may drop the resident translation model.
> It then loads again automatically at the next translation (that one translation takes a few seconds).
### Using it: translate with the globe button
With Prompt Translation on, a **globe (🌐)** button appears to the right of the **↺** button of Positive Prompt and Negative Prompt on the Input tab.
1. Write the prompt in your own language. Mixing in English tags is fine.
```
masterpiece, 夕焼けの海辺に立つ少女、(長い黒髪:1.2), <lora:test_lora:0.8>, {猫耳|狐耳}, 水彩画風
```
2. Press the globe button.
3. Only the parts in your language become English.
```
masterpiece, A girl standing by the seaside at sunset, (long black hair:1.2), <lora:test_lora:0.8>, {cat ears|fox ears}, watercolor style
```
The following prompt syntax is kept as it is, without translating:
- Commas and line breaks
- Weights `(word:1.2)` and `[ ]`
- Random choice `{A|B}` and wildcards `__name__`
- LoRA `<lora:name:0.8>` and `embedding:name`
- `BREAK` and `AND`
After translating, an **Original** button appears. Press it to go back to the text before translation (Ctrl+Z also works).
> **Note**
> Only parts that contain letters outside ASCII are translated; English-only parts stay as they are.
> Conversely, an English word that contains an accented letter such as é is translated too.
### Translating automatically when running
Under **Translate automatically when running** in Settings, checkboxes choose when to translate automatically.
- **GenerateUI - Generate**: pressing Generate replaces the text of the Positive/Negative fields with the translation, then generates
- **GenerateUI Lab - Run**: a Lab run translates only what is sent. The Lab plan keeps your text
- **Video Plan - Run**: a Video tab Plan run translates only the prompts that are sent. The plan keeps your text
Because Lab and Video Plan keep your text, you can keep editing the plan in your own language.
The information saved with the generated images and videos (metadata) records the English prompt that was actually sent.
> **Note**
> If translation fails while automatic translation is on (for example, Ollama is not running), generation stops so that untranslated text is not sent.
> For Lab and Video Plan, the reason is shown in the progress line.
### Also in the Lab modal and Video Plan
The globe button is also on these fields, and works the same way:
- The Positive and Negative fields of the screen that opens when you double-click a cell in Lab's prompt column (5-7)
- The **Prompt** field of a block in the Video tab's Plan subtab (Chapter 11, section 11-4)
> **Tip**
> PLaMo 3 Translate is free for personal use, hobbies, research and other non-commercial use (PLaMo Community License).
> Check the license terms before using it commercially.
> **Summary of this chapter (second half)**
> - With the Batch subtab, you can generate in bulk while changing the model, prompt, sampler and so on, one kind at a time
> - Choose the kind with the Batch Queue column checkbox and start with Generate. Turn the check off when done
> - Feeder is a feature that feeds many images one at a time into a workflow. A dedicated node is required
> - Lab is an advanced feature that can switch several settings at separate timings
> - Partner API nodes (Nano Banana and others) run once `COMFY_API_KEY` is set in `.env`. They consume credits
> - With Prompt Translation on, the globe button turns a prompt written in your own language into English. Prompt syntax is kept, and Generate, Lab and Video Plan runs can translate automatically
@@ -223,6 +223,10 @@ With the buttons shown below the translation result, you can use the result.
When you want to know the meaning of an English prompt, you can conversely translate it with **EN->JA**.
> **Tip**
> To translate directly in GenerateUI's prompt fields, **Prompt Translation** (the globe button) in Chapter 5, section 5-10 is handy. It turns only the parts in your language into English and keeps weights and LoRA tags.
> The AI Assistant's translation can also use the translation model **PLaMo 3 Translate** (v0.9.9 and later; before that, the instruction text was translated too, and ZH->EN came out in Japanese).
### Making prompts while consulting the AI
Write a question in the input box and press **Send** (or the Enter key) to chat with the AI.
@@ -110,6 +110,11 @@ The screen is divided into four parts, all shown at the same time.
> Small models sometimes return the original text as it is without translating.
> In that case a message says the model returned the original text, so try another model.
> **Tip**
> You can also choose the translation model **PLaMo 3 Translate** (Chapter 5, section 5-10) as the model in Settings (v0.9.9 and later).
> Besides Japanese, English and Chinese, **Free** accepts language names such as French, フランス語 or français for any language PLaMo supports. A target language PLaMo does not support (such as Portuguese) gives an error.
> The same applies to the translation in WS Library's A tab.
### Chat
Talk in the input box and send with **Enter** (**Shift + Enter** for a line break).
It remembers the flow of the conversation, so follow-up requests such as "make it a brighter mood" also work.
@@ -155,6 +155,14 @@ You load a matching workflow with **Load in Video** in the Workflow tab and use
There are also the **Frame** feature, which saves a favorite scene of a video as a still image, and **GIF**, which converts a video to an animated GIF.
> **Tip**
> The **Prompt** field of a Plan block also has the Prompt Translation globe button (Chapter 5, section 5-10).
> With **Video Plan - Run** turned on in Settings, you can run the plan with prompts in your own language and send only English.
> **Note**
> Up to v0.9.8, with workflows saved by newer ComfyUI versions, the prompt, length (Duration) and Turbo settings entered in Plan were not applied, and the values saved in the workflow were used instead (fixed in v0.9.9).
> Because Plan values are applied now, running the same plan as before may give different results.
### Register as Asset and audio conversion (v0.8.9 and later)
- **Register as Asset** - a button in the Video Source panel on the left. It copies a video from your PC into the output folder (`output/video`) and registers it in the "Video Assets" group.
Once registered, it appears in the Asset subtab list and can be used in Edit and elsewhere.
@@ -203,6 +211,7 @@ Here we mainly introduce items not covered in earlier chapters.
| CivitAI API Key | For fetching Civitai information (optional) | Chapter 6 |
| Eagle integration (optional) | Automatically save generated images to Eagle | Chapter 8 |
| VRAM Management (Ollama) | Unload AI models from memory before generation | Chapter 10 |
| Prompt Translation (native language -> English) | Turn prompts in your own language into English with the globe button or automatic translation when running | Chapter 5, 5-10 |
| Data Management | Backing up and restoring settings and data | Below |
### Default Workflow
@@ -72,6 +72,7 @@ This means **the custom nodes the workflow needs are not installed**.
### You changed a setting but the result did not change
The most common cause is **forgetting to press Apply**.
In GenerateUI, after changing the model or Settings values (steps, size and so on), the change is not reflected in the workflow unless you press **Apply** (Chapter 5, section 5-3).
The Switches column at the right of the Settings tab is the exception: it takes effect the moment you click (Chapter 5, section 5-4).
Looking at the **Raw JSON** on the right lets you check the values actually reflected.
### It generates something different from the loaded workflow
@@ -87,6 +88,15 @@ In the **Model** subtab of GenerateUI, choose the model you want to use again an
If you always want to use the same model, the **Default Checkpoint** in the Settings tab is handy (Chapter 11, section 11-5).
### The Prompt Translation globe button does not appear or translation fails
| Symptom | Cause | Fix |
| --- | --- | --- |
| No globe button | Prompt Translation is off | In the Settings tab, open **Prompt Translation (native language -> English)** and check **Enable prompt translation in GenerateUI** (Chapter 5, section 5-10) |
| "Translation error" appears | Ollama is not running, or the translation model is not in Ollama | Start Ollama. If **Translation model** shows **(not downloaded)**, press **Download** |
| Nothing changes ("Nothing to translate") | The prompt is English (ASCII) only | Only parts with letters outside ASCII are translated; English parts stay as they are |
| Generation does not start with automatic translation on | Translation failed, so generation stopped instead of sending untranslated text | Same fix as "Translation error". In Lab and Video Plan the reason is shown in the progress line |
| The translation is off | Small models (0.8B) can shift the meaning of sentences | Choose 2B or larger in **Translation model** |
### It does not become the same image even with the same settings
Is the seed mode in the generate panel set to **Random**?
To make the same image, you need to set the seed to **Workflow** (use the workflow's seed as it is) or **Fixed** (fix it to the value in the Seed field).
+2
View File
@@ -34,6 +34,7 @@ The main terms that appear in this book, in alphabetical order.
| Node set | Several nodes and their connections saved together | Chapter 9 |
| OCR | Reading the text shown in an image | Chapter 10 |
| Ollama | Software for running local AI easily | Chapter 10 |
| PLaMo 3 Translate | A translation AI model by Preferred Networks (PFN), used by Prompt Translation | Chapter 5 |
| Port number | A number for distinguishing which software to communicate with inside one computer | Chapter 2 |
| Positive prompt | A prompt where you write what you want drawn | Chapter 7 |
| Preset | A pair of positive and negative prompts saved together | Chapter 7 |
@@ -122,6 +123,7 @@ The main terms that appear in this book, in alphabetical order.
| --- | --- | --- |
| Civitai (https://civitai.com/) | Getting models, fetching information | Chapter 6 |
| Ollama (https://ollama.com/) | Local AI | Chapter 10 |
| PLaMo 3 Translate (https://huggingface.co/pfnet/plamo-3-translate-2b-GGUF) | Translation model for Prompt Translation | Chapter 5 |
| LM Studio (https://lmstudio.ai/) | Local AI | Chapter 10 |
| Lemonade Server (https://lemonade-server.ai/) | Local AI | Chapter 10 |
| Eagle (https://eagle.cool/) | Image-management software | Chapter 8 |
+57
View File
@@ -1,9 +1,66 @@
import os
import logging
import re
from pathlib import Path
logger = logging.getLogger(__name__)
# ─── パス検査(クライアント由来のパスを resolve() する前に文字列のまま検査する)───
# Windows では UNC パス(\\host\share)は resolve()・exists() を呼んだ時点で相手ホストへ
# Windows アカウントの資格情報付きで接続してしまうため、チェックはパス操作より先に行う。
_DRIVE_RE = re.compile(r"^[A-Za-z]:")
def is_unc(value) -> bool:
"""ネットワーク共有/デバイスパス(\\\\host\\share, //host/share, \\\\?\\ 等)か。"""
return str(value or "").replace("/", "\\").startswith("\\\\")
def is_safe_rel(rel) -> bool:
"""相対パスとして安全か。UNC・ドライブ指定・絶対パス・「..」・空セグメントを
結合・resolve() より前に文字列のまま拒否する。"""
s = str(rel or "")
if not s or "\x00" in s:
return False
if s.startswith(("/", "\\")) or _DRIVE_RE.match(s):
return False
return not any(part in ("", "..") for part in re.split(r"[/\\]", s))
def resolve_untrusted(value, roots=()) -> Path:
"""クライアント由来の絶対パス文字列を検査してから resolve() して返す。
ファイルシステムに触れない文字列処理(normpath)だけで「..」を正規化・拒否し、
UNC は roots のうち UNC のルート配下に文字列上一致する場合のみ許可する
(ギャラリーのルートをネットワーク共有に置いている構成だけを通すため)。
拒否時は ValueError。"""
s = str(value or "")
if not s or "\x00" in s:
raise ValueError("Access denied: invalid path")
p = Path(os.path.normpath(s))
if ".." in p.parts:
raise ValueError("Access denied: path traversal")
if is_unc(s):
for root in roots:
if root is None or not is_unc(str(root)):
continue
try:
p.relative_to(Path(root)) # 文字列上の照合(解決しない)
return p.resolve()
except (ValueError, OSError):
continue
raise ValueError("Access denied: network path")
# 受け付ける形はドライブレター絶対パス(Windows)または / 始まり(その他OS)に限る。
# ルート相対(\foo)・ドライブ相対(C:foo)・NT デバイス形式(\??\, \.\ 等)・相対パスは拒否する
if os.name == "nt":
if not re.match(r"^[A-Za-z]:[\\/]", s):
raise ValueError("Access denied: invalid path")
elif not s.startswith("/"):
raise ValueError("Access denied: invalid path")
return p.resolve()
# Plugin root directory
PLUGIN_DIR = Path(__file__).resolve().parent.parent
+10 -4
View File
@@ -9,6 +9,7 @@ import urllib.error
from aiohttp import web
from ..config import is_safe_rel
from ..services.url_guard import require_backend_url, urlopen as _guarded_urlopen
logger = logging.getLogger(__name__)
@@ -38,6 +39,11 @@ def _resolve_local_path(filename, subfolder, type_name):
if not filename:
return None
# subfolder の UNC・ドライブ指定・絶対パス・「..」は、結合より前に文字列のまま拒否する
# (別ドライブ/UNC だと下の commonpath が ValueError を投げるため、その前に弾く)
if subfolder and not is_safe_rel(subfolder):
return None
full_dir = os.path.abspath(os.path.join(base_dir, subfolder or ""))
# Windowsはパス比較で大文字小文字を区別しないため normcase で正規化してから比較する
if os.path.commonpath((os.path.normcase(full_dir), os.path.normcase(base_dir))) != os.path.normcase(base_dir):
@@ -62,9 +68,9 @@ def _resolve_absolute_svg_path(abs_path):
except Exception:
return None
# isfile() も UNC(\\host\share)だと相手ホストへ資格情報付きで接続してしまうため、
# 文字列処理(abspath/commonpath)だけで ComfyUI 管理下と確認できるまでファイルシステムに触れない
abs_path = os.path.abspath(abs_path)
if not os.path.isfile(abs_path):
return None
for type_name in ("output", "temp", "input"):
base_dir = folder_paths.get_directory_by_type(type_name)
@@ -74,9 +80,9 @@ def _resolve_absolute_svg_path(abs_path):
try:
# Windowsはパス比較で大文字小文字を区別しないため normcase で正規化してから比較する
if os.path.commonpath((os.path.normcase(abs_path), os.path.normcase(base_dir))) == os.path.normcase(base_dir):
return abs_path
return abs_path if os.path.isfile(abs_path) else None
except ValueError:
# Windowsで異なるドライブレターの場合 commonpath が例外を投げる
# Windowsで異なるドライブレターやUNCの場合 commonpath が例外を投げる
continue
return None
+9 -6
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from aiohttp import web
from ..services.settings_service import SettingsService
from ..config import DATA_DIR
from ..config import DATA_DIR, resolve_untrusted
logger = logging.getLogger(__name__)
@@ -153,17 +153,20 @@ async def handle_result(request: web.Request) -> web.Response:
try:
body = await request.json()
result_path = body.get("result_path", "")
if not result_path or not os.path.exists(result_path):
return web.json_response({"error": f"Result file not found: {result_path}"}, status=404)
# Path traversal guard: only allow files inside gmic_temp
# Path traversal guard: only allow files inside gmic_temp.
# UNC(\\host\share)は exists()/resolve() を呼んだ時点で相手ホストへ資格情報付きで
# 接続してしまうため、パス操作より先に文字列のまま検査する(resolve_untrusted)。
allowed_dir = (Path(DATA_DIR) / "gmic_temp").resolve()
try:
Path(result_path).resolve().relative_to(allowed_dir)
resolved = resolve_untrusted(result_path)
resolved.relative_to(allowed_dir)
except ValueError:
return web.json_response({"error": "Access denied: path outside gmic_temp"}, status=403)
if not resolved.is_file():
return web.json_response({"error": f"Result file not found: {result_path}"}, status=404)
with open(result_path, "rb") as f:
with open(resolved, "rb") as f:
img_bytes = f.read()
ext = os.path.splitext(result_path)[1].lower()
+15 -2
View File
@@ -12,7 +12,7 @@ from pathlib import Path
from aiohttp import web
from ..services.settings_service import SettingsService
from ..config import DEFAULT_WORKFLOWS_DIR, DATA_DIR, VIDEO_EDIT_PROJECT_DIR
from ..config import DEFAULT_WORKFLOWS_DIR, DATA_DIR, VIDEO_EDIT_PROJECT_DIR, is_safe_rel
logger = logging.getLogger(__name__)
@@ -397,6 +397,10 @@ def _add_video_edit_media_to_zip(zf: zipfile.ZipFile) -> None:
input_dir = _comfyui_input_dir().resolve()
seen = set()
for subfolder, filename in _iter_video_edit_media_refs():
# プロジェクトファイルは細工できるため、UNC・絶対パス・「..」は結合・resolve() の前に
# 文字列のまま拒否する(Windows では UNC を resolve() するだけで相手ホストへ接続する)
if not is_safe_rel(filename) or (subfolder and not is_safe_rel(subfolder)):
continue
src = (input_dir / subfolder / filename).resolve()
try:
rel = src.relative_to(input_dir) # a crafted project file must not reach outside input/
@@ -466,9 +470,15 @@ def _apply_full_backup_zip(zip_source) -> dict:
summary["skipped"].append(info.filename)
continue
if info.filename.startswith(_VIDEO_MEDIA_PREFIX):
media_rel = info.filename[len(_VIDEO_MEDIA_PREFIX):]
# ZIP エントリ名は細工できるため、UNC・絶対パス・「..」は結合・resolve() の前に
# 文字列のまま拒否する(Windows では UNC を resolve() するだけで相手ホストへ接続する)
if not is_safe_rel(media_rel):
summary["skipped"].append(info.filename)
continue
if input_base is None:
input_base = _comfyui_input_dir().resolve()
target = (input_base / info.filename[len(_VIDEO_MEDIA_PREFIX):]).resolve()
target = (input_base / media_rel).resolve()
try:
target.relative_to(input_base)
except ValueError:
@@ -482,6 +492,9 @@ def _apply_full_backup_zip(zip_source) -> dict:
shutil.copyfileobj(src, dst)
summary["media_restored"].append(info.filename)
continue
if not is_safe_rel(info.filename):
summary["skipped"].append(info.filename)
continue
target = (base / info.filename).resolve()
try:
target.relative_to(base)
+93
View File
@@ -0,0 +1,93 @@
"""Prompt translation routes (GenerateUI native-language prompt input)."""
import asyncio
import logging
from aiohttp import web
from ..services import translate_service as svc
from ..services.url_guard import require_backend_url
logger = logging.getLogger(__name__)
def setup_routes(app: web.Application):
app.router.add_get("/api/wfm/translate/config", handle_config)
app.router.add_get("/api/wfm/translate/models", handle_models)
app.router.add_post("/api/wfm/translate", handle_translate)
app.router.add_post("/api/wfm/translate/pull", handle_pull)
app.router.add_post("/api/wfm/translate/warmup", handle_warmup)
app.router.add_post("/api/wfm/translate/unload", handle_unload)
async def handle_config(request: web.Request) -> web.Response:
"""GET /api/wfm/translate/config - effective settings + PLaMo presets."""
cfg = await asyncio.to_thread(svc.get_config)
return web.json_response({"config": cfg, "presets": svc.PLAMO_PRESETS})
async def handle_models(request: web.Request) -> web.Response:
"""GET /api/wfm/translate/models - models installed in the translation Ollama."""
try:
models = await asyncio.to_thread(svc.list_models)
return web.json_response({"status": "success", "models": models, "presets": svc.PLAMO_PRESETS})
except Exception as e:
logger.warning("Translate models error: %s", e)
return web.json_response({"status": "error", "message": str(e), "presets": svc.PLAMO_PRESETS}, status=502)
async def handle_translate(request: web.Request) -> web.Response:
"""POST /api/wfm/translate {texts: [...], model?, source_lang?} -> {translations: [...]}"""
try:
body = await request.json()
texts = body.get("texts")
translations, cached = await asyncio.to_thread(
svc.translate_texts, texts, body.get("model") or None, body.get("source_lang"),
)
return web.json_response({"status": "success", "translations": translations, "cached": cached})
except ValueError as e:
return web.json_response({"status": "error", "message": str(e)}, status=400)
except Exception as e:
logger.error("Translate error: %s", e)
return web.json_response({"status": "error", "message": str(e)}, status=502)
async def handle_warmup(request: web.Request) -> web.Response:
"""POST /api/wfm/translate/warmup - preload the model when CPU-resident mode is on."""
try:
loaded = await asyncio.to_thread(svc.warmup)
return web.json_response({"status": "success", "loaded": loaded})
except Exception as e:
logger.warning("Translate warmup error: %s", e)
return web.json_response({"status": "error", "message": str(e)}, status=502)
async def handle_unload(request: web.Request) -> web.Response:
"""POST /api/wfm/translate/unload - release the resident model (device switch / disable)."""
try:
cfg = await asyncio.to_thread(svc.get_config)
base_url = require_backend_url(cfg["prompt_translate_url"])
await asyncio.to_thread(svc.unload, base_url, cfg["prompt_translate_model"])
return web.json_response({"status": "success"})
except Exception as e:
return web.json_response({"status": "error", "message": str(e)}, status=502)
async def handle_pull(request: web.Request) -> web.Response:
"""POST /api/wfm/translate/pull {model} - pull one of the PLaMo presets into Ollama."""
try:
body = await request.json()
model = body.get("model", "")
if model not in svc.PLAMO_PRESETS:
return web.json_response({"status": "error", "message": "only PLaMo presets can be pulled"}, status=400)
cfg = await asyncio.to_thread(svc.get_config)
base_url = require_backend_url(cfg["prompt_translate_url"])
data = await asyncio.to_thread(
svc._post_json, f"{base_url}/api/pull", {"model": model, "stream": False}, 3600,
)
if data.get("error"):
return web.json_response({"status": "error", "message": data["error"]}, status=502)
return web.json_response({"status": "success", "result": data.get("status", "")})
except Exception as e:
logger.error("Translate pull error: %s", e)
return web.json_response({"status": "error", "message": str(e)}, status=502)
+52 -25
View File
@@ -16,6 +16,8 @@ from pathlib import Path
from typing import Any
from urllib.parse import unquote
from ..config import is_safe_rel, resolve_untrusted
logger = logging.getLogger(__name__)
# 動画拡張子(PyAV(av)がコンテナ形式を問わず共通で扱えるため、寸法/再生時間/埋め込み
@@ -270,7 +272,7 @@ class GalleryService:
"""バックグラウンドスレッドで未キャッシュ画像のprompt_cacheを構築する。
10枚処理するたびに50msスリープしてメインスレッドへの影響を最小化。"""
try:
folder = Path(folder_path).resolve()
folder = self._resolve_untrusted(folder_path)
if not folder.is_dir() or not self._check_path_allowed(folder):
return
entries = self._scan_folder(folder)
@@ -333,13 +335,19 @@ class GalleryService:
pass
return False
def _resolve_untrusted(self, value) -> Path:
"""クライアント由来のパスを resolve() の前に文字列のまま検査して返す(拒否時 ValueError)。
Windows では UNC パスは resolve() した時点で相手ホストへ資格情報付きで接続してしまうため、
UNC は許可ルート自身が UNC の場合のみ、その配下を許す。"""
return resolve_untrusted(value, (self._allowed_root, self._comfy_output_root))
# ──────────────────────────────────────────────────────────────
# フォルダツリー
# ──────────────────────────────────────────────────────────────
def list_folder_tree(self, root_path: str) -> dict:
"""outputフォルダのフォルダツリーを返す"""
root = Path(root_path).resolve()
root = self._resolve_untrusted(root_path)
if not root.is_dir():
return {"error": f"Directory not found: {root_path}"}
if self._allowed_root is None:
@@ -484,7 +492,7 @@ class GalleryService:
recursive: bool = False,
) -> list[dict]:
"""指定フォルダ内の画像一覧を返す(recursive=Trueならサブフォルダも含める)"""
folder = Path(folder_path).resolve()
folder = self._resolve_untrusted(folder_path)
if not folder.is_dir():
return []
if not self._check_path_allowed(folder):
@@ -592,7 +600,7 @@ class GalleryService:
def save_image_to_folder(self, folder_path: str, filename: str, image_data: str) -> dict:
"""任意フォルダへ画像を保存する(data URL想定、同名なら上書き)。
Style Catalogのカタログ作成(スタイル名でのファイル保存)で使用。"""
folder = Path(folder_path).resolve()
folder = self._resolve_untrusted(folder_path)
if not folder.is_dir():
return {"ok": False, "error": "Destination folder not found"}
if not self._check_path_allowed(folder):
@@ -740,7 +748,7 @@ class GalleryService:
def save_image_prompt(self, image_path: str, text: str) -> bool:
"""画像と同名の.txtサイドカーにプロンプトテキストを保存する"""
p = Path(image_path).resolve()
p = self._resolve_untrusted(image_path)
if not p.is_file():
return False
if not self._check_path_allowed(p):
@@ -758,7 +766,7 @@ class GalleryService:
def get_image_metadata(self, image_path: str) -> dict:
"""PNG/JPEGからメタデータを抽出し、保存済みメタと合わせて返す"""
path = Path(image_path).resolve()
path = self._resolve_untrusted(image_path)
if not path.is_file():
return {"error": "File not found"}
if not self._check_path_allowed(path):
@@ -1022,7 +1030,7 @@ class GalleryService:
extractPromptsLiteGraphがトップレベル系統しか拾えないことがあるため、
プロンプト抽出専用にAPI形式を優先する(Metadataタブのプロンプト抽出と同じ優先順位)。
"""
path = Path(image_path).resolve()
path = self._resolve_untrusted(image_path)
if not self._check_path_allowed(path):
return None
@@ -1083,7 +1091,7 @@ class GalleryService:
"""
from .convert_service import OUTPUT_FORMATS, convert_audio, supports_tags
p = Path(image_path).resolve()
p = self._resolve_untrusted(image_path)
if not p.is_file():
return {"ok": False, "error": "File not found"}
if p.suffix.lower() not in AUDIO_EXTENSIONS | VIDEO_EXTENSIONS:
@@ -1152,7 +1160,7 @@ class GalleryService:
}
def save_image_meta(self, image_path: str, data: dict) -> bool:
resolved = Path(image_path).resolve()
resolved = self._resolve_untrusted(image_path)
if not self._check_path_allowed(resolved):
logger.warning("save_image_meta: path not allowed: %s", resolved)
return False
@@ -1162,7 +1170,7 @@ class GalleryService:
return self.metadata_store.save(str(resolved), data)
def toggle_favorite(self, image_path: str) -> bool:
resolved = Path(image_path).resolve()
resolved = self._resolve_untrusted(image_path)
if not self._check_path_allowed(resolved):
return False
# get_image_metadata()/list_images() 側はresolve()済みパスをキーにして取得する
@@ -1178,7 +1186,11 @@ class GalleryService:
"""複数画像のお気に入りを一括設定する"""
ok = fail = 0
for p in paths:
resolved = Path(p).resolve()
try:
resolved = self._resolve_untrusted(p)
except ValueError:
fail += 1
continue
if not self._check_path_allowed(resolved):
fail += 1
continue
@@ -1216,7 +1228,7 @@ class GalleryService:
return self.metadata_store.delete_group(name)
def add_to_group(self, image_path: str, group_name: str) -> bool:
resolved = Path(image_path).resolve()
resolved = self._resolve_untrusted(image_path)
if not self._check_path_allowed(resolved):
return False
# get_image_metadata()/list_images() 側はresolve()済みパスをキーにして取得する
@@ -1231,7 +1243,7 @@ class GalleryService:
return True
def remove_from_group(self, image_path: str, group_name: str) -> bool:
resolved = Path(image_path).resolve()
resolved = self._resolve_untrusted(image_path)
if not self._check_path_allowed(resolved):
return False
resolved_str = str(resolved)
@@ -1260,7 +1272,7 @@ class GalleryService:
def create_folder(self, parent_path: str, name: str) -> dict:
"""選択フォルダ内に新しいサブフォルダを作成する"""
parent = Path(parent_path).resolve()
parent = self._resolve_untrusted(parent_path)
if not parent.is_dir():
return {"ok": False, "error": "Parent directory not found"}
if not self._check_path_allowed(parent):
@@ -1282,7 +1294,7 @@ class GalleryService:
def delete_folder(self, folder_path: str) -> dict:
"""フォルダを再帰的に削除する"""
import shutil
folder = Path(folder_path).resolve()
folder = self._resolve_untrusted(folder_path)
if not folder.is_dir():
return {"ok": False, "error": "Directory not found"}
if not self._check_path_allowed(folder):
@@ -1302,7 +1314,11 @@ class GalleryService:
deleted = []
errors = []
for img_path in paths:
p = Path(img_path).resolve()
try:
p = self._resolve_untrusted(img_path)
except ValueError:
errors.append(f"{Path(img_path).name}: access denied")
continue
if not p.is_file():
errors.append(f"{Path(img_path).name}: not found")
continue
@@ -1323,7 +1339,7 @@ class GalleryService:
def move_images(self, paths: list, dest_folder: str) -> dict:
"""画像ファイルを別フォルダへ移動する(複数対応)"""
dest = Path(dest_folder).resolve()
dest = self._resolve_untrusted(dest_folder)
if not dest.is_dir():
return {"ok": False, "error": "Destination folder not found", "moved": [], "errors": []}
if not self._check_path_allowed(dest):
@@ -1331,7 +1347,11 @@ class GalleryService:
moved = []
errors = []
for img_path in paths:
p = Path(img_path).resolve()
try:
p = self._resolve_untrusted(img_path)
except ValueError:
errors.append(f"{Path(img_path).name}: access denied")
continue
if not p.is_file():
errors.append(f"{Path(img_path).name}: not found")
continue
@@ -1375,7 +1395,10 @@ class GalleryService:
layers_data = []
for img_path in paths:
p = Path(img_path).resolve()
try:
p = self._resolve_untrusted(img_path)
except ValueError:
continue
if not p.is_file() or p.suffix.lower() not in IMAGE_EXTENSIONS:
continue
if p.suffix.lower() in VIDEO_EXTENSIONS | AUDIO_EXTENSIONS | MODEL3D_EXTENSIONS:
@@ -1543,7 +1566,7 @@ class GalleryService:
"""サーバー上に既にあるPSDファイル(Gallery上のパス)をレイヤー配列に分解する。
Galleryから直接Image Editへ送る際、アップロードの往復無しで済ませるために使う。
"""
p = Path(image_path).resolve()
p = self._resolve_untrusted(image_path)
if not p.is_file() or p.suffix.lower() != ".psd":
raise ValueError("Not a PSD file")
if not self._check_path_allowed(p):
@@ -1567,7 +1590,7 @@ class GalleryService:
def serve_image(self, image_path: str):
"""画像のPathオブジェクトを返す(ルートで使用)。
PSDはブラウザが直接レンダリングできないため、合成済みPNGにキャッシュ変換して返す。"""
p = Path(image_path).resolve()
p = self._resolve_untrusted(image_path)
if not p.is_file():
return None
if p.suffix.lower() not in IMAGE_EXTENSIONS:
@@ -1583,10 +1606,14 @@ class GalleryService:
""".gltfが相対URIで参照する外部ファイル(.bin/テクスチャ)のPathを返す。
uriはglTF仕様どおりパーセントエンコードされている。"""
try:
p = Path(model_path).resolve()
p = self._resolve_untrusted(model_path)
if p.suffix.lower() != ".gltf" or not p.is_file() or not self._check_path_allowed(p):
return None
target = (p.parent / unquote(uri)).resolve()
# .gltf の外部参照 URI は相対パスのみ(UNC・絶対パス・「..」は結合前に拒否)
rel_uri = unquote(uri)
if not is_safe_rel(rel_uri):
return None
target = (p.parent / rel_uri).resolve()
if target.suffix.lower() not in MODEL3D_ASSET_EXTENSIONS or not target.is_file():
return None
except (OSError, ValueError):
@@ -1608,7 +1635,7 @@ class GalleryService:
from io import BytesIO
from PIL import Image
p = Path(model_path).resolve()
p = self._resolve_untrusted(model_path)
if not p.is_file() or p.suffix.lower() not in MODEL3D_EXTENSIONS:
return {"ok": False, "error": "Not a 3D model"}
if not self._check_path_allowed(p):
@@ -1702,7 +1729,7 @@ class GalleryService:
(元ファイルへのフォールバックはできないため、失敗時はNoneを返す)。
PSDはpsd_toolsでレイヤーを合成してからJPEGとしてキャッシュする(同様にフォールバック不可)。
"""
p = Path(image_path).resolve()
p = self._resolve_untrusted(image_path)
if not p.is_file():
return None
if p.suffix.lower() not in IMAGE_EXTENSIONS:
+8 -2
View File
@@ -219,7 +219,10 @@ class SemanticService:
# ── 状態 ───────────────────────────────────────────────
def _scope_entries(self, folder: str, recursive: bool) -> list[tuple[str, str, int, float]]:
p = Path(folder).resolve()
try:
p = self.gallery._resolve_untrusted(folder) # UNC等は resolve() 前に文字列で拒否
except ValueError:
raise SemanticError("folder_not_allowed", str(folder))
if not p.is_dir() or not self.gallery._check_path_allowed(p):
raise SemanticError("folder_not_allowed", str(folder))
raw = self.gallery._scan_folder_recursive(p) if recursive else self.gallery._scan_folder(p)
@@ -539,7 +542,10 @@ class SemanticService:
def search_similar(self, path: str, folder: str, recursive: bool, limit: int, min_score: float = 0.0) -> list[dict]:
self._ensure_loaded()
p = Path(path).resolve()
try:
p = self.gallery._resolve_untrusted(path) # UNC等は resolve() 前に文字列で拒否
except ValueError:
raise SemanticError("folder_not_allowed", path)
if not p.is_file() or not self.gallery._check_path_allowed(p):
raise SemanticError("folder_not_allowed", path)
ap = p.as_posix()
+8 -1
View File
@@ -12,6 +12,8 @@ import threading
import urllib.error
import urllib.request
from pathlib import Path
from ..config import resolve_untrusted
from typing import Optional
from .url_guard import HINT as _URL_HINT, is_allowed_backend_url, urlopen as _guarded_urlopen
@@ -456,7 +458,12 @@ class TaggerService:
_ALLOWED_WRITE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"}
def write_meta_to_file(self, image_path: str, tags: str) -> dict:
path = Path(image_path).resolve()
try:
# UNC(\\host\share)は resolve() した時点で相手ホストへ資格情報付きで
# 接続してしまうため、文字列のまま先に拒否する
path = resolve_untrusted(image_path)
except ValueError:
return {"error": "Access denied"}
if path.suffix.lower() not in self._ALLOWED_WRITE_EXTS:
return {"error": "Unsupported file type"}
if not path.is_file():
+219
View File
@@ -0,0 +1,219 @@
"""Prompt translation service (native language -> English) via Ollama.
PLaMo 3 Translate (pfnet/plamo-3-translate-*-GGUF) models are driven with a raw
PFML3 prompt on /api/generate, which is the route the model card recommends for
Ollama. Any other Ollama model is used through /api/chat with a short
"translate into English" system prompt, so a general LLM or another MT model
can be selected too.
"""
import json
import re
import threading
import urllib.request
from collections import OrderedDict
from .settings_service import SettingsService
from .url_guard import require_backend_url, urlopen
DEFAULTS = {
"prompt_translate_enabled": False,
"prompt_translate_url": "http://localhost:11434",
"prompt_translate_model": "hf.co/pfnet/plamo-3-translate-2b-GGUF:Q4_K_M",
"prompt_translate_source_lang": "auto",
"prompt_translate_auto": False, # GenerateUI Generate
"prompt_translate_auto_lab": False, # GenerateUI Lab Run
"prompt_translate_auto_video": False, # Video Plan Run
"prompt_translate_unload": True,
# "gpu" (default) or "cpu". CPU mode keeps the model resident in system RAM:
# no VRAM use, so it never needs unloading and skips Ollama's ~4 s reload.
"prompt_translate_device": "gpu",
}
CPU_KEEP_ALIVE = -1 # stay loaded until Ollama restarts (about 2 GB RAM for 2B Q4_K_M)
# Suggested PLaMo 3 Translate builds (shown in Settings even before they are pulled).
PLAMO_PRESETS = [
"hf.co/pfnet/plamo-3-translate-0.8b-GGUF:Q4_K_M",
"hf.co/pfnet/plamo-3-translate-2b-GGUF:Q4_K_M",
"hf.co/pfnet/plamo-3-translate-8b-GGUF:Q4_K_M",
"hf.co/pfnet/plamo-3-translate-31b-GGUF:Q4_K_M",
]
_PLAMO_STOP = ["<|plamo:tag|>", "<|plamo:bos|>"]
_PLAMO_SPECIAL = re.compile(r"<\|plamo:[a-z_]+\|>")
_LANG_RE = re.compile(r"^[a-z]{2,3}(-[A-Za-z]{2,4})?$")
_MAX_TEXT_LEN = 4000
_MAX_TEXTS = 200
_CHAT_SYSTEM = (
"You translate text for image generation prompts into English. "
"Output only the English translation, with no explanations, quotes or notes. "
"Keep proper nouns, numbers and any ASCII text unchanged."
)
_cache: "OrderedDict[tuple, str]" = OrderedDict()
_CACHE_MAX = 1024
_cache_lock = threading.Lock()
_settings = SettingsService()
def get_config():
data = _settings.load()
cfg = {k: data.get(k, v) for k, v in DEFAULTS.items()}
cfg["prompt_translate_url"] = (cfg["prompt_translate_url"] or DEFAULTS["prompt_translate_url"]).rstrip("/")
cfg["prompt_translate_model"] = cfg["prompt_translate_model"] or DEFAULTS["prompt_translate_model"]
return cfg
def is_plamo(model: str) -> bool:
return "plamo" in (model or "").lower()
def build_pfml3(text: str, target_lang: str = "en", source_lang: str = "") -> str:
"""Build a PFML3 translation prompt (BOS is added by llama.cpp/Ollama)."""
src = _PLAMO_SPECIAL.sub("", text)
src_attr = f"<|plamo:key|>lang<|plamo:val|>{source_lang}" if source_lang else ""
return (
"<|plamo:tag|>dataset<|plamo:msg|>translation"
f"<|plamo:tag|>input{src_attr}<|plamo:msg|>{src}"
f"<|plamo:tag|>output<|plamo:key|>lang<|plamo:val|>{target_lang}<|plamo:msg|>"
)
def _post_json(url: str, payload: dict, timeout: float):
req = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def _num_predict(text: str) -> int:
# CJK -> English roughly doubles-to-triples the token count; leave headroom.
return max(64, min(2048, len(text) * 4))
def _device_options(device):
return {"num_gpu": 0} if device == "cpu" else {}
def _translate_one(base_url, model, text, source_lang, keep_alive, device="gpu"):
dev = _device_options(device)
if is_plamo(model):
data = _post_json(f"{base_url}/api/generate", {
"model": model,
"prompt": build_pfml3(text, "en", source_lang),
"raw": True,
"stream": False,
"keep_alive": keep_alive,
"options": {"temperature": 0, "num_predict": _num_predict(text), "stop": _PLAMO_STOP, **dev},
}, timeout=180)
out = data.get("response", "")
else:
data = _post_json(f"{base_url}/api/chat", {
"model": model,
"messages": [
{"role": "system", "content": _CHAT_SYSTEM},
{"role": "user", "content": text},
],
"stream": False,
"think": False,
"keep_alive": keep_alive,
"options": {"temperature": 0, "num_predict": _num_predict(text), **dev},
}, timeout=180)
out = (data.get("message") or {}).get("content", "")
out = _PLAMO_SPECIAL.sub("", out)
return out.strip()
def unload(base_url: str, model: str):
try:
_post_json(f"{base_url}/api/generate", {"model": model, "keep_alive": 0}, timeout=15)
except Exception:
pass
def translate_texts(texts, model=None, source_lang=None):
"""Translate each string in ``texts`` into English. Blocking (run in a thread).
Returns (translations, cached_count). Identical inputs are served from a
bounded cache keyed by (model, source_lang, text).
"""
if not isinstance(texts, list) or len(texts) > _MAX_TEXTS:
raise ValueError(f"texts must be a list of at most {_MAX_TEXTS} strings")
cfg = get_config()
base_url = require_backend_url(cfg["prompt_translate_url"])
model = (model or cfg["prompt_translate_model"]).strip()
src = (source_lang if source_lang is not None else cfg["prompt_translate_source_lang"]) or "auto"
src = "" if src == "auto" else src
if src and not _LANG_RE.match(src):
raise ValueError("invalid source language")
device = "cpu" if cfg.get("prompt_translate_device") == "cpu" else "gpu"
keep_alive = CPU_KEEP_ALIVE if device == "cpu" else "5m"
results = []
cached = 0
used_model = False
for raw in texts:
text = str(raw or "")[:_MAX_TEXT_LEN]
if not text.strip():
results.append(text)
continue
key = (model, src, text)
with _cache_lock:
hit = _cache.get(key)
if hit is not None:
_cache.move_to_end(key)
if hit is not None:
results.append(hit)
cached += 1
continue
out = _translate_one(base_url, model, text, src, keep_alive, device)
used_model = True
with _cache_lock:
_cache[key] = out
_cache.move_to_end(key)
while len(_cache) > _CACHE_MAX:
_cache.popitem(last=False)
results.append(out)
if used_model and device == "gpu" and cfg.get("prompt_translate_unload", True):
# Free VRAM for the image model that usually runs right after.
unload(base_url, model)
return results, cached
def warmup():
"""Load the translation model ahead of the first translation (CPU mode).
In CPU mode the model is kept resident, so loading it once in the background
when GenerateUI opens hides Ollama's ~4 s startup from the first translation.
Returns False when there is nothing to do.
"""
cfg = get_config()
if not cfg.get("prompt_translate_enabled") or cfg.get("prompt_translate_device") != "cpu":
return False
base_url = require_backend_url(cfg["prompt_translate_url"])
model = cfg["prompt_translate_model"]
# A request without a prompt only loads the model; the options decide CPU placement.
_post_json(f"{base_url}/api/generate", {
"model": model,
"keep_alive": CPU_KEEP_ALIVE,
"options": _device_options("cpu"),
}, timeout=180)
return True
def list_models():
cfg = get_config()
base_url = require_backend_url(cfg["prompt_translate_url"])
req = urllib.request.Request(f"{base_url}/api/tags")
with urlopen(req, timeout=5) as resp:
data = json.loads(resp.read().decode("utf-8"))
return [m.get("name") for m in data.get("models", []) if m.get("name")]
+6
View File
@@ -12,6 +12,8 @@ import os
import sys
from pathlib import Path
from ..config import is_safe_rel
logger = logging.getLogger(__name__)
try:
@@ -209,6 +211,10 @@ class VideoService:
base_dir = folder_paths.get_directory_by_type(type_)
if not base_dir:
raise ValueError(f"Invalid type: {type_}")
# UNC・ドライブ指定・絶対パス・「..」は、結合・resolve() の前に文字列のまま拒否する
# (Windows では UNC を resolve() するだけで相手ホストへ資格情報付きで接続するため)
if not is_safe_rel(filename) or (subfolder and not is_safe_rel(subfolder)):
raise ValueError("Access denied: path outside allowed directory")
base = Path(base_dir).resolve()
target = (base / (subfolder or "") / filename).resolve()
try:
+3 -2
View File
@@ -83,10 +83,11 @@ class WildcardService:
return None
# Normalize to forward slashes and strip leading/trailing slashes
filename = filename.replace("\\", "/").strip("/")
# Validate each path component — block traversal and empty parts
# Validate each path component — block traversal, empty parts and drive-relative
# names ("C:name"; on Python <=3.11 joinpath would replace the base path with them)
parts = filename.split("/")
for p in parts:
if not p or p == ".." or p == ".":
if not p or p == ".." or p == "." or ":" in p:
return None
ext = Path(parts[-1]).suffix.lstrip(".").lower()
if ext not in ALLOWED_EXTS:
+2 -1
View File
@@ -99,7 +99,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, video_routes, vram_routes, partner_routes, tagspace_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, vram_routes, partner_routes, tagspace_routes, translate_routes
workflow_routes.setup_routes(app)
settings_routes.setup_routes(app)
@@ -119,5 +119,6 @@ class WorkflowStudio:
video_routes.setup_routes(app)
vram_routes.setup_routes(app)
tagspace_routes.setup_routes(app)
translate_routes.setup_routes(app)
logger.info("Workflow Studio: Routes registered successfully")
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-workflow-studio"
description = "A comprehensive workflow, asset management, and generation UI plugin for ComfyUI. Manage workflows, models, images, and prompts; batch generation and Image Feeder in the GenerateUI tab; library side panel with canvas drag & drop and AI tools."
version = "0.9.5"
version = "0.9.9"
license = { file = "LICENSE" }
requires-python = ">= 3.10"
dependencies = ["jinja2"]
+45
View File
@@ -6451,3 +6451,48 @@ body {
max-width: none;
}
.wfm-compat-row .wfm-format-badge:empty { display: none; }
/* GenerateUI Settings tab: Switches column (true/false inputs) */
.wfm-settings-switches {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 420px;
overflow-y: auto;
}
.wfm-settings-switch {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
border: 1px solid var(--wfm-border);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.wfm-settings-switch:hover {
background: var(--wfm-surface);
}
.wfm-settings-switch-text {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.wfm-settings-switch-node {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wfm-settings-switch-key {
color: var(--wfm-text-secondary);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wfm-settings-switch-state {
font-family: monospace;
font-size: 11px;
color: var(--wfm-text-secondary);
}
+8 -2
View File
@@ -15,6 +15,7 @@ import {
imageUrlToJpegBase64, svgToJpegBase64, parseSvgParts, svgChecklist, DEFAULT_JUDGE_MODEL,
} from "./chat-image-check.js";
import { renderMarkdown } from "./chat-markdown.js";
import { isPlamoModel, buildPlamoMessages, cleanPlamoOutput } from "./plamo-chat.js";
import { buildWorkflowDetailContext, buildLogsContext } from "./chat-context.js";
import { openLabPlanData } from "./lab-tab.js";
import { listOcrModels, supportsBoxes, runOcr, renderOcrOverlay } from "./ocr-tools.js";
@@ -245,6 +246,8 @@ async function callLLM(url, backend, model, prompt, settings = {}) {
function buildTranslationMessages(text, srcLang, dstLang, settings) {
const srcName = srcLang === "free" ? (settings.freeSrcLang || "").trim() : LANG_NAMES[srcLang];
const dstName = dstLang === "free" ? (settings.freeDstLang || "English").trim() || "English" : LANG_NAMES[dstLang];
// PLaMo Translate ignores instruction prompts (see plamo-chat.js)
if (isPlamoModel(settings.model)) return buildPlamoMessages(text, srcName, dstName);
const fromPart = srcName ? ` from ${srcName}` : "";
const system = `You are a professional translator. Translate the text the user sends${fromPart} into ${dstName}. `
+ `Reply with ONLY the translated text and nothing else: no explanations, no notes, no preamble like "Here is the translation:", `
@@ -342,8 +345,11 @@ function initTranslateTab() {
try {
const messages = buildTranslationMessages(text, srcLangSel.value, dstLangSel.value, settings);
const { content } = await callChat(url, backend, model, messages, undefined, settings);
const cleaned = cleanTranslationOutput(content);
const plamo = isPlamoModel(model);
// PLaMo has no thinking mode and translates deterministically at temperature 0
const callSettings = plamo ? { ...settings, thinkingMode: false, temperature: 0 } : settings;
const { content } = await callChat(url, backend, model, messages, undefined, callSettings);
const cleaned = plamo ? cleanPlamoOutput(content) : cleanTranslationOutput(content);
outputEl.value = cleaned;
if (srcLangSel.value !== dstLangSel.value && looksUntranslated(text, cleaned)) {
+2
View File
@@ -523,6 +523,7 @@ function applyI18nToHtml() {
"wfm-help-gen-30": "helpGen30",
"wfm-help-gen-31": "helpGen31",
"wfm-help-gen-32": "helpGen32",
"wfm-help-gen-33": "helpGen33",
"wfm-help-feeder-title": "helpFeederTitle",
"wfm-help-feeder-desc": "helpFeederDesc",
"wfm-help-feeder-imgloop-title": "helpFeederImgloopTitle",
@@ -591,6 +592,7 @@ function applyI18nToHtml() {
"wfm-help-settings-20": "helpSettings20", "wfm-help-settings-21": "helpSettings21",
"wfm-help-settings-22": "helpSettings22",
"wfm-help-settings-23": "helpSettings23", "wfm-help-settings-24": "helpSettings24", "wfm-help-settings-25": "helpSettings25",
"wfm-help-settings-26": "helpSettings26",
"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",
+112 -1
View File
@@ -3,10 +3,12 @@
*/
import { comfyUI } from "./comfyui-client.js";
import { comfyWorkflow } from "./comfyui-workflow.js";
import { syncJsonHighlight } from "./json-highlight.js";
import { t } from "./i18n.js";
import { escapeHtml, setupSearchClearBtn } from "./util.js";
import { showToast } from "./app.js";
import { translateButtonsHtml, attachTranslateButtons } from "./prompt-translate.js";
// ── Latent Image preset state ─────────────────────────────
const _LATENT_PRESET_KEY = "wfm_latent_presets";
@@ -315,6 +317,104 @@ function _syncStackToggleAll(stackModels) {
cb.indeterminate = !allOn && !allOff;
}
// Two-option combos that are really an on/off switch (KSamplerAdvanced add_noise, DWPreprocessor
// detect_*, ...): [on value, off value], compared case-insensitively.
const _TOGGLE_COMBO_PAIRS = [["enable", "disable"], ["enabled", "disabled"], ["on", "off"], ["true", "false"], ["yes", "no"]];
function _comboOptions(spec) {
if (Array.isArray(spec?.[0])) return spec[0];
if (spec?.[0] === "COMBO" && Array.isArray(spec[1]?.options)) return spec[1].options;
return null;
}
/** [onValue, offValue] in the node's own spelling when the combo is an on/off pair, else null */
function _toggleComboValues(spec) {
const opts = _comboOptions(spec);
if (!opts || opts.length !== 2 || !opts.every((o) => typeof o === "string")) return null;
const lower = opts.map((o) => o.toLowerCase());
for (const [on, off] of _TOGGLE_COMBO_PAIRS) {
const iOn = lower.indexOf(on), iOff = lower.indexOf(off);
if (iOn >= 0 && iOff >= 0) return [opts[iOn], opts[iOff]];
}
return null;
}
function _compareNodeIds(a, b) {
const x = String(a).split(":").map(Number), y = String(b).split(":").map(Number);
for (let i = 0; i < Math.max(x.length, y.length); i++) {
const d = (x[i] ?? -1) - (y[i] ?? -1);
if (d) return d;
}
return 0;
}
/**
* On/off items of the API workflow for the Settings tab's Switches column: every literal
* true/false input (ComfySwitchNode switch, PrimitiveBoolean value, TextGenerate thinking, ...)
* plus string combos whose only two options are an on/off pair (needs object_info). Linked
* inputs are arrays and never match; their source node shows up instead.
*/
function _collectSwitchInputs(workflow, objectInfo = {}) {
const out = [];
for (const [id, node] of Object.entries(workflow || {})) {
if (!node?.class_type || !node.inputs) continue;
const info = objectInfo[node.class_type]?.input || {};
for (const [key, value] of Object.entries(node.inputs)) {
const base = { id, type: node.class_type, title: node._meta?.title || node.class_type, key };
if (typeof value === "boolean") {
out.push({ ...base, on: value, onValue: true, offValue: false });
} else if (typeof value === "string") {
const pair = _toggleComboValues(info.required?.[key] || info.optional?.[key]);
if (pair && (value === pair[0] || value === pair[1])) {
out.push({ ...base, on: value === pair[0], onValue: pair[0], offValue: pair[1] });
}
}
}
}
return out.sort((a, b) => _compareNodeIds(a.id, b.id));
}
let _switchRenderSeq = 0;
/** Fill the Switches column. Booleans show at once; on/off combos follow once object_info is in. */
async function _renderSwitchColumn(col) {
if (!col) return;
const seq = ++_switchRenderSeq;
const workflow = comfyUI.currentWorkflow;
const draw = (items) => {
col.innerHTML = items.length ? `<div class="wfm-settings-switches">${items.map((s, i) => `
<label class="wfm-settings-switch" title="${escapeHtml(`#${s.id} ${s.type} › ${s.key}`)}">
<input type="checkbox" class="wfm-settings-switch-cb" data-index="${i}" ${s.on ? "checked" : ""}>
<span class="wfm-settings-switch-text">
<span class="wfm-settings-switch-node">${escapeHtml(s.title)}</span>
<span class="wfm-settings-switch-key">${escapeHtml(s.key)}</span>
</span>
<span class="wfm-settings-switch-state">${escapeHtml(String(s.on ? s.onValue : s.offValue))}</span>
</label>`).join("")}
</div>` : `<p class="wfm-placeholder">${t("genSwitchesNone")}</p>`;
// Applies immediately (no Apply button): written straight into the API workflow Generate sends
col.querySelectorAll(".wfm-settings-switch-cb").forEach((cb) => {
cb.addEventListener("change", () => {
const s = items[Number(cb.dataset.index)];
const node = comfyUI.currentWorkflow?.[s.id];
if (!node?.inputs || !(s.key in node.inputs) || Array.isArray(node.inputs[s.key])) return;
s.on = cb.checked;
node.inputs[s.key] = s.on ? s.onValue : s.offValue;
const state = cb.closest(".wfm-settings-switch")?.querySelector(".wfm-settings-switch-state");
if (state) state.textContent = String(node.inputs[s.key]);
_syncRawJson();
});
});
};
draw(_collectSwitchInputs(workflow));
const types = [...new Set(Object.values(workflow || {}).map((n) => n?.class_type).filter(Boolean))];
let objectInfo = {};
try { objectInfo = await comfyWorkflow.getObjectInfoForTypes(types); } catch {}
// A newer render (another workflow loaded meanwhile) owns the column now
if (seq !== _switchRenderSeq || workflow !== comfyUI.currentWorkflow || !col.isConnected) return;
draw(_collectSwitchInputs(workflow, objectInfo));
}
/** Sync comfyUI.currentWorkflow to Raw JSON textarea + highlight */
function _syncRawJson() {
const rawTextarea = document.getElementById("wfm-gen-raw-json");
@@ -546,6 +646,7 @@ export const comfyEditor = {
${nodeOpts}
</select>
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-pos-revert" title="${t("revertPromptTitle")}">↺</button>
${translateButtonsHtml("wfm-prompt-pos-text")}
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-pos-apply" title="Apply (Alt+Click: Apply &amp; Generate)">Apply</button>
</div>
<textarea class="wfm-textarea" id="wfm-prompt-pos-text" rows="6">${positiveNodes[0]?.text || ""}</textarea>
@@ -558,6 +659,7 @@ export const comfyEditor = {
${nodeOpts}
</select>
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-neg-revert" title="${t("revertPromptTitle")}">↺</button>
${translateButtonsHtml("wfm-prompt-neg-text")}
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-neg-apply">Apply</button>
</div>
<textarea class="wfm-textarea" id="wfm-prompt-neg-text" rows="6">${negativeNodes[0]?.text || ""}</textarea>
@@ -620,6 +722,9 @@ export const comfyEditor = {
ta.value = wfNode.inputs[textKey] ?? "";
});
// 🌐 native-language prompt translation (Settings → Prompt Translation)
attachTranslateButtons(el);
// Track last focused prompt textarea for Paste button
["wfm-prompt-pos-text", "wfm-prompt-neg-text"].forEach((taId) => {
const ta = document.getElementById(taId);
@@ -1514,7 +1619,7 @@ export const comfyEditor = {
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-sampler-apply" title="Apply (Alt+Click: Apply &amp; Generate)">Apply</button>
` : "<p class='wfm-placeholder'>No KSampler node found</p>"}
</div>
<div style="flex:1;min-width:0;padding-left:14px;">
<div style="flex:1;min-width:0;padding:0 14px;border-right:1px solid var(--wfm-border);">
<h3 style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--wfm-text-secondary);margin:0 0 12px;">Latent Image</h3>
${latent ? `
<input type="hidden" id="wfm-settings-latent-id" value="${latent.id}">
@@ -1559,9 +1664,15 @@ export const comfyEditor = {
</div>
` : "<p class='wfm-placeholder'>No EmptyLatentImage node found</p>"}
</div>
<div style="flex:1;min-width:0;padding-left:14px;">
<h3 style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--wfm-text-secondary);margin:0 0 12px;">${t("genSwitchesTitle")}</h3>
<div id="wfm-settings-switches-col"></div>
</div>
</div>
`;
_renderSwitchColumn(document.getElementById("wfm-settings-switches-col"));
document.getElementById("wfm-settings-sampler-apply")?.addEventListener("click", (e) => {
const ds = document.getElementById("wfm-settings-sampler-id")?.dataset || {};
const write = (nodeId, key, value) => {
+28 -7
View File
@@ -125,6 +125,12 @@ function _collectNodeTypes(workflow) {
// frontend widgets that occupy a widgets_values slot; sent to the API as {"__value__": [...]}.
const _WIDGET_TYPE_SET = new Set(["INT", "FLOAT", "STRING", "BOOLEAN", "COMBO", "COMFY_DYNAMICCOMBO_V3", "BOUNDING_BOXES"]);
// Required inputs backed by display-only frontend widgets (serialize=false, so no widgets_values
// slot) -> the value the frontend sends for them in the API prompt.
const _DISPLAY_WIDGET_DEFAULTS = {
IMAGECOMPARE: () => ({ __value__: ["", ""] }),
};
/**
* Resolve a possibly-compound object_info type spec (e.g. LTXVEmptyLatentAudio's frame_rate:
* "FLOAT,INT", a MultiType declaration meaning "widget defaults to FLOAT but also accepts an INT
@@ -998,6 +1004,15 @@ export const comfyWorkflow = {
}
}
// Display-only frontend widgets (ImageCompare's socketless compare_view) are never
// stored in widgets_values, yet the backend requires them. The frontend sends the
// widget's initial value, so do the same.
for (const [k, spec] of Object.entries(objectInfo[node.type]?.input?.required || {})) {
if (!(k in inputs) && _DISPLAY_WIDGET_DEFAULTS[spec?.[0]]) {
inputs[k] = _DISPLAY_WIDGET_DEFAULTS[spec[0]]();
}
}
api[nodeId] = {
class_type: node.type,
inputs,
@@ -1284,9 +1299,10 @@ export const comfyWorkflow = {
// "prompt" (prompt-enhancer switch chain, Qwen Image 2.1 Turbo) is the positive text,
// a linked "negative_prompt" the negative.
// TextEncodeFibo2 (Fibo 2.0 Edit) has the same shape, its prompt arriving via a
// PreviewAny -> prompt-enhancer switch chain. PH3ImageConditioning (p-h3-image) too,
// without a negative prompt.
if (ct === "TextEncodeQwenImage21" || ct === "TextEncodeFibo2" || ct === "PH3ImageConditioning") {
// PreviewAny -> prompt-enhancer switch chain. PH3ImageConditioning (p-h3-image) and
// MiniMaxH3ImageConditioning (MiniMax-H3-Image) too, without a negative prompt.
if (ct === "TextEncodeQwenImage21" || ct === "TextEncodeFibo2" || ct === "PH3ImageConditioning"
|| ct === "MiniMaxH3ImageConditioning") {
if (Array.isArray(inputs.prompt)) samplerPositiveRef[inputs.prompt[0]] = true;
if (Array.isArray(inputs.negative_prompt)) samplerNegativeRef[inputs.negative_prompt[0]] = true;
}
@@ -1533,8 +1549,10 @@ export const comfyWorkflow = {
// nodes already picked up generically by the load_image_nodes pass below.
// TextEncodeFibo2 (Fibo 2.0) — same prompt/negative_prompt/images.image_N shape; its
// negative_prompt is a structured VGL JSON caption (edited as plain text here).
// PH3ImageConditioning (p-h3-image) has prompt and images.image_N, no negative_prompt.
if (ct === "TextEncodeQwenImage21" || ct === "TextEncodeFibo2" || ct === "PH3ImageConditioning") {
// PH3ImageConditioning (p-h3-image) and MiniMaxH3ImageConditioning (MiniMax-H3-Image) have
// prompt and images.image_N, no negative_prompt.
if (ct === "TextEncodeQwenImage21" || ct === "TextEncodeFibo2" || ct === "PH3ImageConditioning"
|| ct === "MiniMaxH3ImageConditioning") {
if (typeof inputs.prompt === "string") {
result.prompt_nodes.push({
id, type: ct, title: `${title} [positive]`, role: "positive",
@@ -1717,8 +1735,10 @@ export const comfyWorkflow = {
// (array link ref) rather than a direct number — leave those as undefined so
// the settings-tab UI falls back to its default instead of rendering a link ref.
// TextEncodeFibo2 likewise (0 = Fibo 2.0's trained size for the first image's aspect),
// and PH3ImageConditioning (0 = image_1's size, or 2912 without images).
if (ct === "TextEncodeMageFlowEdit" || ct === "TextEncodeFibo2" || ct === "PH3ImageConditioning") {
// PH3ImageConditioning (0 = image_1's size, or 2912 without images) and
// MiniMaxH3ImageConditioning (0 = image_1's size, or 2048 without images).
if (ct === "TextEncodeMageFlowEdit" || ct === "TextEncodeFibo2" || ct === "PH3ImageConditioning"
|| ct === "MiniMaxH3ImageConditioning") {
result.latent_nodes.push({
id, type: ct, title,
width: typeof inputs.width === "number" ? inputs.width : undefined,
@@ -1994,6 +2014,7 @@ function _getWidgetMapping(nodeType) {
TextEncodeQwenImage21: ["prompt", "negative_prompt", "resolution"],
TextEncodeFibo2: ["prompt", "negative_prompt", "width", "height", "batch_size"],
PH3ImageConditioning: ["prompt", "width", "height"],
MiniMaxH3ImageConditioning: ["prompt", "width", "height"],
TextEncodeMingImageEdit: ["prompt"],
EmptyQwenImageLayeredLatentImage: ["width", "height", "layers", "batch_size"],
};
+17
View File
@@ -12,6 +12,7 @@ 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";
import { autoTranslateBeforeGenerate } from "./prompt-translate.js";
// ============================================
// Gallery Metadata - ワークフロー保存
@@ -2032,6 +2033,22 @@ async function handleGenerate(workflowOverride = null, genOptions = {}) {
}
if (comfyUI.generating) return;
// 設定「プロンプト翻訳」で生成前の自動翻訳がオンなら、Positive/Negative欄の自国語部分を英語へ置き換えてから同期する。
// 失敗時は未翻訳のまま生成しないよう中断する。
if (!workflowOverride) {
const genBtnEl = document.getElementById("wfm-gen-generate-btn");
if (genBtnEl) genBtnEl.disabled = true;
try {
await autoTranslateBeforeGenerate();
} catch (err) {
if (genBtnEl) genBtnEl.disabled = false;
const progressText = document.getElementById("wfm-gen-progress-text");
if (progressText) progressText.textContent = "";
showToast(t("promptTranslateError", err.message), "error");
return;
}
}
// オーバーライド実行時は GenerateUI 側の表示中プロンプトを別ワークフローへ誤って書き込まないよう同期をスキップする
if (!workflowOverride) comfyEditor.syncToWorkflow();
+114 -3
View File
@@ -315,6 +315,8 @@ const LANGUAGES = {
// -- GenerateUI Input tab: Image sub-panel placeholder --
i2iPlaceholderTitle: "Placeholder Image",
genSwitchesTitle: "Switches",
genSwitchesNone: "No true/false settings in this workflow",
i2iPlaceholderColor: "Color",
i2iPlaceholderImage: "Image",
i2iPlaceholderWidth: "Width",
@@ -1054,7 +1056,7 @@ const LANGUAGES = {
helpGen4: "Input tab: Prompt (left) and Image (right, about 3:2) are shown side by side on one screen (drag-and-drop upload); when the Prompt/Image area gets narrow (about 720px or less) Image wraps under Prompt, and the Raw JSON column can be collapsed to a slim strip at the right edge with its » button (Input tab only; remembered); the Prompt area has Positive Prompt, Negative Prompt, and an Embeddings selector at the bottom — select a model, set Weight (default 1.0), then click Paste to insert (embedding:Name:weight) at the cursor position of the last focused textarea (defaults to Positive if neither is focused); a ↺ button next to each Apply button reverts the textarea to the node's currently applied value; select text (or just place the cursor on a word/parenthesis block) and press Ctrl+↑/↓ to adjust its emphasis weight in (text:weight) syntax by ±0.05 — the parentheses are removed automatically when the weight returns to 1.0",
helpGen5: "Model tab: Checkpoint, VAE, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — type and device selectors; DualCLIP shows two clip selectors) are always shown; ControlNet and Hypernetwork (with Strength field) are collapsed by default since they're used less often — click their header to expand; LoRA (Single/Stack tabs) has its own column to the right; all selectors have a filter; LoRA Stack — Apply writes all active LoRAs to the target node and simultaneously inserts LoRA syntax and CivitAI trigger words into the Positive prompt; re-applying updates the prompt by removing inactive LoRAs and re-adding active ones",
helpGen28: "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",
helpGen6: "Settings tab: KSampler and Latent Image side by side, each at 50% width",
helpGen6: "Settings tab: KSampler, Latent Image and Switches side by side. Switches lists every true/false value in the workflow (e.g. the prompt enhancer on/off switch, TextGenerate thinking) and every two-choice enable/disable, on/off or yes/no option (e.g. KSamplerAdvanced add_noise, DWPreprocessor detect_hand) with its node name; clicking one toggles it immediately (no Apply needed) and the change shows up in Raw JSON",
helpGen31: "LTX-2.5 and Wan2.2 video workflows also work when loaded as regular workflows in this tab (not just the dedicated Video tab): Positive/Negative Prompt detection follows LTX-2.5's LTXVDualCFGGuider / LTXVConditioning nodes to find the actual prompt text, and the Settings tab's Latent Image section recognizes Wan2.2's EmptyHunyuanLatentVideo and LTX-2.5's EmptyLTXVLatentVideo nodes (with an added Length (frames) field) instead of showing \"No EmptyLatentImage node found\"; generating from these workflows also gets the same extended timeout as MiniMax H3 video generations",
helpGen7: "Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow; if the loaded workflow has Bypass or Mute nodes, a note listing them appears above the search bar (Bypass nodes are excluded with their wires reconnected around them; Mute nodes are excluded with no reconnection, matching ComfyUI itself); the search bar highlights matches and scrolls to them without moving the cursor into the editor, so typing right after a search can't accidentally overwrite the JSON — click into the editor first to actually edit",
helpGen8: "Seed control: random, fixed, increment, decrement, workflow (use the workflow's own seed as-is, without overriding it)",
@@ -1078,6 +1080,7 @@ const LANGUAGES = {
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, every entry's strength/clipStrength for a Lora Manager node (its LoRA list is kept intact, not replaced), or every row's strength on both native Load LoRA nodes (Model and Text Encoder; rows kept) — 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",
helpGen32: "Partner API nodes (Nano Banana / Gemini image nodes, etc.): GenerateUI can run workflows that contain ComfyUI's partner API nodes. These nodes need Comfy credentials in the /prompt request — ComfyUI's own screen adds them automatically, but a separate page cannot — so add COMFY_API_KEY=... (a Comfy API key issued at platform.comfy.org) to the plugin's .env file and restart ComfyUI. With the key set, GenerateUI sends the request through the plugin server, which adds the key only when the workflow contains a partner API node (the key never reaches the browser); usage is billed as Comfy credits. A Gemini image node's prompt appears in the Prompt tab, and its reference images go through LoadImage nodes in the Image tab. Sample: ws_nano_banana_2_1_t2i",
helpGen33: "🌐 prompt translation: when Settings → Prompt Translation is on, a 🌐 button sits next to the Positive/Negative prompts (Input tab), the Positive/Negative fields of the Lab prompt modal, and the Video Plan block prompt. It translates only the parts written in your own language into English and leaves prompt syntax as it is — commas, (word:1.2), [ ], {a|b}, <lora:...>, __wildcards__, embedding:name and BREAK; \"Original\" puts back the text from before translation (also Ctrl+Z) while that field stays open. Settings can also translate automatically when Generate, Lab Run or Video Plan Run is pressed",
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",
@@ -1186,6 +1189,7 @@ const LANGUAGES = {
helpSettings23: "Models Folder: add extra model root folders (e.g. Stability Matrix's Models) — their subfolders (StableDiffusion / Lora / VAE / ControlNet / TextEncoders / DiffusionModels / Embeddings / HyperNetworks, or ComfyUI names like checkpoints / loras) are listed in the Models tab in addition to ComfyUI's own folders; separate several roots with ; — Apply saves, Default clears",
helpSettings24: "Windows Stability: \"Use the Selector event loop on Windows\" switches ComfyUI's asyncio loop from Proactor to Selector (applied at the next ComfyUI start). Turn it on if ComfyUI freezes entirely after switching Models lists that load many preview images; the trade-offs are no asyncio subprocess support and a connection limit of about 512. The same switch is available through the WFS_SELECTOR_LOOP=1 environment variable",
helpSettings25: "VRAM Management (Ollama): right before every image generation from Workflow Studio (GenerateUI, Lab, batches, Video, AI TOOL chat generation, etc.), unload the models Ollama has loaded to free VRAM. Before generating: \"Do nothing\" (default) / \"Unload only as much as needed when free VRAM is below the target\" (unloads the models using the most VRAM one at a time and stops once the target is reached) / \"Unload everything every time\". Free VRAM is measured with nvidia-smi (ComfyUI's own figure may not change when Ollama loads a model; non-NVIDIA GPUs use ComfyUI's value). Targets Ollama on localhost — the default 127.0.0.1:11434 plus the AI TOOL / Prompt / Tagger AI settings and the Decision Model when they use Ollama (other hosts only if allowed by WFS_ALLOWED_BACKEND_HOSTS); LM Studio etc. and ComfyUI's own models are not touched. Unloaded models reload the next time they are used; a toast appears when models are unloaded, the target can't be reached, or the step fails, and generation continues either way. \"Unload all now\" unloads every loaded Ollama model immediately; the same \"Unload all now\" is also in the sidepanel A tab → Settings → VRAM (Ollama). Running a workflow from the ComfyUI canvas itself is not covered",
helpSettings26: "Prompt Translation (native language → English): off by default. Translates prompts with a local Ollama model — PLaMo 3 Translate (0.8B / 2B / 8B / 31B; models not yet in Ollama can be fetched with the Download button) or any other Ollama model through a translation instruction. Options: source language (auto-detect or one of the 51 languages PLaMo supports), Ollama URL, where to run the model — GPU (about 0.1 s per phrase; with \"Unload after translating\" on, each translation first waits about 4 s for the model to reload) or CPU kept loaded (about 0.3 s per phrase, no VRAM, loaded in the background when GenerateUI opens) — and \"Translate automatically when running\" for GenerateUI Generate (replaces the prompt text with the translation), Lab Run and Video Plan Run (translate only what is sent; the plan keeps your text). If translation fails, nothing is generated. Test translates a sample prompt. 2B translates sentences more accurately than 0.8B. PLaMo 3 Translate is free for personal and non-commercial use (PLaMo Community License). PLaMo models also work for the AI TOOL / Prompt tab AI Assistant / sidepanel A tab translation buttons (the languages are passed in the format PLaMo expects)",
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",
@@ -1530,6 +1534,39 @@ const LANGUAGES = {
selectorLoopLabel: "Use the Selector event loop on Windows (restart required)",
selectorLoopHint: "Turn this on if ComfyUI freezes (all tabs stop responding, even generation) after switching Models lists with many previews. Switches the Windows asyncio loop from Proactor to Selector. Takes effect after restarting ComfyUI; asyncio subprocess features are unavailable and the connection limit is about 512. Leave off if you have no problem.",
selectorLoopSaved: "Saved. Restart ComfyUI to apply.",
promptTranslateSection: "Prompt Translation (native language → English)",
promptTranslateEnable: "Enable prompt translation in GenerateUI",
promptTranslateHint: "Type prompts in your own language and translate them into English with a local Ollama model (PLaMo 3 Translate by default). A 🌐 button appears next to the Positive/Negative prompts. Only parts containing non-ASCII letters are translated; commas, (weights:1.2), [ ], {a|b}, <lora:...>, __wildcards__, embedding:name and BREAK stay unchanged. PLaMo 3 Translate is free for personal and non-commercial use (PLaMo Community License).",
promptTranslateAutoHeading: "Translate automatically when running",
promptTranslateAuto: "GenerateUI — Generate",
promptTranslateAutoLab: "GenerateUI Lab — Run",
promptTranslateAutoVideo: "Video Plan — Run",
promptTranslateAutoHint: "Generate replaces the Positive/Negative text with the translation. Lab Run and Video Plan Run translate only what is sent, so the plan keeps your original text. If translation fails, nothing is generated.",
promptTranslateUnload: "Unload the translation model from VRAM after translating (slower next time, frees VRAM for image generation)",
promptTranslateSourceLang: "Source language",
promptTranslateSourceAuto: "Auto-detect",
promptTranslateUrl: "Ollama URL",
promptTranslateModel: "Translation model",
promptTranslateModelHint: "PLaMo 3 Translate sizes: 0.8B (0.6 GB, fastest), 2B (1.8 GB, recommended), 8B (5.3 GB), 31B (19.5 GB). Models not yet in Ollama can be downloaded with the Download button. Other Ollama models are translated through a chat prompt.",
promptTranslatePull: "Download",
promptTranslatePulling: "Downloading…",
promptTranslatePulled: "Model downloaded",
promptTranslateNotPulled: "Not downloaded",
promptTranslateNotPulledMark: "(not downloaded)",
promptTranslateOtherModels: "Other Ollama models",
promptTranslateSaved: "Prompt translation settings saved",
promptTranslateRunning: "Translating prompt…",
promptTranslateNothing: "Nothing to translate (no non-ASCII text)",
promptTranslateBtnTitle: "Translate the native-language parts of this prompt into English",
promptTranslateRestore: "Original",
promptTranslateRestoreTitle: "Restore the text before translation",
aiTranslating: "Translating…",
promptTranslateDevice: "Where to run the translation model",
promptTranslateDeviceGpu: "GPU (fastest per translation, uses VRAM)",
promptTranslateDeviceCpu: "CPU, kept loaded (no VRAM, no reload wait)",
promptTranslateDeviceHint: "CPU mode keeps the model in system RAM (about 2 GB for 2B) and loads it in the background when GenerateUI opens, so translations take about 0.3 s per phrase with no VRAM use and no ~4 s reload. GPU mode is faster per phrase (about 0.1 s) but, when unloading after translation is on, every translation waits about 4 s for the model to reload.",
promptTranslateError: (msg) => `Translation error: ${msg}`,
promptTranslateModelsError: (msg) => `Could not get the model list: ${msg}`,
settingsTitle: "Settings",
langLabel: "UI Language",
summaryLangLabel: "Summary Language",
@@ -2225,6 +2262,8 @@ const LANGUAGES = {
// -- GenerateUIタブ Inputサブタブ: Image内プレースホルダ --
i2iPlaceholderTitle: "プレースホルダ画像",
genSwitchesTitle: "スイッチ",
genSwitchesNone: "このワークフローに true/false の項目はありません",
i2iPlaceholderColor: "色",
i2iPlaceholderImage: "画像",
i2iPlaceholderWidth: "幅",
@@ -2964,7 +3003,7 @@ const LANGUAGES = {
helpGen4: "Inputタブ:Prompt(左)とImage(右、幅は約3:2)を同一画面に並べて表示(ドラッグ&ドロップ対応)。Prompt/Image領域の幅が約720px以下になるとImageがPromptの下へ折り返し、右端のRaw JSONは»ボタンで細い帯に折りたためる(Inputタブのみ・状態は記憶);PromptエリアにはPositive Prompt・Negative Promptに加え最下部にEmbeddingsセレクタを表示(Filter+セレクト+Weight入力+Pasteボタン)— Pasteは最後にフォーカスしたテキストエリアのカーソル位置に(embedding:Name:weight)を挿入(未フォーカス時はPositiveに追加);各Applyボタンの左隣の↺ボタンはテキストエリアをノードの現在の適用値に戻す;テキストを選択(または単語・括弧内にカーソルを置くだけでも可)してCtrl+↑/↓を押すと(text:weight)形式で強調weightを±0.05単位で調整(weightが1.0に戻ると括弧を自動解除)",
helpGen5: "Modelタブ:Checkpoint/VAE/Diffusion Model(UNETLoader / UnetLoaderGGUF / LoaderGGUF対応)/Text Encoder(CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF対応 — typeおよびdevice選択付き、DualCLIPは2段モデル選択)は常時表示;ControlNetとHypernetwork(Strength入力付き)は使用頻度が低いためデフォルトで折りたたみ — 見出しをクリックで展開;LoRA(Single/Stackタブ)は右側に独立した列を表示;すべてのセレクタにフィルタ検索付き;LoRA Stack — Applyで有効な全LoRAを対象ノードに書き込み、同時にLoRA構文とCivitAIトリガーワードをPositiveプロンプトへ挿入;再Applyでは非アクティブなLoRAを削除し、アクティブなLoRAを再追加してプロンプトを更新",
helpGen28: "Modelタブ — ハイライト色:読み込んだワークフローに実際に該当ノードが含まれる場合、各モデル種別(Checkpoint/VAE/Diffusion Model/Text Encoder/ControlNet/Hypernetwork)のラベルとLoRA列見出しが指定色(デフォルトは明るい緑)で表示される;ControlNet/Hypernetworkが折りたたまれた状態でも色は見えるため、展開しなくても使用有無が一目で分かる;色はSettingsタブでカスタマイズ可能",
helpGen6: "Settingsタブ:KSamplerとLatent Imageを横並び(各50%幅)",
helpGen6: "Settingsタブ:KSampler・Latent Image・スイッチの3列。スイッチ列にはワークフロー内の true/false の項目(プロンプト強化のオン/オフ、TextGenerateのthinkingなど)と、enable/disable・on/off・yes/no の2択項目(KSamplerAdvancedのadd_noise、DWPreprocessorのdetect_handなど)がノード名付きで並び、クリックで即座に切り替わる(Apply不要、Raw JSONにも反映)",
helpGen31: "LTX-2.5とWan2.2の動画ワークフローも、専用のVideoタブだけでなく本タブに通常ワークフローとして読み込んだ場合に対応 — Positive/Negative PromptはLTX-2.5のLTXVDualCFGGuider / LTXVConditioningノードをたどって実際のプロンプト文を検出し、SettingsタブのLatent ImageはWan2.2のEmptyHunyuanLatentVideoとLTX-2.5のEmptyLTXVLatentVideoノードを認識(Length(フレーム数)欄も追加表示)して「No EmptyLatentImage node found」にならない;これらのワークフローからの生成もMiniMax H3の動画生成と同じ延長タイムアウトが適用される",
helpGen7: "Raw JSONを各タブの右列に常時表示 — 直接編集してApplyでワークフロー即時反映;読み込んだワークフローにBypass/Muteノードがある場合、検索バー上部に一覧が表示される(Bypassは除外され配線が再接続される、Muteは除外されるが再接続はされない点はComfyUI本体と同じ);検索バーはヒット箇所をハイライト表示してスクロールするだけでエディタにカーソルを移動しないため、検索直後にキー入力してもJSONが誤って書き換わることはない — 編集する際はエディタ内をクリックしてから行う",
helpGen8: "シード管理:ランダム / 固定 / 増加 / 減少 / Workflow(ワークフロー側のシードをそのまま使用)",
@@ -2988,6 +3027,7 @@ const LANGUAGES = {
helpGen29: "バイパスボタン(⛔、LoRA列ヘッダー内、Refresh Stack groupボタンの隣):対象のLoRAノードの強度を強制的に0にします — 通常のLoraLoaderならstrength_model/strength_clip、Lora Managerノードならリスト内の全エントリのstrength/clipStrength(LoRAリスト自体は差し替えず維持)、ComfyUI標準の Load LoRA ノードなら両ノード(Model/Text Encoder)の全行のstrength(行は維持)が対象で、実際のComfyUIノードのmodeには一切触れません;もう一度クリックすると、切り替え前の値に正確に戻ります;対象はSingle/Stack Applyがデフォルトで対象とするノード(ワークフローにLora Managerノードがあればそれ、無ければ最初のLoRAノード);SingleまたはStackのApplyボタンをクリックするとバイパス状態はクリアされ、後から無言で再適用されることはありません",
helpGen30: "Saveボタン(サブタブバー右端)は、BatchまたはLabサブタブがアクティブな間は非表示になります — これらは現在読み込み中のワークフロー本体ではなく、ワークフローの複製や独自のPlanファイルを操作するためです。Input/Model/Settings/Feederでは再表示されます",
helpGen32: "パートナーAPIノード(Nano Banana/Gemini画像ノードなど):GenerateUIで、ComfyUIのパートナーAPIノードを含むワークフローを実行できます。これらのノードは /prompt リクエストにComfyの認証情報が必要で、ComfyUI本体の画面は自動で付けますが別ページからは付けられないため、プラグインの .env に COMFY_API_KEY=...(platform.comfy.org で発行するComfy APIキー)を追加して ComfyUI を再起動してください。キーを設定すると、GenerateUIはプラグインのサーバー経由でリクエストを送り、ワークフローにパートナーAPIノードがあるときだけキーを付与します(キーはブラウザへは出ません)。課金はComfyクレジットです。Gemini画像ノードのプロンプトはPromptタブに表示され、参考画像はImageタブのLoadImageノード経由で指定します。サンプル: ws_nano_banana_2_1_t2i",
helpGen33: "🌐 プロンプト翻訳:設定 →「プロンプト翻訳」をオンにすると、Positive/Negativeプロンプト(Inputタブ)、Labのプロンプトモーダルのpositive/negative欄、Video Planのブロックのプロンプト欄の横に🌐ボタンが出ます。自国語で書いた部分だけを英語に翻訳し、カンマ・(語句:1.2)・[ ]・{a|b}・<lora:...>・__ワイルドカード__・embedding:名前・BREAKなどのプロンプト記法はそのまま残します。「原文」で翻訳前の文に戻せます(その欄を開いている間はCtrl+Zでも可)。設定で、Generate・Lab Run・Video Plan Runの実行時に自動で翻訳することもできます",
helpFeederTitle: "GenerateUI Tab — Feeder サブタブ",
helpFeederDesc: "左ペイン上部の [Image Loop] / [Gallery] トグルで2つのモードを切り替えられます。",
helpFeederImgloopTitle: "Image Loop モード",
@@ -3096,6 +3136,7 @@ const LANGUAGES = {
helpSettings23: "モデルフォルダ:追加のモデルルートフォルダ(例: Stability Matrix の Models)を指定すると、その中のサブフォルダ(StableDiffusion / Lora / VAE / ControlNet / TextEncoders / DiffusionModels / Embeddings / HyperNetworks、またはcheckpoints / lorasなどComfyUI名)がComfyUI本来のフォルダに加えてModelsタブに表示されます。複数のルートは ; 区切り。「適用」で保存、「デフォルト」でクリア",
helpSettings24: "Windowsの安定性:「WindowsでSelectorイベントループを使用する」をオンにすると、ComfyUIのasyncioループがProactorからSelectorに切り替わります(次回のComfyUI起動時に反映)。プレビュー画像を多数読み込むModelsの一覧を切り替えたあとにComfyUIが完全に固まる場合にオンにしてください。トレードオフとして、asyncioのサブプロセス機能が使えず、同時接続数は約512が上限です。環境変数 WFS_SELECTOR_LOOP=1 でも同じ切り替えができます",
helpSettings25: "VRAM調整(Ollama):Workflow Studioからの画像生成(GenerateUI・Lab・バッチ・Video・AI TOOLのChat生成など、どこから実行しても同じ)の直前に、Ollamaでロード中のモデルをアンロードして空きVRAMを作ります。生成前の動作は「何もしない」(既定)/「空きが目標未満のときだけ必要な分をアンロード」(VRAM使用量の大きいモデルから1台ずつアンロードし、目標の空きに届いたら止める)/「毎回すべてアンロード」。空きVRAMはnvidia-smiの実測で判定します(ComfyUIが示す空きは、Ollamaがモデルを読み込んでも変わらないことがあるため。NVIDIA以外のGPUではComfyUIの値を使用)。対象はローカルホストのOllamaで、既定の127.0.0.1:11434と、Ollamaを使っているAI TOOL・Prompt・TaggerのAI設定および意思決定モデルです(他のホストはWFS_ALLOWED_BACKEND_HOSTSで許可した場合のみ)。LM Studio等のモデルやComfyUI自身のモデルは対象外です。アンロードしたモデルは次に使うときに読み込み直しになります。アンロードしたとき・目標に届かないとき・失敗したときはトーストで通知し、失敗しても生成は続けます。「今すぐすべてアンロード」でロード中のOllamaのモデルをその場でアンロードできます。サイドパネルのAタブ → Settings →「VRAM (Ollama)」にも同じ「Unload all now」ボタンがあります。ComfyUIのキャンバスで直接実行する生成は対象外です",
helpSettings26: "プロンプト翻訳(自国語 → 英語):既定はオフ。ローカルのOllamaモデルでプロンプトを翻訳します。PLaMo 3 Translate(0.8B/2B/8B/31B。Ollamaに未取得のモデルは「ダウンロード」ボタンで取得)のほか、他のOllamaモデルも翻訳指示付きで使えます。設定項目は、入力する言語(自動判定、またはPLaMoが対応する51言語)、Ollama URL、翻訳モデルの実行場所(GPU:1語句約0.1秒。「翻訳後にVRAMから解放」がオンだと毎回モデルの再読み込みで約4秒待つ/CPUで常駐:1語句約0.3秒・VRAMを使わず、生成UIを開いたときに裏で読み込む)、「実行時に自動で翻訳する」(生成UIのGenerate:欄の文を翻訳結果に置き換え/Lab Run・Video Plan Run:送信する内容だけ翻訳し、プランには元の文が残る)です。翻訳に失敗した場合は生成しません。「テスト」で見本のプロンプトを翻訳します。文章の翻訳は0.8Bより2Bのほうが正確です。PLaMo 3 Translateは個人・非商用利用なら無料です(PLaMoコミュニティライセンス)。PLaMoのモデルは、AI TOOL・PromptタブのAIアシスタント・サイドパネルAタブの翻訳でも使えます(PLaMoが求める形式で言語を指定して送ります)",
helpSettings9: "各セクションはアコーディオン式で折りたたみ可能",
helpSettings10: "Wildcard Integration:WFSのワイルドカードディレクトリをComfyUI-Impact-Packのwildcardsディレクトリへリンク(Windowsはジャンクション、他OSはシンボリックリンク)。既存ファイルは自動で移行されます",
helpSettings11: "テキストサイズ:スライダー1本(10〜28px)でプロンプト・チャット全テキストエリアのフォントサイズを一括調整 — GenerateUI ポジティブ/ネガティブプロンプト、AIアシスタントチャット入力、プリセット ポジティブ/ネガティブ、ワイルドカードプロンプト・ファイルエディタ、メタデータプロンプト全文プレビュー;即時反映・保存で確定",
@@ -3440,6 +3481,39 @@ const LANGUAGES = {
selectorLoopLabel: "WindowsでSelectorイベントループを使用する(要再起動)",
selectorLoopHint: "プレビュー画像が多いModelsの一覧を切り替えたあと、ComfyUIが固まる(全タブが無応答になり、生成もできなくなる)場合にオンにしてください。WindowsのasyncioループをProactorからSelectorに切り替えます。ComfyUIの再起動後に有効になります。asyncioのサブプロセス機能は使えなくなり、同時接続数は約512が上限です。問題が出ていなければオフのままで構いません。",
selectorLoopSaved: "保存しました。反映にはComfyUIの再起動が必要です。",
promptTranslateSection: "プロンプト翻訳(自国語 → 英語)",
promptTranslateEnable: "生成UIでプロンプト翻訳を使う",
promptTranslateHint: "プロンプトを自分の言語で入力し、ローカルのOllamaモデル(既定はPLaMo 3 Translate)で英語に翻訳します。Positive/Negativeプロンプトの横に🌐ボタンが表示されます。翻訳されるのは英数字以外の文字を含む部分だけで、カンマ・(重み:1.2)・[ ]・{a|b}・<lora:...>・__ワイルドカード__・embedding:名前・BREAK はそのまま残ります。PLaMo 3 Translateは個人・非商用利用なら無料です(PLaMoコミュニティライセンス)。",
promptTranslateAutoHeading: "実行時に自動で翻訳する",
promptTranslateAuto: "生成UI — Generate",
promptTranslateAutoLab: "生成UI Lab — Run",
promptTranslateAutoVideo: "Video Plan — Run",
promptTranslateAutoHint: "Generateでは、Positive/Negative欄の文が翻訳結果に置き換わります。Lab RunとVideo Plan Runでは送信する内容だけを翻訳し、プランには元の文が残ります。翻訳に失敗した場合は生成しません。",
promptTranslateUnload: "翻訳後に翻訳モデルをVRAMから解放する(次回の翻訳は遅くなるが、画像生成用のVRAMを空ける)",
promptTranslateSourceLang: "入力する言語",
promptTranslateSourceAuto: "自動判定",
promptTranslateUrl: "Ollama URL",
promptTranslateModel: "翻訳モデル",
promptTranslateModelHint: "PLaMo 3 Translateのサイズ: 0.8B(0.6GB・最速)、2B(1.8GB・推奨)、8B(5.3GB)、31B(19.5GB)。Ollamaに未取得のモデルは「ダウンロード」ボタンで取得できます。PLaMo以外のOllamaモデルはチャット形式の指示で翻訳します。",
promptTranslatePull: "ダウンロード",
promptTranslatePulling: "ダウンロード中…",
promptTranslatePulled: "モデルをダウンロードしました",
promptTranslateNotPulled: "未取得",
promptTranslateNotPulledMark: "(未取得)",
promptTranslateOtherModels: "その他のOllamaモデル",
promptTranslateSaved: "プロンプト翻訳の設定を保存しました",
promptTranslateRunning: "プロンプトを翻訳中…",
promptTranslateNothing: "翻訳する部分がありません(英数字以外の文字がありません)",
promptTranslateBtnTitle: "このプロンプトの自国語部分を英語に翻訳",
promptTranslateRestore: "原文",
promptTranslateRestoreTitle: "翻訳前のテキストに戻す",
aiTranslating: "翻訳中…",
promptTranslateDevice: "翻訳モデルの実行場所",
promptTranslateDeviceGpu: "GPU(1回の翻訳が最速・VRAMを使う)",
promptTranslateDeviceCpu: "CPUで常駐(VRAMを使わず、再読み込みの待ちなし)",
promptTranslateDeviceHint: "CPUモードではモデルをメインメモリに常駐させ(2Bで約2GB)、生成UIを開いたときに裏で読み込んでおきます。VRAMを使わず約4秒の再読み込みも発生しないため、1語句あたり約0.3秒で翻訳できます。GPUモードは1語句あたり約0.1秒と速い反面、「翻訳後にVRAMから解放」がオンだと毎回モデルの再読み込みで約4秒待ちます。",
promptTranslateError: (msg) => `翻訳エラー: ${msg}`,
promptTranslateModelsError: (msg) => `モデル一覧を取得できません: ${msg}`,
settingsTitle: "設定",
langLabel: "UI言語",
summaryLangLabel: "要約言語",
@@ -4135,6 +4209,8 @@ const LANGUAGES = {
// -- GenerateUI标签页 Input子标签: Image内占位图 --
i2iPlaceholderTitle: "占位图片",
genSwitchesTitle: "开关",
genSwitchesNone: "此工作流中没有 true/false 项",
i2iPlaceholderColor: "颜色",
i2iPlaceholderImage: "图片",
i2iPlaceholderWidth: "宽度",
@@ -4874,7 +4950,7 @@ const LANGUAGES = {
helpGen4: "Input标签:Prompt(左)与Image(右,宽度约3:2)并排显示在同一画面(支持拖放上传);当Prompt/Image区域宽度约720px以下时,Image会折行到Prompt下方,右侧的Raw JSON可通过»按钮折叠为细条(仅Input标签,状态会被记住);Prompt区域显示Positive Prompt、Negative Prompt,以及底部的Embeddings选择器(过滤+下拉+权重+Paste按钮)— Paste将(embedding:Name:weight)插入最后聚焦文本框的光标位置(未聚焦时默认追加到Positive);每个Apply按钮左侧的↺按钮可将文本框恢复为节点当前已应用的值;选中文字(或将光标置于单词/括号内也可)后按Ctrl+↑/↓,即可以(text:weight)语法以±0.05为单位调整强调权重(权重恢复为1.0时自动去除括号)",
helpGen5: "Model标签:Checkpoint/VAE/Diffusion Model(支持UNETLoader / UnetLoaderGGUF / LoaderGGUF)/Text Encoder(支持CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — 含type和device选择器,DualCLIP显示两段模型选择)始终显示;ControlNet和Hypernetwork(带Strength输入)因使用频率较低默认折叠 — 点击标题展开;LoRA(Single/Stack标签)拥有独立的列;所有选择器均含筛选;LoRA Stack — 点击Apply会将所有启用的LoRA写入目标节点,并同时将LoRA语法和CivitAI触发词插入Positive提示词;再次Apply会移除未启用的LoRA并重新添加启用的LoRA以更新提示词",
helpGen28: "Model标签 — 高亮颜色:当前工作流实际包含某类型节点时(Checkpoint/VAE/Diffusion Model/Text Encoder/ControlNet/Hypernetwork),对应标签及LoRA列标题会以指定颜色(默认亮绿色)显示;即使ControlNet/Hypernetwork处于折叠状态该颜色依然可见,无需展开即可判断是否在使用;颜色可在Settings标签中自定义",
helpGen6: "Settings标签:KSampler与Latent Image并排显示(各占50%宽度)",
helpGen6: "Settings标签:KSampler、Latent Image与开关三列并排。开关列列出工作流中所有 true/false 项(如提示词增强开关、TextGenerate 的 thinking)以及 enable/disable、on/off、yes/no 二选一项(如 KSamplerAdvanced 的 add_noise、DWPreprocessor 的 detect_hand)并显示节点名,点击即可立即切换(无需 Apply,Raw JSON 同步更新)",
helpGen31: "LTX-2.5和Wan2.2视频工作流在本标签中作为普通工作流加载时(不仅限于专用的Video标签)也可正常使用 — Positive/Negative Prompt会沿LTX-2.5的LTXVDualCFGGuider / LTXVConditioning节点追溯找到实际提示词文本,Settings标签的Latent Image部分能识别Wan2.2的EmptyHunyuanLatentVideo和LTX-2.5的EmptyLTXVLatentVideo节点(并新增Length(帧数)字段),不再显示\"No EmptyLatentImage node found\";从这些工作流生成时也会应用与MiniMax H3视频生成相同的延长超时",
helpGen7: "Raw JSON在每个标签右列常驻显示 — 直接编辑并Apply即时重载工作流;如果加载的工作流包含Bypass或Mute节点,搜索栏上方会显示列表(Bypass节点会被排除但其连线会重新连接;Mute节点会被排除且不重新连接,与ComfyUI本体行为一致);搜索栏只会高亮匹配项并滚动到该位置,不会将光标移入编辑器,因此搜索后立即按键不会意外覆盖JSON — 需要先点击编辑器内部才能实际编辑",
helpGen8: "种子管理:随机/固定/递增/递减/Workflow(直接使用工作流自带的种子)",
@@ -4898,6 +4974,7 @@ const LANGUAGES = {
helpGen29: "绕过按钮(⛔,位于LoRA列表头,紧邻Refresh Stack group):将目标LoRA节点的强度强制设为0——普通LoraLoader对应strength_model/strength_clip,Lora Manager节点则对应列表中每一项的strength/clipStrength(LoRA列表本身保持不变,不会被替换),ComfyUI原生 Load LoRA 节点则对应两个节点(Model/Text Encoder)所有行的strength(行保持不变),不会改动实际ComfyUI节点的mode;再次点击可精确恢复切换前的数值;作用于Single/Stack Apply默认所用的节点(工作流中若有Lora Manager节点则用它,否则用第一个LoRA节点);点击Single或Stack的Apply按钮会清除绕过状态,避免其在之后被无声地重新生效",
helpGen30: "Save按钮(子标签栏右端)在Batch或Lab子标签处于激活状态时会隐藏,因为它们操作的是工作流副本或各自的Plan文件,而不是当前加载的工作流本身;切换回Input/Model/Settings/Feeder时会重新显示",
helpGen32: "合作伙伴API节点(Nano Banana/Gemini图像节点等):GenerateUI可以运行包含ComfyUI合作伙伴API节点的工作流。这些节点需要在 /prompt 请求中携带Comfy认证信息——ComfyUI自带界面会自动添加,独立页面则无法添加——因此请在插件的 .env 中添加 COMFY_API_KEY=...(在 platform.comfy.org 获取的Comfy API密钥)并重启ComfyUI。设置密钥后,GenerateUI会通过插件服务器发送请求,仅当工作流包含合作伙伴API节点时才附加密钥(密钥不会到达浏览器);费用按Comfy积分计算。Gemini图像节点的提示词显示在Prompt标签中,参考图通过Image标签的LoadImage节点指定。示例:ws_nano_banana_2_1_t2i",
helpGen33: "🌐 提示词翻译:在 设置 →“提示词翻译”中启用后,Positive/Negative 提示词(Input 标签)、Lab 提示词弹窗的 Positive/Negative 栏以及 Video Plan 区块的提示词栏旁会出现🌐按钮。只把用母语写的部分翻译成英语,逗号、(词语:1.2)、[ ]、{a|b}、<lora:...>、__通配符__、embedding:名称、BREAK 等提示词语法保持不变。“原文”可恢复翻译前的文本(该栏打开期间也可用 Ctrl+Z)。还可以在设置中让 Generate、Lab Run、Video Plan Run 运行时自动翻译",
helpFeederTitle: "GenerateUI Tab — Feeder 子标签",
helpFeederDesc: "通过左侧面板顶部的 [图片循环] / [图库] 切换按钮可在两种模式间切换。",
helpFeederImgloopTitle: "图片循环模式",
@@ -5006,6 +5083,7 @@ const LANGUAGES = {
helpSettings23: "模型文件夹:可添加额外的模型根文件夹(例如 Stability Matrix 的 Models),其中的子文件夹(StableDiffusion / Lora / VAE / ControlNet / TextEncoders / DiffusionModels / Embeddings / HyperNetworks,或 checkpoints / loras 等ComfyUI名称)会与ComfyUI自带文件夹一起显示在Models标签页;多个根文件夹用 ; 分隔,“应用”保存,“默认”清除",
helpSettings24: "Windows 稳定性:开启“在Windows上使用Selector事件循环”会把ComfyUI的asyncio循环从Proactor切换为Selector(下次启动ComfyUI时生效)。如果切换加载大量预览图的Models列表后ComfyUI完全卡死,请开启。代价是不支持asyncio子进程,并发连接数上限约为512。也可通过环境变量 WFS_SELECTOR_LOOP=1 切换",
helpSettings25: "显存调整(Ollama):在 Workflow Studio 每次生成图像之前(GenerateUI・Lab・批量・Video・AI TOOL 的 Chat 生成等,无论从哪里执行都相同),卸载 Ollama 已加载的模型以腾出显存。生成前的操作:“不做任何操作”(默认)/“仅在空闲显存低于目标时卸载所需的部分”(从显存占用最大的模型开始逐个卸载,达到目标后停止)/“每次全部卸载”。空闲显存以 nvidia-smi 的实测值判断(ComfyUI 显示的空闲显存在 Ollama 加载模型后可能不变;非 NVIDIA GPU 使用 ComfyUI 的值)。对象为本机的 Ollama:默认的 127.0.0.1:11434,以及使用 Ollama 的 AI TOOL・Prompt・Tagger 的 AI 设置和决策模型(其他主机仅在 WFS_ALLOWED_BACKEND_HOSTS 允许时)。LM Studio 等的模型和 ComfyUI 自身的模型不在范围内。卸载的模型在下次使用时会重新加载;卸载时、未达到目标时或失败时会以提示通知,失败也会继续生成。“立即全部卸载”可当场卸载所有已加载的 Ollama 模型。侧面板的 A 标签 → Settings →“VRAM (Ollama)”中也有相同的“Unload all now”按钮。直接在 ComfyUI 画布上运行的生成不在范围内",
helpSettings26: "提示词翻译(母语 → 英语):默认关闭。使用本地 Ollama 模型翻译提示词:PLaMo 3 Translate(0.8B/2B/8B/31B;Ollama 中尚未下载的模型可用“下载”按钮获取),也可通过翻译指令使用其他 Ollama 模型。设置项包括:输入语言(自动检测或 PLaMo 支持的 51 种语言)、Ollama URL、翻译模型的运行位置(GPU:每个短语约 0.1 秒;开启“翻译后从显存卸载”时,每次都要等待约 4 秒重新加载模型/CPU 常驻:每个短语约 0.3 秒,不占显存,打开生成UI时在后台加载),以及“运行时自动翻译”(生成UI 的 Generate:用翻译结果替换栏中文本/Lab Run・Video Plan Run:只翻译发送的内容,计划中保留原文)。翻译失败时不会生成。“测试”会翻译一段示例提示词。翻译句子时 2B 比 0.8B 更准确。PLaMo 3 Translate 可免费用于个人和非商业用途(PLaMo 社区许可证)。PLaMo 模型也可用于 AI TOOL、Prompt 标签的 AI 助手和侧面板 A 标签的翻译(以 PLaMo 要求的格式指定语言)",
helpSettings9: "各部分可折叠展开",
helpSettings10: "Wildcard Integration:将WFS通配符目录链接到ComfyUI-Impact-Pack的wildcards目录(Windows为目录联结,其他OS为符号链接);现有文件自动迁移",
helpSettings11: "文字大小:单个滑块(10–28 px)可一次性调整所有提示词和聊天文本框的字体大小 — GenerateUI正/负向提示词、AI助手聊天输入、预设正/负向提示、通配符提示和文件编辑器、元数据提示词全文预览;即时生效并通过「保存设置」保存",
@@ -5350,6 +5428,39 @@ const LANGUAGES = {
selectorLoopLabel: "在Windows上使用Selector事件循环(需重启)",
selectorLoopHint: "如果在切换含大量预览图的Models列表后ComfyUI卡死(所有标签页无响应,也无法生成),请开启此项。它会把Windows的asyncio循环从Proactor切换为Selector。重启ComfyUI后生效;asyncio子进程功能将不可用,并发连接数上限约为512。没有问题时请保持关闭。",
selectorLoopSaved: "已保存。重启ComfyUI后生效。",
promptTranslateSection: "提示词翻译(母语 → 英语)",
promptTranslateEnable: "在生成UI中启用提示词翻译",
promptTranslateHint: "用自己的语言输入提示词,并通过本地Ollama模型(默认PLaMo 3 Translate)翻译成英语。Positive/Negative提示词旁会显示🌐按钮。只翻译包含非ASCII字母的部分;逗号、(权重:1.2)、[ ]、{a|b}、<lora:...>、__通配符__、embedding:名称和BREAK保持不变。PLaMo 3 Translate可免费用于个人和非商业用途(PLaMo社区许可证)。",
promptTranslateAutoHeading: "运行时自动翻译",
promptTranslateAuto: "生成UI — Generate",
promptTranslateAutoLab: "生成UI Lab — Run",
promptTranslateAutoVideo: "Video Plan — Run",
promptTranslateAutoHint: "Generate会将Positive/Negative栏的文本替换为翻译结果。Lab Run和Video Plan Run只翻译发送的内容,计划中保留原文。翻译失败时不会生成。",
promptTranslateUnload: "翻译后从显存卸载翻译模型(下次翻译变慢,但为图像生成腾出显存)",
promptTranslateSourceLang: "输入语言",
promptTranslateSourceAuto: "自动检测",
promptTranslateUrl: "Ollama URL",
promptTranslateModel: "翻译模型",
promptTranslateModelHint: "PLaMo 3 Translate尺寸:0.8B(0.6GB,最快)、2B(1.8GB,推荐)、8B(5.3GB)、31B(19.5GB)。Ollama中尚未下载的模型可用“下载”按钮获取。其他Ollama模型通过聊天指令进行翻译。",
promptTranslatePull: "下载",
promptTranslatePulling: "下载中…",
promptTranslatePulled: "模型已下载",
promptTranslateNotPulled: "未下载",
promptTranslateNotPulledMark: "(未下载)",
promptTranslateOtherModels: "其他Ollama模型",
promptTranslateSaved: "已保存提示词翻译设置",
promptTranslateRunning: "正在翻译提示词…",
promptTranslateNothing: "没有需要翻译的部分(没有非ASCII文字)",
promptTranslateBtnTitle: "将此提示词中的母语部分翻译成英语",
promptTranslateRestore: "原文",
promptTranslateRestoreTitle: "恢复翻译前的文本",
aiTranslating: "翻译中…",
promptTranslateDevice: "翻译模型的运行位置",
promptTranslateDeviceGpu: "GPU(单次翻译最快,占用显存)",
promptTranslateDeviceCpu: "CPU常驻(不占显存,无需等待重新加载)",
promptTranslateDeviceHint: "CPU模式将模型常驻在内存中(2B约2GB),并在打开生成UI时于后台加载。由于不占用显存,也不会发生约4秒的重新加载,每个短语约0.3秒即可完成翻译。GPU模式每个短语约0.1秒更快,但开启“翻译后从显存卸载”时,每次翻译都要等待约4秒重新加载模型。",
promptTranslateError: (msg) => `翻译错误: ${msg}`,
promptTranslateModelsError: (msg) => `无法获取模型列表: ${msg}`,
settingsTitle: "设置",
langLabel: "UI语言",
summaryLangLabel: "摘要语言",
+31 -3
View File
@@ -19,6 +19,7 @@ import { syncJsonHighlight, syncScroll } from "./json-highlight.js";
import { _expandWildcardsInWorkflow } from "./generate-tab.js";
import { pmGroups, getPresetsInGroup } from "./prompt-presets.js";
import { isVideoFilename, extractLastFrameBlob } from "./video-utils.js";
import { translateButtonsHtml, attachTranslateButtons, isAutoTranslateOn, translateWorkflowPrompts } from "./prompt-translate.js";
// Checkpoint/LoRA/VAE are three fully independent keyframe timelines — same as
// Prompt/KSampler — each with its own +/-, its own atIteration sequence, its own
@@ -834,11 +835,17 @@ function _openCellModal(col, idx) {
<button type="button" id="wfm-lab-modal-clear" class="wfm-btn wfm-btn-sm">${t("labClearPrompt")}</button>
</div>
<div class="wfm-lab-modal-row">
<label>Positive</label>
<div style="display:flex;align-items:center;gap:6px;">
<label style="flex:1;">Positive</label>
${translateButtonsHtml("wfm-lab-modal-positive")}
</div>
<textarea id="wfm-lab-modal-positive" class="wfm-input" rows="3">${escapeHtml(v.positive || "")}</textarea>
</div>
<div class="wfm-lab-modal-row">
<label>Negative</label>
<div style="display:flex;align-items:center;gap:6px;">
<label style="flex:1;">Negative</label>
${translateButtonsHtml("wfm-lab-modal-negative")}
</div>
<textarea id="wfm-lab-modal-negative" class="wfm-input" rows="3">${escapeHtml(v.negative || "")}</textarea>
</div>
<div class="wfm-lab-modal-checkbox">
@@ -915,7 +922,10 @@ function _openCellModal(col, idx) {
<button id="wfm-lab-modal-save" class="wfm-btn wfm-btn-primary">${t("save")}</button>
</div>`;
openModal(`${col} #${idx + 1}`, html);
if (col === "prompt") _wireLabPromptModalExtras(idx);
if (col === "prompt") {
_wireLabPromptModalExtras(idx);
attachTranslateButtons(document.getElementById("wfm-lab-modal-fields") || document);
}
if (col === "checkpoint" || col === "vae") _wireLabValueFilter(col);
if (col === "lora") _wireLabLoraFilter();
@@ -1452,6 +1462,10 @@ async function _runLabBatch() {
// mid-batch (see comfyWorkflow.isVideoWorkflow and generate-tab.js's own use of this).
const isVideoWf = comfyWorkflow.isVideoWorkflow(comfyUI.currentAnalysis);
// Settings -> Prompt Translation -> "Lab Run": translate each iteration's prompts
// (the plan itself keeps the original text).
const autoTranslate = await isAutoTranslateOn("lab");
let completed = 0, failed = 0;
let prevLoraInjection = null;
try {
@@ -1465,6 +1479,20 @@ async function _runLabBatch() {
const { workflow, seed, loraInjection } = _buildWorkflowForIteration(i, chainRef, prevLoraInjection);
prevLoraInjection = loraInjection;
const workflowExpanded = await _expandWildcardsInWorkflow(workflow);
if (autoTranslate) {
try {
if (progressText) progressText.textContent = `[${i}/${_lab.batchCount}] ${t("promptTranslateRunning")}`;
await translateWorkflowPrompts(workflowExpanded, comfyUI.currentAnalysis.prompt_nodes);
} catch (err) {
// Do not send untranslated text - stop the batch like GenerateUI does.
// The end-of-batch "stopped" toast replaces this one, so also keep the
// reason in the progress line.
showToast(t("promptTranslateError", err.message), "error");
if (progressText) progressText.textContent = t("promptTranslateError", err.message);
_aborted = true;
break;
}
}
try {
const { images } = await comfyUI.generate(workflowExpanded, {
+2 -1
View File
@@ -672,7 +672,8 @@ function extractPromptsAPI(wf) {
const srcNode = wf[String(val[0])];
// TextEncodeFibo2(Fibo 2.0)も同形。promptはPreviewAny→プロンプト強化スイッチ経由でリンクされる。
if (srcNode?.class_type === "TextEncodeMageFlowEdit" || srcNode?.class_type === "TextEncodeBooguEdit"
|| srcNode?.class_type === "TextEncodeFibo2" || srcNode?.class_type === "PH3ImageConditioning") {
|| srcNode?.class_type === "TextEncodeFibo2" || srcNode?.class_type === "PH3ImageConditioning"
|| srcNode?.class_type === "MiniMaxH3ImageConditioning") {
const raw = isPos ? srcNode.inputs?.prompt : srcNode.inputs?.negative_prompt;
const text = Array.isArray(raw) ? resolveLinkedText(wf, raw[0], raw[1] ?? 0) : raw;
if (text && typeof text === "string") { if (isPos) pos.add(text); else neg.add(text); }
+74
View File
@@ -0,0 +1,74 @@
/**
* PLaMo Translate over a chat API (Ollama /api/chat, LM Studio, ...).
*
* PLaMo Translate is a translation-only model. Its chat template treats the user
* message as source text and a system message that does not start with
* "[plamo-translate]" as mere context, so instruction prompts are translated or
* ignored and the direction falls back to Japanese <-> English auto-switching.
* Per the model card, chat clients pass the languages in a leading
* "[plamo-translate]" system message and send the text alone.
*
* Keep in sync with the inline copy in web/comfyui/node_sets_menu.js (side panel A tab),
* which cannot import from static/.
*/
// Languages PLaMo 3 Translate supports.
export const PLAMO_LANGS = [
"ar", "az", "be", "bg", "bn", "ca", "ceb", "de", "en", "es", "et", "eu", "fa", "fr", "gu", "he",
"hi", "hr", "hu", "hy", "id", "it", "ja", "jv", "ka", "kk", "km", "ko", "lt", "lv", "ml", "mr",
"ms", "my", "ne", "nl", "pa", "pl", "ru", "si", "sk", "sl", "sr", "sw", "ta", "te", "th", "tl",
"ur", "uz", "vi", "zh",
];
export const PLAMO_STOP = ["<|plamo:tag|>", "<|plamo:bos|>"];
export function isPlamoModel(model) {
return /plamo/i.test(model || "");
}
let _nameMap = null;
function _buildNameMap() {
const map = new Map();
const locales = ["en", "ja", "zh", typeof navigator !== "undefined" ? navigator.language : "en"];
for (const code of PLAMO_LANGS) {
map.set(code, code);
for (const loc of [...locales, code]) { // `code` adds the endonym (français, 한국어, ...)
try {
const name = new Intl.DisplayNames([loc], { type: "language" }).of(code);
if (name) map.set(name.toLowerCase(), code);
} catch { /* Intl.DisplayNames unavailable */ }
}
}
// Common spellings Intl does not produce
[["chinese", "zh"], ["中文", "zh"], ["中国語", "zh"], ["filipino", "tl"], ["tagalog", "tl"], ["persian", "fa"], ["farsi", "fa"]]
.forEach(([k, v]) => map.set(k, v));
return map;
}
/** "ja" / "Japanese" / "日本語" / "French" -> PLaMo language code, or "" when unknown. */
export function plamoLangCode(nameOrCode) {
const key = (nameOrCode || "").trim().toLowerCase();
if (!key) return "";
if (!_nameMap) _nameMap = _buildNameMap();
return _nameMap.get(key) || _nameMap.get(key.split(/[-_]/)[0]) || "";
}
/**
* Chat messages for translating ``text`` with PLaMo. ``src``/``dst`` are codes or
* language names; an unknown source is left to auto-detection. Throws when the
* target language is not one PLaMo supports.
*/
export function buildPlamoMessages(text, src, dst) {
const dstCode = plamoLangCode(dst);
if (!dstCode) throw new Error(`PLaMo Translate does not support the target language "${dst}"`);
const srcCode = plamoLangCode(src);
const header = `[plamo-translate]\ntarget_lang=${dstCode}` + (srcCode && srcCode !== dstCode ? `\nsource_lang=${srcCode}` : "");
return [
{ role: "system", content: header },
{ role: "user", content: text },
];
}
export function cleanPlamoOutput(text) {
return (text || "").replace(/<\|plamo:[a-z_]+\|>/g, "").trim();
}
+57 -9
View File
@@ -46,30 +46,37 @@ function _stripThinkingTags(text) {
}
// Ollamaは `think` (bool) と `options.num_predict`、OpenAI互換は `max_tokens` で制御
function _applyGenOptions(body, backend, cfg) {
// extra: { temperature, stop, noThink } — 翻訳専用モデル(PLaMo)向けの追加指定
function _applyGenOptions(body, backend, cfg, extra = {}) {
if (backend === "ollama") {
body.think = !!cfg.thinkingMode;
if (cfg.maxTokens > 0) body.options = { ...(body.options || {}), num_predict: cfg.maxTokens };
} else if (cfg.maxTokens > 0) {
body.max_tokens = cfg.maxTokens;
if (!extra.noThink) body.think = !!cfg.thinkingMode;
const opts = { ...(body.options || {}) };
if (cfg.maxTokens > 0) opts.num_predict = cfg.maxTokens;
if (extra.temperature !== undefined) opts.temperature = extra.temperature;
if (extra.stop) opts.stop = extra.stop;
if (Object.keys(opts).length) body.options = opts;
} else {
if (cfg.maxTokens > 0) body.max_tokens = cfg.maxTokens;
if (extra.temperature !== undefined) body.temperature = extra.temperature;
if (extra.stop) body.stop = extra.stop;
}
return body;
}
async function chatWithAi(model, messages) {
async function chatWithAi(model, messages, extra = {}) {
const cfg = getPromptAiConfig();
const { backend, url } = cfg;
if (backend === "ollama") {
const res = await fetch(`${url}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(_applyGenOptions({ model, messages, stream: false }, backend, cfg)),
body: JSON.stringify(_applyGenOptions({ model, messages, stream: false }, backend, cfg, extra)),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = (await res.json()).message?.content || "";
return cfg.thinkingMode ? text : _stripThinkingTags(text);
} else if (backend === "unsloth") {
const body = _applyGenOptions({ model, messages: _toOpenAiMessages(messages), stream: false }, backend, cfg);
const body = _applyGenOptions({ model, messages: _toOpenAiMessages(messages), stream: false }, backend, cfg, extra);
const data = await unslothProxy(url, "/v1/chat/completions", "POST", body);
const msg = data.choices?.[0]?.message;
// Unslothはreasoningを別フィールドで返すため<think>タグに畳んで表示に混ぜる
@@ -80,7 +87,7 @@ async function chatWithAi(model, messages) {
const res = await fetch(`${url}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(_applyGenOptions({ model, messages: _toOpenAiMessages(messages), stream: false }, backend, cfg)),
body: JSON.stringify(_applyGenOptions({ model, messages: _toOpenAiMessages(messages), stream: false }, backend, cfg, extra)),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = (await res.json()).choices?.[0]?.message?.content || "";
@@ -141,6 +148,25 @@ function addChatMessage(role, content) {
chat.scrollTop = chat.scrollHeight;
}
// 応答待ちの間だけ表示するプレースホルダ(初回はモデル読み込みで十数秒かかることがある)
function _addPendingMessage(text) {
const chat = document.getElementById("wfm-ollama-chat");
if (!chat) return null;
const msg = document.createElement("div");
msg.className = "wfm-ollama-msg assistant wfm-ollama-pending";
const roleLabel = document.createElement("div");
roleLabel.className = "wfm-ollama-msg-role";
roleLabel.textContent = "AI";
const contentDiv = document.createElement("div");
contentDiv.className = "wfm-ollama-msg-content";
contentDiv.style.opacity = "0.6";
contentDiv.textContent = text;
msg.append(roleLabel, contentDiv);
chat.appendChild(msg);
chat.scrollTop = chat.scrollHeight;
return msg;
}
function clearChat() {
ollamaState.chatHistory = [];
ollamaState.attachedImage = null;
@@ -243,13 +269,16 @@ async function sendMessage() {
const sendBtn = document.getElementById("wfm-ollama-send-btn");
if (sendBtn) sendBtn.disabled = true;
const pending = _addPendingMessage("…");
try {
const reply = await chatWithAi(model, ollamaState.chatHistory);
pending?.remove();
addChatMessage("assistant", reply);
ollamaState.chatHistory.push({ role: "assistant", content: reply });
ollamaState.attachedImage = null;
updateAttachmentDisplay();
} catch (err) {
pending?.remove();
showToast(t("error") + ": " + err.message, "error");
} finally {
if (sendBtn) sendBtn.disabled = false;
@@ -281,7 +310,24 @@ async function sendTranslate(direction) {
.map((id) => document.getElementById(id));
btns.forEach((b) => { if (b) b.disabled = true; });
const pending = _addPendingMessage(t("aiTranslating"));
try {
// PLaMo Translate is a translation-only model: anything in the user message is
// treated as source text, so an instruction prompt gets translated too, and without
// an explicit target it only switches between Japanese and English (ZH→EN / EN→ZH
// came out in Japanese). Per its model card, Ollama/LM Studio clients pass the
// languages in a leading "[plamo-translate]" system message and send the text alone.
if (/plamo/i.test(model)) {
const [src, dst] = { ja2en: ["ja", "en"], en2ja: ["en", "ja"], zh2en: ["zh", "en"], en2zh: ["en", "zh"] }[direction];
const reply = await chatWithAi(model, [
{ role: "system", content: `[plamo-translate]\ntarget_lang=${dst}\nsource_lang=${src}` },
{ role: "user", content: text },
], { temperature: 0, stop: ["<|plamo:tag|>", "<|plamo:bos|>"], noThink: true });
pending?.remove();
addChatMessage("assistant", reply.replace(/<\|plamo:[a-z_]+\|>/g, "").trim());
return;
}
const prompts = {
ja2en: `Translate the following Japanese text into English. Output only the translated text, nothing else.\n\n${text}`,
en2ja: `以下の英語のテキストを日本語に翻訳してください。翻訳結果のみを出力し、他の説明は不要です。\n\n${text}`,
@@ -291,8 +337,10 @@ async function sendTranslate(direction) {
const prompt = prompts[direction];
const reply = await chatWithAi(model, [{ role: "user", content: prompt }]);
pending?.remove();
addChatMessage("assistant", reply);
} catch (err) {
pending?.remove();
showToast(t("error") + ": " + err.message, "error");
} finally {
btns.forEach((b) => { if (b) b.disabled = false; });
+279
View File
@@ -0,0 +1,279 @@
/**
* Settings tab section for Prompt Translate (see prompt-translate.js).
* Values live in the server settings.json so the backend can read them.
*/
import { showToast } from "./app.js";
import { t, getLang } from "./i18n.js";
import { escapeHtml } from "./util.js";
import { setTranslateConfig, translatePrompt } from "./prompt-translate.js";
const DEFAULT_URL = "http://localhost:11434";
const DEFAULT_MODEL = "hf.co/pfnet/plamo-3-translate-2b-GGUF:Q4_K_M";
// Languages PLaMo 3 Translate supports (English is the target, so it is left out).
const SOURCE_LANGS = [
"ja", "zh", "ko", "ar", "az", "be", "bg", "bn", "ca", "ceb", "de", "es", "et", "eu", "fa", "fr",
"gu", "he", "hi", "hr", "hu", "hy", "id", "it", "jv", "ka", "kk", "km", "lt", "lv", "ml", "mr",
"ms", "my", "ne", "nl", "pa", "pl", "ru", "si", "sk", "sl", "sr", "sw", "ta", "te", "th", "tl",
"ur", "uz", "vi",
];
const SAMPLE_TEXT = {
ja: "夕焼けの海辺に立つ少女、(長い黒髪:1.2), masterpiece",
zh: "站在夕阳海边的少女, (长长的黑发:1.2), masterpiece",
};
function _langName(code) {
try {
const dn = new Intl.DisplayNames([getLang() || "en"], { type: "language" });
return dn.of(code) || code;
} catch {
return code;
}
}
export function promptTranslateSettingsHtml(s) {
const enabled = !!s.prompt_translate_enabled;
const src = s.prompt_translate_source_lang || "auto";
const unload = s.prompt_translate_unload !== false;
const cpu = s.prompt_translate_device === "cpu";
return `
<details class="wfm-settings-section" id="wfm-pt-settings">
<summary class="wfm-settings-summary">${t("promptTranslateSection")}</summary>
<div class="wfm-form-group">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="wfm-pt-enabled" ${enabled ? "checked" : ""}>
<span>${t("promptTranslateEnable")}</span>
</label>
<small style="color:var(--wfm-text-secondary);font-size:11px;display:block;margin-top:4px;">
${t("promptTranslateHint")}
</small>
</div>
<div id="wfm-pt-body" style="${enabled ? "" : "opacity:0.55;"}">
<div class="wfm-form-group">
<label>${t("promptTranslateAutoHeading")}</label>
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="wfm-pt-auto" ${s.prompt_translate_auto ? "checked" : ""}>
<span>${t("promptTranslateAuto")}</span>
</label>
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;margin-top:2px;">
<input type="checkbox" id="wfm-pt-auto-lab" ${s.prompt_translate_auto_lab ? "checked" : ""}>
<span>${t("promptTranslateAutoLab")}</span>
</label>
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;margin-top:2px;">
<input type="checkbox" id="wfm-pt-auto-video" ${s.prompt_translate_auto_video ? "checked" : ""}>
<span>${t("promptTranslateAutoVideo")}</span>
</label>
<small style="color:var(--wfm-text-secondary);font-size:11px;display:block;margin-top:4px;">
${t("promptTranslateAutoHint")}
</small>
</div>
<div class="wfm-form-group">
<label>${t("promptTranslateDevice")}</label>
<select class="wfm-select" id="wfm-pt-device" style="width:100%;">
<option value="gpu" ${cpu ? "" : "selected"}>${t("promptTranslateDeviceGpu")}</option>
<option value="cpu" ${cpu ? "selected" : ""}>${t("promptTranslateDeviceCpu")}</option>
</select>
<label id="wfm-pt-unload-label" style="display:flex;align-items:center;gap:6px;cursor:pointer;margin-top:6px;${cpu ? "opacity:0.5;" : ""}">
<input type="checkbox" id="wfm-pt-unload" ${unload ? "checked" : ""} ${cpu ? "disabled" : ""}>
<span>${t("promptTranslateUnload")}</span>
</label>
<small style="color:var(--wfm-text-secondary);font-size:11px;display:block;margin-top:4px;">
${t("promptTranslateDeviceHint")}
</small>
</div>
<div class="wfm-form-group">
<label>${t("promptTranslateSourceLang")}</label>
<select class="wfm-select" id="wfm-pt-source-lang" style="width:100%;">
<option value="auto" ${src === "auto" ? "selected" : ""}>${t("promptTranslateSourceAuto")}</option>
${SOURCE_LANGS.map((c) => `<option value="${c}" ${src === c ? "selected" : ""}>${escapeHtml(_langName(c))} (${c})</option>`).join("")}
</select>
</div>
<div class="wfm-form-group">
<label>${t("promptTranslateUrl")}</label>
<div style="display:flex;gap:8px;">
<input type="text" class="wfm-input" id="wfm-pt-url" value="${escapeHtml(s.prompt_translate_url || "")}" placeholder="${DEFAULT_URL}">
<button class="wfm-btn wfm-btn-sm" id="wfm-pt-url-save">${t("save")}</button>
</div>
</div>
<div class="wfm-form-group">
<label>${t("promptTranslateModel")}</label>
<div style="display:flex;gap:8px;">
<select class="wfm-select" id="wfm-pt-model" style="flex:1;min-width:0;">
<option value="${escapeHtml(s.prompt_translate_model || DEFAULT_MODEL)}" selected>${escapeHtml(s.prompt_translate_model || DEFAULT_MODEL)}</option>
</select>
<button class="wfm-btn wfm-btn-sm" id="wfm-pt-model-refresh">${t("refresh")}</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-pt-model-pull" style="display:none;">${t("promptTranslatePull")}</button>
</div>
<small id="wfm-pt-model-hint" style="color:var(--wfm-text-secondary);font-size:11px;display:block;margin-top:4px;">
${t("promptTranslateModelHint")}
</small>
</div>
<div class="wfm-form-group">
<div style="display:flex;gap:8px;align-items:center;">
<button class="wfm-btn wfm-btn-sm" id="wfm-pt-test">${t("test")}</button>
<span id="wfm-pt-test-status" style="font-size:12px;word-break:break-all;"></span>
</div>
</div>
</div>
</details>`;
}
export function initPromptTranslateSettings(serverSettings, saveServerSettings) {
const $ = (id) => document.getElementById(id);
let installed = [];
let presets = [];
const save = async (patch, toast = true) => {
try {
const res = await saveServerSettings(patch);
if (res?.error) throw new Error(res.error);
Object.assign(serverSettings, patch);
setTranslateConfig(patch);
if (toast) showToast(t("promptTranslateSaved"), "success");
return true;
} catch (err) {
showToast(t("errorWithMsg", err.message), "error");
return false;
}
};
const currentModel = () => serverSettings.prompt_translate_model || DEFAULT_MODEL;
const renderModelOptions = () => {
const sel = $("wfm-pt-model");
if (!sel) return;
const cur = currentModel();
const plamoInstalled = installed.filter((m) => /plamo/i.test(m));
// Embedding-only models cannot generate text, so they are not offered.
const others = installed.filter((m) => !/plamo/i.test(m) && !/embed|minilm/i.test(m));
const missingPresets = presets.filter((p) => !installed.includes(p));
const opt = (v, label) => `<option value="${escapeHtml(v)}" ${v === cur ? "selected" : ""}>${escapeHtml(label || v)}</option>`;
let html = "";
if (plamoInstalled.length) html += `<optgroup label="PLaMo 3 Translate">${plamoInstalled.map((m) => opt(m)).join("")}</optgroup>`;
if (missingPresets.length) html += `<optgroup label="${escapeHtml(t("promptTranslateNotPulled"))}">${missingPresets.map((m) => opt(m, `${m} ${t("promptTranslateNotPulledMark")}`)).join("")}</optgroup>`;
if (others.length) html += `<optgroup label="${escapeHtml(t("promptTranslateOtherModels"))}">${others.map((m) => opt(m)).join("")}</optgroup>`;
if (![...installed, ...presets].includes(cur)) html = opt(cur) + html;
sel.innerHTML = html;
updatePullButton();
};
const updatePullButton = () => {
const btn = $("wfm-pt-model-pull");
if (!btn) return;
const m = currentModel();
btn.style.display = presets.includes(m) && !installed.includes(m) ? "" : "none";
};
const refreshModels = async (quiet = false) => {
try {
const res = await fetch("/api/wfm/translate/models");
const data = await res.json();
presets = data.presets || [];
if (data.status !== "success") throw new Error(data.message || `HTTP ${res.status}`);
installed = data.models || [];
} catch (err) {
installed = [];
if (!quiet) showToast(t("promptTranslateModelsError", err.message), "error");
}
renderModelOptions();
};
// CPU mode keeps the model resident: preload it now, and release it when leaving CPU mode
// or turning the feature off so it does not sit in RAM unused.
const postTranslate = (path) => fetch(`/api/wfm/translate/${path}`, { method: "POST" }).catch(() => {});
const isCpu = () => serverSettings.prompt_translate_device === "cpu";
$("wfm-pt-device")?.addEventListener("change", async (e) => {
const device = e.target.value === "cpu" ? "cpu" : "gpu";
const prev = isCpu() ? "cpu" : "gpu";
if (!(await save({ prompt_translate_device: device }))) { e.target.value = prev; return; }
const cpu = device === "cpu";
const cb = $("wfm-pt-unload");
if (cb) cb.disabled = cpu;
const lbl = $("wfm-pt-unload-label");
if (lbl) lbl.style.opacity = cpu ? "0.5" : "";
// Ollama reloads the model for the new placement; drop the old instance first.
await postTranslate("unload");
if (cpu) postTranslate("warmup");
});
$("wfm-pt-enabled")?.addEventListener("change", async (e) => {
const on = !!e.target.checked;
if (!(await save({ prompt_translate_enabled: on }))) { e.target.checked = !on; return; }
if (isCpu()) postTranslate(on ? "warmup" : "unload");
const body = $("wfm-pt-body");
if (body) body.style.opacity = on ? "" : "0.55";
});
[["wfm-pt-auto", "prompt_translate_auto"], ["wfm-pt-auto-lab", "prompt_translate_auto_lab"], ["wfm-pt-auto-video", "prompt_translate_auto_video"]]
.forEach(([id, key]) => {
$(id)?.addEventListener("change", async (e) => {
const on = !!e.target.checked;
if (!(await save({ [key]: on }))) e.target.checked = !on;
});
});
$("wfm-pt-unload")?.addEventListener("change", async (e) => {
const on = !!e.target.checked;
if (!(await save({ prompt_translate_unload: on }))) e.target.checked = !on;
});
$("wfm-pt-source-lang")?.addEventListener("change", (e) => save({ prompt_translate_source_lang: e.target.value }));
$("wfm-pt-url-save")?.addEventListener("click", async () => {
const url = ($("wfm-pt-url")?.value || "").trim();
if (await save({ prompt_translate_url: url })) refreshModels();
});
$("wfm-pt-model")?.addEventListener("change", async (e) => {
const cpu = isCpu() && serverSettings.prompt_translate_enabled;
if (cpu) await postTranslate("unload"); // release the previous resident model
await save({ prompt_translate_model: e.target.value });
if (cpu) postTranslate("warmup");
updatePullButton();
});
$("wfm-pt-model-refresh")?.addEventListener("click", () => refreshModels());
$("wfm-pt-model-pull")?.addEventListener("click", async (e) => {
const btn = e.currentTarget;
const model = currentModel();
btn.disabled = true;
const label = btn.textContent;
btn.textContent = t("promptTranslatePulling");
try {
const res = await fetch("/api/wfm/translate/pull", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model }),
});
const data = await res.json();
if (data.status !== "success") throw new Error(data.message || `HTTP ${res.status}`);
showToast(t("promptTranslatePulled"), "success");
await refreshModels(true);
} catch (err) {
showToast(t("errorWithMsg", err.message), "error");
} finally {
btn.disabled = false;
btn.textContent = label;
}
});
$("wfm-pt-test")?.addEventListener("click", async (e) => {
const btn = e.currentTarget;
const status = $("wfm-pt-test-status");
const sample = SAMPLE_TEXT[getLang()] || SAMPLE_TEXT.ja;
btn.disabled = true;
if (status) { status.style.color = "var(--wfm-text-secondary)"; status.textContent = t("promptTranslateRunning"); }
const t0 = performance.now();
try {
const out = await translatePrompt(sample);
const sec = ((performance.now() - t0) / 1000).toFixed(1);
if (status) { status.style.color = "var(--wfm-success, #4caf50)"; status.textContent = `${sample} → ${out} (${sec}s)`; }
} catch (err) {
if (status) { status.style.color = "var(--wfm-danger, #f44336)"; status.textContent = err.message; }
} finally {
btn.disabled = false;
}
});
// Fill the model list when the section is first opened (avoids an Ollama call on every Settings visit).
const section = $("wfm-pt-settings");
section?.addEventListener("toggle", () => { if (section.open && !installed.length) refreshModels(true); });
}
+265
View File
@@ -0,0 +1,265 @@
/**
* Prompt Translate — type GenerateUI prompts in your own language and translate
* them into English with a local Ollama model (PLaMo 3 Translate by default).
*
* Works like the A1111 prompt-translator extensions: only the parts of a prompt
* that contain non-ASCII letters are sent to the model. Prompt syntax such as
* commas, (weights:1.2), [ ], {a|b}, <lora:...>, __wildcards__,
* embedding:name and BREAK/AND is kept as it is, so a mixed prompt like
* "masterpiece, (赤いドレスの女性:1.2), <lora:foo:0.8>" only translates the Japanese part.
*/
import { showToast } from "./app.js";
import { t } from "./i18n.js";
let _config = null;
let _configPromise = null;
// textarea -> { original, translated } for the "restore original" button
const _history = new WeakMap();
export async function loadTranslateConfig(force = false) {
if (_config && !force) return _config;
if (!_configPromise || force) {
_configPromise = fetch("/api/wfm/translate/config")
.then((r) => r.json())
.then((d) => { _config = d.config || {}; return _config; })
.catch(() => { _config = {}; return _config; })
.finally(() => { _configPromise = null; });
}
return _configPromise;
}
export function getTranslateConfig() {
return _config || {};
}
export function isTranslateEnabled() {
return !!_config?.prompt_translate_enabled;
}
/** Called by the Settings tab after saving, so open UIs update without a reload. */
export function setTranslateConfig(patch) {
_config = { ...(_config || {}), ...patch };
updateTranslateButtons();
}
// ------------------------------------------------------------
// Prompt splitting
// ------------------------------------------------------------
// Spans that are never translated: prompt syntax and delimiters.
const PROTECTED_RE = /\b(?:BREAK|AND)\b|<[^<>\n]*>|__[^\s_][^\s]*?__|embedding:[^\s,()[\]{}|]+|:\s*-?\d+(?:\.\d+)?|[,\n()[\]{}|]/g;
// A piece needs translation when it contains a non-ASCII letter (CJK, Cyrillic, accented Latin, ...).
const NEEDS_TRANSLATION_RE = /(?![\x00-\x7F])\p{L}/u;
/** Split a prompt into [{ text, translate }] pieces. Joining all .text gives back the input. */
export function splitPrompt(text) {
const pieces = [];
let last = 0;
for (const m of text.matchAll(PROTECTED_RE)) {
if (m.index > last) pieces.push(text.slice(last, m.index));
pieces.push({ keep: m[0] });
last = m.index + m[0].length;
}
if (last < text.length) pieces.push(text.slice(last));
return pieces.map((p) => {
if (typeof p !== "string") return { text: p.keep, translate: false };
return { text: p, translate: NEEDS_TRANSLATION_RE.test(p) };
});
}
export function needsTranslation(text) {
return splitPrompt(text || "").some((p) => p.translate);
}
// Keep the surrounding whitespace of a piece and tidy the model output for prompt use.
function _rebuild(source, translated) {
const lead = source.match(/^\s*/)[0];
const trail = source.match(/\s*$/)[0];
const core = source.trim();
let out = (translated || "").replace(/\s*\n+\s*/g, " ").trim();
// A tag such as "赤いドレス" should not become "Red dress." — drop the final period
// unless the source itself was a full sentence.
if (!/[。..!?!?]$/.test(core)) out = out.replace(/[.。]+$/, "");
// "少女、(" -> "girl, (" rather than "girl,(" — a Japanese comma needs no space, an English one does.
if (!trail && /[,;:]$/.test(out)) out += " ";
return lead + out + trail;
}
/** Translate a whole prompt string. Throws on backend errors. */
export async function translatePrompt(text) {
const pieces = splitPrompt(text || "");
const cores = [...new Set(pieces.filter((p) => p.translate).map((p) => p.text.trim()))];
if (cores.length === 0) return text;
const res = await fetch("/api/wfm/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ texts: cores }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data.status !== "success") {
throw new Error(data.message || `HTTP ${res.status}`);
}
const map = new Map(cores.map((c, i) => [c, data.translations[i] ?? c]));
return pieces.map((p) => (p.translate ? _rebuild(p.text, map.get(p.text.trim())) : p.text)).join("");
}
// Replace textarea text while keeping the browser's Ctrl+Z history where possible.
function _setTextareaValue(ta, value) {
ta.focus();
ta.select();
let ok = false;
try { ok = document.execCommand("insertText", false, value); } catch { ok = false; }
if (!ok || ta.value !== value) ta.value = value;
ta.dispatchEvent(new Event("input", { bubbles: true }));
}
/**
* Translate one textarea in place. Returns true when the text changed.
* The original text is remembered for the restore button.
*/
export async function translateTextarea(ta) {
if (!ta || ta.disabled) return false;
const original = ta.value;
if (!needsTranslation(original)) return false;
const translated = await translatePrompt(original);
if (translated === original) return false;
_setTextareaValue(ta, translated);
_history.set(ta, { original, translated });
_updateRestoreButton(ta);
return true;
}
export function restoreTextarea(ta) {
const h = ta && _history.get(ta);
if (!h || ta.disabled) return;
_setTextareaValue(ta, h.original);
_history.delete(ta);
_updateRestoreButton(ta);
}
const PROMPT_TEXTAREAS = ["wfm-prompt-pos-text", "wfm-prompt-neg-text"];
/**
* GenerateUI hook: when translation and "translate before Generate" are both on,
* translate the Positive/Negative prompt textareas. Throws if translation fails so
* the caller can stop instead of sending untranslated text.
*/
export async function autoTranslateBeforeGenerate() {
if (!(await isAutoTranslateOn("generate"))) return false;
const targets = PROMPT_TEXTAREAS.map((id) => document.getElementById(id))
.filter((ta) => ta && needsTranslation(ta.value));
if (targets.length === 0) return false;
const progress = document.getElementById("wfm-gen-progress-text");
if (progress) progress.textContent = t("promptTranslateRunning");
for (const ta of targets) await translateTextarea(ta);
if (progress) progress.textContent = "";
return true;
}
// Settings key for each "translate automatically when running" checkbox.
const AUTO_KEYS = {
generate: "prompt_translate_auto",
lab: "prompt_translate_auto_lab",
video: "prompt_translate_auto_video",
};
/** True when translation is enabled and the auto checkbox for ``kind`` (generate/lab/video) is on. */
export async function isAutoTranslateOn(kind) {
await loadTranslateConfig();
return isTranslateEnabled() && !!_config[AUTO_KEYS[kind]];
}
/**
* Lab Run hook: translate the prompt texts inside one iteration's API-format workflow
* copy, in place. The Lab plan's own keyframe values stay in the user's language;
* repeated phrases come from the server-side cache, so later iterations cost ~nothing.
* Throws on translation failure so the caller can stop the batch.
*/
export async function translateWorkflowPrompts(workflow, promptNodes) {
for (const n of promptNodes || []) {
const inputs = workflow?.[n.id]?.inputs;
const key = n.textKey || "text";
if (inputs && typeof inputs[key] === "string" && needsTranslation(inputs[key])) {
inputs[key] = await translatePrompt(inputs[key]);
}
}
return workflow;
}
// ------------------------------------------------------------
// Buttons in the GenerateUI Prompt tab
// ------------------------------------------------------------
function _updateRestoreButton(ta) {
const btn = document.querySelector(`.wfm-pt-restore[data-target="${ta.id}"]`);
if (!btn) return;
const h = _history.get(ta);
btn.style.display = isTranslateEnabled() && h && ta.value === h.translated ? "" : "none";
}
export function updateTranslateButtons() {
const on = isTranslateEnabled();
document.querySelectorAll(".wfm-pt-translate").forEach((b) => { b.style.display = on ? "" : "none"; });
document.querySelectorAll(".wfm-pt-restore").forEach((b) => {
const ta = document.getElementById(b.dataset.target);
if (ta) _updateRestoreButton(ta);
});
}
/**
* Wire the 🌐 / restore buttons produced by translateButtonsHtml() inside ``root``
* (GenerateUI prompt tab, Lab prompt modal, Video Plan block prompt, ...).
*/
export function attachTranslateButtons(root = document) {
root.querySelectorAll(".wfm-pt-translate").forEach((btn) => {
btn.addEventListener("click", async () => {
const ta = document.getElementById(btn.dataset.target);
if (!ta) return;
if (!needsTranslation(ta.value)) {
showToast(t("promptTranslateNothing"), "info");
return;
}
btn.disabled = true;
const label = btn.textContent;
btn.textContent = "…";
try {
await translateTextarea(ta);
} catch (err) {
showToast(t("promptTranslateError", err.message), "error");
} finally {
btn.disabled = false;
btn.textContent = label;
}
});
});
root.querySelectorAll(".wfm-pt-restore").forEach((btn) => {
btn.addEventListener("click", () => restoreTextarea(document.getElementById(btn.dataset.target)));
});
root.querySelectorAll(".wfm-pt-restore").forEach((btn) => {
const ta = document.getElementById(btn.dataset.target);
ta?.addEventListener("input", () => _updateRestoreButton(ta));
});
loadTranslateConfig().then(() => {
updateTranslateButtons();
_warmupOnce();
});
}
// CPU-resident mode: load the model in the background once per page so the first 🌐
// does not pay Ollama's ~4 s startup. The server ignores this unless CPU mode is on.
let _warmedUp = false;
function _warmupOnce() {
if (_warmedUp || !isTranslateEnabled() || _config.prompt_translate_device !== "cpu") return;
_warmedUp = true;
fetch("/api/wfm/translate/warmup", { method: "POST" }).catch(() => {});
}
/** HTML for the two buttons placed next to a prompt textarea's Apply button. */
export function translateButtonsHtml(textareaId) {
const on = isTranslateEnabled();
return `<button class="wfm-btn wfm-btn-sm wfm-pt-translate" data-target="${textareaId}" title="${t("promptTranslateBtnTitle")}" style="${on ? "" : "display:none;"}">🌐</button>`
+ `<button class="wfm-btn wfm-btn-sm wfm-pt-restore" data-target="${textareaId}" title="${t("promptTranslateRestoreTitle")}" style="display:none;">${t("promptTranslateRestore")}</button>`;
}
+7
View File
@@ -9,6 +9,7 @@ import { t, getLang, getSummaryLang, setLang, setSummaryLang, getLanguageOptions
import { getSettings, readJsonStorage, escapeHtml } from "./util.js";
import { DECISION_BACKENDS, getDecisionSettings, saveDecisionSettings, testDecisionConnection, listDecisionModels } from "./decision-client.js";
import { initVramSettings } from "./vram-prepare.js";
import { initPromptTranslateSettings, promptTranslateSettingsHtml } from "./prompt-translate-settings.js";
const SETTINGS_KEY = "wfm_settings";
@@ -772,6 +773,9 @@ export async function initSettingsTab() {
<button class="wfm-btn wfm-btn-primary wfm-btn-sm" id="wfm-settings-default-ckpt-save">${t("save")}</button>
</details>
<!-- Prompt Translation (native language -> English, PLaMo 3 Translate via Ollama) -->
${promptTranslateSettingsHtml(serverSettings)}
<!-- CivitAI Settings -->
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("civitaiApiKeySetting")}</summary>
@@ -1228,6 +1232,9 @@ export async function initSettingsTab() {
}
});
// --- Prompt translation ---
initPromptTranslateSettings(serverSettings, saveServerSettings);
// --- Windows selector event loop (needs ComfyUI restart) ---
document.getElementById("wfm-settings-selector-loop")?.addEventListener("change", async (e) => {
const enabled = !!e.target.checked;
+28 -2
View File
@@ -25,6 +25,7 @@ import { isVideoFilename, extractLastFrameBlob } from "./video-utils.js";
import { locateVideoModelNodes, readTemplateDefaults, applyBlockToWorkflow } from "./video-workflow.js";
import { VTEMP_GROUP } from "./gallery-tab.js";
import { setResultPreview } from "./video-preview.js";
import { translateButtonsHtml, attachTranslateButtons, isAutoTranslateOn, translatePrompt, needsTranslation } from "./prompt-translate.js";
const VIDEO_BLOCK_COLORS = ["#4caf7d", "#3b4b8a", "#8e5cd9", "#e0c341", "#4ac0d9", "#9ad14b", "#e08a3c", "#d9527a"];
const VIDEO_PLAN_PREFIX = "ws_videoplan_";
@@ -277,7 +278,10 @@ function _blockEditorHtml(block, isFirstBlock) {
<span class="wfm-video-color-swatch" style="background:${escapeHtml(block.color)}"></span>
<span>color</span>
</div>
<label id="wfm-video-block-prompt-label">Prompt</label>
<div style="display:flex;align-items:center;gap:6px;">
<label id="wfm-video-block-prompt-label" style="flex:1;">Prompt</label>
${translateButtonsHtml("wfm-video-block-prompt")}
</div>
<textarea id="wfm-video-block-prompt" class="wfm-textarea" rows="8">${escapeHtml(block.prompt || "")}</textarea>
</div>
`;
@@ -385,6 +389,9 @@ function _renderBlockEditor() {
document.getElementById("wfm-video-block-last-clear")?.addEventListener("click", () => _clearBlockImage("last"));
document.getElementById("wfm-video-block-prompt")?.addEventListener("input", (e) => { block.prompt = e.target.value; });
// 🌐 native-language translation (Settings → Prompt Translation); translating fires
// "input", so block.prompt follows the translated text.
attachTranslateButtons(document.getElementById("wfm-video-block-prompt")?.closest(".wfm-video-block-prompt-col") || document);
// Durationの入力欄はtopbarのbasesettings行に固定配置されている(ブロックエディタの
// innerHTMLと違って再生成されない)ため、ここでは現在選択中ブロックの値を反映するだけ。
@@ -707,6 +714,10 @@ async function _runVideoPlanBatch() {
const progressBar = document.getElementById("wfm-video-plan-progress-bar");
const progressText = document.getElementById("wfm-video-plan-progress-text");
// Settings -> Prompt Translation -> "Video Plan Run": translate each block's prompt
// when sending it (the plan itself keeps the original text).
const autoTranslate = await isAutoTranslateOn("video");
let completed = 0, failed = 0;
let prevGeneratedLastFrameRef = null;
@@ -727,9 +738,24 @@ async function _runVideoPlanBatch() {
break;
}
let prompt = block.prompt;
if (autoTranslate && needsTranslation(prompt)) {
try {
if (progressText) progressText.textContent = `[${i + 1}/${total}] ${t("promptTranslateRunning")}`;
prompt = await translatePrompt(prompt);
} catch (err) {
// The end-of-batch "stopped" toast replaces this one, so also keep the
// reason in the progress line.
showToast(t("promptTranslateError", err.message), "error");
if (progressText) progressText.textContent = t("promptTranslateError", err.message);
_aborted = true;
break;
}
}
const firstImageFilename = await _resolveFirstImage(block, i, prevGeneratedLastFrameRef);
applyBlockToWorkflow(clone, nodes, {
prompt: block.prompt,
prompt,
duration: block.duration,
aspectRatio: _plan.base_settings.aspect_ratio,
megapixels: _plan.base_settings.megapixels,
+23
View File
@@ -397,4 +397,27 @@ export function applyBlockToWorkflow(clone, nodes, params) {
} else if (nodes.disconnectUnsetFrames && lastFrameSlot !== -1) {
_disconnectInput(clone, instanceNode, lastFrameSlot);
}
// Workflows saved by newer frontends also carry a name-keyed copy of a subgraph
// instance's widget values (widgets_values_named), and convertUiToApi() prefers that
// copy. Without mirroring the writes above into it, the template's own prompt /
// duration / turbo values were sent instead of the Plan's (e.g. minimax_turbo.json).
new Set([instanceNode, promptNode, durationNode]).forEach(_syncNamedWidgetValues);
}
function _syncNamedWidgetValues(node) {
const named = node?.widgets_values_named;
const values = node?.widgets_values;
if (!named || typeof named !== "object" || !Array.isArray(values)) return;
const keys = Object.keys(named);
if (keys.length === values.length) {
// The frontend writes both from the same widget list, so the orders line up.
keys.forEach((k, i) => { named[k] = values[i]; });
return;
}
// Lengths differ: fall back to the outer widget inputs' order, the same mapping
// convertUiToApi() uses when there is no named copy.
(node.inputs || []).filter((inp) => inp.widget).forEach((inp, i) => {
if (i < values.length && Object.prototype.hasOwnProperty.call(named, inp.name)) named[inp.name] = values[i];
});
}
+3 -1
View File
@@ -1435,11 +1435,12 @@
<li id="wfm-help-gen-4">Input tab: <b>Prompt</b> (left) and <b>Image</b> (right, about 3:2) are shown side by side on one screen (drag-and-drop upload); when the Prompt/Image area gets narrow (about 720px or less) Image wraps under Prompt, and the Raw JSON column can be collapsed to a slim strip at the right edge with its <b>»</b> button (Input tab only; remembered); the Prompt area has Positive Prompt, Negative Prompt, and an <b>Embeddings</b> selector at the bottom — select a model, set Weight (default 1.0), then click <b>Paste</b> to insert <code>(embedding:Name:weight)</code> at the cursor position of the last focused textarea (defaults to Positive if neither is focused); a <b>↺</b> button next to each Apply button reverts the textarea to the node's currently applied value; select text (or just place the cursor on a word/parenthesis block) and press <b>Ctrl+↑/↓</b> to adjust its emphasis weight in <code>(text:weight)</code> syntax by ±0.05 — the parentheses are removed automatically when the weight returns to 1.0</li>
<li id="wfm-help-gen-5">Model tab: Checkpoint, VAE, Diffusion Model (UNETLoader / UnetLoaderGGUF / LoaderGGUF), Text Encoder (CLIPLoader / DualCLIPLoader / ClipLoaderGGUF / DualClipLoaderGGUF — type and device selectors; DualCLIP shows two clip selectors) are always shown; ControlNet and Hypernetwork (with Strength field) are collapsed by default since they're used less often — click their header to expand; LoRA (Single/Stack tabs) has its own column to the right; all selectors have a filter; LoRA Stack — Apply writes all active LoRAs to the target node and simultaneously inserts LoRA syntax and CivitAI trigger words into the Positive prompt; re-applying updates the prompt by removing inactive LoRAs and re-adding active ones</li>
<li id="wfm-help-gen-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-6">Settings tab: KSampler, Latent Image and Switches side by side. Switches lists every true/false value in the workflow (e.g. the prompt enhancer on/off switch, TextGenerate thinking) and every two-choice enable/disable, on/off or yes/no option (e.g. KSamplerAdvanced add_noise, DWPreprocessor detect_hand) with its node name; clicking one toggles it immediately (no Apply needed) and the change shows up in Raw JSON</li>
<li id="wfm-help-gen-31">LTX-2.5 and Wan2.2 video workflows also work when loaded as regular workflows in this tab (not just the dedicated Video tab): Positive/Negative Prompt detection follows LTX-2.5's LTXVDualCFGGuider / LTXVConditioning nodes to find the actual prompt text, and the Settings tab's Latent Image section recognizes Wan2.2's EmptyHunyuanLatentVideo and LTX-2.5's EmptyLTXVLatentVideo nodes (with an added Length (frames) field) instead of showing "No EmptyLatentImage node found"; generating from these workflows also gets the same extended timeout as MiniMax H3 video generations</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); the search bar highlights matches and scrolls to them without moving the cursor into the editor, so typing right after a search can't accidentally overwrite the JSON — click into the editor first to actually edit</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>
<li id="wfm-help-gen-32"><b>Partner API nodes</b> (Nano Banana / Gemini image nodes, etc.): GenerateUI can run workflows that contain ComfyUI's partner API nodes. These nodes need Comfy credentials in the /prompt request — ComfyUI's own screen adds them automatically, but a separate page cannot — so add <code>COMFY_API_KEY=...</code> (a Comfy API key issued at platform.comfy.org) to the plugin's .env file and restart ComfyUI. With the key set, GenerateUI sends the request through the plugin server, which adds the key only when the workflow contains a partner API node (the key never reaches the browser); usage is billed as Comfy credits. A Gemini image node's prompt appears in the Prompt tab, and its reference images go through LoadImage nodes in the Image tab. Sample: ws_nano_banana_2_1_t2i</li>
<li id="wfm-help-gen-33">🌐 prompt translation: when Settings → Prompt Translation is on, a 🌐 button sits next to the Positive/Negative prompts (Input tab), the Positive/Negative fields of the Lab prompt modal, and the Video Plan block prompt. It translates only the parts written in your own language into English and leaves prompt syntax as it is — commas, (word:1.2), [ ], {a|b}, &lt;lora:...&gt;, __wildcards__, embedding:name and BREAK; &quot;Original&quot; puts back the text from before translation (also Ctrl+Z) while that field stays open. Settings can also translate automatically when Generate, Lab Run or Video Plan Run is pressed</li>
</ul>
</div>
<div class="wfm-help-card">
@@ -1694,6 +1695,7 @@
<li id="wfm-help-settings-22">G'MIC-Qt Integration: path to the G'MIC-Qt executable (gmic_qt.exe) used by the Image Edit tab's G'MIC filter (Edit with G'MIC); Save stores it — see the Image Edit help for installing G'MIC-Qt</li>
<li id="wfm-help-settings-17">Decision Model: its own section, independent of the AI TOOL / Tagger LLM/VLM backends so it can be used alongside them — backend, URL, model and a default probability threshold (answers below it are treated as suggestions to confirm). Unsloth: Laya models (laya / laya-multilingual / laya-english / laya-typed-decisions); requires Settings → API → Decision API → Serve requests in Unsloth Desktop (otherwise HTTP 404 "The Decision API is off") and either UNSLOTH_API_KEY in .env or Keyless API access → "Chat and inference". Ollama (0.35+): lists installed models whose capabilities include "decision" (tev1, tev1:0.8b, nimble — ollama pull first; ↻ refreshes the list); no key needed. Test sends one yes/no question with the unsaved form values; the first request loads the model.</li>
<li id="wfm-help-settings-25">VRAM Management (Ollama): right before every image generation from Workflow Studio, unload the models Ollama has loaded to free VRAM (off / only as much as needed to reach a target free VRAM / everything every time); free VRAM is measured with nvidia-smi; &quot;Unload all now&quot; unloads them immediately; the same "Unload all now" is also in the sidepanel A tab → Settings → VRAM (Ollama). Running a workflow from the ComfyUI canvas itself is not covered</li>
<li id="wfm-help-settings-26">Prompt Translation (native language → English): off by default. Translates prompts with a local Ollama model — PLaMo 3 Translate (0.8B / 2B / 8B / 31B; models not yet in Ollama can be fetched with the Download button) or any other Ollama model through a translation instruction. Options: source language (auto-detect or one of the 51 languages PLaMo supports), Ollama URL, where to run the model — GPU (about 0.1 s per phrase; with &quot;Unload after translating&quot; on, each translation first waits about 4 s for the model to reload) or CPU kept loaded (about 0.3 s per phrase, no VRAM, loaded in the background when GenerateUI opens) — and &quot;Translate automatically when running&quot; for GenerateUI Generate (replaces the prompt text with the translation), Lab Run and Video Plan Run (translate only what is sent; the plan keeps your text). If translation fails, nothing is generated. Test translates a sample prompt. 2B translates sentences more accurately than 0.8B. PLaMo 3 Translate is free for personal and non-commercial use (PLaMo Community License). PLaMo models also work for the AI TOOL / Prompt tab AI Assistant / sidepanel A tab translation buttons (the languages are passed in the format PLaMo expects)</li>
</ul>
</div>
<div class="wfm-help-card">
+42 -4
View File
@@ -3603,7 +3603,8 @@ function _extractPromptsAPI(wf) {
const srcNode = wf[String(val[0])];
// TextEncodeFibo2(Fibo 2.0)も同形。promptはPreviewAny→プロンプト強化スイッチ経由でリンクされる。
if (srcNode?.class_type === "TextEncodeMageFlowEdit" || srcNode?.class_type === "TextEncodeBooguEdit"
|| srcNode?.class_type === "TextEncodeFibo2" || srcNode?.class_type === "PH3ImageConditioning") {
|| srcNode?.class_type === "TextEncodeFibo2" || srcNode?.class_type === "PH3ImageConditioning"
|| srcNode?.class_type === "MiniMaxH3ImageConditioning") {
const raw = isPos ? srcNode.inputs?.prompt : srcNode.inputs?.negative_prompt;
const txt = Array.isArray(raw) ? _resolveLinkedText(wf, raw[0], raw[1] ?? 0) : raw;
if (txt && typeof txt === "string") { if (isPos) pos.add(txt); else neg.add(txt); }
@@ -4034,6 +4035,39 @@ const renderInfoTab = (container) => {
const AI_SETTINGS_KEY = "wfm_ai_settings";
const AI_LANG_NAMES = { ja: "Japanese", en: "English", zh: "Chinese" };
// PLaMo Translate over chat — inline copy of static/js/plamo-chat.js (this file cannot import
// from static/). PLaMo treats an instruction system prompt as context and the user text as the
// source, so the languages go in a leading "[plamo-translate]" system message instead.
const AI_PLAMO_LANGS = ["ar","az","be","bg","bn","ca","ceb","de","en","es","et","eu","fa","fr","gu","he","hi","hr","hu","hy","id","it","ja","jv","ka","kk","km","ko","lt","lv","ml","mr","ms","my","ne","nl","pa","pl","ru","si","sk","sl","sr","sw","ta","te","th","tl","ur","uz","vi","zh"];
let _aiPlamoNameMap = null;
function aiIsPlamoModel(model) { return /plamo/i.test(model || ""); }
function aiPlamoLangCode(nameOrCode) {
const key = (nameOrCode || "").trim().toLowerCase();
if (!key) return "";
if (!_aiPlamoNameMap) {
_aiPlamoNameMap = new Map();
for (const code of AI_PLAMO_LANGS) {
_aiPlamoNameMap.set(code, code);
for (const loc of ["en", "ja", "zh", navigator.language, code]) { // `code` adds the endonym
try {
const name = new Intl.DisplayNames([loc], { type: "language" }).of(code);
if (name) _aiPlamoNameMap.set(name.toLowerCase(), code);
} catch { /* Intl.DisplayNames unavailable */ }
}
}
[["chinese", "zh"], ["中文", "zh"], ["中国語", "zh"], ["filipino", "tl"], ["tagalog", "tl"], ["persian", "fa"], ["farsi", "fa"]]
.forEach(([k, v]) => _aiPlamoNameMap.set(k, v));
}
return _aiPlamoNameMap.get(key) || _aiPlamoNameMap.get(key.split(/[-_]/)[0]) || "";
}
function aiBuildPlamoMessages(text, src, dst) {
const dstCode = aiPlamoLangCode(dst);
if (!dstCode) throw new Error(`PLaMo Translate does not support the target language "${dst}"`);
const srcCode = aiPlamoLangCode(src);
const header = `[plamo-translate]\ntarget_lang=${dstCode}` + (srcCode && srcCode !== dstCode ? `\nsource_lang=${srcCode}` : "");
return [{ role: "system", content: header }, { role: "user", content: text }];
}
const AI_BACKEND_DEFAULT_URLS = {
ollama: "http://localhost:11434",
lmstudio: "http://localhost:1234",
@@ -4564,7 +4598,7 @@ const setupAiHandlers = (container) => {
const systemPrompt = `You are a professional translator. Translate the text the user sends${fromPart} into ${dstName}. `
+ `Reply with ONLY the translated text and nothing else: no explanations, no notes, no preamble like "Here is the translation:", `
+ `no surrounding quotes, and do not repeat or echo the original text.`;
const messages = [{ role: "system", content: systemPrompt }, { role: "user", content: text }];
const plamo = aiIsPlamoModel(model);
const transBtn = container.querySelector("#wfm-nlp-ai-trans-btn");
const statusEl = container.querySelector("#wfm-nlp-ai-status");
@@ -4575,8 +4609,12 @@ const setupAiHandlers = (container) => {
outputEl.value = "";
try {
const result = await aiCallChat(url, backend, model, messages, c);
const cleaned = cleanAiTranslationOutput(result);
const messages = plamo
? aiBuildPlamoMessages(text, srcName, dstName)
: [{ role: "system", content: systemPrompt }, { role: "user", content: text }];
const callCfg = plamo ? { ...c, thinkingMode: false } : c; // PLaMo has no thinking mode
const result = await aiCallChat(url, backend, model, messages, callCfg);
const cleaned = plamo ? result.replace(/<\|plamo:[a-z_]+\|>/g, "").trim() : cleanAiTranslationOutput(result);
outputEl.value = cleaned;
statusEl.textContent = "完了";
statusEl.className = "wfm-nlp-ai-status wfm-nlp-ai-ok";