Compare commits
@@ -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 }}
|
||||
@@ -185,3 +185,18 @@ os.environ["HF_HOME"] = "D:\\hf_cache"
|
||||
5. `INPUT_TYPES` 変更時は `widgets_values` も同時更新
|
||||
6. 削除した依存への参照が残っていないか grep で確認
|
||||
7. ComfyUI完全再起動 → ワークフロー読み込み → エンドツーエンド実行確認
|
||||
|
||||
---
|
||||
|
||||
## リリース手順
|
||||
|
||||
コミット・プッシュ・GitHub リリース(`gh release create`)を行う際は、**必ず以下の順序**で実施する。
|
||||
|
||||
1. `pyproject.toml` の `version` を新バージョン番号に更新してステージングに含める
|
||||
2. `README.md` のバージョンバッジを更新(``)
|
||||
3. `README.md` の Changelog に新バージョンのエントリを追加
|
||||
4. `DEVLOG.md` の先頭に変更詳細エントリを追加
|
||||
5. 上記すべてを同一コミットにまとめてプッシュ
|
||||
6. `gh release create vX.Y.Z` でリリースを作成
|
||||
|
||||
`pyproject.toml` のバージョン変更が GitHub Actions のトリガー(`paths: pyproject.toml`)になっており、ComfyUI Registry への自動公開もここで行われる。バージョン更新を忘れると Registry に反映されない。
|
||||
|
||||
@@ -5,36 +5,32 @@ 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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
| Workflow Tab | GenerateUI Tab |
|
||||
| Workflow Tab | Models Tab |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| Prompt Tab | Settings Tab |
|
||||
| Prompt Input Assistance | Gen UI Feeder |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| Nodes Tab | Models Tab |
|
||||
| Top Bar | WS Library |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| Gallery Tab | Help & Support Tab |
|
||||
| Library Information | Customize |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|
||||
| Workflow Studio Library (ComfyUI) | ComfyUI Integration |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Workflow Tab
|
||||
- **Thumbnail / Card / Table views** — switch between view modes to browse your workflow library
|
||||
- **Thumbnail / Table views** — switch between view modes to browse your workflow library
|
||||
- **Thumbnail side panel** — preview workflow canvas snapshots in the side panel
|
||||
- **Badge filtering** — filter by user-defined badges (free labels you assign to each workflow)
|
||||
- **Search** — full-text search across workflow names and metadata
|
||||
@@ -42,7 +38,7 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
- **Badge management** — add, rename, delete badges with custom colors shared with the Models tab (⚙ Badge button)
|
||||
- **AI summary** — generate workflow descriptions using Ollama
|
||||
- **Import / Export** — import workflows from files or clipboard, open in ComfyUI directly
|
||||
- **Default view setting** — persist your preferred view mode (Thumbnail / Card / Table)
|
||||
- **Default view setting** — persist your preferred view mode (Thumbnail / Table)
|
||||
|
||||
### Canvas Snapshot (v0.1.2)
|
||||
- **One-click capture** — click the camera button in ComfyUI's top bar to snapshot the current workflow canvas
|
||||
@@ -50,20 +46,37 @@ 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)
|
||||
- **5-tab layout** — Input / Model / Settings / Feeder / Batch tabs; Input, Model, and Settings each include a Raw JSON column on the right for instant preview and direct editing
|
||||
- **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
|
||||
- **Batch type selector** — check one of the column header checkboxes in the Batch Queue pane (Checkpoint / Lora / Prompt / Workflow) to activate that batch type; only one type can be active at a time; the Batch panel below Generate shows the active type, progress, and Pause/Resume/Stop controls
|
||||
- **Batch tab** (v0.3.18) — dedicated 3-pane layout for assembling the batch queue:
|
||||
- **Left pane** — file-tree checkpoint selection; Filter input to search; All / None buttons; initial state is all unchecked
|
||||
- **Center pane** — group-based selection with inner tabs (Checkpoint | Lora | Prompt | Workflow); Checkpoint/Lora groups come from the Models tab, Prompt groups from the Prompt tab, Workflow groups from the Workflow tab — check a group to add all its members, expand ▶ to select individually
|
||||
- **Right pane (Batch Queue)** — shows items queued for each batch type; column headers (Checkpoint / Lora / Prompt / Workflow) each have a checkbox — check one to activate that batch type (radio behavior: only one at a time); count shown at top
|
||||
- **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 +85,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)
|
||||
@@ -82,8 +107,13 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
- **Gallery output directory** — configure which output folder the Gallery tab scans for images
|
||||
- **Eagle connection** — set Eagle API endpoint for auto-save
|
||||
- **Ollama connection** — configure Ollama server URL
|
||||
- **CivitAI Host** — choose which site opens when clicking a model link: `civitai.com` (SFW only) or `civitai.red` (unrestricted); saved to `settings.json` and synced to `localStorage` for the Models tab to use without extra fetches
|
||||
- **CivitAI API Key** — optional Bearer token for authenticated CivitAI access; stored in `settings.json` (excluded from data exports); environment variable `CIVITAI_API_KEY` takes priority if set
|
||||
- **Default workflow** — set a workflow to auto-load on startup
|
||||
- **Data Management** — export all plugin data (settings, metadata, prompts, etc.) to a single JSON file; import to restore data (useful when migrating or reinstalling)
|
||||
- **Data Management** — export all plugin data (settings, metadata, prompts, etc.) to a single JSON file; import to restore data (useful when migrating or reinstalling); API keys are excluded from exports for security
|
||||
- **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)
|
||||
@@ -111,27 +141,44 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
|
||||
### Models Tab (v0.2.3)
|
||||
- **Model Browser** — browse all installed ComfyUI models (Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding) with sub-tab switching
|
||||
- **Thumbnail / Card / Table views** — switch between view modes with pagination (24 items per page)
|
||||
- **Thumbnail / Table views** — switch between view modes with pagination (24 items per page)
|
||||
- **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; side panel shows **Type** (badge), **Base Model**, **Hash** (BLAKE3/SHA256, click to copy), trained words, tags, and model page link; model link opens in the site selected in Settings (civitai.com or civitai.red)
|
||||
- **CivitAI panel — Info / Sample sub-tabs** — Info tab shows model details (name, type, base model, hash, trigger words, tags, description); Sample tab shows all sample images (count shown in tab label); clicking any image opens the full-size version in a new tab
|
||||
- **CivitAI panel states** — three distinct states: not yet checked (fetch button), checked but not found on CivitAI (re-check button with notice), and found (full info display); clicking the CivitAI tab always refreshes to the latest state
|
||||
- **Batch CivitAI fetch** — one-click batch fetch using `POST /model-versions/by-hash` (up to 100 models per request) 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)
|
||||
- **Table view memo** — memo column displayed in table view for quick reference
|
||||
- **Preview images** — auto-detect `{model_stem}.preview.png` next to model files
|
||||
- **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`)
|
||||
- **Multi-select & bulk operations** — enter selection mode to check multiple models; bulk action bar supports: **Deselect All** (clear all without exiting select mode), **★ Favorite / ☆ Unfavorite** (toggle favorites for all selected), **Add / Remove from Group**, **Create & Add** (new group), **+Badge / −Badge** (apply or remove a badge from all selected), and permanent **Delete Files** (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 +211,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 +261,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 +279,118 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.3.22
|
||||
- **Card view removed** — Workflow tab and Models tab no longer offer Card view; Thumbnail and Table remain; if `"card"` was previously stored in localStorage it falls back to Thumbnail automatically
|
||||
- **Batch type selector** — replaced the Checkpoint Batch toggle with per-type checkboxes on the Batch Queue column headers (Checkpoint / Lora / Prompt / Workflow); radio behavior ensures only one type is active at a time; the Batch panel below Generate repurposed as a status display showing the active type, amber progress bar, and Pause/Resume / Stop controls
|
||||
- **4-type batch generation** — `_runBatchGenerate()` dispatches to Checkpoint (file-tree + group selection), Lora (group selection), Prompt (preset group items applied to positive/negative nodes), and Workflow (group selection, restores original workflow after batch); all types use the same generic `_runBatchLoop()` with pause/abort support
|
||||
- **Help updated** — EN / JA / ZH strings updated for `helpWf1`, `helpModels2`, `helpGen11`, `helpGen13`, `helpGen14`
|
||||
|
||||
### v0.3.21
|
||||
- **CivitAI Host setting** — new dropdown in Settings tab (CivitAI section): choose `civitai.com` (SFW only) or `civitai.red` (unrestricted); selection is saved to `settings.json` and synced to `localStorage` so the Models tab uses it immediately without extra server calls
|
||||
- **CivitAI panel — Info / Sample sub-tabs** — the CivitAI side panel is split into two sub-tabs; Info tab contains model details and actions; Sample tab shows all sample images (count displayed in label); images open full-size in a new browser tab on click
|
||||
- **Bug fix: model page URL for existing cache** — `modelId: null` entries (the majority of the cache) previously generated broken links (`/models?modelVersionId=…`); both the Python service and the JS renderer now fall back to `/{host}/model-versions/{versionId}` which redirects correctly to the model page; all 324 existing cache entries are fixed immediately without re-fetching
|
||||
- **Help updated** — Batch tab entries (gen-12 / gen-13 / gen-14) and Settings entries (settings-11 Text Size / settings-12 RAW JSON Colors / settings-13 CivitAI Host) are now fully localized in EN / JA / ZH; `helpGen3` updated to reflect 5-tab layout; `helpGen11` rewritten to describe the Batch toggle; JA/ZH translations no longer contain untranslated English terms (Filter, Pause/Resume, Stop)
|
||||
|
||||
### v0.3.20
|
||||
- **CivitAI panel — Type / Base Model / Hash display** — side panel CivitAI tab now shows a Type badge (e.g. CHECKPOINT, LORA), Base Model row, and Hash row (BLAKE3 preferred, SHA256 fallback from CivitAI API or local metadata); click the hash to copy the full value to clipboard
|
||||
- **CivitAI panel — images open in new tab** — sample images in the CivitAI tab are now wrapped in links; clicking an image opens the full-size version in a new browser tab
|
||||
- **CivitAI panel — 3-state UI** — the panel now distinguishes between "not yet checked" (blue fetch button), "checked but not found on CivitAI" (re-check button with a notice), and "found" (full info); no more ambiguity after running batch fetch
|
||||
- **CivitAI tab click refresh** — clicking the CivitAI tab in the side panel now re-renders its content, ensuring the latest fetch state is always shown without needing to reselect the model
|
||||
- **Bug fix: model page URL broken after batch fetch** — the batch `POST /model-versions/by-hash` endpoint can omit `model.id` inside the version object; `_extract_info` now falls back to the top-level `modelId` field so the model page link is always correct; existing cached entries with a broken URL are also fixed client-side from `modelId` + `versionId`
|
||||
- **CivitAI cache — `fileHashes` field added** — `_extract_info` now stores the primary file's full hash map (BLAKE3, SHA256, AutoV2, etc.) so the Hash row is populated for newly fetched or refreshed models
|
||||
|
||||
### v0.3.19
|
||||
- **CivitAI batch fetch — major speedup** — switched from one-by-one `GET` requests (0.5 s delay each) to `POST /model-versions/by-hash` batch endpoint; up to 100 hashes per request, reducing 100-model fetch from ~50 s to a few seconds
|
||||
- **Retry with exponential backoff** — `fetch_by_hash` and the new batch POST both retry on HTTP 429 / 500 / 502 / 503 / 504 with 1 s → 2 s → 4 s delays (max 3 attempts), as recommended in the CivitAI API docs
|
||||
- **CivitAI API key support** — optional Bearer token can be set in Settings tab (CivitAI API Key section); environment variable `CIVITAI_API_KEY` takes priority over the stored key; API keys are excluded from data exports
|
||||
- **Richer metadata** — `_extract_info` now captures `nsfwLevel` (numeric), `air` (AIR URN identifier), `stats` (download count, thumbs-up/down), `updatedAt`, `publishedAt`, image dimensions / per-image NSFW level (`imageDetails`), and primary file precision / format (`fileMeta`)
|
||||
- **Hash consistency** — cache keys use lowercase hex; API requests send uppercase hex as required by the CivitAI spec
|
||||
- **Bug fix: CivitAI tab blank after batch fetch** — the `done` SSE event now applies `data.hashes` directly to `state.modelMetadata` so the side-panel CivitAI tab renders correctly even when the subsequent `fetchModelMetadata()` call races ahead of the server flush
|
||||
|
||||
### v0.3.18
|
||||
- **Batch tab** — new 5th subtab in GenerateUI with a dedicated 3-pane layout for assembling checkpoint batch queues; left pane: file-tree checkpoint selection (filter, All/None, initially unchecked); center pane: group-based selection with inner tabs (Checkpoint / Lora / Prompt / Workflow — Lora/Prompt/Workflow coming soon); right pane: live Batch Queue preview combining both pane selections with duplicates removed
|
||||
- **Checkpoint selector moved** — the checkpoint dropdown is removed from the right panel; selection is now done exclusively in the Batch tab left and center panes; the right panel retains only the enable/disable toggle and progress display
|
||||
- **Group selection fix** — batch group selection now stores selected group names (not raw model names), so changes to group membership in the Models tab are always reflected in the queue without stale entries
|
||||
- **Models tab bulk operations expanded** — bulk action bar now includes: Deselect All (clear all selections without exiting select mode), ★ Favorite / ☆ Unfavorite, +Badge / −Badge (apply or remove a badge from all selected models); existing group and delete operations unchanged
|
||||
|
||||
### v0.3.17
|
||||
- **CivitAI SSL fix** — resolved `CERTIFICATE_VERIFY_FAILED` errors when fetching model info on Windows Portable Python environments; uses `certifi` CA bundle when available, falls back to system store, and disables verification only as a last resort with a warning log
|
||||
- **Lora Manager support** — `Lora Loader (LoraManager)` nodes (ComfyUI-Lora-Manager) are now correctly converted from UI format to API format in the GenerateUI tab; the `loras` widget array is wrapped to `{"__value__": [...]}` as required by the node, enabling generation with LoRA Manager workflows
|
||||
|
||||
### v0.3.16
|
||||
- **CivitAI preview fallback** — model cards and the detail panel's Info tab now display the first CivitAI image directly when no local preview file exists; if the backend download fails (e.g. network restriction server-side), the browser fetches the image URL from the CivitAI cache as a fallback so the preview is always shown after fetching
|
||||
|
||||
### v0.3.15
|
||||
- **Sample workflows** — added `workflows/` directory containing 13 sample workflows (SD1.5 / SDXL / DWPose / Face Detailer / Image Editing) with PNG thumbnails, bundled in the package
|
||||
- **CivitAI auto-preview** — when fetching CivitAI info (individual or batch), the first CivitAI image is automatically downloaded and saved as `{model_stem}.preview.png` if no preview exists; batch summary reports the count of auto-saved previews
|
||||
- **TOOLS pane — Create Tags** — new task option in the VLM dropdown (both SPA AI TOOL tab and Library A tab): generates a comma-separated list of descriptive English tags from the image
|
||||
|
||||
### v0.3.14
|
||||
- **AI TOOL tab redesign** — tab renamed from "A" to "AI TOOL"; sub-tab navigation removed in favor of a permanent 3-pane layout (Translation 40% | TOOLS 40% | Settings 20%) — all panes always visible simultaneously; "VLM" pane renamed to "TOOLS"
|
||||
- **Settings — RAW JSON Colors** — new accordion section with 6 color pickers to customize the syntax highlight colors for the Raw JSON editor in Generate UI: Default Text, Name/Scheduler, Title, Width/Height, Prompt/Text, Image/File; changes apply immediately on color pick; Reset Defaults restores the original scheme; saved to `localStorage` under `wfm_settings.jsonColors` and applied on startup
|
||||
- **Help tab updated** — AI TOOL tab card rewritten; Settings card updated with RAW JSON Colors entry; Library panel sidepanel-16 entry updated
|
||||
|
||||
### v0.3.13
|
||||
- **AI Tab (A)** — new tab added to both the SPA and the Workflow Studio Library side panel (rightmost tab)
|
||||
- **Translation sub-tab** — translate text via Ollama or LM Studio between Japanese, English, Chinese, or a custom Free language; ⇄ swap button exchanges both language selectors and text content simultaneously; language selections persisted automatically
|
||||
- **VLM sub-tab** — drop zone (110px, matching the I tab) accepts images via drag & drop or click; task selector (Describe Image / Create Prompt); Run button streams result into output area with Copy button; Ollama uses `images:[base64]` payload, LM Studio uses `image_url` content block
|
||||
- **Settings sub-tab** — backend radio (Ollama / LM Studio), API URL input, connection test with live status, model selector with refresh, Free language name fields (source and destination); saved to `localStorage` key `wfm_ai_settings` and shared between SPA and Library panel
|
||||
- **URL security** — backend URL validated via `new URL()` to enforce `http://` or `https://` scheme before any network call
|
||||
- **Settings Tab — Ollama section renamed** — "Ollama 設定" → "Ollama 設定(プロンプトタブ)" to distinguish it from the new AI tab's settings
|
||||
- **Help Tab updated** — added AI Tab (A) feature card and Library panel A tab entry; i18n support in EN/JA/ZH
|
||||
|
||||
### v0.3.12
|
||||
- **GenerateUI — Raw JSON search** — always-visible search bar above the Raw JSON editor; finds all matches as you type with match count (`3/12`); navigate forward/back with ↑/↓ buttons, Enter / Shift+Enter, or Escape / ✕ to clear; current match highlighted in orange, other matches in yellow; search overlay syncs with editor scroll and stays updated while editing
|
||||
- **Workflow analyzer — extended model type detection** — added CLIPLoader `type` field mapping to model family (`flux`, `hidream_i`, `wan`, `cosmos`, `lumina`, `ovis`, etc.) via `_CLIP_TYPE_TO_MODEL`; subgraph nodes (`definitions.subgraphs`) now scanned for model detection; `UnetLoaderGGUF` and similar loaders matched via `"UnetLoader" in ntype`; new families added: NewBie, Ovis, HiDream, Wan, Cosmos, Lumina; model-name detection refactored into reusable `_detect_model_type_from_name()`
|
||||
- **Screenshots refreshed** — replaced tab-by-tab screenshots with 8 feature-focused images
|
||||
|
||||
### v0.3.11
|
||||
- **Metadata Tab — extended loader support** — `metadata-tab.js` now recognizes `UnetLoaderGGUF` and `UNETLoaderGGUF` as Diffusion Model (e.g. HiDream GGUF workflows), and `QuadrupleCLIPLoader` as Text Encoder (e.g. HiDream 4-CLIP); previously these were detected only in the side panel I tab, not in the Metadata tab itself
|
||||
- **Settings Tab — Text Size** — new slider (10–28 px) in the Settings tab adjusts font size for all prompt and chat text areas at once: Generate UI positive/negative prompts, AI Assistant chat input, Preset prompts, Wildcard text area and file editor, and Metadata prompt full preview; applied immediately on drag and persisted with Save Settings
|
||||
|
||||
### v0.3.10
|
||||
- **Information tab — drag to canvas** — model, lora, and Prompts sub-tabs in the side panel I tab now support drag & drop onto the ComfyUI canvas; double-click also places nodes at canvas center
|
||||
- model sub-tab: each item places the corresponding loader node (Checkpoint → `CheckpointLoaderSimple`, VAE → `VAELoader`, Diffusion Model → `UNETLoader`, Text Encoder → `CLIPLoader`)
|
||||
- lora sub-tab: drag individual LoRA to place `LoraLoader`; **Multiple LORA** section (shown for 1+ LoRAs) places all LoRAs in a single `Lora Loader (LoraManager)` node
|
||||
- Prompts sub-tab: drag a prompt to place `CLIPTextEncode` with the full text pre-filled
|
||||
- **Extended node type support** — `UnetLoaderGGUF` / `UNETLoaderGGUF` now detected as Diffusion Model; `QuadrupleCLIPLoader` (HiDream and similar 4-CLIP workflows) now detected as Text Encoder
|
||||
- **Bug fix: CLIPLoader widget name** — `textencoder` type in `MODEL_NODE_MAP` was incorrectly using `clip_name1` (DualCLIPLoader's widget) instead of `clip_name`; Text Encoder models now drop with the correct filename
|
||||
- **Preview area fixed height** — Information tab drop zone fixed at 110px so model/prompt lists are no longer compressed when a large image is loaded
|
||||
|
||||
### v0.3.9
|
||||
- **Side panel I tab** — new Information tab in the Workflow Studio Library side panel; drop a ComfyUI PNG/WebP/JSON directly in the panel to inspect models, LoRAs, and prompts without opening the Metadata tab; sub-tabs: model / lora / Prompts with full-text preview and Copy button
|
||||
- **Side panel tab redesign** — tabs shortened to single letters (W / N / P / M / I) with full name shown on hover
|
||||
- **Bug fix: top bar icons** — icons (Workflow Studio / camera / Node Sets) were invisible in newer ComfyUI versions because Iconify converted the MDI CSS class to an inline SVG before the custom SVG could be injected; fixed by removing the early-return guard and adopting the same simple `requestAnimationFrame` retry pattern used by lora-manager
|
||||
|
||||
### v0.3.8
|
||||
- **Metadata Tab: extended subgraph support** — added extraction for Flux.2 Dev fp8 (`SamplerCustomAdvanced`), Flux.2 Klein 4B Distilled (image edit), Ernie Image, Qwen-Image-Edit 2511 (`TextEncodeQwenImageEditPlus`), and WAN2.2 14B Animate (top-level CLIPTextEncode + subgraph KSampler)
|
||||
- **Prompt type: Text (no badge)** — when positive/negative polarity cannot be determined (indirect sampler connections, cross-level links, `PrimitiveStringMultiline` etc.), prompts are listed without a POS/NEG badge; action buttons still work normally
|
||||
- **Prompt extraction improvements** — `extractPromptsFromNodeSet` now resolves link sources from any STRING-output node (e.g. `ComfySwitchNode`, `PrimitiveStringMultiline`) without type restriction; `PrimitiveStringMultiline` search and final text-encoder fallback both extended to include subgraph nodes via `collectAllNodes`
|
||||
- **i18n** — added `metaPromptText` key (EN: "Text" / JA: "テキスト" / ZH: "文本")
|
||||
|
||||
### v0.3.7
|
||||
- **Metadata Tab: subgraph workflow support** — Flux.2 (Dev fp8 / Klein), Qwen-Image-Edit (/ 2511 / Layered), and Z-Image (Base / Turbo) official templates now correctly extract models and prompts; these use ComfyUI's `definitions.subgraphs` format with `UNETLoader`, `CLIPLoader`, and `VAELoader` inside the subgraph
|
||||
- **Prompt extraction improvements** — 7-stage fallback chain: ImageMetadataPromptLoader → WFS_PromptText → top-level CLIPTextEncode+KSampler → `PrimitiveStringMultiline` (flux2-klein) → subgraph CLIPTextEncode+KSampler → PromptStyler → all CLIPTextEncode text
|
||||
- **MarkdownNote model fallback** — `extractMarkdownNoteModels()` parses `**section** → - [model](url)` patterns from MarkdownNote nodes as a supplemental model source
|
||||
- **Format note updated** — "Flux.2 / Qwen-Image / Z-Image subgraph workflows supported" (EN/JA/ZH)
|
||||
|
||||
### v0.3.6
|
||||
- **Metadata Tab** — new tab (between Prompt and Gallery) that extracts model and prompt information from ComfyUI-generated PNG/WebP images and workflow JSON files; 3-column layout: drop zone with image preview (left), Checkpoint/VAE/Diffusion Model/Text Encoder lists (center), LoRA list with strength values + Prompt list with full-text viewer (right)
|
||||
- **Prompt action buttons** — Copy, GenUI:P/N (set GenerateUI prompts), Prompt:P/N (set Prompt tab preset prompts) directly from the extracted prompt text
|
||||
- **SDXL Prompt Styler support** — improved prompt extraction resolves `PromptStyler → CLIPTextEncode` link chains in both LiteGraph and API workflow formats
|
||||
- **Format support** — ComfyUI (PNG/WebP/JSON), SD WebUI, SD Forge, Fooocus
|
||||
|
||||
### v0.3.5
|
||||
- **Feeder subtab** — new subtab in GenerateUI for controlling [comfyui-image-feeder](https://github.com/ketle-man/comfyui-image-feeder) nodes; left pane: node selector + all ImageFeeder parameters + presets + Run/Stop controls; center pane: folder tree, image grid with checkbox selection, and preview panel
|
||||
- **Run loop with After gen modes** — Loop (wrap and continue), Increment (advance and auto-stop), Fixed (constant index); index updates automatically via WebSocket `image_loop_node_sync` after each generation; seed comes from the right-pane seed setting
|
||||
- **Workflow analysis improvements** — `analyzeWorkflow` now handles multi-hop CONDITIONING chains via BFS (up to 5 iterations), CLIPTextEncodeSDXL / CLIPTextEncodeSDXLRefiner, SDXLPromptStyler / SDXLPromptStylerAdvanced, KSamplerAdvanced (`noise_seed`), TextEncodeQwenImageEditPlus, PrimitiveStringMultiline, Power Lora Loader (rgthree), and `Checkpoint Loader` (WAS) with space in the class name
|
||||
|
||||
### v0.3.4
|
||||
- **Wildcard support panel** — new right column in the Prompt tab with a one-click toolbar for inserting wildcard syntax (`{|}`, `{n$|}`, `__|__`, `:`, `;`, `$`, `<lora::1:LBW=;>`, `[]`, single/multi pick) and wrapping selected text; dedicated prompt textarea and wildcard file picker (click to insert `__filename__`)
|
||||
- **Wildcard file manager** — create, view, edit, and delete `.txt` / `.yaml` wildcard files stored in `user/default/Workflow-Studio/wildcard/`; editor opens inline with filename input and save/cancel controls
|
||||
- **Preset/Preset Manager tab switch** — the center pane of the Prompt tab is now a two-tab panel (Presets / Preset Manager) to reclaim horizontal space for the new wildcard panel
|
||||
- **Wildcard Integration setting** — new accordion section in Settings tab; auto-detects ComfyUI-Impact-Pack; when installed, provides a button to create a directory junction (Windows) or symlink (other OS) from the WFS wildcard directory to Impact Pack's `wildcards/` directory; existing WFS files are migrated automatically; a Remove Link button reverses the operation
|
||||
|
||||
### v0.3.2
|
||||
- **Checkpoint Batch** — new panel in the GenerateUI right column (below Seed, above results); enable via checkbox to auto-generate once per checkpoint model; filter included/excluded subfolders by comma-separated names (empty = all checkpoints); real-time preview of matched model count; amber progress bar shows current model name and index; Stop button aborts the loop after the current generation completes; failed models are counted and reported in the summary toast
|
||||
- **Seed layout fix** — seed value input and mode selector (Random / Fixed / Increment / Decrement) now stacked vertically so neither is clipped in narrow panels
|
||||
@@ -366,7 +542,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 +550,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
|
||||
|
||||
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 975 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -179,13 +179,20 @@ def _load_data_file(filename: str):
|
||||
return None
|
||||
|
||||
|
||||
# エクスポートから除外するsettings.jsonのキー(APIキーなどの機密情報)
|
||||
_SETTINGS_EXPORT_EXCLUDE = {"civitai_api_key"}
|
||||
|
||||
|
||||
def _build_export_bundle() -> dict:
|
||||
"""Collect all data files into a single export bundle."""
|
||||
bundle = {"__version": 1, "__source": "ComfyUI-Workflow-Studio"}
|
||||
for filename in _DATA_FILES:
|
||||
data = _load_data_file(filename)
|
||||
if data is not None:
|
||||
bundle[filename] = data
|
||||
if data is None:
|
||||
continue
|
||||
if filename == "settings.json" and isinstance(data, dict):
|
||||
data = {k: v for k, v in data.items() if k not in _SETTINGS_EXPORT_EXCLUDE}
|
||||
bundle[filename] = data
|
||||
return bundle
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -3,6 +3,8 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import ssl
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import URLError, HTTPError
|
||||
@@ -11,9 +13,48 @@ from ..config import DATA_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _make_ssl_context():
|
||||
"""Return an SSL context with CA verification.
|
||||
|
||||
1. certifi CA bundle (available in virtually all ComfyUI envs via torch→requests→certifi)
|
||||
2. System default SSL context (OS certificate store)
|
||||
Returns None if both fail — callers fall back to urlopen without context
|
||||
(Python's own default, which also uses certifi when available).
|
||||
SSL verification is never disabled to avoid MitM exposure.
|
||||
"""
|
||||
try:
|
||||
import certifi
|
||||
return ssl.create_default_context(cafile=certifi.where())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return ssl.create_default_context()
|
||||
except Exception:
|
||||
pass
|
||||
logger.warning("CivitAI: could not build SSL context; falling back to urllib default")
|
||||
return None
|
||||
|
||||
|
||||
_SSL_CONTEXT = _make_ssl_context # lazy sentinel
|
||||
|
||||
|
||||
def _get_ssl_context():
|
||||
global _SSL_CONTEXT
|
||||
if callable(_SSL_CONTEXT):
|
||||
_SSL_CONTEXT = _make_ssl_context()
|
||||
return _SSL_CONTEXT
|
||||
|
||||
|
||||
CIVITAI_API_BASE = "https://civitai.com/api/v1"
|
||||
CIVITAI_CACHE_FILE = DATA_DIR / "civitai_cache.json"
|
||||
|
||||
# HTTPステータスコード: 指数バックオフでリトライする対象
|
||||
_RETRY_CODES = {429, 500, 502, 503, 504}
|
||||
_MAX_RETRIES = 3
|
||||
# POST /model-versions/by-hash は最大100件まで一括送信可能
|
||||
_BATCH_CHUNK_SIZE = 100
|
||||
|
||||
|
||||
class CivitaiService:
|
||||
"""Fetch and cache CivitAI model metadata."""
|
||||
@@ -21,6 +62,8 @@ class CivitaiService:
|
||||
def __init__(self):
|
||||
self._cache = None
|
||||
|
||||
# ── キャッシュ管理 ────────────────────────────────────────
|
||||
|
||||
def _load_cache(self):
|
||||
if self._cache is not None:
|
||||
return self._cache
|
||||
@@ -42,12 +85,37 @@ class CivitaiService:
|
||||
def get_cached(self, sha256_hash):
|
||||
"""Return cached CivitAI data for a given hash, or None."""
|
||||
cache = self._load_cache()
|
||||
return cache.get(sha256_hash)
|
||||
return cache.get(sha256_hash.lower())
|
||||
|
||||
def get_all_cached(self):
|
||||
"""Return the full cache dict."""
|
||||
return self._load_cache()
|
||||
|
||||
# ── APIキー / ヘッダー ────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _get_api_key():
|
||||
"""Return CivitAI API key. Env var CIVITAI_API_KEY takes priority over settings.json."""
|
||||
import os
|
||||
env_key = os.environ.get("CIVITAI_API_KEY", "").strip()
|
||||
if env_key:
|
||||
return env_key
|
||||
try:
|
||||
from ..services.settings_service import SettingsService
|
||||
return SettingsService().load().get("civitai_api_key", "").strip() or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _build_headers(self):
|
||||
"""Build request headers, optionally including Bearer token."""
|
||||
headers = {"User-Agent": "ComfyUI-Workflow-Studio/1.0"}
|
||||
api_key = self._get_api_key()
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
return headers
|
||||
|
||||
# ── ハッシュ計算 ──────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def calculate_sha256(file_path, chunk_size=65536):
|
||||
"""Calculate SHA256 hash of a file."""
|
||||
@@ -61,43 +129,130 @@ class CivitaiService:
|
||||
if not chunk:
|
||||
break
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
return h.hexdigest() # 小文字16進数
|
||||
|
||||
# ── 単体フェッチ (GET) ────────────────────────────────────
|
||||
|
||||
def fetch_by_hash(self, sha256_hash):
|
||||
"""Fetch model version info from CivitAI by SHA256 hash.
|
||||
"""Fetch model version info from CivitAI by SHA256 hash (GET).
|
||||
|
||||
429/5xx は指数バックオフでリトライ。
|
||||
Returns dict with model info, or None if not found.
|
||||
Caches successful results.
|
||||
"""
|
||||
# Check cache first
|
||||
sha256_lower = sha256_hash.lower()
|
||||
cache = self._load_cache()
|
||||
if sha256_hash in cache:
|
||||
return cache[sha256_hash]
|
||||
if sha256_lower in cache:
|
||||
return cache[sha256_lower]
|
||||
|
||||
url = f"{CIVITAI_API_BASE}/model-versions/by-hash/{sha256_hash}"
|
||||
try:
|
||||
req = Request(url, headers={"User-Agent": "ComfyUI-Workflow-Studio/1.0"})
|
||||
with urlopen(req, timeout=15) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
url = f"{CIVITAI_API_BASE}/model-versions/by-hash/{sha256_hash.upper()}"
|
||||
|
||||
if not data or "id" not in data:
|
||||
for attempt in range(_MAX_RETRIES):
|
||||
try:
|
||||
req = Request(url, headers=self._build_headers())
|
||||
with urlopen(req, timeout=15, context=_get_ssl_context()) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
if not data or "id" not in data:
|
||||
return None
|
||||
|
||||
info = self._extract_info(data)
|
||||
cache[sha256_lower] = info
|
||||
self._save_cache()
|
||||
return info
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
logger.debug("CivitAI: model not found for hash %s", sha256_hash[:16])
|
||||
return None
|
||||
if e.code in _RETRY_CODES and attempt < _MAX_RETRIES - 1:
|
||||
wait = 2 ** attempt
|
||||
logger.warning("CivitAI %s: retrying in %ss (attempt %d/%d)",
|
||||
e.code, wait, attempt + 1, _MAX_RETRIES)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
logger.warning("CivitAI API error: %s %s", e.code, e.reason)
|
||||
return None
|
||||
except (URLError, Exception) as e:
|
||||
if attempt < _MAX_RETRIES - 1:
|
||||
wait = 2 ** attempt
|
||||
logger.warning("CivitAI request failed: %s — retrying in %ss", e, wait)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
logger.warning("CivitAI request failed: %s", e)
|
||||
return None
|
||||
|
||||
# Extract useful fields
|
||||
info = self._extract_info(data)
|
||||
cache[sha256_hash] = info
|
||||
self._save_cache()
|
||||
return info
|
||||
return None
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
logger.debug("CivitAI: model not found for hash %s", sha256_hash[:16])
|
||||
return None
|
||||
logger.warning("CivitAI API error: %s %s", e.code, e.reason)
|
||||
return None
|
||||
except (URLError, Exception) as e:
|
||||
logger.warning("CivitAI request failed: %s", e)
|
||||
return None
|
||||
# ── 一括フェッチ (POST) ───────────────────────────────────
|
||||
|
||||
def _batch_fetch_post(self, sha256_hashes):
|
||||
"""POST /model-versions/by-hash でハッシュリストを一括取得(最大100件/リクエスト)。
|
||||
|
||||
レスポンスの files[].hashes.SHA256 でリクエストのハッシュと照合する。
|
||||
Returns: { sha256_lower: info_or_none }
|
||||
"""
|
||||
if not sha256_hashes:
|
||||
return {}
|
||||
|
||||
url = f"{CIVITAI_API_BASE}/model-versions/by-hash"
|
||||
results = {h.lower(): None for h in sha256_hashes}
|
||||
cache = self._load_cache()
|
||||
|
||||
for chunk_start in range(0, len(sha256_hashes), _BATCH_CHUNK_SIZE):
|
||||
chunk = sha256_hashes[chunk_start:chunk_start + _BATCH_CHUNK_SIZE]
|
||||
chunk_lower = {h.lower() for h in chunk}
|
||||
|
||||
for attempt in range(_MAX_RETRIES):
|
||||
try:
|
||||
body = json.dumps([h.upper() for h in chunk]).encode("utf-8")
|
||||
req = Request(
|
||||
url,
|
||||
data=body,
|
||||
headers={**self._build_headers(), "Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urlopen(req, timeout=30, context=_get_ssl_context()) as resp:
|
||||
versions = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
# 各バージョンを files[].hashes.SHA256 でリクエストのハッシュと照合
|
||||
for version_data in versions:
|
||||
info = self._extract_info(version_data)
|
||||
matched = False
|
||||
for f in version_data.get("files", []):
|
||||
file_sha256 = f.get("hashes", {}).get("SHA256", "").lower()
|
||||
if file_sha256 in chunk_lower:
|
||||
cache[file_sha256] = info
|
||||
results[file_sha256] = info
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
# files にハッシュがない場合は versionId で照合を試みる
|
||||
logger.debug("CivitAI: could not match version %s to a requested hash",
|
||||
version_data.get("id"))
|
||||
break # チャンク成功
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code in _RETRY_CODES and attempt < _MAX_RETRIES - 1:
|
||||
wait = 2 ** attempt
|
||||
logger.warning("CivitAI batch POST %s: retrying in %ss", e.code, wait)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
logger.warning("CivitAI batch POST error: %s %s", e.code, e.reason)
|
||||
break
|
||||
except (URLError, Exception) as e:
|
||||
if attempt < _MAX_RETRIES - 1:
|
||||
wait = 2 ** attempt
|
||||
logger.warning("CivitAI batch POST failed: %s — retrying in %ss", e, wait)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
logger.warning("CivitAI batch POST failed: %s", e)
|
||||
break
|
||||
|
||||
self._save_cache()
|
||||
return results
|
||||
|
||||
# ── 情報抽出 ──────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _extract_info(data):
|
||||
@@ -106,101 +261,170 @@ class CivitaiService:
|
||||
images = data.get("images", [])
|
||||
files = data.get("files", [])
|
||||
|
||||
# Get primary file info
|
||||
primary_file = None
|
||||
for f in files:
|
||||
if f.get("primary"):
|
||||
primary_file = f
|
||||
break
|
||||
# プライマリファイルを特定
|
||||
primary_file = next((f for f in files if f.get("primary")), None)
|
||||
if not primary_file and files:
|
||||
primary_file = files[0]
|
||||
|
||||
# Get image URLs (first 5)
|
||||
image_urls = []
|
||||
# 画像情報(URL・寸法・NSFWレベル)を最大5件取得
|
||||
image_list = []
|
||||
for img in images[:5]:
|
||||
url = img.get("url", "")
|
||||
if url:
|
||||
# Optimize for thumbnail
|
||||
optimized = url.replace("/original=true", "/width=450,optimized=true")
|
||||
image_urls.append(optimized)
|
||||
img_url = img.get("url", "")
|
||||
if not img_url:
|
||||
continue
|
||||
image_list.append({
|
||||
"url": img_url,
|
||||
"width": img.get("width"),
|
||||
"height": img.get("height"),
|
||||
"nsfwLevel": img.get("nsfwLevel", 0),
|
||||
})
|
||||
|
||||
# プライマリファイルのメタ情報(精度・フォーマット)
|
||||
file_meta = {}
|
||||
file_hashes = {}
|
||||
if primary_file:
|
||||
pm = primary_file.get("metadata", {})
|
||||
file_meta = {
|
||||
"fp": pm.get("fp"),
|
||||
"size": pm.get("size"),
|
||||
"format": pm.get("format"),
|
||||
}
|
||||
# BLAKE3, SHA256, AutoV2 等のハッシュ
|
||||
file_hashes = primary_file.get("hashes", {})
|
||||
|
||||
stats = data.get("stats", {})
|
||||
|
||||
# バッチ POST API は model オブジェクト内に id を含まない場合がある
|
||||
# トップレベルの modelId をフォールバックとして使用する
|
||||
model_id = model.get("id") or data.get("modelId")
|
||||
version_id = data.get("id", "")
|
||||
|
||||
return {
|
||||
"versionId": data.get("id"),
|
||||
"modelId": model.get("id"),
|
||||
"versionId": version_id,
|
||||
"modelId": model_id,
|
||||
"modelName": model.get("name", ""),
|
||||
"versionName": data.get("name", ""),
|
||||
"type": model.get("type", ""),
|
||||
"description": data.get("description") or model.get("description", ""),
|
||||
"tags": model.get("tags", []),
|
||||
"nsfw": model.get("nsfw", False),
|
||||
"nsfwLevel": data.get("nsfwLevel", 0),
|
||||
"air": data.get("air", ""),
|
||||
"creator": data.get("creator", {}).get("username", ""),
|
||||
"images": image_urls,
|
||||
# 後方互換のため URLリストを維持しつつ詳細情報も保存
|
||||
"images": [img["url"] for img in image_list],
|
||||
"imageDetails": image_list,
|
||||
"trainedWords": data.get("trainedWords", []),
|
||||
"baseModel": data.get("baseModel", ""),
|
||||
"fileSize": primary_file.get("sizeKB", 0) if primary_file else 0,
|
||||
"fileMeta": file_meta,
|
||||
"fileHashes": file_hashes,
|
||||
"downloadUrl": data.get("downloadUrl", ""),
|
||||
"modelUrl": f"https://civitai.com/models/{model.get('id', '')}?modelVersionId={data.get('id', '')}",
|
||||
"modelUrl": (
|
||||
f"https://civitai.com/models/{model_id}?modelVersionId={version_id}"
|
||||
if model_id else (
|
||||
f"https://civitai.com/model-versions/{version_id}"
|
||||
if version_id else ""
|
||||
)
|
||||
),
|
||||
"stats": {
|
||||
"downloadCount": stats.get("downloadCount", 0),
|
||||
"thumbsUpCount": stats.get("thumbsUpCount", 0),
|
||||
"thumbsDownCount": stats.get("thumbsDownCount", 0),
|
||||
},
|
||||
"updatedAt": data.get("updatedAt", ""),
|
||||
"publishedAt": data.get("publishedAt", ""),
|
||||
}
|
||||
|
||||
# ── バッチフェッチ ────────────────────────────────────────
|
||||
|
||||
def batch_fetch(self, model_files, progress_callback=None):
|
||||
"""Batch fetch CivitAI info for multiple model files.
|
||||
|
||||
Phase 1: SHA256 計算("hashing")
|
||||
Phase 2: POST で一括取得("fetching")— キャッシュ済みはスキップ
|
||||
|
||||
Args:
|
||||
model_files: list of (model_name, file_path) tuples
|
||||
progress_callback: fn(current, total, model_name, status) called per model
|
||||
|
||||
Returns: dict of { model_name: { sha256, civitai_info_or_none } }
|
||||
"""
|
||||
import time
|
||||
|
||||
results = {}
|
||||
total = len(model_files)
|
||||
cache = self._load_cache()
|
||||
|
||||
# Phase 1: ハッシュ計算
|
||||
hashes_needed = [] # [(model_name, sha256_lower, original_index)]
|
||||
|
||||
for i, (model_name, file_path) in enumerate(model_files):
|
||||
if progress_callback:
|
||||
progress_callback(i, total, model_name, "hashing")
|
||||
|
||||
# Calculate hash
|
||||
sha256 = self.calculate_sha256(file_path)
|
||||
if not sha256:
|
||||
results[model_name] = {"sha256": None, "civitai": None, "error": "hash_failed"}
|
||||
continue
|
||||
|
||||
results[model_name] = {"sha256": sha256, "civitai": None}
|
||||
|
||||
# Check cache
|
||||
if sha256 in cache:
|
||||
results[model_name]["civitai"] = cache[sha256]
|
||||
if progress_callback:
|
||||
progress_callback(i, total, model_name, "cached")
|
||||
progress_callback(i + 1, total, model_name, "not_found")
|
||||
continue
|
||||
|
||||
# Fetch from API
|
||||
sha256_lower = sha256.lower()
|
||||
results[model_name] = {"sha256": sha256_lower, "civitai": None}
|
||||
|
||||
if sha256_lower in cache:
|
||||
results[model_name]["civitai"] = cache[sha256_lower]
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, total, model_name, "cached")
|
||||
else:
|
||||
hashes_needed.append((model_name, sha256_lower, i))
|
||||
|
||||
# Phase 2: POST で一括取得(未キャッシュ分)
|
||||
if hashes_needed:
|
||||
if progress_callback:
|
||||
progress_callback(i, total, model_name, "fetching")
|
||||
progress_callback(len(results), total, "", "fetching")
|
||||
|
||||
info = self.fetch_by_hash(sha256)
|
||||
if info:
|
||||
results[model_name]["civitai"] = info
|
||||
# 同一ハッシュが複数モデルに対応する場合を考慮
|
||||
sha256_to_entries: dict[str, list] = {}
|
||||
for name, sha256_lower, idx in hashes_needed:
|
||||
sha256_to_entries.setdefault(sha256_lower, []).append((name, idx))
|
||||
|
||||
if progress_callback:
|
||||
status = "found" if info else "not_found"
|
||||
progress_callback(i, total, model_name, status)
|
||||
batch_results = self._batch_fetch_post(list(sha256_to_entries.keys()))
|
||||
|
||||
# Rate limit: small delay between API calls
|
||||
time.sleep(0.5)
|
||||
for sha256_lower, info in batch_results.items():
|
||||
for name, idx in sha256_to_entries.get(sha256_lower, []):
|
||||
results[name]["civitai"] = info
|
||||
if progress_callback:
|
||||
status = "found" if info else "not_found"
|
||||
progress_callback(idx + 1, total, name, status)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(total, total, "", "done")
|
||||
|
||||
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, context=_get_ssl_context()) 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()
|
||||
if sha256_hash:
|
||||
cache.pop(sha256_hash, None)
|
||||
cache.pop(sha256_hash.lower(), None)
|
||||
else:
|
||||
cache.clear()
|
||||
self._save_cache()
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.22"
|
||||
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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,27 @@ 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-gen-12": "helpGen12",
|
||||
"wfm-help-gen-13": "helpGen13", "wfm-help-gen-14": "helpGen14",
|
||||
"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-settings-11": "helpSettings11", "wfm-help-settings-12": "helpSettings12",
|
||||
"wfm-help-settings-13": "helpSettings13",
|
||||
"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 +362,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 +370,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 +429,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 +460,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initModelsTab();
|
||||
initGenerateTab();
|
||||
initPromptTab();
|
||||
initMetadataTab();
|
||||
initGalleryTab();
|
||||
initAiTab();
|
||||
|
||||
console.log("Workflow Studio: App initialized");
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -305,6 +305,21 @@ export const comfyWorkflow = {
|
||||
// 2. Map widgets_values to widget input names using object_info
|
||||
const widgets = node.widgets_values || [];
|
||||
if (widgets.length > 0) {
|
||||
// Lora Loader (LoraManager): use __lm_widget_ids from node properties
|
||||
// widgets_values: [autocomplete_meta_obj, text_str, loras_array]
|
||||
// API format: {__lm_autocomplete_meta_text: obj, text: str, loras: {__value__: arr}}
|
||||
const lmWidgetIds = node.properties?.__lm_widget_ids;
|
||||
if (lmWidgetIds && Array.isArray(lmWidgetIds)) {
|
||||
lmWidgetIds.forEach((name, idx) => {
|
||||
if (idx >= widgets.length || linkedInputNames.has(name)) return;
|
||||
let val = widgets[idx];
|
||||
if (name === "loras" && Array.isArray(val)) {
|
||||
val = { "__value__": val };
|
||||
}
|
||||
inputs[name] = val;
|
||||
});
|
||||
} else {
|
||||
|
||||
const widgetNames = _getWidgetInputNames(objectInfo, node.type);
|
||||
|
||||
if (widgetNames.length > 0) {
|
||||
@@ -343,6 +358,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 {
|
||||
@@ -356,6 +389,7 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
}
|
||||
} // end else (non-LoraManager path)
|
||||
}
|
||||
|
||||
api[nodeId] = {
|
||||
@@ -446,7 +480,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 +496,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 +672,7 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// --- LoRA nodes ---
|
||||
if (ct === "LoraLoader" || ct === "LoraLoaderModelOnly") {
|
||||
result.lora_nodes.push({
|
||||
id, type: ct, title,
|
||||
@@ -513,14 +682,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 +714,7 @@ export const comfyWorkflow = {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Text encoder (CLIP) nodes ---
|
||||
if (ct === "DualCLIPLoader" || ct === "CLIPLoader") {
|
||||
result.text_encoder_nodes.push({
|
||||
id, type: ct, title,
|
||||
@@ -536,12 +723,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,
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -240,6 +240,10 @@ const LANGUAGES = {
|
||||
modelsBadges: "Badges",
|
||||
modelsInfo: "Info",
|
||||
modelsFavorite: "Favorite",
|
||||
modelsBatch: "Batch",
|
||||
modelsBatchClear: "Clear Batch",
|
||||
promptBatchClear: "Cleared batch presets",
|
||||
wfBatchClear: "Cleared batch workflows",
|
||||
modelsMemo: "Memo",
|
||||
modelsMemoPlaceholder: "Notes about this model...",
|
||||
modelsSave: "Save",
|
||||
@@ -273,6 +277,7 @@ const LANGUAGES = {
|
||||
modelsGroupExists: "Group already exists",
|
||||
modelsRenamePrompt: "Enter new group name:",
|
||||
modelsDeleteGroupConfirm: "Delete group \"{name}\"?",
|
||||
modelsGroupReserved: "This group is reserved and cannot be deleted or renamed.",
|
||||
modelEnable: "Enable",
|
||||
modelDisable: "Disable",
|
||||
modelDisabled: "Disabled",
|
||||
@@ -295,12 +300,30 @@ const LANGUAGES = {
|
||||
modelBulkDeleteConfirm: "Permanently delete {count} model file(s) and their preview images? This cannot be undone.",
|
||||
modelBulkDeleteDone: "model file(s) deleted",
|
||||
modelBulkDeleteError: "Failed to delete some models",
|
||||
modelBulkDeselectAll: "Deselect All",
|
||||
modelBulkFavAdd: "★ Favorite",
|
||||
modelBulkFavRemove: "☆ Unfavorite",
|
||||
modelBulkFavDone: "models favorited",
|
||||
modelBulkUnfavDone: "models unfavorited",
|
||||
modelBulkBadgeApply: "+Badge",
|
||||
modelBulkBadgeRemove: "−Badge",
|
||||
modelBulkBadgeApplyDone: "badge applied to models",
|
||||
modelBulkBadgeRemoveDone: "badge removed from models",
|
||||
modelBulkNoBadge: "Select badge...",
|
||||
civitaiFetchDesc: "Fetch model information from CivitAI using SHA256 hash.",
|
||||
civitaiFetch: "Fetch from CivitAI",
|
||||
civitaiHashing: "Calculating hash... (may take a while for large files)",
|
||||
civitaiFound: "CivitAI info loaded",
|
||||
civitaiNotFound: "Model not found on CivitAI",
|
||||
civitaiNotFoundDesc: "This model was checked but not found on CivitAI.",
|
||||
civitaiRefetchBtn: "Re-check CivitAI",
|
||||
civitaiError: "Error fetching from CivitAI",
|
||||
civitaiType: "Type",
|
||||
civitaiBaseModel: "Base Model",
|
||||
civitaiHashLabel: "Hash",
|
||||
civitaiCopyHash: "Click to copy hash",
|
||||
civitaiHashCopied: "Hash copied",
|
||||
civitaiOpenImage: "Open image in new tab",
|
||||
civitaiTriggerWords: "Trigger Words",
|
||||
civitaiCopyWord: "Click to copy",
|
||||
civitaiWordCopied: "Trigger word copied",
|
||||
@@ -309,6 +332,19 @@ const LANGUAGES = {
|
||||
civitaiHashing2: "Hashing...",
|
||||
civitaiFetching: "Fetching...",
|
||||
civitaiBatchDone: (found, notFound) => `CivitAI: ${found} found, ${notFound} not found`,
|
||||
civitaiApiKeySetting: "CivitAI API Key",
|
||||
civitaiApiKeyHint: "Optional. Enables authenticated access. Obtain at civitai.com → Account Settings → API Keys.",
|
||||
civitaiApiKeyPlaceholder: "Paste your CivitAI API key here",
|
||||
civitaiApiKeySave: "Save",
|
||||
civitaiApiKeySaved: "CivitAI API key saved",
|
||||
civitaiTabInfo: "Info",
|
||||
civitaiTabSample: "Sample",
|
||||
civitaiNoImages: "No sample images available",
|
||||
civitaiHostSetting: "CivitAI Host",
|
||||
civitaiHostHint: "Choose which CivitAI site opens when you click a model link. civitai.com is SFW-only; civitai.red has no content restrictions.",
|
||||
civitaiHostCom: "civitai.com (SFW only)",
|
||||
civitaiHostRed: "civitai.red (unrestricted)",
|
||||
civitaiHostSaved: "CivitAI host saved",
|
||||
modelsSideInfo: "Info",
|
||||
modelsSideGroup: "Group",
|
||||
modelsSideCivitai: "CivitAI",
|
||||
@@ -324,6 +360,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",
|
||||
@@ -331,7 +413,7 @@ const LANGUAGES = {
|
||||
helpAboutDesc: "A centralized control UI for managing ComfyUI workflow JSON files, generating images via API, assisting with prompt creation, and browsing/organizing nodes.",
|
||||
helpAbout1: "Install via ComfyUI Manager or git clone to custom_nodes",
|
||||
helpAbout2: "Launch from the Workflow Studio button in the ComfyUI toolbar, or open http://127.0.0.1:8188/wfm",
|
||||
helpWf1: "Switch between Thumbnail, Card, and Table view modes",
|
||||
helpWf1: "Switch between Thumbnail and Table view modes",
|
||||
helpWf2: "Search, category filter, favorites filter, and group filter",
|
||||
helpWf3: "Create, rename, delete groups and assign/remove workflows",
|
||||
helpWf4: "Import JSON/PNG files via button or drag & drop",
|
||||
@@ -340,14 +422,29 @@ 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: "5-tab layout — Input (Prompt + Image), Model, Settings, Feeder, Batch — Input/Model/Settings each include a Raw JSON column for instant preview and direct editing",
|
||||
helpGen4: "Input tab: 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: "Batch type selector: check one of the column header checkboxes in the Batch Queue pane (Checkpoint / Lora / Prompt / Workflow) to activate that batch type — only one type can be active at a time; the Batch panel below Generate shows the active type, progress, and Pause/Resume/Stop controls",
|
||||
helpGen12: "Batch tab — 3-pane layout: Left pane (file tree) selects checkpoints by folder; use Filter to narrow the list, All/None buttons to select or clear all; initial state is all unchecked",
|
||||
helpGen13: "Batch tab — Center pane: group-based selection with inner tabs (Checkpoint | Lora | Prompt | Workflow); Checkpoint/Lora groups come from the Models tab, Prompt groups from the Prompt tab, Workflow groups from the Workflow tab — check a group header to select all its members, or expand ▶ to select individually",
|
||||
helpGen14: "Batch tab — Right pane (Batch Queue): shows items queued for each batch type; column headers (Checkpoint / Lora / Prompt / Workflow) each have a checkbox — check one to activate that batch type (radio behavior: only one at a time); count shown at top",
|
||||
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 +452,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 +463,10 @@ 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",
|
||||
helpSettings11: "Text Size: one slider (10–28 px) adjusts font size for all prompt and chat textareas at once — Generate UI positive/negative prompts, AI Assistant chat input, Preset positive/negative, Wildcard prompt and file editor, and Metadata prompt full preview; takes effect immediately and saved with Save Settings",
|
||||
helpSettings12: "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",
|
||||
helpSettings13: "CivitAI Host: choose between civitai.com (SFW only) and civitai.red (unrestricted) — determines which site opens when clicking a model link in the Models tab CivitAI panel",
|
||||
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",
|
||||
@@ -371,10 +474,10 @@ const LANGUAGES = {
|
||||
helpNodes5: "Side panel: Details (class name, package, category, description, flags), I/O Spec (inputs/outputs with types and defaults), Groups management",
|
||||
helpNodes6: "Node Sets: create reusable node collections with descriptions, tags, and link definitions between nodes",
|
||||
helpModels1: "Browse installed models: Checkpoint, LoRA, VAE, ControlNet, UNET, TextEncoder, Hypernetwork, Embedding with sub-tab switching",
|
||||
helpModels2: "Thumbnail, Card, and Table view modes with pagination",
|
||||
helpModels2: "Thumbnail 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 +489,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 +509,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 +555,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 +640,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: {
|
||||
@@ -763,6 +909,10 @@ const LANGUAGES = {
|
||||
modelsBadges: "バッジ",
|
||||
modelsInfo: "情報",
|
||||
modelsFavorite: "お気に入り",
|
||||
modelsBatch: "バッチ",
|
||||
modelsBatchClear: "バッチ一括解除",
|
||||
promptBatchClear: "バッチプリセットを一括解除しました",
|
||||
wfBatchClear: "バッチワークフローを一括解除しました",
|
||||
modelsMemo: "メモ",
|
||||
modelsMemoPlaceholder: "このモデルについてのメモ...",
|
||||
modelsSave: "保存",
|
||||
@@ -796,6 +946,7 @@ const LANGUAGES = {
|
||||
modelsGroupExists: "同名のグループが既に存在します",
|
||||
modelsRenamePrompt: "新しいグループ名を入力:",
|
||||
modelsDeleteGroupConfirm: "グループ「{name}」を削除しますか?",
|
||||
modelsGroupReserved: "このグループは予約済みのため削除・名前変更できません。",
|
||||
modelEnable: "有効化",
|
||||
modelDisable: "無効化",
|
||||
modelDisabled: "無効",
|
||||
@@ -818,12 +969,30 @@ const LANGUAGES = {
|
||||
modelBulkDeleteConfirm: "{count}件のモデルファイルとプレビュー画像を完全に削除しますか?この操作は取り消せません。",
|
||||
modelBulkDeleteDone: "件のモデルファイルを削除しました",
|
||||
modelBulkDeleteError: "一部のモデルの削除に失敗しました",
|
||||
modelBulkDeselectAll: "選択解除",
|
||||
modelBulkFavAdd: "★ お気に入り追加",
|
||||
modelBulkFavRemove: "☆ お気に入り解除",
|
||||
modelBulkFavDone: "件をお気に入りに追加しました",
|
||||
modelBulkUnfavDone: "件のお気に入りを解除しました",
|
||||
modelBulkBadgeApply: "+バッジ",
|
||||
modelBulkBadgeRemove: "−バッジ",
|
||||
modelBulkBadgeApplyDone: "件のモデルにバッジを適用しました",
|
||||
modelBulkBadgeRemoveDone: "件のモデルからバッジを削除しました",
|
||||
modelBulkNoBadge: "バッジを選択...",
|
||||
civitaiFetchDesc: "SHA256ハッシュを使用してCivitAIからモデル情報を取得します。",
|
||||
civitaiFetch: "CivitAIから取得",
|
||||
civitaiHashing: "ハッシュ計算中...(大きいファイルは時間がかかります)",
|
||||
civitaiFound: "CivitAI情報を取得しました",
|
||||
civitaiNotFound: "CivitAIにモデルが見つかりません",
|
||||
civitaiNotFoundDesc: "確認済みですが、このモデルはCivitAIに見つかりませんでした。",
|
||||
civitaiRefetchBtn: "再確認する",
|
||||
civitaiError: "CivitAIからの取得エラー",
|
||||
civitaiType: "タイプ",
|
||||
civitaiBaseModel: "ベースモデル",
|
||||
civitaiHashLabel: "ハッシュ",
|
||||
civitaiCopyHash: "クリックでハッシュをコピー",
|
||||
civitaiHashCopied: "ハッシュをコピーしました",
|
||||
civitaiOpenImage: "画像を別タブで開く",
|
||||
civitaiTriggerWords: "トリガーワード",
|
||||
civitaiCopyWord: "クリックでコピー",
|
||||
civitaiWordCopied: "トリガーワードをコピーしました",
|
||||
@@ -832,6 +1001,19 @@ const LANGUAGES = {
|
||||
civitaiHashing2: "ハッシュ計算中...",
|
||||
civitaiFetching: "取得中...",
|
||||
civitaiBatchDone: (found, notFound) => `CivitAI: ${found}件取得, ${notFound}件未登録`,
|
||||
civitaiApiKeySetting: "CivitAI APIキー",
|
||||
civitaiApiKeyHint: "省略可。認証済みアクセスが有効になります。civitai.com → アカウント設定 → API Keys で取得。",
|
||||
civitaiApiKeyPlaceholder: "CivitAI APIキーを貼り付けてください",
|
||||
civitaiApiKeySave: "保存",
|
||||
civitaiApiKeySaved: "CivitAI APIキーを保存しました",
|
||||
civitaiTabInfo: "情報",
|
||||
civitaiTabSample: "サンプル",
|
||||
civitaiNoImages: "サンプル画像がありません",
|
||||
civitaiHostSetting: "CivitAI ホスト",
|
||||
civitaiHostHint: "モデルリンクをクリックしたときに開くCivitAIサイトを選択します。civitai.comはSFWコンテンツのみ、civitai.redはコンテンツ制限なしです。",
|
||||
civitaiHostCom: "civitai.com(SFW のみ)",
|
||||
civitaiHostRed: "civitai.red(制限なし)",
|
||||
civitaiHostSaved: "CivitAI ホストを保存しました",
|
||||
modelsSideInfo: "基本情報",
|
||||
modelsSideGroup: "グループ",
|
||||
modelsSideCivitai: "CivitAI",
|
||||
@@ -847,6 +1029,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: "機能一覧",
|
||||
@@ -854,7 +1082,7 @@ const LANGUAGES = {
|
||||
helpAboutDesc: "ComfyUIのワークフローJSONファイルの管理、API接続による画像生成、プロンプト作成支援、ノードの閲覧・整理を行う集中コントロールUIです。",
|
||||
helpAbout1: "ComfyUIマネージャーからインストール、またはcustom_nodesフォルダへgit clone",
|
||||
helpAbout2: "ComfyUIツールバーのWorkflow Studioボタンから起動、またはhttp://127.0.0.1:8188/wfmを開く",
|
||||
helpWf1: "サムネイル / カード / テーブルの表示モード切り替え",
|
||||
helpWf1: "サムネイル / テーブルの表示モード切り替え",
|
||||
helpWf2: "テキスト検索、カテゴリフィルタ、お気に入りフィルタ、グループフィルタ",
|
||||
helpWf3: "グループの作成・名前変更・削除、ワークフローの割り当て・解除",
|
||||
helpWf4: "JSON/PNGファイルのインポート(ボタンまたはドラッグ&ドロップ)",
|
||||
@@ -863,14 +1091,29 @@ const LANGUAGES = {
|
||||
helpWf7: "バッジ管理:バッジの追加・名前変更・削除、カスタム色設定(Modelsタブと共有)— ⚙ Badgeボタン",
|
||||
helpGen1: "ComfyUIの接続・切断、接続状態の確認",
|
||||
helpGen2: "Workflowタブからワークフローを読み込み(App形式 .app.json は非対応)",
|
||||
helpGen3: "3タブ構成 — Input(プロンプト+画像)、Model、Settings — 各タブにRaw JSON列を常時表示",
|
||||
helpGen3: "5タブ構成 — Input(プロンプト+画像)、Model、Settings、Feeder、Batch — 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: "バッチタイプ選択:Batchキュー右ペインのカラムヘッダーチェックボックス(Checkpoint / Lora / Prompt / Workflow)で実行タイプを選択 — 同時に1種類のみ有効;Generateボタン下のBatchパネルで有効タイプ・進捗・一時停止/再開・停止を操作可能",
|
||||
helpGen12: "Batchタブ — 3ペインレイアウト:左ペイン(ファイルツリー)でフォルダ単位にチェックポイントを選択;フィルタで絞り込み、All/Noneボタンで一括選択・解除;初期状態は全てチェックなし",
|
||||
helpGen13: "Batchタブ — 中央ペイン:グループ選択(Checkpoint | Lora | Prompt | Workflowの内部タブ);Checkpoint/LoraグループはModelsタブ、PromptグループはPromptタブ、WorkflowグループはWorkflowタブの定義を反映 — グループヘッダーにチェックで全メンバーを選択、▶で展開して個別選択も可能",
|
||||
helpGen14: "Batchタブ — 右ペイン(バッチキュー):各バッチタイプのキュー項目を表示;カラムヘッダー(Checkpoint / Lora / Prompt / Workflow)のチェックボックスで実行タイプを選択(ラジオ動作:1種類のみ);上部に件数を表示",
|
||||
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 +1121,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 +1132,10 @@ const LANGUAGES = {
|
||||
helpSettings7: "Eagle連携URL、自動保存の有効/無効",
|
||||
helpSettings8: "データ管理:設定・メタデータ・プロンプトなど全データをJSONにエクスポート、またはインポートして復元。インポート後はComfyUIを再起動してください。",
|
||||
helpSettings9: "各セクションはアコーディオン式で折りたたみ可能",
|
||||
helpSettings10: "Wildcard Integration:WFSのワイルドカードディレクトリをComfyUI-Impact-Packのwildcardsディレクトリへリンク(Windowsはジャンクション、他OSはシンボリックリンク)。既存ファイルは自動で移行されます",
|
||||
helpSettings11: "テキストサイズ:スライダー1本(10〜28px)でプロンプト・チャット全テキストエリアのフォントサイズを一括調整 — GenerateUI ポジティブ/ネガティブプロンプト、AIアシスタントチャット入力、プリセット ポジティブ/ネガティブ、ワイルドカードプロンプト・ファイルエディタ、メタデータプロンプト全文プレビュー;即時反映・保存で確定",
|
||||
helpSettings12: "RAW JSON カラー:GenerateUI の Raw JSON エディタの構文ハイライト色(6色)をカスタマイズ — デフォルトテキスト・名前/スケジューラー・タイトル・幅/高さ・プロンプト/テキスト・画像/ファイル;カラーピッカーで即時反映;「デフォルトにリセット」で元の配色に戻す",
|
||||
helpSettings13: "CivitAI ホスト:civitai.com(SFWのみ)と civitai.red(制限なし)を切り替え — ModelsタブのCivitAIパネルでモデルリンクをクリックしたときに開くサイトを指定",
|
||||
helpNodes1: "ノードブラウザ:名前/説明/エイリアス/タグで検索、カテゴリ・パッケージ・タグ・グループ・お気に入りフィルタ",
|
||||
helpNodes2: "カード / テーブル表示モード切り替え(ページネーション付き)",
|
||||
helpNodes3: "ノードのお気に入り登録、カスタムタグの追加で整理",
|
||||
@@ -894,10 +1143,10 @@ const LANGUAGES = {
|
||||
helpNodes5: "サイドパネル:詳細(クラス名、パッケージ、カテゴリ、説明、フラグ)、I/O仕様(入出力の型・デフォルト値)、グループ管理",
|
||||
helpNodes6: "ノードセット:再利用可能なノードコレクションの作成(説明、タグ、ノード間リンク定義付き)",
|
||||
helpModels1: "インストール済みモデルの閲覧:Checkpoint、LoRA、VAE、ControlNet、UNET、TextEncoder、Hypernetwork、Embeddingをサブタブで切り替え",
|
||||
helpModels2: "サムネイル / カード / テーブル表示モード切り替え(ページネーション付き)",
|
||||
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 +1158,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 +1178,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 +1224,7 @@ const LANGUAGES = {
|
||||
comfyuiUrlHint: "ComfyUIプラグインとして動作する場合は空欄(同一オリジン推奨)。別サーバーの場合のみ設定してください。",
|
||||
connectedCheck: "接続成功 \u2713",
|
||||
failedConnect: "接続失敗 \u2717",
|
||||
ollamaSettings: "Ollama 設定",
|
||||
ollamaSettings: "Ollama 設定(プロンプトタブ)",
|
||||
ollamaUrl: "Ollama API URL",
|
||||
ollamaDefaultModel: "デフォルトモデル",
|
||||
selectModel: "モデルを選択...",
|
||||
@@ -1048,6 +1309,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: {
|
||||
@@ -1282,6 +1574,10 @@ const LANGUAGES = {
|
||||
modelsBadges: "标签",
|
||||
modelsInfo: "信息",
|
||||
modelsFavorite: "收藏",
|
||||
modelsBatch: "批次",
|
||||
modelsBatchClear: "清空批次",
|
||||
promptBatchClear: "已清空批次预设",
|
||||
wfBatchClear: "已清空批次工作流",
|
||||
modelsMemo: "备注",
|
||||
modelsMemoPlaceholder: "关于此模型的备注...",
|
||||
modelsSave: "保存",
|
||||
@@ -1315,6 +1611,7 @@ const LANGUAGES = {
|
||||
modelsGroupExists: "同名分组已存在",
|
||||
modelsRenamePrompt: "输入新分组名称:",
|
||||
modelsDeleteGroupConfirm: "删除分组「{name}」?",
|
||||
modelsGroupReserved: "此分组为系统保留分组,无法删除或重命名。",
|
||||
modelEnable: "启用",
|
||||
modelDisable: "禁用",
|
||||
modelDisabled: "已禁用",
|
||||
@@ -1337,12 +1634,30 @@ const LANGUAGES = {
|
||||
modelBulkDeleteConfirm: "永久删除 {count} 个模型文件及其预览图?此操作无法撤销。",
|
||||
modelBulkDeleteDone: "个模型文件已删除",
|
||||
modelBulkDeleteError: "部分模型删除失败",
|
||||
modelBulkDeselectAll: "取消全选",
|
||||
modelBulkFavAdd: "★ 收藏",
|
||||
modelBulkFavRemove: "☆ 取消收藏",
|
||||
modelBulkFavDone: "个模型已收藏",
|
||||
modelBulkUnfavDone: "个模型已取消收藏",
|
||||
modelBulkBadgeApply: "+徽章",
|
||||
modelBulkBadgeRemove: "−徽章",
|
||||
modelBulkBadgeApplyDone: "个模型已应用徽章",
|
||||
modelBulkBadgeRemoveDone: "个模型已移除徽章",
|
||||
modelBulkNoBadge: "选择徽章...",
|
||||
civitaiFetchDesc: "使用SHA256哈希从CivitAI获取模型信息。",
|
||||
civitaiFetch: "从CivitAI获取",
|
||||
civitaiHashing: "计算哈希中...(大文件可能需要较长时间)",
|
||||
civitaiFound: "已加载CivitAI信息",
|
||||
civitaiNotFound: "在CivitAI上未找到该模型",
|
||||
civitaiNotFoundDesc: "已检查,但在CivitAI上未找到此模型。",
|
||||
civitaiRefetchBtn: "重新检查",
|
||||
civitaiError: "从CivitAI获取时出错",
|
||||
civitaiType: "类型",
|
||||
civitaiBaseModel: "基础模型",
|
||||
civitaiHashLabel: "哈希",
|
||||
civitaiCopyHash: "点击复制哈希",
|
||||
civitaiHashCopied: "哈希已复制",
|
||||
civitaiOpenImage: "在新标签页中打开图片",
|
||||
civitaiTriggerWords: "触发词",
|
||||
civitaiCopyWord: "点击复制",
|
||||
civitaiWordCopied: "触发词已复制",
|
||||
@@ -1351,6 +1666,19 @@ const LANGUAGES = {
|
||||
civitaiHashing2: "计算哈希中...",
|
||||
civitaiFetching: "获取中...",
|
||||
civitaiBatchDone: (found, notFound) => `CivitAI: ${found}个已找到, ${notFound}个未找到`,
|
||||
civitaiApiKeySetting: "CivitAI API密钥",
|
||||
civitaiApiKeyHint: "可选。启用认证访问。在 civitai.com → 账户设置 → API Keys 获取。",
|
||||
civitaiApiKeyPlaceholder: "在此粘贴您的CivitAI API密钥",
|
||||
civitaiApiKeySave: "保存",
|
||||
civitaiApiKeySaved: "CivitAI API密钥已保存",
|
||||
civitaiTabInfo: "信息",
|
||||
civitaiTabSample: "样本图",
|
||||
civitaiNoImages: "没有样本图片",
|
||||
civitaiHostSetting: "CivitAI 站点",
|
||||
civitaiHostHint: "选择点击模型链接时打开的 CivitAI 站点。civitai.com 仅限 SFW 内容;civitai.red 无内容限制。",
|
||||
civitaiHostCom: "civitai.com(仅 SFW)",
|
||||
civitaiHostRed: "civitai.red(无限制)",
|
||||
civitaiHostSaved: "CivitAI 站点已保存",
|
||||
modelsSideInfo: "基本信息",
|
||||
modelsSideGroup: "分组",
|
||||
modelsSideCivitai: "CivitAI",
|
||||
@@ -1366,6 +1694,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: "功能列表",
|
||||
@@ -1373,7 +1747,7 @@ const LANGUAGES = {
|
||||
helpAboutDesc: "用于管理ComfyUI工作流JSON文件、通过API生成图片、辅助提示词创作、浏览和整理节点的集中控制UI。",
|
||||
helpAbout1: "通过ComfyUI Manager安装,或git clone到custom_nodes文件夹",
|
||||
helpAbout2: "从ComfyUI工具栏的Workflow Studio按钮启动,或打开http://127.0.0.1:8188/wfm",
|
||||
helpWf1: "切换缩略图/卡片/表格视图模式",
|
||||
helpWf1: "切换缩略图/表格视图模式",
|
||||
helpWf2: "文本搜索、分类筛选、收藏筛选、分组筛选",
|
||||
helpWf3: "创建、重命名、删除分组,分配/移除工作流",
|
||||
helpWf4: "通过按钮或拖放导入JSON/PNG文件",
|
||||
@@ -1382,14 +1756,29 @@ const LANGUAGES = {
|
||||
helpWf7: "徽章管理:添加、重命名、删除徽章并自定义颜色(与Models标签共享)— ⚙ Badge按钮",
|
||||
helpGen1: "连接/断开ComfyUI,检查连接状态",
|
||||
helpGen2: "从工作流标签页加载工作流(不支持App格式.app.json)",
|
||||
helpGen3: "3标签布局 — Input(提示词+图片)、Model、Settings — 每个标签含Raw JSON列实时预览",
|
||||
helpGen3: "5标签布局 — Input(提示词+图像)、Model、Settings、Feeder、Batch — 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 / Lora / Prompt / Workflow)激活对应批次类型 — 每次只能激活一种;Generate按钮下方的Batch面板显示当前类型、进度及暂停/继续/停止控制",
|
||||
helpGen12: "Batch标签 — 3窗格布局:左窗格(文件树)按文件夹选择检查点;使用筛选器缩小列表,All/None按钮全选或清除;初始状态全部未勾选",
|
||||
helpGen13: "Batch标签 — 中央窗格:基于组的选择(含Checkpoint | Lora | Prompt | Workflow内部标签);Checkpoint/Lora组来自Models标签,Prompt组来自Prompt标签,Workflow组来自Workflow标签 — 勾选组标题选中所有成员,▶展开后也可单独选择",
|
||||
helpGen14: "Batch标签 — 右窗格(批量队列):显示各批次类型的队列项目;列标题(Checkpoint / Lora / Prompt / Workflow)各有复选框 — 勾选一个激活该类型(单选行为);顶部显示数量",
|
||||
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 +1786,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 +1797,10 @@ const LANGUAGES = {
|
||||
helpSettings7: "Eagle集成URL、自动保存开关",
|
||||
helpSettings8: "数据管理:将所有插件数据(设置、元数据、提示词等)导出为JSON文件,或导入以恢复。导入后请重启ComfyUI。",
|
||||
helpSettings9: "各部分可折叠展开",
|
||||
helpSettings10: "Wildcard Integration:将WFS通配符目录链接到ComfyUI-Impact-Pack的wildcards目录(Windows为目录联结,其他OS为符号链接);现有文件自动迁移",
|
||||
helpSettings11: "文字大小:单个滑块(10–28 px)可一次性调整所有提示词和聊天文本框的字体大小 — GenerateUI正/负向提示词、AI助手聊天输入、预设正/负向提示、通配符提示和文件编辑器、元数据提示词全文预览;即时生效并通过「保存设置」保存",
|
||||
helpSettings12: "RAW JSON 配色:自定义GenerateUI中Raw JSON编辑器的6种语法高亮颜色 — 默认文本、名称/调度器、标题、宽度/高度、提示词/文本、图像/文件;颜色选择即时生效;「重置默认」恢复原始配色",
|
||||
helpSettings13: "CivitAI 站点:在 civitai.com(仅SFW)和 civitai.red(无限制)之间切换 — 决定在Models标签CivitAI面板中点击模型链接时打开的站点",
|
||||
helpNodes1: "节点浏览器:按名称/描述/别名/标签搜索,按分类、包、标签、组、收藏筛选",
|
||||
helpNodes2: "卡片和表格视图模式切换(带分页)",
|
||||
helpNodes3: "收藏节点,添加自定义标签进行整理",
|
||||
@@ -1413,10 +1808,10 @@ const LANGUAGES = {
|
||||
helpNodes5: "侧面板:详情(类名、包、分类、描述、标志)、I/O规格(输入输出类型和默认值)、组管理",
|
||||
helpNodes6: "节点集:创建可复用的节点集合(含描述、标签、节点间链接定义)",
|
||||
helpModels1: "浏览已安装模型:Checkpoint、LoRA、VAE、ControlNet、UNET、TextEncoder、Hypernetwork、Embedding,通过子标签切换",
|
||||
helpModels2: "缩略图、卡片和表格视图模式切换(带分页)",
|
||||
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 +1823,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 +1843,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 +1889,7 @@ const LANGUAGES = {
|
||||
comfyuiUrlHint: "作为ComfyUI插件运行时留空(推荐同源)。仅在不同服务器时设置。",
|
||||
connectedCheck: "已连接 \u2713",
|
||||
failedConnect: "连接失败 \u2717",
|
||||
ollamaSettings: "Ollama 设置",
|
||||
ollamaSettings: "Ollama 设置(提示词标签页)",
|
||||
ollamaUrl: "Ollama API URL",
|
||||
ollamaDefaultModel: "默认模型",
|
||||
selectModel: "选择模型...",
|
||||
@@ -1567,6 +1974,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。",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">"); }
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import { t } from "./i18n.js";
|
||||
import { comfyUI } from "./comfyui-client.js";
|
||||
import { comfyEditor } from "./comfyui-editor.js";
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────
|
||||
const RESERVED_GROUPS = ["Batch"];
|
||||
const BATCH_MODEL_TYPES = ["checkpoint", "lora"];
|
||||
|
||||
// ── State ─────────────────────────────────────────────────
|
||||
|
||||
const state = {
|
||||
@@ -35,7 +39,8 @@ const state = {
|
||||
groupFilter: "",
|
||||
statusFilter: "all", // "all" | "enabled" | "disabled"
|
||||
showFavoritesOnly: false,
|
||||
viewMode: localStorage.getItem("wfm_models_view") || "thumb",
|
||||
showBatchOnly: false,
|
||||
viewMode: localStorage.getItem("wfm_models_view") === "table" ? "table" : "thumb",
|
||||
activeModelType: "checkpoint",
|
||||
selectedModel: null,
|
||||
loaded: {},
|
||||
@@ -169,6 +174,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 +183,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;
|
||||
}
|
||||
@@ -256,20 +277,39 @@ function renderBulkActionBar() {
|
||||
? `<option value="">${t("modelsNoGroupAvailable")}</option>`
|
||||
: groupNames.map((g) => `<option value="${escapeHtml(g)}">${escapeHtml(g)}</option>`).join("");
|
||||
|
||||
const palette = getBadgePalette();
|
||||
const badgeLabels = Object.keys(palette).sort();
|
||||
const noBadges = badgeLabels.length === 0;
|
||||
const badgeOptions = noBadges
|
||||
? `<option value="">${t("modelBulkNoBadge")}</option>`
|
||||
: badgeLabels.map((b) => `<option value="${escapeHtml(b)}">${escapeHtml(b)}</option>`).join("");
|
||||
|
||||
bar.style.display = "flex";
|
||||
bar.innerHTML = `
|
||||
<span class="wfm-bulk-count">${count} ${t("modelSelected")}</span>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-bulk-deselect-all-btn">${t("modelBulkDeselectAll")}</button>
|
||||
<span class="wfm-bulk-sep">|</span>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-bulk-fav-add-btn" title="${t("modelBulkFavAdd")}">${t("modelBulkFavAdd")}</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-bulk-fav-remove-btn" title="${t("modelBulkFavRemove")}">${t("modelBulkFavRemove")}</button>
|
||||
<span class="wfm-bulk-sep">|</span>
|
||||
<select id="wfm-bulk-group-select" class="wfm-select" style="font-size:12px;">${groupOptions}</select>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-primary" id="wfm-bulk-add-btn"${noGroups ? " disabled" : ""}>${t("modelBulkAddGroup")}</button>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger" id="wfm-bulk-remove-btn"${noGroups ? " disabled" : ""}>${t("modelBulkRemoveGroup")}</button>
|
||||
<span class="wfm-bulk-sep">|</span>
|
||||
<input type="text" id="wfm-bulk-new-group-input" class="wfm-search-input" style="width:120px;font-size:12px;" placeholder="${t("modelsGroupName")}">
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-bulk-create-add-btn">${t("modelBulkCreateAdd")}</button>
|
||||
<span class="wfm-bulk-sep">|</span>
|
||||
<select id="wfm-bulk-badge-select" class="wfm-select" style="font-size:12px;">${badgeOptions}</select>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-primary" id="wfm-bulk-badge-apply-btn"${noBadges ? " disabled" : ""}>${t("modelBulkBadgeApply")}</button>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger" id="wfm-bulk-badge-remove-btn"${noBadges ? " disabled" : ""}>${t("modelBulkBadgeRemove")}</button>
|
||||
<span class="wfm-bulk-sep">|</span>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-bulk-clear-btn" title="Clear selection">×</button>
|
||||
<span class="wfm-bulk-sep" style="margin-left:8px;"></span>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger" id="wfm-bulk-delete-btn">${t("modelBulkDelete")}</button>
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger" id="wfm-bulk-delete-btn" style="margin-left:4px;">${t("modelBulkDelete")}</button>
|
||||
`;
|
||||
|
||||
document.getElementById("wfm-bulk-deselect-all-btn")?.addEventListener("click", clearSelection);
|
||||
document.getElementById("wfm-bulk-fav-add-btn")?.addEventListener("click", () => bulkSetFavorite(true));
|
||||
document.getElementById("wfm-bulk-fav-remove-btn")?.addEventListener("click", () => bulkSetFavorite(false));
|
||||
document.getElementById("wfm-bulk-add-btn")?.addEventListener("click", () => {
|
||||
const g = document.getElementById("wfm-bulk-group-select")?.value;
|
||||
if (g) bulkAddToGroup(g);
|
||||
@@ -285,6 +325,14 @@ function renderBulkActionBar() {
|
||||
if (state.modelGroups[name]) { showToast(t("modelsGroupExists"), "warning"); return; }
|
||||
bulkAddToGroup(name);
|
||||
});
|
||||
document.getElementById("wfm-bulk-badge-apply-btn")?.addEventListener("click", () => {
|
||||
const label = document.getElementById("wfm-bulk-badge-select")?.value;
|
||||
if (label) bulkApplyBadge(label, true);
|
||||
});
|
||||
document.getElementById("wfm-bulk-badge-remove-btn")?.addEventListener("click", () => {
|
||||
const label = document.getElementById("wfm-bulk-badge-select")?.value;
|
||||
if (label) bulkApplyBadge(label, false);
|
||||
});
|
||||
document.getElementById("wfm-bulk-clear-btn")?.addEventListener("click", clearSelection);
|
||||
document.getElementById("wfm-bulk-delete-btn")?.addEventListener("click", bulkDeleteModels);
|
||||
}
|
||||
@@ -312,6 +360,44 @@ async function bulkRemoveFromGroup(groupName) {
|
||||
renderBulkActionBar();
|
||||
}
|
||||
|
||||
async function bulkSetFavorite(isFav) {
|
||||
const models = [...state.selectedModels];
|
||||
let count = 0;
|
||||
for (const mn of models) {
|
||||
const meta = state.modelMetadata[mn] || {};
|
||||
if (meta.favorite === isFav) continue;
|
||||
await saveModelMetadata(mn, { favorite: isFav });
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
showToast(`${count} ${isFav ? t("modelBulkFavDone") : t("modelBulkUnfavDone")}`, "success");
|
||||
renderModelGrid();
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkApplyBadge(badgeLabel, add) {
|
||||
const models = [...state.selectedModels];
|
||||
let count = 0;
|
||||
for (const mn of models) {
|
||||
const meta = state.modelMetadata[mn] || {};
|
||||
const badges = [...(meta.badges || [])];
|
||||
if (add) {
|
||||
if (badges.includes(badgeLabel)) continue;
|
||||
badges.push(badgeLabel);
|
||||
} else {
|
||||
const idx = badges.indexOf(badgeLabel);
|
||||
if (idx === -1) continue;
|
||||
badges.splice(idx, 1);
|
||||
}
|
||||
await saveModelMetadata(mn, { badges });
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
showToast(`${count} ${add ? t("modelBulkBadgeApplyDone") : t("modelBulkBadgeRemoveDone")}`, "success");
|
||||
renderModelGrid();
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDeleteModels() {
|
||||
const count = state.selectedModels.size;
|
||||
if (count === 0) return;
|
||||
@@ -473,6 +559,11 @@ function filterModels() {
|
||||
});
|
||||
}
|
||||
|
||||
if (state.showBatchOnly) {
|
||||
const batchMembers = state.modelGroups["Batch"] || [];
|
||||
models = models.filter((m) => batchMembers.includes(m));
|
||||
}
|
||||
|
||||
if (state.tagFilter) {
|
||||
models = models.filter((m) => {
|
||||
const meta = state.modelMetadata[m];
|
||||
@@ -659,17 +750,8 @@ function renderGroupFilter() {
|
||||
const select = document.getElementById("wfm-models-group-filter");
|
||||
if (!select) return;
|
||||
|
||||
// Collect groups across all types with type labels
|
||||
const options = [];
|
||||
const typeOrder = Object.keys(TYPE_LABELS);
|
||||
typeOrder.forEach((type) => {
|
||||
const groups = state.allModelGroups[type];
|
||||
if (!groups) return;
|
||||
const names = Object.keys(groups).sort();
|
||||
names.forEach((g) => {
|
||||
options.push({ type, name: g });
|
||||
});
|
||||
});
|
||||
const groups = state.allModelGroups[state.activeModelType] || {};
|
||||
const names = Object.keys(groups).sort();
|
||||
|
||||
const currentValue = state.groupFilter
|
||||
? `${state.activeModelType}::${state.groupFilter}`
|
||||
@@ -677,11 +759,10 @@ function renderGroupFilter() {
|
||||
|
||||
select.innerHTML =
|
||||
`<option value="">${t("modelsAllGroups")}</option>` +
|
||||
options.map(({ type, name }) => {
|
||||
const typeLabel = TYPE_LABELS[type] || type;
|
||||
const value = `${type}::${name}`;
|
||||
names.map((name) => {
|
||||
const value = `${state.activeModelType}::${name}`;
|
||||
const isActive = value === currentValue;
|
||||
return `<option value="${escapeHtml(value)}"${isActive ? " selected" : ""}>[${typeLabel}] ${escapeHtml(name)}</option>`;
|
||||
return `<option value="${escapeHtml(value)}"${isActive ? " selected" : ""}>${escapeHtml(name)}</option>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
@@ -748,8 +829,6 @@ function renderModelGrid() {
|
||||
|
||||
if (state.viewMode === "table") {
|
||||
renderTableView(grid, filtered);
|
||||
} else if (state.viewMode === "card") {
|
||||
renderCardView(grid, filtered);
|
||||
} else {
|
||||
renderThumbView(grid, filtered);
|
||||
}
|
||||
@@ -767,6 +846,8 @@ function renderThumbView(grid, models) {
|
||||
const tagsHtml = (meta.tags || []).map((tag) => `<span class="wfm-badge wfm-badge-sm">${escapeHtml(tag)}</span>`).join("");
|
||||
const favStar = meta.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
const inBatch = isInBatch(modelName);
|
||||
const batchClass = inBatch ? "wfm-batch-btn active" : "wfm-batch-btn";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "wfm-card";
|
||||
@@ -784,6 +865,7 @@ function renderThumbView(grid, models) {
|
||||
<div class="wfm-card-title" title="${escapeHtml(modelName)}">${escapeHtml(getStem(name))}</div>
|
||||
<div class="wfm-card-meta">${userBadges} ${tagsHtml}</div>
|
||||
</div>
|
||||
<button class="${batchClass}" title="${t("modelsBatch")}">B</button>
|
||||
<button class="${favClass}" title="${t("modelsFavorite")}">${favStar}</button>
|
||||
<button class="wfm-toggle-btn${disabled ? " wfm-toggle-disabled" : ""}" title="${disabled ? t("modelEnable") : t("modelDisable")}">${disabled ? "▶" : "⏸"}</button>`;
|
||||
|
||||
@@ -792,6 +874,10 @@ function renderThumbView(grid, models) {
|
||||
const placeholder = card.querySelector(".wfm-card-thumb-placeholder");
|
||||
loadPreviewImage(img, placeholder, modelName);
|
||||
|
||||
card.querySelector(".wfm-batch-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleBatch(modelName);
|
||||
});
|
||||
card.querySelector(".wfm-fav-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(modelName);
|
||||
@@ -807,7 +893,7 @@ function renderThumbView(grid, models) {
|
||||
checkEl.className = "wfm-select-check" + (isChecked ? " checked" : "");
|
||||
card.appendChild(checkEl);
|
||||
card.addEventListener("click", (e) => {
|
||||
if (e.target.closest(".wfm-fav-btn, .wfm-toggle-btn")) return;
|
||||
if (e.target.closest(".wfm-batch-btn, .wfm-fav-btn, .wfm-toggle-btn")) return;
|
||||
toggleModelSelection(modelName);
|
||||
});
|
||||
} else {
|
||||
@@ -818,61 +904,6 @@ function renderThumbView(grid, models) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Card View (no thumbnail, compact) ─────────────────────
|
||||
|
||||
function renderCardView(grid, models) {
|
||||
grid.innerHTML = "";
|
||||
models.forEach((modelName) => {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
const { dir, name } = parseModelPath(modelName);
|
||||
const ext = getExtension(name);
|
||||
const disabled = isModelDisabled(modelName);
|
||||
const userBadges = modelBadgesHtml(modelName);
|
||||
const tagsHtml = (meta.tags || []).map((tag) => `<span class="wfm-badge wfm-badge-sm">${escapeHtml(tag)}</span>`).join("");
|
||||
const favStar = meta.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "wfm-card wfm-model-card";
|
||||
if (disabled) card.classList.add("wfm-model-disabled");
|
||||
if (state.selectedModel === modelName) card.classList.add("wfm-card-selected");
|
||||
card.dataset.modelName = modelName;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${escapeHtml(modelName)}">${escapeHtml(name)}${disabled ? ` <span class="wfm-badge wfm-badge-disabled">${t("modelDisabled")}</span>` : ""}</div>
|
||||
<div class="wfm-card-meta">
|
||||
${userBadges} ${tagsHtml}
|
||||
</div>
|
||||
</div>
|
||||
<button class="${favClass}" title="${t("modelsFavorite")}">${favStar}</button>
|
||||
<button class="wfm-toggle-btn${disabled ? " wfm-toggle-disabled" : ""}" title="${disabled ? t("modelEnable") : t("modelDisable")}">${disabled ? "▶" : "⏸"}</button>`;
|
||||
|
||||
card.querySelector(".wfm-fav-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(modelName);
|
||||
});
|
||||
card.querySelector(".wfm-toggle-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleModelEnable(modelName);
|
||||
});
|
||||
if (state.selectMode) {
|
||||
const isChecked = state.selectedModels.has(modelName);
|
||||
card.classList.toggle("wfm-card-checked", isChecked);
|
||||
const checkEl = document.createElement("div");
|
||||
checkEl.className = "wfm-select-check" + (isChecked ? " checked" : "");
|
||||
card.appendChild(checkEl);
|
||||
card.addEventListener("click", (e) => {
|
||||
if (e.target.closest(".wfm-fav-btn, .wfm-toggle-btn")) return;
|
||||
toggleModelSelection(modelName);
|
||||
});
|
||||
} else {
|
||||
card.addEventListener("click", () => showSidePanel(modelName));
|
||||
card.addEventListener("dblclick", (e) => { e.stopPropagation(); openDetailModal(modelName); });
|
||||
}
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Table View ────────────────────────────────────────────
|
||||
|
||||
@@ -885,6 +916,7 @@ function renderTableView(grid, models) {
|
||||
const disabled = isModelDisabled(modelName);
|
||||
const isChecked = state.selectMode && state.selectedModels.has(modelName);
|
||||
const favIcon = meta.favorite ? "★" : "☆";
|
||||
const batchIcon = isInBatch(modelName) ? `<button class="wfm-batch-btn active" title="${t("modelsBatch")}">B</button>` : `<button class="wfm-batch-btn" title="${t("modelsBatch")}">B</button>`;
|
||||
const tagsStr = (meta.tags || []).join(", ");
|
||||
const memo = meta.memo || "";
|
||||
const toggleLabel = disabled ? t("modelEnable") : t("modelDisable");
|
||||
@@ -902,6 +934,7 @@ function renderTableView(grid, models) {
|
||||
<td>${escapeHtml(tagsStr)}</td>
|
||||
<td class="wfm-table-td-memo" title="${escapeHtml(memo)}">${escapeHtml(memo)}</td>
|
||||
<td class="wfm-table-td-toggle"><button class="wfm-toggle-btn${disabled ? " wfm-toggle-disabled" : ""}" title="${toggleLabel}">${toggleIcon}</button></td>
|
||||
<td class="wfm-table-td-batch">${batchIcon}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
@@ -917,6 +950,7 @@ function renderTableView(grid, models) {
|
||||
<th>${t("modelsTags")}</th>
|
||||
<th>${t("modelsMemo")}</th>
|
||||
<th style="width:50px;"></th>
|
||||
<th style="width:30px;">B</th>
|
||||
</tr></thead><tbody>${rows}</tbody></table>`;
|
||||
|
||||
grid.querySelectorAll(".wfm-models-table-row").forEach((row) => {
|
||||
@@ -926,7 +960,7 @@ function renderTableView(grid, models) {
|
||||
|
||||
if (state.selectMode) {
|
||||
row.addEventListener("click", (e) => {
|
||||
if (e.target.closest(".wfm-models-table-fav, .wfm-toggle-btn")) return;
|
||||
if (e.target.closest(".wfm-models-table-fav, .wfm-toggle-btn, .wfm-batch-btn")) return;
|
||||
toggleModelSelection(mn);
|
||||
});
|
||||
} else {
|
||||
@@ -941,9 +975,45 @@ function renderTableView(grid, models) {
|
||||
e.stopPropagation();
|
||||
toggleModelEnable(mn);
|
||||
});
|
||||
row.querySelector(".wfm-batch-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleBatch(mn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Batch group helpers ───────────────────────────────────
|
||||
|
||||
function isInBatch(modelName) {
|
||||
return (state.modelGroups["Batch"] || []).includes(modelName);
|
||||
}
|
||||
|
||||
async function toggleBatch(modelName) {
|
||||
const batch = state.modelGroups["Batch"] || [];
|
||||
const idx = batch.indexOf(modelName);
|
||||
if (idx >= 0) {
|
||||
batch.splice(idx, 1);
|
||||
} else {
|
||||
batch.push(modelName);
|
||||
}
|
||||
state.modelGroups["Batch"] = batch;
|
||||
state.allModelGroups[state.activeModelType] = state.modelGroups;
|
||||
await saveModelGroups(state.modelGroups);
|
||||
renderModelGrid();
|
||||
}
|
||||
|
||||
async function clearBatchGroup() {
|
||||
state.modelGroups["Batch"] = [];
|
||||
state.allModelGroups[state.activeModelType] = state.modelGroups;
|
||||
await saveModelGroups(state.modelGroups);
|
||||
if (state.showBatchOnly) {
|
||||
state.showBatchOnly = false;
|
||||
document.getElementById("wfm-models-batch-filter-btn")?.classList.remove("active");
|
||||
}
|
||||
renderModelGrid();
|
||||
showToast(t("modelsBatchClear"), "success");
|
||||
}
|
||||
|
||||
// ── Favorite toggle ───────────────────────────────────────
|
||||
|
||||
async function toggleFavorite(modelName) {
|
||||
@@ -1358,6 +1428,10 @@ function renderSideGroup(modelName) {
|
||||
const sel = document.getElementById("wfm-models-group-manage-select");
|
||||
const oldName = sel?.value;
|
||||
if (!oldName) return;
|
||||
if (RESERVED_GROUPS.includes(oldName)) {
|
||||
showToast(t("modelsGroupReserved"), "warning");
|
||||
return;
|
||||
}
|
||||
const newName = prompt(t("modelsRenamePrompt"), oldName);
|
||||
if (!newName || newName === oldName) return;
|
||||
if (state.modelGroups[newName]) {
|
||||
@@ -1377,6 +1451,10 @@ function renderSideGroup(modelName) {
|
||||
const sel = document.getElementById("wfm-models-group-manage-select");
|
||||
const g = sel?.value;
|
||||
if (!g) return;
|
||||
if (RESERVED_GROUPS.includes(g)) {
|
||||
showToast(t("modelsGroupReserved"), "warning");
|
||||
return;
|
||||
}
|
||||
if (!confirm(t("modelsDeleteGroupConfirm").replace("{name}", g))) return;
|
||||
delete state.modelGroups[g];
|
||||
saveModelGroups(state.modelGroups).then(() => {
|
||||
@@ -1415,6 +1493,21 @@ function renderSideCivitai(modelName) {
|
||||
|
||||
if (cached) {
|
||||
renderCivitaiInfo(el, cached, modelName);
|
||||
} else if (sha256) {
|
||||
// SHA256 is known but model was not found on CivitAI
|
||||
el.innerHTML = `
|
||||
<div style="padding:0 4px;text-align:center;">
|
||||
<p style="color:var(--wfm-text-secondary);font-size:13px;margin-bottom:12px;">
|
||||
${t("civitaiNotFoundDesc")}
|
||||
</p>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-civitai-fetch-btn">
|
||||
${t("civitaiRefetchBtn")}
|
||||
</button>
|
||||
<div id="wfm-civitai-status" style="margin-top:8px;font-size:12px;color:var(--wfm-text-secondary);"></div>
|
||||
</div>`;
|
||||
document.getElementById("wfm-civitai-fetch-btn")?.addEventListener("click", () => {
|
||||
fetchCivitaiForModel(modelName, el);
|
||||
});
|
||||
} else {
|
||||
el.innerHTML = `
|
||||
<div style="padding:0 4px;text-align:center;">
|
||||
@@ -1456,6 +1549,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;
|
||||
@@ -1471,9 +1581,47 @@ async function fetchCivitaiForModel(modelName, el) {
|
||||
}
|
||||
|
||||
function renderCivitaiInfo(el, info, modelName) {
|
||||
const imagesHtml = (info.images || []).map((url) =>
|
||||
`<img src="${escapeHtml(url)}" style="width:100%;border-radius:4px;margin-bottom:6px;" loading="lazy" />`
|
||||
).join("");
|
||||
// URL: ユーザー設定ホスト(localStorage)を使用、modelId なし時は /model-versions/ にフォールバック
|
||||
const civitaiHost = localStorage.getItem("wfm_civitai_host") || "civitai.com";
|
||||
const modelUrl = info.modelId && info.versionId
|
||||
? `https://${civitaiHost}/models/${info.modelId}?modelVersionId=${info.versionId}`
|
||||
: info.versionId
|
||||
? `https://${civitaiHost}/model-versions/${info.versionId}`
|
||||
: (info.modelUrl || "#");
|
||||
|
||||
// Hash: BLAKE3 優先、なければ SHA256
|
||||
const fileHashes = info.fileHashes || {};
|
||||
const blake3 = fileHashes.BLAKE3 || fileHashes.Blake3 || "";
|
||||
const sha256 = fileHashes.SHA256 || (state.modelMetadata[modelName] || {}).sha256 || "";
|
||||
const hashType = blake3 ? "BLAKE3" : (sha256 ? "SHA256" : "");
|
||||
const hashFull = blake3 || sha256;
|
||||
|
||||
// Detail rows
|
||||
const ROW = "display:flex;align-items:center;font-size:12px;margin-bottom:5px;";
|
||||
const LABEL = "color:var(--wfm-text-secondary);min-width:80px;flex-shrink:0;";
|
||||
|
||||
const typeRow = info.type ? `
|
||||
<div style="${ROW}">
|
||||
<span style="${LABEL}">${t("civitaiType")}</span>
|
||||
<span style="font-size:10px;font-weight:700;background:var(--wfm-bg-tertiary,rgba(255,255,255,0.1));padding:2px 7px;border-radius:3px;letter-spacing:0.6px;">${escapeHtml(info.type.toUpperCase())}</span>
|
||||
</div>` : "";
|
||||
|
||||
const baseModelRow = info.baseModel ? `
|
||||
<div style="${ROW}">
|
||||
<span style="${LABEL}">${t("civitaiBaseModel")}</span>
|
||||
<span>${escapeHtml(info.baseModel)}</span>
|
||||
</div>` : "";
|
||||
|
||||
const hashRow = hashFull ? `
|
||||
<div style="${ROW}">
|
||||
<span style="${LABEL}">${t("civitaiHashLabel")}</span>
|
||||
<code class="wfm-hash-value" data-hash="${escapeHtml(hashFull)}"
|
||||
style="background:var(--wfm-bg-secondary);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:calc(100% - 84px);"
|
||||
title="${t("civitaiCopyHash")}">${escapeHtml(hashType)}: ${escapeHtml(hashFull.substring(0, 16).toUpperCase())}…</code>
|
||||
</div>` : "";
|
||||
|
||||
const detailSection = (typeRow || baseModelRow || hashRow)
|
||||
? `<div style="margin-bottom:10px;">${typeRow}${baseModelRow}${hashRow}</div>` : "";
|
||||
|
||||
const tagsHtml = (info.tags || []).map((tag) =>
|
||||
`<span class="wfm-badge wfm-badge-sm">${escapeHtml(tag)}</span>`
|
||||
@@ -1483,25 +1631,64 @@ function renderCivitaiInfo(el, info, modelName) {
|
||||
`<code style="font-size:11px;background:var(--wfm-bg-secondary);padding:1px 4px;border-radius:3px;cursor:pointer;" class="wfm-trained-word" title="${t("civitaiCopyWord")}">${escapeHtml(w)}</code>`
|
||||
).join(" ");
|
||||
|
||||
// Sample images — all go to Sample pane
|
||||
const images = info.images || [];
|
||||
const sampleImagesHtml = images.map((url) =>
|
||||
`<a href="${escapeHtml(url)}" target="_blank" rel="noopener" title="${t("civitaiOpenImage")}"><img src="${escapeHtml(url)}" style="width:100%;border-radius:4px;margin-bottom:6px;cursor:pointer;display:block;" loading="lazy" /></a>`
|
||||
).join("");
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:0 4px;">
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="font-weight:700;font-size:14px;margin-bottom:2px;">
|
||||
<a href="${escapeHtml(info.modelUrl)}" target="_blank" style="color:var(--wfm-primary);text-decoration:none;">${escapeHtml(info.modelName)}</a>
|
||||
<div style="display:flex;border-bottom:1px solid var(--wfm-border);margin-bottom:10px;">
|
||||
<button class="wfm-civitai-subtab-btn" data-pane="info"
|
||||
style="padding:4px 10px;font-size:12px;border:none;background:none;cursor:pointer;border-bottom:2px solid var(--wfm-primary);color:var(--wfm-primary);font-weight:600;margin-bottom:-1px;">
|
||||
${t("civitaiTabInfo")}
|
||||
</button>
|
||||
<button class="wfm-civitai-subtab-btn" data-pane="sample"
|
||||
style="padding:4px 10px;font-size:12px;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:var(--wfm-text-secondary);margin-bottom:-1px;">
|
||||
${t("civitaiTabSample")}${images.length ? ` (${images.length})` : ""}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="wfm-civitai-pane-info">
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="font-weight:700;font-size:14px;margin-bottom:2px;">
|
||||
<a href="${escapeHtml(modelUrl)}" target="_blank" style="color:var(--wfm-primary);text-decoration:none;">${escapeHtml(info.modelName)}</a>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--wfm-text-secondary);">
|
||||
${escapeHtml(info.versionName)}${info.creator ? ` · by ${escapeHtml(info.creator)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--wfm-text-secondary);">
|
||||
${escapeHtml(info.versionName)} · ${escapeHtml(info.baseModel)} · by ${escapeHtml(info.creator)}
|
||||
${detailSection}
|
||||
${tagsHtml ? `<div style="margin-bottom:8px;">${tagsHtml}</div>` : ""}
|
||||
${trainedWordsHtml ? `<div style="margin-bottom:10px;"><div style="font-weight:600;font-size:12px;margin-bottom:4px;">${t("civitaiTriggerWords")}</div>${trainedWordsHtml}</div>` : ""}
|
||||
${info.description ? `<div style="font-size:12px;color:var(--wfm-text-secondary);line-height:1.5;max-height:120px;overflow-y:auto;">${info.description}</div>` : ""}
|
||||
<div style="margin-top:10px;">
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-civitai-refresh-btn">${t("civitaiRefresh")}</button>
|
||||
</div>
|
||||
</div>
|
||||
${imagesHtml ? `<div style="margin-bottom:10px;">${imagesHtml}</div>` : ""}
|
||||
${tagsHtml ? `<div style="margin-bottom:8px;">${tagsHtml}</div>` : ""}
|
||||
${trainedWordsHtml ? `<div style="margin-bottom:10px;"><div style="font-weight:600;font-size:12px;margin-bottom:4px;">${t("civitaiTriggerWords")}</div>${trainedWordsHtml}</div>` : ""}
|
||||
${info.description ? `<div style="font-size:12px;color:var(--wfm-text-secondary);line-height:1.5;max-height:200px;overflow-y:auto;">${info.description}</div>` : ""}
|
||||
<div style="margin-top:10px;">
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-civitai-refresh-btn">${t("civitaiRefresh")}</button>
|
||||
|
||||
<div id="wfm-civitai-pane-sample" style="display:none;">
|
||||
${sampleImagesHtml || `<p style="color:var(--wfm-text-secondary);font-size:13px;text-align:center;margin-top:20px;">${t("civitaiNoImages")}</p>`}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Sub-tab switching
|
||||
const subtabBtns = el.querySelectorAll(".wfm-civitai-subtab-btn");
|
||||
subtabBtns.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const pane = btn.dataset.pane;
|
||||
subtabBtns.forEach((b) => {
|
||||
const isActive = b === btn;
|
||||
b.style.borderBottom = isActive ? "2px solid var(--wfm-primary)" : "2px solid transparent";
|
||||
b.style.color = isActive ? "var(--wfm-primary)" : "var(--wfm-text-secondary)";
|
||||
b.style.fontWeight = isActive ? "600" : "400";
|
||||
});
|
||||
el.querySelector("#wfm-civitai-pane-info").style.display = pane === "info" ? "" : "none";
|
||||
el.querySelector("#wfm-civitai-pane-sample").style.display = pane === "sample" ? "" : "none";
|
||||
});
|
||||
});
|
||||
|
||||
// Copy trigger word on click
|
||||
el.querySelectorAll(".wfm-trained-word").forEach((wordEl) => {
|
||||
wordEl.addEventListener("click", () => {
|
||||
@@ -1511,9 +1698,17 @@ function renderCivitaiInfo(el, info, modelName) {
|
||||
});
|
||||
});
|
||||
|
||||
// Copy hash on click
|
||||
el.querySelectorAll(".wfm-hash-value").forEach((hashEl) => {
|
||||
hashEl.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(hashEl.dataset.hash).then(() => {
|
||||
showToast(t("civitaiHashCopied"), "success");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Refresh button
|
||||
document.getElementById("wfm-civitai-refresh-btn")?.addEventListener("click", () => {
|
||||
// Clear cache for this model and re-fetch
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
if (meta.sha256) delete state.civitaiCache[meta.sha256];
|
||||
delete meta.sha256;
|
||||
@@ -1530,6 +1725,10 @@ async function loadModelsForCurrentType() {
|
||||
if (state.loaded[type] && state.modelsByType[type].length > 0) {
|
||||
// Reload groups for this type (groups are per-type)
|
||||
state.modelGroups = await fetchModelGroups();
|
||||
if (BATCH_MODEL_TYPES.includes(type) && !state.modelGroups["Batch"]) {
|
||||
state.modelGroups["Batch"] = [];
|
||||
await saveModelGroups(state.modelGroups);
|
||||
}
|
||||
state.allModelGroups[type] = state.modelGroups;
|
||||
renderTagFilter();
|
||||
renderDirFilter();
|
||||
@@ -1549,6 +1748,10 @@ async function loadModelsForCurrentType() {
|
||||
]);
|
||||
const disabledSet = new Set(Array.isArray(disabledList) ? disabledList : []);
|
||||
state.disabledModels[type] = disabledSet;
|
||||
if (BATCH_MODEL_TYPES.includes(type) && !groups["Batch"]) {
|
||||
groups["Batch"] = [];
|
||||
await saveModelGroups(groups);
|
||||
}
|
||||
state.modelGroups = groups;
|
||||
state.allModelGroups[type] = groups;
|
||||
|
||||
@@ -1637,9 +1840,18 @@ 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()]);
|
||||
// Apply sha256 hashes from batch result directly in case fetchModelMetadata
|
||||
// returns before the server has flushed updated metadata to disk
|
||||
if (data.hashes) {
|
||||
for (const [modelName, sha256] of Object.entries(data.hashes)) {
|
||||
if (!newMeta[modelName]) newMeta[modelName] = {};
|
||||
if (!newMeta[modelName].sha256) newMeta[modelName].sha256 = sha256;
|
||||
}
|
||||
}
|
||||
state.modelMetadata = newMeta;
|
||||
state.civitaiCache = newCache;
|
||||
renderModelGrid();
|
||||
@@ -1781,6 +1993,19 @@ export function initModelsTab() {
|
||||
});
|
||||
}
|
||||
|
||||
// Batch filter
|
||||
document.getElementById("wfm-models-batch-filter-btn")?.addEventListener("click", () => {
|
||||
state.showBatchOnly = !state.showBatchOnly;
|
||||
document.getElementById("wfm-models-batch-filter-btn").classList.toggle("active", state.showBatchOnly);
|
||||
state.currentPage = 0;
|
||||
renderModelGrid();
|
||||
});
|
||||
|
||||
// Batch clear
|
||||
document.getElementById("wfm-models-batch-clear-btn")?.addEventListener("click", () => {
|
||||
clearBatchGroup();
|
||||
});
|
||||
|
||||
// View mode (thumb / card / table)
|
||||
document.querySelectorAll("[data-models-view]").forEach((btn) => {
|
||||
if (btn.dataset.modelsView === state.viewMode) {
|
||||
@@ -1826,6 +2051,7 @@ export function initModelsTab() {
|
||||
const map = { info: "wfm-models-side-info", group: "wfm-models-side-group", civitai: "wfm-models-side-civitai" };
|
||||
const target = document.getElementById(map[tabId]);
|
||||
if (target) target.style.display = "block";
|
||||
if (tabId === "civitai" && state.selectedModel) renderSideCivitai(state.selectedModel);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ let pmSearchText = "";
|
||||
let pmSelectedId = null; // currently selected preset id
|
||||
let pmGroups = {}; // { groupName: [presetId, ...] }
|
||||
const PM_GROUPS_KEY = "wfm_prompt_preset_groups";
|
||||
const PROMPT_RESERVED_GROUPS = ["Batch"];
|
||||
|
||||
// ============================================
|
||||
// Preset API helpers
|
||||
@@ -118,11 +119,15 @@ async function loadAllPresets() {
|
||||
pmGroups = JSON.parse(localStorage.getItem(PM_GROUPS_KEY) || "{}");
|
||||
} catch { pmGroups = {}; }
|
||||
|
||||
// Clean stale entries from groups
|
||||
// Clean stale entries from groups (preserve reserved groups even if empty)
|
||||
const validIds = new Set(promptPresets.map(p => p.id));
|
||||
for (const g of Object.keys(pmGroups)) {
|
||||
pmGroups[g] = (pmGroups[g] || []).filter(id => validIds.has(id));
|
||||
if (pmGroups[g].length === 0) delete pmGroups[g];
|
||||
if (pmGroups[g].length === 0 && !PROMPT_RESERVED_GROUPS.includes(g)) delete pmGroups[g];
|
||||
}
|
||||
// Ensure reserved groups always exist
|
||||
for (const g of PROMPT_RESERVED_GROUPS) {
|
||||
if (!pmGroups[g]) pmGroups[g] = [];
|
||||
}
|
||||
saveGroups();
|
||||
|
||||
@@ -213,6 +218,25 @@ function matchesSearch(p) {
|
||||
(p.tags || []).some(t => t.toLowerCase().includes(s));
|
||||
}
|
||||
|
||||
function isInBatchPreset(id) {
|
||||
return (pmGroups["Batch"] || []).includes(id);
|
||||
}
|
||||
|
||||
function toggleBatchPreset(id) {
|
||||
const batch = pmGroups["Batch"] || [];
|
||||
const idx = batch.indexOf(id);
|
||||
if (idx >= 0) { batch.splice(idx, 1); } else { batch.push(id); }
|
||||
pmGroups["Batch"] = batch;
|
||||
saveGroups();
|
||||
}
|
||||
|
||||
function clearBatchPresets() {
|
||||
pmGroups["Batch"] = [];
|
||||
saveGroups();
|
||||
renderPresetManager();
|
||||
showToast(t("promptBatchClear"), "success");
|
||||
}
|
||||
|
||||
function createPmItem(preset) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "wfm-pm-item" + (pmSelectedId === preset.id ? " active" : "");
|
||||
@@ -225,12 +249,15 @@ function createPmItem(preset) {
|
||||
? `<span style="font-size:9px;color:var(--wfm-primary);margin-left:4px;">[${esc(preset.category)}]</span>`
|
||||
: "";
|
||||
|
||||
const inBatch = isInBatchPreset(preset.id);
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="wfm-pm-item-body">
|
||||
<div class="wfm-pm-item-name">${preset.favorite ? '<span style="color:#ffd700;">★</span> ' : ""}${esc(preset.name)}${catBadge}</div>
|
||||
<div class="wfm-pm-item-sub">${esc(previewText)}</div>
|
||||
</div>
|
||||
<div class="wfm-pm-item-actions">
|
||||
<button class="wfm-pm-action-btn pm-batch-btn${inBatch ? " batch-active" : ""}" title="Batch">B</button>
|
||||
<button class="wfm-pm-action-btn pm-fav-btn${preset.favorite ? " fav-active" : ""}" title="Favorite">★</button>
|
||||
<button class="wfm-pm-action-btn pm-del-btn" title="Delete" style="color:var(--wfm-danger);">✕</button>
|
||||
</div>
|
||||
@@ -242,6 +269,13 @@ function createPmItem(preset) {
|
||||
selectPresetInEditor(preset);
|
||||
});
|
||||
|
||||
// Batch toggle
|
||||
el.querySelector(".pm-batch-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleBatchPreset(preset.id);
|
||||
renderPresetManager();
|
||||
});
|
||||
|
||||
// Favorite toggle
|
||||
el.querySelector(".pm-fav-btn").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -593,6 +627,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">✎</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
|
||||
// ============================================
|
||||
@@ -750,18 +1011,14 @@ export function initPromptTab() {
|
||||
});
|
||||
if (created) {
|
||||
promptPresets.push(created);
|
||||
pmSelectedId = created.id;
|
||||
showToast(t("presetSaved"), "success");
|
||||
}
|
||||
}
|
||||
|
||||
pmSelectedId = null;
|
||||
renderPresetSelect();
|
||||
renderPresetManager();
|
||||
|
||||
// Re-select
|
||||
if (pmSelectedId && presetSelect) {
|
||||
presetSelect.value = pmSelectedId;
|
||||
}
|
||||
if (presetSelect) presetSelect.value = "";
|
||||
});
|
||||
|
||||
// Apply preset to GenerateUI
|
||||
@@ -832,6 +1089,10 @@ export function initPromptTab() {
|
||||
showToast("Select a group first", "error");
|
||||
return;
|
||||
}
|
||||
if (PROMPT_RESERVED_GROUPS.includes(groupName)) {
|
||||
showToast(t("modelsGroupReserved"), "warning");
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Delete group "${groupName}"?`)) return;
|
||||
delete pmGroups[groupName];
|
||||
saveGroups();
|
||||
@@ -853,4 +1114,161 @@ export function initPromptTab() {
|
||||
pmSearchText = e.target.value.trim();
|
||||
renderPresetManager();
|
||||
});
|
||||
|
||||
document.getElementById("wfm-pm-batch-clear-btn")?.addEventListener("click", () => {
|
||||
clearBatchPresets();
|
||||
});
|
||||
|
||||
// ── 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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -339,6 +393,11 @@ export async function initSettingsTab() {
|
||||
const settings = loadLocalSettings();
|
||||
const serverSettings = await loadServerSettings();
|
||||
|
||||
// Sync civitai_host to localStorage so other tabs can read it without an extra fetch
|
||||
if (serverSettings.civitai_host) {
|
||||
localStorage.setItem("wfm_civitai_host", serverSettings.civitai_host);
|
||||
}
|
||||
|
||||
// Load current workflows dir info
|
||||
let workflowsDirInfo = { current: "", default: "" };
|
||||
try {
|
||||
@@ -485,6 +544,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>
|
||||
@@ -570,6 +664,36 @@ export async function initSettingsTab() {
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- CivitAI Settings -->
|
||||
<details class="wfm-settings-section">
|
||||
<summary class="wfm-settings-summary">${t("civitaiApiKeySetting")}</summary>
|
||||
<div class="wfm-form-group">
|
||||
<label style="display:flex;align-items:center;gap:4px;">
|
||||
${t("civitaiHostSetting")}
|
||||
<span title="${t("civitaiHostHint")}" style="cursor:help;color:var(--wfm-text-secondary);font-size:13px;">ℹ</span>
|
||||
</label>
|
||||
<select class="wfm-select" id="wfm-settings-civitai-host" style="width:100%;">
|
||||
<option value="civitai.com" ${(serverSettings.civitai_host || "civitai.com") === "civitai.com" ? "selected" : ""}>${t("civitaiHostCom")}</option>
|
||||
<option value="civitai.red" ${serverSettings.civitai_host === "civitai.red" ? "selected" : ""}>${t("civitaiHostRed")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="wfm-form-group">
|
||||
<small style="color:var(--wfm-text-secondary);font-size:11px;display:block;margin-bottom:8px;">
|
||||
${t("civitaiApiKeyHint")}
|
||||
</small>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<input type="password" class="wfm-input" id="wfm-settings-civitai-api-key"
|
||||
value="${serverSettings.civitai_api_key || ""}"
|
||||
placeholder="${t("civitaiApiKeyPlaceholder")}"
|
||||
style="flex:1;">
|
||||
<button class="wfm-btn wfm-btn-primary wfm-btn-sm" id="wfm-settings-civitai-api-key-save">
|
||||
${t("civitaiApiKeySave")}
|
||||
</button>
|
||||
</div>
|
||||
<div id="wfm-settings-civitai-api-key-status" style="font-size:12px;margin-top:4px;"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Default Workflow -->
|
||||
<details class="wfm-settings-section">
|
||||
<summary class="wfm-settings-summary">${t("defaultWorkflow")}</summary>
|
||||
@@ -620,6 +744,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>
|
||||
|
||||
@@ -976,6 +1106,36 @@ export async function initSettingsTab() {
|
||||
}
|
||||
});
|
||||
|
||||
// CivitAI host select
|
||||
document.getElementById("wfm-settings-civitai-host")?.addEventListener("change", async (e) => {
|
||||
const host = e.target.value;
|
||||
try {
|
||||
await saveServerSettings({ civitai_host: host });
|
||||
serverSettings.civitai_host = host;
|
||||
localStorage.setItem("wfm_civitai_host", host);
|
||||
showToast(t("civitaiHostSaved"), "success");
|
||||
} catch (err) {
|
||||
showToast(`${t("saveError")}: ${err.message}`, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Save CivitAI API key
|
||||
document.getElementById("wfm-settings-civitai-api-key-save")?.addEventListener("click", async () => {
|
||||
const apiKey = document.getElementById("wfm-settings-civitai-api-key")?.value.trim() || "";
|
||||
const statusEl = document.getElementById("wfm-settings-civitai-api-key-status");
|
||||
try {
|
||||
await saveServerSettings({ civitai_api_key: apiKey });
|
||||
serverSettings.civitai_api_key = apiKey;
|
||||
showToast(t("civitaiApiKeySaved"), "success");
|
||||
if (statusEl) { statusEl.textContent = ""; }
|
||||
} catch (err) {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `${t("saveError")}: ${err.message}`;
|
||||
statusEl.style.color = "var(--wfm-danger)";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test ComfyUI URL
|
||||
document.getElementById("wfm-settings-test-url")?.addEventListener("click", async () => {
|
||||
const urlInput = document.getElementById("wfm-settings-comfyui-url");
|
||||
@@ -1026,8 +1186,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 +1225,7 @@ export async function initSettingsTab() {
|
||||
uiLang: getLang(),
|
||||
summaryLang: getSummaryLang(),
|
||||
theme: document.documentElement.getAttribute("data-theme") || "",
|
||||
textareaFontSize: taFontSize,
|
||||
};
|
||||
|
||||
saveLocalSettings(patch);
|
||||
@@ -1051,6 +1242,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");
|
||||
|
||||
@@ -11,6 +11,11 @@ import { t, getSummaryPrompt } from "./i18n.js";
|
||||
import { highlightJSON } from "./json-highlight.js";
|
||||
import { openBadgeEditModal } from "./models-tab.js";
|
||||
|
||||
// ============================================
|
||||
// Constants
|
||||
// ============================================
|
||||
const WF_RESERVED_GROUPS = ["Batch"];
|
||||
|
||||
// ============================================
|
||||
// State
|
||||
// ============================================
|
||||
@@ -19,10 +24,11 @@ const state = {
|
||||
workflows: [],
|
||||
activeBadge: "ALL",
|
||||
searchText: "",
|
||||
viewMode: localStorage.getItem("wfm_default_view") || "thumb", // thumb | card | table
|
||||
viewMode: localStorage.getItem("wfm_default_view") === "table" ? "table" : "thumb",
|
||||
selectedWf: null,
|
||||
groupFilter: "", // empty = all groups
|
||||
currentPage: 0,
|
||||
showBatchOnly: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -92,6 +98,11 @@ const groups = {
|
||||
} catch {
|
||||
this._data = {};
|
||||
}
|
||||
let needsSave = false;
|
||||
for (const g of WF_RESERVED_GROUPS) {
|
||||
if (!this._data[g]) { this._data[g] = []; needsSave = true; }
|
||||
}
|
||||
if (needsSave) this.save();
|
||||
return this._data;
|
||||
},
|
||||
save() {
|
||||
@@ -117,11 +128,14 @@ const groups = {
|
||||
},
|
||||
|
||||
deleteGroup(name) {
|
||||
if (WF_RESERVED_GROUPS.includes(name)) return false;
|
||||
delete this.data[name];
|
||||
this.save();
|
||||
return true;
|
||||
},
|
||||
|
||||
renameGroup(oldName, newName) {
|
||||
if (WF_RESERVED_GROUPS.includes(oldName)) return false;
|
||||
if (!newName || this.data[newName] || !this.data[oldName]) return false;
|
||||
this.data[newName] = this.data[oldName];
|
||||
delete this.data[oldName];
|
||||
@@ -154,6 +168,32 @@ const groups = {
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Batch helpers
|
||||
// ============================================
|
||||
|
||||
function isInBatch(filename) {
|
||||
return (groups.data["Batch"] || []).includes(filename);
|
||||
}
|
||||
|
||||
function toggleBatch(filename) {
|
||||
const batch = groups.data["Batch"] || [];
|
||||
const idx = batch.indexOf(filename);
|
||||
if (idx >= 0) { batch.splice(idx, 1); } else { batch.push(filename); }
|
||||
groups.data["Batch"] = batch;
|
||||
groups.save();
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
function clearBatch() {
|
||||
groups.data["Batch"] = [];
|
||||
groups.save();
|
||||
state.showBatchOnly = false;
|
||||
renderModelFilters();
|
||||
renderGrid();
|
||||
showToast(t("wfBatchClear"), "success");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API
|
||||
// ============================================
|
||||
@@ -250,6 +290,7 @@ function renderModelFilters() {
|
||||
container.innerHTML = `
|
||||
<button class="wfm-filter-btn ${state.activeBadge === "ALL" ? "active" : ""}" data-filter="ALL">ALL</button>
|
||||
<button class="wfm-filter-btn ${state.activeBadge === "FAVORITE" ? "active" : ""}" data-filter="FAVORITE">★</button>
|
||||
<button class="wfm-filter-btn wfm-wf-batch-filter-btn${state.showBatchOnly ? " active" : ""}">B</button>
|
||||
${badges.map((b) => {
|
||||
const color = palette[b] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
@@ -259,6 +300,13 @@ function renderModelFilters() {
|
||||
|
||||
container.querySelectorAll(".wfm-filter-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
if (btn.classList.contains("wfm-wf-batch-filter-btn")) {
|
||||
state.showBatchOnly = !state.showBatchOnly;
|
||||
state.currentPage = 0;
|
||||
renderModelFilters();
|
||||
renderGrid();
|
||||
return;
|
||||
}
|
||||
state.activeBadge = btn.dataset.filter;
|
||||
state.currentPage = 0;
|
||||
renderModelFilters();
|
||||
@@ -278,6 +326,11 @@ function filterWorkflows() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Batch filter
|
||||
if (state.showBatchOnly) {
|
||||
if (!(groups.data["Batch"] || []).includes(wf.filename)) return false;
|
||||
}
|
||||
|
||||
// Group filter
|
||||
if (state.groupFilter) {
|
||||
const grpFiles = groups.data[state.groupFilter] || [];
|
||||
@@ -333,30 +386,24 @@ function renderGrid() {
|
||||
card.dataset.filename = wf.filename;
|
||||
const hasThumb = !!wf.thumbnail;
|
||||
const badges = wfBadgesHtml(wf);
|
||||
const tags = (wf.metadata.tags || []).map((t) => `<span class="wfm-tag">#${t}</span>`).join("");
|
||||
const favStar = wf.metadata.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = wf.metadata.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
|
||||
if (state.viewMode === "thumb") {
|
||||
card.innerHTML = `
|
||||
<div class="wfm-card-thumb">${hasThumb ? `<img src="${wf.thumbnail}" loading="lazy" />` : `<span class="wfm-card-thumb-placeholder">${t("noImage")}</span>`}</div>
|
||||
card.innerHTML = `
|
||||
<div class="wfm-card-thumb">
|
||||
${hasThumb ? `<img src="${wf.thumbnail}" loading="lazy" />` : `<span class="wfm-card-thumb-placeholder">${t("noImage")}</span>`}
|
||||
<button class="wfm-batch-btn${isInBatch(wf.filename) ? ' active' : ''}" title="Batch">B</button>
|
||||
</div>
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${wf.filename}">${wf.filename.replace(/\.json$/, "")}</div>
|
||||
<div class="wfm-card-meta">${badges}</div>
|
||||
</div>
|
||||
<button class="${favClass}" title="${t("favorite")}">${favStar}</button>`;
|
||||
} else {
|
||||
// Card view: no thumbnail, more meta info
|
||||
const io = `P:${wf.analysis.inputs?.prompts || 0} I:${wf.analysis.inputs?.images || 0} → ${wf.analysis.outputs?.images || 0}`;
|
||||
card.innerHTML = `
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${wf.filename}">${wf.filename.replace(/\.json$/, "")}</div>
|
||||
<div class="wfm-card-meta">${badges}</div>
|
||||
<div class="wfm-card-io">${io}</div>
|
||||
<div class="wfm-card-tags">${tags}</div>
|
||||
</div>
|
||||
<button class="${favClass}" title="${t("favorite")}">${favStar}</button>`;
|
||||
}
|
||||
|
||||
card.querySelector(".wfm-batch-btn")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleBatch(wf.filename);
|
||||
});
|
||||
|
||||
card.querySelector(".wfm-fav-btn")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -390,6 +437,7 @@ function renderTableView(grid, filtered) {
|
||||
<th style="width:60px;">${t("tags")}</th>
|
||||
<th class="wfm-table-th-memo">${t("memo")}</th>
|
||||
<th class="wfm-table-th-summary">${t("summary")}</th>
|
||||
<th style="width:30px;"></th>
|
||||
</tr></thead>`;
|
||||
const tbody = document.createElement("tbody");
|
||||
|
||||
@@ -414,7 +462,13 @@ function renderTableView(grid, filtered) {
|
||||
<td class="wfm-table-td-io">${io}</td>
|
||||
<td>${tags}</td>
|
||||
<td class="wfm-table-td-memo" title="${(wf.metadata.memo || "").replace(/"/g, """)}">${wf.metadata.memo || ""}</td>
|
||||
<td class="wfm-table-td-summary" title="${(wf.metadata.summary || "").replace(/"/g, """)}">${wf.metadata.summary || ""}</td>`;
|
||||
<td class="wfm-table-td-summary" title="${(wf.metadata.summary || "").replace(/"/g, """)}">${wf.metadata.summary || ""}</td>
|
||||
<td class="wfm-table-td-batch"><button class="wfm-batch-btn${isInBatch(wf.filename) ? ' active' : ''}" title="Batch">B</button></td>`;
|
||||
|
||||
tr.querySelector(".wfm-batch-btn")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleBatch(wf.filename);
|
||||
});
|
||||
|
||||
tr.querySelector(".wfm-fav-btn")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -656,6 +710,10 @@ function renderSideGroup(wf) {
|
||||
el.querySelector("#wfm-grp-rename-btn")?.addEventListener("click", () => {
|
||||
const oldName = el.querySelector("#wfm-grp-manage-sel")?.value;
|
||||
if (!oldName) return;
|
||||
if (WF_RESERVED_GROUPS.includes(oldName)) {
|
||||
showToast(t("modelsGroupReserved"), "warning");
|
||||
return;
|
||||
}
|
||||
const newName = prompt(`Rename group "${oldName}" to:`, oldName);
|
||||
if (!newName || newName === oldName) return;
|
||||
if (!groups.renameGroup(oldName, newName)) {
|
||||
@@ -672,6 +730,10 @@ function renderSideGroup(wf) {
|
||||
el.querySelector("#wfm-grp-delete-btn")?.addEventListener("click", () => {
|
||||
const name = el.querySelector("#wfm-grp-manage-sel")?.value;
|
||||
if (!name) return;
|
||||
if (WF_RESERVED_GROUPS.includes(name)) {
|
||||
showToast(t("modelsGroupReserved"), "warning");
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Delete group "${name}"?`)) return;
|
||||
groups.deleteGroup(name);
|
||||
if (state.groupFilter === name) {
|
||||
@@ -1271,6 +1333,11 @@ export function initWorkflowTab() {
|
||||
openBadgeEditModal(() => renderGrid());
|
||||
});
|
||||
|
||||
// Toolbar: BC (batch clear) button
|
||||
document.getElementById("wfm-wf-batch-clear-btn")?.addEventListener("click", () => {
|
||||
clearBatch();
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadWorkflows();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -48,7 +50,6 @@
|
||||
<button id="wfm-list-open-comfyui-btn" class="wfm-btn wfm-btn-sm" disabled title="Select a card first">Open in ComfyUI</button>
|
||||
<div class="wfm-view-btns">
|
||||
<button class="wfm-view-btn active" data-view="thumb" title="Thumbnail">⊞</button>
|
||||
<button class="wfm-view-btn" data-view="card" title="Card">☰</button>
|
||||
<button class="wfm-view-btn" data-view="table" title="Table">≡</button>
|
||||
</div>
|
||||
<button id="wfm-refresh-btn" class="wfm-btn wfm-btn-sm">Refresh</button>
|
||||
@@ -61,6 +62,7 @@
|
||||
<button id="wfm-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="View settings">⚙</button>
|
||||
<div id="wfm-badge-settings-panel" class="wfm-badge-settings-panel" style="display:none;"></div>
|
||||
</div>
|
||||
<button id="wfm-wf-batch-clear-btn" class="wfm-btn wfm-btn-sm" title="Clear all batch workflows">BC</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-workflow-area">
|
||||
@@ -188,6 +190,7 @@
|
||||
<!-- Row 2: Search / filter toolbar -->
|
||||
<div id="wfm-models-toolbar" class="wfm-toolbar-row" style="gap:6px;flex-wrap:wrap;">
|
||||
<button class="wfm-filter-btn" id="wfm-models-fav-btn" title="Favorites">★</button>
|
||||
<button class="wfm-filter-btn" id="wfm-models-batch-filter-btn" title="Batch">B</button>
|
||||
<input type="text" id="wfm-models-search" class="wfm-search-input" placeholder="Search models...">
|
||||
<select id="wfm-models-tag-filter" class="wfm-select wfm-toolbar-select"><option value="">All Tags</option></select>
|
||||
<select id="wfm-models-dir-filter" class="wfm-select wfm-toolbar-select"><option value="">All Folders</option></select>
|
||||
@@ -199,7 +202,6 @@
|
||||
</select>
|
||||
<div class="wfm-view-btns">
|
||||
<button class="wfm-view-btn active" data-models-view="thumb" title="Thumbnail">⊞</button>
|
||||
<button class="wfm-view-btn" data-models-view="card" title="Card">☰</button>
|
||||
<button class="wfm-view-btn" data-models-view="table" title="Table">≡</button>
|
||||
</div>
|
||||
<span id="wfm-models-count" class="wfm-nodes-count"></span>
|
||||
@@ -208,6 +210,7 @@
|
||||
<span id="wfm-models-civitai-progress" style="font-size:12px;color:var(--wfm-text-secondary);"></span>
|
||||
<button id="wfm-models-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="Badge settings">⚙ Badge</button>
|
||||
<button id="wfm-models-select-btn" class="wfm-btn wfm-btn-sm" title="Multi-select models">Select</button>
|
||||
<button id="wfm-models-batch-clear-btn" class="wfm-btn wfm-btn-sm" title="Clear all batch entries">BC</button>
|
||||
</div>
|
||||
<!-- Bulk action bar (shown in select mode) -->
|
||||
<div id="wfm-models-bulk-bar" class="wfm-bulk-bar" style="display:none;"></div>
|
||||
@@ -264,6 +267,8 @@
|
||||
<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>
|
||||
<button class="wfm-gen-subtab-btn" data-subtab="batch">Batch</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Input — left sub-col: Prompt(top)+Image(bottom), right sub-col: Raw JSON -->
|
||||
@@ -310,6 +315,202 @@
|
||||
<div class="wfm-gen-rawjson-col" id="wfm-gen-rawjson-col-settings"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab: Batch — 3-pane: file tree | group select | queue preview -->
|
||||
<div id="wfm-gen-subtab-batch" class="wfm-gen-subtab-content">
|
||||
<div class="wfm-batch-layout">
|
||||
<!-- Left pane: file-tree checkpoint selection -->
|
||||
<div class="wfm-batch-pane wfm-batch-left">
|
||||
<div class="wfm-batch-pane-header">Checkpoints</div>
|
||||
<div class="wfm-batch-toolbar">
|
||||
<input type="text" id="wfm-ckpt-search" class="wfm-input" placeholder="Filter...">
|
||||
<button type="button" id="wfm-ckpt-select-all" class="wfm-btn wfm-btn-sm">All</button>
|
||||
<button type="button" id="wfm-ckpt-deselect-all" class="wfm-btn wfm-btn-sm">None</button>
|
||||
</div>
|
||||
<div id="wfm-ckpt-list" class="wfm-batch-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Center pane: group-based selection with inner tabs -->
|
||||
<div class="wfm-batch-pane wfm-batch-center">
|
||||
<div class="wfm-batch-inner-tab-nav">
|
||||
<button class="wfm-batch-inner-tab active" data-batch-inner="checkpoint">Checkpoint</button>
|
||||
<button class="wfm-batch-inner-tab" data-batch-inner="lora">Lora</button>
|
||||
<button class="wfm-batch-inner-tab" data-batch-inner="prompt">Prompt</button>
|
||||
<button class="wfm-batch-inner-tab" data-batch-inner="workflow">Workflow</button>
|
||||
</div>
|
||||
<div id="wfm-batch-inner-checkpoint" class="wfm-batch-inner-content active">
|
||||
<div class="wfm-batch-pane-header" style="border-top:none;">Groups</div>
|
||||
<div id="wfm-batch-group-list" class="wfm-batch-group-list"></div>
|
||||
</div>
|
||||
<div id="wfm-batch-inner-lora" class="wfm-batch-inner-content">
|
||||
<div class="wfm-batch-pane-header" style="border-top:none;">Groups</div>
|
||||
<div id="wfm-batch-lora-group-list" class="wfm-batch-group-list"></div>
|
||||
</div>
|
||||
<div id="wfm-batch-inner-prompt" class="wfm-batch-inner-content">
|
||||
<div class="wfm-batch-pane-header" style="border-top:none;">Groups</div>
|
||||
<div id="wfm-batch-prompt-group-list" class="wfm-batch-group-list"></div>
|
||||
</div>
|
||||
<div id="wfm-batch-inner-workflow" class="wfm-batch-inner-content">
|
||||
<div class="wfm-batch-pane-header" style="border-top:none;">Groups</div>
|
||||
<div id="wfm-batch-wf-group-list" class="wfm-batch-group-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right pane: batch queue preview (4 columns) -->
|
||||
<div class="wfm-batch-pane wfm-batch-right">
|
||||
<div class="wfm-batch-pane-header">Batch Queue</div>
|
||||
<div class="wfm-batch-queue-grid">
|
||||
<div class="wfm-batch-queue-col">
|
||||
<div class="wfm-batch-queue-col-header">
|
||||
<span>Checkpoint</span>
|
||||
<input type="checkbox" class="wfm-batch-type-cb" data-batch-type="checkpoint" title="Checkpoint batch を有効にする">
|
||||
</div>
|
||||
<div id="wfm-batch-preview-count" class="wfm-batch-count"></div>
|
||||
<div id="wfm-batch-preview-list" class="wfm-batch-preview-list"></div>
|
||||
</div>
|
||||
<div class="wfm-batch-queue-col">
|
||||
<div class="wfm-batch-queue-col-header">
|
||||
<span>Lora</span>
|
||||
<input type="checkbox" class="wfm-batch-type-cb" data-batch-type="lora" title="Lora batch を有効にする">
|
||||
</div>
|
||||
<div id="wfm-batch-lora-count" class="wfm-batch-count"></div>
|
||||
<div id="wfm-batch-lora-list" class="wfm-batch-preview-list"></div>
|
||||
</div>
|
||||
<div class="wfm-batch-queue-col">
|
||||
<div class="wfm-batch-queue-col-header">
|
||||
<span>Prompt</span>
|
||||
<input type="checkbox" class="wfm-batch-type-cb" data-batch-type="prompt" title="Prompt batch を有効にする">
|
||||
</div>
|
||||
<div id="wfm-batch-prompt-count" class="wfm-batch-count"></div>
|
||||
<div id="wfm-batch-prompt-list" class="wfm-batch-preview-list"></div>
|
||||
</div>
|
||||
<div class="wfm-batch-queue-col">
|
||||
<div class="wfm-batch-queue-col-header">
|
||||
<span>Workflow</span>
|
||||
<input type="checkbox" class="wfm-batch-type-cb" data-batch-type="workflow" title="Workflow batch を有効にする">
|
||||
</div>
|
||||
<div id="wfm-batch-wf-count" class="wfm-batch-count"></div>
|
||||
<div id="wfm-batch-wf-list" class="wfm-batch-preview-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</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 -->
|
||||
@@ -344,23 +545,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkpoint Batch Panel -->
|
||||
<!-- Batch Status Panel -->
|
||||
<div class="wfm-gen-panel" id="wfm-ckpt-batch-panel">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" id="wfm-ckpt-batch-enabled" style="width:14px;height:14px;cursor:pointer;accent-color:var(--wfm-primary);">
|
||||
<label for="wfm-ckpt-batch-enabled" style="font-weight:600;font-size:13px;cursor:pointer;">Checkpoint Batch</label>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
|
||||
<span style="font-weight:600;font-size:13px;">Batch</span>
|
||||
<span id="wfm-batch-type-label" style="font-size:11px;color:var(--wfm-text-secondary);">—</span>
|
||||
</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>
|
||||
<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;">
|
||||
<div id="wfm-ckpt-batch-body">
|
||||
<div id="wfm-ckpt-batch-progress" style="display:none;margin-top:6px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;font-size:11px;">
|
||||
<span id="wfm-ckpt-batch-current-name" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:75%;color:var(--wfm-text-primary);"></span>
|
||||
<span id="wfm-ckpt-batch-count" style="color:var(--wfm-text-secondary);white-space:nowrap;margin-left:8px;"></span>
|
||||
@@ -368,6 +560,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 +581,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 +637,210 @@
|
||||
</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">★</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..." style="flex:1;min-width:0;">
|
||||
<button id="wfm-pm-batch-clear-btn" class="wfm-btn wfm-btn-sm" title="Clear all batch presets" style="flex-shrink:0;">BC</button>
|
||||
</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">★</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. <lora:name:1:LBW=...;...>"><lora></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">📂 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... 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">→ Pos</button>
|
||||
<button id="wfm-wc-to-neg-btn" class="wfm-btn wfm-btn-sm" title="Send to Preset Negative Prompt">→ 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">↻</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 | 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) 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 +886,34 @@
|
||||
<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">5-tab layout — Input (Prompt + Image), Model, Settings, Feeder, Batch — Input/Model/Settings each include a Raw JSON column for instant preview and direct editing</li>
|
||||
<li id="wfm-help-gen-4">Input tab: 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 toggle (right panel): enable/disable batch mode — when enabled, Generate runs through all queued checkpoints sequentially with Pause/Resume and Stop controls</li>
|
||||
<li id="wfm-help-gen-12">Batch tab — 3-pane layout: Left pane (file tree) selects checkpoints by folder; use Filter to narrow the list, All/None buttons to select or clear all; initial state is all unchecked</li>
|
||||
<li id="wfm-help-gen-13">Batch tab — Center pane: group-based selection with inner tabs (Checkpoint | Lora | Prompt | Workflow); Checkpoint tab shows groups defined in the Models tab — check a group header to select all its members, or expand ▶ to check individual models; group membership always reflects the current Models tab state</li>
|
||||
<li id="wfm-help-gen-14">Batch tab — Right pane (Batch Queue): live preview of all checkpoints that will be processed, combining file-tree and group selections with duplicates removed; count shown at top</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 +927,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$|}, __|__, <lora:>, etc.); wraps selected text</li>
|
||||
<li id="wfm-help-prompt-9">Wildcard file manager: create/edit/delete .txt/.yaml wildcard files; click filename to insert __filename__ at cursor</li>
|
||||
</ul>
|
||||
</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 +956,10 @@
|
||||
<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>
|
||||
<li id="wfm-help-settings-13">CivitAI Host: choose between civitai.com (SFW only) and civitai.red (unrestricted) — determines which site opens when clicking a model link in the Models tab CivitAI panel</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -616,7 +1005,7 @@
|
||||
<li id="wfm-help-models-8">GenUI Model button: apply the selected model to the corresponding node in GenerateUI's current workflow (Checkpoint / LoRA / VAE / ControlNet / UNET / TextEncoder)</li>
|
||||
<li id="wfm-help-models-9">User-defined badges: assign free-label badges with custom colors shared with the Workflow tab</li>
|
||||
<li id="wfm-help-models-10">Enable/Disable models: toggle visibility in ComfyUI by renaming file extension (.disabled suffix). Disable per model (⏸ button on each card) or per group (Enable All / Disable All in Group panel). Changes take effect after ComfyUI refresh.</li>
|
||||
<li id="wfm-help-models-11">Multi-select & bulk operations: click Select to enter selection mode, check multiple models, then use the bulk action bar to add/remove from groups or permanently delete files (model file + preview images + sidecar files such as .json/.info).</li>
|
||||
<li id="wfm-help-models-11">Multi-select & bulk operations: click Select to enter selection mode, then check multiple models; the bulk action bar provides: Deselect All — clear all selections without exiting select mode; ★ Favorite / ☆ Unfavorite — toggle favorites for all selected models; Add to Group / Remove from Group — assign or remove from an existing group; Create & Add — type a new group name and add all selected; +Badge / −Badge — apply or remove a badge from all selected; Delete Files — permanently remove model files, preview images, and sidecar files (.json/.info)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -635,6 +1024,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 +1220,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 -->
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 758 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 316 KiB |
@@ -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
|
||||
}
|
||||
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 741 KiB |
|
After Width: | Height: | Size: 303 KiB |
@@ -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
|
||||
}
|
||||
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 510 KiB |
@@ -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
|
||||
}
|
||||
|
After Width: | Height: | Size: 258 KiB |
@@ -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
|
||||
}
|
||||
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 411 KiB |