Compare commits

...
33 Commits
Author SHA1 Message Date
ketle-manandClaude Sonnet 4.6 b20f46dc93 feat: show CivitAI image as preview fallback when no local file exists
When the backend preview download fails (server-side network restriction etc.),
loadPreviewImage() now falls back to civitaiCache[sha256].images[0] so the
card and side panel always show the CivitAI image after fetching.
fetchCivitaiForModel() also refreshes the grid and side panel regardless of
preview_saved, using the CivitAI URL directly when no local file was saved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 18:32:20 +09:00
ketle-manandClaude Sonnet 4.6 14c538eb55 docs: v0.3.15 README and DEVLOG update
- README: version badge 0.3.14 → 0.3.15, Sample Workflows section,
  CivitAI auto-preview note, Create Tags in TOOLS pane, v0.3.15 changelog
- DEVLOG: add 2026-05-20 v0.3.15 entry covering all three changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 17:35:51 +09:00
ketle-manandClaude Sonnet 4.6 ef0017a0fc docs: update help text for CivitAI auto-preview and Create Tags
- helpModels5 (EN/JA/ZH): note that preview is auto-downloaded when missing
- helpAi3 (EN/JA/ZH): add Create Tags / タグ作成 / 标签生成 to TOOLS task list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 17:33:37 +09:00
ketle-manandClaude Sonnet 4.6 19e77a00db feat: add Create tags option to TOOLS pane VLM task selector
Add "Create tags" task to the VLM dropdown in both the SPA AI TOOL tab
and the Library panel A tab. Sends a prompt that asks the VLM to output
comma-separated English tags describing the image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 17:31:38 +09:00
ketle-manandClaude Sonnet 4.6 826988cc0b feat: auto-save CivitAI preview image for models without preview
When fetching CivitAI info (individual or batch), if the model has no
preview image, automatically download the first CivitAI image and save
it as {model_stem}.preview.png next to the model file.

- civitai_service.py: add download_image() static method
- models_routes.py: auto-download preview in handle_civitai_fetch and
  handle_civitai_batch; batch summary includes preview_saved count
- models-tab.js: refresh side panel and grid on preview_saved=true;
  batch done toast shows "+N preview" when previews were saved

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 17:16:16 +09:00
ketle-manandClaude Sonnet 4.6 28bfbcda6b docs: add Sample Workflows section to README
Explains that workflows/ contains sample workflows and that some may
require additional custom nodes installable via ComfyUI Manager or git clone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:54:18 +09:00
ketle-manandClaude Sonnet 4.6 6eef19e656 feat: add sample workflow files for distribution
Add workflows/ directory with 13 sample workflows (JSON + PNG thumbnails)
covering SD1.5, SDXL, DWPose, face detailer, and image editing presets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:46:51 +09:00
ketle-manandClaude Sonnet 4.6 646721a5f4 docs: update DEVLOG for AI TOOL tab English label fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:54:47 +09:00
ketle-manandClaude Sonnet 4.6 4c44215f70 fix: English-only static labels for AI TOOL tab (SPA + Library panel)
- Replace all Japanese-hardcoded labels in templates/index.html AI TOOL section with English
- Replace all Japanese-hardcoded labels in node_sets_menu.js AI tab (HTML + JS toast/status strings)
- Rename VLM subtab to TOOLS in Library panel subtab list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:52:20 +09:00
ketle-manandClaude Sonnet 4.6 66f46af8b6 v0.3.14: AI TOOL tab 3-pane layout + RAW JSON color customization
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:34:17 +09:00
ketle-manandClaude Sonnet 4.6 95266a20b8 v0.3.13: AI tab (Translation, VLM, Settings) for SPA and Library panel
- Add A tab to SPA (templates/index.html) with Translation, VLM, Settings sub-tabs
- Add new static/js/ai-tab.js module (Ollama / LM Studio backends, full i18n via t())
- Add A tab to Workflow Studio Library side panel (node_sets_menu.js)
- Settings shared between SPA and Library panel via localStorage (wfm_ai_settings)
- URL security: validate backend URL with new URL() before any network call
- Rename ollamaSettings to 'Ollama Settings (Prompt Tab)' in EN/JA/ZH to distinguish from AI tab
- Add AI tab help card and helpSidepanel16 to Help tab (index.html + i18n.js + app.js)
- Update docs/6_ws_library.png, README.md (v0.3.13), DEVLOG.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:42:40 +09:00
ketle-manandClaude Sonnet 4.6 d54234b18d v0.3.12: Raw JSON search, workflow analyzer extended model detection, README screenshots refresh
- GenerateUI: always-visible search bar in Raw JSON panel (match count, ↑/↓ nav, Escape/✕ clear, orange/yellow highlights, scroll sync)
- workflow_analyzer: CLIPLoader type-field detection (_CLIP_TYPE_TO_MODEL), subgraph node scanning, UnetLoaderGGUF support, NewBie/Ovis/HiDream/Wan/Cosmos/Lumina added
- README: screenshots replaced with 8 feature-focused images; v0.3.12 changelog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 13:46:59 +09:00
ketle-manandClaude Sonnet 4.6 4298fd9ea2 v0.3.11: Metadata tab extended loader support + Settings text size slider
- metadata-tab.js: add UnetLoaderGGUF/UNETLoaderGGUF as Diffusion Model
  and QuadrupleCLIPLoader as Text Encoder (was already in node_sets_menu.js
  for the I tab; now consistent across both Metadata tab and side panel)
- settings-tab.js: add applyTextareaFontSize() + Text Size section (10-28px
  slider that applies to all 8 prompt/chat textareas at once)
- app.js: apply saved textareaFontSize on page load alongside theme
- templates/index.html: update help text for Metadata and Settings tabs
- README/DEVLOG: v0.3.11 documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 16:03:10 +09:00
ketle-manandClaude Sonnet 4.6 b8b4a65881 v0.3.10: Information tab drag-to-canvas, extended node type support, bug fixes
- Add drag & drop from I tab (model/lora/Prompts) to ComfyUI canvas
  - model sub-tab: places CheckpointLoaderSimple / VAELoader / UNETLoader / CLIPLoader
  - lora sub-tab: drag individual LoRA → LoraLoader; Multiple LORA section → Lora Loader (LoraManager)
  - Prompts sub-tab: drag prompt → CLIPTextEncode with text pre-filled
  - Double-click also places nodes at canvas center
- Add UnetLoaderGGUF / UNETLoaderGGUF detection as Diffusion Model
- Add QuadrupleCLIPLoader (HiDream, 4-CLIP workflows) detection as Text Encoder
- Fix CLIPLoader widget name in MODEL_NODE_MAP (clip_name1 → clip_name)
- Fix Information tab preview area height (fixed 110px, prevents list compression)
- Update help (index.html, app.js, i18n.js EN/JA/ZH) with new drag features
- Update README and DEVLOG for v0.3.10

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:25:37 +09:00
ketle-manandClaude Sonnet 4.6 0015638874 v0.3.9: side panel I tab + top bar icon fix
- Add Information (I) tab to ComfyUI side panel: drop PNG/WebP/JSON to
  inspect model, LoRA, and prompt metadata without leaving the canvas;
  sub-tabs: model / lora / Prompts with full-text preview and Copy button
- Shorten side panel tab labels to W / N / P / M / I with full name on hover
- Fix top bar icons invisible in newer ComfyUI: remove early-return guard
  that skipped SVG injection when Iconify had already converted the MDI
  class to an inline SVG; replace MutationObserver with simple rAF retry
- Update README, DEVLOG, help text (EN/JA/ZH), and index.html for v0.3.9

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:48:37 +09:00
ketle-manandClaude Sonnet 4.6 a48d9c1437 v0.3.8: Metadata tab - data9 workflow support and unclassified prompt text type
- Extended extractPromptsFromNodeSet to resolve link sources from any STRING-output
  node (ComfySwitchNode, PrimitiveStringMultiline, etc.) without type restriction
- Added fallback for SamplerCustomAdvanced (no positive/negative inputs): return
  textMap contents as texts[] instead of null
- Extended step4 PrimitiveStringMultiline and step7 final fallback to search
  subgraph nodes via collectAllNodes
- Added "text" prompt type (no POS/NEG badge) for cases where polarity cannot be
  determined; buildPromptItem and handleFile updated accordingly
- Added metaPromptText i18n key (EN/JA/ZH)
- Updated README (v0.3.8, feature description) and DEVLOG

Supported in data9: flux2_fp8, flux2_klein_4b_distilled, ernie_image,
qwen_image_edit_2511, wan2_2_14B_animate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:32:49 +09:00
ketle-manandClaude Sonnet 4.6 48303e1452 docs: update README and DEVLOG for v0.3.7
README:
- Bump version badge to 0.3.7
- Update Metadata Tab feature section: subgraph support, expanded
  format list, updated prompt extraction description
- Add v0.3.7 changelog entry

DEVLOG:
- Append i18n/HTML text update notes to v0.3.7 entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:05:56 +09:00
ketle-manandClaude Sonnet 4.6 b0171add24 docs: update Metadata tab format note and help text (EN/JA/ZH)
- metaFormatTodo: replace "more formats planned" with note that
  Flux.2 / Qwen-Image / Z-Image subgraph workflows are now supported
- helpMetadata5: list Flux.2 / Qwen-Image / Z-Image explicitly
- Update static fallback text in index.html accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:03:17 +09:00
ketle-manandClaude Sonnet 4.6 7958ad1276 v0.3.7: Metadata tab support for Flux2/Qwen/Z-Image subgraph workflows
- Add extractPromptsFromNodeSet() to handle CLIPTextEncode+KSampler in
  subgraphs (definitions.subgraphs) used by official ComfyUI templates
- Add extractMarkdownNoteModels() to parse **section** → - [model](url)
  from MarkdownNote nodes as fallback model extraction
- Add PrimitiveStringMultiline prompt support (flux2-klein workflows)
- Fix extractCheckpoints/extractVAEs to use collectAllNodes() so
  subgraph loaders are included
- Restructure extractPromptsLiteGraph into 7-stage fallback chain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 22:09:06 +09:00
ketle-manandClaude Sonnet 4.6 fa62684b2e v0.3.6: Metadata tab for PNG/WebP/JSON model and prompt extraction
- Add Metadata tab (between Prompt and Gallery) with drag-and-drop support
- Extract checkpoint, VAE, diffusion model, text encoder, LoRA from PNG/WebP/JSON
- Extract positive/negative prompts with SDXL Prompt Styler support
- One-click transfer to GenerateUI and Prompt tab presets
- Support ComfyUI LiteGraph, API format, SD WebUI, Fooocus metadata
- Add i18n (EN/JA/ZH) for all new UI elements and help card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 21:49:38 +09:00
ketle-manandClaude Sonnet 4.6 8fcc4af262 fix: correct PublisherId to statsu for Registry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 09:49:52 +09:00
ketle-manandClaude Sonnet 4.6 33f1d22e30 ci: use official Comfy-Org/publish-node-action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 09:46:57 +09:00
ketle-manandClaude Sonnet 4.6 184da84718 ci: pass registry token via env variable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 09:40:22 +09:00
ketle-manandClaude Sonnet 4.6 e3aeec8022 ci: pipe N to skip tracking prompt in publish workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 09:39:04 +09:00
ketle-manandClaude Sonnet 4.6 c444db45e6 ci: add GitHub Actions workflow for Registry publish
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 09:37:33 +09:00
ketle-manandClaude Sonnet 4.6 a1613bd1e8 chore: bump version to 0.3.5 for Registry publish
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 09:28:05 +09:00
ketle-manandClaude Sonnet 4.6 30162789b8 docs: add thumbnail and update Registry icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 09:21:44 +09:00
ketle-manandClaude Sonnet 4.6 1cd3fb3fff v0.3.5: Feeder subtab for comfyui-image-feeder + workflow analysis improvements
- Add Feeder subtab to GenerateUI: ImageFeeder node selector, all node parameters,
  folder tree, image grid with checkbox selection, preview panel, presets,
  Run/Stop loop with After gen modes (Loop / Increment / Fixed)
- Run loop captures image_loop_node_sync WebSocket messages to auto-update Index;
  uses right-pane seed settings for KSampler (fixes same-image-repeated bug)
- Improve analyzeWorkflow: BFS CONDITIONING chain expansion (5 iterations),
  CLIPTextEncodeSDXL/Refiner, SDXLPromptStyler/Advanced, KSamplerAdvanced noise_seed,
  TextEncodeQwenImageEditPlus, PrimitiveStringMultiline, Power Lora Loader (rgthree),
  Checkpoint Loader (WAS) with space in class name
- Fix KSamplerAdvanced seed key via data-seed-key attribute in editor
- Add Help card for Feeder subtab with EN/JA/ZH i18n
- Update README to v0.3.5, add Feeder subtab section and changelog entry
- Update DEVLOG with full implementation notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:11:25 +09:00
ketle-manandClaude Sonnet 4.6 5b087713b9 v0.3.4: Checkpoint Batch folder-tree dropdown, Pause/Resume, Settings side-by-side layout
- Checkpoint Batch: replace Include/Exclude text inputs with folder-tree dropdown
  - Folder-level checkboxes with indeterminate state for partial selection
  - Expand/collapse folders with ▶; supports any subfolder depth
  - Filter search, All / None quick-select buttons
- Checkpoint Batch: add Pause / Resume button (suspends between models)
- Checkpoint Batch: Stop while paused now immediately exits the loop
- GenerateUI Settings tab: KSampler and Latent Image side by side (50% each)
- Help tab: update gen-6, add gen-11 (batch), fix gallery-5~11 i18n gap (5-item shift)
- i18n: add helpGen11, helpGallery11; update helpGallery5-10 across EN/JA/ZH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:41:57 +09:00
ketle-manandClaude Sonnet 4.6 6a2da8e4e9 docs: update DEVLOG v0.3.3 with subdirectory support and ImpactWildcardEncode fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:43:52 +09:00
ketle-manandClaude Sonnet 4.6 54fc57344a feat: wildcard subdirectory support
- wildcard_service.py: rglob recursive scan; _safe_path allows
  forward-slash separated relative paths (path traversal still blocked);
  save_file auto-creates parent directories; all responses include
  dir and wc_name fields (e.g. wc_name="color/animals").
- prompt-tab.js: file list and file picker group files by directory with
  a collapsible directory header; insert uses __wc_name__ (e.g.
  __color/animals__); editor name input accepts "folder/name" paths;
  save validation checks each path component individually.
- index.html: update editor name input placeholder to "name or folder/name".
- main.css: add .wfm-wc-dir-header, .wfm-wc-file-item--sub,
  .wfm-wc-picker-dir, .wfm-wc-picker-item--sub; add -webkit-user-select
  for Safari compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:41:46 +09:00
ketle-manandClaude Sonnet 4.6 1cc0e94724 Fix: ImpactWildcardEncode validation error in GenerateUI
- comfyui-workflow.js: validate COMBO widget values against object_info
  choices; fall back to first valid choice when stored value (e.g.
  "Select Wildcard 🟢 Full Cache") is not in the current options list.
  Prevents "Prompt outputs failed validation" with Impact Pack nodes.
- comfyui-workflow.js: recognize ImpactWildcardEncode/ImpactWildcardProcessor
  as prompt nodes in analyzeWorkflow (role detection via sampler refs);
  use wildcard_text as the editable text field (textKey).
- comfyui-editor.js: use node's textKey (defaulting to "text") when
  applying and syncing prompt fields, so ImpactWildcardEncode edits
  update wildcard_text instead of the non-existent text field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:27:53 +09:00
ketle-manandClaude Sonnet 4.6 823f1ebd54 v0.3.3: Wildcard support panel & Impact Pack integration
- Add wildcard input toolbar to Prompt tab (right column) with one-click
  syntax buttons ({|}, {n$|}, __|__, <lora:>, etc.) and selection wrapping
- Add wildcard file manager: create/edit/delete .txt/.yaml files stored in
  user/default/Workflow-Studio/wildcard/
- Tab-ify Prompt tab center pane (Presets / Preset Manager toggle)
- New wildcard CRUD API (GET/POST /api/wfm/wildcards/*)
- New Wildcard Integration section in Settings tab: auto-detects
  ComfyUI-Impact-Pack and creates a directory junction (Windows) or symlink
  (other OS) between WFS wildcard dir and Impact Pack wildcards dir; existing
  files are migrated automatically; link can be removed via UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:02:10 +09:00
71 changed files with 24208 additions and 263 deletions
+22
View File
@@ -0,0 +1,22 @@
name: Publish to Comfy Registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+1046
View File
File diff suppressed because it is too large Load Diff
+162 -29
View File
@@ -5,29 +5,25 @@ A comprehensive workflow management and generation UI plugin for [ComfyUI](https
Browse, organize, and execute workflows directly from a dedicated studio interface — without switching between windows or manually editing JSON.
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.3.2-green)
![Version](https://img.shields.io/badge/version-0.3.16-green)
## Screenshots
| Workflow Tab | GenerateUI Tab |
| Workflow Tab | Models Tab |
|:---:|:---:|
| ![Workflow](docs/screenshot_workflow.png) | ![GenerateUI](docs/screenshot_generate.png) |
| ![Workflow Tab](docs/1_workflowtab.png) | ![Models Tab](docs/2_modelstab.png) |
| Prompt Tab | Settings Tab |
| Prompt Input Assistance | Gen UI Feeder |
|:---:|:---:|
| ![Prompt](docs/screenshot_prompt.png) | ![Settings](docs/screenshot_settings.png) |
| ![Prompt Input Assistance](docs/3_PromptInputAssistance.png) | ![Gen UI Feeder](docs/4_GenUI_feeder.png) |
| Nodes Tab | Models Tab |
| Top Bar | WS Library |
|:---:|:---:|
| ![Nodes](docs/screenshot_nodes.png) | ![Models](docs/screenshot_models.png) |
| ![Top Bar](docs/5_topbar.png) | ![WS Library](docs/6_ws_library.png) |
| Gallery Tab | Help & Support Tab |
| Library Information | Customize |
|:---:|:---:|
| ![Gallery](docs/screenshot_gallery.png) | ![Help](docs/screenshot_help.png) |
| Workflow Studio Library (ComfyUI) | ComfyUI Integration |
|:---:|:---:|
| ![Workflow Studio Library](docs/screenshot_wf_node_library.png) | ![ComfyUI Top Bar](docs/screenshot_comfyui_topbar.png) |
| ![Library Information](docs/7_library_Infomation.png) | ![Customize](docs/8_Customize.png) |
---
@@ -50,20 +46,33 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Embedded workflow metadata** — workflow JSON is embedded in the PNG (tEXt chunk), compatible with ComfyUI's drag-and-drop import
- **Auto-import** — the captured workflow is automatically imported and appears in the Workflow tab
### GenerateUI Tab (v0.3.2)
- **3-tab layout** — Input / Model / Settings tabs, each with a Raw JSON column on the right for instant preview and direct editing
### GenerateUI Tab (v0.3.5)
- **4-tab layout** — Input / Model / Settings / Feeder tabs; Input, Model, and Settings each include a Raw JSON column on the right for instant preview and direct editing
- **Input tab** — Prompt (top) and Image drag-and-drop (bottom) in the left column; Raw JSON (540px) in the right column
- **Model tab** — Checkpoint, VAE, LoRA, ControlNet, UNET, TextEncoder selectors with filter; Raw JSON on the right
- **Settings tab** — KSampler (top) and LatentImage (bottom) in the left column; Raw JSON on the right
- **Always-visible Raw JSON** — edit the API-format JSON directly from any tab with syntax highlighting; Apply button reloads the workflow
- **Settings tab** — KSampler and Latent Image side by side at 50% width each; Raw JSON on the right
- **Always-visible Raw JSON** — edit the API-format JSON directly from any tab with syntax highlighting; Apply button reloads the workflow; built-in **search bar** (always shown) finds all matches as you type with count display (`3/12`); navigate with ↑/↓ buttons or Enter / Shift+Enter; Escape or ✕ clears; current match highlighted in orange, other matches in yellow
- **One-click generation** — queue prompts to ComfyUI without leaving the studio
- **Seed control** — randomize, lock, or manually set seeds; seed input and mode selector stacked vertically for readability
- **Checkpoint Batch** — enable via checkbox in the right panel to sequentially generate with every checkpoint model; filter by include folders (comma-separated subfolder names, empty = all) and/or exclude folders; Stop button aborts after the current generation completes; amber progress bar tracks per-model progress
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion
- **Checkpoint Batch** — enable via checkbox in the right panel to sequentially generate with every checkpoint model; select checkpoints by folder from the dropdown (check a folder to select all its files, expand with ▶ for individual file selection, supports any subfolder depth); Filter input to search; All / None buttons for quick selection; **Pause/Resume** suspends processing between models; Stop aborts after the current generation; amber progress bar tracks per-model progress
- **UI-to-API conversion** — automatic conversion supporting subgraphs (nested workflows), COMBO types, and display-only node exclusion; improved analysis covers SDXL multi-hop CONDITIONING chains, CLIPTextEncodeSDXL, SDXLPromptStyler, KSamplerAdvanced, and more
- **Eagle integration** — auto-save generated images to [Eagle](https://eagle.cool/) with metadata
### Feeder subtab (v0.3.5)
Requires the **[comfyui-image-feeder](https://github.com/ketle-man/comfyui-image-feeder)** custom node.
- **ImageFeeder node control** — select the target node from a dropdown auto-populated from the loaded workflow; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection) and Apply to the workflow
- **Image library** — 3-pane layout: folder tree (left) browsing `user/default/image-loop-data/`, image grid with checkbox selection (center), preview panel with resolution and file size (right)
- **Selection management** — check individual images; All / None buttons for the current folder; selected files are reflected in `selected_files` on Apply
- **Presets** — save the current directory + selection as a named preset; load or delete presets (server-side persistence via `image-feeder-presets.json`)
- **Continuous Run loop** — Run / Stop buttons below the presets; **After gen** mode controls index behavior after each generation:
- **Loop** — advance index and wrap back to 0 when all images are exhausted (runs indefinitely)
- **Increment** — advance index and auto-stop when all images are consumed
- **Fixed** — always use the same index
- **Index sync** — after each generation the node returns `next_index` via WebSocket (`image_loop_node_sync`); the Index field updates automatically
- **Seed** — Run loop uses the right-pane seed setting (Random / Fixed / Increment / Decrement); the node's own Seed field only affects random sort order
### Prompt Tab
- **3-column layout** — AI Assistant, Presets editor, and Preset Manager displayed side-by-side
- **3-column layout** — AI Assistant (left), Presets/Preset Manager tab-panel (center), Wildcard support (right)
- **AI chat assistant** — powered by [Ollama](https://ollama.com/), generate and refine prompts interactively
- **Image attachment** — attach reference images for vision-capable models
- **Translation** — JA/EN/ZH translation buttons for multilingual prompt creation
@@ -72,6 +81,18 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Group management** — create groups, assign/remove presets, delete groups from the Presets panel
- **Clipboard copy** — copy positive/negative prompts individually (PP Copy / NP Copy)
- **GenUI Set** — apply preset prompts directly to the GenerateUI interface
- **Wildcard input toolbar** — one-click buttons to insert `{|}`, `{n$|}`, `__|__`, `<lora::1:LBW=;>` and other wildcard syntax; wraps selected text when applicable
- **Wildcard file manager** — create, view, and edit `.txt` / `.yaml` wildcard files stored in `user/default/Workflow-Studio/wildcard/`; click a filename in the file picker to insert `__filename__` at cursor
### Metadata Tab (v0.3.8)
- **3-column layout** — Drop zone (left) | Model info (center) | LoRA + Prompt (right)
- **File drop** — drop a ComfyUI-generated PNG / WebP or workflow JSON onto the drop zone (or click to open a file picker); PNG/WebP images are shown as a preview
- **Model extraction** — automatically extracts Checkpoint, VAE, Diffusion Model, and Text Encoder names from the workflow; supports both standard and subgraph-based workflows (Flux.2 Dev/Klein, Qwen-Image-Edit/2511/Layered, Z-Image Base/Turbo, Ernie Image, WAN2.2); node types covered: UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, QuadrupleCLIPLoader (e.g. HiDream 4-CLIP)
- **LoRA extraction** — lists all LoRA models with `strength_model / strength_clip` values
- **Prompt extraction** — lists prompts with POS / NEG badges when positive/negative can be determined; when distinction is not possible (e.g. `SamplerCustomAdvanced`, intermediate nodes, cross-level connections), prompts are shown without a badge as plain **Text**; click any entry to view the full text below
- **Prompt actions** — Copy to clipboard, **GenUI:P/N** (set GenerateUI positive/negative prompt), **Prompt:P/N** (set Prompt tab preset positive/negative)
- **Format support** — ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image / Ernie Image / WAN2.2 subgraph workflows), SD WebUI, SD Forge, Fooocus
- **Format note** — supported formats and covered model types are always shown in the left column
### Settings Tab
- **2-column layout** — left column for all settings; right column shows the Theme panel fixed in place (sticky)
@@ -84,6 +105,9 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Ollama connection** — configure Ollama server URL
- **Default workflow** — set a workflow to auto-load on startup
- **Data Management** — export all plugin data (settings, metadata, prompts, etc.) to a single JSON file; import to restore data (useful when migrating or reinstalling)
- **Text Size** — one slider (10–28 px) adjusts font size for all prompt and chat textareas at once: Generate UI positive/negative prompts, AI Assistant chat input, Preset prompts, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings
- **RAW JSON Colors** — customize the 6 syntax highlight colors for the Raw JSON editor in Generate UI: Default Text (base), Name/Scheduler (yellow), Title (pink), Width/Height (green), Prompt/Text (cyan), Image/File (red); changes apply immediately on color pick; Reset Defaults restores the original scheme; saved to `localStorage` under `wfm_settings.jsonColors` and applied on startup
- **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)
- **Language** — English / Japanese / Chinese
### Gallery Tab (v0.3.1)
@@ -115,8 +139,8 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Search & Filter** — full-text search, filter by tags, groups, and favorites
- **User-defined badges** — assign free-label badges to models; badge colors shared with the Workflow tab palette
- **Side panel tabs** — Info (file path display with click-to-copy, tags, memo), Groups management, CivitAI integration
- **CivitAI integration** — fetch model metadata by SHA256 hash, view base model, trained words, tags, and model page link
- **Batch CivitAI fetch** — one-click batch fetch for all models of a type with SSE progress streaming
- **CivitAI integration** — fetch model metadata by SHA256 hash, view base model, trained words, tags, and model page link; preview image is automatically downloaded and saved if none exists
- **Batch CivitAI fetch** — one-click batch fetch for all models of a type with SSE progress streaming; previews are auto-saved for models without one
- **Detail modal** — preview image, CivitAI info, thumbnail change via file upload
- **GenUI Model button** — apply the selected model directly to the corresponding node in GenerateUI's current workflow (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder)
- **Group management** — create, rename, delete groups and assign/remove models; groups are scoped per model type (checkpoint groups only appear in the Checkpoint tab)
@@ -125,13 +149,28 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **Enable / Disable models** — hide models from ComfyUI by renaming the file extension (`.disabled` suffix); toggle per card (⏸ button), per group (Enable All / Disable All), or filter by status (All / Enabled / Disabled)
- **Multi-select & bulk operations** — enter selection mode to check multiple models; bulk action bar supports add/remove from groups and permanent file deletion (model file + preview images + sidecar files such as `.json` / `.info`)
### Workflow Studio Library (ComfyUI Side Panel) (v0.2.5)
- **Workflows tab** — browse favorite workflows (All / ★ Favorites / Groups / By Badge sub-tabs), ★ star shown for favorites in All view
- **Nodes tab** — browse favorite nodes (All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs), ★ star shown for favorites in All view
### AI TOOL Tab (v0.3.14)
- **3-pane layout** — Translation (40%) | TOOLS (40%) | Settings (20%); all panes always visible simultaneously; no sub-tab switching required
- **Translation pane** — translate text between Japanese, English, Chinese, or a custom Free language using Ollama or LM Studio; language selectors with ⇄ swap button (swaps both language selectors and text content); selections saved automatically
- **TOOLS pane (VLM)** — drop an image into the 110px drop zone, select a task (Describe Image / Create Prompt / Create Tags), and click Run to analyze with a vision model; result shown in the output area with a Copy button
- **Settings pane** — choose backend (Ollama / LM Studio), set the API URL, test connection, select a model (with refresh button), and configure Free language names for translation source and destination
- **Settings shared** — settings saved to `localStorage` under `wfm_ai_settings`; shared with the Library panel's AI TOOL tab so configuration is consistent across both interfaces
- **Backend support** — Ollama (`/api/generate` for text, `/api/tags` for model list); LM Studio OpenAI-compatible API (`/v1/chat/completions`, `/v1/models`); VLM images sent as base64 (`images:[]` for Ollama, `image_url` content block for LM Studio)
- **URL security** — backend URL validated via `new URL()` to enforce `http://` or `https://` scheme
### Workflow Studio Library (ComfyUI Side Panel) (v0.3.9)
- **Tab layout (W / N / P / M / I / A)** — compact single-letter tabs with full name shown on hover
- **W — Workflows tab** — browse favorite workflows (All / ★ Favorites / Groups / By Badge sub-tabs), ★ star shown for favorites in All view
- **N — Nodes tab** — browse favorite nodes (All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs), ★ star shown for favorites in All view
- **Category sub-tab** — dropdown to filter nodes by top-level category
- **Package sub-tab** — dropdown to filter nodes by custom node package name
- **Models tab** — browse installed models (All / ★ Favorites / Groups / By Type sub-tabs)
- **Prompts tab** — browse prompt presets with All / Favorites / Categories sub-tabs
- **M — Models tab** — browse installed models (All / ★ Favorites / Groups / By Type sub-tabs)
- **P — Prompts tab** — browse prompt presets with All / Favorites / Categories sub-tabs
- **I — Information tab** — drop a ComfyUI-generated PNG/WebP or workflow JSON in the side panel to view its metadata; supports `UnetLoaderGGUF` and `QuadrupleCLIPLoader` node types; preview area fixed at 110px
- **A — AI TOOL tab** — 3-pane layout (Translation | TOOLS | Settings) always visible; Translation and TOOLS (VLM) powered by Ollama or LM Studio; settings (backend, URL, model) shared with the SPA AI TOOL tab via `localStorage`
- **model sub-tab** — Checkpoint, VAE, Diffusion Model, and Text Encoder; drag items to canvas to place the corresponding loader node (Checkpoint → `CheckpointLoaderSimple`, VAE → `VAELoader`, Diffusion Model → `UNETLoader`, Text Encoder → `CLIPLoader`); double-click also places at canvas center
- **lora sub-tab** — LoRA names with `strength_model / strength_clip` values; drag individual LoRA to place `LoraLoader`; **Multiple LORA** section (appears for 1+ LoRAs) drags all LoRAs into a single `Lora Loader (LoraManager)` node
- **Prompts sub-tab** — POS / NEG badge list; drag a prompt to place `CLIPTextEncode` with text pre-filled; click any entry to view full text + Copy button
- **Drag & drop workflows** — drag a workflow onto the canvas to load it
- **Drag & drop nodes** — drag nodes/node sets onto the canvas to place them
- **Drag & drop prompts** — drag a preset onto the canvas to create a WFS_PromptText node with positive/negative prompts
@@ -164,6 +203,22 @@ Restart ComfyUI after installation.
---
## Sample Workflows
Sample workflows are included in the `workflows/` folder. You can open them directly in ComfyUI via drag & drop, or load them from the Workflow tab.
> **Note:** Some sample workflows require additional custom nodes.
> If a node is shown as missing (red/unknown) after loading, install the required custom nodes via **ComfyUI Manager** or by cloning the repository from GitHub into your `ComfyUI/custom_nodes/` directory.
>
> ```bash
> cd ComfyUI/custom_nodes
> git clone <repository-url>
> ```
>
> After installation, restart ComfyUI to activate the new nodes.
---
## Usage
### Launch
@@ -198,7 +253,8 @@ 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 and translation features
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features
- **[LM Studio](https://lmstudio.ai/)** — alternative backend for translation and VLM (OpenAI-compatible API)
- **[Eagle](https://eagle.cool/)** — for auto-saving generated images with metadata
---
@@ -215,6 +271,80 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
## Changelog
### v0.3.16
- **CivitAI preview fallback** — model cards and the detail panel's Info tab now display the first CivitAI image directly when no local preview file exists; if the backend download fails (e.g. network restriction server-side), the browser fetches the image URL from the CivitAI cache as a fallback so the preview is always shown after fetching
### v0.3.15
- **Sample workflows** — added `workflows/` directory containing 13 sample workflows (SD1.5 / SDXL / DWPose / Face Detailer / Image Editing) with PNG thumbnails, bundled in the package
- **CivitAI auto-preview** — when fetching CivitAI info (individual or batch), the first CivitAI image is automatically downloaded and saved as `{model_stem}.preview.png` if no preview exists; batch summary reports the count of auto-saved previews
- **TOOLS pane — Create Tags** — new task option in the VLM dropdown (both SPA AI TOOL tab and Library A tab): generates a comma-separated list of descriptive English tags from the image
### v0.3.14
- **AI TOOL tab redesign** — tab renamed from "A" to "AI TOOL"; sub-tab navigation removed in favor of a permanent 3-pane layout (Translation 40% | TOOLS 40% | Settings 20%) — all panes always visible simultaneously; "VLM" pane renamed to "TOOLS"
- **Settings — RAW JSON Colors** — new accordion section with 6 color pickers to customize the syntax highlight colors for the Raw JSON editor in Generate UI: Default Text, Name/Scheduler, Title, Width/Height, Prompt/Text, Image/File; changes apply immediately on color pick; Reset Defaults restores the original scheme; saved to `localStorage` under `wfm_settings.jsonColors` and applied on startup
- **Help tab updated** — AI TOOL tab card rewritten; Settings card updated with RAW JSON Colors entry; Library panel sidepanel-16 entry updated
### v0.3.13
- **AI Tab (A)** — new tab added to both the SPA and the Workflow Studio Library side panel (rightmost tab)
- **Translation sub-tab** — translate text via Ollama or LM Studio between Japanese, English, Chinese, or a custom Free language; ⇄ swap button exchanges both language selectors and text content simultaneously; language selections persisted automatically
- **VLM sub-tab** — drop zone (110px, matching the I tab) accepts images via drag & drop or click; task selector (Describe Image / Create Prompt); Run button streams result into output area with Copy button; Ollama uses `images:[base64]` payload, LM Studio uses `image_url` content block
- **Settings sub-tab** — backend radio (Ollama / LM Studio), API URL input, connection test with live status, model selector with refresh, Free language name fields (source and destination); saved to `localStorage` key `wfm_ai_settings` and shared between SPA and Library panel
- **URL security** — backend URL validated via `new URL()` to enforce `http://` or `https://` scheme before any network call
- **Settings Tab — Ollama section renamed** — "Ollama 設定" → "Ollama 設定(プロンプトタブ)" to distinguish it from the new AI tab's settings
- **Help Tab updated** — added AI Tab (A) feature card and Library panel A tab entry; i18n support in EN/JA/ZH
### v0.3.12
- **GenerateUI — Raw JSON search** — always-visible search bar above the Raw JSON editor; finds all matches as you type with match count (`3/12`); navigate forward/back with ↑/↓ buttons, Enter / Shift+Enter, or Escape / ✕ to clear; current match highlighted in orange, other matches in yellow; search overlay syncs with editor scroll and stays updated while editing
- **Workflow analyzer — extended model type detection** — added CLIPLoader `type` field mapping to model family (`flux`, `hidream_i`, `wan`, `cosmos`, `lumina`, `ovis`, etc.) via `_CLIP_TYPE_TO_MODEL`; subgraph nodes (`definitions.subgraphs`) now scanned for model detection; `UnetLoaderGGUF` and similar loaders matched via `"UnetLoader" in ntype`; new families added: NewBie, Ovis, HiDream, Wan, Cosmos, Lumina; model-name detection refactored into reusable `_detect_model_type_from_name()`
- **Screenshots refreshed** — replaced tab-by-tab screenshots with 8 feature-focused images
### v0.3.11
- **Metadata Tab — extended loader support** — `metadata-tab.js` now recognizes `UnetLoaderGGUF` and `UNETLoaderGGUF` as Diffusion Model (e.g. HiDream GGUF workflows), and `QuadrupleCLIPLoader` as Text Encoder (e.g. HiDream 4-CLIP); previously these were detected only in the side panel I tab, not in the Metadata tab itself
- **Settings Tab — Text Size** — new slider (10–28 px) in the Settings tab adjusts font size for all prompt and chat text areas at once: Generate UI positive/negative prompts, AI Assistant chat input, Preset prompts, Wildcard text area and file editor, and Metadata prompt full preview; applied immediately on drag and persisted with Save Settings
### v0.3.10
- **Information tab — drag to canvas** — model, lora, and Prompts sub-tabs in the side panel I tab now support drag & drop onto the ComfyUI canvas; double-click also places nodes at canvas center
- model sub-tab: each item places the corresponding loader node (Checkpoint → `CheckpointLoaderSimple`, VAE → `VAELoader`, Diffusion Model → `UNETLoader`, Text Encoder → `CLIPLoader`)
- lora sub-tab: drag individual LoRA to place `LoraLoader`; **Multiple LORA** section (shown for 1+ LoRAs) places all LoRAs in a single `Lora Loader (LoraManager)` node
- Prompts sub-tab: drag a prompt to place `CLIPTextEncode` with the full text pre-filled
- **Extended node type support** — `UnetLoaderGGUF` / `UNETLoaderGGUF` now detected as Diffusion Model; `QuadrupleCLIPLoader` (HiDream and similar 4-CLIP workflows) now detected as Text Encoder
- **Bug fix: CLIPLoader widget name** — `textencoder` type in `MODEL_NODE_MAP` was incorrectly using `clip_name1` (DualCLIPLoader's widget) instead of `clip_name`; Text Encoder models now drop with the correct filename
- **Preview area fixed height** — Information tab drop zone fixed at 110px so model/prompt lists are no longer compressed when a large image is loaded
### v0.3.9
- **Side panel I tab** — new Information tab in the Workflow Studio Library side panel; drop a ComfyUI PNG/WebP/JSON directly in the panel to inspect models, LoRAs, and prompts without opening the Metadata tab; sub-tabs: model / lora / Prompts with full-text preview and Copy button
- **Side panel tab redesign** — tabs shortened to single letters (W / N / P / M / I) with full name shown on hover
- **Bug fix: top bar icons** — icons (Workflow Studio / camera / Node Sets) were invisible in newer ComfyUI versions because Iconify converted the MDI CSS class to an inline SVG before the custom SVG could be injected; fixed by removing the early-return guard and adopting the same simple `requestAnimationFrame` retry pattern used by lora-manager
### v0.3.8
- **Metadata Tab: extended subgraph support** — added extraction for Flux.2 Dev fp8 (`SamplerCustomAdvanced`), Flux.2 Klein 4B Distilled (image edit), Ernie Image, Qwen-Image-Edit 2511 (`TextEncodeQwenImageEditPlus`), and WAN2.2 14B Animate (top-level CLIPTextEncode + subgraph KSampler)
- **Prompt type: Text (no badge)** — when positive/negative polarity cannot be determined (indirect sampler connections, cross-level links, `PrimitiveStringMultiline` etc.), prompts are listed without a POS/NEG badge; action buttons still work normally
- **Prompt extraction improvements** — `extractPromptsFromNodeSet` now resolves link sources from any STRING-output node (e.g. `ComfySwitchNode`, `PrimitiveStringMultiline`) without type restriction; `PrimitiveStringMultiline` search and final text-encoder fallback both extended to include subgraph nodes via `collectAllNodes`
- **i18n** — added `metaPromptText` key (EN: "Text" / JA: "テキスト" / ZH: "文本")
### v0.3.7
- **Metadata Tab: subgraph workflow support** — Flux.2 (Dev fp8 / Klein), Qwen-Image-Edit (/ 2511 / Layered), and Z-Image (Base / Turbo) official templates now correctly extract models and prompts; these use ComfyUI's `definitions.subgraphs` format with `UNETLoader`, `CLIPLoader`, and `VAELoader` inside the subgraph
- **Prompt extraction improvements** — 7-stage fallback chain: ImageMetadataPromptLoader → WFS_PromptText → top-level CLIPTextEncode+KSampler → `PrimitiveStringMultiline` (flux2-klein) → subgraph CLIPTextEncode+KSampler → PromptStyler → all CLIPTextEncode text
- **MarkdownNote model fallback** — `extractMarkdownNoteModels()` parses `**section** → - [model](url)` patterns from MarkdownNote nodes as a supplemental model source
- **Format note updated** — "Flux.2 / Qwen-Image / Z-Image subgraph workflows supported" (EN/JA/ZH)
### v0.3.6
- **Metadata Tab** — new tab (between Prompt and Gallery) that extracts model and prompt information from ComfyUI-generated PNG/WebP images and workflow JSON files; 3-column layout: drop zone with image preview (left), Checkpoint/VAE/Diffusion Model/Text Encoder lists (center), LoRA list with strength values + Prompt list with full-text viewer (right)
- **Prompt action buttons** — Copy, GenUI:P/N (set GenerateUI prompts), Prompt:P/N (set Prompt tab preset prompts) directly from the extracted prompt text
- **SDXL Prompt Styler support** — improved prompt extraction resolves `PromptStyler → CLIPTextEncode` link chains in both LiteGraph and API workflow formats
- **Format support** — ComfyUI (PNG/WebP/JSON), SD WebUI, SD Forge, Fooocus
### v0.3.5
- **Feeder subtab** — new subtab in GenerateUI for controlling [comfyui-image-feeder](https://github.com/ketle-man/comfyui-image-feeder) nodes; left pane: node selector + all ImageFeeder parameters + presets + Run/Stop controls; center pane: folder tree, image grid with checkbox selection, and preview panel
- **Run loop with After gen modes** — Loop (wrap and continue), Increment (advance and auto-stop), Fixed (constant index); index updates automatically via WebSocket `image_loop_node_sync` after each generation; seed comes from the right-pane seed setting
- **Workflow analysis improvements** — `analyzeWorkflow` now handles multi-hop CONDITIONING chains via BFS (up to 5 iterations), CLIPTextEncodeSDXL / CLIPTextEncodeSDXLRefiner, SDXLPromptStyler / SDXLPromptStylerAdvanced, KSamplerAdvanced (`noise_seed`), TextEncodeQwenImageEditPlus, PrimitiveStringMultiline, Power Lora Loader (rgthree), and `Checkpoint Loader` (WAS) with space in the class name
### v0.3.4
- **Wildcard support panel** — new right column in the Prompt tab with a one-click toolbar for inserting wildcard syntax (`{|}`, `{n$|}`, `__|__`, `:`, `;`, `$`, `<lora::1:LBW=;>`, `[]`, single/multi pick) and wrapping selected text; dedicated prompt textarea and wildcard file picker (click to insert `__filename__`)
- **Wildcard file manager** — create, view, edit, and delete `.txt` / `.yaml` wildcard files stored in `user/default/Workflow-Studio/wildcard/`; editor opens inline with filename input and save/cancel controls
- **Preset/Preset Manager tab switch** — the center pane of the Prompt tab is now a two-tab panel (Presets / Preset Manager) to reclaim horizontal space for the new wildcard panel
- **Wildcard Integration setting** — new accordion section in Settings tab; auto-detects ComfyUI-Impact-Pack; when installed, provides a button to create a directory junction (Windows) or symlink (other OS) from the WFS wildcard directory to Impact Pack's `wildcards/` directory; existing WFS files are migrated automatically; a Remove Link button reverses the operation
### v0.3.2
- **Checkpoint Batch** — new panel in the GenerateUI right column (below Seed, above results); enable via checkbox to auto-generate once per checkpoint model; filter included/excluded subfolders by comma-separated names (empty = all checkpoints); real-time preview of matched model count; amber progress bar shows current model name and index; Stop button aborts the loop after the current generation completes; failed models are counted and reported in the summary toast
- **Seed layout fix** — seed value input and mode selector (Random / Fixed / Increment / Decrement) now stacked vertically so neither is clipped in narrow panels
@@ -366,7 +496,7 @@ ComfyUI-Workflow-Studio/
│ ├── settings_service.py # Settings persistence
│ └── png_extractor.py # PNG metadata extraction
├── templates/
│ └── index.html # SPA template (Workflow/GenerateUI/Prompt/Settings/Nodes/Models/Help)
│ └── index.html # SPA template (Workflow/GenerateUI/Prompt/Metadata/Gallery/Nodes/Models/Settings/Help/AI)
├── static/
│ ├── favicon.svg # Browser tab icon (W+S Wave)
│ ├── css/main.css # Styles
@@ -374,11 +504,14 @@ ComfyUI-Workflow-Studio/
│ ├── app.js # App initialization & routing
│ ├── workflow-tab.js # Workflow browser
│ ├── generate-tab.js # Generation UI
│ ├── feeder-tab.js # Feeder subtab (ImageFeeder node control + image library)
│ ├── prompt-tab.js # AI assistant & presets
│ ├── metadata-tab.js # Metadata extraction & display (PNG/WebP/JSON)
│ ├── settings-tab.js # Settings panel
│ ├── comfyui-client.js # ComfyUI WebSocket/API client
│ ├── nodes-tab.js # Node browser & node sets
│ ├── models-tab.js # Model browser & CivitAI integration
│ ├── ai-tab.js # AI TOOL tab (Translation | TOOLS/VLM | Settings — Ollama / LM Studio)
│ ├── comfyui-workflow.js # UI-to-API format conversion
│ ├── comfyui-editor.js # Dynamic parameter editor
│ ├── json-highlight.js # JSON syntax highlighting
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+2
View File
@@ -13,6 +13,7 @@ STATIC_DIR = PLUGIN_DIR / "static"
# ComfyUI root is custom_nodes/../../ = two levels up
_COMFYUI_ROOT = PLUGIN_DIR.parent.parent
COMFYUI_ROOT = _COMFYUI_ROOT # exposed for external use
# Data directory: prefer ComfyUI's user/default/Workflow-Studio/, fallback to plugin data/
_COMFYUI_USER_DEFAULT = _COMFYUI_ROOT / "user" / "default"
@@ -67,3 +68,4 @@ NODE_METADATA_FILE = DATA_DIR / "node_metadata.json"
NODE_SETS_FILE = DATA_DIR / "node_sets.json"
PROMPTS_FILE = DATA_DIR / "prompts.json"
MODEL_METADATA_FILE = DATA_DIR / "model_metadata.json"
WILDCARD_DIR = DATA_DIR / "wildcard"
+37
View File
@@ -150,10 +150,24 @@ async def handle_civitai_fetch(request: web.Request) -> web.Response:
"message": "Model not found on CivitAI"
})
# Auto-set preview if none exists and CivitAI has images
preview_saved = False
if info.get("images"):
has_preview = await asyncio.to_thread(_service.find_preview_image, model_type, model_name)
if not has_preview:
image_url = info["images"][0]
preview_path = file_path.parent / (file_path.stem + ".preview.png")
preview_saved = await asyncio.to_thread(
CivitaiService.download_image, image_url, preview_path
)
if preview_saved:
logger.info("Auto-saved preview for %s from CivitAI", model_name)
return web.json_response({
"status": "ok",
"sha256": sha256,
"civitai": info,
"preview_saved": preview_saved,
})
except Exception as e:
logger.error("CivitAI fetch error: %s", e)
@@ -269,6 +283,28 @@ async def handle_civitai_batch(request: web.Request) -> web.Response:
all_meta[mname]["sha256"] = sha
_service._save_metadata(all_meta)
# Auto-set previews for models without preview image
dirs = _get_model_dirs(model_type)
preview_saved_count = 0
for model_name, result in results.items():
civitai = result.get("civitai")
if not civitai or not civitai.get("images"):
continue
if _service.find_preview_image(model_type, model_name):
continue
model_file = None
for d in dirs:
p = d / model_name
if p.is_file():
model_file = p
break
if not model_file:
continue
preview_path = model_file.parent / (model_file.stem + ".preview.png")
if CivitaiService.download_image(civitai["images"][0], preview_path):
preview_saved_count += 1
logger.info("Auto-saved preview for %s from CivitAI", model_name)
# Send final result
summary = {
"total": len(model_files),
@@ -276,6 +312,7 @@ async def handle_civitai_batch(request: web.Request) -> web.Response:
"not_found": sum(1 for r in results.values() if r.get("sha256") and not r.get("civitai")),
"errors": sum(1 for r in results.values() if r.get("error")),
"hashes": {name: r.get("sha256") for name, r in results.items() if r.get("sha256")},
"preview_saved": preview_saved_count,
}
await response.write(send_sse("done", summary))
await response.write_eof()
+98
View File
@@ -0,0 +1,98 @@
"""Wildcard file management API routes."""
import asyncio
import logging
from aiohttp import web
from ..config import DATA_DIR, COMFYUI_ROOT
from ..services.wildcard_service import WildcardService
logger = logging.getLogger(__name__)
_service = WildcardService(DATA_DIR / "wildcard")
def setup_routes(app: web.Application):
app.router.add_get("/api/wfm/wildcards", handle_list)
app.router.add_get("/api/wfm/wildcards/content", handle_get_content)
app.router.add_post("/api/wfm/wildcards/save", handle_save)
app.router.add_post("/api/wfm/wildcards/delete", handle_delete)
app.router.add_get("/api/wfm/wildcards/link-status", handle_link_status)
app.router.add_post("/api/wfm/wildcards/create-link", handle_create_link)
app.router.add_post("/api/wfm/wildcards/remove-link", handle_remove_link)
async def handle_list(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_service.list_wildcards)
return web.json_response(result)
except Exception as e:
logger.error("Error listing wildcards: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_get_content(request: web.Request) -> web.Response:
filename = request.rel_url.query.get("filename", "")
try:
content = await asyncio.to_thread(_service.get_content, filename)
return web.json_response({"content": content})
except FileNotFoundError:
return web.json_response({"error": "not found"}, status=404)
except Exception as e:
logger.error("Error getting wildcard content: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_save(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
content = body.get("content", "")
result = await asyncio.to_thread(_service.save_file, filename, content)
return web.json_response({"status": "ok", "file": result})
except Exception as e:
logger.error("Error saving wildcard: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_delete(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
await asyncio.to_thread(_service.delete_file, filename)
return web.json_response({"status": "ok"})
except Exception as e:
logger.error("Error deleting wildcard: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_link_status(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_service.get_link_status, COMFYUI_ROOT)
return web.json_response(result)
except Exception as e:
logger.error("Error getting wildcard link status: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_create_link(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_service.create_link, COMFYUI_ROOT)
return web.json_response({"status": "ok", **result})
except RuntimeError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error creating wildcard link: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_remove_link(request: web.Request) -> web.Response:
try:
await asyncio.to_thread(_service.remove_link)
return web.json_response({"status": "ok"})
except RuntimeError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error removing wildcard link: %s", e)
return web.json_response({"error": str(e)}, status=500)
+14
View File
@@ -196,6 +196,20 @@ class CivitaiService:
return results
@staticmethod
def download_image(url, save_path, timeout=15):
"""Download an image from URL and save to save_path. Returns True on success."""
try:
req = Request(url, headers={"User-Agent": "ComfyUI-Workflow-Studio/1.0"})
with urlopen(req, timeout=timeout) as resp:
data = resp.read()
with open(save_path, "wb") as f:
f.write(data)
return True
except Exception as e:
logger.warning("Failed to download preview from %s: %s", url, e)
return False
def clear_cache(self, sha256_hash=None):
"""Clear cache for a specific hash or all."""
cache = self._load_cache()
+205
View File
@@ -0,0 +1,205 @@
"""Wildcard file management service."""
import logging
import os
import platform
import shutil
import subprocess
from pathlib import Path
logger = logging.getLogger(__name__)
ALLOWED_EXTS = {"txt", "yaml", "yml"}
class WildcardService:
def __init__(self, wildcard_dir: Path):
self.wildcard_dir = wildcard_dir
# Only mkdir if it is a real directory (not a junction/symlink)
if not self.wildcard_dir.exists() and not self._is_junction(self.wildcard_dir):
self.wildcard_dir.mkdir(parents=True, exist_ok=True)
# ------------------------------------------------------------------
# File CRUD
# ------------------------------------------------------------------
def list_wildcards(self) -> list[dict]:
"""Return sorted list of wildcard files recursively (follows junction/symlink)."""
if not self.wildcard_dir.exists():
return []
files = []
for path in sorted(self.wildcard_dir.rglob("*")):
if path.is_file() and path.suffix.lstrip(".").lower() in ALLOWED_EXTS:
rel = path.relative_to(self.wildcard_dir)
dir_posix = rel.parent.as_posix()
if dir_posix == ".":
dir_posix = ""
files.append({
"name": path.stem,
"filename": rel.as_posix(),
"ext": path.suffix.lstrip(".").lower(),
"size": path.stat().st_size,
"dir": dir_posix,
"wc_name": rel.with_suffix("").as_posix(),
})
return files
def get_content(self, filename: str) -> str:
path = self._safe_path(filename)
if path is None:
raise ValueError(f"Invalid filename: {filename}")
if not path.is_file():
raise FileNotFoundError(f"File not found: {filename}")
return path.read_text(encoding="utf-8")
def save_file(self, filename: str, content: str) -> dict:
path = self._safe_path(filename)
if path is None:
raise ValueError(f"Invalid filename: {filename}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
rel = path.relative_to(self.wildcard_dir)
dir_posix = rel.parent.as_posix()
if dir_posix == ".":
dir_posix = ""
return {
"name": path.stem,
"filename": rel.as_posix(),
"ext": path.suffix.lstrip(".").lower(),
"size": path.stat().st_size,
"dir": dir_posix,
"wc_name": rel.with_suffix("").as_posix(),
}
def delete_file(self, filename: str) -> None:
path = self._safe_path(filename)
if path is None:
raise ValueError(f"Invalid filename: {filename}")
if path.is_file():
path.unlink()
def _safe_path(self, filename: str) -> Path | None:
if not filename or not filename.strip():
return None
# Normalize to forward slashes and strip leading/trailing slashes
filename = filename.replace("\\", "/").strip("/")
# Validate each path component — block traversal and empty parts
parts = filename.split("/")
for p in parts:
if not p or p == ".." or p == ".":
return None
ext = Path(parts[-1]).suffix.lstrip(".").lower()
if ext not in ALLOWED_EXTS:
return None
return self.wildcard_dir.joinpath(*parts)
# ------------------------------------------------------------------
# Impact Pack integration
# ------------------------------------------------------------------
@staticmethod
def find_impact_pack_wildcards(comfyui_root: Path) -> Path | None:
"""Return Impact Pack wildcards directory path, or None if not installed."""
custom_nodes = comfyui_root / "custom_nodes"
if not custom_nodes.is_dir():
return None
for d in custom_nodes.iterdir():
if d.is_dir() and "impact-pack" in d.name.lower():
wildcards = d / "wildcards"
if wildcards.is_dir():
return wildcards
return None
def get_link_status(self, comfyui_root: Path) -> dict:
"""Return current Impact Pack integration status."""
impact_dir = self.find_impact_pack_wildcards(comfyui_root)
is_linked = self._is_junction(self.wildcard_dir) or self.wildcard_dir.is_symlink()
resolved_target = None
if is_linked:
try:
resolved_target = str(self.wildcard_dir.resolve())
except Exception:
pass
return {
"impact_pack_installed": impact_dir is not None,
"impact_pack_wildcards_dir": str(impact_dir) if impact_dir else None,
"wfs_wildcard_dir": str(self.wildcard_dir),
"is_linked": is_linked,
"link_target": resolved_target,
}
def create_link(self, comfyui_root: Path) -> dict:
"""Replace WFS wildcard dir with a junction/symlink → Impact Pack wildcards."""
impact_dir = self.find_impact_pack_wildcards(comfyui_root)
if impact_dir is None:
raise RuntimeError("ComfyUI-Impact-Pack is not installed.")
if self._is_junction(self.wildcard_dir) or self.wildcard_dir.is_symlink():
raise RuntimeError("Link already exists.")
# Migrate existing WFS files to Impact Pack dir
migrated: list[str] = []
if self.wildcard_dir.is_dir():
for f in self.wildcard_dir.iterdir():
if f.is_file():
dest = impact_dir / f.name
if not dest.exists():
shutil.copy2(str(f), str(dest))
migrated.append(f.name)
shutil.rmtree(str(self.wildcard_dir))
# Create junction (Windows) or symlink (others)
self._create_junction_or_symlink(self.wildcard_dir, impact_dir)
logger.info("Wildcard link created: %s -> %s", self.wildcard_dir, impact_dir)
return {"migrated_files": migrated}
def remove_link(self) -> None:
"""Remove junction/symlink and restore WFS wildcard dir as a plain directory."""
if not (self._is_junction(self.wildcard_dir) or self.wildcard_dir.is_symlink()):
raise RuntimeError("No link to remove.")
self._remove_junction_or_symlink(self.wildcard_dir)
self.wildcard_dir.mkdir(parents=True, exist_ok=True)
logger.info("Wildcard link removed, restored plain dir: %s", self.wildcard_dir)
# ------------------------------------------------------------------
# Platform helpers
# ------------------------------------------------------------------
@staticmethod
def _is_junction(path: Path) -> bool:
"""Return True if path is a Windows directory junction."""
if platform.system() != "Windows":
return False
try:
import ctypes
FILE_ATTRIBUTE_REPARSE_POINT = 0x0400
attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))
return attrs != 0xFFFFFFFF and bool(attrs & FILE_ATTRIBUTE_REPARSE_POINT)
except Exception:
return False
@staticmethod
def _create_junction_or_symlink(link: Path, target: Path) -> None:
if platform.system() == "Windows":
result = subprocess.run(
["cmd", "/c", "mklink", "/J", str(link), str(target)],
capture_output=True, text=True,
)
if result.returncode != 0:
raise RuntimeError(f"mklink /J failed: {result.stderr.strip()}")
else:
os.symlink(target, link, target_is_directory=True)
@staticmethod
def _remove_junction_or_symlink(path: Path) -> None:
if platform.system() == "Windows":
result = subprocess.run(
["cmd", "/c", "rmdir", str(path)],
capture_output=True, text=True,
)
if result.returncode != 0:
raise RuntimeError(f"rmdir failed: {result.stderr.strip()}")
else:
os.unlink(str(path))
+101 -20
View File
@@ -3,6 +3,23 @@
import os
import re
# CLIPLoader / DualCLIPLoader の type フィールド → モデル種別マッピング
_CLIP_TYPE_TO_MODEL = {
"flux": "Flux",
"flux2": "Flux2",
"newbie": "NewBie",
"ovis": "Ovis",
"qwen_image": "Qwen",
"sd3": "SD3",
"sd3.5": "SD3",
"hidream_i": "HiDream",
"hidream_e": "HiDream",
"hidream": "HiDream",
"wan": "Wan",
"cosmos": "Cosmos",
"lumina": "Lumina",
}
def _model_name_from_ui_node(node):
"""Extract model filename from UI format node (lowercase)."""
@@ -11,16 +28,65 @@ def _model_name_from_ui_node(node):
return val.lower() if isinstance(val, str) else ""
def _clip_type_from_ui_node(node):
"""Extract CLIPLoader type string from widgets_values (lowercase)."""
ntype = node.get("type", "")
vals = node.get("widgets_values", [])
if ntype == "CLIPLoader" and len(vals) > 1:
return str(vals[1]).lower() if isinstance(vals[1], str) else ""
if ntype in ("DualCLIPLoader", "TripleCLIPLoader") and len(vals) > 2:
return str(vals[2]).lower() if isinstance(vals[2], str) else ""
return ""
def _model_name_from_api_node(node):
"""Extract model filename from API format node (lowercase)."""
inputs = node.get("inputs", {})
for key in ("ckpt_name", "unet_name", "model_name"):
for key in ("ckpt_name", "unet_name", "model_name", "clip_name1", "clip_name"):
val = inputs.get(key, "")
if isinstance(val, str) and val:
return val.lower()
return ""
def _collect_all_ui_nodes(workflow_data):
"""Yield all UI-format nodes including those inside definitions.subgraphs."""
for node in workflow_data.get("nodes", []):
yield node
defs = workflow_data.get("definitions", {})
if isinstance(defs, dict):
for sg in defs.get("subgraphs", []):
if isinstance(sg, dict):
for node in sg.get("nodes", []):
yield node
def _detect_model_type_from_name(mn_base, model_types):
"""Detect model type from a model filename (already lowercased basename)."""
if re.search(r"sd1[._-]?5|v1[._-]5", mn_base):
model_types.add("SD1.5")
elif re.search(r"sdxl|[._-]xl[._-]|[._-]xl$|xl[._-]", mn_base):
model_types.add("SDXL")
elif re.search(r"flux[-_.]?2", mn_base):
model_types.add("Flux2")
elif "flux" in mn_base:
model_types.add("Flux")
elif re.search(r"sd[._-]?3", mn_base):
model_types.add("SD3")
elif "qwen" in mn_base:
model_types.add("Qwen")
elif "zimage" in mn_base or "z-image" in mn_base or "zit" in mn_base:
model_types.add("Z-IMAGE")
elif "newbie" in mn_base:
model_types.add("NewBie")
elif "ovis" in mn_base:
model_types.add("Ovis")
elif "hidream" in mn_base:
model_types.add("HiDream")
elif "wan" in mn_base and re.search(r"wan[-_.]?\d|wan[-_]video", mn_base):
model_types.add("Wan")
def analyze_workflow(workflow_data, filename=""):
"""Analyze workflow JSON and return model types, input/output node counts."""
model_types = set()
@@ -36,23 +102,33 @@ def analyze_workflow(workflow_data, filename=""):
model_types.add("Qwen")
if "zimage" in fn or "z-image" in fn or "zit" in fn:
model_types.add("Z-IMAGE")
if "flux2" in fn:
if re.search(r"flux[-_.]?2", fn):
model_types.add("Flux2")
elif "flux" in fn:
model_types.add("Flux")
if "sd3" in fn:
model_types.add("SD3")
if "newbie" in fn:
model_types.add("NewBie")
if "ovis" in fn:
model_types.add("Ovis")
if "hidream" in fn:
model_types.add("HiDream")
if "wan" in fn and re.search(r"wan[-_.]?\d|wan[-_]video", fn):
model_types.add("Wan")
# Support both UI format (nodes array) and API format (node ID object)
raw_nodes = workflow_data.get("nodes", None)
if raw_nodes is not None:
# UI / app format: collect all nodes including subgraph definitions
node_iter = [
(
node.get("type", ""),
node.get("title", "").lower(),
_model_name_from_ui_node(node),
_clip_type_from_ui_node(node),
)
for node in raw_nodes
for node in _collect_all_ui_nodes(workflow_data)
]
else:
node_iter = []
@@ -62,33 +138,32 @@ def analyze_workflow(workflow_data, filename=""):
ntype = node.get("class_type", "")
title = node.get("_meta", {}).get("title", "").lower()
mn = _model_name_from_api_node(node)
node_iter.append((ntype, title, mn))
node_iter.append((ntype, title, mn, ""))
for ntype, title, mn in node_iter:
for ntype, title, mn, clip_type in node_iter:
if (
"CheckpointLoader" in ntype
or "UNETLoader" in ntype
or "UnetLoader" in ntype # UnetLoaderGGUF 等を含む
or "unet_name" in ntype
):
mn_base = os.path.basename(mn.replace("\\", "/"))
if re.search(r"sd1[._-]?5|v1[._-]5", mn_base):
model_types.add("SD1.5")
elif re.search(r"sdxl|[._-]xl[._-]|[._-]xl$|xl[._-]", mn_base):
model_types.add("SDXL")
elif "flux2" in mn_base:
model_types.add("Flux2")
elif "flux" in mn_base:
model_types.add("Flux")
elif re.search(r"sd[._-]?3", mn_base):
model_types.add("SD3")
elif "qwen" in mn_base:
model_types.add("Qwen")
elif "zimage" in mn_base or "z-image" in mn_base or "zit" in mn_base:
model_types.add("Z-IMAGE")
_detect_model_type_from_name(mn_base, model_types)
# CLIPLoader 系: type フィールドまたはファイル名からモデル種別を検出
# QuadrupleCLIPLoader 等、type フィールドを持たない新型ローダーにも対応
if "CLIPLoader" in ntype:
if clip_type:
detected = _CLIP_TYPE_TO_MODEL.get(clip_type)
if detected:
model_types.add(detected)
mn_base = os.path.basename(mn.replace("\\", "/"))
if mn_base:
_detect_model_type_from_name(mn_base, model_types)
if "sdxl" in title:
model_types.add("SDXL")
if "flux2" in title:
if re.search(r"flux[-_.]?2", title):
model_types.add("Flux2")
elif "flux" in title:
model_types.add("Flux")
@@ -98,6 +173,12 @@ def analyze_workflow(workflow_data, filename=""):
model_types.add("Qwen")
if "zimage" in title or "z-image" in title or "zit" in title:
model_types.add("Z-IMAGE")
if "newbie" in title:
model_types.add("NewBie")
if "ovis" in title:
model_types.add("Ovis")
if "hidream" in title:
model_types.add("HiDream")
if ntype == "CLIPTextEncode" or "prompt" in title:
inputs["prompts"] += 1
+2 -1
View File
@@ -61,7 +61,7 @@ class WorkflowStudio:
app.router.add_get("/wfm", serve_index_page)
# API routes
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes
from .routes import workflow_routes, settings_routes, ollama_routes, eagle_routes, nodes_routes, prompts_routes, models_routes, gallery_routes, wildcard_routes
workflow_routes.setup_routes(app)
settings_routes.setup_routes(app)
@@ -71,5 +71,6 @@ class WorkflowStudio:
prompts_routes.setup_routes(app)
models_routes.setup_routes(app)
gallery_routes.setup_routes(app)
wildcard_routes.setup_routes(app)
logger.info("Workflow Studio: Routes registered successfully")
+3 -3
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-workflow-studio"
description = "A comprehensive workflow management and generation UI plugin for ComfyUI. Browse, organize, and execute workflows from a dedicated studio interface."
version = "0.2.7"
version = "0.3.7"
license = "MIT"
requires-python = ">= 3.10"
dependencies = ["jinja2"]
@@ -10,7 +10,7 @@ dependencies = ["jinja2"]
Repository = "https://github.com/ketle-man/ComfyUI-Workflow-Studio"
[tool.comfy]
PublisherId = "ketle-man"
PublisherId = "statsu"
DisplayName = "Workflow Studio"
Icon = "https://raw.githubusercontent.com/ketle-man/ComfyUI-Workflow-Studio/main/assets/icon.svg"
Icon = "https://raw.githubusercontent.com/ketle-man/ComfyUI-Workflow-Studio/main/docs/thumb.png"
Banner = "https://raw.githubusercontent.com/ketle-man/ComfyUI-Workflow-Studio/main/assets/banner.svg"
+1333 -1
View File
File diff suppressed because it is too large Load Diff
+455
View File
@@ -0,0 +1,455 @@
/**
* AI Tab - Translation, VLM, Settings
* Supports Ollama and LM Studio as backends
*/
import { showToast } from "./app.js";
import { t } from "./i18n.js";
const SETTINGS_KEY = "wfm_ai_settings";
const LANG_NAMES = {
ja: "Japanese",
en: "English",
zh: "Chinese",
};
// ============================================
// Settings persistence
// ============================================
function loadAiSettings() {
try {
return JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}");
} catch {
return {};
}
}
function saveAiSettings(patch) {
const data = { ...loadAiSettings(), ...patch };
localStorage.setItem(SETTINGS_KEY, JSON.stringify(data));
return data;
}
const VLM_PROMPTS = {
describe: "Describe this image in detail.",
prompt: "Create a detailed Stable Diffusion image generation prompt based on this image. Output only the prompt text, nothing else.",
tags: "Generate a list of descriptive tags for this image. Output only comma-separated tags in English, nothing else.",
};
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => {
const b64 = e.target.result.split(",")[1];
resolve({ base64: b64, mimeType: file.type });
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
async function callVLM(url, backend, model, prompt, base64Image, mimeType) {
if (backend === "ollama") {
const res = await fetch(`${url}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, prompt, images: [base64Image], stream: false }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).response || "";
} else {
const res = await fetch(`${url}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
messages: [{ role: "user", content: [
{ type: "text", text: prompt },
{ type: "image_url", image_url: { url: `data:${mimeType};base64,${base64Image}` } },
]}],
stream: false,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).choices?.[0]?.message?.content || "";
}
}
// ============================================
// URL validation
// ============================================
function isValidBackendUrl(url) {
try {
const u = new URL(url);
return u.protocol === "http:" || u.protocol === "https:";
} catch {
return false;
}
}
// ============================================
// API helpers
// ============================================
async function fetchModels(url, backend) {
if (backend === "ollama") {
const res = await fetch(`${url}/api/tags`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return (data.models || []).map((m) => m.name);
} else {
// LM Studio (OpenAI-compatible)
const res = await fetch(`${url}/v1/models`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return (data.data || []).map((m) => m.id);
}
}
async function testConnection(url, backend) {
const models = await fetchModels(url, backend);
return models.length;
}
async function callLLM(url, backend, model, prompt) {
if (backend === "ollama") {
const res = await fetch(`${url}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, prompt, stream: false }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data.response || "";
} else {
// LM Studio (OpenAI-compatible)
const res = await fetch(`${url}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
messages: [{ role: "user", content: prompt }],
stream: false,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data.choices?.[0]?.message?.content || "";
}
}
// ============================================
// Translation prompt builder
// ============================================
function buildTranslationPrompt(text, srcLang, dstLang, settings) {
const srcName = srcLang === "free" ? (settings.freeSrcLang || "Auto") : LANG_NAMES[srcLang];
const dstName = dstLang === "free" ? (settings.freeDstLang || "English") : LANG_NAMES[dstLang];
const fromPart = srcLang === "free" ? "" : `from ${srcName} `;
return `Translate the following text ${fromPart}to ${dstName}. Output only the translated text, nothing else.\n\n${text}`;
}
// ============================================
// Translation tab
// ============================================
function initTranslateTab() {
const srcLangSel = document.getElementById("wfm-ai-src-lang");
const dstLangSel = document.getElementById("wfm-ai-dst-lang");
const swapBtn = document.getElementById("wfm-ai-trans-swap-btn");
const inputEl = document.getElementById("wfm-ai-trans-input");
const outputEl = document.getElementById("wfm-ai-trans-output");
const transBtn = document.getElementById("wfm-ai-trans-btn");
const copyBtn = document.getElementById("wfm-ai-trans-copy-btn");
const statusEl = document.getElementById("wfm-ai-trans-status");
if (!transBtn) return;
// Restore saved language selections
const saved = loadAiSettings();
if (saved.srcLang && srcLangSel) srcLangSel.value = saved.srcLang;
if (saved.dstLang && dstLangSel) dstLangSel.value = saved.dstLang;
// Persist language selections
srcLangSel?.addEventListener("change", () => saveAiSettings({ srcLang: srcLangSel.value }));
dstLangSel?.addEventListener("change", () => saveAiSettings({ dstLang: dstLangSel.value }));
// Swap languages
swapBtn?.addEventListener("click", () => {
const tmp = srcLangSel.value;
srcLangSel.value = dstLangSel.value;
dstLangSel.value = tmp;
saveAiSettings({ srcLang: srcLangSel.value, dstLang: dstLangSel.value });
// Swap text content too
const tmpText = inputEl.value;
inputEl.value = outputEl.value;
outputEl.value = tmpText;
});
// Translate
transBtn.addEventListener("click", async () => {
const text = inputEl.value.trim();
if (!text) {
showToast(t("aiToastNoText"), "error");
return;
}
const settings = loadAiSettings();
const { backend = "ollama", backendUrl, model } = settings;
const url = backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
if (!isValidBackendUrl(url)) {
showToast(t("aiToastInvalidUrl"), "error");
return;
}
if (!model) {
showToast(t("aiToastNoModel"), "error");
return;
}
transBtn.disabled = true;
statusEl.textContent = t("aiStatusTranslating");
statusEl.className = "wfm-ai-trans-status wfm-ai-status-working";
outputEl.value = "";
try {
const prompt = buildTranslationPrompt(text, srcLangSel.value, dstLangSel.value, settings);
const result = await callLLM(url, backend, model, prompt);
outputEl.value = result.trim();
statusEl.textContent = t("aiStatusDone");
statusEl.className = "wfm-ai-trans-status wfm-ai-status-ok";
} catch (err) {
statusEl.textContent = `${t("aiStatusConnectFail")}${err.message}`;
statusEl.className = "wfm-ai-trans-status wfm-ai-status-error";
showToast(t("aiToastTransFailed") + err.message, "error");
} finally {
transBtn.disabled = false;
}
});
// Copy translated text
copyBtn?.addEventListener("click", () => {
const text = outputEl.value;
if (!text) {
showToast(t("aiToastNoCopyText"), "error");
return;
}
navigator.clipboard.writeText(text).then(() => {
showToast(t("aiToastCopied"), "success");
});
});
}
// ============================================
// Settings tab
// ============================================
async function refreshModels() {
const settings = loadAiSettings();
const backend = document.querySelector("input[name='wfm-ai-backend']:checked")?.value || "ollama";
const url = document.getElementById("wfm-ai-backend-url")?.value?.trim() || "";
const modelSel = document.getElementById("wfm-ai-model-select");
const refreshBtn = document.getElementById("wfm-ai-model-refresh-btn");
if (!modelSel) return;
if (refreshBtn) refreshBtn.disabled = true;
try {
const models = await fetchModels(url, backend);
modelSel.innerHTML = `<option value="">-- ${t("aiSettingsModelPlaceholder")} --</option>`;
models.forEach((name) => {
const opt = document.createElement("option");
opt.value = name;
opt.textContent = name;
if (name === settings.model) opt.selected = true;
modelSel.appendChild(opt);
});
if (models.length === 0) showToast(t("aiToastNoModels"), "error");
} catch (err) {
showToast(t("aiToastModelsFailed") + err.message, "error");
} finally {
if (refreshBtn) refreshBtn.disabled = false;
}
}
function initSettingsTab() {
const saved = loadAiSettings();
// Restore saved values
const backendRadios = document.querySelectorAll("input[name='wfm-ai-backend']");
backendRadios.forEach((r) => {
if (r.value === (saved.backend || "ollama")) r.checked = true;
});
const urlInput = document.getElementById("wfm-ai-backend-url");
if (urlInput && saved.backendUrl) urlInput.value = saved.backendUrl;
// Update URL placeholder when backend changes
backendRadios.forEach((r) => {
r.addEventListener("change", () => {
if (!urlInput) return;
const defaultUrl = r.value === "ollama" ? "http://localhost:11434" : "http://localhost:1234";
if (!saved.backendUrl) urlInput.value = defaultUrl;
});
});
// Connection test
document.getElementById("wfm-ai-test-btn")?.addEventListener("click", async () => {
const testBtn = document.getElementById("wfm-ai-test-btn");
const resultEl = document.getElementById("wfm-ai-test-result");
const backend = document.querySelector("input[name='wfm-ai-backend']:checked")?.value || "ollama";
const url = urlInput?.value?.trim() || "";
if (!isValidBackendUrl(url)) {
if (resultEl) {
resultEl.textContent = t("aiToastInvalidUrlInput");
resultEl.className = "wfm-ai-test-result wfm-ai-status-error";
}
return;
}
if (testBtn) testBtn.disabled = true;
if (resultEl) {
resultEl.textContent = t("aiStatusConnecting");
resultEl.className = "wfm-ai-test-result wfm-ai-status-working";
}
try {
const count = await testConnection(url, backend);
if (resultEl) {
resultEl.textContent = `${t("aiStatusConnectOk")} (${count} ${t("aiModels")})`;
resultEl.className = "wfm-ai-test-result wfm-ai-status-ok";
}
await refreshModels();
} catch (err) {
if (resultEl) {
resultEl.textContent = `${t("aiStatusConnectFail")}${err.message}`;
resultEl.className = "wfm-ai-test-result wfm-ai-status-error";
}
} finally {
if (testBtn) testBtn.disabled = false;
}
});
// Model refresh
document.getElementById("wfm-ai-model-refresh-btn")?.addEventListener("click", () => refreshModels());
// Free language inputs
const freeSrcInput = document.getElementById("wfm-ai-free-src-lang");
const freeDstInput = document.getElementById("wfm-ai-free-dst-lang");
if (freeSrcInput && saved.freeSrcLang) freeSrcInput.value = saved.freeSrcLang;
if (freeDstInput && saved.freeDstLang) freeDstInput.value = saved.freeDstLang;
// Save settings
document.getElementById("wfm-ai-settings-save-btn")?.addEventListener("click", () => {
const backend = document.querySelector("input[name='wfm-ai-backend']:checked")?.value || "ollama";
const url = urlInput?.value?.trim() || "";
const model = document.getElementById("wfm-ai-model-select")?.value || "";
const freeSrcLang = freeSrcInput?.value?.trim() || "";
const freeDstLang = freeDstInput?.value?.trim() || "";
if (url && !isValidBackendUrl(url)) {
showToast(t("aiToastInvalidUrl"), "error");
return;
}
saveAiSettings({ backend, backendUrl: url, model, freeSrcLang, freeDstLang });
showToast(t("aiToastSettingsSaved"), "success");
});
// Load models on init if settings exist
if (saved.backendUrl && saved.backend) {
refreshModels().catch(() => {});
}
}
// ============================================
// VLM tab
// ============================================
function initVlmTab() {
const dropEl = document.getElementById("wfm-ai-vlm-drop");
const previewEl = document.getElementById("wfm-ai-vlm-preview");
const labelEl = document.getElementById("wfm-ai-vlm-label");
const fileInput = document.getElementById("wfm-ai-vlm-file");
const taskSel = document.getElementById("wfm-ai-vlm-task");
const runBtn = document.getElementById("wfm-ai-vlm-run");
const statusEl = document.getElementById("wfm-ai-vlm-status");
const resultEl = document.getElementById("wfm-ai-vlm-result");
const copyBtn = document.getElementById("wfm-ai-vlm-copy");
if (!dropEl) return;
let vlmImage = null; // { base64, mimeType }
const loadImage = async (file) => {
if (!file || !file.type.startsWith("image/")) return;
vlmImage = await fileToBase64(file);
previewEl.src = `data:${vlmImage.mimeType};base64,${vlmImage.base64}`;
previewEl.style.display = "block";
labelEl.style.display = "none";
};
dropEl.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", e => { if (e.target.files[0]) loadImage(e.target.files[0]); });
dropEl.addEventListener("dragover", e => { e.preventDefault(); dropEl.classList.add("drag-over"); });
dropEl.addEventListener("dragleave", () => dropEl.classList.remove("drag-over"));
dropEl.addEventListener("drop", e => {
e.preventDefault();
dropEl.classList.remove("drag-over");
if (e.dataTransfer.files[0]) loadImage(e.dataTransfer.files[0]);
});
runBtn.addEventListener("click", async () => {
if (!vlmImage) { showToast(t("aiToastNoImage"), "error"); return; }
const settings = loadAiSettings();
const { backend = "ollama", backendUrl, model } = settings;
const url = backendUrl || (backend === "ollama" ? "http://localhost:11434" : "http://localhost:1234");
if (!isValidBackendUrl(url)) {
showToast(t("aiToastInvalidUrl"), "error");
return;
}
if (!model) { showToast(t("aiToastNoModel"), "error"); return; }
const task = taskSel?.value || "describe";
runBtn.disabled = true;
statusEl.textContent = t("aiStatusRunning");
statusEl.className = "wfm-ai-trans-status wfm-ai-status-working";
resultEl.value = "";
try {
const result = await callVLM(url, backend, model, VLM_PROMPTS[task], vlmImage.base64, vlmImage.mimeType);
resultEl.value = result.trim();
statusEl.textContent = t("aiStatusDone");
statusEl.className = "wfm-ai-trans-status wfm-ai-status-ok";
} catch (err) {
statusEl.textContent = `${t("aiStatusConnectFail")}${err.message}`;
statusEl.className = "wfm-ai-trans-status wfm-ai-status-error";
showToast(t("aiToastVlmFailed") + err.message, "error");
} finally {
runBtn.disabled = false;
}
});
copyBtn.addEventListener("click", () => {
const text = resultEl.value;
if (!text) { showToast(t("aiToastNoCopyText"), "error"); return; }
navigator.clipboard.writeText(text).then(() => showToast(t("aiToastCopied"), "success"));
});
}
// ============================================
// Export
// ============================================
export function initAiTab() {
initTranslateTab();
initVlmTab();
initSettingsTab();
}
+97 -4
View File
@@ -90,7 +90,7 @@ function initTabs() {
function applyI18nToHtml() {
// Tab labels
const tabMap = { workflow: "tabWorkflow", nodes: "tabNodes", models: "tabModels", generate: "tabGenerate", prompt: "tabPrompt", gallery: "tabGallery", settings: "tabSettings", help: "tabHelp" };
const tabMap = { workflow: "tabWorkflow", nodes: "tabNodes", models: "tabModels", generate: "tabGenerate", prompt: "tabPrompt", metadata: "tabMetadata", gallery: "tabGallery", settings: "tabSettings", help: "tabHelp", ai: "tabAi" };
document.querySelectorAll(".wfm-tab").forEach((tab) => {
const key = tabMap[tab.dataset.tab];
if (key) tab.textContent = t(key);
@@ -224,6 +224,67 @@ function applyI18nToHtml() {
const presetCopyNegBtn = document.getElementById("wfm-preset-copy-neg-btn");
if (presetCopyNegBtn) presetCopyNegBtn.textContent = t("copyNegativePrompt");
// AI tab
document.querySelectorAll(".wfm-ai-subtab-btn").forEach(btn => {
if (btn.dataset.aiSubtab === "translate") btn.textContent = t("aiSubTabTranslate");
if (btn.dataset.aiSubtab === "vlm") btn.textContent = t("aiSubTabVlm");
if (btn.dataset.aiSubtab === "ai-settings") btn.textContent = t("aiSubTabSettings");
});
const aiLangMap = { ja: "aiLangJa", en: "aiLangEn", zh: "aiLangZh", free: "aiLangFree" };
[document.getElementById("wfm-ai-src-lang"), document.getElementById("wfm-ai-dst-lang")].forEach(sel => {
if (!sel) return;
sel.querySelectorAll("option").forEach(opt => {
const key = aiLangMap[opt.value];
if (key) opt.textContent = t(key);
});
});
const aiTransInput = document.getElementById("wfm-ai-trans-input");
if (aiTransInput) aiTransInput.placeholder = t("aiTransInputPlaceholder");
const aiTransOutput = document.getElementById("wfm-ai-trans-output");
if (aiTransOutput) aiTransOutput.placeholder = t("aiTransOutputPlaceholder");
const aiTransBtn = document.getElementById("wfm-ai-trans-btn");
if (aiTransBtn) aiTransBtn.textContent = t("aiTranslateBtn");
const aiTransCopyBtn = document.getElementById("wfm-ai-trans-copy-btn");
if (aiTransCopyBtn) aiTransCopyBtn.textContent = t("aiCopyBtn");
const aiVlmLabel = document.getElementById("wfm-ai-vlm-label");
if (aiVlmLabel) aiVlmLabel.textContent = t("aiVlmDropLabel");
const aiVlmTask = document.getElementById("wfm-ai-vlm-task");
if (aiVlmTask) {
aiVlmTask.querySelectorAll("option").forEach(opt => {
if (opt.value === "describe") opt.textContent = t("aiVlmDescribe");
if (opt.value === "prompt") opt.textContent = t("aiVlmPromptCreate");
});
}
const aiVlmRunBtn = document.getElementById("wfm-ai-vlm-run");
if (aiVlmRunBtn) aiVlmRunBtn.textContent = t("aiVlmRunBtn");
const aiVlmResult = document.getElementById("wfm-ai-vlm-result");
if (aiVlmResult) aiVlmResult.placeholder = t("aiVlmResultPlaceholder");
const aiVlmCopyBtn = document.getElementById("wfm-ai-vlm-copy");
if (aiVlmCopyBtn) aiVlmCopyBtn.textContent = t("aiCopyBtn");
const aiBackendTitle = document.getElementById("wfm-ai-settings-backend-title");
if (aiBackendTitle) aiBackendTitle.textContent = t("aiSettingsBackend");
const aiConnTitle = document.getElementById("wfm-ai-settings-conn-title");
if (aiConnTitle) aiConnTitle.textContent = t("aiSettingsConnection");
const aiTestBtn = document.getElementById("wfm-ai-test-btn");
if (aiTestBtn) aiTestBtn.textContent = t("aiSettingsTestBtn");
const aiModelTitle = document.getElementById("wfm-ai-settings-model-title");
if (aiModelTitle) aiModelTitle.textContent = t("aiSettingsModelSection");
const aiModelSel = document.getElementById("wfm-ai-model-select");
if (aiModelSel) {
const firstOpt = aiModelSel.querySelector("option[value='']");
if (firstOpt) firstOpt.textContent = t("aiSettingsModelPlaceholder");
}
const aiModelRefreshBtn = document.getElementById("wfm-ai-model-refresh-btn");
if (aiModelRefreshBtn) aiModelRefreshBtn.textContent = t("aiSettingsRefreshBtn");
const aiFreeLangTitle = document.getElementById("wfm-ai-settings-freelang-title");
if (aiFreeLangTitle) aiFreeLangTitle.textContent = t("aiSettingsFreeLang");
const aiFreeSrcLabel = document.getElementById("wfm-ai-free-src-label");
if (aiFreeSrcLabel) aiFreeSrcLabel.textContent = t("aiSettingsInputLang");
const aiFreeDstLabel = document.getElementById("wfm-ai-free-dst-label");
if (aiFreeDstLabel) aiFreeDstLabel.textContent = t("aiSettingsOutputLang");
const aiSaveBtn = document.getElementById("wfm-ai-settings-save-btn");
if (aiSaveBtn) aiSaveBtn.textContent = t("aiSettingsSaveBtn");
// Group panel labels
const propTitle = document.querySelector("#wfm-side-tab-group .wfm-group-section-title");
if (propTitle) propTitle.textContent = t("properties");
@@ -265,15 +326,24 @@ function applyI18nToHtml() {
"wfm-help-gen-3": "helpGen3", "wfm-help-gen-4": "helpGen4",
"wfm-help-gen-5": "helpGen5", "wfm-help-gen-6": "helpGen6", "wfm-help-gen-7": "helpGen7",
"wfm-help-gen-8": "helpGen8", "wfm-help-gen-9": "helpGen9", "wfm-help-gen-10": "helpGen10",
"wfm-help-gen-11": "helpGen11",
"wfm-help-feeder-title": "helpFeederTitle",
"wfm-help-feeder-desc": "helpFeederDesc",
"wfm-help-feeder-1": "helpFeeder1", "wfm-help-feeder-2": "helpFeeder2",
"wfm-help-feeder-3": "helpFeeder3", "wfm-help-feeder-4": "helpFeeder4",
"wfm-help-feeder-5": "helpFeeder5", "wfm-help-feeder-6": "helpFeeder6",
"wfm-help-feeder-7": "helpFeeder7", "wfm-help-feeder-8": "helpFeeder8",
"wfm-help-feeder-9": "helpFeeder9",
"wfm-help-prompt-1": "helpPrompt1", "wfm-help-prompt-2": "helpPrompt2",
"wfm-help-prompt-3": "helpPrompt3", "wfm-help-prompt-4": "helpPrompt4",
"wfm-help-prompt-5": "helpPrompt5", "wfm-help-prompt-6": "helpPrompt6",
"wfm-help-prompt-7": "helpPrompt7",
"wfm-help-prompt-7": "helpPrompt7", "wfm-help-prompt-8": "helpPrompt8",
"wfm-help-prompt-9": "helpPrompt9",
"wfm-help-settings-1": "helpSettings1", "wfm-help-settings-2": "helpSettings2",
"wfm-help-settings-3": "helpSettings3", "wfm-help-settings-4": "helpSettings4",
"wfm-help-settings-5": "helpSettings5", "wfm-help-settings-6": "helpSettings6",
"wfm-help-settings-7": "helpSettings7", "wfm-help-settings-8": "helpSettings8",
"wfm-help-settings-9": "helpSettings9",
"wfm-help-settings-9": "helpSettings9", "wfm-help-settings-10": "helpSettings10",
"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",
@@ -289,6 +359,7 @@ function applyI18nToHtml() {
"wfm-help-gallery-5": "helpGallery5", "wfm-help-gallery-6": "helpGallery6",
"wfm-help-gallery-7": "helpGallery7", "wfm-help-gallery-8": "helpGallery8",
"wfm-help-gallery-9": "helpGallery9", "wfm-help-gallery-10": "helpGallery10",
"wfm-help-gallery-11": "helpGallery11",
"wfm-help-sidepanel-title": "helpSidepanelTitle",
"wfm-help-sidepanel-1": "helpSidepanel1", "wfm-help-sidepanel-2": "helpSidepanel2",
"wfm-help-sidepanel-3": "helpSidepanel3", "wfm-help-sidepanel-4": "helpSidepanel4",
@@ -296,6 +367,12 @@ function applyI18nToHtml() {
"wfm-help-sidepanel-7": "helpSidepanel7", "wfm-help-sidepanel-8": "helpSidepanel8",
"wfm-help-sidepanel-9": "helpSidepanel9", "wfm-help-sidepanel-10": "helpSidepanel10",
"wfm-help-sidepanel-11": "helpSidepanel11", "wfm-help-sidepanel-12": "helpSidepanel12",
"wfm-help-sidepanel-13": "helpSidepanel13",
"wfm-help-sidepanel-14": "helpSidepanel14", "wfm-help-sidepanel-15": "helpSidepanel15",
"wfm-help-sidepanel-16": "helpSidepanel16",
"wfm-help-ai-title": "helpAiTitle",
"wfm-help-ai-1": "helpAi1", "wfm-help-ai-2": "helpAi2", "wfm-help-ai-3": "helpAi3",
"wfm-help-ai-4": "helpAi4", "wfm-help-ai-5": "helpAi5", "wfm-help-ai-6": "helpAi6",
"wfm-help-shortcuts-title": "helpShortcutsTitle",
"wfm-help-shortcuts-1": "helpShortcuts1", "wfm-help-shortcuts-2": "helpShortcuts2",
"wfm-help-shortcuts-3": "helpShortcuts3", "wfm-help-shortcuts-4": "helpShortcuts4",
@@ -349,13 +426,27 @@ import { initWorkflowTab } from "./workflow-tab.js";
import { initNodesTab } from "./nodes-tab.js";
import { initGenerateTab } from "./generate-tab.js";
import { initPromptTab } from "./prompt-tab.js";
import { initSettingsTab, applyTheme, getSavedTheme } from "./settings-tab.js";
import { initSettingsTab, applyTheme, getSavedTheme, applyTextareaFontSize, applyJsonColors } from "./settings-tab.js";
import { initModelsTab } from "./models-tab.js";
import { initGalleryTab } from "./gallery-tab.js";
import { initMetadataTab } from "./metadata-tab.js";
import { initAiTab } from "./ai-tab.js";
// Apply saved theme immediately to prevent flash of default theme
applyTheme(getSavedTheme());
// Apply saved textarea font size
try {
const _s = JSON.parse(localStorage.getItem("wfm_settings") || "{}");
if (_s.textareaFontSize) applyTextareaFontSize(_s.textareaFontSize);
} catch {}
// Apply saved JSON highlight colors
try {
const _s = JSON.parse(localStorage.getItem("wfm_settings") || "{}");
applyJsonColors(_s.jsonColors);
} catch {}
document.addEventListener("DOMContentLoaded", () => {
applyI18nToHtml();
initTabs();
@@ -366,7 +457,9 @@ document.addEventListener("DOMContentLoaded", () => {
initModelsTab();
initGenerateTab();
initPromptTab();
initMetadataTab();
initGalleryTab();
initAiTab();
console.log("Workflow Studio: App initialized");
});
+23 -13
View File
@@ -77,7 +77,7 @@ export const comfyEditor = {
<label>Positive Prompt</label>
<div style="display:flex;gap:8px;margin-bottom:6px;">
<select id="wfm-prompt-pos-target" class="wfm-select" style="width:auto;flex:1">
${positiveNodes.map((n) => `<option value="${n.id}" selected>ID:${n.id} (${n.title})</option>`).join("")}
${positiveNodes.map((n) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" selected>ID:${n.id} (${n.title})</option>`).join("")}
${nodeOpts}
</select>
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-pos-apply">Apply</button>
@@ -88,7 +88,7 @@ export const comfyEditor = {
<label>Negative Prompt</label>
<div style="display:flex;gap:8px;margin-bottom:6px;">
<select id="wfm-prompt-neg-target" class="wfm-select" style="width:auto;flex:1">
${negativeNodes.map((n) => `<option value="${n.id}" selected>ID:${n.id} (${n.title})</option>`).join("")}
${negativeNodes.map((n) => `<option value="${n.id}" data-text-key="${n.textKey || "text"}" selected>ID:${n.id} (${n.title})</option>`).join("")}
${nodeOpts}
</select>
<button class="wfm-btn wfm-btn-sm" id="wfm-prompt-neg-apply">Apply</button>
@@ -101,7 +101,9 @@ export const comfyEditor = {
const nodeId = document.getElementById("wfm-prompt-pos-target")?.value;
const text = document.getElementById("wfm-prompt-pos-text")?.value;
if (nodeId && comfyUI.currentWorkflow?.[nodeId]) {
comfyUI.currentWorkflow[nodeId].inputs.text = text;
const promptNode = (analysis.prompt_nodes || []).find(n => n.id === nodeId);
const textKey = promptNode?.textKey || "text";
comfyUI.currentWorkflow[nodeId].inputs[textKey] = text;
_syncRawJson();
}
});
@@ -110,7 +112,9 @@ export const comfyEditor = {
const nodeId = document.getElementById("wfm-prompt-neg-target")?.value;
const text = document.getElementById("wfm-prompt-neg-text")?.value;
if (nodeId && comfyUI.currentWorkflow?.[nodeId]) {
comfyUI.currentWorkflow[nodeId].inputs.text = text;
const promptNode = (analysis.prompt_nodes || []).find(n => n.id === nodeId);
const textKey = promptNode?.textKey || "text";
comfyUI.currentWorkflow[nodeId].inputs[textKey] = text;
_syncRawJson();
}
});
@@ -209,11 +213,11 @@ export const comfyEditor = {
const latent = analysis.latent_nodes?.[0];
el.innerHTML = `
<div style="display:flex;flex-direction:column;gap:0;">
<div style="padding-bottom:14px;border-bottom:1px solid var(--wfm-border);margin-bottom:14px;">
<div style="display:flex;flex-direction:row;gap:0;align-items:flex-start;">
<div style="flex:1;min-width:0;padding-right:14px;border-right:1px solid var(--wfm-border);">
<h3 style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--wfm-text-secondary);margin:0 0 12px;">KSampler</h3>
${sampler ? `
<input type="hidden" id="wfm-settings-sampler-id" value="${sampler.id}">
<input type="hidden" id="wfm-settings-sampler-id" value="${sampler.id}" data-seed-key="${sampler.seedKey || "seed"}">
<div class="wfm-form-group">
<label>Seed</label>
<input type="number" class="wfm-input" id="wfm-settings-seed" value="${sampler.seed ?? -1}">
@@ -245,7 +249,7 @@ export const comfyEditor = {
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-sampler-apply">Apply</button>
` : "<p class='wfm-placeholder'>No KSampler node found</p>"}
</div>
<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;">Latent Image</h3>
${latent ? `
<input type="hidden" id="wfm-settings-latent-id" value="${latent.id}">
@@ -271,7 +275,9 @@ export const comfyEditor = {
const nodeId = document.getElementById("wfm-settings-sampler-id")?.value;
if (!nodeId || !comfyUI.currentWorkflow?.[nodeId]) return;
const inputs = comfyUI.currentWorkflow[nodeId].inputs;
inputs.seed = parseInt(document.getElementById("wfm-settings-seed")?.value) || -1;
// KSamplerAdvanced uses noise_seed; fall back to seed
const seedKey = document.getElementById("wfm-settings-sampler-id")?.dataset?.seedKey || "seed";
inputs[seedKey] = parseInt(document.getElementById("wfm-settings-seed")?.value) || -1;
inputs.steps = parseInt(document.getElementById("wfm-settings-steps")?.value) || 20;
inputs.cfg = parseFloat(document.getElementById("wfm-settings-cfg")?.value) || 7;
inputs.sampler_name = document.getElementById("wfm-settings-sampler-name")?.value;
@@ -402,16 +408,20 @@ export const comfyEditor = {
syncToWorkflow() {
// Sync prompt texts before generation
const posTarget = document.getElementById("wfm-prompt-pos-target")?.value;
const posSelect = document.getElementById("wfm-prompt-pos-target");
const posTarget = posSelect?.value;
const posText = document.getElementById("wfm-prompt-pos-text")?.value;
if (posTarget && comfyUI.currentWorkflow?.[posTarget]) {
comfyUI.currentWorkflow[posTarget].inputs.text = posText;
const posTextKey = posSelect.selectedOptions[0]?.dataset?.textKey || "text";
comfyUI.currentWorkflow[posTarget].inputs[posTextKey] = posText;
}
const negTarget = document.getElementById("wfm-prompt-neg-target")?.value;
const negSelect = document.getElementById("wfm-prompt-neg-target");
const negTarget = negSelect?.value;
const negText = document.getElementById("wfm-prompt-neg-text")?.value;
if (negTarget && comfyUI.currentWorkflow?.[negTarget]) {
comfyUI.currentWorkflow[negTarget].inputs.text = negText;
const negTextKey = negSelect.selectedOptions[0]?.dataset?.textKey || "text";
comfyUI.currentWorkflow[negTarget].inputs[negTextKey] = negText;
}
},
};
+183 -10
View File
@@ -343,6 +343,24 @@ export const comfyWorkflow = {
wIdx++;
}
}
// For COMBO inputs, validate the value against the available choices.
// Dynamic COMBO options (e.g. Impact Pack's "Select to add Wildcard") may
// differ between the saved workflow and the current ComfyUI instance
// (e.g. "Select Wildcard 🟢 Full Cache" vs "Select Wildcard"), causing
// "Prompt outputs failed validation". Fall back to first choice when mismatch.
if (expectedType === "COMBO") {
const allInputDefs = {
...(objectInfo[node.type]?.input?.required || {}),
...(objectInfo[node.type]?.input?.optional || {}),
};
const spec = allInputDefs[name];
if (spec) {
const choices = Array.isArray(spec[0]) ? spec[0] : null;
if (choices && choices.length > 0 && !choices.includes(val)) {
val = choices[0];
}
}
}
inputs[name] = val;
}
} else {
@@ -446,7 +464,7 @@ export const comfyWorkflow = {
if (!workflow) return result;
// Build sampler connections for prompt classification
// === Pass 1: Collect sampler refs + all_nodes ===
const samplerPositiveRef = {};
const samplerNegativeRef = {};
@@ -462,40 +480,174 @@ export const comfyWorkflow = {
if (Array.isArray(inputs.positive)) samplerPositiveRef[inputs.positive[0]] = true;
if (Array.isArray(inputs.negative)) samplerNegativeRef[inputs.negative[0]] = true;
// KSamplerAdvanced uses noise_seed instead of seed
const seedKey = "seed" in inputs ? "seed" : "noise_seed";
result.sampler_nodes.push({
id, type: ct, title,
seed: inputs.seed, steps: inputs.steps, cfg: inputs.cfg,
seed: inputs.seed ?? inputs.noise_seed,
seedKey,
steps: inputs.steps, cfg: inputs.cfg,
sampler_name: inputs.sampler_name, scheduler: inputs.scheduler,
denoise: inputs.denoise,
});
}
}
// === Pass 1b: Expand refs through CONDITIONING graph (BFS, 5 iterations) ===
// Nodes that pass CONDITIONING through without changing positive/negative role
const COND_PASSTHROUGH = new Set([
"ConditioningCombine", "ConditioningConcat", "ConditioningAverage",
"ConditioningZeroOut", "ConditioningSetTimestepRange",
"ControlNetApply", "ControlNetApplyAdvanced",
"IPAdapterApply", "IPAdapterApplyFaceID",
"StyleModelApply",
]);
for (let iter = 0; iter < 5; iter++) {
for (const [id, node] of Object.entries(workflow)) {
if (!node?.class_type) continue;
const ct = node.class_type;
const inputs = node.inputs || {};
const isPos = !!samplerPositiveRef[id];
const isNeg = !!samplerNegativeRef[id];
if (!isPos && !isNeg) continue;
if (COND_PASSTHROUGH.has(ct)) {
for (const v of Object.values(inputs)) {
if (Array.isArray(v)) {
if (isPos) samplerPositiveRef[v[0]] = true;
if (isNeg) samplerNegativeRef[v[0]] = true;
}
}
}
// Propagate role through text encoder linked inputs (CLIPTextEncode → upstream text source)
if (ct === "CLIPTextEncode") {
const tv = inputs.text;
if (Array.isArray(tv)) {
if (isPos) samplerPositiveRef[tv[0]] = true;
if (isNeg) samplerNegativeRef[tv[0]] = true;
}
}
if (ct === "CLIPTextEncodeSDXL" || ct === "CLIPTextEncodeSDXLRefiner") {
for (const key of ["text_g", "text_l"]) {
const tv = inputs[key];
if (Array.isArray(tv)) {
if (isPos) samplerPositiveRef[tv[0]] = true;
if (isNeg) samplerNegativeRef[tv[0]] = true;
}
}
}
if (ct === "ImpactWildcardEncode" || ct === "ImpactWildcardProcessor") {
const tv = inputs.wildcard_text;
if (Array.isArray(tv)) {
if (isPos) samplerPositiveRef[tv[0]] = true;
if (isNeg) samplerNegativeRef[tv[0]] = true;
}
}
}
}
// === Pass 2: Main analysis ===
for (const [id, node] of Object.entries(workflow)) {
if (!node?.class_type) continue;
const ct = node.class_type;
const inputs = node.inputs || {};
const title = node._meta?.title || ct;
if (ct.includes("CheckpointLoader")) {
const isPos = !!samplerPositiveRef[id];
const isNeg = !!samplerNegativeRef[id];
const getRole = () => {
if (isPos && !isNeg) return "positive";
if (isNeg && !isPos) return "negative";
if (/pos|正/i.test(title)) return "positive";
if (/neg|負/i.test(title)) return "negative";
return "unknown";
};
// --- Checkpoint nodes ---
// Matches: CheckpointLoaderSimple, CheckpointLoader, "Checkpoint Loader" (WAS), etc.
if (ct.includes("CheckpointLoader") || ct === "Checkpoint Loader") {
result.checkpoint_nodes.push({
id, type: ct, title,
ckpt_name: inputs.ckpt_name,
});
}
// --- Prompt nodes ---
if (ct === "CLIPTextEncode") {
let role = "unknown";
if (samplerPositiveRef[id]) role = "positive";
else if (samplerNegativeRef[id]) role = "negative";
else if (/pos|正/i.test(title)) role = "positive";
else if (/neg|負/i.test(title)) role = "negative";
// Only add when text is a direct string (if linked, upstream node will be detected)
const textVal = inputs.text;
if (typeof textVal === "string") {
result.prompt_nodes.push({
id, type: ct, title, role: getRole(),
text: textVal, textKey: "text",
});
}
}
// SDXL dual-text encoder
if (ct === "CLIPTextEncodeSDXL" || ct === "CLIPTextEncodeSDXLRefiner") {
const textG = inputs.text_g;
const textL = inputs.text_l;
// Add only if at least one text field is a direct string
if (typeof textG === "string" || typeof textL === "string") {
result.prompt_nodes.push({
id, type: ct, title, role: getRole(),
text: typeof textG === "string" ? textG : (typeof textL === "string" ? textL : ""),
textKey: typeof textG === "string" ? "text_g" : "text_l",
});
}
}
// Qwen-based image+text encoder
if (ct === "TextEncodeQwenImageEditPlus") {
result.prompt_nodes.push({
id, type: ct, title, role,
text: inputs.text,
id, type: ct, title, role: getRole(),
text: inputs.prompt || "",
textKey: "prompt",
});
}
// SDXLPromptStyler / SDXLPromptStylerAdvanced — contains both pos & neg in one node
if (ct === "SDXLPromptStyler" || ct === "SDXLPromptStylerAdvanced") {
if (inputs.text_positive !== undefined) {
result.prompt_nodes.push({
id, type: ct, title: `${title} [positive]`, role: "positive",
text: inputs.text_positive || "", textKey: "text_positive",
});
}
if (inputs.text_negative !== undefined) {
result.prompt_nodes.push({
id, type: ct, title: `${title} [negative]`, role: "negative",
text: inputs.text_negative || "", textKey: "text_negative",
});
}
}
// ImpactWildcardEncode / ImpactWildcardProcessor — treat wildcard_text as prompt
if (ct === "ImpactWildcardEncode" || ct === "ImpactWildcardProcessor") {
const textVal = inputs.wildcard_text;
if (typeof textVal === "string") {
result.prompt_nodes.push({
id, type: ct, title, role: getRole(),
text: textVal, textKey: "wildcard_text",
});
}
}
// PrimitiveStringMultiline / PrimitiveString — when feeding a prompt node
if ((ct === "PrimitiveStringMultiline" || ct === "PrimitiveString") && (isPos || isNeg)) {
result.prompt_nodes.push({
id, type: ct, title, role: getRole(),
text: inputs.value || "",
textKey: "value",
});
}
// --- Latent nodes ---
if (ct === "EmptyLatentImage" || ct === "EmptySD3LatentImage") {
result.latent_nodes.push({
id, type: ct, title,
@@ -504,6 +656,7 @@ export const comfyWorkflow = {
});
}
// --- LoRA nodes ---
if (ct === "LoraLoader" || ct === "LoraLoaderModelOnly") {
result.lora_nodes.push({
id, type: ct, title,
@@ -513,14 +666,31 @@ export const comfyWorkflow = {
});
}
// Power Lora Loader (rgthree) — dynamic lora_N inputs in API format
if (ct === "Power Lora Loader (rgthree)") {
for (const [k, v] of Object.entries(inputs)) {
if (/^lora_\d+$/.test(k) && typeof v === "string") {
result.lora_nodes.push({
id, type: ct, title,
lora_name: v,
strength_model: inputs[k.replace("lora_", "strength_")] ?? 1,
strength_clip: inputs[k.replace("lora_", "strength_clip_")] ?? 1,
});
}
}
}
// --- VAE nodes ---
if (ct === "VAELoader") {
result.vae_nodes.push({ id, type: ct, title, vae_name: inputs.vae_name });
}
// --- Save/preview nodes ---
if (ct === "SaveImage" || ct === "PreviewImage") {
result.save_nodes.push({ id, type: ct, title });
}
// --- Load image nodes ---
if (ct === "LoadImage" || ct === "LoadImageMask"
|| (ct.toLowerCase().includes("load") && ct.toLowerCase().includes("image") && inputs.image !== undefined)) {
result.load_image_nodes.push({
@@ -528,6 +698,7 @@ export const comfyWorkflow = {
});
}
// --- Text encoder (CLIP) nodes ---
if (ct === "DualCLIPLoader" || ct === "CLIPLoader") {
result.text_encoder_nodes.push({
id, type: ct, title,
@@ -536,12 +707,14 @@ export const comfyWorkflow = {
});
}
// --- Diffusion model nodes ---
if (ct === "UNETLoader" || ct === "UnetLoaderGGUF") {
result.diffusion_model_nodes.push({
id, type: ct, title, unet_name: inputs.unet_name,
});
}
// --- ControlNet loader nodes ---
if (ct.includes("ControlNetLoader")) {
result.controlnet_nodes.push({
id, type: ct, title,
+510
View File
@@ -0,0 +1,510 @@
/**
* Feeder Tab - ImageFeeder node control with embedded image library
*/
import { comfyUI } from "./comfyui-client.js";
import { showToast } from "./app.js";
// ── Module State ──────────────────────────────────────────────────
const _s = {
dir: "", // current folder relative path
images: [], // filenames in current folder
selected: new Set(),// selected filenames (across all interactions)
presets: {}, // {presetName: {directory, selected_files}}
running: false, // feeder run loop active
};
// ── Node helpers ──────────────────────────────────────────────────
function _feederNodes() {
const wf = comfyUI.currentWorkflow;
if (!wf) return [];
return Object.entries(wf)
.filter(([, n]) => n?.class_type === "ImageFeeder")
.map(([id, n]) => ({ id, title: n._meta?.title || `ImageFeeder` }));
}
export function refreshFeederNodeList() {
const sel = document.getElementById("wfm-feeder-node-sel");
if (!sel) return;
const nodes = _feederNodes();
if (nodes.length === 0) {
sel.innerHTML = `<option value="">-- No ImageFeeder node --</option>`;
return;
}
const prevVal = sel.value;
sel.innerHTML = nodes.map(n =>
`<option value="${n.id}">${n.title} (ID:${n.id})</option>`
).join("");
if (prevVal && nodes.find(n => n.id === prevVal)) sel.value = prevVal;
_loadFromNode(sel.value || nodes[0].id);
}
function _loadFromNode(nodeId) {
const wf = comfyUI.currentWorkflow;
if (!wf?.[nodeId]) return;
const inp = wf[nodeId].inputs || {};
_setVal("wfm-feeder-dir-input", inp.directory || "");
_setVal("wfm-feeder-sort", inp.sort_mode || "ascending");
_setVal("wfm-feeder-index", inp.index ?? 0);
_setVal("wfm-feeder-start", inp.start_index ?? 0);
_setVal("wfm-feeder-end", inp.end_index ?? 0);
_setVal("wfm-feeder-batch", inp.batch_size ?? 1);
_setVal("wfm-feeder-seed", inp.seed ?? 0);
_setCheck("wfm-feeder-use-sel", inp.use_selection ?? true);
// Restore selected_files
_s.selected.clear();
try {
const arr = JSON.parse(inp.selected_files || "[]");
if (Array.isArray(arr)) arr.forEach(f => _s.selected.add(f));
} catch {}
const dir = inp.directory || "";
if (_s.dir !== dir) {
_s.dir = dir;
_loadImages(dir);
_highlightTreeRow(dir);
} else {
_refreshGridCbs();
}
_updateStatus();
}
function _applyToWorkflow() {
const nodeId = document.getElementById("wfm-feeder-node-sel")?.value;
const wf = comfyUI.currentWorkflow;
if (!nodeId || !wf?.[nodeId]) {
showToast("No ImageFeeder node selected", "error");
return;
}
_applyToNode(nodeId);
showToast("Applied to workflow", "success");
}
// ── Run Loop ──────────────────────────────────────────────────────
function _setRunUI(running) {
const runBtn = document.getElementById("wfm-feeder-run-btn");
const stopBtn = document.getElementById("wfm-feeder-stop-btn");
if (runBtn) runBtn.disabled = running;
if (stopBtn) stopBtn.disabled = !running;
}
function _applyToNode(nodeId) {
const wf = comfyUI.currentWorkflow;
const inp = wf[nodeId].inputs;
inp.directory = _s.dir;
inp.sort_mode = document.getElementById("wfm-feeder-sort")?.value || "ascending";
inp.index = parseInt(document.getElementById("wfm-feeder-index")?.value) || 0;
inp.start_index = parseInt(document.getElementById("wfm-feeder-start")?.value) || 0;
inp.end_index = parseInt(document.getElementById("wfm-feeder-end")?.value) || 0;
inp.batch_size = parseInt(document.getElementById("wfm-feeder-batch")?.value) || 1;
inp.seed = parseInt(document.getElementById("wfm-feeder-seed")?.value) || 0;
inp.use_selection = document.getElementById("wfm-feeder-use-sel")?.checked ?? true;
inp.selected_files = JSON.stringify([..._s.selected]);
}
async function _startRun() {
if (_s.running) return;
if (!comfyUI.currentWorkflow) { showToast("No workflow loaded", "error"); return; }
if (!comfyUI.connected) { showToast("Not connected to ComfyUI", "error"); return; }
const nodeId = document.getElementById("wfm-feeder-node-sel")?.value;
if (!nodeId || !comfyUI.currentWorkflow[nodeId]) {
showToast("No ImageFeeder node selected", "error");
return;
}
// WebSocket を先に接続してノード sync メッセージを受け取れるようにする
const wsOk = await comfyUI.connectWebSocket();
if (!wsOk) { showToast("WebSocket connection failed", "error"); return; }
_s.running = true;
_setRunUI(true);
const progressBar = document.getElementById("wfm-gen-progress-bar");
const progressText = document.getElementById("wfm-gen-progress-text");
const resultImg = document.getElementById("wfm-gen-result-img");
// image_loop_node_sync メッセージを捕捉するリスナー
let _lastSync = null;
const _syncHandler = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === "image_loop_node_sync" && String(msg.data?.node_id) === String(nodeId)) {
_lastSync = msg.data;
}
} catch {}
};
comfyUI.socket.addEventListener("message", _syncHandler);
let count = 0;
try {
while (_s.running) {
_lastSync = null;
_applyToNode(nodeId);
const wf = comfyUI.currentWorkflow;
const inp = wf[nodeId].inputs;
if (progressBar) progressBar.style.width = "0%";
if (progressText) progressText.textContent = `#${++count} 0%`;
// 右ペインのseed設定を使用(KSamplerに適用される)
const seedMode = document.getElementById("wfm-gen-seed-mode")?.value || "random";
const seedValue = parseInt(document.getElementById("wfm-gen-seed-value")?.value) || -1;
const { images, seed } = await comfyUI.generate(
{ ...wf },
{
seedMode,
seedValue,
onProgress: (pct) => {
if (progressBar) progressBar.style.width = `${(pct * 100).toFixed(1)}%`;
if (progressText) progressText.textContent = `#${count} ${(pct * 100).toFixed(0)}%`;
},
}
);
// 右ペインのseed表示を更新(通常の生成と同じ挙動)
const seedEl = document.getElementById("wfm-gen-seed-value");
if (seedEl) seedEl.value = seed;
if (progressBar) progressBar.style.width = "100%";
if (images.length > 0 && resultImg) {
const blob = await comfyUI.getImageBlob(images[0]);
resultImg.src = URL.createObjectURL(blob);
resultImg.style.display = "block";
}
if (!_s.running) break;
// control after generate: ノードから返された next_index を反映
const control = document.getElementById("wfm-feeder-control-after")?.value || "loop";
if (control === "fixed") {
// index を変えない
} else if (_lastSync) {
const indexEl = document.getElementById("wfm-feeder-index");
if (indexEl) indexEl.value = _lastSync.next_index;
if (control === "increment" && !_lastSync.has_next) {
_s.running = false;
showToast(`Feeder complete (${count} generated)`, "success");
break;
}
// loop: has_next=false でも次は index=0 からなので続行
}
}
} catch (err) {
if (_s.running) showToast("Feeder run error: " + err.message, "error");
} finally {
comfyUI.socket?.removeEventListener("message", _syncHandler);
_s.running = false;
_setRunUI(false);
if (progressText) progressText.textContent = `Done (${count} generated)`;
}
}
async function _stopRun() {
_s.running = false;
try { await comfyUI.interrupt(); } catch {}
_setRunUI(false);
showToast("Feeder stopped", "info");
}
// ── Folder Tree ───────────────────────────────────────────────────
async function _loadTree() {
const container = document.getElementById("wfm-feeder-tree");
if (!container) return;
container.innerHTML = `<div class="wfm-feeder-tree-msg">Loading...</div>`;
try {
const res = await fetch("/image_loop/tree");
if (!res.ok) throw new Error(res.status);
const data = await res.json();
_renderTree(data.tree || [], container);
} catch (err) {
container.innerHTML = `<div class="wfm-feeder-tree-msg">Cannot reach image-loop API</div>`;
console.warn("[Feeder] tree load failed:", err);
}
}
function _renderTree(items, container, depth = 0) {
if (depth === 0) {
container.innerHTML = "";
container.appendChild(_makeTreeRow("", "🏠 (root)", 0));
}
for (const item of items) {
const icon = item.children?.length > 0 ? "📂" : "📁";
container.appendChild(_makeTreeRow(item.path, `${icon} ${item.name}`, depth + 1));
if (item.children?.length > 0) _renderTree(item.children, container, depth + 1);
}
}
function _makeTreeRow(path, label, depth) {
const row = document.createElement("div");
row.className = "wfm-feeder-tree-row" + (_s.dir === path ? " active" : "");
row.style.paddingLeft = `${8 + depth * 12}px`;
row.dataset.path = path;
row.textContent = label;
row.title = path || "(root)";
row.addEventListener("click", () => _selectDir(path));
return row;
}
function _selectDir(path) {
if (_s.dir !== path) {
_s.selected.clear();
_s.dir = path;
_setVal("wfm-feeder-dir-input", path);
}
_highlightTreeRow(path);
_loadImages(path);
_updateStatus();
}
function _highlightTreeRow(path) {
document.querySelectorAll(".wfm-feeder-tree-row").forEach(el => {
el.classList.toggle("active", el.dataset.path === path);
});
}
// ── Image Grid ────────────────────────────────────────────────────
async function _loadImages(dir) {
const grid = document.getElementById("wfm-feeder-grid");
if (!grid) return;
grid.innerHTML = `<div class="wfm-feeder-grid-msg">Loading...</div>`;
try {
const url = "/image_loop/images" + (dir ? `?dir=${encodeURIComponent(dir)}` : "");
const res = await fetch(url);
if (!res.ok) throw new Error(res.status);
const data = await res.json();
_s.images = data.images || [];
_renderGrid();
} catch (err) {
_s.images = [];
grid.innerHTML = `<div class="wfm-feeder-grid-msg" style="color:var(--wfm-danger)">Load failed: ${err.message}</div>`;
}
_updateStatus();
}
function _renderGrid() {
const grid = document.getElementById("wfm-feeder-grid");
if (!grid) return;
if (_s.images.length === 0) {
grid.innerHTML = `<div class="wfm-feeder-grid-msg">No images found</div>`;
return;
}
grid.innerHTML = "";
for (const fname of _s.images) grid.appendChild(_makeCard(fname));
}
function _makeCard(fname) {
const relPath = _s.dir ? `${_s.dir}/${fname}` : fname;
const checked = _s.selected.has(fname);
const card = document.createElement("div");
card.className = "wfm-feeder-card" + (checked ? " selected" : "");
card.dataset.name = fname;
const cb = document.createElement("input");
cb.type = "checkbox";
cb.className = "wfm-feeder-card-cb";
cb.checked = checked;
cb.addEventListener("change", e => {
e.stopPropagation();
if (cb.checked) _s.selected.add(fname);
else _s.selected.delete(fname);
card.classList.toggle("selected", cb.checked);
_updateStatus();
});
const img = document.createElement("img");
img.className = "wfm-feeder-card-img";
img.loading = "lazy";
img.src = `/image_loop/thumbnail?path=${encodeURIComponent(relPath)}`;
img.onerror = () => { img.style.display = "none"; card.style.minHeight = "110px"; };
const name = document.createElement("div");
name.className = "wfm-feeder-card-name";
name.textContent = fname;
name.title = fname;
card.append(cb, img, name);
card.addEventListener("click", e => {
if (e.target === cb) return;
_showPreview(relPath, fname);
});
return card;
}
function _refreshGridCbs() {
document.querySelectorAll(".wfm-feeder-card").forEach(card => {
const sel = _s.selected.has(card.dataset.name);
card.classList.toggle("selected", sel);
const cb = card.querySelector("input[type=checkbox]");
if (cb) cb.checked = sel;
});
}
function _selectAll() {
_s.images.forEach(f => _s.selected.add(f));
_refreshGridCbs();
_updateStatus();
}
function _deselectAll() {
_s.images.forEach(f => _s.selected.delete(f));
_refreshGridCbs();
_updateStatus();
}
// ── Status Bar ───────────────────────────────────────────────────
function _updateStatus() {
const el = document.getElementById("wfm-feeder-status");
if (!el) return;
const inCurrent = _s.images.filter(f => _s.selected.has(f)).length;
el.textContent = `📂 ${_s.dir || "(root)"} | ${inCurrent} / ${_s.images.length} selected`;
}
// ── Preview Panel ────────────────────────────────────────────────
async function _showPreview(relPath, fname) {
const imgEl = document.getElementById("wfm-feeder-preview-img");
const nameEl = document.getElementById("wfm-feeder-preview-name");
const infoEl = document.getElementById("wfm-feeder-preview-info");
if (imgEl) {
imgEl.src = `/image_loop/thumbnail?path=${encodeURIComponent(relPath)}`;
imgEl.style.display = "block";
}
if (nameEl) nameEl.textContent = fname;
if (infoEl) infoEl.textContent = "Loading...";
try {
const res = await fetch(`/image_loop/image_info?path=${encodeURIComponent(relPath)}`);
if (res.ok && infoEl) {
const d = await res.json();
const kb = d.size_bytes ? (d.size_bytes / 1024).toFixed(1) + " KB" : "";
infoEl.textContent = `${d.width ?? "?"}×${d.height ?? "?"} px\n${kb}`;
}
} catch { if (infoEl) infoEl.textContent = ""; }
}
// ── Presets ──────────────────────────────────────────────────────
async function _loadPresets() {
try {
const res = await fetch("/image_feeder/presets");
if (!res.ok) return;
_s.presets = await res.json();
} catch { _s.presets = {}; }
_renderPresets();
}
function _renderPresets() {
const sel = document.getElementById("wfm-feeder-preset-sel");
if (!sel) return;
const names = Object.keys(_s.presets).sort();
sel.innerHTML = `<option value="">-- Select preset --</option>` +
names.map(n => `<option value="${n}">${n}</option>`).join("");
}
async function _savePreset() {
const name = document.getElementById("wfm-feeder-preset-name")?.value.trim();
if (!name) { showToast("Enter a preset name", "error"); return; }
const sel = _s.images.filter(f => _s.selected.has(f));
try {
const res = await fetch("/image_feeder/presets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, directory: _s.dir, selected_files: sel }),
});
if (res.ok) {
showToast(`Preset saved: ${name}`, "success");
const nameInput = document.getElementById("wfm-feeder-preset-name");
if (nameInput) nameInput.value = "";
await _loadPresets();
const selEl = document.getElementById("wfm-feeder-preset-sel");
if (selEl) selEl.value = name;
} else { showToast("Save failed", "error"); }
} catch (err) { showToast("Save error: " + err.message, "error"); }
}
async function _applyPreset() {
const selEl = document.getElementById("wfm-feeder-preset-sel");
const name = selEl?.value;
if (!name || !_s.presets[name]) { showToast("Select a preset first", "error"); return; }
const preset = _s.presets[name];
const dir = preset.directory || "";
_s.selected.clear();
(preset.selected_files || []).forEach(f => _s.selected.add(f));
_setVal("wfm-feeder-dir-input", dir);
if (_s.dir !== dir) {
_s.dir = dir;
_highlightTreeRow(dir);
await _loadImages(dir);
}
_refreshGridCbs();
_updateStatus();
showToast(`Preset loaded: ${name}`, "success");
}
async function _deletePreset() {
const name = document.getElementById("wfm-feeder-preset-sel")?.value;
if (!name) return;
try {
await fetch(`/image_feeder/presets/${encodeURIComponent(name)}`, { method: "DELETE" });
showToast(`Preset deleted: ${name}`, "info");
await _loadPresets();
} catch (err) { showToast("Delete error", "error"); }
}
// ── Helpers ───────────────────────────────────────────────────────
function _setVal(id, val) {
const el = document.getElementById(id);
if (el) el.value = val ?? "";
}
function _setCheck(id, val) {
const el = document.getElementById(id);
if (el) el.checked = !!val;
}
// ── Init ─────────────────────────────────────────────────────────
export async function initFeederTab() {
document.getElementById("wfm-feeder-node-sel")?.addEventListener("change", e => {
_loadFromNode(e.target.value);
});
document.getElementById("wfm-feeder-apply-btn")?.addEventListener("click", _applyToWorkflow);
document.getElementById("wfm-feeder-dir-input")?.addEventListener("change", e => {
const val = e.target.value.trim();
if (_s.dir !== val) {
_s.dir = val;
_highlightTreeRow(val);
_loadImages(val);
}
});
document.getElementById("wfm-feeder-reload-tree")?.addEventListener("click", _loadTree);
document.getElementById("wfm-feeder-sel-all")?.addEventListener("click", _selectAll);
document.getElementById("wfm-feeder-desel-all")?.addEventListener("click", _deselectAll);
document.getElementById("wfm-feeder-preset-save")?.addEventListener("click", _savePreset);
document.getElementById("wfm-feeder-preset-load")?.addEventListener("click", _applyPreset);
document.getElementById("wfm-feeder-preset-del")?.addEventListener("click", _deletePreset);
document.getElementById("wfm-feeder-run-btn")?.addEventListener("click", _startRun);
document.getElementById("wfm-feeder-stop-btn")?.addEventListener("click", _stopRun);
await Promise.all([_loadTree(), _loadPresets()]);
}
+431 -49
View File
@@ -8,6 +8,7 @@ import { comfyWorkflow } from "./comfyui-workflow.js";
import { comfyEditor } from "./comfyui-editor.js";
import { t } from "./i18n.js";
import { syncJsonHighlight, syncScroll } from "./json-highlight.js";
import { initFeederTab, refreshFeederNodeList } from "./feeder-tab.js";
// ============================================
// Eagle Auto-Save
@@ -145,6 +146,8 @@ export async function loadWorkflowIntoEditor(workflow, filename) {
const genBtn = document.getElementById("wfm-gen-generate-btn");
if (genBtn) genBtn.disabled = !comfyUI.connected;
refreshFeederNodeList();
showToast(`Workflow loaded: ${filename || ""}`, "success");
return true;
}
@@ -153,49 +156,286 @@ export async function loadWorkflowIntoEditor(workflow, filename) {
// Checkpoint Batch
// ============================================
const _ckptBatch = { aborted: false };
const _ckptBatch = { aborted: false, paused: false, _resumeResolve: null };
function _parseFolderList(str) {
return str.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
async function _waitIfPaused() {
if (!_ckptBatch.paused) return;
await new Promise((resolve) => { _ckptBatch._resumeResolve = resolve; });
}
function _getModelFolder(modelPath) {
const normalized = modelPath.replace(/\\/g, "/");
const idx = normalized.indexOf("/");
return idx === -1 ? "" : normalized.substring(0, idx).toLowerCase();
function _setPauseBtnState(paused) {
const btn = document.getElementById("wfm-ckpt-batch-pause-btn");
if (!btn) return;
btn.textContent = paused ? "Resume" : "Pause";
btn.style.background = paused ? "var(--wfm-success, #22c55e)" : "";
btn.style.color = paused ? "#fff" : "";
}
function _filterCheckpoints(checkpoints, includeStr, excludeStr) {
const inc = _parseFolderList(includeStr);
const exc = _parseFolderList(excludeStr);
return checkpoints.filter((m) => {
const folder = _getModelFolder(m);
if (inc.length > 0 && !inc.includes(folder)) return false;
if (exc.length > 0 && exc.includes(folder)) return false;
return true;
});
// mode: "all" = 全選択, "some" = 一部選択, "none" = 全解除
const _ckptState = { mode: "all", selected: new Set() };
function _getSelectedCheckpoints() {
const all = comfyEditor.models.checkpoints || [];
if (_ckptState.mode === "all") return all;
if (_ckptState.mode === "none") return [];
return all.filter((m) => _ckptState.selected.has(m));
}
function _updateDropdownLabel() {
const labelEl = document.getElementById("wfm-ckpt-dropdown-label");
if (!labelEl) return;
const total = (comfyEditor.models.checkpoints || []).length;
if (_ckptState.mode === "all") {
labelEl.textContent = total > 0 ? `All (${total})` : "All checkpoints";
} else if (_ckptState.mode === "none") {
labelEl.textContent = `0 / ${total} selected`;
} else {
labelEl.textContent = `${_ckptState.selected.size} / ${total} selected`;
}
}
// モデルパスリストをフォルダ → [modelPath, ...] のMapに変換
function _buildFolderTree(models) {
const map = new Map();
for (const m of models) {
const normalized = m.replace(/\\/g, "/");
const lastSlash = normalized.lastIndexOf("/");
const folder = lastSlash === -1 ? "" : normalized.substring(0, lastSlash);
if (!map.has(folder)) map.set(folder, []);
map.get(folder).push(m);
}
return new Map([...map.entries()].sort((a, b) => {
if (a[0] === b[0]) return 0;
if (a[0] === "") return -1;
if (b[0] === "") return 1;
return a[0].localeCompare(b[0]);
}));
}
// フォルダ内の選択状態を返す: "checked" | "indeterminate" | "unchecked"
function _getFolderCheckState(folderModels) {
if (_ckptState.mode === "all") return "checked";
if (_ckptState.mode === "none") return "unchecked";
const selCount = folderModels.filter((m) => _ckptState.selected.has(m)).length;
if (selCount === 0) return "unchecked";
if (selCount === folderModels.length) return "checked";
return "indeterminate";
}
// 単一モデルの選択トグル(_ckptState を更新)
function _toggleSingleModel(m, checked, all) {
if (_ckptState.mode === "all" && !checked) {
_ckptState.mode = "some";
_ckptState.selected.clear();
all.forEach((x) => _ckptState.selected.add(x));
} else if (_ckptState.mode === "none" && checked) {
_ckptState.mode = "some";
_ckptState.selected.clear();
}
if (checked) _ckptState.selected.add(m);
else _ckptState.selected.delete(m);
if (_ckptState.selected.size === all.length) { _ckptState.mode = "all"; _ckptState.selected.clear(); }
if (_ckptState.mode === "some" && _ckptState.selected.size === 0) _ckptState.mode = "none";
}
// フォルダ単位の一括トグル(_ckptState を更新)
function _toggleFolderModels(folderModels, checked, all) {
if (_ckptState.mode === "all" && !checked) {
_ckptState.mode = "some";
_ckptState.selected.clear();
all.forEach((x) => _ckptState.selected.add(x));
} else if (_ckptState.mode === "none" && checked) {
_ckptState.mode = "some";
_ckptState.selected.clear();
}
folderModels.forEach((m) => { if (checked) _ckptState.selected.add(m); else _ckptState.selected.delete(m); });
if (_ckptState.selected.size === all.length) { _ckptState.mode = "all"; _ckptState.selected.clear(); }
if (_ckptState.mode === "some" && _ckptState.selected.size === 0) _ckptState.mode = "none";
}
function _rebuildCkptList() {
const listEl = document.getElementById("wfm-ckpt-list");
if (!listEl) return;
const all = comfyEditor.models.checkpoints || [];
const search = document.getElementById("wfm-ckpt-search")?.value.toLowerCase() || "";
const filtered = search ? all.filter((m) => m.toLowerCase().includes(search)) : all;
if (filtered.length === 0) {
listEl.innerHTML = `<div style="padding:8px 10px;font-size:11px;color:var(--wfm-text-secondary);">No checkpoints</div>`;
return;
}
const folderTree = _buildFolderTree(filtered);
listEl.innerHTML = "";
for (const [folder, models] of folderTree) {
const group = document.createElement("div");
group.className = "wfm-ckpt-folder-group open";
// --- フォルダヘッダー ---
const header = document.createElement("div");
header.className = "wfm-ckpt-folder-header";
const folderCb = document.createElement("input");
folderCb.type = "checkbox";
const folderState = _getFolderCheckState(models);
folderCb.checked = folderState === "checked";
folderCb.indeterminate = folderState === "indeterminate";
const toggle = document.createElement("span");
toggle.className = "wfm-ckpt-folder-toggle";
toggle.textContent = "▶";
const nameSpan = document.createElement("span");
nameSpan.className = "wfm-ckpt-folder-name";
nameSpan.textContent = folder || "(root)";
nameSpan.title = folder || "(root)";
const countSpan = document.createElement("span");
countSpan.className = "wfm-ckpt-folder-count";
countSpan.textContent = models.length;
// --- ファイルリスト ---
const filesDiv = document.createElement("div");
filesDiv.className = "wfm-ckpt-folder-files";
for (const m of models) {
const itemLabel = document.createElement("label");
itemLabel.className = "wfm-ckpt-item wfm-ckpt-item--indented";
const fileCb = document.createElement("input");
fileCb.type = "checkbox";
fileCb.value = m;
if (_ckptState.mode === "all") fileCb.checked = true;
else if (_ckptState.mode === "none") fileCb.checked = false;
else fileCb.checked = _ckptState.selected.has(m);
const normalized = m.replace(/\\/g, "/");
const lastSlash = normalized.lastIndexOf("/");
const fileName = lastSlash === -1 ? m : m.substring(lastSlash + 1);
const fileSpan = document.createElement("span");
fileSpan.className = "wfm-ckpt-item-label";
fileSpan.textContent = fileName;
fileSpan.title = m;
fileCb.addEventListener("change", () => {
_toggleSingleModel(m, fileCb.checked, all);
const s = _getFolderCheckState(models);
folderCb.checked = s === "checked";
folderCb.indeterminate = s === "indeterminate";
_updateDropdownLabel();
_updateBatchInfo();
});
itemLabel.appendChild(fileCb);
itemLabel.appendChild(fileSpan);
filesDiv.appendChild(itemLabel);
}
// フォルダCBのchange
folderCb.addEventListener("change", () => {
_toggleFolderModels(models, folderCb.checked, all);
folderCb.indeterminate = false;
filesDiv.querySelectorAll("input[type=checkbox]").forEach((cb) => { cb.checked = folderCb.checked; });
_updateDropdownLabel();
_updateBatchInfo();
});
// フォルダ名 / トグル矢印クリックで展開・折りたたみ
[toggle, nameSpan].forEach((el) => {
el.addEventListener("click", () => group.classList.toggle("open"));
});
header.appendChild(folderCb);
header.appendChild(toggle);
header.appendChild(nameSpan);
header.appendChild(countSpan);
group.appendChild(header);
group.appendChild(filesDiv);
listEl.appendChild(group);
}
}
function _updateBatchInfo() {
const infoEl = document.getElementById("wfm-ckpt-batch-info");
if (!infoEl) return;
const inc = document.getElementById("wfm-ckpt-batch-include")?.value || "";
const exc = document.getElementById("wfm-ckpt-batch-exclude")?.value || "";
const n = _filterCheckpoints(comfyEditor.models.checkpoints, inc, exc).length;
const n = _getSelectedCheckpoints().length;
infoEl.textContent = `${n} checkpoint${n !== 1 ? "s" : ""} will be processed`;
}
function initCheckpointBatch() {
const checkbox = document.getElementById("wfm-ckpt-batch-enabled");
const body = document.getElementById("wfm-ckpt-batch-body");
const wrap = document.getElementById("wfm-ckpt-dropdown-wrap");
const btn = document.getElementById("wfm-ckpt-dropdown-btn");
const panel = document.getElementById("wfm-ckpt-dropdown-panel");
checkbox?.addEventListener("change", () => {
if (body) body.style.display = checkbox.checked ? "block" : "none";
if (checkbox.checked) _updateBatchInfo();
if (checkbox.checked) {
_updateDropdownLabel();
_updateBatchInfo();
}
});
document.getElementById("wfm-ckpt-batch-include")?.addEventListener("input", _updateBatchInfo);
document.getElementById("wfm-ckpt-batch-exclude")?.addEventListener("input", _updateBatchInfo);
// ドロップダウン開閉
btn?.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = panel.style.display !== "none";
if (isOpen) {
panel.style.display = "none";
wrap.classList.remove("open");
} else {
_rebuildCkptList();
panel.style.display = "block";
wrap.classList.add("open");
document.getElementById("wfm-ckpt-search")?.focus();
}
});
// 外側クリックで閉じる
document.addEventListener("click", (e) => {
if (panel && panel.style.display !== "none" && !wrap?.contains(e.target)) {
panel.style.display = "none";
wrap?.classList.remove("open");
}
});
// 検索フィルター
document.getElementById("wfm-ckpt-search")?.addEventListener("input", _rebuildCkptList);
// 全選択
document.getElementById("wfm-ckpt-select-all")?.addEventListener("click", () => {
_ckptState.mode = "all";
_ckptState.selected.clear();
_rebuildCkptList();
_updateDropdownLabel();
_updateBatchInfo();
});
// 全解除
document.getElementById("wfm-ckpt-deselect-all")?.addEventListener("click", () => {
_ckptState.mode = "none";
_ckptState.selected.clear();
_rebuildCkptList();
_updateDropdownLabel();
_updateBatchInfo();
});
// Pause / Resume
document.getElementById("wfm-ckpt-batch-pause-btn")?.addEventListener("click", () => {
if (!_ckptBatch.paused) {
_ckptBatch.paused = true;
_setPauseBtnState(true);
} else {
_ckptBatch.paused = false;
_setPauseBtnState(false);
if (_ckptBatch._resumeResolve) {
_ckptBatch._resumeResolve();
_ckptBatch._resumeResolve = null;
}
}
});
}
// ============================================
@@ -283,12 +523,10 @@ async function _runBatchGenerate() {
return;
}
const inc = document.getElementById("wfm-ckpt-batch-include")?.value || "";
const exc = document.getElementById("wfm-ckpt-batch-exclude")?.value || "";
const list = _filterCheckpoints(comfyEditor.models.checkpoints, inc, exc);
const list = _getSelectedCheckpoints();
if (list.length === 0) {
showToast("No checkpoints match the folder filter", "error");
showToast("No checkpoints selected", "error");
return;
}
@@ -299,33 +537,54 @@ async function _runBatchGenerate() {
const progressText = document.getElementById("wfm-gen-progress-text");
if (batchProgress) batchProgress.style.display = "block";
const pauseBtn = document.getElementById("wfm-ckpt-batch-pause-btn");
if (pauseBtn) { pauseBtn.style.display = "block"; pauseBtn.disabled = false; }
_setPauseBtnState(false);
let completed = 0;
let failed = 0;
for (let i = 0; i < list.length; i++) {
if (_ckptBatch.aborted) break;
try {
for (let i = 0; i < list.length; i++) {
if (_ckptBatch.aborted) break;
const model = list[i];
if (batchCurrentName) batchCurrentName.textContent = model;
if (batchCount) batchCount.textContent = `${i + 1} / ${list.length}`;
if (batchBar) batchBar.style.width = `${((i / list.length) * 100).toFixed(1)}%`;
if (progressText) progressText.textContent = `[${i + 1}/${list.length}] Loading...`;
// 一時停止中は次のモデルへ進む前に待機
if (_ckptBatch.paused) {
if (batchCurrentName) batchCurrentName.textContent = "Paused...";
if (progressText) progressText.textContent = "Paused";
}
await _waitIfPaused();
if (_ckptBatch.aborted) break;
for (const node of ckptNodes) {
if (comfyUI.currentWorkflow?.[node.id]) {
comfyUI.currentWorkflow[node.id].inputs.ckpt_name = model;
const model = list[i];
if (batchCurrentName) batchCurrentName.textContent = model;
if (batchCount) batchCount.textContent = `${i + 1} / ${list.length}`;
if (batchBar) batchBar.style.width = `${((i / list.length) * 100).toFixed(1)}%`;
if (progressText) progressText.textContent = `[${i + 1}/${list.length}] Loading...`;
for (const node of ckptNodes) {
if (comfyUI.currentWorkflow?.[node.id]) {
comfyUI.currentWorkflow[node.id].inputs.ckpt_name = model;
}
}
try {
await _coreGenerate(true);
completed++;
} catch (err) {
if (_ckptBatch.aborted) break;
failed++;
showToast(`[${i + 1}/${list.length}] Failed: ${err.message}`, "error");
}
}
try {
await _coreGenerate(true);
completed++;
} catch (err) {
if (_ckptBatch.aborted) break;
failed++;
showToast(`[${i + 1}/${list.length}] Failed: ${err.message}`, "error");
} finally {
if (pauseBtn) pauseBtn.disabled = true;
_ckptBatch.paused = false;
if (_ckptBatch._resumeResolve) {
_ckptBatch._resumeResolve();
_ckptBatch._resumeResolve = null;
}
_setPauseBtnState(false);
}
if (batchBar) batchBar.style.width = "100%";
@@ -360,6 +619,8 @@ async function handleGenerate() {
genBtn.disabled = true;
if (interruptBtn) interruptBtn.style.display = "inline-block";
_ckptBatch.aborted = false;
_ckptBatch.paused = false;
_ckptBatch._resumeResolve = null;
const batchEnabled = document.getElementById("wfm-ckpt-batch-enabled")?.checked;
@@ -408,6 +669,7 @@ export async function initGenerateTab() {
comfyEditor.renderAll(comfyUI.currentAnalysis, comfyUI.currentWorkflow);
}
showToast("Model lists refreshed", "success");
_updateDropdownLabel();
_updateBatchInfo();
});
@@ -417,6 +679,12 @@ export async function initGenerateTab() {
// Interrupt button (stops both single generation and batch loop)
document.getElementById("wfm-gen-interrupt-btn")?.addEventListener("click", async () => {
_ckptBatch.aborted = true;
// 一時停止中でもループを抜けられるよう待機を解除
_ckptBatch.paused = false;
if (_ckptBatch._resumeResolve) {
_ckptBatch._resumeResolve();
_ckptBatch._resumeResolve = null;
}
await comfyUI.interrupt();
showToast("Interrupted", "info");
});
@@ -424,8 +692,13 @@ export async function initGenerateTab() {
// Move shared Raw JSON widget into the active tab's rawjson-col
function moveRawJsonToTab(tabKey) {
const widget = document.getElementById("wfm-gen-rawjson-widget");
if (!widget) return;
if (tabKey === "feeder") {
widget.style.display = "none";
return;
}
const col = document.getElementById(`wfm-gen-rawjson-col-${tabKey}`);
if (widget && col) {
if (col) {
col.appendChild(widget);
widget.style.display = "flex";
}
@@ -458,14 +731,119 @@ export async function initGenerateTab() {
}
});
// Raw JSON highlight sync on input/scroll
// Raw JSON highlight sync + search
{
const editor = document.getElementById("wfm-gen-raw-json");
const highlight = document.getElementById("wfm-gen-raw-json-highlight");
if (editor && highlight) {
editor.addEventListener("input", () => syncJsonHighlight(highlight, editor.value));
editor.addEventListener("scroll", () => syncScroll(editor, highlight));
const searchOverlay = document.getElementById("wfm-gen-raw-json-search-overlay");
const searchInput = document.getElementById("wfm-gen-raw-search");
const searchCount = document.getElementById("wfm-gen-raw-search-count");
const searchPrev = document.getElementById("wfm-gen-raw-search-prev");
const searchNext = document.getElementById("wfm-gen-raw-search-next");
const searchClear = document.getElementById("wfm-gen-raw-search-clear");
let currentMatchIndex = 0;
let matchPositions = [];
function updateSearchOverlay() {
if (!editor || !searchOverlay || !searchInput) return;
const term = searchInput.value;
if (!term) {
searchOverlay.innerHTML = "";
if (searchCount) { searchCount.textContent = ""; searchCount.style.color = ""; }
matchPositions = [];
return;
}
const text = editor.value;
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(escapedTerm, "gi");
matchPositions = [];
let m;
while ((m = regex.exec(text)) !== null) matchPositions.push(m.index);
if (matchPositions.length === 0) {
searchOverlay.innerHTML = "";
if (searchCount) { searchCount.textContent = "No results"; searchCount.style.color = "#e06c75"; }
return;
}
if (searchCount) searchCount.style.color = "";
if (currentMatchIndex >= matchPositions.length) currentMatchIndex = 0;
if (searchCount) searchCount.textContent = `${currentMatchIndex + 1}/${matchPositions.length}`;
const escapedHtml = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
let matchIdx = 0;
const html = escapedHtml.replace(new RegExp(escapedTerm, "gi"), (match) => {
const cls = matchIdx === currentMatchIndex ? "wfm-search-current" : "wfm-search-match";
matchIdx++;
return `<mark class="${cls}">${match}</mark>`;
});
searchOverlay.innerHTML = html + "\n";
// Scroll editor to current match
const pos = matchPositions[currentMatchIndex];
editor.focus();
editor.setSelectionRange(pos, pos + term.length);
const lineNum = (text.slice(0, pos).match(/\n/g) || []).length;
const lineH = parseFloat(getComputedStyle(editor).lineHeight) || 18;
editor.scrollTop = Math.max(0, lineH * lineNum - editor.clientHeight / 2);
searchOverlay.scrollTop = editor.scrollTop;
searchOverlay.scrollLeft = editor.scrollLeft;
if (highlight) highlight.scrollTop = editor.scrollTop;
}
if (editor && highlight) {
editor.addEventListener("input", () => {
syncJsonHighlight(highlight, editor.value);
if (searchInput?.value) updateSearchOverlay();
});
editor.addEventListener("scroll", () => {
syncScroll(editor, highlight);
if (searchOverlay) {
searchOverlay.scrollTop = editor.scrollTop;
searchOverlay.scrollLeft = editor.scrollLeft;
}
});
}
searchInput?.addEventListener("input", () => { currentMatchIndex = 0; updateSearchOverlay(); });
searchNext?.addEventListener("click", () => {
if (!matchPositions.length) return;
currentMatchIndex = (currentMatchIndex + 1) % matchPositions.length;
updateSearchOverlay();
});
searchPrev?.addEventListener("click", () => {
if (!matchPositions.length) return;
currentMatchIndex = (currentMatchIndex - 1 + matchPositions.length) % matchPositions.length;
updateSearchOverlay();
});
function clearSearch() {
if (searchInput) searchInput.value = "";
currentMatchIndex = 0;
matchPositions = [];
if (searchOverlay) searchOverlay.innerHTML = "";
if (searchCount) { searchCount.textContent = ""; searchCount.style.color = ""; }
}
searchClear?.addEventListener("click", clearSearch);
searchInput?.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
if (!matchPositions.length) return;
currentMatchIndex = e.shiftKey
? (currentMatchIndex - 1 + matchPositions.length) % matchPositions.length
: (currentMatchIndex + 1) % matchPositions.length;
updateSearchOverlay();
e.preventDefault();
} else if (e.key === "Escape") {
clearSearch();
}
});
}
// Check for workflow loaded from Workflow tab (via sessionStorage)
@@ -487,11 +865,15 @@ export async function initGenerateTab() {
// Checkpoint batch UI
initCheckpointBatch();
// Feeder tab
await initFeederTab();
// Auto-connect on init
const connected = await comfyUI.checkConnection();
updateStatus(connected);
if (connected) {
await comfyEditor.loadModelLists();
_updateDropdownLabel();
_updateBatchInfo();
}
+342 -30
View File
@@ -324,6 +324,52 @@ const LANGUAGES = {
galleryOutputDirLabel: "Output Folder Path",
galleryOutputDirHint: "Set the ComfyUI output folder to browse in the Gallery tab. Leave blank to use ComfyUI's default output directory.",
tabAi: "AI TOOL",
// -- AI Tab --
aiSubTabTranslate: "Translate",
aiSubTabVlm: "VLM",
aiSubTabSettings: "Settings",
aiLangJa: "Japanese", aiLangEn: "English", aiLangZh: "Chinese", aiLangFree: "Free",
aiTransInputPlaceholder: "Enter text to translate...",
aiTransOutputPlaceholder: "Translation result will appear here...",
aiTranslateBtn: "Translate",
aiCopyBtn: "Copy",
aiVlmDropLabel: "Drop PNG / JPG / WebP here or click",
aiVlmDescribe: "Describe Image",
aiVlmPromptCreate: "Create Prompt",
aiVlmRunBtn: "Run",
aiVlmResultPlaceholder: "Result will appear here...",
aiSettingsBackend: "Backend",
aiSettingsConnection: "Connection",
aiSettingsTestBtn: "Test Connection",
aiSettingsModelSection: "Model",
aiSettingsRefreshBtn: "Refresh",
aiSettingsModelPlaceholder: "-- Select Model --",
aiSettingsFreeLang: "Free Language",
aiSettingsInputLang: "Input",
aiSettingsOutputLang: "Output",
aiSettingsSaveBtn: "Save Settings",
aiToastNoText: "Please enter text",
aiToastNoModel: "Please select a model in the Settings tab",
aiToastInvalidUrl: "URL must start with http:// or https://",
aiToastInvalidUrlInput: "Please enter a URL starting with http:// or https://",
aiStatusTranslating: "Translating...",
aiStatusRunning: "Running...",
aiStatusDone: "Done",
aiStatusConnecting: "Connecting...",
aiStatusConnectOk: "Connected",
aiStatusConnectFail: "Connection failed: ",
aiToastTransFailed: "Translation failed: ",
aiToastNoCopyText: "No text to copy",
aiToastCopied: "Copied",
aiToastNoModels: "No models found",
aiToastModelsFailed: "Failed to fetch models: ",
aiToastSettingsSaved: "Settings saved",
aiToastNoImage: "Please drop an image",
aiToastVlmFailed: "VLM failed: ",
aiModels: "models",
// -- Help & Support --
tabHelp: "Help",
helpManualTitle: "Features",
@@ -340,14 +386,26 @@ const LANGUAGES = {
helpWf7: "Badge management: add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)",
helpGen1: "Connect/disconnect ComfyUI and check connection status",
helpGen2: "Load workflows from Workflow tab (App format .app.json not supported)",
helpGen3: "3-tab layout — Input (Prompt + Image), Model, Settings — each with a Raw JSON column for instant preview and direct editing",
helpGen3: "4-tab layout — Input (Prompt + Image), Model, Settings, Feeder — Input/Model/Settings each include a Raw JSON column for instant preview and direct editing",
helpGen4: "Input tab: Prompt (top) and Image drag-and-drop (bottom) in the left column",
helpGen5: "Model tab: Checkpoint, VAE, LoRA, ControlNet, UNET, TextEncoder selectors with filter",
helpGen6: "Settings tab: KSampler (top) and LatentImage (bottom) in the left column",
helpGen6: "Settings tab: KSampler and Latent Image side by side, each at 50% width",
helpGen7: "Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow",
helpGen8: "Seed control: random, fixed, increment, decrement",
helpGen9: "Generate, progress display, stop, and view results",
helpGen10: "Eagle integration: auto-save generated images to Eagle",
helpGen11: "Checkpoint Batch: select checkpoints by folder from the dropdown — check a folder to select all its files, or expand with ▶ to check individual files; use Filter to search; Pause/Resume suspends and continues between models; Stop aborts after the current generation",
helpFeederTitle: "GenerateUI Tab — Feeder subtab",
helpFeederDesc: "Requires the comfyui-image-feeder custom node. Controls ImageFeeder nodes and runs continuous generation loops over an image library stored in user/default/image-loop-data/.",
helpFeeder1: "Left pane — Node settings: select the target ImageFeeder node from the dropdown; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection); click Apply to write changes to the loaded workflow",
helpFeeder2: "Left pane — Presets: save the current directory + selection as a named preset; load or delete saved presets",
helpFeeder3: "Left pane — Run: After gen mode determines index behavior after each generation — Loop (wrap back to 0 and continue indefinitely), Increment (advance index and stop when all images are exhausted), Fixed (always use the same index); click Run to start the loop, Stop to interrupt",
helpFeeder4: "Center pane — Folder tree: browse subfolders inside image-loop-data; click a folder to load its images; click ↺ to reload the tree",
helpFeeder5: "Center pane — Image grid: displays thumbnails of all images in the selected folder; check individual images to add them to the selection; All / None buttons select or clear the entire folder; click an image to open the preview panel",
helpFeeder6: "Center pane — Preview panel: shows the clicked image at larger size with filename, resolution, and file size",
helpFeeder7: "Status bar: shows the current folder path and how many images are selected out of the total in the folder",
helpFeeder8: "Seed during Run: uses the right-pane seed setting (Random / Fixed / Increment / Decrement) — the ImageFeeder's own Seed field controls random sort order only",
helpFeeder9: "Index sync: after each generation the node returns the next index via WebSocket; the Index field updates automatically so the display stays in sync",
helpPrompt1: "AI assistant (Ollama): model selection, chat for prompt consultation, image attachment (vision models)",
helpPrompt2: "Apply AI responses to GenerateUI prompts",
helpPrompt3: "Translation: JA\u2194EN, ZH\u2194EN",
@@ -355,6 +413,8 @@ const LANGUAGES = {
helpPrompt5: "Preset Manager: browse all presets, favorites, and group-based filtering",
helpPrompt6: "Group management: create groups, assign/remove presets, delete groups",
helpPrompt7: "GenUI Set: apply preset prompts directly to GenerateUI",
helpPrompt8: "Wildcard toolbar: one-click buttons to insert wildcard syntax ({|}, {n$|}, __|__, <lora:>, etc.); wraps selected text",
helpPrompt9: "Wildcard file manager: create/edit/delete .txt/.yaml wildcard files; click filename to insert __filename__ at cursor",
helpSettings1: "UI language and summary language",
helpSettings2: "Theme selection (13 presets) with customization: color overrides, background patterns (stripes, polka dot, checkerboard, SVG tiling), and font selection (16 Google Fonts including Japanese display fonts)",
helpSettings3: "Workflow data folder path",
@@ -364,6 +424,7 @@ const LANGUAGES = {
helpSettings7: "Eagle integration URL and auto-save toggle",
helpSettings8: "Data Management: export all plugin data (settings, metadata, prompts, etc.) to a JSON file, or import to restore. After importing, restart ComfyUI.",
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",
helpNodes1: "Node Browser: search by name/description/alias/tag, filter by category, package, tag, group, favorites",
helpNodes2: "Card and Table view modes with pagination",
helpNodes3: "Favorite nodes and add custom tags for organization",
@@ -374,7 +435,7 @@ const LANGUAGES = {
helpModels2: "Thumbnail, Card, and Table view modes with pagination",
helpModels3: "Search, tag filter, group filter, favorites filter, and status filter (All / Enabled only / Disabled only)",
helpModels4: "Side panel: Info (file path, tags, memo), Groups management, CivitAI integration",
helpModels5: "CivitAI integration: fetch model info by SHA256 hash, batch fetch per model type",
helpModels5: "CivitAI integration: fetch model info by SHA256 hash, batch fetch per model type; preview image is automatically downloaded and saved if none exists",
helpModels6: "Detail modal: preview image, CivitAI info (base model, trained words, tags), thumbnail change",
helpModels7: "Group management: create, rename, delete groups and assign/remove models",
helpModels8: "GenUI Model button: apply the selected model to the corresponding node in GenerateUI's current workflow (Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)",
@@ -386,12 +447,13 @@ const LANGUAGES = {
helpGallery2: "3-column layout: folder tree (left) | image grid (center) | detail panel (right)",
helpGallery3: "Thumbnail and Table view modes; double-click any image to open fullscreen lightbox",
helpGallery4: "Toolbar filters: favorites (★), text search, tag filter, group filter, sort by date/name",
helpGallery5: "Multi-select with Ctrl+click — bulk operations: Add to Group, Favorite All, Unfavorite All",
helpGallery6: "Detail panel — Info tab: filename, size, date, tags (add/remove), memo with save",
helpGallery7: "Detail panel — Metadata tab: embedded ComfyUI workflow JSON viewer with one-click clipboard copy",
helpGallery8: "Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image",
helpGallery9: "Supports PNG (tEXt/iTXt metadata), JPEG, WebP, GIF; PNG workflow extraction reads ComfyUI-embedded JSON",
helpGallery10: "Folder tree: click a folder to load its images; expand subfolders with ▶ arrow",
helpGallery5: "Folder tree: click to load images, expand with ▶; '+ New' creates a subfolder inside the selected folder, 'Del' deletes it with all contents",
helpGallery6: "Multi-select with Ctrl+click — bulk operations: Add to Group, Favorite All, Unfavorite All, Move To..., Delete",
helpGallery7: "Detail panel — Info tab: filename, size, date, tags (add/remove), memo; 'Move To...' moves the image to another folder, 'Delete' removes it permanently",
helpGallery8: "Detail panel — Metadata tab: displays workflow JSON from PNG embedded data (prompt / workflow keys) or from metadata saved by the Generate UI tab; one-click clipboard copy",
helpGallery9: "Detail panel — Groups tab: create, assign, remove, rename, and delete groups per image",
helpGallery10: "Workflow auto-save: images generated in the Generate UI tab automatically have their workflow saved to gallery metadata and shown in the Metadata tab",
helpGallery11: "Supports PNG (tEXt/iTXt), JPEG, WebP, GIF; workflow extraction reads ComfyUI prompt and workflow keys embedded in PNG metadata",
helpSidepanelTitle: "Workflow Studio Library (ComfyUI Side Panel)",
helpSidepanel1: "Open from the Workflow Studio Library button in the ComfyUI toolbar",
helpSidepanel2: "Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view",
@@ -405,6 +467,17 @@ const LANGUAGES = {
helpSidepanel10: "Search within each sub-tab to quickly find items",
helpSidepanel11: "Models tab: All / ★ Favorites / Groups / By Type sub-tabs",
helpSidepanel12: "⚙ Theme button (header): customize panel background, sub-header background, text, border, and secondary text colors — changes are saved to localStorage and applied on next open",
helpSidepanel13: "I tab (Information): drop a ComfyUI PNG/WebP/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF and QuadrupleCLIPLoader node types",
helpSidepanel14: "Information tab — drag items to canvas: model sub-tab places the loader node (Checkpoint → CheckpointLoaderSimple, VAE → VAELoader, Diffusion Model → UNETLoader, Text Encoder → CLIPLoader); double-click also places at canvas center",
helpSidepanel15: "Information tab — LoRA sub-tab: drag individual LoRA to place LoraLoader; \"Multiple LORA\" section at bottom places all LoRAs in a single Lora Loader (LoraManager) node; Prompts sub-tab: drag a prompt to place CLIPTextEncode node with text pre-filled; click prompt to view full text and copy",
helpSidepanel16: "A tab (AI Tools): Translation and VLM sub-tabs powered by Ollama or LM Studio; settings (backend, URL, model) are shared with the SPA AI tab via localStorage",
helpAiTitle: "AI Tab (A)",
helpAi1: "Translation sub-tab: translate text between Japanese, English, Chinese, or a custom Free language using Ollama or LM Studio",
helpAi2: "Language selectors for source and destination with ⇄ swap button; text content is also swapped; selections are saved automatically",
helpAi3: "TOOLS pane: drop an image into the drop zone and select a task — Describe Image, Create Prompt, or Create Tags — then click Run to analyze with a vision model",
helpAi4: "VLM result is displayed in the output area with a Copy button",
helpAi5: "Settings sub-tab: choose backend (Ollama / LM Studio), set the API URL, test connection, select model, and configure Free language names for translation",
helpAi6: "Settings are saved to localStorage and shared with the Library panel's A tab",
helpShortcutsTitle: "Keyboard Shortcuts",
helpShortcuts1: "Escape: Close modal/panel",
helpShortcuts2: "Enter: Confirm rename, send chat",
@@ -440,7 +513,7 @@ const LANGUAGES = {
comfyuiUrlHint: "When running as ComfyUI plugin, leave empty to use same origin. Only set if ComfyUI is on a different server.",
connectedCheck: "Connected \u2713",
failedConnect: "Failed to connect \u2717",
ollamaSettings: "Ollama Settings",
ollamaSettings: "Ollama Settings (Prompt Tab)",
ollamaUrl: "Ollama API URL",
ollamaDefaultModel: "Default Model",
selectModel: "Select model...",
@@ -525,6 +598,37 @@ const LANGUAGES = {
themeFontTrainOne: "Train One",
themeFontSpaceMono: "Space Mono",
themeFontFiraCode: "Fira Code",
// -- Metadata tab --
tabMetadata: "Metadata",
metaSectionCkpt: "Checkpoint",
metaSectionVae: "VAE",
metaSectionDiff: "Diffusion Model",
metaSectionTe: "Text Encoder",
metaSectionLora: "LoRA",
metaSectionPrompt: "Prompt",
metaDropMain: "Drop image / JSON",
metaDropSub: "PNG · WebP · JSON",
metaParsing: "Parsing...",
metaFileTooLarge: "File too large (max 50 MB)",
metaNoMetadata: "No metadata found",
metaParseError: "Failed to parse metadata",
metaPromptPositive: "Positive",
metaPromptNegative: "Negative",
metaPromptText: "Text",
metaSelectPrompt: "Select a prompt above to view full text...",
metaFormatNoteTitle: "Supported formats",
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
metaFmtSdwebui: "SD WebUI / SD Forge — PNG (parameters)",
metaFmtFooocus: "Fooocus — PNG (fooocus_scheme)",
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image subgraph workflows supported",
helpMetadataTitle: "Metadata Tab",
helpMetadataDesc: "Drop a ComfyUI-generated PNG/WebP or workflow JSON to instantly extract and view the embedded model and prompt information.",
helpMetadata1: "Left column: drop zone (image preview for PNG/WebP) and file info; click or drop to load",
helpMetadata2: "Center column: Checkpoint, VAE, Diffusion Model, and Text Encoder lists extracted from the workflow",
helpMetadata3: "Right column: LoRA list (with strength values) and Prompt list (click to view full text below)",
helpMetadata4: "Prompt actions — Copy: copy to clipboard; GenUI:P/N: set GenerateUI positive/negative prompt; Prompt:P/N: set Prompt tab preset positive/negative",
helpMetadata5: "Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image subgraph workflows), SD WebUI, SD Forge, Fooocus.",
},
ja: {
@@ -847,6 +951,52 @@ const LANGUAGES = {
galleryOutputDirLabel: "Outputフォルダのパス",
galleryOutputDirHint: "ギャラリータブで参照するComfyUIのoutputフォルダを設定します。空欄の場合はComfyUIのデフォルトoutputディレクトリを使用します。",
tabAi: "AI TOOL",
// -- AI Tab --
aiSubTabTranslate: "翻訳",
aiSubTabVlm: "VLM",
aiSubTabSettings: "設定",
aiLangJa: "日本語", aiLangEn: "英語", aiLangZh: "中国語", aiLangFree: "Free",
aiTransInputPlaceholder: "翻訳するテキストを入力...",
aiTransOutputPlaceholder: "翻訳結果がここに表示されます...",
aiTranslateBtn: "翻訳",
aiCopyBtn: "コピー",
aiVlmDropLabel: "PNG / JPG / WebP をドロップまたはクリック",
aiVlmDescribe: "画像の解説",
aiVlmPromptCreate: "プロンプト作成",
aiVlmRunBtn: "実行",
aiVlmResultPlaceholder: "実行結果がここに表示されます...",
aiSettingsBackend: "バックエンド",
aiSettingsConnection: "接続設定",
aiSettingsTestBtn: "接続テスト",
aiSettingsModelSection: "モデル選択",
aiSettingsRefreshBtn: "更新",
aiSettingsModelPlaceholder: "-- モデルを選択 --",
aiSettingsFreeLang: "Free 言語設定",
aiSettingsInputLang: "入力言語",
aiSettingsOutputLang: "翻訳言語",
aiSettingsSaveBtn: "設定を保存",
aiToastNoText: "テキストを入力してください",
aiToastNoModel: "設定タブでモデルを選択してください",
aiToastInvalidUrl: "URLは http:// または https:// で始まる必要があります",
aiToastInvalidUrlInput: "http:// または https:// で始まるURLを入力してください",
aiStatusTranslating: "翻訳中...",
aiStatusRunning: "実行中...",
aiStatusDone: "完了",
aiStatusConnecting: "接続中...",
aiStatusConnectOk: "接続成功",
aiStatusConnectFail: "接続失敗: ",
aiToastTransFailed: "翻訳に失敗しました: ",
aiToastNoCopyText: "コピーするテキストがありません",
aiToastCopied: "コピーしました",
aiToastNoModels: "モデルが見つかりません",
aiToastModelsFailed: "モデル取得失敗: ",
aiToastSettingsSaved: "設定を保存しました",
aiToastNoImage: "画像をドロップしてください",
aiToastVlmFailed: "VLM実行失敗: ",
aiModels: "モデル",
// -- Help & Support --
tabHelp: "ヘルプ",
helpManualTitle: "機能一覧",
@@ -863,14 +1013,26 @@ const LANGUAGES = {
helpWf7: "バッジ管理:バッジの追加・名前変更・削除、カスタム色設定(Modelsタブと共有)— ⚙ Badgeボタン",
helpGen1: "ComfyUIの接続・切断、接続状態の確認",
helpGen2: "Workflowタブからワークフローを読み込み(App形式 .app.json は非対応)",
helpGen3: "3タブ構成 — Input(プロンプト+画像)、Model、Settings — 各タブにRaw JSON列を常時表示",
helpGen3: "4タブ構成 — Input(プロンプト+画像)、Model、Settings、Feeder — Input/Model/SettingsタブにはRaw JSON列を常時表示",
helpGen4: "Inputタブ:左列にPrompt(上段)+ Image ドラッグ&ドロップ(下段)",
helpGen5: "Modelタブ:Checkpoint/VAE/LoRA/ControlNet/UNET/TextEncoderのセレクタ(フィルタ検索付き)",
helpGen6: "Settingsタブ:左列にKSampler(上段)+ LatentImage(下段)",
helpGen6: "Settingsタブ:KSamplerとLatent Imageを横並び(各50%幅)",
helpGen7: "Raw JSONを各タブの右列に常時表示 — 直接編集してApplyでワークフロー即時反映",
helpGen8: "シード管理:ランダム / 固定 / 増加 / 減少",
helpGen9: "画像生成、進捗表示、生成停止、結果表示",
helpGen10: "Eagle連携:生成画像をEagleへ自動保存",
helpGen11: "Checkpoint Batch:ドロップダウンのフォルダツリーからチェックポイントを選択 — フォルダにチェックでそのフォルダ内を一括選択、▶で展開して個別選択も可能;Filterで絞り込み;Pause/Resumeでモデル間の処理を一時停止・再開;Stopで現在の生成完了後に中断",
helpFeederTitle: "GenerateUI Tab — Feeder サブタブ",
helpFeederDesc: "comfyui-image-feeder カスタムノードが必要です。ImageFeeder ノードを操作し、user/default/image-loop-data/ に保存された画像ライブラリを使って連続生成ループを実行します。",
helpFeeder1: "左ペイン — ノード設定:ドロップダウンで対象 ImageFeeder ノードを選択;Directory・Sort Mode・Index・Start/End Index・Batch Size・Seed・Use Selection の各パラメータを編集;Apply で読み込み済みワークフローに反映",
helpFeeder2: "左ペイン — プリセット:現在のフォルダ+選択ファイルを名前付きプリセットとして保存;保存済みプリセットの読み込み・削除",
helpFeeder3: "左ペイン — Run:After gen モードで各生成後の index 動作を設定 — Loop(index を 0 に折り返して無限継続)、Increment(index を進め全画像消化で自動停止)、Fixed(常に同じ index を使用);Run ボタンでループ開始、Stop で中断",
helpFeeder4: "中央ペイン — フォルダツリー:image-loop-data 内のサブフォルダを階層表示;クリックで画像を読み込み;↺ ボタンでツリー再読み込み",
helpFeeder5: "中央ペイン — 画像グリッド:選択フォルダ内の画像をサムネイル表示;チェックボックスで選択に追加;All/None ボタンで一括選択・解除;クリックでプレビューパネルに表示",
helpFeeder6: "中央ペイン — プレビューパネル:クリックした画像を拡大表示し、ファイル名・解像度・ファイルサイズを確認",
helpFeeder7: "ステータスバー:現在のフォルダパスと、そのフォルダ内の総枚数に対する選択枚数を表示",
helpFeeder8: "Run 時のシード:右ペインのシード設定(ランダム / 固定 / 増加 / 減少)を使用 — ImageFeeder 自身の Seed フィールドはランダムソートの並び順のみに影響",
helpFeeder9: "Index 同期:生成完了後にノードが次の index を WebSocket で返送;Index フィールドが自動更新されて表示と実行状態が一致",
helpPrompt1: "AIアシスタント(Ollama):モデル選択、チャットでプロンプト相談、画像添付(ビジョンモデル対応)",
helpPrompt2: "AIの応答をGenerateUIのプロンプトに適用",
helpPrompt3: "翻訳機能:日⇔英、中⇔英",
@@ -878,6 +1040,8 @@ const LANGUAGES = {
helpPrompt5: "プリセットマネージャー:全プリセット一覧、お気に入り、グループ別フィルタリング",
helpPrompt6: "グループ管理:グループ作成、プリセットの割り当て・解除、グループ削除",
helpPrompt7: "GenUI Set:プリセットのプロンプトをGenerateUIに直接適用",
helpPrompt8: "ワイルドカードツールバー:{|}・{n$|}・__|__・<lora:>などの構文をワンクリック挿入。選択テキストのラップも対応",
helpPrompt9: "ワイルドカードファイルマネージャー:.txt/.yamlファイルの作成・編集・削除。ファイル名をクリックすると__ファイル名__をカーソル位置に挿入",
helpSettings1: "UI言語と要約言語の設定",
helpSettings2: "テーマ選択(13種プリセット)+カスタマイズ:カラー調整、背景パターン(ストライプ、ポルカドット、チェック、SVGタイリング)、フォント選択(日本語デザインフォント含む16種のGoogle Fonts)",
helpSettings3: "ワークフローデータフォルダのパス変更・リセット",
@@ -887,6 +1051,7 @@ const LANGUAGES = {
helpSettings7: "Eagle連携URL、自動保存の有効/無効",
helpSettings8: "データ管理:設定・メタデータ・プロンプトなど全データをJSONにエクスポート、またはインポートして復元。インポート後はComfyUIを再起動してください。",
helpSettings9: "各セクションはアコーディオン式で折りたたみ可能",
helpSettings10: "Wildcard Integration:WFSのワイルドカードディレクトリをComfyUI-Impact-Packのwildcardsディレクトリへリンク(Windowsはジャンクション、他OSはシンボリックリンク)。既存ファイルは自動で移行されます",
helpNodes1: "ノードブラウザ:名前/説明/エイリアス/タグで検索、カテゴリ・パッケージ・タグ・グループ・お気に入りフィルタ",
helpNodes2: "カード / テーブル表示モード切り替え(ページネーション付き)",
helpNodes3: "ノードのお気に入り登録、カスタムタグの追加で整理",
@@ -897,7 +1062,7 @@ const LANGUAGES = {
helpModels2: "サムネイル / カード / テーブル表示モード切り替え(ページネーション付き)",
helpModels3: "検索、タグフィルタ、グループフィルタ、お気に入りフィルタ、ステータスフィルタ(すべて / 有効のみ / 無効のみ)",
helpModels4: "サイドパネル:基本情報(ファイルパス、タグ、メモ)、グループ管理、CivitAI連携",
helpModels5: "CivitAI連携:SHA256ハッシュでモデル情報取得、モデルタイプごとの一括取得",
helpModels5: "CivitAI連携:SHA256ハッシュでモデル情報取得、モデルタイプごとの一括取得;プレビュー画像がない場合は自動でダウンロード・保存",
helpModels6: "詳細モーダル:プレビュー画像、CivitAI情報(ベースモデル、トリガーワード、タグ)、サムネイル変更",
helpModels7: "グループ管理:グループの作成・名前変更・削除、モデルの割り当て・解除",
helpModels8: "GenUI Modelボタン:選択モデルをGenerateUIの現在のワークフローの該当ノードに直接適用(Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)",
@@ -909,12 +1074,13 @@ const LANGUAGES = {
helpGallery2: "3カラムレイアウト: フォルダツリー (左) | 画像グリッド (中央) | 詳細パネル (右)",
helpGallery3: "サムネイル・テーブル表示モード切替;ダブルクリックで全画面ライトボックス表示",
helpGallery4: "ツールバーフィルタ:お気に入り(★)、テキスト検索、タグフィルタ、グループフィルタ、日付・名前ソート",
helpGallery5: "Ctrl+クリックで複数選択 — 一括操作:グループへ追加、一括お気に入り、一括お気に入り解除",
helpGallery6: "詳細パネル — Infoタブ:ファイル名、サイズ、日付、タグ(追加/削除)、メモ(保存機能付き)",
helpGallery7: "詳細パネル — Metadataタブ:埋め込みComfyUIワークフローJSONビューアとワンクリッククリップボードコピー",
helpGallery8: "詳細パネル — Groupsタブ:画像ごとのグループ作成、割り当て、解除、名前変更、削除",
helpGallery9: "PNG (tEXt/iTXt メタデータ), JPEG, WebP, GIF対応; PNGはComfyUIの埋め込みJSONを読み込んでワークフロー抽出可能",
helpGallery10: "フォルダツリー:フォルダをクリックして画像を読み込む;▶矢印でサブフォルダを展開",
helpGallery5: "フォルダツリー:クリックで画像を読み込み;▶で展開;「+ New」で選択フォルダ内にサブフォルダを作成;「Del」でそのフォルダとすべての内容を削除",
helpGallery6: "Ctrl+クリックで複数選択 — 一括操作:グループへ追加、一括お気に入り、一括お気に入り解除、移動(Move To...)、削除",
helpGallery7: "詳細パネル — Infoタブ:ファイル名、サイズ、日付、タグ(追加/削除)、メモ;「Move To...」で他フォルダへ移動;「Delete」で完全削除",
helpGallery8: "詳細パネル — Metadataタブ:PNGの埋め込みデータ(prompt / workflow キー)またはGenerateUIタブで保存したメタデータからワークフローJSONを表示;ワンクリックでクリップボードコピー",
helpGallery9: "詳細パネル — Groupsタブ:画像ごとのグループ作成、割り当て、解除、名前変更、削除",
helpGallery10: "ワークフロー自動保存:GenerateUIタブで生成した画像はワークフローが自動でギャラリーメタデータに保存され、Metadataタブで確認可能",
helpGallery11: "PNG (tEXt/iTXt)、JPEG、WebP、GIF対応;PNG形式はComfyUIの埋め込みJSON(prompt / workflow キー)を読み込んでワークフロー抽出が可能",
helpSidepanelTitle: "Workflow Studio Library(ComfyUIサイドパネル)",
helpSidepanel1: "ComfyUIツールバーのWorkflow Studio Libraryボタンから開く",
helpSidepanel2: "Workflowsタブ:All / ★ Favorites / Groupsサブタブ、Allビューでお気に入りに★表示",
@@ -928,6 +1094,17 @@ const LANGUAGES = {
helpSidepanel10: "各サブタブ内で検索してアイテムをすばやく検索",
helpSidepanel11: "Modelsタブ:All / ★ Favorites / Groups / By Typeサブタブ",
helpSidepanel12: "⚙ テーマボタン(ヘッダー):パネル背景・サブヘッダー背景・文字色・ボーダー・補助テキスト色をカスタマイズ可能、設定はlocalStorageに保存され次回起動時も反映",
helpSidepanel13: "I タブ(Information):ComfyUI の PNG / WebP / JSON をドロップするとサイドパネル内でモデル・LoRA・プロンプトのメタデータを確認可能 — サブタブ: model / lora / Prompts;UnetLoaderGGUF・QuadrupleCLIPLoader ノードタイプに対応",
helpSidepanel14: "Information タブ — キャンバスへドラッグ:model サブタブのアイテムをドラッグすると対応ローダーノードを配置(Checkpoint → CheckpointLoaderSimple、VAE → VAELoader、Diffusion Model → UNETLoader、Text Encoder → CLIPLoader);ダブルクリックでもキャンバス中央に配置可能",
helpSidepanel15: "Information タブ — lora サブタブ:個別 LoRA をドラッグで LoraLoader を配置;下部の「Multiple LORA」セクションをドラッグすると全 LoRA を一括で Lora Loader (LoraManager) に配置;Prompts サブタブ:プロンプトをドラッグするとテキスト入力済みの CLIPTextEncode ノードを配置;クリックで全文表示・コピー可能",
helpSidepanel16: "A タブ(AI ツール):翻訳・VLM サブタブ;設定(バックエンド・URL・モデル)は SPA の AI タブと localStorage で共有される",
helpAiTitle: "AI タブ(A)",
helpAi1: "翻訳サブタブ:Ollama または LM Studio を使って日本語・英語・中国語・カスタム言語(Free)間でテキストを翻訳",
helpAi2: "入力言語・出力言語セレクターと ⇄ 入替ボタン付き;テキスト内容も同時に入替;選択は自動保存",
helpAi3: "TOOLS ペイン:画像をドロップゾーンにドロップしてタスクを選択(画像の解説 / プロンプト作成 / タグ作成)し、実行ボタンでビジョンモデルに解析させる",
helpAi4: "VLM の結果は出力エリアに表示され、コピーボタンでクリップボードに取得可能",
helpAi5: "設定サブタブ:バックエンド(Ollama / LM Studio)・API URL・接続テスト・モデル選択・翻訳の Free 言語名を設定",
helpAi6: "設定は localStorage に保存され、Library パネルの A タブと共有される",
helpShortcutsTitle: "キーボード操作",
helpShortcuts1: "Escape:モーダル・パネルを閉じる",
helpShortcuts2: "Enter:名前変更の確定、チャット送信",
@@ -963,7 +1140,7 @@ const LANGUAGES = {
comfyuiUrlHint: "ComfyUIプラグインとして動作する場合は空欄(同一オリジン推奨)。別サーバーの場合のみ設定してください。",
connectedCheck: "接続成功 \u2713",
failedConnect: "接続失敗 \u2717",
ollamaSettings: "Ollama 設定",
ollamaSettings: "Ollama 設定(プロンプトタブ)",
ollamaUrl: "Ollama API URL",
ollamaDefaultModel: "デフォルトモデル",
selectModel: "モデルを選択...",
@@ -1048,6 +1225,37 @@ const LANGUAGES = {
themeFontTrainOne: "トレイン One",
themeFontSpaceMono: "Space Mono",
themeFontFiraCode: "Fira Code",
// -- Metadata tab --
tabMetadata: "メタデータ",
metaSectionCkpt: "Checkpoint",
metaSectionVae: "VAE",
metaSectionDiff: "Diffusion Model",
metaSectionTe: "Text Encoder",
metaSectionLora: "LoRA",
metaSectionPrompt: "プロンプト",
metaDropMain: "画像 / JSON をドロップ",
metaDropSub: "PNG · WebP · JSON",
metaParsing: "解析中...",
metaFileTooLarge: "ファイルが大きすぎます(最大 50 MB)",
metaNoMetadata: "メタデータが見つかりません",
metaParseError: "メタデータの解析に失敗しました",
metaPromptPositive: "ポジティブ",
metaPromptNegative: "ネガティブ",
metaPromptText: "テキスト",
metaSelectPrompt: "上のプロンプトを選択すると全文を表示します...",
metaFormatNoteTitle: "対応フォーマット",
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
metaFmtSdwebui: "SD WebUI / SD Forge — PNG(parameters チャンク)",
metaFmtFooocus: "Fooocus — PNG(fooocus_scheme チャンク)",
metaFormatTodo: "Flux.2 / Qwen-Image / Z-Image サブグラフ形式に対応済み",
helpMetadataTitle: "Metadata タブ",
helpMetadataDesc: "ComfyUI で生成した PNG / WebP または ワークフロー JSON をドロップすると、埋め込まれたモデル・プロンプト情報を即座に抽出・表示します。",
helpMetadata1: "左列: ドロップゾーン(PNG/WebP は画像プレビュー表示)+ファイル情報・対応フォーマット説明",
helpMetadata2: "中列: ワークフローから抽出した Checkpoint・VAE・Diffusion Model・Text Encoder の一覧",
helpMetadata3: "右列: LoRA 一覧(強度値付き)+プロンプト一覧(クリックで下部に全文表示)",
helpMetadata4: "プロンプトアクション — Copy: クリップボードにコピー; GenUI:P/N: GenerateUI のポジティブ/ネガティブプロンプトにセット; Prompt:P/N: Prompt タブ Presets のポジティブ/ネガティブにセット",
helpMetadata5: "対応フォーマット: ComfyUI PNG/WebP/JSON(標準形式 + Flux.2 / Qwen-Image / Z-Image サブグラフ形式)・SD WebUI・SD Forge・Fooocus。",
},
zh: {
@@ -1366,6 +1574,52 @@ const LANGUAGES = {
galleryOutputDirLabel: "输出文件夹路径",
galleryOutputDirHint: "设置图库标签页浏览的ComfyUI输出文件夹。留空则使用ComfyUI默认输出目录。",
tabAi: "AI TOOL",
// -- AI Tab --
aiSubTabTranslate: "翻译",
aiSubTabVlm: "VLM",
aiSubTabSettings: "设置",
aiLangJa: "日语", aiLangEn: "英语", aiLangZh: "中文", aiLangFree: "Free",
aiTransInputPlaceholder: "输入要翻译的文字...",
aiTransOutputPlaceholder: "翻译结果将显示在这里...",
aiTranslateBtn: "翻译",
aiCopyBtn: "复制",
aiVlmDropLabel: "拖放 PNG / JPG / WebP 或点击",
aiVlmDescribe: "图像说明",
aiVlmPromptCreate: "生成提示词",
aiVlmRunBtn: "执行",
aiVlmResultPlaceholder: "执行结果将显示在这里...",
aiSettingsBackend: "后端",
aiSettingsConnection: "连接设置",
aiSettingsTestBtn: "连接测试",
aiSettingsModelSection: "选择模型",
aiSettingsRefreshBtn: "刷新",
aiSettingsModelPlaceholder: "-- 请选择模型 --",
aiSettingsFreeLang: "Free 语言设置",
aiSettingsInputLang: "输入语言",
aiSettingsOutputLang: "翻译语言",
aiSettingsSaveBtn: "保存设置",
aiToastNoText: "请输入文字",
aiToastNoModel: "请在设置标签中选择模型",
aiToastInvalidUrl: "URL必须以 http:// 或 https:// 开头",
aiToastInvalidUrlInput: "请输入以 http:// 或 https:// 开头的URL",
aiStatusTranslating: "翻译中...",
aiStatusRunning: "执行中...",
aiStatusDone: "完成",
aiStatusConnecting: "连接中...",
aiStatusConnectOk: "连接成功",
aiStatusConnectFail: "连接失败:",
aiToastTransFailed: "翻译失败:",
aiToastNoCopyText: "没有可复制的文字",
aiToastCopied: "已复制",
aiToastNoModels: "未找到模型",
aiToastModelsFailed: "获取模型失败:",
aiToastSettingsSaved: "设置已保存",
aiToastNoImage: "请拖放图像",
aiToastVlmFailed: "VLM执行失败:",
aiModels: "个模型",
// -- Help & Support --
tabHelp: "帮助",
helpManualTitle: "功能列表",
@@ -1382,14 +1636,26 @@ const LANGUAGES = {
helpWf7: "徽章管理:添加、重命名、删除徽章并自定义颜色(与Models标签共享)— ⚙ Badge按钮",
helpGen1: "连接/断开ComfyUI,检查连接状态",
helpGen2: "从工作流标签页加载工作流(不支持App格式.app.json)",
helpGen3: "3标签布局 — Input(提示词+图片)、Model、Settings — 每个标签含Raw JSON列实时预览",
helpGen3: "4标签布局 — Input(提示词+图片)、Model、Settings、Feeder — Input/Model/Settings标签各含Raw JSON列实时预览",
helpGen4: "Input标签:左列为Prompt(上)+ Image拖放(下)",
helpGen5: "Model标签:Checkpoint/VAE/LoRA/ControlNet/UNET/TextEncoder选择器(含筛选)",
helpGen6: "Settings标签:左列为KSampler(上)+ LatentImage(下)",
helpGen6: "Settings标签:KSampler与Latent Image并排显示(各占50%宽度)",
helpGen7: "Raw JSON在每个标签右列常驻显示 — 直接编辑并Apply即时重载工作流",
helpGen8: "种子管理:随机/固定/递增/递减",
helpGen9: "生成图片、进度显示、停止生成、查看结果",
helpGen10: "Eagle集成:自动保存生成图片到Eagle",
helpGen11: "Checkpoint Batch:从下拉框文件夹树中选择检查点 — 勾选文件夹可一键选中该文件夹内的所有文件,▶展开后也可单独选择;使用Filter筛选;Pause/Resume可在模型之间暂停和继续;Stop在当前生成完成后中止",
helpFeederTitle: "GenerateUI Tab — Feeder 子标签",
helpFeederDesc: "需要 comfyui-image-feeder 自定义节点。用于操控 ImageFeeder 节点,并对存储在 user/default/image-loop-data/ 中的图片库执行连续生成循环。",
helpFeeder1: "左侧面板 — 节点设置:从下拉框选择目标 ImageFeeder 节点;编辑所有节点参数(Directory、Sort Mode、Index、Start/End Index、Batch Size、Seed、Use Selection);点击 Apply 将更改写入当前工作流",
helpFeeder2: "左侧面板 — 预设:将当前目录+选择文件保存为命名预设;加载或删除已保存的预设",
helpFeeder3: "左侧面板 — Run:After gen 模式决定每次生成后的 index 行为 — Loop(回到0并无限循环)、Increment(递增 index,全部图片用完后自动停止)、Fixed(始终使用相同 index);点击 Run 开始循环,Stop 中断",
helpFeeder4: "中央面板 — 文件夹树:浏览 image-loop-data 内的子文件夹;点击文件夹加载图片;点击 ↺ 重新加载目录树",
helpFeeder5: "中央面板 — 图片网格:缩略图显示所选文件夹内的所有图片;勾选图片加入选择集;All/None 按钮一键全选或清除;点击图片打开预览面板",
helpFeeder6: "中央面板 — 预览面板:放大显示点击的图片,并显示文件名、分辨率和文件大小",
helpFeeder7: "状态栏:显示当前文件夹路径,以及该文件夹内已选中图片数占总数的比例",
helpFeeder8: "Run 时的种子:使用右侧面板的种子设置(随机/固定/递增/递减)— ImageFeeder 自身的 Seed 字段仅影响随机排序的顺序",
helpFeeder9: "Index 同步:每次生成完成后节点通过 WebSocket 返回下一个 index;Index 字段自动更新,保持显示与执行状态一致",
helpPrompt1: "AI助手(Ollama):模型选择、聊天咨询提示词、图片附件(视觉模型)",
helpPrompt2: "将AI回复应用到生成UI的提示词",
helpPrompt3: "翻译功能:日\u2194英、中\u2194英",
@@ -1397,6 +1663,8 @@ const LANGUAGES = {
helpPrompt5: "预设管理器:浏览所有预设、收藏、按组筛选",
helpPrompt6: "组管理:创建组、分配/移除预设、删除组",
helpPrompt7: "GenUI Set:将预设提示词直接应用到生成UI",
helpPrompt8: "通配符工具栏:一键插入通配符语法({|}、{n$|}、__|__、<lora:>等);支持包裹选中文本",
helpPrompt9: "通配符文件管理:创建/编辑/删除.txt/.yaml通配符文件;点击文件名在光标处插入__文件名__",
helpSettings1: "UI语言和摘要语言设置",
helpSettings2: "主题选择(13种预设)+ 自定义:颜色调整、背景图案(条纹、波点、棋盘格、SVG平铺)、字体选择(含日文显示字体在内的16种Google Fonts)",
helpSettings3: "工作流数据文件夹路径更改/重置",
@@ -1406,6 +1674,7 @@ const LANGUAGES = {
helpSettings7: "Eagle集成URL、自动保存开关",
helpSettings8: "数据管理:将所有插件数据(设置、元数据、提示词等)导出为JSON文件,或导入以恢复。导入后请重启ComfyUI。",
helpSettings9: "各部分可折叠展开",
helpSettings10: "Wildcard Integration:将WFS通配符目录链接到ComfyUI-Impact-Pack的wildcards目录(Windows为目录联结,其他OS为符号链接);现有文件自动迁移",
helpNodes1: "节点浏览器:按名称/描述/别名/标签搜索,按分类、包、标签、组、收藏筛选",
helpNodes2: "卡片和表格视图模式切换(带分页)",
helpNodes3: "收藏节点,添加自定义标签进行整理",
@@ -1416,7 +1685,7 @@ const LANGUAGES = {
helpModels2: "缩略图、卡片和表格视图模式切换(带分页)",
helpModels3: "搜索、标签筛选、分组筛选、收藏筛选、状态筛选(全部 / 仅启用 / 仅禁用)",
helpModels4: "侧面板:基本信息(文件路径、标签、备注)、分组管理、CivitAI集成",
helpModels5: "CivitAI集成:通过SHA256哈希获取模型信息,按模型类型批量获取",
helpModels5: "CivitAI集成:通过SHA256哈希获取模型信息,按模型类型批量获取;无预览图时自动下载并保存",
helpModels6: "详情弹窗:预览图片、CivitAI信息(基础模型、触发词、标签)、缩略图更改",
helpModels7: "分组管理:创建、重命名、删除分组,分配/移除模型",
helpModels8: "GenUI Model按钮:将选中模型直接应用到GenerateUI当前工作流的对应节点(Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)",
@@ -1428,12 +1697,13 @@ const LANGUAGES = {
helpGallery2: "三列布局:文件夹树(左) | 图像网格(中) | 详情面板(右)",
helpGallery3: "缩略图和表格视图模式;双击任何图像可在全屏灯箱中打开",
helpGallery4: "工具栏过滤器:收藏 (★)、文本搜索、标签过滤、分组过滤、按日期/名称排序",
helpGallery5: "使用 Ctrl+点击 多选 — 批量操作:添加到分组、全部收藏、取消全部收藏",
helpGallery6: "详情面板 — 信息(Info)标签:文件名、大小、日期、标签(添加/移除)、包含保存功能的备注",
helpGallery7: "详情面板 — 元数据(Metadata)标签:ComfyUI 嵌入的内置工作流 JSON 查看器及一键复制功能",
helpGallery8: "详情面板 — 分组(Groups)标签:对每个图像进行组的创建、分配、移除、重命名和删除",
helpGallery9: "支持 PNG(tEXt/iTXt 元数据)、JPEG、WebP、GIF;对于 PNG 会读取 ComfyUI 内置的 JSON 提取工作流",
helpGallery10: "文件夹树:点击文件夹加载其包含的图像;使用 ▶ 箭头展开子文件夹",
helpGallery5: "文件夹树:点击加载图像,▶展开;'+ New'在选定文件夹内创建子文件夹;'Del'删除该文件夹及其所有内容",
helpGallery6: "使用Ctrl+点击多选 — 批量操作:添加到分组、全部收藏、取消全部收藏、移动到...、删除",
helpGallery7: "详情面板 — 信息(Info)标签:文件名、大小、日期、标签(添加/移除)、备注;'移动到...'将图像移至另一文件夹;'删除'永久删除",
helpGallery8: "详情面板 — 元数据(Metadata)标签:显示来自PNG嵌入数据(prompt / workflow键)或Generate UI标签保存的工作流JSON;一键复制到剪贴板",
helpGallery9: "详情面板 — 分组(Groups)标签:对每个图像进行组的创建、分配、移除、重命名和删除",
helpGallery10: "工作流自动保存:在Generate UI标签生成的图像,其工作流会自动保存到图库元数据,并在Metadata标签中显示",
helpGallery11: "支持PNG(tEXt/iTXt)、JPEG、WebP、GIF;PNG格式读取ComfyUI嵌入的JSON(prompt / workflow键)提取工作流",
helpSidepanelTitle: "Workflow Studio Library(ComfyUI侧面板)",
helpSidepanel1: "从ComfyUI工具栏的Workflow Studio Library按钮打开",
helpSidepanel2: "Workflows标签:All / ★ Favorites / Groups子标签,All视图中收藏项显示★",
@@ -1447,6 +1717,17 @@ const LANGUAGES = {
helpSidepanel10: "在各子标签内搜索快速查找项目",
helpSidepanel11: "Models标签:All / ★ Favorites / Groups / By Type子标签",
helpSidepanel12: "⚙ 主题按钮(标题栏):可自定义面板背景、子标题背景、文字颜色、边框及辅助文字颜色,设置保存至localStorage并在下次打开时生效",
helpSidepanel13: "I 标签(Information):拖放 ComfyUI PNG/WebP/JSON 即可在侧面板内查看模型、LoRA 和提示词元数据 — 子标签:model / lora / Prompts;支持 UnetLoaderGGUF 和 QuadrupleCLIPLoader 节点类型",
helpSidepanel14: "Information 标签 — 拖拽到画布:model 子标签拖拽可放置对应加载节点(Checkpoint → CheckpointLoaderSimple、VAE → VAELoader、Diffusion Model → UNETLoader、Text Encoder → CLIPLoader);双击也可放置到画布中央",
helpSidepanel15: "Information 标签 — lora 子标签:拖拽单个 LoRA 放置 LoraLoader;底部「Multiple LORA」区域可将全部 LoRA 一次性放入 Lora Loader (LoraManager) 节点;Prompts 子标签:拖拽提示词放置预填文本的 CLIPTextEncode 节点;点击可查看全文并复制",
helpSidepanel16: "A 标签(AI 工具):翻译子标签和 VLM 子标签;设置(后端、URL、模型)通过 localStorage 与 SPA 的 AI 标签共享",
helpAiTitle: "AI 标签页(A)",
helpAi1: "翻译子标签:使用 Ollama 或 LM Studio 在日语、英语、中文或自定义语言(Free)之间翻译文本",
helpAi2: "源语言和目标语言选择器,带 ⇄ 互换按钮;文本内容也同时互换;选择自动保存",
helpAi3: "TOOLS 面板:将图像拖放到投放区域,选择任务(图像描述 / 提示词生成 / 标签生成),点击执行按钮用视觉模型进行分析",
helpAi4: "VLM 结果显示在输出区域,可通过复制按钮复制到剪贴板",
helpAi5: "设置子标签:选择后端(Ollama / LM Studio)、设置 API URL、测试连接、选择模型、配置翻译的 Free 语言名称",
helpAi6: "设置保存至 localStorage,并与 Library 面板的 A 标签共享",
helpShortcutsTitle: "键盘操作",
helpShortcuts1: "Escape:关闭弹窗/面板",
helpShortcuts2: "Enter:确认重命名、发送聊天",
@@ -1482,7 +1763,7 @@ const LANGUAGES = {
comfyuiUrlHint: "作为ComfyUI插件运行时留空(推荐同源)。仅在不同服务器时设置。",
connectedCheck: "已连接 \u2713",
failedConnect: "连接失败 \u2717",
ollamaSettings: "Ollama 设置",
ollamaSettings: "Ollama 设置(提示词标签页)",
ollamaUrl: "Ollama API URL",
ollamaDefaultModel: "默认模型",
selectModel: "选择模型...",
@@ -1567,6 +1848,37 @@ const LANGUAGES = {
themeFontTrainOne: "Train One",
themeFontSpaceMono: "Space Mono",
themeFontFiraCode: "Fira Code",
// -- Metadata tab --
tabMetadata: "元数据",
metaSectionCkpt: "Checkpoint",
metaSectionVae: "VAE",
metaSectionDiff: "扩散模型",
metaSectionTe: "文本编码器",
metaSectionLora: "LoRA",
metaSectionPrompt: "提示词",
metaDropMain: "拖放图片 / JSON",
metaDropSub: "PNG · WebP · JSON",
metaParsing: "解析中...",
metaFileTooLarge: "文件过大(最大 50 MB)",
metaNoMetadata: "未找到元数据",
metaParseError: "元数据解析失败",
metaPromptPositive: "正向提示",
metaPromptNegative: "负向提示",
metaPromptText: "文本",
metaSelectPrompt: "选择上方提示词以查看全文...",
metaFormatNoteTitle: "支持格式",
metaFmtComfyui: "ComfyUI — PNG / WebP / JSON",
metaFmtSdwebui: "SD WebUI / SD Forge — PNG(parameters)",
metaFmtFooocus: "Fooocus — PNG(fooocus_scheme)",
metaFormatTodo: "已支持 Flux.2 / Qwen-Image / Z-Image 子图工作流",
helpMetadataTitle: "Metadata 标签页",
helpMetadataDesc: "拖放 ComfyUI 生成的 PNG/WebP 或工作流 JSON,即可提取并显示其中的模型和提示词信息。",
helpMetadata1: "左列:拖放区(PNG/WebP 显示图片预览)及文件信息与支持格式说明",
helpMetadata2: "中列:从工作流中提取的 Checkpoint、VAE、扩散模型、文本编码器列表",
helpMetadata3: "右列:LoRA 列表(含强度值)和提示词列表(点击在下方显示全文)",
helpMetadata4: "提示词操作 — Copy:复制到剪贴板;GenUI:P/N:设置 GenerateUI 正/负向提示;Prompt:P/N:设置 Prompt 标签页预设正/负向提示",
helpMetadata5: "支持格式:ComfyUI PNG/WebP/JSON(标准格式 + Flux.2 / Qwen-Image / Z-Image 子图工作流)、SD WebUI、SD Forge、Fooocus。",
},
};
+752
View File
@@ -0,0 +1,752 @@
/**
* Metadata Tab
* Drop a PNG/WebP/JSON file to extract and display model/prompt metadata.
* Parsing logic adapted from model-and-prompt-from-metadata (workflow_utils.js).
*/
import { t } from "./i18n.js";
// ── File size limit ───────────────────────────────────────────
const MAX_FILE_SIZE = 50 * 1024 * 1024;
// ── Sanitize JSON (NaN/Infinity → null) ──────────────────────
function sanitizeJSON(text) {
return text
.replace(/-Infinity\b/g, "null")
.replace(/\bInfinity\b/g, "null")
.replace(/\bNaN\b/g, "null");
}
// ── WebP EXIF ────────────────────────────────────────────────
async function readWebPEXIFChunk(file) {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
const view = new DataView(buffer);
const ascii = new TextDecoder("latin1");
if (bytes.byteLength < 12) return null;
if (ascii.decode(bytes.slice(0, 4)) !== "RIFF") return null;
if (ascii.decode(bytes.slice(8, 12)) !== "WEBP") return null;
let offset = 12;
while (offset + 8 <= buffer.byteLength) {
const fourcc = ascii.decode(bytes.slice(offset, offset + 4));
const chunkSize = view.getUint32(offset + 4, true);
if (fourcc === "EXIF") return bytes.slice(offset + 8, offset + 8 + chunkSize);
offset += 8 + chunkSize;
if (chunkSize % 2 === 1) offset++;
}
return null;
}
function extractWorkflowFromEXIF(exifBytes) {
const utf8 = new TextDecoder("utf-8", { fatal: false });
const text = utf8.decode(exifBytes);
for (const key of ["workflow:", "prompt:"]) {
const idx = text.indexOf(key + "{");
if (idx < 0) continue;
let jsonStr = text.slice(idx + key.length);
const nullIdx = jsonStr.indexOf("\x00");
if (nullIdx >= 0) jsonStr = jsonStr.slice(0, nullIdx);
try { return JSON.parse(sanitizeJSON(jsonStr)); } catch {
const lb = jsonStr.lastIndexOf("}");
if (lb > 0) { try { return JSON.parse(sanitizeJSON(jsonStr.slice(0, lb + 1))); } catch {} }
}
}
return null;
}
// ── PNG text chunks ───────────────────────────────────────────
function findNull(arr, start = 0) {
for (let i = start; i < arr.length; i++) if (arr[i] === 0) return i;
return -1;
}
function parseTEXtChunk(data, latin1) {
const np = findNull(data);
if (np === -1) return null;
return { keyword: latin1.decode(data.slice(0, np)), text: latin1.decode(data.slice(np + 1)) };
}
function parseITXtChunk(data, latin1, utf8) {
const np = findNull(data);
if (np === -1) return null;
const keyword = latin1.decode(data.slice(0, np));
let pos = np + 3;
pos = findNull(data, pos); if (pos === -1) return null; pos++;
pos = findNull(data, pos); if (pos === -1) return null; pos++;
return { keyword, text: utf8.decode(data.slice(pos)) };
}
async function readAllPNGTextChunks(file) {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
const PNG_SIG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
for (let i = 0; i < 8; i++) if (bytes[i] !== PNG_SIG[i]) return null;
const view = new DataView(buffer);
const latin1 = new TextDecoder("latin1");
const utf8 = new TextDecoder("utf-8");
let offset = 8;
const chunks = {};
while (offset + 12 <= buffer.byteLength) {
const length = view.getUint32(offset);
if (offset + 12 + length > buffer.byteLength) break;
const type = latin1.decode(bytes.slice(offset + 4, offset + 8));
const data = bytes.slice(offset + 8, offset + 8 + length);
if (type === "tEXt") { const c = parseTEXtChunk(data, latin1); if (c) chunks[c.keyword] = c.text; }
else if (type === "iTXt") { const c = parseITXtChunk(data, latin1, utf8); if (c) chunks[c.keyword] = c.text; }
offset += 12 + length;
}
return chunks;
}
// ── Workflow extraction helpers ───────────────────────────────
function collectUnique(arr) {
const seen = new Set(), out = [];
for (const v of arr) { if (v && typeof v === "string" && !seen.has(v)) { seen.add(v); out.push(v); } }
return out;
}
function collectAllNodes(workflow) {
if (!Array.isArray(workflow.nodes)) return [];
const all = [...workflow.nodes];
for (const sg of workflow.definitions?.subgraphs ?? []) if (Array.isArray(sg.nodes)) all.push(...sg.nodes);
return all;
}
const META_NODE_TYPES = new Set(["ImageMetadataCheckpointLoader", "ImageMetadataPromptLoader"]);
const VAE_NONE = "None";
function extractCheckpoints(wf) {
if (!wf || typeof wf !== "object") return [];
if (Array.isArray(wf.nodes)) return collectUnique(collectAllNodes(wf).filter(n => n.type?.toLowerCase().includes("checkpoint") || META_NODE_TYPES.has(n.type)).map(n => n.widgets_values?.[0]));
return collectUnique(Object.values(wf).filter(n => n?.class_type?.toLowerCase().includes("checkpoint") || META_NODE_TYPES.has(n?.class_type)).map(n => n.inputs?.ckpt_name));
}
function extractVAEs(wf) {
if (!wf || typeof wf !== "object") return [];
if (Array.isArray(wf.nodes)) return collectUnique(collectAllNodes(wf).flatMap(n => { if (n.type === "VAELoader") return [n.widgets_values?.[0]]; if (META_NODE_TYPES.has(n.type ?? "")) { const v = n.widgets_values?.[1]; return v && v !== VAE_NONE ? [v] : []; } return []; }));
return collectUnique(Object.values(wf).flatMap(n => { if (!n || typeof n !== "object") return []; if (n.class_type === "VAELoader") return [n.inputs?.vae_name]; if (META_NODE_TYPES.has(n.class_type ?? "")) { const v = n.inputs?.vae_name; return v && v !== VAE_NONE ? [v] : []; } return []; }));
}
function extractDiffusionModels(wf) {
if (!wf || typeof wf !== "object") return [];
const UNET_TYPES = new Set(["UNETLoader", "UnetLoaderGGUF", "UNETLoaderGGUF"]);
if (Array.isArray(wf.nodes)) return collectUnique(collectAllNodes(wf).filter(n => UNET_TYPES.has(n.type)).map(n => n.widgets_values?.[0]));
return collectUnique(Object.values(wf).filter(n => UNET_TYPES.has(n?.class_type)).map(n => n.inputs?.unet_name));
}
function extractTextEncoders(wf) {
if (!wf || typeof wf !== "object") return [];
const names = [];
if (Array.isArray(wf.nodes)) {
for (const n of collectAllNodes(wf)) {
if (n.type === "CLIPLoader") { if (n.widgets_values?.[0]) names.push(n.widgets_values[0]); }
else if (n.type === "DualCLIPLoader") { [0,1].forEach(i => { if (n.widgets_values?.[i]) names.push(n.widgets_values[i]); }); }
else if (n.type === "TripleCLIPLoader") { [0,1,2].forEach(i => { if (n.widgets_values?.[i]) names.push(n.widgets_values[i]); }); }
else if (n.type === "QuadrupleCLIPLoader") { [0,1,2,3].forEach(i => { if (n.widgets_values?.[i]) names.push(n.widgets_values[i]); }); }
}
} else {
for (const n of Object.values(wf)) {
if (!n || typeof n !== "object") continue;
const ct = n.class_type ?? "";
if (ct === "CLIPLoader") { if (n.inputs?.clip_name) names.push(n.inputs.clip_name); }
else if (ct === "DualCLIPLoader") { if (n.inputs?.clip_name1) names.push(n.inputs.clip_name1); if (n.inputs?.clip_name2) names.push(n.inputs.clip_name2); }
else if (ct === "TripleCLIPLoader") { ["clip_name1","clip_name2","clip_name3"].forEach(k => { if (n.inputs?.[k]) names.push(n.inputs[k]); }); }
else if (ct === "QuadrupleCLIPLoader") { ["clip_name1","clip_name2","clip_name3","clip_name4"].forEach(k => { if (n.inputs?.[k]) names.push(n.inputs[k]); }); }
}
}
return collectUnique(names);
}
function extractLoRAs(wf) {
if (!wf || typeof wf !== "object") return [];
const results = [], seen = new Set();
function add(name, sm, sc) {
if (!name || typeof name !== "string" || name === "None" || seen.has(name)) return;
seen.add(name);
results.push({ name, strength_model: typeof sm === "number" ? sm : 1.0, strength_clip: typeof sc === "number" ? sc : 1.0 });
}
if (Array.isArray(wf.nodes)) {
for (const n of collectAllNodes(wf)) {
const type = n.type ?? "";
if (type === "LoraLoader") add(n.widgets_values?.[0], n.widgets_values?.[1], n.widgets_values?.[2]);
else if (type === "LoraLoaderModelOnly") add(n.widgets_values?.[0], n.widgets_values?.[1], 1.0);
else if (type === "ImageMetadataLoRALoader") { for (let i = 0; i < 3; i++) add(n.widgets_values?.[i*3], n.widgets_values?.[i*3+1], n.widgets_values?.[i*3+2]); }
else if (type === "Lora Loader (LoraManager)") { const list = n.widgets_values?.find(v => Array.isArray(v)); if (list) for (const l of list) { if (l?.active !== false) add(l?.name, l?.strength ?? 1.0, l?.clipStrength ?? l?.strength ?? 1.0); } }
}
} else {
for (const n of Object.values(wf)) {
if (!n || typeof n !== "object") continue;
const ct = n.class_type ?? "";
if (ct === "LoraLoader") add(n.inputs?.lora_name, n.inputs?.strength_model, n.inputs?.strength_clip);
else if (ct === "LoraLoaderModelOnly") add(n.inputs?.lora_name, n.inputs?.strength, 1.0);
}
}
return results;
}
function isTextEncoderNode(ct) { return ct === "CLIPTextEncode" || ct.includes("TextEncode") || ct.includes("TextEncoderSD"); }
function isSamplerNode(ct) { return ct === "KSampler" || ct === "KSamplerAdvanced" || ct.includes("KSampler") || ct.includes("Sampler"); }
function isPromptStylerNode(ct) { return ct.includes("PromptStyler"); }
function extractPrompts(wf) {
if (!wf || typeof wf !== "object") return { positives: [], negatives: [] };
return Array.isArray(wf.nodes) ? extractPromptsLiteGraph(wf) : extractPromptsAPI(wf);
}
// CLIPTextEncode + KSampler でプロンプト抽出(トップレベル・サブグラフ共用)
// サンプラーが見つかりテキストが取れた場合のみ非null を返す
function extractPromptsFromNodeSet(nodes, links) {
const nodeMap = new Map();
for (const n of nodes) nodeMap.set(n.id, n);
const linkOrigin = new Map(), linkSlot = new Map();
if (Array.isArray(links)) {
for (const lk of links) {
if (Array.isArray(lk)) {
linkOrigin.set(lk[0], lk[1]);
linkSlot.set(lk[0], lk[2] ?? 0);
} else if (lk && typeof lk === "object") {
const id = lk.id ?? lk[0], origin = lk.origin_id ?? lk[1], slot = lk.origin_slot ?? lk[2] ?? 0;
if (id != null && origin != null) { linkOrigin.set(id, origin); linkSlot.set(id, slot); }
}
}
}
const textMap = new Map();
for (const n of nodes) {
if (!isTextEncoderNode(n.type ?? "")) continue;
const text = n.widgets_values?.[0];
if (text && typeof text === "string") {
textMap.set(n.id, text);
} else if (Array.isArray(n.inputs)) {
const textInput = n.inputs.find(inp => inp.name === "text" || inp.name === "text_g" || inp.name === "prompt");
if (textInput?.link != null) {
const originId = linkOrigin.get(textInput.link);
const originSlot = linkSlot.get(textInput.link) ?? 0;
const srcNode = originId != null ? nodeMap.get(originId) : null;
if (srcNode) {
const srcType = srcNode.type ?? "";
if (isPromptStylerNode(srcType)) {
const v = srcNode.widgets_values?.[originSlot];
if (v && typeof v === "string") textMap.set(n.id, v);
} else {
// WFS_PromptText, PrimitiveStringMultiline, ComfySwitchNode 等、任意の STRING 出力ノード
const v = srcNode.widgets_values?.[originSlot] ?? srcNode.widgets_values?.[0];
if (v && typeof v === "string") textMap.set(n.id, v);
}
}
}
}
}
const pos = new Set(), neg = new Set();
let foundSampler = false;
for (const n of nodes) {
if (!isSamplerNode(n.type ?? "") || !Array.isArray(n.inputs)) continue;
foundSampler = true;
for (const inp of n.inputs) {
if (!inp || inp.link == null) continue;
const originId = linkOrigin.get(inp.link);
if (originId == null) continue;
const text = textMap.get(originId);
if (!text) continue;
const name = inp.name ?? "";
if (name === "positive" || name.startsWith("positive")) pos.add(text);
else if (name === "negative" || name.startsWith("negative")) neg.add(text);
}
}
if (!foundSampler) return null;
// SamplerCustomAdvanced などで positive/negative 直結がない場合、判別不能テキストとして返す
if (pos.size === 0 && neg.size === 0) {
const allTexts = [...textMap.values()].filter(t => t.trim());
if (allTexts.length > 0) return { positives: [], negatives: [], texts: allTexts };
return null;
}
return { positives: [...pos], negatives: [...neg] };
}
// MarkdownNote の **section** → - [name](url) パターンからモデルを抽出
// flux/qwen/z-image などのサブグラフ形式ワークフロー向け補完用
function extractMarkdownNoteModels(wf) {
const allNodes = [];
if (Array.isArray(wf.nodes)) allNodes.push(...wf.nodes);
for (const sg of wf.definitions?.subgraphs ?? []) if (Array.isArray(sg.nodes)) allNodes.push(...sg.nodes);
const result = { checkpoints: [], vaes: [], diffusionModels: [], textEncoders: [], loras: [] };
const seen = { checkpoints: new Set(), vaes: new Set(), diffusionModels: new Set(), textEncoders: new Set(), loras: new Set() };
function addU(arr, set, name) { if (name && typeof name === "string" && !set.has(name)) { set.add(name); arr.push(name); } }
for (const n of allNodes) {
if (n.type !== "MarkdownNote") continue;
const raw = n.widgets_values;
const text = Array.isArray(raw) ? raw[0] : (typeof raw === "string" ? raw : null);
if (!text) continue;
const sRe = /\*\*([^*\n]+)\*\*/g;
let sm;
while ((sm = sRe.exec(text)) !== null) {
const sec = sm[1].trim().toLowerCase().replace(/\s+/g, "_");
if (!["text_encoders", "diffusion_models", "vae", "checkpoints", "loras"].includes(sec)) continue;
const rest = text.slice(sm.index + sm[0].length);
const end = rest.search(/\n\*\*|\n##/);
const content = end >= 0 ? rest.slice(0, end) : rest;
const lRe = /^- \[([^\]]+)\]/gm;
let lm;
while ((lm = lRe.exec(content)) !== null) {
const name = lm[1].trim();
if (sec === "text_encoders") addU(result.textEncoders, seen.textEncoders, name);
else if (sec === "diffusion_models") addU(result.diffusionModels, seen.diffusionModels, name);
else if (sec === "vae") addU(result.vaes, seen.vaes, name);
else if (sec === "checkpoints") addU(result.checkpoints, seen.checkpoints, name);
else if (sec === "loras") addU(result.loras, seen.loras, name);
}
}
}
const hasAny = result.checkpoints.length || result.vaes.length || result.diffusionModels.length || result.textEncoders.length || result.loras.length;
return hasAny ? result : null;
}
// API形式: リンク参照 [srcNodeId, slot] からテキストを解決
function resolveLinkedText(wf, srcId, slot) {
const src = wf[String(srcId)];
if (!src || typeof src !== "object") return null;
const ct = src.class_type ?? "";
// PromptStyler系: slot 0 = text_positive, slot 1 = text_negative
if (isPromptStylerNode(ct)) {
const v = slot === 0 ? src.inputs?.text_positive : src.inputs?.text_negative;
return (v && typeof v === "string") ? v : null;
}
// 汎用テキストキー
const keys = slot === 0
? ["text_positive", "text", "text_g", "prompt"]
: ["text_negative", "text_l"];
for (const k of keys) {
const v = src.inputs?.[k];
if (v && typeof v === "string") return v;
}
return null;
}
function extractPromptsAPI(wf) {
const metaNodes = Object.values(wf).filter(n => n?.class_type === "ImageMetadataPromptLoader");
if (metaNodes.length > 0) {
const pos = new Set(), neg = new Set();
for (const n of metaNodes) { if (n.inputs?.positive_text) pos.add(n.inputs.positive_text); if (n.inputs?.negative_text) neg.add(n.inputs.negative_text); }
if (pos.size > 0 || neg.size > 0) return { positives: [...pos], negatives: [...neg] };
}
const textMap = new Map();
for (const [id, n] of Object.entries(wf)) {
if (!n || !isTextEncoderNode(n.class_type ?? "")) continue;
const raw = n.inputs?.text ?? n.inputs?.text_g ?? null;
if (raw && typeof raw === "string") {
textMap.set(id, raw);
} else if (Array.isArray(raw)) {
// リンク参照 [srcNodeId, slot] → 解決
const text = resolveLinkedText(wf, raw[0], raw[1] ?? 0);
if (text) textMap.set(id, text);
}
}
const pos = new Set(), neg = new Set();
let foundSampler = false;
for (const n of Object.values(wf)) {
if (!n || !isSamplerNode(n.class_type ?? "")) continue;
foundSampler = true;
for (const [key, val] of Object.entries(n.inputs ?? {})) {
if (!Array.isArray(val)) continue;
const text = textMap.get(String(val[0]));
if (!text) continue;
if (key === "positive" || key.startsWith("positive")) pos.add(text);
else if (key === "negative" || key.startsWith("negative")) neg.add(text);
}
}
if (!foundSampler || (pos.size === 0 && neg.size === 0)) { const all = [...textMap.values()].filter(t => t && t.trim()); return { positives: [], negatives: [], texts: all }; }
return { positives: [...pos], negatives: [...neg] };
}
function extractPromptsLiteGraph(wf) {
const { nodes, links } = wf;
if (!Array.isArray(nodes)) return { positives: [], negatives: [] };
// 1. ImageMetadataPromptLoader (WFS専用)
const metaNodes = nodes.filter(n => n.type === "ImageMetadataPromptLoader");
if (metaNodes.length > 0) {
const pos = new Set(), neg = new Set();
for (const n of metaNodes) { const p = n.widgets_values?.[2], ng = n.widgets_values?.[3]; if (p) pos.add(p); if (ng) neg.add(ng); }
if (pos.size > 0 || neg.size > 0) return { positives: [...pos], negatives: [...neg] };
}
// 2. WFS_PromptText (WFS専用)
const wfsNodes = nodes.filter(n => n.type === "WFS_PromptText");
if (wfsNodes.length > 0) {
const pos = new Set(), neg = new Set();
for (const n of wfsNodes) { const p = n.widgets_values?.[0], ng = n.widgets_values?.[1]; if (p) pos.add(p); if (ng) neg.add(ng); }
if (pos.size > 0 || neg.size > 0) return { positives: [...pos], negatives: [...neg] };
}
// 3. トップレベルの CLIPTextEncode + KSampler
const topResult = extractPromptsFromNodeSet(nodes, links ?? []);
if (topResult) return topResult;
// 4. PrimitiveStringMultiline(flux2-klein / ernie など)- サブグラフ内も探す
const primTexts = [];
for (const n of collectAllNodes(wf)) {
if (n.type !== "PrimitiveStringMultiline") continue;
const t = Array.isArray(n.widgets_values) ? n.widgets_values[0] : n.widgets_values;
if (t && typeof t === "string" && t.trim()) primTexts.push(t.trim());
}
if (primTexts.length > 0) return { positives: [], negatives: [], texts: primTexts };
// 5. サブグラフ内の CLIPTextEncode + KSampler(z-image / qwen / flux など)
for (const sg of wf.definitions?.subgraphs ?? []) {
if (!Array.isArray(sg.nodes)) continue;
const sgResult = extractPromptsFromNodeSet(sg.nodes, sg.links ?? []);
if (sgResult) return sgResult;
}
// 6. PromptStyler フォールバック
const stylerPos = new Set(), stylerNeg = new Set();
for (const n of nodes) {
if (!isPromptStylerNode(n.type ?? "")) continue;
const vals = n.widgets_values ?? [];
for (let i = 0; i < vals.length; i++) {
if (typeof vals[i] !== "string" || !vals[i].trim()) continue;
if (i % 2 === 0) stylerPos.add(vals[i]);
else stylerNeg.add(vals[i]);
}
}
if (stylerPos.size > 0 || stylerNeg.size > 0) return { positives: [...stylerPos], negatives: [...stylerNeg] };
// 7. テキストエンコーダー全テキスト(最終フォールバック)- サブグラフも含む・判別不能
const all = [];
for (const n of collectAllNodes(wf)) {
if (!isTextEncoderNode(n.type ?? "")) continue;
const t = n.widgets_values?.[0];
if (t && typeof t === "string" && t.trim()) all.push(t);
}
return { positives: [], negatives: [], texts: all };
}
// ── SD/Fooocus prompt extraction ──────────────────────────────
function parseSDAParameters(raw) {
const text = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const stepsMatch = text.match(/\nSteps:\s+\d/);
if (!stepsMatch) return null;
const paramsStart = stepsMatch.index + 1;
const promptSection = text.slice(0, paramsStart - 1);
const paramsLine = text.slice(paramsStart);
const negSep = "\nNegative prompt: ";
const negIdx = promptSection.indexOf(negSep);
let positive = "", negative = "";
if (negIdx !== -1) { positive = promptSection.slice(0, negIdx).trim(); negative = promptSection.slice(negIdx + negSep.length).trim(); }
else positive = promptSection.trim();
const params = {};
const re = /,?\s*([A-Za-z][A-Za-z0-9 ]*):\s*("(?:[^"\\]|\\.)*"|[^,]+)/g;
let m;
while ((m = re.exec(paramsLine)) !== null) params[m[1].trim()] = m[2].trim().replace(/^"|"$/g, "");
return { positive, negative, params };
}
function parseFooocusMetadata(raw) {
let obj; try { obj = JSON.parse(raw); } catch { return null; }
if (!obj?.base_model) return null;
const toArray = v => !v ? [] : Array.isArray(v) ? v.filter(Boolean) : [String(v)];
return { checkpoint: obj.base_model, vae: (obj.vae && obj.vae !== "Default") ? obj.vae : null, positives: toArray(obj.full_prompt ?? obj.prompt), negatives: toArray(obj.full_negative_prompt ?? obj.negative_prompt) };
}
// ── Master extractAllMetadata ─────────────────────────────────
async function extractAllMetadata(file) {
const name = file.name.toLowerCase();
const isJSON = file.type === "application/json" || name.endsWith(".json");
const isWebP = file.type === "image/webp" || name.endsWith(".webp");
function fromWorkflow(wf, source) {
const base = { source, checkpoints: extractCheckpoints(wf), vaes: extractVAEs(wf), diffusionModels: extractDiffusionModels(wf), textEncoders: extractTextEncoders(wf), loras: extractLoRAs(wf), ...extractPrompts(wf) };
// MarkdownNote からモデル情報を補完(subgraph形式の flux/qwen/z-image など)
const mdm = extractMarkdownNoteModels(wf);
if (mdm) {
if (!base.checkpoints.length) base.checkpoints = mdm.checkpoints;
if (!base.vaes.length) base.vaes = mdm.vaes;
if (!base.diffusionModels.length) base.diffusionModels = mdm.diffusionModels;
if (!base.textEncoders.length) base.textEncoders = mdm.textEncoders;
if (!base.loras.length) base.loras = mdm.loras;
}
return base;
}
if (isJSON) {
let wf; try { wf = JSON.parse(sanitizeJSON(await file.text())); } catch { return null; }
return wf ? fromWorkflow(wf, "comfyui") : null;
}
if (isWebP) {
const exif = await readWebPEXIFChunk(file);
if (!exif) return null;
const wf = extractWorkflowFromEXIF(exif);
return wf ? fromWorkflow(wf, "comfyui") : null;
}
// PNG
const chunks = await readAllPNGTextChunks(file);
if (!chunks) return null;
if (chunks.prompt) { let wf; try { wf = JSON.parse(sanitizeJSON(chunks.prompt)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
if (chunks.workflow) { let wf; try { wf = JSON.parse(sanitizeJSON(chunks.workflow)); } catch { return null; } return wf ? fromWorkflow(wf, "comfyui") : null; }
if (chunks.fooocus_scheme === "fooocus" && chunks.parameters) {
const f = parseFooocusMetadata(chunks.parameters);
if (!f) return null;
return { source: "fooocus", checkpoints: [f.checkpoint], vaes: f.vae ? [f.vae] : [], diffusionModels: [], textEncoders: [], loras: [], positives: f.positives, negatives: f.negatives };
}
if (chunks.parameters) {
const p = parseSDAParameters(chunks.parameters);
if (!p) return null;
const { positive, negative, params } = p;
const modelName = params["Model"];
if (!modelName) return null;
if (params["Module 2"] != null) {
const textEncoders = [];
for (let i = 2; i <= 9; i++) { const mod = params[`Module ${i}`]; if (!mod) break; textEncoders.push(mod); }
return { source: "sd_forge", checkpoints: [], vaes: params["Module 1"] ? [params["Module 1"]] : [], diffusionModels: [modelName], textEncoders, loras: [], positives: positive ? [positive] : [], negatives: negative ? [negative] : [] };
}
const vaeValue = params["Module 1"] ?? params["VAE"] ?? null;
return { source: "sd", checkpoints: [modelName], vaes: vaeValue ? [vaeValue] : [], diffusionModels: [], textEncoders: [], loras: [], positives: positive ? [positive] : [], negatives: negative ? [negative] : [] };
}
return null;
}
// ── UI helpers ────────────────────────────────────────────────
function escapeHtml(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
function buildModelItem(label) {
const el = document.createElement("div");
el.className = "wfm-meta-item";
el.title = label;
el.innerHTML = `<span class="wfm-meta-item-name">${escapeHtml(label)}</span>`;
return el;
}
function buildLoRAItem(lora) {
const el = document.createElement("div");
el.className = "wfm-meta-item";
el.title = lora.name;
const sm = lora.strength_model.toFixed(2);
const sc = lora.strength_clip.toFixed(2);
el.innerHTML = `<span class="wfm-meta-item-name">${escapeHtml(lora.name)}</span><span class="wfm-meta-item-badge">${sm}/${sc}</span>`;
return el;
}
function buildPromptItem(label, type, full, fullArea, fullLabel, listEl) {
const el = document.createElement("div");
el.className = "wfm-meta-item wfm-meta-item-clickable";
const snippet = label.length > 60 ? label.slice(0, 60) + "…" : label;
const typeBadge = type === "positive" ? `<span class="wfm-meta-badge-pos">POS</span>`
: type === "negative" ? `<span class="wfm-meta-badge-neg">NEG</span>`
: ``;
el.innerHTML = `${typeBadge}<span class="wfm-meta-item-name">${escapeHtml(snippet)}</span>`;
el.addEventListener("click", () => {
listEl.querySelectorAll(".wfm-meta-item-clickable").forEach(e => e.classList.remove("selected"));
el.classList.add("selected");
fullArea.value = full;
fullLabel.textContent = type === "positive" ? t("metaPromptPositive")
: type === "negative" ? t("metaPromptNegative")
: t("metaPromptText") || "Text";
});
return el;
}
function renderSection(sectionEl, listEl, items, buildFn) {
listEl.innerHTML = "";
if (items.length === 0) { sectionEl.classList.add("wfm-meta-section-empty"); return; }
sectionEl.classList.remove("wfm-meta-section-empty");
for (const item of items) listEl.appendChild(buildFn(item));
}
// ── Tab initialization ────────────────────────────────────────
export function initMetadataTab() {
const dropZone = document.getElementById("wfm-meta-drop");
const dropLabel = document.getElementById("wfm-meta-drop-label");
const previewImg = document.getElementById("wfm-meta-preview-img");
const fileInfo = document.getElementById("wfm-meta-file-info");
const fileInput = document.getElementById("wfm-meta-file-input");
const ckptSection = document.getElementById("wfm-meta-ckpt-section");
const vaeSection = document.getElementById("wfm-meta-vae-section");
const diffSection = document.getElementById("wfm-meta-diff-section");
const teSection = document.getElementById("wfm-meta-te-section");
const loraSection = document.getElementById("wfm-meta-lora-section");
const promptSection = document.getElementById("wfm-meta-prompt-section");
const ckptList = document.getElementById("wfm-meta-ckpt-list");
const vaeList = document.getElementById("wfm-meta-vae-list");
const diffList = document.getElementById("wfm-meta-diff-list");
const teList = document.getElementById("wfm-meta-te-list");
const loraList = document.getElementById("wfm-meta-lora-list");
const promptList = document.getElementById("wfm-meta-prompt-list");
const promptFull = document.getElementById("wfm-meta-prompt-full");
const promptFullLabel = document.getElementById("wfm-meta-prompt-full-label");
if (!dropZone) return;
// Apply i18n to section titles
const titleMap = {
"wfm-meta-ckpt-section": "metaSectionCkpt",
"wfm-meta-vae-section": "metaSectionVae",
"wfm-meta-diff-section": "metaSectionDiff",
"wfm-meta-te-section": "metaSectionTe",
"wfm-meta-lora-section": "metaSectionLora",
"wfm-meta-prompt-section": "metaSectionPrompt",
};
for (const [id, key] of Object.entries(titleMap)) {
const title = document.querySelector(`#${id} .wfm-meta-section-title`);
if (title) { const tx = t(key); if (tx && tx !== key) title.textContent = tx; }
}
// Apply i18n to format note
const i18nIds = {
"wfm-meta-format-note-title": "metaFormatNoteTitle",
"wfm-meta-fmt-comfyui": "metaFmtComfyui",
"wfm-meta-fmt-sdwebui": "metaFmtSdwebui",
"wfm-meta-fmt-fooocus": "metaFmtFooocus",
"wfm-meta-format-todo": "metaFormatTodo",
};
for (const [id, key] of Object.entries(i18nIds)) {
const el = document.getElementById(id);
if (el) { const tx = t(key); if (tx && tx !== key) el.textContent = tx; }
}
// Apply i18n to help card
const helpIds = {
"wfm-help-metadata-title": "helpMetadataTitle",
"wfm-help-metadata-desc": "helpMetadataDesc",
"wfm-help-metadata-1": "helpMetadata1",
"wfm-help-metadata-2": "helpMetadata2",
"wfm-help-metadata-3": "helpMetadata3",
"wfm-help-metadata-4": "helpMetadata4",
"wfm-help-metadata-5": "helpMetadata5",
};
for (const [id, key] of Object.entries(helpIds)) {
const el = document.getElementById(id);
if (el) { const tx = t(key); if (tx && tx !== key) el.textContent = tx; }
}
function clearAll() {
[ckptList, vaeList, diffList, teList, loraList, promptList].forEach(l => { if (l) l.innerHTML = ""; });
[ckptSection, vaeSection, diffSection, teSection, loraSection, promptSection].forEach(s => { if (s) s.classList.add("wfm-meta-section-empty"); });
if (promptFull) promptFull.value = "";
if (promptFullLabel) promptFullLabel.textContent = "";
}
async function handleFile(file) {
if (!file) return;
if (file.size > MAX_FILE_SIZE) {
fileInfo.textContent = t("metaFileTooLarge");
fileInfo.style.color = "var(--wfm-warning)";
return;
}
fileInfo.textContent = t("metaParsing");
fileInfo.style.color = "var(--wfm-text-secondary)";
clearAll();
// Show preview for images
const isImage = file.type.startsWith("image/") || file.name.toLowerCase().match(/\.(png|webp|jpg|jpeg)$/);
if (isImage) {
const url = URL.createObjectURL(file);
previewImg.src = url;
previewImg.style.display = "block";
dropLabel.style.display = "none";
previewImg.onload = () => URL.revokeObjectURL(url);
} else {
previewImg.style.display = "none";
dropLabel.style.display = "flex";
}
let meta;
try {
meta = await extractAllMetadata(file);
} catch (err) {
console.error("[MetadataTab]", err);
fileInfo.textContent = t("metaParseError");
fileInfo.style.color = "var(--wfm-danger)";
return;
}
if (!meta) {
fileInfo.textContent = t("metaNoMetadata");
fileInfo.style.color = "var(--wfm-warning)";
return;
}
const sizeKB = (file.size / 1024).toFixed(1);
const sourceLabel = { comfyui: "ComfyUI", sd: "SD WebUI", sd_forge: "SD Forge", fooocus: "Fooocus" }[meta.source] ?? meta.source;
fileInfo.textContent = `${file.name} (${sizeKB} KB · ${sourceLabel})`;
fileInfo.style.color = "var(--wfm-text-secondary)";
renderSection(ckptSection, ckptList, meta.checkpoints, n => buildModelItem(n));
renderSection(vaeSection, vaeList, meta.vaes, n => buildModelItem(n));
renderSection(diffSection, diffList, meta.diffusionModels, n => buildModelItem(n));
renderSection(teSection, teList, meta.textEncoders, n => buildModelItem(n));
renderSection(loraSection, loraList, meta.loras, l => buildLoRAItem(l));
// Prompts
promptList.innerHTML = "";
const allPrompts = [
...meta.positives.map(p => ({ type: "positive", text: p })),
...meta.negatives.map(p => ({ type: "negative", text: p })),
...(meta.texts ?? []).map(p => ({ type: "text", text: p })),
];
if (allPrompts.length > 0) {
promptSection.classList.remove("wfm-meta-section-empty");
for (const { type, text } of allPrompts) {
promptList.appendChild(buildPromptItem(text, type, text, promptFull, promptFullLabel, promptList));
}
// Auto-select first positive
const firstPositive = promptList.querySelector(".wfm-meta-item-clickable");
if (firstPositive) firstPositive.click();
} else {
promptSection.classList.add("wfm-meta-section-empty");
}
}
// ── Prompt action buttons ─────────────────────────────────
function getPromptText() { return promptFull?.value ?? ""; }
function flashBtn(btn) {
btn.classList.add("wfm-meta-btn-flash");
setTimeout(() => btn.classList.remove("wfm-meta-btn-flash"), 600);
}
function setTextareaValue(id, text) {
const el = document.getElementById(id);
if (!el) return false;
el.value = text;
el.dispatchEvent(new Event("input", { bubbles: true }));
return true;
}
document.getElementById("wfm-meta-copy-btn")?.addEventListener("click", function () {
const text = getPromptText();
if (!text) return;
navigator.clipboard.writeText(text).then(() => flashBtn(this));
});
document.getElementById("wfm-meta-genui-pos-btn")?.addEventListener("click", function () {
const text = getPromptText();
if (!text) return;
if (setTextareaValue("wfm-prompt-pos-text", text)) flashBtn(this);
});
document.getElementById("wfm-meta-genui-neg-btn")?.addEventListener("click", function () {
const text = getPromptText();
if (!text) return;
if (setTextareaValue("wfm-prompt-neg-text", text)) flashBtn(this);
});
document.getElementById("wfm-meta-preset-pos-btn")?.addEventListener("click", function () {
const text = getPromptText();
if (!text) return;
if (setTextareaValue("wfm-preset-pos", text)) flashBtn(this);
});
document.getElementById("wfm-meta-preset-neg-btn")?.addEventListener("click", function () {
const text = getPromptText();
if (!text) return;
if (setTextareaValue("wfm-preset-neg", text)) flashBtn(this);
});
// Drag & Drop
dropZone.addEventListener("dragover", e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add("drag-over"); });
dropZone.addEventListener("dragleave", e => { e.stopPropagation(); dropZone.classList.remove("drag-over"); });
dropZone.addEventListener("drop", e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove("drag-over"); handleFile(e.dataTransfer.files?.[0]); });
dropZone.addEventListener("click", e => { if (e.target === previewImg) return; fileInput.click(); });
fileInput.addEventListener("change", () => { handleFile(fileInput.files?.[0]); fileInput.value = ""; });
// Prevent scroll passthrough in lists
[ckptList, vaeList, diffList, teList, loraList, promptList].forEach(l => {
if (l) l.addEventListener("wheel", e => e.stopPropagation(), { passive: true });
});
clearAll();
}
+37 -3
View File
@@ -169,6 +169,7 @@ function previewUrl(modelName, modelType) {
/**
* Load preview image. Uses img onload/onerror instead of HEAD request
* (aiohttp add_get does not auto-handle HEAD method).
* Falls back to CivitAI cached image if no local preview exists.
*/
function loadPreviewImage(imgEl, placeholderEl, modelName, modelType) {
const url = previewUrl(modelName, modelType);
@@ -177,8 +178,23 @@ function loadPreviewImage(imgEl, placeholderEl, modelName, modelType) {
if (placeholderEl) placeholderEl.style.display = "none";
};
imgEl.onerror = () => {
imgEl.style.display = "none";
if (placeholderEl) placeholderEl.style.display = "";
// Fallback: use CivitAI cached image if available
const meta = state.modelMetadata[modelName] || {};
const sha256 = meta.sha256;
const civitai = sha256 && state.civitaiCache[sha256];
const civitaiImg = civitai && civitai.images && civitai.images[0];
if (civitaiImg) {
imgEl.onerror = () => {
imgEl.style.display = "none";
if (placeholderEl) placeholderEl.style.display = "";
};
imgEl.src = civitaiImg;
imgEl.style.display = "";
if (placeholderEl) placeholderEl.style.display = "none";
} else {
imgEl.style.display = "none";
if (placeholderEl) placeholderEl.style.display = "";
}
};
imgEl.src = url;
}
@@ -1456,6 +1472,23 @@ async function fetchCivitaiForModel(modelName, el) {
}
renderCivitaiInfo(el, data.civitai, modelName);
showToast(t("civitaiFound"), "success");
// Always refresh preview: local file if saved, otherwise civitai image as fallback
const sidePanel = document.getElementById("wfm-models-side-panel");
if (sidePanel) {
const sideImg = sidePanel.querySelector(".wfm-side-thumb-img-wrap img");
const sidePh = sidePanel.querySelector(".wfm-side-thumb-placeholder");
if (sideImg) {
if (data.preview_saved) {
sideImg.src = previewUrl(modelName) + "&t=" + Date.now();
} else {
const civitaiImg = data.civitai.images && data.civitai.images[0];
if (civitaiImg) sideImg.src = civitaiImg;
}
sideImg.style.display = "";
if (sidePh) sidePh.style.display = "none";
}
}
renderModelGrid();
} else if (data.status === "not_found") {
if (statusEl) statusEl.textContent = t("civitaiNotFound");
if (fetchBtn) fetchBtn.disabled = false;
@@ -1637,7 +1670,8 @@ async function batchFetchCivitai() {
if (progressEl) progressEl.textContent = `${pct}% (${data.current}/${data.total}) ${statusText}`;
} else if (eventType === "done") {
if (progressEl) progressEl.textContent = "";
showToast(t("civitaiBatchDone", data.found, data.not_found), "success");
const previewNote = data.preview_saved > 0 ? ` (+${data.preview_saved} preview)` : "";
showToast(t("civitaiBatchDone", data.found, data.not_found) + previewNote, "success");
// Reload caches
const [newMeta, newCache] = await Promise.all([fetchModelMetadata(), fetchCivitaiCache()]);
state.modelMetadata = newMeta;
+380
View File
@@ -593,6 +593,233 @@ function applyToGenerateUI(text) {
showToast(t("appliedToGenerateUI"), "success");
}
// ============================================
// Wildcard API helpers
// ============================================
async function wcFetchFiles() {
try {
const res = await fetch("/api/wfm/wildcards");
return res.ok ? await res.json() : [];
} catch { return []; }
}
async function wcFetchContent(filename) {
try {
const res = await fetch(`/api/wfm/wildcards/content?filename=${encodeURIComponent(filename)}`);
const data = await res.json();
return data.content ?? null;
} catch { return null; }
}
async function wcSaveFile(filename, content) {
try {
const res = await fetch("/api/wfm/wildcards/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename, content }),
});
const data = await res.json();
return data.status === "ok" ? data.file : null;
} catch { return null; }
}
async function wcDeleteFile(filename) {
try {
await fetch("/api/wfm/wildcards/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename }),
});
} catch { /* ignore */ }
}
// ============================================
// Wildcard state & helpers
// ============================================
let wcFiles = [];
let wcEditingFilename = null; // null = new file
// Insert text at cursor; if open+close provided wraps selection
function wcInsertAtCursor(textarea, open, close = "") {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selected = textarea.value.substring(start, end);
let inserted, cursorStart, cursorEnd;
if (selected && close) {
inserted = open + selected + close;
cursorStart = start + open.length;
cursorEnd = cursorStart + selected.length;
} else if (close) {
inserted = open + close;
cursorStart = start + open.length;
cursorEnd = cursorStart;
} else {
inserted = open;
cursorStart = start + open.length;
cursorEnd = cursorStart;
}
textarea.value =
textarea.value.substring(0, start) +
inserted +
textarea.value.substring(end);
textarea.setSelectionRange(cursorStart, cursorEnd);
textarea.focus();
textarea.dispatchEvent(new Event("input"));
}
// ============================================
// Wildcard file list rendering
// ============================================
function wcRenderFileList() {
const list = document.getElementById("wfm-wc-file-list");
if (!list) return;
list.innerHTML = "";
if (wcFiles.length === 0) {
list.innerHTML = `<div class="wfm-pm-empty" style="font-size:12px;">No wildcard files yet.<br><small>Click "+ New" to create one.</small></div>`;
return;
}
// Group by directory; root ("") first, then alphabetical
const grouped = {};
for (const f of wcFiles) {
const dir = f.dir || "";
if (!grouped[dir]) grouped[dir] = [];
grouped[dir].push(f);
}
const dirs = Object.keys(grouped).sort((a, b) => {
if (a === "") return -1;
if (b === "") return 1;
return a.localeCompare(b);
});
function addFileItem(f, indented) {
const item = document.createElement("div");
item.className = "wfm-wc-file-item" + (indented ? " wfm-wc-file-item--sub" : "");
item.innerHTML = `
<div class="wfm-wc-file-item-info">
<span class="wfm-wc-file-name">${esc(f.name)}</span>
<span class="wfm-wc-file-ext">.${esc(f.ext)}</span>
</div>
<div class="wfm-wc-file-item-actions">
<button class="wfm-pm-action-btn wfm-wc-use-btn" title="Insert __${esc(f.wc_name)}__ at cursor">Use</button>
<button class="wfm-pm-action-btn wfm-wc-edit-btn" title="Edit file">&#9998;</button>
</div>
`;
item.querySelector(".wfm-wc-use-btn").addEventListener("click", (e) => {
e.stopPropagation();
const ta = document.getElementById("wfm-wc-prompt");
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
});
item.querySelector(".wfm-wc-edit-btn").addEventListener("click", async (e) => {
e.stopPropagation();
const content = await wcFetchContent(f.filename);
wcOpenEditor(f.wc_name, f.ext, content ?? "", f.filename);
});
item.addEventListener("click", () => {
const ta = document.getElementById("wfm-wc-prompt");
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
});
list.appendChild(item);
}
for (const dir of dirs) {
if (dir) {
const header = document.createElement("div");
header.className = "wfm-wc-dir-header";
header.textContent = dir + "/";
list.appendChild(header);
}
for (const f of grouped[dir]) {
addFileItem(f, dir !== "");
}
}
}
// ============================================
// Wildcard file editor
// ============================================
function wcOpenEditor(name, ext, content, existingFilename) {
wcEditingFilename = existingFilename || null;
const editor = document.getElementById("wfm-wc-editor");
const nameInput = document.getElementById("wfm-wc-editor-name");
const extSelect = document.getElementById("wfm-wc-editor-ext");
const contentTA = document.getElementById("wfm-wc-editor-content");
const deleteBtn = document.getElementById("wfm-wc-editor-delete-btn");
if (!editor) return;
nameInput.value = name || "";
extSelect.value = ext || "txt";
contentTA.value = content || "";
deleteBtn.style.display = existingFilename ? "" : "none";
editor.style.display = "";
nameInput.focus();
}
function wcCloseEditor() {
wcEditingFilename = null;
const editor = document.getElementById("wfm-wc-editor");
if (editor) editor.style.display = "none";
}
async function wcRefreshFiles() {
wcFiles = await wcFetchFiles();
wcRenderFileList();
wcUpdateFilePicker();
}
// ============================================
// Wildcard file picker popup
// ============================================
function wcUpdateFilePicker() {
const picker = document.getElementById("wfm-wc-file-picker");
if (!picker) return;
picker.innerHTML = "";
if (wcFiles.length === 0) {
picker.innerHTML = `<div style="padding:8px;font-size:11px;color:var(--wfm-text-secondary);">No wildcard files</div>`;
return;
}
// Group by directory same as file list
const grouped = {};
for (const f of wcFiles) {
const dir = f.dir || "";
if (!grouped[dir]) grouped[dir] = [];
grouped[dir].push(f);
}
const dirs = Object.keys(grouped).sort((a, b) => {
if (a === "") return -1;
if (b === "") return 1;
return a.localeCompare(b);
});
for (const dir of dirs) {
if (dir) {
const sep = document.createElement("div");
sep.className = "wfm-wc-picker-dir";
sep.textContent = dir + "/";
picker.appendChild(sep);
}
for (const f of grouped[dir]) {
const btn = document.createElement("button");
btn.className = "wfm-wc-picker-item" + (dir ? " wfm-wc-picker-item--sub" : "");
btn.textContent = `__${f.wc_name}__`;
btn.addEventListener("click", () => {
const ta = document.getElementById("wfm-wc-prompt");
if (ta) wcInsertAtCursor(ta, `__${f.wc_name}__`);
picker.style.display = "none";
});
picker.appendChild(btn);
}
}
}
// ============================================
// Initialize
// ============================================
@@ -853,4 +1080,157 @@ export function initPromptTab() {
pmSearchText = e.target.value.trim();
renderPresetManager();
});
// ── Center column tab switching ──────────────────────────
document.querySelectorAll(".wfm-prompt-center-tab").forEach(btn => {
btn.addEventListener("click", () => {
const target = btn.dataset.ptab;
document.querySelectorAll(".wfm-prompt-center-tab").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
document.querySelectorAll(".wfm-prompt-center-pane").forEach(p => {
p.style.display = p.id === `wfm-ptab-${target}` ? "" : "none";
});
});
});
// ── Wildcard syntax buttons ──────────────────────────────
document.querySelectorAll(".wfm-wc-btn[data-wc-open]").forEach(btn => {
btn.addEventListener("click", () => {
const ta = document.getElementById("wfm-wc-prompt");
if (!ta) return;
const open = btn.dataset.wcOpen || "";
const close = btn.dataset.wcClose || "";
wcInsertAtCursor(ta, open, close);
});
});
// LoRA LBW template button
document.getElementById("wfm-wc-lora-btn")?.addEventListener("click", () => {
const ta = document.getElementById("wfm-wc-prompt");
if (!ta) return;
wcInsertAtCursor(ta, "<lora::1:LBW=;>");
// Move cursor to after "lora:" for the name
const pos = ta.selectionStart - "<lora::1:LBW=;>".length + 6;
ta.setSelectionRange(pos, pos);
ta.focus();
});
// n$${ } button — asks for n then inserts multi-pick template
document.getElementById("wfm-wc-nset-btn")?.addEventListener("click", () => {
const ta = document.getElementById("wfm-wc-prompt");
if (!ta) return;
const n = prompt("Number of items to pick (n):", "2");
if (n === null) return;
const num = parseInt(n, 10);
if (isNaN(num) || num < 1) { showToast("Enter a positive integer", "error"); return; }
const template = "{" + num + "$$|}";
const start = ta.selectionStart;
wcInsertAtCursor(ta, template);
// Move cursor between $$ and |
const inner = start + ("{" + num + "$$").length;
ta.setSelectionRange(inner, inner);
ta.focus();
});
// File picker toggle
const filePickBtn = document.getElementById("wfm-wc-filepick-btn");
const filePicker = document.getElementById("wfm-wc-file-picker");
filePickBtn?.addEventListener("click", (e) => {
e.stopPropagation();
if (!filePicker) return;
const isOpen = filePicker.style.display !== "none";
filePicker.style.display = isOpen ? "none" : "";
if (!isOpen) wcUpdateFilePicker();
});
document.addEventListener("click", () => {
if (filePicker) filePicker.style.display = "none";
});
// ── Wildcard prompt actions ──────────────────────────────
document.getElementById("wfm-wc-copy-btn")?.addEventListener("click", () => {
const ta = document.getElementById("wfm-wc-prompt");
const text = ta?.value || "";
if (!text.trim()) { showToast(t("noTextToCopy"), "error"); return; }
navigator.clipboard.writeText(text).then(() => showToast(t("copiedToClipboard"), "success"));
});
document.getElementById("wfm-wc-to-pos-btn")?.addEventListener("click", () => {
const src = document.getElementById("wfm-wc-prompt");
const dst = document.getElementById("wfm-preset-pos");
if (!src || !dst) return;
dst.value = src.value;
dst.dispatchEvent(new Event("input", { bubbles: true }));
showToast("Sent to Positive Prompt", "success");
});
document.getElementById("wfm-wc-to-neg-btn")?.addEventListener("click", () => {
const src = document.getElementById("wfm-wc-prompt");
const dst = document.getElementById("wfm-preset-neg");
if (!src || !dst) return;
dst.value = src.value;
dst.dispatchEvent(new Event("input", { bubbles: true }));
showToast("Sent to Negative Prompt", "success");
});
document.getElementById("wfm-wc-clear-btn")?.addEventListener("click", () => {
const ta = document.getElementById("wfm-wc-prompt");
if (ta) { ta.value = ""; ta.focus(); }
});
// ── Wildcard file manager ────────────────────────────────
document.getElementById("wfm-wc-new-file-btn")?.addEventListener("click", () => {
wcOpenEditor("", "txt", "", null);
});
document.getElementById("wfm-wc-files-refresh-btn")?.addEventListener("click", () => {
wcRefreshFiles();
});
// Editor: save
document.getElementById("wfm-wc-editor-save-btn")?.addEventListener("click", async () => {
const nameInput = document.getElementById("wfm-wc-editor-name");
const extSelect = document.getElementById("wfm-wc-editor-ext");
const contentTA = document.getElementById("wfm-wc-editor-content");
// Normalize separators and strip leading/trailing slashes
const rawName = (nameInput?.value || "").trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
const ext = extSelect?.value || "txt";
const content = contentTA?.value || "";
if (!rawName) { showToast("Enter a filename", "error"); return; }
// Validate each path component (allow letters, numbers, -, _, ., space)
const nameParts = rawName.split("/");
if (nameParts.some(p => !p || !/^[\w\-. ]+$/.test(p))) {
showToast("Invalid path. Use folder/name format, each part: letters, numbers, -, _, . only", "error");
return;
}
const name = rawName;
const filename = `${name}.${ext}`;
const saved = await wcSaveFile(filename, content);
if (saved) {
showToast(`Saved: ${filename}`, "success");
wcCloseEditor();
wcRefreshFiles();
} else {
showToast("Failed to save file", "error");
}
});
// Editor: delete
document.getElementById("wfm-wc-editor-delete-btn")?.addEventListener("click", async () => {
if (!wcEditingFilename) return;
if (!confirm(`Delete "${wcEditingFilename}"?`)) return;
await wcDeleteFile(wcEditingFilename);
showToast(`Deleted: ${wcEditingFilename}`, "success");
wcCloseEditor();
wcRefreshFiles();
});
// Editor: cancel
document.getElementById("wfm-wc-editor-cancel-btn")?.addEventListener("click", () => {
wcCloseEditor();
});
// Initial wildcard file load
wcRefreshFiles();
}
+209
View File
@@ -314,6 +314,60 @@ function clearCustomOverrides() {
if (fontEl) fontEl.remove();
}
const TEXTAREA_FONT_SIZE_DEFAULT = 13;
const TEXTAREA_FONT_SIZE_STYLE_ID = "wfm-ta-font-size-style";
const TEXTAREA_FONT_SIZE_TARGETS = [
"#wfm-prompt-pos-text",
"#wfm-prompt-neg-text",
"#wfm-ollama-input",
"#wfm-preset-pos",
"#wfm-preset-neg",
"#wfm-wc-prompt",
"#wfm-wc-editor-content",
"#wfm-meta-prompt-full",
].join(",\n ");
export function applyTextareaFontSize(size) {
const px = Math.max(10, Math.min(28, parseInt(size) || TEXTAREA_FONT_SIZE_DEFAULT));
let el = document.getElementById(TEXTAREA_FONT_SIZE_STYLE_ID);
if (!el) {
el = document.createElement("style");
el.id = TEXTAREA_FONT_SIZE_STYLE_ID;
document.head.appendChild(el);
}
el.textContent = `${TEXTAREA_FONT_SIZE_TARGETS} { font-size: ${px}px !important; }`;
}
// JSON highlight color settings
const JSON_COLOR_STYLE_ID = "wfm-json-color-style";
const JSON_COLOR_DEFS = [
{ id: "base", label: () => t("jsonColorBase") || "通常テキスト", def: "#abb2bf" },
{ id: "yellow", label: () => t("jsonColorYellow") || "名前 / スケジューラ", def: "#e5c07b" },
{ id: "pink", label: () => t("jsonColorPink") || "タイトル", def: "#c678dd" },
{ id: "green", label: () => t("jsonColorGreen") || "Width / Height", def: "#98c379" },
{ id: "cyan", label: () => t("jsonColorCyan") || "プロンプト / テキスト", def: "#61afef" },
{ id: "red", label: () => t("jsonColorRed") || "画像 / ファイル", def: "#e06c75" },
];
export function applyJsonColors(colors) {
const vals = Object.fromEntries(JSON_COLOR_DEFS.map(d => [d.id, colors?.[d.id] || d.def]));
let el = document.getElementById(JSON_COLOR_STYLE_ID);
if (!el) {
el = document.createElement("style");
el.id = JSON_COLOR_STYLE_ID;
document.head.appendChild(el);
}
el.textContent = [
`.wfm-json-highlight { color: ${vals.base}; }`,
`.json-key-yellow, .json-val-yellow { color: ${vals.yellow}; }`,
`.json-key-pink, .json-val-pink { color: ${vals.pink}; }`,
`.json-key-green, .json-val-green { color: ${vals.green}; }`,
`.json-key-cyan, .json-val-cyan { color: ${vals.cyan}; }`,
`.json-key-red, .json-val-red { color: ${vals.red}; }`,
].join("\n");
}
export function applyTheme(themeId) {
if (themeId) {
document.documentElement.setAttribute("data-theme", themeId);
@@ -485,6 +539,41 @@ export async function initSettingsTab() {
</div>
</details>
<!-- Text Size -->
<details class="wfm-settings-section" open>
<summary class="wfm-settings-summary">${t("textSizeLabel") || "Text Size"}</summary>
<div class="wfm-form-group">
<label style="display:flex;align-items:center;justify-content:space-between;">
<span>${t("textSizeTextarea") || "Prompt / Chat Textarea"}</span>
<span id="wfm-ta-font-size-val" style="font-size:12px;color:var(--wfm-text-secondary);">${settings.textareaFontSize || TEXTAREA_FONT_SIZE_DEFAULT}px</span>
</label>
<input type="range" class="wfm-range" id="wfm-ta-font-size"
min="10" max="28" step="1"
value="${settings.textareaFontSize || TEXTAREA_FONT_SIZE_DEFAULT}"
style="width:100%;margin-top:4px;">
<small style="color:var(--wfm-text-secondary);font-size:11px;display:block;margin-top:4px;">
${t("textSizeHint") || "Applies to: Generate UI prompts, AI Assistant chat, Preset prompts, Wildcard textarea, Metadata prompt preview"}
</small>
</div>
</details>
<!-- RAW JSON Colors -->
<details class="wfm-settings-section" open>
<summary class="wfm-settings-summary">${t("jsonColorsSection") || "RAW JSON Colors"}</summary>
<div class="wfm-form-group">
<div class="wfm-color-grid" id="wfm-json-color-grid">
${JSON_COLOR_DEFS.map(d => {
const saved = settings.jsonColors?.[d.id] || d.def;
return `<div class="wfm-color-item">
<label>${d.label()}</label>
<input type="color" data-json-color="${d.id}" value="${saved}">
</div>`;
}).join("")}
</div>
<button class="wfm-btn wfm-btn-sm" id="wfm-json-color-reset" style="margin-top:8px;">${t("jsonColorReset") || "デフォルトに戻す"}</button>
</div>
</details>
<!-- Workflow Data Folder -->
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("workflowsDir")}</summary>
@@ -620,6 +709,12 @@ export async function initSettingsTab() {
<div id="wfm-settings-data-status" style="font-size:12px;margin-top:8px;"></div>
</details>
<!-- Wildcard Integration -->
<details class="wfm-settings-section" id="wfm-wildcard-integration-section">
<summary class="wfm-settings-summary">Wildcard Integration</summary>
<div id="wfm-wildcard-link-status" style="font-size:13px;color:var(--wfm-text-secondary);">Loading...</div>
</details>
<!-- Save Button -->
<button class="wfm-btn wfm-btn-primary" id="wfm-settings-save" style="min-width:120px;">${t("saveSettings")}</button>
@@ -1026,8 +1121,38 @@ export async function initSettingsTab() {
showToast(t("defaultWorkflowCleared"), "success");
});
// --- Textarea font size slider ---
document.getElementById("wfm-ta-font-size")?.addEventListener("input", (e) => {
const px = parseInt(e.target.value);
const valEl = document.getElementById("wfm-ta-font-size-val");
if (valEl) valEl.textContent = `${px}px`;
applyTextareaFontSize(px);
});
// --- JSON highlight color pickers ---
document.getElementById("wfm-json-color-grid")?.addEventListener("input", (e) => {
if (e.target.type !== "color") return;
const id = e.target.dataset.jsonColor;
const jsonColors = { ...(loadLocalSettings().jsonColors || {}), [id]: e.target.value };
saveLocalSettings({ jsonColors });
applyJsonColors(jsonColors);
});
document.getElementById("wfm-json-color-reset")?.addEventListener("click", () => {
const cur = loadLocalSettings();
delete cur.jsonColors;
localStorage.setItem(SETTINGS_KEY, JSON.stringify(cur));
applyJsonColors(undefined);
document.querySelectorAll("#wfm-json-color-grid input[type=color]").forEach(inp => {
const def = JSON_COLOR_DEFS.find(d => d.id === inp.dataset.jsonColor);
if (def) inp.value = def.def;
});
showToast(t("settingsSaved"), "success");
});
// Save (local settings)
document.getElementById("wfm-settings-save")?.addEventListener("click", () => {
const taFontSize = parseInt(document.getElementById("wfm-ta-font-size")?.value) || TEXTAREA_FONT_SIZE_DEFAULT;
const patch = {
comfyuiUrl: document.getElementById("wfm-settings-comfyui-url")?.value.trim() || "",
eagleUrl: document.getElementById("wfm-settings-eagle-url")?.value.trim() || "http://localhost:41595",
@@ -1035,6 +1160,7 @@ export async function initSettingsTab() {
uiLang: getLang(),
summaryLang: getSummaryLang(),
theme: document.documentElement.getAttribute("data-theme") || "",
textareaFontSize: taFontSize,
};
saveLocalSettings(patch);
@@ -1051,6 +1177,89 @@ export async function initSettingsTab() {
comfyUI.updateUrl(savedUrl);
}
// --- Wildcard Integration ---
async function wcLoadLinkStatus() {
const el = document.getElementById("wfm-wildcard-link-status");
if (!el) return;
try {
const res = await fetch("/api/wfm/wildcards/link-status");
const data = await res.json();
wcRenderLinkStatus(el, data);
} catch (e) {
el.innerHTML = `<span style="color:var(--wfm-error,#f44336)">Error: ${e.message}</span>`;
}
}
function wcRenderLinkStatus(el, data) {
if (!data.impact_pack_installed) {
el.innerHTML = `
<p style="margin:0 0 8px;">ComfyUI-Impact-Pack がインストールされていません。</p>
<p style="margin:0;font-size:12px;color:var(--wfm-text-secondary);">
ワイルドカードを Impact Pack と共有するには、先に
<a href="https://github.com/ltdrdata/ComfyUI-Impact-Pack" target="_blank" rel="noopener"
style="color:var(--wfm-accent);">ComfyUI-Impact-Pack</a>
をインストールしてください。
</p>`;
return;
}
if (data.is_linked) {
el.innerHTML = `
<p style="margin:0 0 4px;color:var(--wfm-success,#4caf50);">✓ リンク済み</p>
<p style="margin:0 0 8px;font-size:12px;color:var(--wfm-text-secondary);">
WFS wildcard → ${data.link_target || data.impact_pack_wildcards_dir}
</p>
<button class="wfm-btn wfm-btn-sm wfm-btn-danger" id="wfm-wc-remove-link">リンクを解除</button>
<div id="wfm-wc-link-msg" style="font-size:12px;margin-top:8px;"></div>`;
document.getElementById("wfm-wc-remove-link")?.addEventListener("click", async () => {
const msg = document.getElementById("wfm-wc-link-msg");
try {
const r = await fetch("/api/wfm/wildcards/remove-link", { method: "POST" });
const d = await r.json();
if (!r.ok) throw new Error(d.error || r.statusText);
msg.textContent = "リンクを解除しました。";
msg.style.color = "var(--wfm-success,#4caf50)";
setTimeout(wcLoadLinkStatus, 800);
} catch (e) {
msg.textContent = "エラー: " + e.message;
msg.style.color = "var(--wfm-error,#f44336)";
}
});
} else {
el.innerHTML = `
<p style="margin:0 0 4px;">Impact Pack wildcards ディレクトリ:</p>
<p style="margin:0 0 8px;font-size:12px;color:var(--wfm-text-secondary);">${data.impact_pack_wildcards_dir}</p>
<p style="margin:0 0 8px;font-size:12px;color:var(--wfm-text-secondary);">
WFS の wildcard ディレクトリを Impact Pack の wildcards ディレクトリへのリンクに置き換えます。
既存の WFS ワイルドカードファイルは Impact Pack ディレクトリへ移行されます。
</p>
<button class="wfm-btn wfm-btn-sm" id="wfm-wc-create-link">リンクを作成</button>
<div id="wfm-wc-link-msg" style="font-size:12px;margin-top:8px;"></div>`;
document.getElementById("wfm-wc-create-link")?.addEventListener("click", async () => {
const msg = document.getElementById("wfm-wc-link-msg");
try {
const r = await fetch("/api/wfm/wildcards/create-link", { method: "POST" });
const d = await r.json();
if (!r.ok) throw new Error(d.error || r.statusText);
const migrated = d.migrated_files?.length
? ` (移行: ${d.migrated_files.join(", ")})`
: "";
msg.textContent = "リンクを作成しました。" + migrated;
msg.style.color = "var(--wfm-success,#4caf50)";
setTimeout(wcLoadLinkStatus, 800);
} catch (e) {
msg.textContent = "エラー: " + e.message;
msg.style.color = "var(--wfm-error,#f44336)";
}
});
}
}
// Load wildcard link status when section is opened
document.getElementById("wfm-wildcard-integration-section")?.addEventListener("toggle", (e) => {
if (e.target.open) wcLoadLinkStatus();
});
// --- Data Export ---
document.getElementById("wfm-settings-export")?.addEventListener("click", async () => {
const statusEl = document.getElementById("wfm-settings-data-status");
+498 -55
View File
@@ -20,9 +20,11 @@
<button class="wfm-tab" data-tab="models">Models</button>
<button class="wfm-tab" data-tab="generate">GenerateUI</button>
<button class="wfm-tab" data-tab="prompt">Prompt</button>
<button class="wfm-tab" data-tab="metadata">Metadata</button>
<button class="wfm-tab" data-tab="gallery">Gallery</button>
<button class="wfm-tab" data-tab="settings">Settings</button>
<button class="wfm-tab" data-tab="help">Help</button>
<button class="wfm-tab" data-tab="ai">AI TOOL</button>
</nav>
</header>
@@ -264,6 +266,7 @@
<button class="wfm-gen-subtab-btn active" data-subtab="input">Input</button>
<button class="wfm-gen-subtab-btn" data-subtab="model">Model</button>
<button class="wfm-gen-subtab-btn" data-subtab="settings">Settings</button>
<button class="wfm-gen-subtab-btn" data-subtab="feeder">Feeder</button>
</div>
<!-- Tab: Input — left sub-col: Prompt(top)+Image(bottom), right sub-col: Raw JSON -->
@@ -310,6 +313,121 @@
<div class="wfm-gen-rawjson-col" id="wfm-gen-rawjson-col-settings"></div>
</div>
</div>
<!-- Tab: Feeder — full-width: settings(left) + library(center) -->
<div id="wfm-gen-subtab-feeder" class="wfm-gen-subtab-content">
<div class="wfm-feeder-layout">
<!-- Left: Node selector + params + presets -->
<div class="wfm-feeder-settings">
<div class="wfm-feeder-pane-header">ImageFeeder Node</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px;">
<select id="wfm-feeder-node-sel" class="wfm-select" style="flex:1;min-width:0;"></select>
<button id="wfm-feeder-apply-btn" class="wfm-btn wfm-btn-primary" style="white-space:nowrap;">Apply</button>
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">Directory</label>
<input type="text" id="wfm-feeder-dir-input" class="wfm-input wfm-feeder-input" placeholder="e.g. subdir">
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">Sort Mode</label>
<select id="wfm-feeder-sort" class="wfm-select wfm-feeder-input">
<option value="ascending">ascending</option>
<option value="descending">descending</option>
<option value="random">random</option>
</select>
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">Index</label>
<input type="number" id="wfm-feeder-index" class="wfm-input wfm-feeder-input" value="0" min="0">
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">Start Index</label>
<input type="number" id="wfm-feeder-start" class="wfm-input wfm-feeder-input" value="0" min="0">
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">End Index</label>
<input type="number" id="wfm-feeder-end" class="wfm-input wfm-feeder-input" value="0" min="0">
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">Batch Size</label>
<input type="number" id="wfm-feeder-batch" class="wfm-input wfm-feeder-input" value="1" min="1">
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">Seed</label>
<input type="number" id="wfm-feeder-seed" class="wfm-input wfm-feeder-input" value="0">
</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">Use Selection</label>
<input type="checkbox" id="wfm-feeder-use-sel" checked style="width:16px;height:16px;accent-color:var(--wfm-primary);">
</div>
<div class="wfm-feeder-pane-header" style="margin-top:12px;">Presets</div>
<div style="display:flex;gap:6px;margin-bottom:6px;">
<input type="text" id="wfm-feeder-preset-name" class="wfm-input" style="flex:1;min-width:0;" placeholder="Preset name">
<button id="wfm-feeder-preset-save" class="wfm-btn wfm-btn-sm" style="white-space:nowrap;">Save</button>
</div>
<div style="display:flex;gap:6px;">
<select id="wfm-feeder-preset-sel" class="wfm-select" style="flex:1;min-width:0;"></select>
<button id="wfm-feeder-preset-load" class="wfm-btn wfm-btn-sm wfm-btn-primary" style="white-space:nowrap;">Load</button>
<button id="wfm-feeder-preset-del" class="wfm-btn wfm-btn-sm wfm-btn-danger" style="white-space:nowrap;">Del</button>
</div>
<div class="wfm-feeder-pane-header" style="margin-top:12px;">Run</div>
<div class="wfm-feeder-field-row">
<label class="wfm-feeder-label">After gen</label>
<select id="wfm-feeder-control-after" class="wfm-select wfm-feeder-input">
<option value="loop">Loop</option>
<option value="increment">Increment</option>
<option value="fixed">Fixed</option>
</select>
</div>
<div style="display:flex;gap:8px;margin-top:4px;">
<button id="wfm-feeder-run-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">▶ Run</button>
<button id="wfm-feeder-stop-btn" class="wfm-btn wfm-btn-danger" style="flex:1;" disabled>■ Stop</button>
</div>
</div>
<!-- Center: Image Library -->
<div class="wfm-feeder-library">
<!-- Status bar -->
<div id="wfm-feeder-status" class="wfm-feeder-status-bar">Select a folder</div>
<div class="wfm-feeder-library-body">
<!-- Folder tree -->
<div class="wfm-feeder-tree-pane">
<div class="wfm-feeder-pane-header" style="display:flex;align-items:center;justify-content:space-between;">
<span>Folders</span>
<button id="wfm-feeder-reload-tree" class="wfm-btn wfm-btn-sm" title="Reload">↺</button>
</div>
<div id="wfm-feeder-tree" class="wfm-feeder-tree-scroll">
<div class="wfm-feeder-tree-msg">Loading...</div>
</div>
</div>
<!-- Image grid -->
<div class="wfm-feeder-grid-pane">
<div class="wfm-feeder-pane-header" style="display:flex;align-items:center;justify-content:space-between;">
<span>Images</span>
<div style="display:flex;gap:4px;">
<button id="wfm-feeder-sel-all" class="wfm-btn wfm-btn-sm">All</button>
<button id="wfm-feeder-desel-all" class="wfm-btn wfm-btn-sm">None</button>
</div>
</div>
<div id="wfm-feeder-grid" class="wfm-feeder-grid">
<div class="wfm-feeder-grid-msg">Select a folder</div>
</div>
</div>
<!-- Preview -->
<div class="wfm-feeder-preview-pane">
<div class="wfm-feeder-pane-header">Preview</div>
<img id="wfm-feeder-preview-img" class="wfm-feeder-preview-img" style="display:none;" alt="">
<div id="wfm-feeder-preview-name" class="wfm-feeder-preview-name"></div>
<pre id="wfm-feeder-preview-info" class="wfm-feeder-preview-info"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Generation Controls & Results -->
@@ -352,12 +470,23 @@
</div>
<div id="wfm-ckpt-batch-body" style="display:none;margin-top:8px;">
<div style="margin-bottom:6px;">
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-bottom:3px;">Include Folders (empty = all, comma-separated)</div>
<input type="text" id="wfm-ckpt-batch-include" class="wfm-input" placeholder="e.g. sdxl, flux">
</div>
<div style="margin-bottom:6px;">
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-bottom:3px;">Exclude Folders (comma-separated)</div>
<input type="text" id="wfm-ckpt-batch-exclude" class="wfm-input" placeholder="e.g. old, test">
<div style="font-size:11px;color:var(--wfm-text-secondary);margin-bottom:3px;">Checkpoints</div>
<div class="wfm-ckpt-dropdown-wrap" id="wfm-ckpt-dropdown-wrap">
<button type="button" id="wfm-ckpt-dropdown-btn" class="wfm-ckpt-dropdown-trigger">
<span id="wfm-ckpt-dropdown-label">All checkpoints</span>
<span class="wfm-ckpt-dropdown-arrow">&#9660;</span>
</button>
<div id="wfm-ckpt-dropdown-panel" class="wfm-ckpt-dropdown-panel" style="display:none;">
<div style="padding:6px 8px 4px;">
<input type="text" id="wfm-ckpt-search" class="wfm-input" style="height:28px;padding:4px 8px;font-size:11px;" placeholder="Filter...">
</div>
<div style="display:flex;gap:4px;padding:0 8px 4px;">
<button type="button" id="wfm-ckpt-select-all" class="wfm-btn wfm-btn-sm" style="flex:1;font-size:10px;">All</button>
<button type="button" id="wfm-ckpt-deselect-all" class="wfm-btn wfm-btn-sm" style="flex:1;font-size:10px;">None</button>
</div>
<div id="wfm-ckpt-list" class="wfm-ckpt-dropdown-list"></div>
</div>
</div>
</div>
<div id="wfm-ckpt-batch-info" style="font-size:11px;color:var(--wfm-text-secondary);"></div>
<div id="wfm-ckpt-batch-progress" style="margin-top:8px;display:none;">
@@ -368,6 +497,9 @@
<div class="wfm-gen-progress-track">
<div id="wfm-ckpt-batch-bar" class="wfm-gen-progress-bar" style="background:var(--wfm-warning,#f59e0b);"></div>
</div>
<div style="margin-top:6px;">
<button id="wfm-ckpt-batch-pause-btn" class="wfm-btn wfm-btn-sm" style="width:100%;">Pause</button>
</div>
</div>
</div>
</div>
@@ -386,8 +518,16 @@
<span class="wfm-gen-rawjson-label">Raw JSON</span>
<button id="wfm-gen-apply-raw-btn" class="wfm-btn wfm-btn-sm">Apply</button>
</div>
<div class="wfm-rawjson-search">
<input type="text" id="wfm-gen-raw-search" class="wfm-rawjson-search-input" placeholder="Search...">
<span id="wfm-gen-raw-search-count" class="wfm-rawjson-search-count"></span>
<button id="wfm-gen-raw-search-prev" class="wfm-rawjson-search-btn" title="Previous match">↑</button>
<button id="wfm-gen-raw-search-next" class="wfm-rawjson-search-btn" title="Next match">↓</button>
<button id="wfm-gen-raw-search-clear" class="wfm-rawjson-search-btn wfm-rawjson-search-clear" title="Clear search">✕</button>
</div>
<div class="wfm-json-container wfm-gen-rawjson-editor">
<pre id="wfm-gen-raw-json-highlight" class="wfm-json-highlight"></pre>
<pre id="wfm-gen-raw-json-search-overlay" class="wfm-json-highlight wfm-json-search-overlay"></pre>
<textarea id="wfm-gen-raw-json" class="wfm-json-editor" spellcheck="false" placeholder="Paste workflow JSON here..."></textarea>
</div>
</div>
@@ -434,61 +574,209 @@
</div>
</div>
<!-- Col 2: Preset Editor -->
<!-- Col 2: Presets + Preset Manager (tabbed) -->
<div class="wfm-prompt-split-col">
<div class="wfm-prompt-split-header">Presets</div>
<div class="wfm-preset-panel">
<div style="display:flex;gap:6px;margin-bottom:10px;align-items:center;">
<select id="wfm-preset-select" class="wfm-select" style="flex:1;">
<option value="">New Preset (none selected)</option>
</select>
<button id="wfm-preset-apply-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">GenUI Set</button>
<!-- Center column tab navigation -->
<div class="wfm-prompt-center-tabnav">
<button class="wfm-prompt-center-tab active" data-ptab="presets">Presets</button>
<button class="wfm-prompt-center-tab" data-ptab="manager">Preset Manager</button>
</div>
<!-- Presets pane -->
<div id="wfm-ptab-presets" class="wfm-prompt-center-pane">
<div class="wfm-preset-panel">
<div style="display:flex;gap:6px;margin-bottom:10px;align-items:center;">
<select id="wfm-preset-select" class="wfm-select" style="flex:1;">
<option value="">New Preset (none selected)</option>
</select>
<button id="wfm-preset-apply-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">GenUI Set</button>
</div>
<div class="wfm-form-group">
<label>Preset Name</label>
<input type="text" id="wfm-preset-name" class="wfm-input" placeholder="Enter preset name...">
</div>
<div class="wfm-form-group">
<label>Category</label>
<input type="text" id="wfm-preset-category" class="wfm-input" placeholder="e.g. Portrait, Landscape...">
</div>
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
<label>Positive Prompt</label>
<textarea id="wfm-preset-pos" class="wfm-textarea" style="flex:1;" placeholder="Positive prompt..."></textarea>
</div>
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
<label>Negative Prompt</label>
<textarea id="wfm-preset-neg" class="wfm-textarea" style="flex:1;" placeholder="Negative prompt..."></textarea>
</div>
<div style="display:flex; gap:4px;">
<button id="wfm-preset-copy-pos-btn" class="wfm-btn" style="flex:2;" title="Copy Positive Prompt">PP Copy</button>
<button id="wfm-preset-copy-neg-btn" class="wfm-btn" style="flex:2;" title="Copy Negative Prompt">NP Copy</button>
<button id="wfm-preset-save-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">Save</button>
</div>
<div style="display:flex; gap:4px; margin-top:4px;">
<select id="wfm-preset-group-select" class="wfm-select" style="flex:1;">
<option value="">Select group...</option>
</select>
<button id="wfm-preset-add-to-group-btn" class="wfm-btn wfm-btn-sm">Add to Group</button>
<button id="wfm-preset-new-group-btn" class="wfm-btn wfm-btn-sm">+ Group</button>
<button id="wfm-preset-del-group-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" title="Delete selected group">Del Group</button>
</div>
</div>
<div class="wfm-form-group">
<label>Preset Name</label>
<input type="text" id="wfm-preset-name" class="wfm-input" placeholder="Enter preset name...">
</div>
<div class="wfm-form-group">
<label>Category</label>
<input type="text" id="wfm-preset-category" class="wfm-input" placeholder="e.g. Portrait, Landscape...">
</div>
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
<label>Positive Prompt</label>
<textarea id="wfm-preset-pos" class="wfm-textarea" style="flex:1;" placeholder="Positive prompt..."></textarea>
</div>
<div class="wfm-form-group" style="flex:1;min-height:0;display:flex;flex-direction:column;">
<label>Negative Prompt</label>
<textarea id="wfm-preset-neg" class="wfm-textarea" style="flex:1;" placeholder="Negative prompt..."></textarea>
</div>
<div style="display:flex; gap:4px;">
<button id="wfm-preset-copy-pos-btn" class="wfm-btn" style="flex:2;" title="Copy Positive Prompt">PP Copy</button>
<button id="wfm-preset-copy-neg-btn" class="wfm-btn" style="flex:2;" title="Copy Negative Prompt">NP Copy</button>
<button id="wfm-preset-save-btn" class="wfm-btn wfm-btn-primary" style="flex:1;">Save</button>
</div>
<div style="display:flex; gap:4px; margin-top:4px;">
<select id="wfm-preset-group-select" class="wfm-select" style="flex:1;">
<option value="">Select group...</option>
</select>
<button id="wfm-preset-add-to-group-btn" class="wfm-btn wfm-btn-sm">Add to Group</button>
<button id="wfm-preset-new-group-btn" class="wfm-btn wfm-btn-sm">+ Group</button>
<button id="wfm-preset-del-group-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" title="Delete selected group">Del Group</button>
</div>
<!-- Preset Manager pane -->
<div id="wfm-ptab-manager" class="wfm-prompt-center-pane" style="display:none;">
<div class="wfm-preset-manager">
<div class="wfm-pm-tabs">
<button class="wfm-pm-tab active" data-pmtab="all">All</button>
<button class="wfm-pm-tab" data-pmtab="favorites">&#9733;</button>
<button class="wfm-pm-tab" data-pmtab="groups">Groups</button>
</div>
<div class="wfm-pm-search">
<input type="text" id="wfm-pm-search-input" class="wfm-input" placeholder="Search presets...">
</div>
<div id="wfm-pm-list" class="wfm-pm-list"></div>
</div>
</div>
</div>
<!-- Col 3: Preset Manager (list, favorites, groups) -->
<!-- Col 3: Wildcard support -->
<div class="wfm-prompt-split-col">
<div class="wfm-prompt-split-header">Preset Manager</div>
<div class="wfm-preset-manager">
<div class="wfm-pm-tabs">
<button class="wfm-pm-tab active" data-pmtab="all">All</button>
<button class="wfm-pm-tab" data-pmtab="favorites">&#9733;</button>
<button class="wfm-pm-tab" data-pmtab="groups">Groups</button>
<div class="wfm-prompt-split-header">Wildcard</div>
<div class="wfm-wc-panel">
<!-- Input assistance section -->
<div class="wfm-wc-input-section">
<!-- Wildcard syntax buttons -->
<div class="wfm-wc-toolbar">
<button class="wfm-wc-btn" data-wc-open="{" data-wc-close="}" title="Random pick set e.g. {cat|dog}">{ }</button>
<button class="wfm-wc-btn" data-wc-open="|" title="Separator">|</button>
<button class="wfm-wc-btn" data-wc-open="__" data-wc-close="__" title="Wildcard file reference e.g. __animals__">__</button>
<button class="wfm-wc-btn" data-wc-open=":" title="Colon">:</button>
<button class="wfm-wc-btn" data-wc-open=";" title="Semicolon">;</button>
<button class="wfm-wc-btn" data-wc-open="$$" title="Multi-pick e.g. {2$$cat|dog|bird}">$$</button>
<button class="wfm-wc-btn" data-wc-open="[" data-wc-close="]" title="Bracket set e.g. [cat|dog]">[ ]</button>
<button class="wfm-wc-btn" id="wfm-wc-lora-btn" title="LoRA with LBW e.g. &lt;lora:name:1:LBW=...;...&gt;">&lt;lora&gt;</button>
<button class="wfm-wc-btn" id="wfm-wc-nset-btn" title="Multi-pick set e.g. {2$$cat|dog|bird}">n$${ }</button>
<div class="wfm-wc-file-picker-wrap">
<button class="wfm-wc-btn" id="wfm-wc-filepick-btn" title="Insert wildcard file reference">&#128194; File</button>
<div id="wfm-wc-file-picker" class="wfm-wc-file-picker" style="display:none;"></div>
</div>
</div>
<!-- Prompt textarea -->
<textarea id="wfm-wc-prompt" class="wfm-textarea wfm-wc-prompt-ta" rows="4" placeholder="Compose wildcard prompt here...&#10;e.g. a photo of {cat|dog}, __colors__ background"></textarea>
<!-- Action buttons -->
<div class="wfm-wc-prompt-actions">
<button id="wfm-wc-copy-btn" class="wfm-btn wfm-btn-sm">Copy</button>
<button id="wfm-wc-to-pos-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary" title="Send to Preset Positive Prompt">&#8594; Pos</button>
<button id="wfm-wc-to-neg-btn" class="wfm-btn wfm-btn-sm" title="Send to Preset Negative Prompt">&#8594; Neg</button>
<button id="wfm-wc-clear-btn" class="wfm-btn wfm-btn-sm" style="margin-left:auto;">Clear</button>
</div>
</div>
<div class="wfm-pm-search">
<input type="text" id="wfm-pm-search-input" class="wfm-input" placeholder="Search presets...">
<!-- Wildcard file manager section -->
<div class="wfm-wc-files-section">
<div class="wfm-wc-files-header">
<span class="wfm-wc-files-title">Wildcard Files</span>
<div style="display:flex;gap:4px;">
<button id="wfm-wc-new-file-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">+ New</button>
<button id="wfm-wc-files-refresh-btn" class="wfm-btn wfm-btn-sm" title="Reload file list">&#8635;</button>
</div>
</div>
<!-- File list -->
<div id="wfm-wc-file-list" class="wfm-wc-file-list">
<p class="wfm-placeholder" style="padding:12px;font-size:12px;">Loading...</p>
</div>
<!-- Editor (shown when a file is selected or New is clicked) -->
<div id="wfm-wc-editor" class="wfm-wc-editor" style="display:none;">
<div class="wfm-wc-editor-header">
<input type="text" id="wfm-wc-editor-name" class="wfm-input" placeholder="name or folder/name" style="flex:1;min-width:0;">
<select id="wfm-wc-editor-ext" class="wfm-select" style="width:70px;">
<option value="txt">txt</option>
<option value="yaml">yaml</option>
</select>
</div>
<div class="wfm-wc-editor-hint">
txt: one item per line &nbsp;|&nbsp; yaml: key: [item1, item2, ...]
</div>
<textarea id="wfm-wc-editor-content" class="wfm-textarea wfm-wc-editor-ta" rows="8" placeholder="one item per line (txt)&#10;or YAML format (yaml)"></textarea>
<div class="wfm-wc-editor-actions">
<button id="wfm-wc-editor-save-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary">Save</button>
<button id="wfm-wc-editor-delete-btn" class="wfm-btn wfm-btn-sm wfm-btn-danger" style="display:none;">Delete</button>
<button id="wfm-wc-editor-cancel-btn" class="wfm-btn wfm-btn-sm" style="margin-left:auto;">Cancel</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Metadata Tab -->
<section id="wfm-tab-metadata" class="wfm-tab-content">
<div class="wfm-metadata-layout">
<!-- Col1: Drop zone + preview -->
<div class="wfm-metadata-col1">
<div class="wfm-metadata-drop-zone" id="wfm-meta-drop">
<div class="wfm-meta-drop-label" id="wfm-meta-drop-label">
<div class="wfm-meta-drop-main">Drop image / JSON</div>
<div class="wfm-meta-drop-sub">PNG · WebP · JSON</div>
</div>
<img id="wfm-meta-preview-img" class="wfm-meta-preview-img" style="display:none;" alt="preview">
</div>
<div class="wfm-meta-file-info" id="wfm-meta-file-info"></div>
<div class="wfm-meta-format-note" id="wfm-meta-format-note">
<div class="wfm-meta-format-note-title" id="wfm-meta-format-note-title">Supported formats</div>
<ul class="wfm-meta-format-list">
<li id="wfm-meta-fmt-comfyui">ComfyUI — PNG / WebP / JSON</li>
<li id="wfm-meta-fmt-sdwebui">SD WebUI / SD Forge — PNG (parameters)</li>
<li id="wfm-meta-fmt-fooocus">Fooocus — PNG (fooocus_scheme)</li>
</ul>
<div class="wfm-meta-format-todo" id="wfm-meta-format-todo">Flux.2 / Qwen-Image / Z-Image subgraph workflows supported</div>
</div>
<input type="file" id="wfm-meta-file-input" accept="image/png,image/webp,.json,application/json" style="display:none;">
</div>
<!-- Col2: Model info -->
<div class="wfm-metadata-col2">
<div class="wfm-meta-section" id="wfm-meta-ckpt-section">
<div class="wfm-meta-section-title">Checkpoint</div>
<div class="wfm-meta-list" id="wfm-meta-ckpt-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-vae-section">
<div class="wfm-meta-section-title">VAE</div>
<div class="wfm-meta-list" id="wfm-meta-vae-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-diff-section">
<div class="wfm-meta-section-title">Diffusion Model</div>
<div class="wfm-meta-list" id="wfm-meta-diff-list"></div>
</div>
<div class="wfm-meta-section" id="wfm-meta-te-section">
<div class="wfm-meta-section-title">Text Encoder</div>
<div class="wfm-meta-list" id="wfm-meta-te-list"></div>
</div>
</div>
<!-- Col3: LoRA + Prompt -->
<div class="wfm-metadata-col3">
<div class="wfm-meta-section" id="wfm-meta-lora-section">
<div class="wfm-meta-section-title">LoRA</div>
<div class="wfm-meta-list" id="wfm-meta-lora-list"></div>
</div>
<div class="wfm-meta-section wfm-meta-prompt-section" id="wfm-meta-prompt-section">
<div class="wfm-meta-section-title">Prompt</div>
<div class="wfm-meta-list" id="wfm-meta-prompt-list"></div>
<div class="wfm-meta-prompt-full-wrap">
<div class="wfm-meta-prompt-full-label" id="wfm-meta-prompt-full-label"></div>
<textarea class="wfm-meta-prompt-full" id="wfm-meta-prompt-full" readonly placeholder="Select a prompt above to view full text..."></textarea>
<div class="wfm-meta-prompt-actions">
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-copy-btn">Copy</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-pos-btn">GenUI:P</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-genui-neg-btn">GenUI:N</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-pos-btn">Prompt:P</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-meta-preset-neg-btn">Prompt:N</button>
</div>
</div>
<div id="wfm-pm-list" class="wfm-pm-list"></div>
</div>
</div>
</div>
@@ -534,15 +822,31 @@
<ul>
<li id="wfm-help-gen-1">Connect/disconnect ComfyUI and check connection status</li>
<li id="wfm-help-gen-2">Load workflows from Workflow tab (App format .app.json not supported)</li>
<li id="wfm-help-gen-3">3-tab layout — Input (Prompt + Image), Model, Settings — each with a Raw JSON column for instant preview and direct editing</li>
<li id="wfm-help-gen-3">4-tab layout — Input (Prompt + Image), Model, Settings, Feeder — Input/Model/Settings each include a Raw JSON column for instant preview and direct editing</li>
<li id="wfm-help-gen-4">Input tab: Prompt (top) and Image drag-and-drop (bottom) in the left column</li>
<li id="wfm-help-gen-5">Model tab: Checkpoint, VAE, LoRA, ControlNet, UNET, TextEncoder selectors with filter</li>
<li id="wfm-help-gen-6">Settings tab: KSampler (top) and LatentImage (bottom) in the left column</li>
<li id="wfm-help-gen-6">Settings tab: KSampler and Latent Image side by side, each at 50% width</li>
<li id="wfm-help-gen-7">Raw JSON always visible in the right column of each tab — edit directly and Apply to reload the workflow</li>
<li id="wfm-help-gen-8">Seed control: random, fixed, increment, decrement — seed input and mode selector stacked vertically for readability</li>
<li id="wfm-help-gen-9">Generate, progress display, stop, and view results</li>
<li id="wfm-help-gen-10">Eagle integration: auto-save generated images to Eagle</li>
<li id="wfm-help-gen-11">Checkpoint Batch: enable via checkbox in the right panel to auto-generate once per checkpoint model; filter by include folders (comma-separated), exclude folders, or leave blank to use all checkpoints; Stop button aborts the loop after the current generation; per-model progress bar shown in amber</li>
<li id="wfm-help-gen-11">Checkpoint Batch: select checkpoints by folder from the dropdown — check a folder to select all its files, or expand with ▶ to check individual files; use Filter to search; Pause/Resume suspends and continues between models; Stop aborts after the current generation</li>
</ul>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-feeder-title">GenerateUI Tab — Feeder subtab</h3>
<p id="wfm-help-feeder-desc">Requires the comfyui-image-feeder custom node. Controls ImageFeeder nodes and runs continuous generation loops over an image library stored in user/default/image-loop-data/.</p>
<ul>
<li id="wfm-help-feeder-1">Left pane — Node settings: select the target ImageFeeder node from the dropdown; edit all node parameters (Directory, Sort Mode, Index, Start/End Index, Batch Size, Seed, Use Selection); click Apply to write changes to the loaded workflow</li>
<li id="wfm-help-feeder-2">Left pane — Presets: save the current directory + selection as a named preset; load or delete saved presets</li>
<li id="wfm-help-feeder-3">Left pane — Run: After gen mode determines index behavior after each generation — Loop (wrap back to 0 and continue indefinitely), Increment (advance index and stop when all images are exhausted), Fixed (always use the same index); click Run to start the loop, Stop to interrupt</li>
<li id="wfm-help-feeder-4">Center pane — Folder tree: browse subfolders inside image-loop-data; click a folder to load its images; click ↺ to reload the tree</li>
<li id="wfm-help-feeder-5">Center pane — Image grid: displays thumbnails of all images in the selected folder; check individual images to add them to the selection; All / None buttons select or clear the entire folder; click an image to open the preview panel</li>
<li id="wfm-help-feeder-6">Center pane — Preview panel: shows the clicked image at larger size with filename, resolution, and file size</li>
<li id="wfm-help-feeder-7">Status bar: shows the current folder path and how many images are selected out of the total in the folder</li>
<li id="wfm-help-feeder-8">Seed during Run: uses the right-pane seed setting (Random / Fixed / Increment / Decrement) — the ImageFeeder's own Seed field controls random sort order only</li>
<li id="wfm-help-feeder-9">Index sync: after each generation the node returns the next index via WebSocket; the Index field updates automatically so the display stays in sync</li>
</ul>
</div>
@@ -556,6 +860,20 @@
<li id="wfm-help-prompt-5">Preset Manager: browse all presets, favorites, and group-based filtering</li>
<li id="wfm-help-prompt-6">Group management: create groups, assign/remove presets, delete groups</li>
<li id="wfm-help-prompt-7">GenUI Set: apply preset prompts directly to GenerateUI</li>
<li id="wfm-help-prompt-8">Wildcard toolbar: one-click buttons to insert wildcard syntax ({|}, {n$|}, __|__, &lt;lora:&gt;, etc.); wraps selected text</li>
<li id="wfm-help-prompt-9">Wildcard file manager: create/edit/delete .txt/.yaml wildcard files; click filename to insert __filename__ at cursor</li>
</ul>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-metadata-title">Metadata Tab</h3>
<p id="wfm-help-metadata-desc">Drop a ComfyUI-generated PNG/WebP or workflow JSON to instantly extract and view the embedded model and prompt information.</p>
<ul>
<li id="wfm-help-metadata-1">Left column: drop zone (image preview for PNG/WebP) and file info; click or drop to load</li>
<li id="wfm-help-metadata-2">Center column: Checkpoint, VAE, Diffusion Model, and Text Encoder lists extracted from the workflow</li>
<li id="wfm-help-metadata-3">Right column: LoRA list (with strength values) and Prompt list (click to view full text below)</li>
<li id="wfm-help-metadata-4">Prompt actions — Copy: copy to clipboard; GenUI:P/N: set GenerateUI positive/negative prompt; Prompt:P/N: set Prompt tab preset positive/negative</li>
<li id="wfm-help-metadata-5">Supported: ComfyUI PNG/WebP/JSON (standard + Flux.2 / Qwen-Image / Z-Image subgraph workflows), SD WebUI, SD Forge, Fooocus. Model extraction covers UNETLoader, UnetLoaderGGUF, UNETLoaderGGUF (e.g. HiDream GGUF), CLIPLoader, DualCLIPLoader, TripleCLIPLoader, and QuadrupleCLIPLoader (e.g. HiDream 4-CLIP).</li>
</ul>
</div>
@@ -571,6 +889,9 @@
<li id="wfm-help-settings-7">Eagle integration URL and auto-save toggle</li>
<li id="wfm-help-settings-8">Data Management: export all plugin data to JSON / import to restore</li>
<li id="wfm-help-settings-9">All sections are collapsible accordion panels</li>
<li id="wfm-help-settings-10">Wildcard Integration: link WFS wildcard dir to ComfyUI-Impact-Pack wildcards (junction/symlink); existing files migrated automatically</li>
<li id="wfm-help-settings-11">Text Size: one slider (10–28 px) adjusts font size for all prompt and chat textareas at once — Generate UI positive/negative prompts, AI Assistant chat input, Preset positive/negative, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings</li>
<li id="wfm-help-settings-12">RAW JSON Colors: customize the 6 syntax highlight colors for the Raw JSON editor in Generate UI — Default Text, Name/Scheduler, Title, Width/Height, Prompt/Text, Image/File; changes apply immediately on color pick; Reset Defaults restores the original color scheme</li>
</ul>
</div>
@@ -635,6 +956,21 @@
<li id="wfm-help-sidepanel-9">Double-click items to load/place without dragging</li>
<li id="wfm-help-sidepanel-10">Search within each sub-tab to quickly find items</li>
<li id="wfm-help-sidepanel-12">⚙ Theme button (header): customize panel background, text, border colors — saved to localStorage</li>
<li id="wfm-help-sidepanel-13">I tab (Information): drop a ComfyUI PNG/WebP/JSON to inspect model, LoRA, and prompt metadata in the side panel — sub-tabs: model / lora / Prompts; supports UnetLoaderGGUF and QuadrupleCLIPLoader node types</li>
<li id="wfm-help-sidepanel-14">Information tab — drag items to canvas: model sub-tab places the loader node (Checkpoint → CheckpointLoaderSimple, VAE → VAELoader, Diffusion Model → UNETLoader, Text Encoder → CLIPLoader); double-click also places at canvas center</li>
<li id="wfm-help-sidepanel-15">Information tab — LoRA sub-tab: drag individual LoRA to place LoraLoader; "Multiple LORA" section at bottom places all LoRAs in a single Lora Loader (LoraManager) node; Prompts sub-tab: drag a prompt to place CLIPTextEncode node with text pre-filled; click prompt to view full text and copy</li>
<li id="wfm-help-sidepanel-16">AI TOOL tab: Translation and TOOLS (VLM) panes powered by Ollama or LM Studio; 3-pane layout (Translation | TOOLS | Settings) always visible; settings (backend, URL, model) are shared with the SPA AI TOOL tab via localStorage</li>
</ul>
</div>
<div class="wfm-help-card">
<h3 id="wfm-help-ai-title">AI TOOL Tab</h3>
<ul>
<li id="wfm-help-ai-1">3-pane layout — Translation (40%) | TOOLS (40%) | Settings (20%) — all panes are always visible simultaneously</li>
<li id="wfm-help-ai-2">Translation pane: translate text between Japanese, English, Chinese, or a custom Free language using Ollama or LM Studio; language selectors with ⇄ swap button; text content is also swapped on swap; selections are saved automatically</li>
<li id="wfm-help-ai-3">TOOLS pane (VLM): drop an image into the drop zone and select a task — Describe Image or Create Prompt — then click Run to analyze with a vision model; result displayed with Copy button</li>
<li id="wfm-help-ai-4">Settings pane: choose backend (Ollama / LM Studio), set the API URL, test connection, select model, and configure Free language names for translation</li>
<li id="wfm-help-ai-5">Settings are saved to localStorage and shared with the Library panel's AI TOOL tab</li>
</ul>
</div>
@@ -816,6 +1152,113 @@
</div>
</div>
</section>
<!-- AI Tab -->
<section id="wfm-tab-ai" class="wfm-tab-content">
<!-- Translation pane -->
<div class="wfm-ai-pane wfm-ai-pane-translate">
<div class="wfm-ai-pane-header">Translation</div>
<div class="wfm-ai-trans-container">
<div class="wfm-ai-trans-lang-row">
<select id="wfm-ai-src-lang" class="wfm-select wfm-ai-lang-select">
<option value="ja">Japanese</option>
<option value="en">English</option>
<option value="zh">Chinese</option>
<option value="free">Free</option>
</select>
<button id="wfm-ai-trans-swap-btn" class="wfm-btn wfm-ai-swap-btn" title="Swap languages">⇄</button>
<select id="wfm-ai-dst-lang" class="wfm-select wfm-ai-lang-select">
<option value="en">English</option>
<option value="ja">Japanese</option>
<option value="zh">Chinese</option>
<option value="free">Free</option>
</select>
</div>
<textarea id="wfm-ai-trans-input" class="wfm-ai-trans-textarea" placeholder="Enter text to translate..."></textarea>
<div class="wfm-ai-trans-actions">
<button id="wfm-ai-trans-btn" class="wfm-btn wfm-btn-primary">Translate</button>
<button id="wfm-ai-trans-copy-btn" class="wfm-btn">Copy</button>
<span id="wfm-ai-trans-status" class="wfm-ai-trans-status"></span>
</div>
<textarea id="wfm-ai-trans-output" class="wfm-ai-trans-textarea wfm-ai-trans-output" placeholder="Translation result..." readonly></textarea>
</div>
</div>
<div class="wfm-ai-pane-divider"></div>
<!-- TOOLS pane -->
<div class="wfm-ai-pane wfm-ai-pane-tools">
<div class="wfm-ai-pane-header">TOOLS</div>
<div class="wfm-ai-vlm-container">
<div class="wfm-ai-vlm-drop" id="wfm-ai-vlm-drop">
<img id="wfm-ai-vlm-preview" style="display:none;max-width:100%;max-height:100%;object-fit:contain;pointer-events:none;">
<span id="wfm-ai-vlm-label" class="wfm-ai-vlm-label">Drop or click PNG / JPG / WebP</span>
<input type="file" id="wfm-ai-vlm-file" accept="image/png,image/jpeg,image/webp" style="display:none;">
</div>
<div class="wfm-ai-vlm-task-row">
<select id="wfm-ai-vlm-task" class="wfm-select">
<option value="describe">Describe image</option>
<option value="prompt">Create prompt</option>
<option value="tags">Create tags</option>
</select>
<button id="wfm-ai-vlm-run" class="wfm-btn wfm-btn-primary">Run</button>
</div>
<span id="wfm-ai-vlm-status" class="wfm-ai-trans-status"></span>
<textarea id="wfm-ai-vlm-result" class="wfm-ai-vlm-result" placeholder="Result..." readonly></textarea>
<button id="wfm-ai-vlm-copy" class="wfm-btn wfm-ai-vlm-copy-btn">Copy</button>
</div>
</div>
<div class="wfm-ai-pane-divider"></div>
<!-- Settings pane -->
<div class="wfm-ai-pane wfm-ai-pane-settings">
<div class="wfm-ai-pane-header">Settings</div>
<div class="wfm-ai-settings-container">
<div class="wfm-ai-settings-section">
<div class="wfm-ai-settings-title" id="wfm-ai-settings-backend-title">Backend</div>
<div class="wfm-ai-backend-row">
<label class="wfm-ai-radio-label">
<input type="radio" name="wfm-ai-backend" value="ollama" checked> Ollama
</label>
<label class="wfm-ai-radio-label">
<input type="radio" name="wfm-ai-backend" value="lmstudio"> LM Studio
</label>
</div>
</div>
<div class="wfm-ai-settings-section">
<div class="wfm-ai-settings-title" id="wfm-ai-settings-conn-title">Connection</div>
<div class="wfm-ai-settings-row">
<label class="wfm-ai-label">URL</label>
<input type="text" id="wfm-ai-backend-url" class="wfm-input wfm-ai-url-input" value="http://localhost:11434">
</div>
<div class="wfm-ai-settings-row">
<button id="wfm-ai-test-btn" class="wfm-btn">Test connection</button>
<span id="wfm-ai-test-result" class="wfm-ai-test-result"></span>
</div>
</div>
<div class="wfm-ai-settings-section">
<div class="wfm-ai-settings-title" id="wfm-ai-settings-model-title">Model</div>
<div class="wfm-ai-settings-row">
<select id="wfm-ai-model-select" class="wfm-select wfm-ai-model-select">
<option value="">-- Select model --</option>
</select>
<button id="wfm-ai-model-refresh-btn" class="wfm-btn">Refresh</button>
</div>
</div>
<div class="wfm-ai-settings-section">
<div class="wfm-ai-settings-title" id="wfm-ai-settings-freelang-title">Free language</div>
<div class="wfm-ai-settings-row">
<label class="wfm-ai-label" id="wfm-ai-free-src-label">Source</label>
<input type="text" id="wfm-ai-free-src-lang" class="wfm-input" placeholder="e.g. French">
</div>
<div class="wfm-ai-settings-row">
<label class="wfm-ai-label" id="wfm-ai-free-dst-label">Target</label>
<input type="text" id="wfm-ai-free-dst-lang" class="wfm-input" placeholder="e.g. German">
</div>
</div>
<div class="wfm-ai-settings-section">
<button id="wfm-ai-settings-save-btn" class="wfm-btn wfm-btn-primary">Save</button>
</div>
</div>
</div>
</section>
</main>
<!-- Workflow Detail Modal -->
File diff suppressed because it is too large Load Diff
+16 -33
View File
@@ -605,50 +605,33 @@ app.registerExtension({
};
injectStyles();
const applyButtonIcon = (button, className, getIconFn, extraStyles) => {
if (button.querySelector("svg")) return;
button.classList.add(className);
button.innerHTML = getIconFn();
button.style.borderRadius = "4px";
button.style.padding = "6px";
if (extraStyles) Object.assign(button.style, extraStyles);
const svg = button.querySelector("svg");
if (svg) {
svg.style.width = "20px";
svg.style.height = "20px";
}
};
const buttonConfigs = [
{ tooltip: BUTTON_TOOLTIP, className: "wfm-top-menu-button", getIcon: getWfmIcon, styles: { backgroundColor: "var(--primary-bg)" } },
{ tooltip: SNAPSHOT_TOOLTIP, className: "wfm-snapshot-button", getIcon: getSnapshotIcon, styles: null },
{ tooltip: NODE_SETS_TOOLTIP, className: "wfm-node-sets-btn", getIcon: getNodeSetsIcon, styles: null },
];
const replaceAllIcons = () => {
const replaceButtonIcons = () => {
let foundAny = false;
for (const cfg of buttonConfigs) {
document.querySelectorAll(`button[aria-label="${cfg.tooltip}"]`).forEach((btn) => {
applyButtonIcon(btn, cfg.className, cfg.getIcon, cfg.styles);
foundAny = true;
btn.classList.add(cfg.className);
btn.innerHTML = cfg.getIcon();
btn.style.borderRadius = "4px";
btn.style.padding = "6px";
if (cfg.styles) Object.assign(btn.style, cfg.styles);
const svg = btn.querySelector("svg");
if (svg) {
svg.style.width = "20px";
svg.style.height = "20px";
}
});
}
};
const waitAndObserve = () => {
const firstBtn = document.querySelector(`button[aria-label="${BUTTON_TOOLTIP}"]`);
if (!firstBtn) {
requestAnimationFrame(waitAndObserve);
return;
}
replaceAllIcons();
const container = firstBtn.closest(".actionbar-container") || firstBtn.parentElement;
if (container) {
new MutationObserver(() => replaceAllIcons()).observe(container, {
childList: true,
subtree: true,
});
if (!foundAny) {
requestAnimationFrame(replaceButtonIcons);
}
};
requestAnimationFrame(waitAndObserve);
requestAnimationFrame(replaceButtonIcons);
},
});
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

+599
View File
@@ -0,0 +1,599 @@
{
"id": "f11699d9-4b62-47bd-b501-e49d609d9810",
"revision": 0,
"last_node_id": 10,
"last_link_id": 10,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
413,
389
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 5
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
863,
186
],
"size": [
315,
474
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"link": 1
},
{
"localized_name": "positive",
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"localized_name": "negative",
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"localized_name": "latent_image",
"name": "latent_image",
"type": "LATENT",
"link": 2
},
{
"localized_name": "seed",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"localized_name": "steps",
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"localized_name": "cfg",
"name": "cfg",
"type": "FLOAT",
"widget": {
"name": "cfg"
},
"link": null
},
{
"localized_name": "sampler_name",
"name": "sampler_name",
"type": "COMBO",
"widget": {
"name": "sampler_name"
},
"link": null
},
{
"localized_name": "scheduler",
"name": "scheduler",
"type": "COMBO",
"widget": {
"name": "scheduler"
},
"link": null
},
{
"localized_name": "denoise",
"name": "denoise",
"type": "FLOAT",
"widget": {
"name": "denoise"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "KSampler",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
378124453179007,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1209,
188
],
"size": [
210,
46
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"localized_name": "samples",
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": 10
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
9
]
}
],
"properties": {
"Node name for S&R": "VAEDecode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": []
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1451,
189
],
"size": [
210,
270
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 9
},
{
"localized_name": "filename_prefix",
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
473,
609
],
"size": [
315,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "width",
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": null
},
{
"localized_name": "height",
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": null
},
{
"localized_name": "batch_size",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
51.3,
182.49999999999997
],
"size": [
315,
98
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "ckpt_name",
"name": "ckpt_name",
"type": "COMBO",
"widget": {
"name": "ckpt_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "MODEL",
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
1
]
},
{
"localized_name": "CLIP",
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": [
3,
5
]
},
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": []
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"v1-5-pruned-emaonly-fp16.safetensors"
]
},
{
"id": 10,
"type": "VAELoader",
"pos": [
61.897062649999256,
347.8818042399999
],
"size": [
270,
58
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "vae_name",
"name": "vae_name",
"type": "COMBO",
"widget": {
"name": "vae_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"links": [
10
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "VAELoader"
},
"widgets_values": [
"vae-ft-mse-840000-ema-pruned.safetensors"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
415,
186
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 3
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
4
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"flower"
]
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
10,
10,
0,
8,
1,
"VAE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [
0,
0
],
"scale": 1
},
"ue_links": [],
"links_added_by_ue": []
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 741 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

+887
View File
@@ -0,0 +1,887 @@
{
"id": "831a2929-0536-4715-a530-72a8b91c00fd",
"revision": 0,
"last_node_id": 13,
"last_link_id": 21,
"nodes": [
{
"id": 12,
"type": "CLIPTextEncodeEditPlus",
"pos": [
1364.9859298350216,
-408.28982585532515
],
"size": [
300,
257
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 16
},
{
"localized_name": "text1",
"name": "text1",
"type": "STRING",
"link": 18
},
{
"localized_name": "text2",
"name": "text2",
"shape": 7,
"type": "STRING",
"link": null
},
{
"localized_name": "text_edit",
"name": "text_edit",
"type": "STRING",
"widget": {
"name": "text_edit"
},
"link": null
},
{
"localized_name": "mode",
"name": "mode",
"type": "COMBO",
"widget": {
"name": "mode"
},
"link": null
}
],
"outputs": [
{
"localized_name": "conditioning",
"name": "conditioning",
"type": "CONDITIONING",
"links": [
17
]
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"links": [
21
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "CLIPTextEncodeEditPlus"
},
"widgets_values": [
"blue flower",
"+af",
""
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1742.6139298350226,
-576.3158258553248
],
"size": [
315,
474
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"link": 15
},
{
"localized_name": "positive",
"name": "positive",
"type": "CONDITIONING",
"link": 17
},
{
"localized_name": "negative",
"name": "negative",
"type": "CONDITIONING",
"link": 5
},
{
"localized_name": "latent_image",
"name": "latent_image",
"type": "LATENT",
"link": 6
},
{
"localized_name": "seed",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"localized_name": "steps",
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"localized_name": "cfg",
"name": "cfg",
"type": "FLOAT",
"widget": {
"name": "cfg"
},
"link": null
},
{
"localized_name": "sampler_name",
"name": "sampler_name",
"type": "COMBO",
"widget": {
"name": "sampler_name"
},
"link": null
},
{
"localized_name": "scheduler",
"name": "scheduler",
"type": "COMBO",
"widget": {
"name": "scheduler"
},
"link": null
},
{
"localized_name": "denoise",
"name": "denoise",
"type": "FLOAT",
"widget": {
"name": "denoise"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
7
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
},
"Node name for S&R": "KSampler"
},
"widgets_values": [
1018108390999488,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 4,
"type": "VAEDecode",
"pos": [
2086.446929835019,
-568.6398258553247
],
"size": [
210,
46
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"localized_name": "samples",
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": 20
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
12
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
},
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 9,
"type": "SaveImage",
"pos": [
2326.33789983502,
-566.7928258553247
],
"size": [
210,
270
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 12
},
{
"localized_name": "filename_prefix",
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
},
"Node name for S&R": "SaveImage"
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 13,
"type": "ShowText|pysssss",
"pos": [
1363.0960298350246,
-578.3718258553248
],
"size": [
305.77,
111.08999999999992
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"link": 21
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"shape": 6,
"type": "STRING",
"links": null
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "ShowText|pysssss"
},
"widgets_values": [
"girl,sentai,sentai,flower petal,full body,long hair,long legs,white boots and white gloves,<lora:9_Toshikazu:0.4> Toshikazu,<lora:9_Iggym:0.3> Iggym, painterly, realistic, sketch, traditional media, \\(medium\\)\n<lora:9_Landuo941:0.3> Landuo941\n<lora:Manga122:0.2>,<lora:MayuMurataIllustrious:0.3> Mayu Murata\n<lora:9_Navybluesparrow:0.4> Navybluesparrow\n<lora:9_RiyokoIkeda:0.2> Riyoko Ikeda\n<lora:9_ShumiiberryAlt:0.2> Shumiiberry,\n<lora:Shuiyuan:0.2> 5huny4n_5ty13 \n<lora:test_mix:0.2>,,blue flower"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
992.022929835024,
79.15817414467442
],
"size": [
271.44000000000005,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "width",
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": null
},
{
"localized_name": "height",
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": null
},
{
"localized_name": "batch_size",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
},
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 1,
"type": "ImageMetadataLoRALoader",
"pos": [
987.5885798350228,
-582.1963058553249
],
"size": [
286,
565
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"link": 1
},
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 2
},
{
"localized_name": "metadata",
"name": "metadata",
"shape": 7,
"type": "METADATA",
"link": 3
},
{
"localized_name": "lora_1",
"name": "lora_1",
"type": "COMBO",
"widget": {
"name": "lora_1"
},
"link": null
},
{
"localized_name": "strength_model_1",
"name": "strength_model_1",
"type": "FLOAT",
"widget": {
"name": "strength_model_1"
},
"link": null
},
{
"localized_name": "strength_clip_1",
"name": "strength_clip_1",
"type": "FLOAT",
"widget": {
"name": "strength_clip_1"
},
"link": null
},
{
"localized_name": "lora_2",
"name": "lora_2",
"type": "COMBO",
"widget": {
"name": "lora_2"
},
"link": null
},
{
"localized_name": "strength_model_2",
"name": "strength_model_2",
"type": "FLOAT",
"widget": {
"name": "strength_model_2"
},
"link": null
},
{
"localized_name": "strength_clip_2",
"name": "strength_clip_2",
"type": "FLOAT",
"widget": {
"name": "strength_clip_2"
},
"link": null
},
{
"localized_name": "lora_3",
"name": "lora_3",
"type": "COMBO",
"widget": {
"name": "lora_3"
},
"link": null
},
{
"localized_name": "strength_model_3",
"name": "strength_model_3",
"type": "FLOAT",
"widget": {
"name": "strength_model_3"
},
"link": null
},
{
"localized_name": "strength_clip_3",
"name": "strength_clip_3",
"type": "FLOAT",
"widget": {
"name": "strength_clip_3"
},
"link": null
}
],
"outputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"links": [
15
]
},
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"links": [
14,
16
]
}
],
"properties": {
"Node name for S&R": "ImageMetadataLoRALoader",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": [
"temp\\Manga122.safetensors",
0.2,
0.2,
"temp\\MayuMurataIllustrious.safetensors",
0.3,
0.3,
"temp\\Shuiyuan.safetensors",
0.2,
0.2,
""
]
},
{
"id": 2,
"type": "ImageMetadataPromptLoader",
"pos": [
626.8189298350237,
-586.5118258553252
],
"size": [
286,
709
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "ckpt_name",
"name": "ckpt_name",
"type": "COMBO",
"widget": {
"name": "ckpt_name"
},
"link": null
},
{
"localized_name": "vae_name",
"name": "vae_name",
"type": "COMBO",
"widget": {
"name": "vae_name"
},
"link": null
},
{
"localized_name": "positive_text",
"name": "positive_text",
"type": "STRING",
"widget": {
"name": "positive_text"
},
"link": null
},
{
"localized_name": "negative_text",
"name": "negative_text",
"type": "STRING",
"widget": {
"name": "negative_text"
},
"link": null
},
{
"localized_name": "_metadata_json",
"name": "_metadata_json",
"type": "STRING",
"widget": {
"name": "_metadata_json"
},
"link": null
}
],
"outputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"links": [
1
]
},
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"links": [
2
]
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"links": [
20
]
},
{
"localized_name": "positive",
"name": "positive",
"type": "CONDITIONING",
"links": null
},
{
"localized_name": "negative",
"name": "negative",
"type": "CONDITIONING",
"links": null
},
{
"localized_name": "positive_text",
"name": "positive_text",
"type": "STRING",
"links": [
18
]
},
{
"localized_name": "negative_text",
"name": "negative_text",
"type": "STRING",
"links": []
},
{
"localized_name": "metadata",
"name": "metadata",
"type": "METADATA",
"links": [
3
]
}
],
"properties": {
"Node name for S&R": "ImageMetadataPromptLoader",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": [
"anime\\CHEYENNE_v24HyperIllustration.safetensors",
"SDXL-VAE-FP16-Fix.safetensors",
"girl,sentai,sentai,flower petal,full body,long hair,long legs,white boots and white gloves,<lora:9_Toshikazu:0.4> Toshikazu,<lora:9_Iggym:0.3> Iggym, painterly, realistic, sketch, traditional media, \\(medium\\)\n<lora:9_Landuo941:0.3> Landuo941\n<lora:Manga122:0.2>,<lora:MayuMurataIllustrious:0.3> Mayu Murata\n<lora:9_Navybluesparrow:0.4> Navybluesparrow\n<lora:9_RiyokoIkeda:0.2> Riyoko Ikeda\n<lora:9_ShumiiberryAlt:0.2> Shumiiberry,\n<lora:Shuiyuan:0.2> 5huny4n_5ty13 \n<lora:test_mix:0.2>,",
"helmet,mask",
"{\"loras\":[{\"name\":\"9_Toshikazu\",\"strength_model\":0.4,\"strength_clip\":0.4},{\"name\":\"9_Iggym\",\"strength_model\":0.3,\"strength_clip\":0.3},{\"name\":\"9_Landuo941\",\"strength_model\":0.3,\"strength_clip\":0.3},{\"name\":\"Manga122\",\"strength_model\":0.2,\"strength_clip\":0.2},{\"name\":\"MayuMurataIllustrious\",\"strength_model\":0.3,\"strength_clip\":0.3},{\"name\":\"9_Navybluesparrow\",\"strength_model\":0.4,\"strength_clip\":0.4},{\"name\":\"9_RiyokoIkeda\",\"strength_model\":0.2,\"strength_clip\":0.2},{\"name\":\"9_ShumiiberryAlt\",\"strength_model\":0.2,\"strength_clip\":0.2},{\"name\":\"Shuiyuan\",\"strength_model\":0.2,\"strength_clip\":0.2},{\"name\":\"test_mix\",\"strength_model\":0.2,\"strength_clip\":0.2}]}",
""
]
},
{
"id": 10,
"type": "CLIPTextEncode",
"pos": [
1360.6159298350235,
-96.8748258553254
],
"size": [
301.4950151367186,
88
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 14
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
5
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
},
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
}
],
"links": [
[
1,
2,
0,
1,
0,
"MODEL"
],
[
2,
2,
1,
1,
1,
"CLIP"
],
[
3,
2,
7,
1,
2,
"METADATA"
],
[
5,
10,
0,
3,
2,
"CONDITIONING"
],
[
6,
5,
0,
3,
3,
"LATENT"
],
[
7,
3,
0,
4,
0,
"LATENT"
],
[
12,
4,
0,
9,
0,
"IMAGE"
],
[
14,
1,
1,
10,
0,
"CLIP"
],
[
15,
1,
0,
3,
0,
"MODEL"
],
[
16,
1,
1,
12,
0,
"CLIP"
],
[
17,
12,
0,
3,
1,
"CONDITIONING"
],
[
18,
2,
5,
12,
1,
"STRING"
],
[
20,
2,
2,
4,
1,
"VAE"
],
[
21,
12,
1,
13,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ue_links": [],
"links_added_by_ue": []
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

+719
View File
@@ -0,0 +1,719 @@
{
"id": "03047124-7d5d-4eb4-b458-558d6dec9b5e",
"revision": 0,
"last_node_id": 28,
"last_link_id": 50,
"nodes": [
{
"id": 8,
"type": "VAEEncode",
"pos": [
551.8454158175873,
-685.9812381649755
],
"size": [
242.296875,
96
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"localized_name": "pixels",
"name": "pixels",
"type": "IMAGE",
"link": 49
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": 34
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"links": [
5
]
}
],
"properties": {
"Node name for S&R": "VAEEncode",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": []
},
{
"id": 22,
"type": "VAELoader",
"pos": [
75.4452850000009,
-464.22850100000056
],
"size": [
270,
58
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "vae_name",
"name": "vae_name",
"type": "COMBO",
"widget": {
"name": "vae_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"links": [
34,
35
]
}
],
"properties": {
"Node name for S&R": "VAELoader",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": [
"SDXL-VAE-FP16-Fix.safetensors"
]
},
{
"id": 5,
"type": "VAEDecode",
"pos": [
1312.282348239462,
-683.1137137997414
],
"size": [
254.390625,
96
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"localized_name": "samples",
"name": "samples",
"type": "LATENT",
"link": 11
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": 35
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"links": [
50
]
}
],
"properties": {
"Node name for S&R": "VAEDecode",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": []
},
{
"id": 7,
"type": "KSampler",
"pos": [
1000.0439480441502,
-674.780789922787
],
"size": [
270,
589.3125
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"link": 36
},
{
"localized_name": "positive",
"name": "positive",
"type": "CONDITIONING",
"link": 37
},
{
"localized_name": "negative",
"name": "negative",
"type": "CONDITIONING",
"link": 10
},
{
"localized_name": "latent_image",
"name": "latent_image",
"type": "LATENT",
"link": 5
},
{
"localized_name": "seed",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"localized_name": "steps",
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"localized_name": "cfg",
"name": "cfg",
"type": "FLOAT",
"widget": {
"name": "cfg"
},
"link": null
},
{
"localized_name": "sampler_name",
"name": "sampler_name",
"type": "COMBO",
"widget": {
"name": "sampler_name"
},
"link": null
},
{
"localized_name": "scheduler",
"name": "scheduler",
"type": "COMBO",
"widget": {
"name": "scheduler"
},
"link": null
},
{
"localized_name": "denoise",
"name": "denoise",
"type": "FLOAT",
"widget": {
"name": "denoise"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"links": [
11
]
}
],
"properties": {
"Node name for S&R": "KSampler",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": [
240074286219855,
"randomize",
30,
8,
"euler",
"karras",
0.7
]
},
{
"id": 24,
"type": "CLIPTextEncode",
"pos": [
528.6156850000002,
-509.49460100000033
],
"size": [
400,
200
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 38
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
37
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": [
"flower,anime girl"
]
},
{
"id": 11,
"type": "CLIPTextEncode",
"pos": [
526.8870184488446,
-252.08752270664039
],
"size": [
400,
200
],
"flags": {
"collapsed": false
},
"order": 3,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 9
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
10
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": [
""
]
},
{
"id": 9,
"type": "CheckpointLoaderSimple",
"pos": [
70.30033916275676,
-694.6312926669282
],
"size": [
270,
154.65625
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "ckpt_name",
"name": "ckpt_name",
"type": "COMBO",
"widget": {
"name": "ckpt_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "MODEL",
"name": "MODEL",
"type": "MODEL",
"links": [
36
]
},
{
"localized_name": "CLIP",
"name": "CLIP",
"type": "CLIP",
"links": [
9,
38
]
},
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"links": []
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": [
"anime\\IllustriousStyle_v22.safetensors"
]
},
{
"id": 28,
"type": "ImageFeeder",
"pos": [
76.77559983502367,
-338.79391585532534
],
"size": [
253.923,
319.923
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "directory",
"name": "directory",
"type": "STRING",
"widget": {
"name": "directory"
},
"link": null
},
{
"localized_name": "sort_mode",
"name": "sort_mode",
"type": "COMBO",
"widget": {
"name": "sort_mode"
},
"link": null
},
{
"localized_name": "index",
"name": "index",
"type": "INT",
"widget": {
"name": "index"
},
"link": null
},
{
"localized_name": "start_index",
"name": "start_index",
"type": "INT",
"widget": {
"name": "start_index"
},
"link": null
},
{
"localized_name": "end_index",
"name": "end_index",
"type": "INT",
"widget": {
"name": "end_index"
},
"link": null
},
{
"localized_name": "batch_size",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": null
},
{
"localized_name": "seed",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"localized_name": "use_selection",
"name": "use_selection",
"type": "BOOLEAN",
"widget": {
"name": "use_selection"
},
"link": null
},
{
"localized_name": "selected_files",
"name": "selected_files",
"type": "STRING",
"widget": {
"name": "selected_files"
},
"link": null
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"links": [
49
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "ImageFeeder"
},
"widgets_values": [
"test",
"ascending",
0,
0,
0,
1,
908906407258686,
"randomize",
true,
"[\"00030-2656222951.png\",\"00063-2334831609.png\",\"02121-3711614362.png\"]",
""
]
},
{
"id": 4,
"type": "PreviewImage",
"pos": [
1603.3762031816507,
-693.7018206825535
],
"size": [
284.5062499999997,
301.77375000000023
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 50
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.8"
}
},
"widgets_values": []
}
],
"links": [
[
5,
8,
0,
7,
3,
"LATENT"
],
[
9,
9,
1,
11,
0,
"CLIP"
],
[
10,
11,
0,
7,
2,
"CONDITIONING"
],
[
11,
7,
0,
5,
0,
"LATENT"
],
[
34,
22,
0,
8,
1,
"VAE"
],
[
35,
22,
0,
5,
1,
"VAE"
],
[
36,
9,
0,
7,
0,
"MODEL"
],
[
37,
24,
0,
7,
1,
"CONDITIONING"
],
[
38,
9,
1,
24,
0,
"CLIP"
],
[
49,
28,
0,
8,
0,
"IMAGE"
],
[
50,
5,
0,
4,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ue_links": [],
"links_added_by_ue": [],
"linearData": {
"inputs": [
[
"9",
"ckpt_name"
],
[
"3",
"pose_editor_3d_widget"
],
[
"3",
"timer_interval"
]
],
"outputs": []
}
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

+599
View File
@@ -0,0 +1,599 @@
{
"id": "f11699d9-4b62-47bd-b501-e49d609d9810",
"revision": 0,
"last_node_id": 10,
"last_link_id": 10,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
413,
389
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 5
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
863,
186
],
"size": [
315,
474
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"localized_name": "model",
"name": "model",
"type": "MODEL",
"link": 1
},
{
"localized_name": "positive",
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"localized_name": "negative",
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"localized_name": "latent_image",
"name": "latent_image",
"type": "LATENT",
"link": 2
},
{
"localized_name": "seed",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"localized_name": "steps",
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"localized_name": "cfg",
"name": "cfg",
"type": "FLOAT",
"widget": {
"name": "cfg"
},
"link": null
},
{
"localized_name": "sampler_name",
"name": "sampler_name",
"type": "COMBO",
"widget": {
"name": "sampler_name"
},
"link": null
},
{
"localized_name": "scheduler",
"name": "scheduler",
"type": "COMBO",
"widget": {
"name": "scheduler"
},
"link": null
},
{
"localized_name": "denoise",
"name": "denoise",
"type": "FLOAT",
"widget": {
"name": "denoise"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "KSampler",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
636718080453780,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1209,
188
],
"size": [
210,
46
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"localized_name": "samples",
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": 10
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
9
]
}
],
"properties": {
"Node name for S&R": "VAEDecode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": []
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1451,
189
],
"size": [
210,
270
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 9
},
{
"localized_name": "filename_prefix",
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
473,
609
],
"size": [
315,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "width",
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": null
},
{
"localized_name": "height",
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": null
},
{
"localized_name": "batch_size",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": null
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
415,
186
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": 3
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
4
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"flower"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
51.3,
182.49999999999997
],
"size": [
315,
98
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "ckpt_name",
"name": "ckpt_name",
"type": "COMBO",
"widget": {
"name": "ckpt_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "MODEL",
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
1
]
},
{
"localized_name": "CLIP",
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": [
3,
5
]
},
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": []
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.8",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"anime\\artUniverse_sdxlV60.safetensors"
]
},
{
"id": 10,
"type": "VAELoader",
"pos": [
61.897062649999256,
347.8818042399999
],
"size": [
270,
58
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "vae_name",
"name": "vae_name",
"type": "COMBO",
"widget": {
"name": "vae_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"links": [
10
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "VAELoader"
},
"widgets_values": [
"sdxl_vae.safetensors"
]
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
10,
10,
0,
8,
1,
"VAE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [
0,
0
],
"scale": 1
},
"ue_links": [],
"links_added_by_ue": []
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB