Compare commits

...
231 Commits
Author SHA1 Message Date
ketle-manandClaude Fable 5 f39227a95b security: harden path validation (NT device forms, Eagle SVG path)
Follow-up to ea44cf4, from the automated security review of that commit:

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 17:47:14 +09:00
ketle-manandClaude Opus 5.5 0a98ff53a6 release: v0.9.5 — AI TOOL Chat skills v2, image check, Workflow Doctor, OCR
- Chat skills v2: metadata (output auto-check, tools, temperature, think,
  context), six versioned built-in skills, prompt action buttons, Markdown
- Image check with an Ollama vision decision model + refine loop
- Workflow Doctor, Storyboard to Lab, Agent Skills folder import
- TOOLS OCR (Ollama OCR models), collapsible Translation/Settings panes
- Fix: dead legacy top-menu code with deprecated imports removed (#3)
- Help, manual v0.9.5, TagSpace screenshot, registry-sized icon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:37:26 +09:00
ketle-manandClaude Opus 5.5 720d24b2ab docs: add TagSpace screenshot to README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:35:27 +09:00
ketle-manandClaude Opus 5.5 62b074e352 docs: manual v0.9.5 (ja/en) and registry-sized thumbnail
- Manual (text-only edition): Chapter 10 gains 10-5 Skills, 10-6 image
  check, 10-7 Workflow Doctor, 10-8 OCR, 10-9 making/importing skills;
  collapsible panes and TOOLS send buttons in 10-2; Lab storyboard loading
  and KSampler cell display in 5-7; troubleshooting and glossary entries
- Catch up v0.9.2-v0.9.4: Gallery 3D models (8-8), P-H3 Image (5-9),
  Image Edit draw palette / eyedropper / shortcuts (11-3, appendix),
  Selector event loop freeze and corrupted-image fix (Chapter 12)
- Combined MD / README / ZIP rebuilt as v0.9.5; v0.9.1 files removed;
  README manual links updated
- docs/thumb.png: new artwork resized to 800x400 (registry icon limit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:32:10 +09:00
ketle-manandClaude Opus 5.5 7a571b98c7 fix: remove dead legacy top-menu button code using deprecated imports
The ComfyButton / ComfyButtonGroup imports from
/scripts/ui/components/button.js and buttonGroup.js lived in the legacy
top-menu path for frontends older than 1.33.9. That path has not been
called since the extension switched to the actionBarButtons API
(a6f45ef), so the four functions and their constants were dead code.
Removed them instead of migrating to window.comfyAPI; the menu buttons are
still provided through actionBarButtons. Also removed the two stale .bak
copies in web/comfyui that shipped with the same legacy imports.

Verified on ComfyUI 0.45.0 / frontend 1.58.5: the extension registers and
its toolbar buttons appear, no console errors.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:31:46 +09:00
ketle-manandClaude Opus 5.5 059cb766fc feat: AI TOOL — collapsible Translation/Settings panes, TOOLS OCR, help rework
- Translation and Settings panes collapse to a thin strip (arrow in header,
  click the strip to reopen); state kept in localStorage
- TOOLS > "OCR (extract text)": Ollama OCR models (LightOnOCR-3 default,
  PaddleOCR-VL, GLM-OCR, DeepSeek-OCR), ported from Comic Creator's OCR tab:
  1600px white-background JPEG, temp 0, HTTP 500 retry, grayscale retry on
  empty/looping output, per-model prompts; optional text-position overlay
  (LightOnOCR / DeepSeek-OCR) with click-to-select line
- "To Translation" / "To Chat" buttons for any TOOLS result
- AI TOOL help regrouped into 12 cards (skill options, Doctor/Storyboard/
  folders, image check, OCR as their own cards; built-in list updated),
  Lab help gains the Storyboard entry (en/ja/zh)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:13:38 +09:00
ketle-manandClaude Opus 5.5 4cd9b0d033 feat: AI TOOL Chat Phase 3 — Workflow Doctor, Storyboard to Lab, folder skills, Markdown
- Workflow Doctor skill: context [workflow-detail, logs] injects checked facts
  (not-installed node types, values not in ComfyUI's lists with closest names,
  broken links, unconnected core-type required inputs, last /history error,
  latest ComfyUI log error block) — no false positives on 77 local workflows
- Storyboard to Lab skill: ```labplan block -> Lab plan (one scene per
  iteration, T2I mode, fixed seed) via "Open in Lab" (lab-tab openLabPlanData)
- Agent Skills folder import (.zip with SKILL.md or single .md): text files
  only, scripts never extracted, zip-slip/size checks; reference files are
  read on demand through a read_skill_file tool (progressive disclosure)
- Dependency-free Markdown rendering for assistant replies (escape first)
- Workflow summary no longer includes the current prompt text unless the user
  refers to it or the skill sets context: [current-prompt] (small models copied
  the old negative into new prompts even when told not to)
- Fix: Lab KSampler cell showed "(workflow default)" when only seed/sampler/
  scheduler were set
- Built-in skill updates (Enhancer/Image to Prompt v2, Skill Creator v3),
  help (en/ja/zh)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 12:48:03 +09:00
ketle-manandClaude Opus 5.5 8437df4881 feat: AI TOOL Chat image check with a vision decision model + refine loop
- "Check images" toggle: the chat LLM turns the request into yes/no checks
  (JSON schema), an Ollama decision model with image input (d3:4b default)
  judges the generated image with probabilities (pass >= 0.85, uncertain
  0.3-0.85) and shows a result card with overall quality
- Swap-check items (expect: no) catch attribute binding mistakes the judge
  otherwise passes at 0.70-0.79 (measured)
- "Fix missing items and regenerate": only unmet items go back to the chat
  model, which rewrites the prompt; 0-3 automatic rounds in settings
- SVG visual check: render to image, judge "recognizable as request" + parts
- New built-in skill: Image to Prompt
- Chat session state shared at module level, busy guard on send
- Fix: prompt action row no longer shares .wfm-ai-chat-actions with the
  send-button row
- Help (en/ja/zh) updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 12:14:09 +09:00
ketle-manandClaude Opus 5.5 4eaf71db8c feat: AI TOOL Chat skills v2 — metadata, versioned built-ins, prompt actions
- Built-in skills moved to py/services/builtin_skills/*.md with wfs-version;
  untouched copies auto-upgrade, edited ones keep and show a restore button
  (state in ai_skills/.builtin_state.json, legacy v0.3.79 copies detected by hash)
- Frontmatter metadata: output (prompt/svg auto-check + one retry), tools,
  temperature, think, context: [workflow] (GenerateUI workflow summary)
- ```prompt block buttons: Apply to GenerateUI / Generate / Copy
- New built-in skill: T2I Prompt Enhancer (NATURAL / TAG / MIXED by model)
- SVG Icon Generator v2 (style presets, self-check), Skill Creator v2
- SVG validation (well-formed, viewBox/xmlns, forbidden content incl. root attrs)
- Help (en/ja/zh) updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:52:11 +09:00
ketle-manandClaude Opus 5.5 4458a3d6e3 release: v0.9.4 — Gallery 3D models, P-H3 Image, TextGenerate fix
- Gallery: browse GLB, glTF, VRM, PLY (mesh, point cloud, Gaussian splat)
  and SPZ with a built-in three.js viewer; thumbnails are rendered in the
  browser and stored on the server
- Read workflows saved by SaveGLB from asset.extras
- GenerateUI/Metadata/I tab: support PH3ImageConditioning (P-H3 Image
  edit and T2I templates)
- Fix TextGenerate-family widget values shifted by one when the prompt is
  linked (also affected Krea-2, Ernie and LTX-2.5 templates)
- Help (en/ja/zh), README, and DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:53:07 +09:00
ketle-manandClaude Opus 5.5 54940ccf87 release: v0.9.3 — Image Edit: Draw palette, shortcuts, eyedropper
- Color palette on the Draw tool's second row: Color 1/2, 20 basic colors,
  20 custom slots; registered colors are saved in the browser
- Draw shortcuts: D/E mode, X switch color, [ ] brush size, B brush picker
- Eyedropper next to Color picks a color from the canvas
- Default brush color is now black; the selected color carries over to
  new documents
- Help (en/ja/zh), README, and DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:49:53 +09:00
ketle-manandClaude Opus 5.5 db1e834f86 release: v0.9.2 — fix corrupted image transfers with selector event loop
- With windows_selector_event_loop enabled, Gallery previews/downloads could
  arrive corrupted (same size, one 32KB block replaced by the next block).
  Set AIOHTTP_NOSENDFILE when switching to the selector loop.
- DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:54:23 +09:00
ketle-manandClaude Opus 5.5 1db6fda6f1 fix: corrupted file transfers with selector event loop on Windows
With windows_selector_event_loop enabled, aiohttp FileResponse goes through
asyncio's _sendfile_fallback, which reuses a 16KB buffer while the selector
transport queues unsent data without copying. Under backpressure later reads
overwrite queued blocks, so Gallery previews/downloads arrived corrupted
(same size, shifted 32KB block). Set AIOHTTP_NOSENDFILE when switching loops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:53:22 +09:00
ketle-manandClaude Opus 5.5 6cdf4919e4 release: v0.9.1 — TagSpace: add selected tags, Open in Gallery
- Add only the tags picked in the Selection panel ("Add selected (N)")
- Tag / Gallery-image chips re-escape parentheses (no accidental emphasis)
- "Open in Gallery" for a selected Gallery point (revealInGallery)
- Manual (ja/en) renamed to v0.9.1; DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:50:13 +09:00
ketle-manandClaude Opus 5.5 d9fc3bfcda feat(tagspace): "Open in Gallery" for a selected Gallery point
New gallery-tab export revealInGallery(path): switches to the Gallery /
Output subtab, clears search/favorite/tag/group filters and similar mode,
expands the folder tree down to the image's folder, loads it, renders
thumbnail pages up to the image (keeping the infinite-scroll sentinel at
the end) and selects it like a normal click. Returns false for paths
outside the output folder or not in the list; TagSpace shows a toast.
Help tagspace-10 (en/ja/zh), README, manual 11-7 (ja/en, combined/ZIP
rebuilt), DEVLOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:48:49 +09:00
ketle-manandClaude Opus 5.5 410fdbc1bb feat(tagspace): add only the selected tags; escape parentheses in tag chips
- With tags picked in the Selection panel, Add becomes "Add selected (N)"
  and adds just those tags (prompt order) as one chip; with none picked it
  adds the whole item as before.
- Tags are stored unescaped, so "rapunzel (disney)" went into prompts as an
  emphasis group. Tag and Gallery-image chips now re-escape parentheses
  (\( \)), matching card prompts so duplicates still collapse.
- Help tagspace-10 (en/ja/zh), README, manual 11-7 (ja/en, combined/ZIP
  rebuilt), DEVLOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:44:31 +09:00
ketle-manandClaude Opus 5.5 fc635f0618 release: v0.9.0 — TagSpace tab
- New TagSpace tab: tags, ImagePrompt cards and Gallery images laid out
  in EmbeddingGemma 2 space (3D), with Tagger fill, All tags / Min uses /
  Wildcards options, tag picking from cards and Gallery images, prompt
  building into GenerateUI
- Wildcard YAML parsed with PyYAML (multi-line items)
- Ollama embedding: retry on local port exhaustion
- Manual (text-only, ja/en) updated to v0.9.0
- DEVLOG / README updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:53:51 +09:00
ketle-manandClaude Opus 5.5 9cdfa44130 docs: update manual to v0.9.0 (TagSpace, Ollama semantic search, Reference to Video)
Revise the Japanese and English text-only manuals from v0.8.9 to v0.9.0:
new TagSpace section (11-7), Gallery semantic search on Ollama, PixAI
Tagger and GPU check, GenerateUI Input tab layout and Raw JSON folding,
Fibo 2.0 / FastH3 / Pika-H3-Flex, Video Plan Reference Image, Comic
Creator multi-reference runs, and troubleshooting entries. Combined MD,
ZIP and README rebuilt from the chapter files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:51:54 +09:00
ketle-manandClaude Opus 5.5 30ccd838fb Merge branch 'feature/tagspace-tab'
TagSpace tab (unreleased). DEVLOG: TagSpace notes moved into a new
"unreleased" section above v0.8.14 (the auto-merge had put them inside the
released v0.8.14 section); the duplicated Ollama migration notes stay under
v0.8.14 only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:45:40 +09:00
ketle-manandClaude Opus 5.5 6dd7f7523b feat(tagspace): pick a card's tags and search the space with them
Cards get normalized `tags` at build time (same split_tags as Gallery
points); the detail panel shares one tagPicker() for cards and Gallery
images, with the card's full prompt kept in a collapsible "Full prompt".
Cards without tags (or spaces built before this) still show the full text.
Help tagspace-10 (en/ja/zh), README, DEVLOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:35:25 +09:00
ketle-manandClaude Opus 5.5 645dbd8283 feat(tagspace): pick a Gallery image's tags and search the space with them
The selected Gallery point's prompt tags are shown as toggle buttons; the
chosen ones (kept until another item is selected) are joined in prompt
order and passed to the existing space search. The action row sticks to
the bottom of the detail panel so it stays reachable with many tags.
Help tagspace-10 (en/ja/zh, helpIdMap), README, DEVLOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:30:41 +09:00
ketle-manandClaude Opus 5.5 95b3627bd5 fix(tagspace): pick the point nearest the cursor on screen when zoomed in
The raycaster used a fixed world-space threshold (0.12) and took the first
hit, i.e. the one nearest the camera, so after zooming into the cloud a
single near point stayed "hovered" across a wide area and clicks kept
selecting it. Widen the candidate threshold, project each hit to the
screen, and choose the one closest to the cursor (within 8px, or the
drawn radius of enlarged points).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:22:03 +09:00
ketle-manandClaude Opus 5.5 cd35f7bdd2 feat(tagspace): All tags / Min uses / Wildcards options, cleaner tags, Ollama port-exhaustion retry
- Toolbar: All tags (max_tags=0, capped at 20,000), Min uses (1/2/3/5,
  total count over all sources), Wildcards on/off as a tag source; build
  meta records them plus tags_available, shown as "(of N candidates)".
  Defaults reproduce the previous 2000-tag result exactly.
- normalize_tag: drop JSON fragments embedded in prompts ("key": — MOSS-Image
  bbox layouts / text specs formed an isolated cluster of 234 tags) and model
  file names, strip stray double quotes, drop embedding: revealed after
  unwrapping parentheses.
- semantic_ollama: treat "Only one usage of each socket address" (loopback
  TIME_WAIT exhaustion from Ollama's per-input runner connections) as
  transient and wait 30s before retrying.
- Help tagspace-9 (en/ja/zh, helpIdMap), README, DEVLOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:06:19 +09:00
ketle-manandClaude Opus 5.5 24e082ecc2 feat(tagspace): parse wildcard YAML with PyYAML (block scalars, escapes)
The line-based reader took "- >-" as an item and dropped the block scalar's
body, and kept escapes like \u2019 literally. Read YAML with CSafeLoader
(SafeLoader fallback), take only string leaves (no keys, no bool/number
values), and fall back to the line reader for files that fail to parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:05:49 +09:00
ketle-manandClaude Opus 5.5 8fc1a9c68c release: v0.8.14 — Fibo 2.0 / FastH3 OmniRef / Pika-H3-Flex support
- GenerateUI / Metadata / side panel I tab: TextEncodeFibo2 and
  MiniMaxH3ReferenceToVideo prompt detection
- Video Plan tab: FastH3-family Reference to Video (Reference Image /
  Reference Image 2, unset references disconnected)
- Gallery semantic search on Ollama (EmbeddingGemma 2) instead of ONNX
- DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:56:12 +09:00
ketle-manandClaude Opus 5.5 805a4fa30f feat(video): disconnect unset reference images on Reference-to-Video plans
FastH3 / Pika-H3-Flex: a Reference Image / Reference Image 2 field left as
None/Clear now unlinks that ref_images input (and drops the template's
LoadImage once nothing else uses it) instead of silently generating with the
template's wired image. Other video families keep their existing behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:39:08 +09:00
ketle-manandClaude Opus 5.5 d6752c018c feat(video): Reference Image 2 for FastH3 / Pika-H3-Flex Plan blocks
Reuse the Plan block's Last Image field as Reference Image 2
(ref_images.ref_image_1) on Reference-to-Video templates: existing LoadImage
is overwritten or one is injected. Labels (Plan + Asset buttons/toasts)
switch to Reference Image / Reference Image 2. For chain_previous, Reference
Image 2 is never taken as the previous block's last frame — the generated
last frame is extracted instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:36:24 +09:00
ketle-manandClaude Opus 5.5 0039dd4f5b feat(video): support FastH3 OmniRef (MiniMax H3 Reference to Video) in Plan tab
FastH3 is a flat graph (no subgraph), so each field is written to the node
its input is wired from: prompt -> PrimitiveStringMultiline, duration ->
PrimitiveFloat (or length frames on the node itself, snapped to 17k+5),
resolution -> ResolutionSelector, First Image -> ref_images.ref_image_0
(labelled "Reference Image"; Last Image unavailable). widgetIndex falls back
to a default widget order for nodes saved without widget input entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:17:19 +09:00
ketle-manandClaude Opus 5.5 f5ea682440 feat(generateui): support Fibo 2.0 Edit and FastH3 OmniRef prompt detection
- TextEncodeFibo2: fixed positive/negative roles (prompt via PreviewAny ->
  PE switch chain), negative_prompt prompt node, width/height/batch_size
- MiniMaxH3ReferenceToVideo: treated as a video all-in-one node
  (VIDEO_ALL_IN_ONE_RE); a linked prompt source is seeded as positive
- Metadata / side panel I tab: resolve linked video prompts, split Fibo2
  positive/negative

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:17:19 +09:00
ketle-manandClaude Opus 5.5 91d096b69b docs(tagspace): help/README/DEVLOG for glow, twinkle and hover preview
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:33:08 +09:00
ketle-manandClaude Opus 5.5 73cfaca0bc feat(tagspace): glowing/twinkling points, hover zoom, click-to-select
- Points glow like stars (additive glow texture on a night-sky background);
  Gallery points are dimmed so dense areas don't wash out. "Glow" toggle.
- Twinkle: per-point phase/speed brightness modulation injected into
  PointsMaterial (onBeforeCompile). "Twinkle" toggle (effective with Glow).
- Hovering images in Selection / Nearby shows a 360px enlarged preview.
- Add/Remove button moved to the sticky title row of Selection.
- Clicking a point only selects it; double-click (or Add / +) adds/removes.
  Hint, empty-prompt text, help (en/ja/zh) and README updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:29:01 +09:00
ketle-manandClaude Opus 5.5 75269ce38d feat(tagspace): list neighbors for Gallery points
Selecting a Gallery point used to leave the previous list in place:
select() skipped the request and the neighbor matrix had no background
vectors. The background vectors are now saved with the space, centered
matrices are cached per kind, and a Gallery point lists similar Gallery
images, tags and cards interleaved. Cards, tags and searches still list
cards and tags only. Gallery images get round thumbnails in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 19:05:28 +09:00
ketle-manandClaude Opus 5.5 9958000d25 docs(tagspace): record Tagger fill speed on GPU after onnxruntime-gpu fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:07:39 +09:00
ketle-manandClaude Opus 5.5 2c513bb8bd feat(tagspace): Tagger fill for Gallery images without a prompt
- "Tagger fill" toolbar option: ONNX model from the Tagger tab, scope
  "Gallery points only" or "All images without a prompt"
- TaggerService.tag_image(): default thresholds (WD 0.35/0.85, PixAI
  recommended), rating labels dropped
- results cached per image and model (tagspace_tagger.json), saved every
  100 images so stopping keeps finished work; model unloaded afterwards
  unless the Tagger tab had it loaded
- new "Tagger fill" source; Gallery points fall back to Tagger tags,
  marked "(Tagger fill)"; progress shows CUDA/CPU
- help (en/ja/zh), README, DEVLOG

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:35:22 +09:00
ketle-manandClaude Opus 5.5 1011af7261 feat(tagspace): show or hide tags by source
The legend (ImagePrompt / Gallery prompts / Tagger DB / Prompt library /
Wildcards) becomes checkboxes. A tag stays visible while any of its sources
is on and takes the color of its first visible source; hidden tags are also
left out of the nearby/search lists, highlights and labels. Remembered in
localStorage. Help (en/ja/zh) and README updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:18:11 +09:00
ketle-manandClaude Opus 5.5 7c4899cc65 style(tagspace): move the Prompt panel to its own column on the right
The output textarea now fills the column height; chips take only the
height they need (up to 40%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:10:12 +09:00
ketle-manandClaude Opus 5.5 2ef13a6662 feat(tagspace): add tags from Gallery image prompts, clickable Gallery points
- read the positive prompt embedded in indexed Gallery images (upstream from the
  sampler's positive input, never the negative), cached in tagspace_prompts.json
- prompts split at commas only; new "Gallery prompts" tag source
- tags picked round-robin across sources (ranked by number of prompts using the
  tag) so the many Gallery prompts do not crowd out the other sources
- Gallery points carry their prompt tags; clicking adds them as one chip
- fix: background points had no kind (hover/click showed undefined)
- fix: skip images deleted after indexing (thumbnail 404s)
- help (en/ja/zh), README, DEVLOG

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:04:11 +09:00
ketle-manandClaude Opus 5.5 d861d8c76d feat: add TagSpace tab - choose tags visually in a 3D latent space
- ImagePrompt cards (thumbnails), tags split from ImagePrompt / wildcards /
  Tagger DB / Prompt library, and sampled Gallery images placed in the
  EmbeddingGemma 2 space and projected to 3D with PCA
- card vectors reuse the Gallery semantic index (missing ones are indexed on
  build); tag vectors embedded with Ollama and cached (tagspace_tags.npz)
- images and text centered on their own means (cards for images) to remove
  the modality gap so cards overlap their matching tags
- click to add/remove, neighbors list (cards and tags interleaved), text search
  with probe marker, prompt composed with duplicates removed, append to
  GenerateUI Positive / copy
- three.js r187 subset + OrbitControls bundled (static/lib/three, MIT),
  loaded on first tab open
- /wfm/tagspace/{status,space,build,cancel,search,neighbors}
- help page (en/ja/zh), README, DEVLOG

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:36:14 +09:00
ketle-manandClaude Opus 5.5 65ccc60f00 docs: record live verification of Ollama-based semantic search in DEVLOG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:25:20 +09:00
ketle-manandClaude Opus 5.5 ea66f2565d feat: run Gallery semantic search's EmbeddingGemma 2 in Ollama instead of ONNX
- new semantic_ollama.py replaces semantic_local.py (ONNX runtime removed)
- images sent as input=[{"image": b64}] (the "images" field is silently ignored),
  4 per request, per-image fallback on batch failure, retries on dropped connections
- model size selectable: 740m / 440m (identical vectors, no rebuild); :latest used as 740m
- npz model name unchanged, existing indexes keep working (cos ~0.97 vs ONNX vectors)
- Ollama URL checked by url_guard; settings saved via /wfm/gallery/semantic/options
- model is unloaded from VRAM after indexing (keep_alive 0)
- settings UI: device/threads/image precision replaced by Ollama URL + model size
- help (helpGallery33/35/36, helpTrouble10/11 en/ja/zh), README, requirements, DEVLOG updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:16:49 +09:00
ketle-manandClaude Sonnet 5.5 f43ad515bf fix: persist image sent to Mask Editor One node (v0.8.13)
- wfmOpenMaskEditorForNode calls node._persistBgDataUrl so properties.bg_image
  stays in sync (Mask Editor One v0.1.12+); prevents an older image being
  restored on reload and overwriting the server-side cache
- layer_data carries the sent image width/height for preview restore
- help (helpImageEditMask14 en/ja/zh), README, DEVLOG updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 18:47:53 +09:00
ketle-manandClaude Sonnet 5.5 21fe458753 feat: multi-image I2I bridge for Comic Creator + Input tab Prompt/Image merge, collapsible Raw JSON (v0.8.12)
- window._wfmReceiveMultiImageI2IRunRequest: image i -> i-th LoadImage node, optional width/height/steps/cfg/denoise/sampler/scheduler/seed
- GenerateUI Input tab: Prompt | Image side by side (3:2), Image wraps under Prompt when narrow
- Raw JSON column collapsible (Input tab only, remembered)
- help (helpGen4 en/ja/zh), README, DEVLOG updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 18:34:25 +09:00
ketle-manandClaude Sonnet 5.5 f3a103aa67 feat: split Gallery Send CC into Layout / Image buttons (v0.8.11)
- Send CC Layout: existing behavior (panel/overlay, MP4 to Video tool)
- Send CC Image: new, calls Comic Creator's window.sendImageToImageTab
- help (helpGallery37 en/ja/zh), README, DEVLOG, manual ch.8/11 updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 11:43:11 +09:00
ketle-manandClaude Sonnet 5.5 6f24e61c5e feat: support PixAI Tagger v1.0 (ONNX) in the Tagger tab (v0.8.10)
- Detect PixAI-format model folders (preprocess.json + selected_tags.csv with a category column) and run them through a dedicated 1008px preprocessing path (black padding, RGB, mean/std 0.5, probabilities out)
- Tags come from general/style/copyright/character; the Threshold / Char Thr. sliders offset the recommended per-category thresholds
- Selecting a PixAI model switches the sliders to 0.17 / 0.27 (Single and Batch); switching back restores 0.35 / 0.85 unless edited
- Help text (helpTagger1/2/4) updated in en/ja/zh

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 09:57:24 +09:00
ketle-manandClaude Sonnet 5.5 fbaac2adf2 docs: update manual to v0.8.9
Revise the Japanese and English beginner manuals (text-only edition) from v0.8.3 to v0.8.9: partner API (Nano Banana) setup, new supported workflows, Video asset registration and audio conversion, faster GenerateUI check, filename-match model fallback, and troubleshooting entries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:22:13 +09:00
ketle-manandClaude Sonnet 5.5 092b83b35d feat: add Register as Asset, Edit audio conversion, and Video Source layout fix (v0.8.9)
- Video Source: "Register as Asset" copies the loaded video into output/video and tags it into Video Assets
- Video Edit: "Convert audio..." next to the crop buttons, sharing Gallery's convert dialog
- Video Source panel widened so the file name and three buttons fit on one line each
- Help text (helpVideo5/11) updated in en/ja/zh

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 20:25:36 +09:00
ketle-manandClaude Sonnet 5.5 b052826ac2 docs: distinguish official MOSS-Image templates from the custom manga workflow
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 10:25:24 +09:00
ketle-manandClaude Sonnet 5.5 1e18509a4a feat: support MOSS-Image and speed up the first GenerateUI check (v0.8.8)
- MOSS-Image (Base/SFT, T2I and bounding-box layout, 8-step LoRA): read the
  outer subgraph node's widgets_values_named (inputs[] omits some widget
  ports, so index lookup shifted model/sampler values), treat unions such as
  BOUNDING_BOX,ARRAY,STRING by their first segment, support the
  BOUNDING_BOXES widget ({"__value__": [...]} in the API prompt), detect
  MOSSImageLayoutPrompt as the positive prompt and propagate roles through
  linked PrimitiveString nodes
- Gallery Prompt tab converts subgraph UI workflows before extraction; the
  side-panel I tab's converter copy gets the same widgets_values_named fix
- Fetch /object_info/<type> only for the node types a workflow uses instead
  of the full /object_info (43 s -> 0.25 s on first conversion/check); drop
  the full pre-fetch when opening the workflow detail
- README / DEVLOG updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 10:18:42 +09:00
ketle-manandClaude Sonnet 5.5 a993702d86 feat: support Qwen Image 2.1 Turbo and Flux.3 Image Klein templates (v0.8.7)
- Qwen Image 2.1 Turbo (T2I/I2I): detect the positive prompt behind the
  prompt-enhancer ComfySwitchNode, duplicate outer links to every fan-out
  target when flattening subgraphs (enhancer BatchImagesNode was left
  unwired), and merge the enhancer's TextGenerate prompt with the switch's
  raw prompt through one shared PrimitiveStringMultiline
- Flux.3 Image Klein (4 templates): split positive/negative roles through
  ReferenceMultiLatent and hide the unused leftover EmptyFlux2LatentImage
- COMBO validation: prefer the choice whose file name matches before the
  choices[0] fallback (templates store model names without the sub-folder
  prefix, which silently swapped in an unrelated model)
- README: add Gallery semantic search screenshot

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 11:42:57 +09:00
ketle-manandClaude Sonnet 5.5 296f8df4d1 docs: DEVLOG - record follow-up candidates after v0.8.6 (LoRA stack UI format, Gemini metadata display, partner node cost guard, sync script)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 15:37:11 +09:00
ketle-manandClaude Sonnet 5.5 091d37fa1a release: v0.8.6 - run partner API nodes (Nano Banana etc.) from GenerateUI, fix DynamicCombo widgets_values conversion
- partner_routes.py: /api/wfm/partner/prompt relays /prompt and adds COMFY_API_KEY (.env) as
  extra_data.api_key_comfy_org only for workflows containing an API node; client uses it when configured
- Gemini image nodes' prompt shows in the Prompt tab; sample workflow ws_nano_banana_2_1_t2i; help (helpGen32)
- converter: count only widget sub-inputs of a DynamicCombo option, and skip the control_after_generate slot
  after a seed even when the next widget is not numeric (checked against 301 saved UI workflows)
- Gallery: show the Prompt tab for cc_nanobanana images that carry embedded metadata
- README, DEVLOG, bump version to 0.8.6

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 15:26:36 +09:00
ketle-manandClaude Sonnet 5.5 ffcc68568c release: v0.8.5 - read Gemini / Nano Banana node images (prompt, model, seed) in Metadata tab and I tab
- metadata-tab.js / node_sets_menu.js: Gemini(Image|NanoBanana)* nodes' prompt input, model name
  and seed are extracted (API format)
- README, DEVLOG, bump version to 0.8.5

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 14:46:32 +09:00
ketle-manandClaude Sonnet 5.5 3fa7147f9c release: v0.8.4 - ComfyUI v0.40 native Load LoRA nodes (Model / Text Encoder), help update
- docs: help (helpGen20/21/29, helpSidepanel15 in en/ja/zh + index.html), README, DEVLOG
- bump version to 0.8.4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:14:42 +09:00
ketle-manandClaude Sonnet 5.5 5078dd7660 feat: support ComfyUI v0.40 native LoRA stack nodes (LoadLoraModel / LoadLoraTextEncoder)
The new nodes take a DynamicGroup of rows, sent in API format as flat keys
"loras.<row>.lora_name|strength|enabled". Previously they were not detected, and
the generic lora_name writers would have produced invalid keys.

- detect them in analyzeWorkflow (is_lora_stack)
- add comfyEditor.applyLoraStack: writes every stack node together (Model node gets
  the model strength, Text Encoder node the clip strength)
- wire it into GenerateUI Single/Stack apply, bypass toggle, Lab, Models "Apply" and
  the LoRA batch
- extract the LoRAs in Metadata tab / I tab (API format)

UI-format workflows are not covered: frontend 1.55.17 has no DynamicGroup widget,
so such nodes carry no widgets_values yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 12:45:12 +09:00
ketle-manandClaude Sonnet 5.5 871d99529d release: v0.8.3 - GenerateUI Workflow seed mode, Gallery Feeder sample, manual (ja/en)
- GenerateUI: new seed mode "Workflow" uses the loaded workflow's own seed as-is
- Fix: seed overrides leaked back into the loaded workflow (generate() now deep-copies)
- Add sample workflow ws_sdxl_gallery_feeder_i2i (WFS_GalleryFeeder)
- Add beginner manual (text-only edition) in docs/manual (Japanese) and docs/manual/en (English)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 09:57:57 +09:00
ketle-manandClaude Sonnet 5.5 59a4650161 docs: add Semantic Search (Gallery) troubleshooting to help (GPU not detected, Gemini key, empty results)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 23:12:04 +09:00
ketle-manandClaude Sonnet 5.5 fb0c8cc979 release: v0.8.2 - Gallery semantic search (Gemini Embedding 2 / local EmbeddingGemma 2, GPU)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 13:39:54 +09:00
ketle-manandClaude Sonnet 5.5 bc436a4435 Merge branch 'feature/gallery-semantic-search': Gallery semantic search (v0.8.2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 13:39:17 +09:00
ketle-manandClaude Sonnet 5.5 4ed361b78a feat: tabbed semantic modal, similarity thresholds, toolbar second row, input guards
- Modal tabs: Index / Model / Search; Search tab sets min score for similar
  images and text search (saved per model) and max results
- Toolbar: "S" toggle; when ON a second row shows Similar, refresh, settings
  and the result count (Similar/refresh moved from the detail panel)
- Backend: min_score filter, numeric parameter validation (400 bad_param),
  text/image/thread input caps in the local embedding runtime
- Help/i18n updated (en/ja/zh)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 13:35:37 +09:00
ketle-manandClaude Sonnet 5.5 821bee4b6e feat: local EmbeddingGemma 2 GPU (CUDA) support, CPU thread limit, image precision
- Device select (auto / CPU / GPU CUDA), CPU thread count (default half the cores)
- Image precision (soft tokens 280/140/70) stored with the index; mismatch needs rebuild
- Provider shown in connection test; model released after indexing
- Settings persisted (semantic_settings.json); help (en/ja/zh) + requirements note
- DirectML unsupported (vision encoder fails)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 13:17:01 +09:00
ketle-manandClaude Sonnet 5.5 63c3321aa6 style: widen help pages to align with the top tab bar (AI TOOL tab)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:43:55 +09:00
ketle-manandClaude Sonnet 5.5 cbedb973aa style: use teal for Similar buttons to distinguish from Tagger
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:39:27 +09:00
ketle-manandClaude Sonnet 5.5 4ec1861790 feat: semantic search modal help button, folder-tree index dots, pricing note
- Modal "?" button jumps to the Gallery help entry (highlighted)
- Folder tree shows a green dot next to the count when all images directly
  in the folder are indexed (GET /wfm/gallery/semantic/folders)
- Cost note: Gemini free tier / paid note (as of 2026-10); help text as plain text

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:36:02 +09:00
ketle-manandClaude Sonnet 5.5 47408506ba feat: Gallery semantic search (Gemini Embedding 2 / local EmbeddingGemma 2)
- Semantic Search toggle on the search box (Enter to search by meaning)
- Detail panel: Similar toggle + refresh button to list similar images
- Semantic settings modal (toolbar right end): build/update/delete the index,
  model radio (Gemini API / EmbeddingGemma 2 local ONNX), vector size, progress
- Backend: semantic_service (index store, job, cosine search) and
  semantic_local (onnxruntime port of EmbeddingGemma 2 image/text pipeline)
- GEMINI_API_KEY read from .env; help + i18n (en/ja/zh)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:14:46 +09:00
ketle-manandClaude Sonnet 5.5 333c5e5021 release: v0.8.1 - code review fixes (Nodes/Metadata/Gallery bugs, perf, dedupe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 13:40:29 +09:00
ketle-manandClaude Sonnet 5.5 1b85fb0cd8 release: v0.8.0 - audio support, file conversion, Nodes tab speedup, metadata reading fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 12:17:30 +09:00
ketle-manandClaude Sonnet 5.5 eacdae5831 docs: help for move filter, waveform colors, audio assets (en/ja/zh)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 12:15:19 +09:00
ketle-manandClaude Sonnet 5.5 15e19bc9e5 fix: neutral 'moved N' wording for files; bust browser cache for audio thumbnails
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 12:11:27 +09:00
ketle-manandClaude Sonnet 5.5 6751555b0b feat: folder filter in Gallery Move To dialog
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 11:33:46 +09:00
ketle-manandClaude Sonnet 5.5 00342a7f68 fix: audio player clipped in Gallery detail panel; per-format waveform colors
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 11:27:39 +09:00
ketle-manandClaude Sonnet 5.5 0973dc15a4 feat: Gallery file conversion (audio formats, audio extraction from video)
Convert... bulk button; PyAV-based; tag carry-over for tag-capable formats,
optional gallery-meta carry-over and original deletion.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 11:00:59 +09:00
ketle-manandClaude Sonnet 5.5 f1dba89f7b feat: audio assets in Video Asset tab (Set as BGM / Add as sound)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 10:50:53 +09:00
ketle-manandClaude Sonnet 5.5 0abebba4b1 feat: audio file support in Gallery (wav/mp3/ogg/m4a/flac/opus)
Waveform thumbnails, built-in player, duration and embedded prompt/workflow tags.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 10:40:19 +09:00
ketle-manandClaude Sonnet 5.5 1185aabbcf perf: show Nodes tab list from a light API, load /object_info in background
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 10:20:21 +09:00
ketle-manandClaude Sonnet 5.5 662b4dd209 perf: chunked rendering and debounced search in Nodes tab
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 10:08:32 +09:00
ketle-manandClaude Sonnet 5.5 3bbbfddd0d perf: do not block the Library N tab on /object_info
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 10:05:07 +09:00
ketle-manandClaude Sonnet 5.5 2355f503cd feat: Alt+click to toggle node favorite in Library N tab
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 09:58:42 +09:00
ketle-manandClaude Sonnet 5.5 be8cdd28ad docs: update help (en/ja/zh) and README for embedded metadata reading fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 09:45:25 +09:00
ketle-manandClaude Sonnet 5.5 12a46c683c fix: improve embedded metadata reading in Gallery/Metadata/Info tabs
- read PNG chunks past IEND (ComfyUI-Custom-Scripts appends workflow after it)
- read workflow/prompt from WebP EXIF in the Gallery
- show prompts for A1111/Forge/Fooocus images in the Gallery Prompt tab
- pick up API-format ImageMetadataLoRALoader LoRAs (Metadata and Info tabs)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 09:43:57 +09:00
ketle-man 8a246bfdb0 release: v0.7.14 - Nanobanana integration, Gallery/Metadata layout, WebM support
- Gallery/Metadata tabs show Comic Creator's Nanobanana generation history
  (history.jsonl) for images in a cc_nanobanana folder; Prompt tab hidden
  there since those images carry no embedded ComfyUI workflow
- Metadata tab: LoRA/Prompt moved to the center column; right column is now
  Settings (image size, KSampler values) + Nanobanana
- Fix: API-format prompt extraction didn't resolve custom multi-output text
  nodes like ComfyUI-LiveChatStream's response/positive/negative outputs
- Gallery: Send to LV node button (native Load Video) and WebM support
- decision-client.js: port vision support (images param, imageToBase64,
  supportsDecisionVision) from ComfyUI Comic Creator
2026-10-09 13:54:32 +09:00
ketle-manandClaude Opus 5.5 fa62e6cf96 release: v0.7.13 - VRAM management (Ollama), GenerateUI compatibility check
- Sidepanel A tab Settings: "VRAM (Ollama) -> Unload all now" button
  (same /api/wfm/vram/prepare step as Settings -> VRAM Management)
- Help: VRAM management, GenerateUI check (side panel / modal format
  badge, API-format handling), Ollama unload buttons, A tab
- README, DEVLOG, version bump

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:05:33 +09:00
ketle-manandClaude Opus 5.5 e22484b713 feat: add GenerateUI compatibility check to the Workflow tab
The detail modal gets a "GenerateUI Check" row (format badge, button and
two badges whose hover tip lists the reasons); the side panel shows the
format badge and the same badges automatically on selection.
- Conversion: runs the same convertUiToApi() as GenerateUI; flags missing
  node types and COMBO values not available here (UI format: silently
  replaced; API format: rejected by ComfyUI validation).
- Operability: share of nodes with settings that GenerateUI exposes.
  The Decision Model was tried but couldn't tell the cases apart.
Results are kept in memory only; /object_info is prefetched when the
modal opens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 08:47:27 +09:00
ketle-manandClaude Opus 5.5 d582ce3e92 feat: add VRAM management (unload Ollama models before generation)
Ported from ComfyUI Comic Creator. Settings tab gets a "VRAM Management
(Ollama)" section (off / auto to a target free VRAM / all) applied right
before every comfyUI.queuePrompt(). Free VRAM is measured with nvidia-smi
via the new /api/wfm/vram/prepare route; backend URLs go through url_guard.
The AI TOOL tab and Prompt/Tagger settings "Unload Model" buttons now
unload every loaded Ollama model (decision models included) for Ollama.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 07:55:19 +09:00
ketle-manandClaude Sonnet 5.5 b81aa84898 release: v0.7.12 - restrict client-supplied backend URLs (SSRF)
Ollama chat, Tagger VLM, Eagle and the Unsloth relay accepted the target URL
from the request. Add py/services/url_guard.py: loopback or operator-listed
WFS_ALLOWED_BACKEND_HOSTS only, http(s) only, no credentials, no redirects.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 06:49:16 +09:00
ketle-manandClaude Sonnet 5.5 4179511e1b fix(security): restrict preview downloads to CivitAI hosts, fix Host-header based origin
- download_image: https + CivitAI hosts only (also validates redirects, 50MB cap);
  sidecar-supplied image URLs could trigger SSRF / file:// reads
- Lora Manager bridge: connect to 127.0.0.1 with the real listening port instead
  of the request Host header
- safeHttpUrl for sample/fallback image URLs; DEVLOG

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:32:58 +09:00
ketle-manandClaude Sonnet 5.5 cb9f5eecc4 release: v0.7.11 - sanitize CivitAI description HTML (stored XSS), harden sidecar import
- util.js: allowlist sanitizeHtml() / safeHttpUrl() (absolute http(s) only);
  applied to the Models detail panel description, model URL and sample images
- cm-info/metadata sidecar import: numeric IDs only, type checks, 2MB size cap
- DEVLOG, README badge, version bump

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:26:31 +09:00
ketle-manandClaude Sonnet 5.5 3262e843cf release: v0.7.10 - Selector event loop switch, no-hash CivitAI batch, models folder, preview 404 fix
- Windows: optional Selector asyncio loop (settings toggle / WFS_SELECTOR_LOOP=1)
  to avoid ComfyUI freezing in closesocket on the Proactor loop
- Models: skip image requests for models without previews (preview-keys API),
  show previews of .disabled models, add extra Models folder setting
- CivitAI: batch fetch no longer hashes; reads .cm-info.json (Stability Matrix);
  unhashed models shown in red and hashed individually; drop file path display
- Help/i18n for the new settings; DEVLOG, README badge, version bump

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 21:26:31 +09:00
ketle-manandClaude Sonnet 5.5 68836b0166 feat: reuse Lora Manager sha256 to skip hashing in CivitAI batch fetch
Query /api/lm/{loras,checkpoints,embeddings}/list for already-computed
sha256 and use it (size-matched only) for models without a stored hash.
Falls back to the existing hashing path if Lora Manager is unavailable.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 12:58:38 +09:00
ketle-manandClaude Sonnet 5.5 7b3d1f0bc3 feat: add Models folder setting and cancel CivitAI batch on disconnect
- Settings: new "Models Folder" (models_dir) to add extra model roots such as
  Stability Matrix's Models; subfolders (StableDiffusion/Lora/VAE/...) are
  merged into the Models tab list and folder resolution
- CivitAI batch: stop hashing/fetching when the client disconnects, keeping
  already computed hashes
- Lower load: hash workers 3->2, read chunk 1MB->512KB, preview download
  workers 4->2

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:14:21 +09:00
ketle-manandClaude Sonnet 5.5 255fdd3c92 release: v0.7.9 - CivitAI auto badges, faster batch fetch, folder link support
- Models: auto-create badges from CivitAI base model (keep existing badges,
  optional color overwrite, optional skip for models that already have badges)
- CivitAI batch fetch: reuse saved hashes (size/mtime checked), import from
  .metadata.json sidecars, parallel hashing with incremental saves, parallel
  preview download off the event loop, "Skip not found" option
- Folder symlink/junction support: loop-safe scan, allow moving into linked folders
- Help (3 languages) and DEVLOG updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 00:25:35 +09:00
ketle-manandClaude Sonnet 5.5 787bf01381 release: v0.7.8 - Video Edit multi-track (overlay/text/audio), timeline text, sounds, backup fix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:44:32 +09:00
ketle-manandClaude Sonnet 5.5 ca5649b926 docs: backup help/README mention sounds and overlay clips media
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:38:46 +09:00
ketle-manandClaude Sonnet 5.5 93d13e1fd0 fix: full backup now includes media used by Video Edit timeline sounds and overlay clips
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:38:24 +09:00
ketle-manandClaude Sonnet 5.5 94840dade5 docs: mention Asset 'Add as overlay' in Video help
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:31:54 +09:00
ketle-manandClaude Sonnet 5.5 9a98e3e50a feat: add 'Add as overlay (PinP)' button to the Video Asset detail panel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:46:14 +09:00
ketle-manandClaude Sonnet 5.5 f20e13e093 fix: overlay/sound file pickers use a transparent native input over the button
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:39:49 +09:00
ketle-manandClaude Sonnet 5.5 98645407d9 feat: Video Edit tool menu follows the track view (video/text/overlay/audio), export stays fixed
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:30:01 +09:00
ketle-manandClaude Sonnet 5.5 e5e2daf523 feat: cascade default position of newly added overlay clips
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:15:31 +09:00
ketle-manandClaude Sonnet 5.5 2413dd2d77 feat: overlay (PinP) video/image track in Video Edit, composited on export
Overlay clips are placed/scaled/faded per clip, drawn over the main video in the
same PyAV pass that burns in text. Includes track view, panel, preview boxes,
undo/redo and project save/load.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:09:14 +09:00
ketle-manandClaude Sonnet 5.5 e197356c6b docs: update Video Edit help for track views, timeline texts and sounds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:03:41 +09:00
ketle-manandClaude Sonnet 5.5 1d7f5721ad feat: timeline sounds on the Video Edit audio track (placeable, trimmable extra audio clips)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:43:49 +09:00
ketle-manandClaude Sonnet 5.5 74d1a5579b refactor: store Video Edit text overlays on the timeline (absolute time) instead of per clip
Texts can now be dragged across clip boundaries; legacy projects with
clip-relative texts are migrated on load.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:40:33 +09:00
ketle-manandClaude Sonnet 5.5 60e94b0ab2 feat: drag/resize text bands on the Video Edit text track
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:34:49 +09:00
ketle-manandClaude Sonnet 5.5 81d7d65af6 feat: add Video/Text/Audio track view switch to Video Edit timeline
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:34:31 +09:00
ketle-manandClaude Opus 5.5 130ba05bd6 chore(release): v0.7.7 - Ollama意思決定モデル対応・Unslothキーなし対応・ヘルプ点検
- docs(help): Video Edit save contents / backup hint, 5 previously
  undocumented Settings sections (helpSettings18-22), font count fix,
  English template fallbacks synced with i18n (EN/JA/ZH)
- docs: DEVLOG
- bump version to 0.7.7

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:36:43 +09:00
ketle-manandClaude Opus 5.5 fe1b896405 feat(decision): add Ollama 0.35+ as a decision model backend
- decision-client.js: "ollama" backend calls Ollama's /v1/systemone directly
  from the browser (no key, CORS allowed) and surfaces its {error} messages;
  listDecisionModels() lists installed models whose /api/tags capabilities
  include "decision" (tev1, tev1:0.8b, nimble)
- Settings: backend switch (Unsloth / Ollama) updates URL and model list,
  refresh button, pull hint when no decision model is installed; section
  renamed to "Decision Model"
- A model name from the other backend falls back to that backend's default
- docs: help (EN/JA/ZH), README

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:26:17 +09:00
ketle-manandClaude Opus 5.5 5dbdbd519b feat(unsloth): allow keyless access when UNSLOTH_API_KEY is unset
- Proxy (AI TOOL / sidepanel / Decision API) and the Tagger's Unsloth VLM
  path now send requests without an Authorization header when no key is
  set, for Unsloth Desktop's Keyless API access -> "Chat and inference"
  (localhost-only restriction unchanged)
- A 401 with no key set returns a message explaining both options
- docs: help (EN/JA/ZH), README, .env.example (LAN exposure / keep
  "Allow tools" off)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:17:06 +09:00
ketle-manandClaude Opus 5.5 20aff8236b chore(release): v0.7.6 - 意思決定モデル設定・フルバックアップのVideo Edit素材対応
- feat(backup): opt-in "Include media used by Video Edit projects" bundles the
  input-dir clips/BGM referenced by saved projects under _video_edit_media/;
  restored into input/ without overwriting existing files
- fix(backup): stream full-backup uploads to a temp file with read_chunk()
  so restores over ComfyUI's --max-upload-size (default 100 MB) no longer fail
  with "Request Entity Too Large"
- docs: help (Decision Model settings, backup), README, DEVLOG
- bump version to 0.7.6

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 12:39:09 +09:00
ketle-manandClaude Opus 5.5 dfe31374cf feat(settings): add dedicated Decision Model (Laya) settings
- New Settings section, stored under wfm_decision_settings independently of
  the AI TOOL / Tagger backends so a decision model can run alongside any
  LLM/VLM: backend (Unsloth for now), URL, model (laya / laya-multilingual /
  laya-english / laya-typed-decisions), default probability threshold
- Test button sends one yes/no question with the unsaved form values and
  shows latency + answer
- decision-client.js reads URL/model/threshold from these settings instead
  of borrowing the AI TOOL backend URL; isYes/pickChoice/pickScore default to
  the saved threshold
- i18n (en/ja/zh)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 12:05:46 +09:00
ketle-manandClaude Opus 5.5 f826b54239 chore(release): v0.7.5 - Ming Image対応・widgets_valuesズレ修正・Unsloth Decision APIクライアント土台
- feat: Unsloth Decision API (/v1/systemone, Laya) relay via the existing
  key-attaching proxy + static/js/decision-client.js (decide/decideMany,
  noul/choice/score builders, probability-threshold helpers)
- docs: README/DEVLOG/help for Ming Image support and the decision client
- bump version to 0.7.5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 11:04:17 +09:00
ketle-manandClaude Opus 5.5 cee7ef1bfa feat: support Ming Image workflows in GenerateUI, Gallery/Metadata and sidepanel I tab
- Detect TextEncodeMingImageEdit prompts (role from sampler/guider wiring,
  propagate role through linked prompt) and EmptyQwenImageLayeredLatentImage
- Settings: disable Sampler field when the SAMPLER source has no sampler_name
  (e.g. SamplerLCM)
- Metadata/I tab: treat BasicGuider "conditioning" as positive; prefer linked
  text source over stale widgets_values[0] in UI-format workflows
- Fix: normalize "legacy full" widgets_values on top-level nodes whose widgets
  are converted to wired inputs (previously only subgraph nodes), which shifted
  later values (e.g. EmptyLatentImage batch_size became 1024)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:41:20 +09:00
ketle-manandClaude Opus 5.5 5fb1b1eb01 chore(release): v0.7.4 - Video EditにBGM合成・テキストオーバーレイ・クロップ・Undo/Redoを追加
Video Editサブタブの残りフェーズ(Phase 2〜5)を実装。
- BGM/音声トラック合成: Core音声ノード+ConcatenateVideoのcomplete_audioでグラフ内完結。複数クリップ結合時も各クリップの音声を保持。BGMはボタン/ドロップ/AssetのBGMに設定で指定
- テキストオーバーレイ: クリップ単位の字幕(位置・区間・色・縁取り・背景)、PyAVストリーム処理で焼き込み
- クロップ: Sourceプレビュー上の矩形エディタ(アスペクト固定・キャンセル)、VideoCropで書き出し
- Undo/Redo: 編集状態のスナップショット履歴(ボタン+Ctrl+Z/Y)
- クリアでオーディオ欄も初期化、BGM/Loadのファイル選択が開かない不具合を修正

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:43:32 +09:00
ketle-manandClaude Opus 5.5 4fa39daf0e docs: document Video Edit BGM, text overlays, crop and undo/redo
README (Video Tab section) and DEVLOG gain an "Unreleased" entry for the
feature/video-edit-bgm-text-overlay branch: audio/BGM track mixing,
per-clip text overlays, crop, undo/redo, BGM from Asset videos / drag &
drop, Clear resetting audio, crop Cancel/auto-exit, and the <label>-based
file picker fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:41:51 +09:00
ketle-manandClaude Opus 5.5 8beec6abdf feat(video-edit): crop edit Cancel button and auto-exit on tab switch
- "Cancel" appears next to "Done" while editing a crop and restores
  the crop the clip had when editing started (including none).
- Switching to the Plan/Asset subtab, another main tab, or another clip
  ends crop editing, keeping the adjusted rect (same as Done). The
  sidebar Project button doesn't, since the Edit subtab stays visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:57:13 +09:00
ketle-manandClaude Opus 5.5 99ffc1fedd feat(video-edit): Clear also resets the Audio section
Clear now empties the clips and restores the soundtrack settings (BGM,
volumes, keep-original, BGM start) to their defaults; it is enabled
whenever either clips exist or audio differs from the defaults, so a
BGM-only timeline can be cleared too. Undo restores both. The confirm
message and help text mention the audio reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:45:04 +09:00
ketle-manandClaude Opus 5.5 a63c6ec53b fix(video-edit): open BGM/Load file pickers via native <label>
The BGM picker (and the project Load button) opened a hidden file input
by calling input.click() from a button's click handler. On the user's
browser this silently showed no dialog, while the Video Source picker in
the same tab — a <label>-wrapped input — worked. Both are now
<label>-wrapped inputs styled as buttons, with Enter/Space wired for
keyboard activation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:28:42 +09:00
ketle-manandClaude Opus 5.5 4e39547a4f feat(video-edit): pick BGM from Asset videos and by drag & drop
- Asset subtab: "Set as BGM (audio only)" on video assets uses the
  video's audio track as the Edit BGM (LoadAudio decodes audio from video
  files); videos without an audio track are rejected with a message.
- Audio section accepts a dropped audio/video file, and the BGM file
  input lists common audio extensions and mp4 explicitly instead of only
  audio/*.
- A failed BGM pick no longer clears the previously set BGM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:20:46 +09:00
ketle-manandClaude Opus 5.5 c6487ad923 feat(video-edit): add undo/redo for timeline edits
Snapshot history of the editorial state (clip order, trim, crop, text
overlays, audio settings), recorded via a debounced hook on the existing
redraw paths and pushed only when the serialized state changes, so
selection/preview/resize never create entries. Deleted clips come back
from a clip registry without re-uploading. Toolbar buttons plus
Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z (ignored inside text fields); history is
reset on project load.

Also fixes a lost click: committing a trim/hold-length value on blur
rebuilt the text overlay section, replacing a "+ Add text" button that
was being clicked; it now only updates the rows' bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:04:55 +09:00
ketle-manandClaude Opus 5.5 0c6787f712 feat(video-edit): add per-clip crop with on-preview rectangle editor
Video clips get a normalized crop rect edited directly on the Source
preview (move / corner resize, optional 16:9, 9:16, 1:1, 4:3, 3:4 lock).
Export inserts core VideoCrop (double-nested {"crop": {...}} input, even
pixel values) after Video Slice. The export size is the first video
clip's cropped size; other clips of a different size are center-fit with
ImageScale. Text overlay previews are drawn relative to the cropped
frame, and crops are saved in Edit projects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:01:32 +09:00
ketle-manandClaude Opus 5.5 59e96ce180 feat(video-edit): add BGM/audio track mixing and simple text overlays
Phase 4 (audio): the Edit tab export now builds a separate soundtrack in
the same ComfyUI graph and passes it to ConcatenateVideo's complete_audio.
Each clip contributes an exact-length segment (EmptyAudio + AudioMerge with
the clip's own GetVideoComponents audio), chained with AudioConcat, with an
optional BGM (LoadAudio -> TrimAudioDuration -> AudioAdjustVolume) mixed on
top. This also removes the old limitation that multi-clip exports lost
their audio. Export panel gets an Audio section (keep original audio +
volume, BGM file, BGM volume, BGM start offset).

Phase 3 (text): per-clip text overlays (text, start/end, size %, color,
9-grid anchor, outline, background) with a live CSS preview over the
Source/Result panes. Export burns them in via the new
/api/wfm/video/edit/overlay-text endpoint (PyAV streaming decode -> Pillow
composite -> libx264 encode, audio packets remuxed untouched, CJK-capable
system font lookup).

Text overlays and audio settings are persisted in Edit projects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:49:55 +09:00
ketle-manandClaude Sonnet 5 6b262a9cf4 chore(release): v0.7.3 - GenerateUIタブにQwen Image 2.1対応を追加
TextEncodeQwenImage21ノード(Comfy-OrgのQwen-Image-2.1テンプレート、T2I/Edit兼用)のprompt/negative_prompt/resolutionをanalyzeWorkflow()で認識し、Input タブのPrompt欄・Settings欄から編集できるようにした。モデルロード・複数参照画像のLoadImageカードは既存の汎用ロジックで無改修対応。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 17:23:13 +09:00
ketle-manandClaude Sonnet 5 cdf18e89ce chore(release): v0.7.2 - Workflowタブソート機能、Video Editプロジェクト永続化、Assetタイプフィルタ
Workflowタブにグループフィルタ右隣のソートドロップダウン(Date/Name昇順降順)を
追加。VideoタブではEditサブタブのトリムパネルの不具合修正(「現在位置を使用」
でスクラバー表示が更新されないバグ、バッジ重なり)とUI調整(左ペイン幅・スクラバー
高さ)を行い、Video Edit Phase 5としてタイムラインのSave/Save As/Load永続化
(VideoEditProjectService新設)とサイドバーProjectタブのPlan/Editモード切替を
実装。Asset subtabには動画/画像タイプフィルタドロップダウンを追加した。

DEVLOG.md/README.mdを更新し、pyproject.tomlのバージョンを0.7.2に更新。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 10:48:36 +09:00
ketle-manandClaude Sonnet 5 7237bec315 feat: add sort dropdown to Workflow tab toolbar
Adds date/name asc/desc sorting next to the group filter, matching the
Gallery tab's existing sort UI and localStorage persistence pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 09:10:52 +09:00
ketle-man f687f8d8ea chore(release): v0.7.1 - Unsloth Tagger経路のSSRFキー漏洩を修正
backend=="unsloth"のTagger VLM呼び出し(vlm_models/vlm_predict)が
unsloth_routes.pyと同じlocalhost allowlistを持たず、クライアント指定の
api_urlへAPIキーを無検証で送信していた問題を修正。副次的にgmic_qt_path
の検証も絶対パス/.exe拡張子/実ファイルチェックへ強化。
2026-09-17 17:53:58 +09:00
ketle-manandClaude Sonnet 5 8a3de00440 chore: QWEN_CHAT_INTEGRATION_PLAN.mdもgit管理対象外に追加
VIDEO_EDIT_TAB_PLAN.mdと同様、作業用の計画メモのため.gitignoreに追加。
未追跡ファイルだったためrm --cachedは不要。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 17:40:13 +09:00
ketle-manandClaude Sonnet 5 cddfa4fdc1 chore: graphify-out全体とVIDEO_EDIT_TAB_PLAN.mdをgit管理対象外に変更
graphify-out/はこれまでgraph.json等の一部ファイルをバージョン管理下に
置いていたが、他クローンへの共有価値がないローカル専用出力として全体を
.gitignore対象にした。VIDEO_EDIT_TAB_PLAN.mdも作業用の計画メモとして
対象外にした。いずれもワーキングツリー上のファイルは削除せず、git管理
からのみ除外している。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:44:38 +09:00
ketle-manandClaude Sonnet 5 ac675ce74f docs: VIDEO_EDIT_TAB_PLAN.mdにv0.7.0でリリースしたUI改善一式を追記
Asset subtabの中央下部移設・トリムスクラバー・プレビュー枠固定・
Frame/Asset連携の4件を第11節として追記し、Phase 1 MVPからv0.7.0
リリースまでの状態を冒頭に明記。Critical Filesにも今回変更した
ファイルを追加した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:40:26 +09:00
ketle-manandClaude Sonnet 5 74eb8b779f Merge branch 'feature/video-edit-tab': Video Edit tab (v0.7.0)
Video タブに簡易動画編集機能(Editサブタブ)を新規実装。クリップの
タイムライン配置、トリム(Clipchamp風ビジュアルスクラバー)、複数
クリップの結合書き出し、静止画/アニメーション画像クリップ対応。
あわせてAssetサブタブをPlan/Editと並ぶ中央下部へ移設し検索・グリッド
/テーブル表示切替を追加、プレビュー枠の高さ固定、Frame保存のVideo
Assets登録とAsset→PlanのFirst/Last Image連携も実装した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:32:40 +09:00
ketle-manandClaude Sonnet 5 baed398fe8 chore(release): v0.7.0 - Video Edit tab初回リリース + Asset移設・トリムスクラバー等
pyproject.tomlのバージョンを0.7.0に更新し、DEVLOG.mdとREADME.mdを
今回のリリース内容(Edit subtab全体の初回公開、Assetサブタブの中央下部
移設、トリムパネルのClipchamp風スクラバー、Frame保存のVideo Assets
登録、Asset→PlanのFirst/Last Image連携)に合わせて更新した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:31:47 +09:00
ketle-manandClaude Sonnet 5 77cfedb243 docs(video-tab): Video Tabヘルプを現行UIとEdit/Asset機能に合わせて更新
Asset subtabが左サイドバーから中央下部(Plan/Editと並ぶ)に移設された
ことを反映し、Project subtabの説明からも「Assetの隣」という古い記述を
削除。Editサブタブ(クリップタイムライン・トリムスクラバー・書き出し)
とAssetサブタブ(検索・グリッド/テーブル切替・First/Last Image連携)の
説明を新規項目として追加し、Frameタブの説明にVideo Assets登録
チェックボックスを追記。index.html・i18n.js(en/ja/zh)・app.jsの
helpIdMapの3点セットで更新。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:28:28 +09:00
ketle-manandClaude Sonnet 5 ad70622942 feat(video-tab): FrameのVideo Assets登録チェックボックスとAsset→Plan画像連携を追加
Frameタブの「Outputに保存」にオプションのチェックボックスを追加し、
チェック時は保存したフレームをGalleryの__Video Assets__グループに
自動登録できるようにした。

またAssetタブの画像アセット詳細に「First Imageに設定」「Last Imageに
設定」ボタンを追加し、Planタブの選択中ブロックのFirst/Last Imageへ
直接送れるようにした(動画クリップでは非表示、画像のみ対象)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:19:51 +09:00
ketle-manandClaude Sonnet 5 f726418aeb feat(video-edit-tab): トリムパネルにClipchamp風のビジュアルスクラバーを追加
動画クリップのトリム編集を、テキスト入力のみだったUIから、時間目盛り・
再生位置(playhead)・ドラッグ可能なトリムハンドルを備えたビジュアルな
スクラバーに拡張。プレビュー動画のcurrentTimeとplayheadが同期し、
トラックのクリック/ドラッグでシークも可能。既存のStart/End数値入力・
「現在位置を使用」ボタンとは同じcommit経路で連動する。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:55:30 +09:00
ketle-manandClaude Sonnet 5 a917e82ad7 feat(video-tab): AssetをPlan/Editと並ぶ中央下部の第3サブタブに移設
左サイドバーにあったAsset一覧・詳細パネルを中央ペイン下部に移設し、
左=素材一覧(サムネイル/テーブル切替可)・右=選択ファイルの詳細という
2カラムレイアウトに再構成。一覧上部にグループ切替・ファイル名検索・
表示切替ボタンを追加した。左サイドバーはProjectのみのシンプルな
パネルになった。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:32:54 +09:00
ketle-manandClaude Sonnet 5 2fc2365461 fix(video-tab): プレビュー枠の高さを固定してPlan/Editの位置ずれを解消
動画ソースのネイティブサイズによってプレビュー枠(min-height)が伸縮し、
中央ペイン下部のPlan/Editサブタブの表示位置が動画ごとに動いてしまう問題を修正。
枠の高さを固定し、動画/画像は縮小表示のみ(拡大はしない)に変更した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:18:58 +09:00
ketle-manandClaude Sonnet 5 337fd6ecb8 docs: log Video Edit tab work-in-progress in DEVLOG
Records this session's feature/video-edit-tab branch work (unreleased,
no version bump) — Edit subtab MVP, Phase 0 node-behavior findings,
image-clip support, and the audio-mismatch fix — under a new
Unreleased section, per existing DEVLOG convention where every
version heading corresponds to an actual tagged release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 20:13:07 +09:00
ketle-manandClaude Sonnet 5 831ba1bd43 Support still/animated image clips in the Edit timeline
Asset's Video Assets/Video Temp groups already list any image tagged
into them (no video-only filter), but selecting one always fed a
<video> element, which can't render image formats — the preview
stayed blank and "Send to Edit" produced a broken clip.

Adds an <img> companion to both preview panes (video-preview.js gains
a mediaType param on setSourcePreview/setResultPreview) so images
preview correctly, and gives Edit-tab clips a kind: "video" | "image".
Image clips get a single editable hold duration instead of in/out trim
points, and export turns them into a real video segment via
LoadImage -> (ImageScale center-crop, only if resolution needs to
match another clip) -> RepeatImageBatch -> CreateVideo — verified on a
live instance to produce an exact-duration, concatenation-compatible
clip. The resolution-mismatch guard now only compares video clips
against each other, since images are auto-fit instead of blocked.

Also fixes a bug this surfaced: ConcatenateVideo hard-errors mixing a
clip with real audio (e.g. stereo/32kHz) next to a silent
image-derived clip. Multi-clip exports now strip audio from every
video clip too (via GetVideoComponents -> CreateVideo) so the whole
timeline is uniformly silent when concatenating; a lone clip keeps its
original audio since no concatenation occurs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 20:10:07 +09:00
ketle-manandClaude Sonnet 5 899feb1bb5 Add sequential timeline preview sharing the Result pane with Export
Adds a Play/Stop toggle on the timeline toolbar that plays every ready
clip back-to-back (each trimmed to its in/out point) in the same
"Result" preview pane Export writes its finished output into — not a
new <video> element — so what's shown there is always exactly what
Export would currently produce. Clip-boundary advancement is driven by
timeupdate/ended listeners comparing against each clip's trimEnd.

video-preview.js gains getResultPreviewVideoElement() since driving
playback (seeking, listening for the trim-out point, swapping sources)
needs direct element access that setResultPreview()'s simple
src-and-visibility API doesn't provide.

Verified end-to-end on a live instance: 2-clip preview with automatic
mid-playback advancement at the first clip's trim-out, then export
producing the same combined duration the preview showed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:55:12 +09:00
ketle-manandClaude Sonnet 5 075e2a8c62 Fix trim panel overflow and add timeline total duration display
The Start/End (seconds) inputs and their "use current position"
buttons were laid out side by side, letting the button label overflow
past the container edge and making the button hard to distinguish
from a text field. Stacks the button below the input instead, shrinks
it, and narrows the input column so both fit cleanly.

Also adds a running total-duration readout next to the Clear button,
summing every clip's trimmed length — updated automatically whenever
the timeline re-renders.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:46:46 +09:00
ketle-manandClaude Sonnet 5 77163de202 Fix Edit tab layout collapsing to center; drop its own dropzone
Root cause: a leftover CSS rule from the pre-implementation
"Editing tools coming soon" placeholder era (display:flex;
align-items:center; justify-content:center; dashed border) still
targeted .wfm-video-edit-panel and, coming later in the cascade,
silently overrode the real layout — shrinking the timeline, trim
panel, and export panel to their content width and centering them
instead of stretching full-width like the Plan subtab. Removed the
stale rule and gave the timeline track explicit width:100%/flex:none
so it stays a fixed full-width bar (matching Plan's track) regardless
of clip count.

Also removes the Edit subtab's own drop zone now that clips can only
be added via Asset's "Editへ送る" and the Video Source panel's
"Add to Edit" button — one fewer entry point to keep in sync, and the
now-unused videoEditDropLabel i18n key goes with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:38:22 +09:00
ketle-manandClaude Sonnet 5 7f0ecc2ba3 Redesign Video Edit timeline to match the Plan subtab's UI pattern
Replaces the vertical clip list (per-row ▲▼⧉✕ buttons) with a
horizontal timeline track whose blocks are left-aligned at their real
duration (px/sec scale) rather than stretched to fill the track like
Plan's blocks. Reordering moves to a shared toolbar below the track
(◀/▶/Duplicate/Delete/Clear acting on the selected clip), matching
Plan's "+ Split/+ Block/Delete" pattern, plus native HTML5
drag-and-drop reordering directly on the blocks.

Adds a new "Add to Edit" button on the Video Source panel so whatever
is loaded there (a plain drop, an Asset selection, a Frame/GIF source)
can be added to the Edit timeline explicitly — deliberately not
automatic on every Source drop, since Frame/GIF is a single-video tool
used far more often than multi-clip editing and auto-adding would
clutter the timeline.

Verified end-to-end on a live ComfyUI 0.36.0 instance: left-aligned
block widths, Asset -> Add to Edit handoff, move/duplicate/delete/
clear (with i18n'd confirm dialog), and export after a clear.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:24:51 +09:00
ketle-manandClaude Sonnet 5 ac733db81e Add Video Edit tab MVP: clip timeline, trim, and concatenate export
Implements the Edit subtab (previously a placeholder) with a minimal
clip timeline: add clips from Asset/local files, reorder/duplicate/
delete, trim per clip, and export by dynamically assembling a ComfyUI
API-format prompt ("Video Slice" per clip -> ConcatenateVideo when
multiple clips -> SaveVideo) run through the existing execution
infrastructure. Exported videos are tagged into the __Video Temp__
gallery group like Plan-tab runs.

Verified end-to-end against a live ComfyUI 0.36.0 instance, including
two bugs only caught by inspecting actual output (not just
execution_success): a missing WebSocket connect before trackProgress,
and VideoTrim silently no-opping on a flat trim value (switched to the
simpler, non-experimental "Video Slice" node instead). See
VIDEO_EDIT_TAB_PLAN.md for the full design and findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:05:31 +09:00
ketle-manandClaude Sonnet 5 7f2720ff8f release: v0.6.3 — Send CCボタンでMP4動画をComic Creatorの動画ツールへ送れるように対応
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 22:24:58 +09:00
ketle-manandClaude Sonnet 5 f9dbf3f399 release: v0.6.2 — Image Editタブにペンタブレット筆圧対応とFillツール(バケツ塗りつぶし)を追加
Draw/MaskツールをPointer Events化し、ペン筆圧でブラシサイズ/不透明度が
変化するように対応(マウス操作の挙動は変更なし)。Comic CreatorのImageタブ
から移植したFillツール(バケツ塗りつぶし、Solid/Gradient対応)を新規追加。
実機フィードバックを受け、Fillのグラデーションランプ/方向ダイヤルが
ペンタブレットで操作できない不具合も追加修正。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 09:25:36 +09:00
ketle-manandClaude Sonnet 5 a99bfc19a1 release: v0.6.1 — Galleryタブ検索の停止不具合・パフォーマンス問題を修正
検索欄のdebounce/AbortController追加、メタデータストアのロック保持時間短縮、
ponyxlWildcardsVaultフォールバック探索でのVaultフォルダ誤検出によるglob多発を解消。
53枚フォルダへの検索が3327ms→74msに改善。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:57:19 +09:00
ketle-manandClaude Sonnet 5 d75a9c812f release: v0.6.0 — Gallery/Image EditにPSD(Photoshop)対応を追加、Image Editのアクションバー/レイヤーパネル改善
- Gallery: 複数選択画像を1つのレイヤー付きPSDにまとめる「PSD作成」一括操作を追加
- Image Edit: Save PSD(レイヤー個別保持で書き出し)/Open PSD(レイヤー分解して読込)を追加
- Gallery→Image Edit: 複数選択画像を各レイヤーとして一括転送する「レイヤーとして編集」を追加
- Gallery: .psdファイルの一覧・サムネイル・プレビュー表示に対応(psd-tools合成→PNGキャッシュ)
- 実機発覚バグ2件を修正: サムネイル配信がイベントループをブロックする問題(run_in_executor化)、
  単一画像送信ボタンでPSDが1レイヤーに潰れる問題
- Image Edit: File Open改名、Open PSD配置、Close/複製レイヤーボタン追加、レイヤーヘッダー2段化
- ヘルプドキュメント更新+検証中に見つかった既存の翻訳漏れ/重複定義を修正
- 依存関係: psd-tools>=1.10を追加(pytoshopはWindowsでCコンパイラ必須のため不採用)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 20:22:41 +09:00
ketle-man 925a249cbe chore: v0.5.8リリース準備
Tagger DBタブの一括削除機能(PR #2)追加に伴うバージョン更新
2026-09-02 01:19:13 +09:00
ketle-man e067f34cd4 Merge pull request #2 from censor-ed/feature/tagger-bulk-delete
検証済み: バックエンドAPI・フロントエンドの動作を実機(ComfyUI + ブラウザ)で確認。一括削除時のcount不整合と全選択チェックボックスのトグル挙動の2点を修正コミットで追加。
2026-09-02 01:10:37 +09:00
ketle-man 995e98e1aa fix(tagger): 一括削除のcount不整合と全選択トグルの挙動を修正
- handle_db_delete_bulk: レスポンスのcountを渡されたID数ではなく
  実際の削除件数(delete_bulkの戻り値)にする
- _dbToggleSelection: 常時反転するトグル方式ではなく、クリック後の
  ヘッダーチェックボックス自身の状態に全行を揃える一般的な全選択挙動にする
2026-09-02 01:05:14 +09:00
censor-ed 398be43060 feat(tagger): Tagger DBの複数選択削除を追加 2026-09-01 10:53:27 +09:00
ketle-manandClaude Sonnet 5 1155cf1233 release: v0.5.7 — Video Planタブの設定項目レイアウト刷新(MiniMax H3 Turbo/LTX-2.5 Prompt Enhance/Wan2.2 Turbo対応) + フルバックアップのドキュメント修正
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:00:36 +09:00
ketle-manandClaude Sonnet 5 5aef634115 fix(tagger): WD Tagger系ONNXモデルの画像前処理をWD Tagger標準に修正
wd-swinv2-tagger-v3等で画像内容と一致しない不自然なタグが出力される問題を修正(Issue #1)。
単純リサイズ+RGBのまま0.0-1.0正規化だった前処理を、WD Tagger系標準の
アスペクト比維持パディング+RGB→BGR+0-255のfloat32のまま、に変更した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 16:54:27 +09:00
ketle-manandClaude Sonnet 5 b09cd2ef90 release: v0.5.5 — Videoタブ全面刷新(Plan/Asset/Editバッチ生成 + Projectサブタブ + プレビュー2ペイン化) + Video Assets/Video Temp運用整理 + Galleryメタデータ消失バグ根本修正
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 15:30:28 +09:00
ketle-manandClaude Sonnet 5 426f27c05a Video: __VideoAssets__グループがGallery再起動後に消えるバグを修正
__VideoAssets__はバッチ実行側からensure_groupを呼ぶコードが無く、一度削除されると
add_to_groupが個別画像への書き込みのみ行いレジストリを再登録しないため二度と一覧・
詳細パネルに出てこなくなっていた。ensureFeederGroup()と同型のensureVideoGroup()を
新設し、Video AssetタブのInitで確実に再登録するよう修正。詳細はDEVLOG.md参照。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 00:30:22 +09:00
ketle-manandClaude Sonnet 5 336e37724f Video: Plan/Asset/Editタブによるバッチ動画生成機能を追加
タイムラインブロック単位でFirst/Last画像・プロンプトを設定しバッチ生成するPlanタブ、
Galleryのグループ機能を再利用したAssetタブ(__VideoAssets__/__vtemp__)を新設。
調査過程でGalleryのジャンクションフォルダ環境でのパス不一致バグ、グループフィルタの
非再帰スキャンバグ、convertUiToApi()のCOMBO無警告フォールバックによるチェイン画像
すり替えバグの3件も修正。詳細はDEVLOG.md参照。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 22:15:01 +09:00
ketle-manandClaude Sonnet 5 7274017347 release: v0.5.4 — GenerateUIタブのLTX-2.5/Wan2.2対応 + VideoタブのLTX-2.5/Wan2.2対応・ハイライト表示 + RAW JSON検索の意図しない編集を修正
- GenerateUIタブ: LTX-2.5/Wan2.2ワークフローのPositive/Negative Prompt検出(LTXVDualCFGGuider/
  LTXVConditioning対応)とSettingsタブLatent Image検出(EmptyHunyuanLatentVideo/EmptyLTXVLatentVideo
  + Lengthフィールド)を追加、isVideoWorkflow()の動画タイムアウト判定も拡張
- Videoタブ: 該当項目のラベルをGenerateUI Modelタブと同じ色でハイライト表示する機能を追加
  (Wan2.2など操作不可な項目は非ハイライトのまま)
- 生成UIタブRAW JSON検索: ヒット箇所へのフォーカス/selectionRange設定を廃止し、ハイライト表示のみ
  に変更(検索直後のキー入力で意図せずJSONが編集される問題を修正)
- 別セッションでのVideoタブLTX-2.5/Wan2.2対応(サブグラフ入力labelベースの汎用検出)・MultiType
  ウィジェット脱落バグ修正・動画再生ボリューム保存設定、およびAI Assistant設定のUnsloth対応・
  Prompt/Tagger独立モーダル化・モデルアンロード機能を本リリースに統合
- ヘルプ(index.html/i18n.js 3言語/app.js helpIdMap)とREADME.md/DEVLOG.mdを更新

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 16:50:30 +09:00
ketle-manandClaude Sonnet 5 b94ab48ca4 AI Assistant設定をUnsloth対応+Prompt/Tagger独立モーダル化、モデルアンロード機能を追加
- Prompt AI/Tagger VLMにUnslothバックエンドを追加(サーバー側プロキシでAPIキー付与)
- Taggerタブ: Ollama直呼び出しを廃止しOllama/LM Studio/Lemonade/Unsloth共通のvlm_models/vlm_predictに統一、API URL直接入力欄を廃止
- 中央Settingsタブの「AI Assistant Settings」を廃止し、Prompt/Tagger各タブに⚙設定ボタン+モーダル(ai-settings-modal.js)を新設、それぞれ独立したlocalStorageキーで管理
- Thinking mode / Max tokens をPrompt/Tagger双方の生成呼び出しに反映(Tagger側はPython側のvlm_predictにも反映)
- Ollama/LM Studio/Lemonadeのモデルアンロード機能を追加(AI TOOL設定にもUnloadボタンを追加、Unslothは非対応として明示)
- ヘルプタブ(Prompt/Settings/AI TOOL/Tagger)の説明文を新仕様に合わせて更新(EN/JA/ZH)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 13:17:03 +09:00
ketle-manandClaude Sonnet 5 ed3512d269 ci: publish-node-actionのバージョンをSHA固定(サプライチェーン対策)
@mainの可変ブランチ参照はComfy-Org/publish-node-actionが更新された際に
挙動が変わりうるため、リリースタグ1.0.1のコミットSHAに固定する。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 10:24:53 +09:00
ketle-manandClaude Sonnet 5 9ed91e64f8 Video: LTX-2.5 / Wan2.2対応 + 動画音量の保存設定を追加
- VideoタブがLTX-2.5 (Text/Image/First&Last-Frame to Video) とWan2.2 (Text to
  Video) を検出・読み込み・生成できるよう汎用化。従来MiniMax H3専用だったノード
  検出/フォーム反映ロジックを、サブグラフが公開する入力のlabelベースで意味的に
  スロットを特定する方式に一般化した
- convertUiToApi(): object_infoの複合型宣言(例 "FLOAT,INT")がウィジェット名
  リストから丸ごと脱落し、後続ウィジェットの値がずれるバグを修正。
  LTXVEmptyLatentAudio.batch_sizeに誤った値が注入され動画生成が
  "Expected size 1 but got size 25" で失敗する原因だった
- Videoタブのminimax_test.json自動読み込みを廃止(同梱していないため)
- 動画再生ボリュームを設定タブに追加し、Video/Galleryの全プレーヤー間で
  保存・共有されるようにした(動画を切り替えるたびに最大音量へ戻る問題を解消)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 00:51:09 +09:00
ketle-manandClaude Sonnet 5 c0db7374b7 release: v0.5.3 — Labタブのワークフロー切替時の表示残留を修正 + Galleryタブ読み込み高速化・rootフォルダ自動展開 + READMEスクリーンショット刷新
- Labタブ: ワークフロー読み込み時、Labサブタブが既にアクティブでも
  Setting 1のライブ反映セルを再描画するよう修正(前ワークフローの表示残留を解消)
- Galleryタブ: フォルダスキャン時の不要なPath.resolve()呼び出しを除去し
  9,777枚のフォルダで7.5秒→0.14秒に高速化。os.walk()ベースの再帰スキャンも
  DirEntryベースに書き換え
- Galleryタブ: フォルダツリーのrootをデフォルト展開状態に変更
- README: スクリーンショット5件を削除、Video/Lab/AI TOOLタブの新規4枚を追加

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 16:08:24 +09:00
ketle-manandClaude Sonnet 5 cd4b721d51 release: v0.5.2 — GenerateUI/Labタブの動画ワークフロー対応 + VideoタブにFrame抽出/GIF変換機能 + データ管理監査とZIPフルバックアップ + Send to LI node統一
- GenerateUI/LabタブでMiniMax H3等のオールインワン動画ノードのプロンプト・画像を編集可能に(analyzeWorkflow拡張)、生成タイムアウト延長、動画結果プレビュー対応
- バグ修正: Lab index imageが真っ黒になる問題(動画は最終フレームを抽出して描画)
- バグ修正: Lab Ksampler列が動画ワークフローでも表示される問題(内部SamplerCustomAdvancedを編集対象から除外)
- Videoタブに Frame抽出(クライアント側canvas)/ アニメーションGIF変換(サーバー側PyAV+Pillow)を追加、Video Sourceパネルでプレビュー統合
- データ管理: civitai_cache.jsonのエクスポート漏れを修正、ZIP形式のフルバックアップ機能を新規実装(ai_skills/lab_plan/style/tagger.db対応、Zip Slip対策、デフォルトワークフロー/wildcardフォルダのオプトイン・復元除外)
- Image Edit/Galleryタブの画像送信ボタンを「Send to LI node」に統一し、Galleryタブにも追加
- Galleryタブの✕クリアボタンを表示形式切替ボタンの左隣に移動

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 13:27:12 +09:00
ketle-manandClaude Sonnet 5 46c789c492 release: v0.5.1 — mp4動画の埋め込みメタデータ対応 + Gallery Infoタブに寸法/再生時間表示 + MiniMax H3等オールインワン動画ノードのプロンプト抽出バグ修正
Gallery Prompt/JSONタブ・Metadataサブタブ・サイドパネルIタブの3箇所で、ComfyUIのSaveVideoがmp4のmoov/udta/metaに埋め込むworkflow/promptタグ(PNGのtEXtチャンクと同一キー名)を読み取れるようにした。バックエンドはPyAV、Metadataサブタブ/サイドパネルIタブはブラウザ側でMP4ボックス構造を直接パースする実装。副産物として、CLIPTextEncode系を介さず自ノードのprompt入力に直接テキストを持つ動画生成オールインワンノード(MiniMaxH3ImageToVideo等)のプロンプトが抽出できていなかったバグも修正。Gallery Infoタブには画像の幅×高さ・動画の再生時間の表示を追加。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:04:48 +09:00
ketle-man ede39ec715 release: v0.5.0 — mp4動画Gallery対応 + Videoタブ新設(MiniMax H3専用) + バグ修正: Galleryのタグ/メモが再起動後に消える
- Galleryタブでmp4動画の閲覧・再生・サムネイル生成(PyAV)に対応
- 新規Videoタブ(MiniMax H3専用、GenerateUIとは独立)を3ペイン構成で追加
  - first_frame/last_frameは両方optional、LoadImageノードの無いワークフローにも対応
  - Workflowタブから「Load in Video」で読み込み可能
- バグ修正: cleanup_stale_images()が非再帰スキャン時にサブフォルダ内画像/動画のメタデータを誤って全削除していた問題を修正
- バグ修正: Gallery側のタグ/メモ保存がサーバーエラーをサイレントに無視していた問題を修正
2026-08-27 14:46:09 +09:00
ketle-manandClaude Sonnet 5 ad3b380929 Save to Input(旧Send to ComfyUI)ボタンを削除
Send to Workflowと機能が重複していたため、Image Editタブの
「Save to Input」ボタン・uploadToComfyUI()を削除。ヘルプ・README・
i18n(日英中)を更新。次回リリース時にバージョンを上げて反映する。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:10:32 +09:00
ketle-manandClaude Sonnet 5 c589e562e4 release: v0.4.8 — LabにPromptグループ一括適用+キーフレーム並べ替え + デフォルトワークフロー上書き保存バグ修正 + StyleテーブルAdd to this file
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 12:16:06 +09:00
ketle-manandClaude Sonnet 5 cb61dc61a7 release: v0.4.7 — Prompt Tab内部整理 + Gallery Promptタブ新設 + Send to Canvas改善 + Presetsソート
- prompt-tab.js(1545行)をprompt-presets/ai-chat/wildcards/styles.jsの4ファイルに分割、
  prompt-tab.jsは薄いオーケストレーターに(prompt-table.js/app.jsは無変更でre-export経由)
- ワイルドカード入力支援ツールバーをPresetsのプロンプト編集モーダルへ追加(Wildcardモーダル
  ではなくプロンプトを書く場所に付けるべきという指摘を受けて再配置)
- Style適用とワイルドカード展開の実行順序を修正: Style内の__file__参照が展開されずに
  送信されていたバグを解消(Style適用→展開の順に変更)
- yamlワイルドカード(ImpactWildcardEncode)を使うサンプルワークフローを追加
- Galleryタブ詳細パネルに「Prompt」タブを新設(既定選択、Metadataタブと同じPOS/NEGリスト
  UIを再利用); Gallery側のプロンプト抽出はAPI形式優先のprompt_workflowを新設し、
  Metadataタブとの抽出結果の食い違いを解消
- Send to Canvasのフォールバックを、Workflow Studio LibraryのWタブを絞り込み表示する
  方式に統一(UI形式のタイトルドラッグ・API形式非対応だった従来方式を置き換え)
- PresetsテーブルツールバーにMarkdown ★/Bソートトグルボタンを追加(複合ソート対応)
- graphifyの運用を一時停止(CLAUDE.md、ユーザー指示による)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 12:19:09 +09:00
ketle-manandClaude Sonnet 5 ad9d06d3e1 graphify: v0.4.6の変更を反映しナレッジグラフを更新
v0.4.6(Metadataタブ移動・GenerateUI Save非表示・Prompt Table表示)で
変更した20ファイルをgraphify --updateで再抽出。
2320ノード・5560エッジ・157コミュニティ(旧グラフから+141ノード/+314エッジ、
重複ノード13件を整理)。DEVLOG.index.md/README.index.mdの2件はOllamaの
出力トークン上限で今回は未反映(次回--updateで再試行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 20:33:26 +09:00
ketle-manandClaude Sonnet 5 ea408690ff release: v0.4.6 — MetadataタブをGalleryへ統合 + PromptタブにTable表示を新設
- MetadataタブをGalleryタブのサブタブ(Style_Catalog右隣)へ移動
- GenerateUI: Batch/LabサブタブでSaveボタンを非表示化
- Promptタブ: Form/Table切替を追加。Tableには編集可能な
  Presets/Presets Group/Wildcards/Styleテーブルを新設
  - #列クリックで行選択→ツールバーDelete/Deselectで一括操作
  - 番号ダブルクリックでPositive/Negative編集モーダル
  - PresetsのB列/BCボタンでBatchグループ登録
  - PP/NP Copy + グループ選択でAdd to Group(Presets Groupの
    Add Preset選択式を置き換え)
- ヘルプ(index.html + i18n.js 3言語 + helpIdMap)とREADMEを更新

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 15:00:23 +09:00
ketle-man cbb096ec85 release: v0.4.5 — Lab Model列の無意味な空キーフレーム表示を解消(対象ノードなし時は非表示化)
対象ノード(Checkpoint/LoRA/VAE)がワークフローに存在しない列でも
Model列に常にSetting 1のキーフレーム行が表示されていた問題を修正。
Run時には何の効果も持たない死んだコントロールだったため、
_hasNodeForColumn()で判定し、対象ノードが無い列はプレースホルダー
表示にして+ボタンでの追加もブロックするようにした。
2026-08-21 20:27:05 +09:00
ketle-manandClaude Sonnet 5 99f342cc89 release: v0.4.4 — LoRAノードバイパス機能(Lab/GenerateUI Model) + Lab LoRA構文/トリガーワードのプロンプト適用 + AI TOOL Chat生成パラメータ拡張
- GenerateUI ModelタブのLoRA列にバイパストグルボタン(⛔)を追加(strength=0の簡易実装)
- Lab機能のLoRAキーフレームに「LoRAノードをバイパス」「LoRA構文/トリガーワードをポジティブプロンプトに適用」チェックボックスを追加
- AI TOOL Chatのgenerate_imageツールにsteps/cfg/sampler_name/scheduler/denoise/seed/width/height/batch_size/aspect_ratioを追加
- Resolution Selectorノード(ComfyUI組み込み)のaspect_ratio COMBO値対応
- ヘルプ(index.html/i18n.js 3言語/app.js helpIdMap)・README.md更新
- README/DEVLOG索引自動化(index.html PostToolUseフック + generate_doc_index.py)を導入

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 15:44:33 +09:00
ketle-manandClaude Sonnet 5 9681e76848 release: v0.4.3 — 共有モーダルの☆/リネーム残留バグ修正 + Lab Model列の+/-をCheckpoint/LoRA/VAEで完全独立化
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:44:26 +09:00
ketle-manandClaude Sonnet 5 e9fbb6dd39 release: v0.4.2 — Lab「Model」列統合(Checkpoint/LoRA Single/VAE)+ フィールド単位継承 + C/L/V表示切替
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 21:24:30 +09:00
ketle-man f858c5138a release: v0.4.1 — Mask Editor One連携ボタンを選択中ノード方式に再設計
Send to Workflowと同じ「選択中ノード優先・フォールバック」方式に
変更し、WFS生成UIタブへの事前読み込みを不要にした。
comfyUI.currentAnalysis依存を撤去し、ボタンは常時有効化。
2026-08-20 13:42:00 +09:00
ketle-man 7d9c498234 fix: Save to Inputボタンの背景色をデフォルトに戻す
改名時にwfm-btn-primaryクラスが残っており、他のアクションバー
ボタン(Save PNG/Save to Gallery/Send to Workflow)と見た目が
不揃いになっていた。
2026-08-20 12:17:49 +09:00
ketle-man d43b35dde3 chore: graphifyナレッジグラフをv0.4.0まで更新 + graphify CLAUDE.md統合
- graphify-out/: モジュール分割・Mask Editor One連携・Send to Workflow等、
  v0.4.0までの変更10ファイルを再抽出しグラフ再構築
- QWEN_CHAT_INTEGRATION_PLAN.md(未追跡の非公開プラン文書)由来の
  ノード/エッジ/ハイパーエッジを除去し、.graphifyignoreで今後も除外
- graphify claude install: CLAUDE.mdにgraphifyクエリ優先参照ルールを追記、
  .claude/settings.jsonにPreToolUseフック(graphify hook-guard)を登録
2026-08-20 12:14:48 +09:00
ketle-man b79f4e8fd0 release: v0.4.0 — モジュール分割リファクタ + Mask Editor One連携 + Send to Workflow
DEVLOGにv0.3.99以降の作業をまとめて記録:
- image-edit-tab.js / models-tab.js の低凝集度モジュール分割(前日実施分)
- Mask Editor One連携ボタン(DEVLOGフェーズ2計画の実装)
- Send to Workflowボタン追加 + Send to ComfyUI→Save to Input改名

pyproject.toml/READMEバージョンバッジをv0.4.0に更新。
push・GitHub Releaseはユーザー指示待ち。
2026-08-20 11:34:59 +09:00
ketle-man 87c53ef977 Rename Send to ComfyUI → Save to Input、Send to Workflowのヘルプ追加
- Image Edit タブの「Send to ComfyUI」ボタンを「Save to Input」に改名
  (README・ヘルプ英/日/中を追従)
- 前回追加した「Send to Workflow」ボタンのヘルプ項目が未記載だったため追加
2026-08-20 11:26:34 +09:00
ketle-man 8b75d2a8d0 Add Image Edit タブ「Send to Workflow」ボタン
Send to ComfyUI(アップロードのみ)とは別に、アップロード後の
ファイル名を ComfyUI 側で選択中のノードの image ウィジェットへ
直接書き込む機能を追加。chat_TE カスタムノードの
「Send to workflow」と同等(window.opener 経由のクロスウィンドウ
ブリッジ、Mask Editor One 連携と同じパターン)。
2026-08-20 11:15:38 +09:00
ketle-man 7aa2535964 Add Mask Editor One連携ボタン(DEVLOGフェーズ2) + ヘルプ更新
Image Edit タブの Mask ツールに「Edit in Mask Editor One →」ボタンを追加。
ワークフロー中の既存 MaskEditorOne ノードへキャンバス合成画像を送信し、
window.opener 経由で ComfyUI 側の実際のモーダルエディタを開いて編集、
Apply 後のマスクを新規マスクレイヤーとして WFS に取り込む。
2026-08-20 10:31:07 +09:00
ketle-manandClaude Sonnet 5 7769de6fbb chore: update graphify knowledge graph after models-tab.js refactor
Incremental update covering the 8 changed/new JS files from the
models-tab.js cohesion refactor (models-tab.js + 7 new models/*.js
modules). Code-only change, so semantic extraction was skipped.
2369->2378 nodes, 5401->5453 edges, 148->151 communities.

Unlike the image-edit-tab.js refactor, this one did NOT improve
cohesion: the 85 models-tab-related nodes remain a single community
(density 0.096), essentially unchanged from before the split (76 nodes,
same single community). Root cause: Phase 5 (grid-view/selection-bulk/
detail-panel) deliberately uses a circular import design rather than
one-directional callback injection, since rendering a card wires up
selection/side-panel/detail-modal handlers that in turn re-render the
grid. Files got smaller and easier to read, but the dependency graph
stayed tightly coupled - documented as a real finding, not a failure
to fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 22:21:29 +09:00
ketle-manandClaude Sonnet 5 7eafe9a684 refactor: models-tab.jsからグリッド描画/選択操作/詳細パネルをmodels/へ切り出し
低凝集度モジュール分割計画(models-tab.js側)Phase 5完了。
models/grid-view.js: グリッド描画(Thumbnail/Table)とBatch/Stack/
Favoriteトグル。
models/selection-bulk.js: 複数選択モードとバルクグループ/バッジ/
お気に入り/移動/削除操作。
models/detail-panel.js: 詳細モーダルとサイドパネルのInfo/Group/CivitAI
タブ。

この3モジュールはmodels-tab.js本体を含めた4者間で循環importする設計
にした(カード描画が選択/サイドパネル/詳細モーダルのクリックハンドラを
配線し、それらが変更後にグリッドを再描画するため、単純な一方向の依存
関係にならない)。全export/importがfunction宣言(hoisting対象)で、
相互呼び出しはすべてイベントハンドラ内(モジュール評価時ではない)に
限られるため、ES Modulesとして安全に解決される。実機でグリッド描画・
サイドパネル・複数選択・バルクバー・お気に入り/バッチトグル・詳細
モーダル開閉を確認し、循環import由来のエラーがないことを確認した。

models-tab.js: 2074→592行(約71%削減、Phase 3-5合計で2522→592行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 22:14:31 +09:00
ketle-manandClaude Sonnet 5 c9089c2892 refactor: models-tab.jsからフィルタ/ソートと共有ヘルパーをmodels/へ切り出し
低凝集度モジュール分割計画(models-tab.js側)Phase 4完了。
models/filters.js: フィルタリング(ステータス/お気に入り/バッチ/タグ/
バッジ/ディレクトリ/グループ/検索)・ソート・タグ/グループ/
ディレクトリフィルタドロップダウンの描画一式。
models/helpers.js: parseModelPath/getExtension/getStem/previewUrl/
loadPreviewImageという、以降のフェーズ(grid-view/detail-panel等)
でも共有される軽量パスヘルパーを、重複を避けるため先出しで切り出した。
(2522→2074行)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 22:01:45 +09:00
ketle-manandClaude Sonnet 5 9b3532704a refactor: models-tab.jsからバッジ機能をmodels/badges.jsへ切り出し
低凝集度モジュール分割計画(models-tab.js側)Phase 4の一部。バッジ
パレット管理・バッジHTML描画・バッジ編集モーダル・バッジフィルターバー
一式を切り出した。openBadgeEditModalはworkflow-tab.jsが直接importする
公開APIのため本体で再exportし、外部呼び出し側は無変更。renderModelGrid
(まだ本体に残るグリッド描画)への依存は循環importを避けるため
setGridChangeCallback()によるコールバック注入で解消した(2522→2300行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 21:55:08 +09:00
ketle-manandClaude Sonnet 5 f6f4b2c95c refactor: models-tab.jsから共有stateとルックアップ定数をmodels/state.jsへ切り出し
低凝集度モジュール分割計画(models-tab.js側)のPhase 3。共有state・
RESERVED_GROUPS/BATCH_MODEL_TYPES/STACK_MODEL_TYPES・FETCH_MAP/
TYPE_LABELS/GENUI_TYPE_MAPを切り出した。stateはconstのまま参照を
再代入せずプロパティ書き換えのみで運用し、ESM read-only制約を回避
している(2522→2446行)。以降のフェーズ(badges/filters/
selection-bulk/detail-panel/grid-view)はこのモジュールをimportして
共有する基盤。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 21:50:45 +09:00
ketle-manandClaude Sonnet 5 ac6a4f2dd6 chore: update graphify knowledge graph after image-edit-tab.js refactor
Incremental update covering the 6 new image-edit/*.js modules from the
cohesion refactor plus the 6 previously-hung docs (README.md, CLAUDE.md,
DEVLOG.md split into 4 parts, QWEN_CHAT_INTEGRATION_PLAN.md,
requirements.txt, publish.yml). 2094->2369 nodes, 5083->5401 edges,
112->148 communities.

Verifies the refactor's intent: each newly extracted module (GmicIntegration,
BlurTool, BgRemove, Sam3Segmentation, InpaintI2IActions, FileExport) now
forms its own high-cohesion community (density 0.18-0.48) versus the
original monolith's 0.07-0.08, while the intentionally-retained high-coupling
core (Mask Editor One dispatch, image loading) stays in one lower-cohesion
community as designed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 18:13:18 +09:00
ketle-manandClaude Sonnet 5 e6c503ef75 refactor: image-edit-tab.jsからファイル出力をFileExport.jsへ切り出し
低凝集度モジュール分割計画のPhase 2続き。レイヤー合成(maskApply対応の
書き出し用コンポジット)・PNG保存・Gallery保存・ComfyUIアップロードの
3出力先を独立クラスに切り出した。レイヤー初期化・画像ロード(_loadFile/
_loadFromDataUrl/_newCanvas)はキャンバス/Undoスタックと深く結合した
高結合コアのため本体に残置(2668→2577行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 17:25:36 +09:00
ketle-manandClaude Sonnet 5 e21f5596d8 refactor: image-edit-tab.jsからInpaint/I2I実行をInpaintI2IActions.jsへ切り出し
低凝集度モジュール分割計画のPhase 2続き。状態管理・ツールパネル描画・
Inpaint実行(ワークフロー合成/専用ワークフロー切替)・マスクなしI2I実行
一式を切り出した。gallery-tab.js(Comic Creator連携)から呼ばれる公開API
runInpaintExternal/runI2IExternalは本体に薄い委譲メソッドとして残し、
外部呼び出し側のインターフェースは変更していない(2932→2668行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 17:17:07 +09:00
ketle-manandClaude Sonnet 5 61bf2c83da refactor: image-edit-tab.jsからSAM3セグメンテーションをSam3Segmentation.jsへ切り出し
低凝集度モジュール分割計画のPhase 2続き。状態管理・可用性チェック・
セグメント実行/マスク適用ロジック・結果サムネイルパネルを独立クラスに
切り出した。マスクツールバー全体(Paint/Color/Alpha/Text/Vector/Shape/
SAM3共通の統合UI)は高結合コアとして本体に残し、SAM3固有部分のみ
インスタンス参照に置き換えている(3130→2932行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 17:11:06 +09:00
ketle-manandClaude Sonnet 5 d000219b86 refactor: image-edit-tab.jsから背景除去をBgRemove.jsへ切り出し
低凝集度モジュール分割計画のPhase 2続き。imgly(軽量ブラウザ内モデル)と
Mask Editor One連携のBiRefNetの2バックエンドを持つ背景除去一式(可用性
チェック・bgremoveツールパネルのrenderブロック・マスク合成処理)を独立
クラスとして切り出した(3304→3130行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 17:06:30 +09:00
ketle-manandClaude Sonnet 5 52ab637bd0 refactor: image-edit-tab.jsからBlur/MosaicツールをBlurTool.jsへ切り出し
低凝集度モジュール分割計画のPhase 2着手分。DrawTool等の既存ツールと同じ
onMouseDown/Move/Up/Leaveインターフェースを持つ独立クラスとして、全体/矩形
Blur・Mosaic機能一式を切り出した。ツール切替ディスパッチャーは委譲のみ残す
(3504→3304行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 17:01:01 +09:00
ketle-manandClaude Sonnet 5 5e5709594b refactor: image-edit-tab.jsからG'MIC統合をGmicIntegration.jsへ切り出し
低凝集度モジュール分割計画のPhase 1。G'MIC統合一式(状態管理・GUI起動/中断/
ジョブ待機/結果反映・filterツールのパネル描画)を独立クラスとして切り出し、
本体はコールバック注入するインスタンスを持つのみにした(3665→3504行)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 16:54:53 +09:00
ketle-manandClaude Sonnet 5 b20519c092 chore: track graphify knowledge graph, ignore cache/cost/local-path files
- Add graphify-out/{graph.json,graph.html,GRAPH_REPORT.md,manifest.json,
  .graphify_labels.json} so the knowledge graph is available to other
  clones and future sessions without rebuilding.
- .gitignore: exclude graphify-out/cost.json (churns every run),
  graphify-out/cache/ (local extraction cache, no cross-machine value),
  and graphify-out/.graphify_python|.graphify_root (machine-local paths).
- Add .claudeignore for graph.json/graphify-out/ so Claude Code's
  automatic context loading doesn't invalidate the prompt cache on
  every graphify rebuild.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 14:18:01 +09:00
ketle-manandClaude Sonnet 5 9d952c4b6e Add Unsloth backend to AI TOOL tab / sidebar A tab (v0.3.99)
Unsloth's OpenAI-compatible API always requires an API key, unlike the
existing Ollama/LM Studio/Lemonade backends, so it's relayed through a
new server-side proxy (py/routes/unsloth_routes.py) that attaches the
key from a local .env file (UNSLOTH_API_KEY, loaded via python-dotenv
in prestartup_script.py) instead of ever sending it to the browser.

Also fixes a client-controlled baseUrl SSRF that could have exfiltrated
the API key, and folds Unsloth's separate reasoning_content field back
into <think> tags so low Max tokens no longer produces silently empty
replies from reasoning models.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 17:51:13 +09:00
ketle-manandClaude Sonnet 5 8248ff8ac7 release: v0.3.98 — AI TOOL翻訳の信頼性向上 + GenerateUI Modelタブ再構成 + Thinking mode/Max tokens設定
- AI TOOL翻訳: system role対応chat APIへ切替、出力クリーニング、未翻訳検知を追加(SPA/サイドパネル両対応)
- GenerateUI Modelタブ: ワークフローに含まれる項目のハイライト色設定を追加、ControlNet/Hypernetworkを折りたたみ化、Text Encoderの表示順をControlNetより上に変更、LoRA列見出しもハイライト対応
- AI TOOL Settings: Thinking mode ON/OFF・Max tokens設定を追加(Ollama think/num_predict、LM Studio/Lemonade max_tokens、OFF時は<think>タグをクライアント側で除去)
- ヘルプタブ・README・DEVLOGを更新

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 12:27:06 +09:00
ketle-manandClaude Sonnet 5 7548c49910 release: v0.3.97 — KREA-2ワークフロー対応 + Labタブ拡張 + Input Imageプレースホルダ機能
- KREA-2ワークフロー対応で発覚した2件の既存バグ修正
  - PreviewAny/StringConcatenate中継ノードの役割伝播バグ修正
    (GenerateUI/Metadata/IタブでPositive Promptが空になる問題)
  - DynamicComboウィジェットのスロット数カウント修正
    (Prompt outputs failed validationエラーの原因)
- Labタブ: プロンプトモーダルにClear/Get from Previousボタン追加
- Labタブ: Setting 1(キーフレーム#1)の各列にGenerateUIの現在値をライブ反映
- Labタブ: 各キーフレームにBypassチェックボックス追加(Lab内部のみのスキップ機構)
- GenerateUI Input Imageサブパネル: Clear/Placeholderボタン追加
  (Color/Imageモード、設定は記憶、低頻度機能のため折りたたみ表示)
- ヘルプタブ更新(3点セット: index.html/app.js/i18n.js)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 12:39:42 +09:00
ketle-manandClaude Sonnet 5 e9f2c6171a release: v0.3.96 — カタログ作成バッチ無言停止バグ修正 + Style Catalog/Promptタブ機能拡張
- trackProgress()にタイムアウト・execution_interrupted処理・WebSocket切断時のPromise解放を追加し、
  バッチ処理が特定件数で無言のまま停止するバグを修正
- Style Catalogタブに「Load in GenerateUI」「Open in Metadata Tab」ボタンとダブルクリック拡大表示を追加
- Promptタブ右ペインにStyle管理サブタブ(Wildcardと切替式)を新設。作成・編集・削除、
  ファイル名バッジ表示、既存ファイルへの追加(+ Add to this file)に対応
- カタログ作成モーダルに「Outputフォルダにコピーを残さない」オプションを追加

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 13:47:03 +09:00
ketle-manandClaude Sonnet 5 90a7d824fb Merge feature/style-catalog-imageprompt: v0.3.95 ImagePrompt改名+Style_Catalogギャラリー
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 01:26:46 +09:00
ketle-manandClaude Sonnet 5 05e061d656 release: v0.3.95 — Style/Prompt→ImagePrompt改名 + Style_Catalogギャラリー + カタログ作成機能
Galleryタブが Output / ImagePrompt / Style_Catalog の3サブタブ構成に。既存の
Style/PromptサブタブをImagePromptへ全面改名(フォルダ ws_style_prompt→ws_image_prompt
含む)した上で、GenerateUIの登録済みStyleをプレビュー画像から視覚的に選べる
Style_Catalogサブタブを新設。

- Style_Catalogは ws_style_catalog フォルダ配下の画像を閲覧し、埋め込み
  Positive/Negativeプロンプトを表示・コピー、「Select as Style」で該当スタイルへ
  GenerateUIのStyleドロップダウンを切り替え可能
- GenerateUIに「カタログ作成」ボタンを追加。Batchタブ Styleサブタブで選択済みの
  スタイル(複数可)を対象に、現在のワークフローで順次生成しスタイル名で
  ws_style_catalog配下へ保存(既存のバッチ実行機構を再利用)。隣に「カタログ」
  ボタンでStyle_Catalogサブタブへワンクリック遷移
- Output Galleryの詳細パネルにプロンプト表示が出ていなかった不具合も併せて修正

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 01:26:41 +09:00
ketle-manandClaude Sonnet 5 5afdec395c Merge feature/style-prompt-gallery: v0.3.94 Style/Promptギャラリー追加
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 19:16:35 +09:00
ketle-manandClaude Sonnet 5 3ab4f3e36b release: v0.3.94 — Galleryタブをサブタブ化しStyle/Promptギャラリーを新設
Output(既存)/ Style-Prompt(新規)の2サブタブ構成に変更。Style/Promptギャラリーは
output/ws_style_prompt フォルダを対象に、フォルダツリー・サムネイル一覧・プロンプト
ビルダー(選択→追加→チップ→最終プロンプト→コピー)を提供する視覚的プロンプト作成UI。

- プロンプトは画像と同名の.txtサイドカーで保存(gallery_metadata.jsonには非依存)
- .txtサイドカーが無い画像は ponyxlWildcardsVault形式(*.yaml + thumbnails/)を
  その場で解決するフォールバックに対応(インポート不要でパックを直接閲覧可能)
- tools/import_style_prompt_seed.py でwildcardデータから画像+プロンプトの対を
  ポータブルな.txtサイドカー形式へ変換投入可能(画像データ自体はリポジトリに含めない)
- 親フォルダ選択時のサブフォルダ画像再帰表示、右ペイン2カラムレイアウトにも対応

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 19:16:22 +09:00
ketle-manandClaude Sonnet 5 4a23564c66 release: v0.3.93 — LabタブVAEキーフレーム不発バグ修正・Style適用UI刷新・Plan JSONスクロール同期修正・キーフレーム個別削除・Batch数超過警告・ワイルドカード対応・Save index imageデフォルトON
- VAE: 独立VAELoaderノードが無いワークフローでキーフレームが無警告で無視されるバグを修正(動的ノード注入+リンク張替え)
- Style: 上部Style有効化チェックボックスに依存しないApplyボタン+ON/OFFチェックボックス+別表示名に刷新
- Plan JSON: テキストエリアとハイライトオーバーレイのスクロール同期漏れを修正
- キーフレーム: 最後以外もモーダルから個別削除可能に
- Batch数: 既存キーフレームの回数目を下回った場合に警告トーストを追加
- ワイルドカード: Promptキーフレームでも__name__記法によるSPA側展開に対応
- Save index image to Output on Run のデフォルトをONに変更(+ Plan Clear時のチェックボックス表示ズレを修正)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 19:28:31 +09:00
ketle-manandClaude Sonnet 5 d8ffcc182e release: v0.3.92 — CLIPTextEncodeEditPlus実行結果不一致の根本修正(convertUiToApiのforceInput未考慮バグ)+LoRA Stack機能改善
- convertUiToApiの_getWidgetInputNames/_getWidgetInputTypesがforceInput:trueを考慮しておらず、未接続の任意入力(例: CLIPTextEncodeEditPlusのtext2)が誤ってwidgets_valuesの余分な値を吸収し送信データに紛れ込んでいたバグを修正。ComfyUI直接実行との生成結果不一致(text_edit欠落)の根本原因だった。
- Metadata/Infoタブ(metadata-tab.js, node_sets_menu.js)でCLIPTextEncodeEditPlusのtext1+text_edit+mode結合ロジック(RAW/EDIT/front/back)を追加し、ノード本体のPython実装と同じルールで正しくプロンプトを抽出するよう修正。
- GenerateUI ModelタブにLoRA Stack機能を3点改善: Use Stack Groupチェックボックス(ワークフロー実データ/Modelsタブ登録グループの切替)、複数LoRAの正しい表示、タブ切替でのON/OFF状態リセットバグ修正。
- ヘルプ(GenerateUIタブページ)にModel Tab — LoRA Detailsカードを追加(EN/JA/ZH)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:54:47 +09:00
ketle-man 3d5bc47e20 release: v0.3.91 — Labインデックス画像にPlanメタデータをPNG埋め込み、READMEのChangelogセクションを廃止
- Lab: インデックス画像PNG(Plan Save/Save As、Run時Output保存の両方)にPlan全体をiTXtチャンク(wfm_lab_plan)として埋め込み
- Lab: Planドロップエリアへの.pngドロップ時、埋め込みメタデータを直接読み込むように変更(対応する.jsonが無いOutputフォルダ側の画像でも読込可能に)。古い画像は従来通りサーバーへフォールバック
- README: 肥大化した Changelog セクションを削除。以後のバージョン履歴はGitHub Releaseのリリースノートで管理
- CLAUDE.md: リリース手順からREADME Changelog更新の手順を削除
2026-08-10 11:14:11 +09:00
ketle-man 35899bf53e release: v0.3.90 — Labタブにワークフロー呼び出し・T2I対応・Run時インデックス画像Output保存+Eagle連携・プラン接頭辞を追加、Galleryルート表示を簡略化
- Lab: Plan JSONに使用ワークフローファイル名を記録し、Open Workflowボタンで呼び戻し可能に
- Lab: T2I workflow (no source image) チェックボックスでソース画像なしのT2Iワークフローにも対応
- Lab: Save index image to Output on Run チェックボックスで、Run完了ごとにインデックス画像をComfyUIのOutputフォルダへ保存し、Eagle自動保存にも連携
- Lab: Plan Save/Save Asで保存されるファイル名にws_labplan_接頭辞を自動付与
- Gallery: フォルダツリーのルート表示を「[root] <フォルダ名>」から「[root]」に簡略化
- README: FeaturesセクションをタブごとにHTML detailsで折りたたみ化
2026-08-10 10:35:20 +09:00
ketle-manandClaude Sonnet 5 f356826e9f release: v0.3.89 — Lab機能への実動作確認フィードバック3件を反映(Get from Image・スタイル追記・Modelタブフィルター✕)
Get from ImageをLabタブに現在読み込まれている画像から取得する方式に変更。
スタイル適用を{prompt}置換ではなく常時カンマ区切り追記(既存プロンプトを消さない)に変更。
GenerateUI ModelタブのCheckpoint/VAE/Diffusion Model/ControlNet/Hypernetwork/Lora各フィルターに
オーバーレイ✕クリアボタンを追加。詳細はDEVLOG.md参照。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 19:43:33 +09:00
ketle-manandClaude Sonnet 5 003f3408fc release: v0.3.88 — Labタブ Promptセルにスタイル適用・画像/GenerateUIからのプロンプト取得、Checkpoint/VAEフィルター、Plan JSONサブタブを追加
Promptセル編集モーダルにGet from GenerateUI/Get from Image/Styleボタンを追加(Styleは
GenerateUIツールバー上部のチェックボックス・ドロップダウンをそのまま参照)。
Checkpoint/VAEセルにModelタブ同様のフィルター入力+オーバーレイ✕クリアボタンを追加。
Labに3つ目のサブタブ「Plan JSON」を新設し、現在のプランをJSONとして確認・編集・
Settingへ適用できるようにした。詳細はDEVLOG.md参照。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 19:16:19 +09:00
ketle-manandClaude Sonnet 5 54198f196a release: v0.3.87 — GenerateUIタブに実験的なLabサブタブ(I2Iバッチ生成・キーフレーム方式)を追加
Checkpoint/VAE/Prompt/KSamplerを列ごとに独立したキーフレームで管理し、任意の回数目から
値を変更できるI2Iバッチ生成機能。ワークフローを直接書き換えず、実行時にクローンして適用。
Plan Save/Save As/Load(json/pngドロップ対応)、画像連鎖生成、Eagle自動保存連携、
インデックス画像(最大9枚コンタクトシート)に対応。詳細はDEVLOG.md参照。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 12:15:07 +09:00
ketle-manandClaude Sonnet 5 3ff76aacc6 release: v0.3.86 — GalleryMetadataStoreの競合状態を修正
画像を一括生成(I2I)等でメタデータ保存リクエストが短時間に連発すると、バックグラウンドのフォルダインデックススレッドと競合し「dictionary changed size during iteration」が発生していた不具合を修正。GalleryMetadataStoreの共有辞書アクセスをthreading.RLockで保護した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 15:50:46 +09:00
ketle-manandClaude Sonnet 5 ec8be7ee6f release: v0.3.85 — Generate UI連携ブリッジ(T2I)にデフォルトワークフロー・Negative対応を追加
_wfmReceiveGenerateRequestをI2I/Inpaint連携と同じパターンに拡張し、
negative/workflowData/workflowFilename引数を追加。Comic Creator側の
「画像を一括生成」(T2I)からデフォルトワークフローを指定できるようになった。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 22:53:00 +09:00
ketle-manandClaude Sonnet 5 117fcad22a fix: release v0.3.84 — ernie_t2i.json実行時のPrompt outputs failed validationエラーを修正
サブグラフ境界のwidget値(width/height等)が複数の内部ノードへfan-outする場合
(EmptyFlux2LatentImage.widthへの実ウィジェット注入 + PreviewAny.sourceという
ウィジェット表現を持たないワイルドカード必須入力への中継)、_flattenSubgraphs
のinputPortTargetsが最後の1ターゲットで上書きされ、片方が未接続のまま
Required input is missing: sourceで検証エラーになっていた。全fan-outターゲット
を追跡し、非ウィジェット入力にはリテラル値を直接注入するよう修正。
実サーバーへの/promptバリデーションおよび実ブラウザでのGenerate実行で確認。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 21:45:00 +09:00
ketle-manandClaude Sonnet 5 feed327c23 release: v0.3.83 — Flux.2 Klein / Ernie Imageのプロンプト表示不具合を修正
CLIPTextEncode.textがPrimitiveStringMultilineへのリンク参照になっているパターン
(Metadata/IタブのresolveLinkedTextがvalueキー未対応)と、ComfySwitchNode経由で
生プロンプト/LLM強化版のどちらかに切り替わるErnie Imageのプロンプト強化トグル
(LLM出力は静的解析不能なため両分岐を辿りリテラルへ解決できる方を採用)に対応。
GenerateUIタブ・Metadataタブ・サイドパネルIタブの3箇所を修正し、実ブラウザで
3ワークフローすべての表示を確認。ヘルプにErnie Imageの表記を追加。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 19:31:37 +09:00
ketle-manandClaude Sonnet 5 82ddbb340c release: v0.3.82 — Metadata/Iタブのプロンプト抽出修正・widgets_values/Rerouteバグ修正
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:17:13 +09:00
ketle-manandClaude Sonnet 5 31f02962f5 ヘルプ・対応フォーマット表記にLongCat/Boogu/HiDream E1/FireRedを追加
GenerateUIタブ(helpGen2)・Metadataタブ(helpMetadata5)のヘルプ、および
Metadataタブ内の対応フォーマット補足(metaFormatTodo)を3言語(EN/JA/ZH)で更新。
併せてmetaFormatTodoにMage-Flowが未記載だった既存の抜けも修正。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:10:01 +09:00
ketle-manandClaude Sonnet 5 413f0d5068 MetadataタブとサイドパネルIタブのプロンプト抽出をImage Edit系ワークフローに対応
Flux.2 Klein/LongCat/Boogu/HiDream E1/FireRedをAI TOOL Chatで実際に生成し
Metadataタブで確認したところ、qwen_image_edit2509/2511・longcat・boogu・
Fireredの画像でプロンプトが表示されない問題が見つかった(同じ画像を
GenerateUIタブへ読み込むと正しく表示されるため、GenerateUIタブ側の解析
[comfyui-workflow.js]とは別に実装されているMetadataタブ独自のプロンプト
抽出ロジックが未対応と判明)。

原因は主に2点:
- PNGは`prompt`チャンク(既にAPI形式)が`workflow`チャンクより優先して読まれる
  ため、extractPromptsAPI()のtextMap構築が常に使われるが、そこは"text"/
  "text_g"キーのみを見ており、TextEncodeQwenImageEdit(Plus)/TextEncodeBooguEdit
  が使う"prompt"キーを見ていなかった。
- SamplerCustomAdvancedはpositive/negativeを直接持たずGuiderノード
  (CFGGuider/DualCFGGuider/BasicGuider)経由のため、サンプラーのinputsを
  直接見るだけのrole判定では見つけられなかった(Flux.2 Klein/HiDream E1)。

extractPromptsAPI/extractPromptsFromNodeSet双方に、guider経由のinputsを
辿る処理、TextEncodeBooguEditの1ノード2ロール(prompt/negative_prompt)対応、
InstructPixToPixConditioning経由の追加ホップ解決を追加。node_sets_menu.js
(ComfyUIサイドパネルのIタブ、web/comfyui/はESモジュールをimportできない
制約からcomfyui-workflow.js/metadata-tab.js相当のロジックを`_wfm`接頭辞で
ローカル複製している)にも同じ修正を適用。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:02:47 +09:00
ketle-manandClaude Sonnet 5 bec84ac30b release: v0.3.81 — Flux.2 Klein / LongCat / Boogu / HiDream E1 / FireRed Image Edit対応
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 17:40:33 +09:00
ketle-manandClaude Sonnet 5 61e16ddce0 Rerouteノードを透過解決し、hidream_comicの400エラーを修正
HiDream E1ワークフロー(hidream_comic.json)はUNETLoaderの出力をBasicScheduler/
DualCFGGuiderへ分岐させるのにRerouteノードを使っていた。RerouteはUI専用の
パススルーノードでComfyUIバックエンドにノードクラスが存在しないため、
そのままAPIペイロードに含めると「missing_node_type」の400エラーになっていた
(実機でGenerateして初めて発覚)。

_resolveBypassSource()をReroute対応に拡張し、Bypass(mode:4)ノードと同様に
配線を実際の入力元まで直結して解決するようにした。Rerouteの入力型は多くの場合
汎用型"*"のまま保存されており、Bypass用の型一致検索では見つからないため、
Reroute(常に1入力1出力)は型チェックを省略して唯一の入力を辿るようにしている。
convertUiToApi本体のノードループでもReroute自体をAPIペイロードから除外する。

実機でGenerate実行し、エラーなく生成できることを確認済み。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:39:48 +09:00
ketle-manandClaude Sonnet 5 174a67a569 LongCat/Boogu/HiDream E1/FireRedのImage Editワークフローに対応
GenerateUIタブでの検出範囲を拡張:
- TextEncodeQwenImageEdit(Plus無し版)とFluxGuidance/FluxKontextMultiReferenceLatentMethod
  のCOND_PASSTHROUGH対応(LongCat)
- SamplerCustomをSamplerCustomAdvancedと分離し、自身が持つpositive/negative/cfg/noise_seed
  を正しく読み取るよう修正、TextEncodeBooguEdit対応(Boogu)
- DualCFGGuiderをguiderとして認識(cfg_condsを代表cfg値に)、InstructPixToPixConditioning
  のrole伝播中継に対応(HiDream E1)
- KSamplerのseed/steps/cfg/denoiseがリンク(Primitive経由)で非数値の場合にundefined化

あわせて、サブグラフ展開処理(_flattenSubgraphs)のwidgets_valuesノーマライズを実機検証で
発見した既存バグごと全面的に書き直した。従来は「widgets_values長とwidget数が完全一致する
場合のみ」ノーマライズが働く設計で、seed直後のcontrol_after_generate拡張値('fixed'等)が
挟まるノード(KSampler等)では条件が成立せず、リンクされたwidget(steps/cfg等)のエントリが
除去されないまま後続処理に渡り、cfg/denoiseなどの値が一つずつズレて上書きされていた
(Flux.2 KleinのEmptyFlux2LatentImage、Firered_comicのKSamplerで実際に発生)。
_simulateWidgetValues/_stripLegacyLinkedWidgetValues/_findInjectedWidgetIndexとして、
legacy-full形式かどうかの自動判定・control_after_generate込みの正確な位置計算・注入を
共通ロジックに統一した。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 14:52:04 +09:00
ketle-manandClaude Sonnet 5 d903f33a31 Flux.2 Klein(Advanced Sampling)ワークフローのGenerateUI対応
CFGGuider+RandomNoise+SamplerCustomAdvanced+KSamplerSelect+Flux2Schedulerという
KSamplerを使わない構成を、SamplerCustomAdvanced起点でたどってSettingsパネルの
Seed/Steps/CFG/Sampler欄に集約表示・編集できるようにした。EmptyFlux2LatentImageも
Latentパネルに対応させ、GetImageSizeへリンクされたwidth/heightは空欄+説明表示にして
誤上書きを防止。CFGGuider/BasicGuiderをpositive/negative判定の起点にも追加。

あわせて実機検証で見つかった既存バグを修正:
- サブグラフ内部完結リンク(GetImageSize等)の扱いが誤り、widgets_values変換で
  batch_sizeが1024になったりCLIPTextEncodeのtextが消えたりしていた不具合
- 同ロールのプロンプトノードが複数ある場合にドロップダウン選択とテキスト内容が
  食い違う不具合
- ミュート/バイパスしたサブグラフの内部ノードにmodeが伝播せず、無効なパイプライン
  のノードがModelタブ等のデフォルト表示に紛れ込む不具合

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 15:55:55 +09:00
ketle-manandClaude Sonnet 5 afed788537 release: v0.3.80 — Comic Creator Semi-Automatic Manga bridges (LLM prompt draft + batch generate)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 14:14:55 +09:00
ketle-manandClaude Sonnet 5 ee020cb0c0 Comic Creator「半自動マンガ作成」向けブリッジを追加(LLMプロンプト下書き・バッチ画像生成)
gallery-tab.jsに_wfmReceiveLLMPromptRequest/_wfmReceiveGenerateRequestを追加。既存のI2I/Inpaint連携と同じ配置パターンで、Comic Creator側からiframe越しに呼び出される。ai-tab.jsのcallLLM/loadAiSettings/isValidBackendUrlをexportし再利用。

Comic Creator側からKaptureで実機E2E確認済み(Ollama接続でのLLM呼び出し、t2iワークフローでの画像生成とも成功)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 20:22:18 +09:00
ketle-manandClaude Sonnet 5 f6942eb7fa release: v0.3.79 — AI TOOL Chat Skills (SVG quality + Skill Creator), Help tab card redesign
- AI TOOL Chat: new "Skills" system — selectable .md system-prompt files
  (frontmatter name/description) managed in-pane via a wildcard-manager-style
  list+editor, backed by a new /api/wfm/skills service (skill_service.py /
  skill_routes.py). Bundled with two starter skills: "SVG Icon Generator"
  (explicit part-decomposition technique that fixes unrecognizable SVG output
  from smaller local models) and "Skill Creator" (a meta-skill that
  interviews the user and drafts a new skill as a ```skill block, with a
  one-click "Save as new skill" button).
- Help tab: every page now opens with a title + "Open tab" jump button and
  splits its content into topic-focused cards instead of one long bullet
  list, matching a pattern independently adopted by a sibling project that
  had in turn been modeled on this app's own help UI. Content itself is
  unchanged — only regrouped — except for fixing a duplicated Settings-pane
  bullet and filling in Tagger/Troubleshooting text that only existed in the
  i18n dictionary and never reached the page markup.
- Version bump bundles the previously-committed sidebar Info tab Mage-Flow/
  subgraph fix into this release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 12:02:46 +09:00
ketle-manandClaude Sonnet 5 bdfd0edb7a Sidebar Info tab: match Metadata tab's Mage-Flow/subgraph workflow support
node_sets_menu.js duplicated a convertUiToApi()-equivalent (subgraph
flattening with outer widget value injection, DynamicCombo support) and
added TextEncodeMageFlowEdit-specific prompt handling, mirroring the
v0.3.78 Metadata tab changes — web/comfyui/ can't import static/js/
modules (different serving URL), so the same local-duplication pattern
used for AI backend URL resolution was followed here too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 18:45:10 +09:00
ketle-manandClaude Sonnet 5 e1ffadbdca release: v0.3.78 — Eagle SVG support, subgraph workflow support (Mage-Flow), structured AI summary
- Eagle integration: SVG auto-save for both standard SaveSVGNode and comfyui-tosvg's Save SVG String node
- GenerateUI/Metadata tabs: subgraph-containing workflows (Mage-Flow etc.) no longer blocked as unsupported App format; outer subgraph widget values now correctly injected into internal nodes; DynamicCombo widget type support
- Workflow tab: AI summary now uses structured analysis instead of raw JSON truncation; accurate P/I/-> node counts and format badge for newer node types
- Workflow/Gallery tabs: Send to Canvas toast now distinguishes direct-load from drag-required fallback

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 14:29:24 +09:00
ketle-manandClaude Sonnet 5 65ef71b4ad release: v0.3.77 — Gallery SVG support, Chat SVG generation without a workflow
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 16:19:19 +09:00
ketle-manandClaude Sonnet 5 49019dfb05 DEVLOGに含まれるユーザー固有のパスを汎用表現に置き換え
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 13:41:28 +09:00
ketle-manandClaude Sonnet 5 78d9bba904 release: v0.3.76 — Comic Creator I2I execution bridge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 13:29:37 +09:00
ketle-manandClaude Sonnet 5 f8435ed770 release: v0.3.75 — Lemonade Server backend support, Bypass/Mute node fix
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 10:04:09 +09:00
203 changed files with 78403 additions and 8483 deletions
+60
View File
@@ -0,0 +1,60 @@
"""PostToolUse hook: regenerate README.index.md / DEVLOG.index.md right after
Claude Code edits README.md or DEVLOG.md, so graphify's excluded-doc index
stand-ins (see .graphifyignore) never drift from the source files within a
session. A git pre-commit hook covers edits made outside Claude Code.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
TARGETS = {"readme.md": "readme", "devlog.md": "devlog"}
def main() -> None:
try:
payload = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
return
if payload.get("tool_name") not in ("Edit", "Write"):
return
file_path = payload.get("tool_input", {}).get("file_path", "")
if not file_path:
return
name = Path(file_path).name.lower()
target = TARGETS.get(name)
if target is None:
return
project_dir = os.environ.get("CLAUDE_PROJECT_DIR") or str(Path(file_path).resolve().parent)
script = Path(project_dir) / "tools" / "generate_doc_index.py"
if not script.exists():
return
result = subprocess.run(
[sys.executable, str(script), target],
cwd=project_dir,
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
print(json.dumps({"systemMessage": f"[doc-index] {result.stdout.strip()}"}))
else:
print(
json.dumps(
{"systemMessage": f"[doc-index] generate_doc_index.py {target} failed: {result.stderr.strip()}"}
),
file=sys.stderr,
)
if __name__ == "__main__":
main()
+36
View File
@@ -0,0 +1,36 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Grep",
"hooks": [
{
"type": "command",
"command": "C:/Users/statsu-11/.local/bin/graphify.EXE hook-guard search"
}
]
},
{
"matcher": "Read|Glob",
"hooks": [
{
"type": "command",
"command": "C:/Users/statsu-11/.local/bin/graphify.EXE hook-guard read"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/on_doc_edit.py\"",
"timeout": 30
}
]
}
]
}
}
+5
View File
@@ -0,0 +1,5 @@
# graphify: keep graph.json/graphify-out/ out of Claude Code's automatic
# context so re-running `graphify extract` doesn't churn the prompt cache
# on every rebuild. These are still tracked by git (see .gitignore).
graph.json
graphify-out/
+29
View File
@@ -0,0 +1,29 @@
# ComfyUI-Workflow-Studio - Unsloth backend (AI TOOL tab / sidebar A tab)
#
# Unsloth Desktop requires an API key even for local access
# (Settings -> API in the Unsloth app, shown only once when issued).
#
# Alternatively leave the key empty and turn on Keyless API access ->
# "Chat and inference" in Unsloth Desktop's Settings -> API; requests are then
# sent without a key. That setting also serves your private LAN, and keep
# "Allow tools" OFF.
#
# 1. Copy this file to ".env" (same directory as this example file)
# 2. Paste your key after the "=" below (do NOT add a leading #, or the
# line becomes a comment and the key will not be loaded)
# 3. Restart ComfyUI
UNSLOTH_API_KEY=
# ComfyUI-Workflow-Studio - Gallery Semantic Search (Gemini Embedding 2)
# Get a key at https://aistudio.google.com (free tier available).
# Same steps as above: copy to ".env", paste the key, restart ComfyUI.
GEMINI_API_KEY=
# ComfyUI-Workflow-Studio - Partner API nodes in GenerateUI (Nano Banana, Kling, Seedream, ...)
# A Comfy API key issued at https://platform.comfy.org (separate from the Gemini key above;
# usage is billed as Comfy credits). With it set, GenerateUI queues prompts through the plugin
# server, which adds the key to the request only when the workflow contains a partner API node.
# Same steps as above: copy to ".env", paste the key, restart ComfyUI.
COMFY_API_KEY=
+1 -1
View File
@@ -17,6 +17,6 @@ jobs:
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
uses: Comfy-Org/publish-node-action@d2366e7abb6ab16f3bb03e3520ae25c8cf749bc9 # 1.0.1
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+7
View File
@@ -5,3 +5,10 @@ __pycache__/
.venv
*.egg-info/
data/
# graphify: local-only knowledge graph output, not shared with other clones
graphify-out/
# Working-notes plan docs — local scratch, not meant to ship with the repo
VIDEO_EDIT_TAB_PLAN.md
QWEN_CHAT_INTEGRATION_PLAN.md
+10
View File
@@ -0,0 +1,10 @@
# Private/unpublished planning notes — never committed to git, must not leak
# into the published graphify-out/ knowledge graph either.
QWEN_CHAT_INTEGRATION_PLAN.md
# Too large for reliable local-Ollama semantic extraction (large chunks make the
# model summarize in prose instead of emitting JSON, and time out on slower
# models/hardware). README.index.md / DEVLOG.index.md are condensed stand-ins
# that graphify extracts instead — regenerate them after editing the originals.
README.md
DEVLOG.md
+29 -4
View File
@@ -194,9 +194,34 @@ os.environ["HF_HOME"] = "D:\\hf_cache"
1. `pyproject.toml` の `version` を新バージョン番号に更新してステージングに含める
2. `README.md` のバージョンバッジを更新(`![Version](https://img.shields.io/badge/version-X.Y.Z-green)`)
3. `README.md` の Changelog に新バージョンのエントリを追加
4. `DEVLOG.md` の先頭に変更詳細エントリを追加
5. 上記すべてを同一コミットにまとめてプッシュ
6. `gh release create vX.Y.Z` でリリースを作成
3. `DEVLOG.md` の先頭に変更詳細エントリを追加
4. 上記すべてを同一コミットにまとめてプッシュ
5. `gh release create vX.Y.Z` でリリースを作成(リリースノート本文に変更点を記載する — v0.3.90でREADME.mdのChangelogセクションは廃止したため、以後のバージョン履歴はGitHub Releaseのリリースノートのみで管理する)
`pyproject.toml` のバージョン変更が GitHub Actions のトリガー(`paths: pyproject.toml`)になっており、ComfyUI Registry への自動公開もここで行われる。バージョン更新を忘れると Registry に反映されない。
<!-- graphify運用は一時停止中(2026-08-23〜、ユーザー指示により凍結)。再開時はこのコメントブロックを解除すること。graphify-out/ 自体は削除しておらずそのまま残っている。
## graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
### README.md / DEVLOG.md の索引ファイル
`README.md`/`DEVLOG.md`はサイズが大きすぎてローカルOllamaでの意味抽出が不安定になる(大きな塊を渡すとJSONではなく散文の要約を返す、または処理がタイムアウトする)ため、`.graphifyignore`でgraphifyの抽出対象から除外している。代わりに縮小版の`README.index.md`/`DEVLOG.index.md`を抽出対象にしている。
- 生成スクリプト: `python tools/generate_doc_index.py [readme|devlog|all]`
- 自動化: `README.md`/`DEVLOG.md`を編集すると以下の2つのフックが自動でindexを再生成する
- Claude Code PostToolUseフック(`.claude/hooks/on_doc_edit.py`)— Edit/Write直後に即時実行
- git pre-commitフック(`.git/hooks/pre-commit`)— コミット時にstagedファイルを検知して再生成し、同じコミットに含める(他エディタでの編集にも対応)
- 索引ファイルを手動で編集しない(次の生成で上書きされる)
- 索引ファイル再生成後は`graphify update .`を実行してグラフに反映する
-->
+216
View File
@@ -0,0 +1,216 @@
# DEVLOG Index — ComfyUI-Workflow-Studio
Condensed version-by-version index of DEVLOG.md (each line: version + one-line summary of what changed and why). Full rationale, code details, and "How to apply" lessons live in the excluded DEVLOG.md itself; this index exists so graphify can extract a queryable semantic node per release without feeding the full log to the local Ollama model. Regenerate with `python tools/generate_doc_index.py devlog` whenever DEVLOG.md changes.
- **v0.9.6(2026-11-12)** — マニュアル v0.9.6版(ja/en)
- **v0.9.5(2026-11-11)** — AI TOOL Chat: スキル v2(metadata・同梱スキルの版管理・プロンプトボタン)
- **v0.9.4(2026-11-07)** — Gallery: 3Dモデル対応(GLB / glTF / VRM / PLY / SPZ)
- **v0.9.3(2026-11-05)** — Image Edit: Drawツールのカラーパレット・ショートカット・スポイト(Comic Creator v1.67.0 から移植)
- **v0.9.2(2026-11-03)** — Selectorイベントループ有効時に画像が壊れて届く不具合を修正
- **v0.9.1(2026-11-03)** — TagSpace: 選んだタグだけを追加 / タグのカッコをエスケープして追加
- **v0.9.0(2026-11-03)** — マニュアル(画像なし配布版)を v0.9.0 に改訂
- **v0.8.14(2026-11-03)** — Fibo 2.0 Edit / FastH3 OmniRef / Pika-H3-Flex 対応(GenerateUI・Metadata・Videoタブ)
- **v0.8.13(2026-10-27)** — Edit in Mask Editor One: 送信画像をノードへ永続化(Mask Editor One v0.1.12 対応)
- **v0.8.12(2026-10-27)** — Comic Creator向け複数画像I2Iブリッジ + GenerateUI Inputタブ: Prompt/Image統合・Raw JSON折りたたみ
- **v0.8.11(2026-10-26)** — Gallery: Send CC をレイアウト/Imageタブの2ボタンに分割
- **v0.8.10(2026-10-23)** — Tagger: PixAI Tagger v1.0 対応
- **v0.8.9(2026-10-20)** — Video: アセット登録ボタン / Editの音声変換 / ソースパネルのレイアウト
- **v0.8.8(2026-10-19)** — MOSS-Image 対応(GenerateUI / Gallery / Metadata / Iタブ)
- **v0.8.7(2026-10-16)** — Qwen Image 2.1 Turbo(ComfyUI公式テンプレート T2I / I2I)対応
- **v0.8.6(2026-10-14)** — GenerateUI: パートナーAPIノード(Nano Banana 等)の実行に対応
- **v0.8.5(2026-10-14)** — Metadata / I タブ: Gemini 系ノード(Nano Banana)の画像を読めるように
- **v0.8.4(2026-10-14)** — ComfyUI v0.40.0 対応: ネイティブ LoRA スタックノード(`LoadLoraModel` / `LoadLoraTextEncoder`)
- **v0.8.3(2026-10-13)** — GenerateUI: シードモード「Workflow」を追加(改善候補メモ 2-A)
- **v0.8.2(2026-10-12)** — Gallery: セマンティック検索(Gemini Embedding 2 / EmbeddingGemma 2)
- **v0.8.1(2026-10-11)** — バグ修正
- **v0.8.0(2026-10-11)** — Gallery: 音声ファイル対応(wav / mp3 / ogg / m4a / flac / opus)
- **v0.7.14(2026-10-09)** — Comic Creator連携: nanobananaタブの生成履歴をGallery/Metadataタブに表示
- **v0.7.13(2026-10-06)** — VRAM調整(Ollama)— Comic Creatorから移植
- **v0.7.12(2026-10-04)** — セキュリティ: バックエンド接続先をブラウザから任意指定できた問題(SSRF)を修正
- **v0.7.11(2026-10-02)** — セキュリティ: CivitAI情報の説明文のHTML無害化(保存型XSS対策)
- **v0.7.10(2026-10-02)** — Windows: ComfyUIが固まる問題の原因特定と対策(Selectorイベントループ)
- **v0.7.9(2026-10-02)** — Models: CivitAIベースモデルからの自動バッジ作成
- **v0.7.8(2026-10-01)** — Video Edit: マルチトラック化(ビデオ/オーバーレイ/テキスト/オーディオ)
- **v0.7.7(2026-09-30)** — 意思決定モデル: Ollama 0.35+ バックエンド追加
- **v0.7.6(2026-09-30)** — Settings: 意思決定モデル (Laya) の専用設定
- **v0.7.5(2026-09-30)** — Ming Image ワークフロー対応(GenerateUI / Gallery・Metadata / サイドパネルIタブ)
- **v0.7.4(2026-09-29)** — Video Edit: BGM/音声トラック合成(Phase 4)— 複数クリップ結合時の音声消失も解消
- **v0.7.3(2026-09-24)** — GenerateUIタブにQwen Image 2.1(TextEncodeQwenImage21)対応を追加
- **v0.7.2(2026-09-18)** — Workflowタブにソート機能追加(Date/Name 昇順降順)
- **v0.7.1(2026-09-17)** — セキュリティ修正: Tagger VLM の Unsloth 経路でAPIキーがSSRFにより外部送信され得る不備を修正
- **v0.7.0(2026-09-17)** — プレビュー枠の高さ固定・AssetサブタブをPlan/Editと並ぶ中央下部へ移設・トリムパネルにClipchamp風スクラバー追加・Frame保存のVideo Assets登録とAsset→Plan画像連携
- **v0.6.3** — Send CCボタンでMP4動画をComic Creatorの新設「動画ツール」へ送れるように対応
- **v0.6.2** — Image Editタブにペンタブレット筆圧対応とFillツール(バケツ塗りつぶし)を追加
- **v0.6.1** — Galleryタブの検索が「読み込み中」のまま止まりクリアでも中断できない不具合、および50枚程度のフォルダでも数秒〜十数秒かかる不具合を修正
- **v0.6.0** — Gallery/Image Edit両タブにPSD(Photoshop)対応を追加 — 複数画像の一括PSD書き出し、レイヤー保存/読み込み、Gallery表示、Image Editのレイヤーパネル改善
- **v0.5.8** — Tagger DBタブに一括削除機能を追加(外部コントリビューターPRのレビュー・検証・修正・マージ)
- **v0.5.7** — 設定エクスポート/フルバックアップのドキュメント修正: v0.5.5のVideo Plan機能追加分の言及漏れ
- **v0.5.6** — バグ修正: WD Taggerで不正確なタグが生成される問題を修正(GitHub Issue #1)
- **v0.5.5** — Videoタブ全面刷新: Plan/Asset/Editバッチ生成 + Projectサブタブ + プレビュー2ペイン化 + Video Assets/Video Temp運用整理 + Galleryメタデータ消失バグ根本修正
- **v0.5.4** — 機能追加: Video/GenerateUIタブのLTX-2.5・Wan2.2対応拡大 + AI Assistant設定のUnsloth対応・独立モーダル化・モデルアンロード機能
- **v0.5.3** — バグ修正: Labタブのワークフロー切替時に前の内容が残留 + Galleryタブの読み込み高速化・root自動展開 + READMEスクリーンショット刷新
- **v0.5.2** — 機能追加: GenerateUI/Labタブの動画ワークフロー対応 + VideoタブにFrame抽出/GIF変換機能 + データ管理監査とZIPフルバックアップ + Send to LI node統一
- **v0.5.1** — 機能追加: mp4動画の埋め込みメタデータ対応(Gallery Prompt/JSON・Metadataサブタブ・サイドパネルIタブ)+ Gallery Infoタブに寸法/再生時間表示 + MiniMax H3等オールインワン動画ノードのプロンプト抽出バグ修正
- **v0.5.0** — 機能追加: mp4動画Gallery対応 + Videoタブ新設(MiniMax H3専用) + バグ修正: Galleryのタグ/メモが再起動後に消える
- **v0.4.8** — 機能追加: LabタブにPromptグループ一括適用+キーフレーム並べ替え + バグ修正: デフォルトワークフローの上書き保存が反映されない + Styleテーブルの「このファイルへ追加」対応
- **v0.4.7** — 機能追加: Prompt Tab内部リファクタ + Gallery Promptタブ新設 + Send to Canvas改善 + Presetsソート
- **v0.4.6** — 機能追加: MetadataタブをGalleryタブへ統合 + Promptタブに一覧編集可能なTable表示を新設
- **v0.4.5** — 調査: トップバー保存ワークフローのRMBG不具合報告(原因なし) + バグ修正: LabタブModel列の無意味な空キーフレーム表示を解消
- **v0.4.4** — 機能追加: Lab/GenerateUI ModelタブへLoRAバイパス機能 + AI TOOL Chatの画像生成パラメータ拡張
- **v0.4.3** — バグ修正: 共有モーダルのお気に入り☆/リネームがLabモーダルへ残留 + Lab Model列の+/-をCheckpoint/LoRA/VAEで完全独立化
- **v0.4.2** — 機能追加: Lab「Model」列統合(Checkpoint / LoRA Single / VAE)+ フィールド単位継承 + C/L/V表示切替
- **v0.4.1** — UX改善: Mask Editor One連携ボタンを「選択中ノード優先」方式に再設計
- **v0.4.0** — リファクタリング: image-edit-tab.js / models-tab.js の低凝集度モジュール分割
- **v0.3.99** — 機能追加: AI TOOLタブ + サイドパネルAタブにUnslothバックエンド追加(APIキーは.env管理、SSRF脆弱性修正、reasoning_content対応)
- **v0.3.98** — 機能追加・バグ修正: AI TOOL翻訳の信頼性向上 + GenerateUI Modelタブ再構成(ハイライト色・折りたたみ・並び替え)+ Thinking mode/Max tokens設定
- **v0.3.97** — 機能追加・バグ修正: KREA-2ワークフロー対応 + Labタブ拡張(Clear/Get from Previous/Bypass/ライブ反映)+ Input Imageプレースホルダ機能
- **v0.3.96** — バグ修正: カタログ作成バッチが特定件数で無言停止する問題の修正 + Style Catalog/Promptタブ機能拡張
- **v0.3.95** — 機能追加: Style/Prompt→ImagePrompt改名 + Style_Catalogギャラリー新設 + GenerateUI「カタログ作成」機能
- **v0.3.94** — 機能追加: Gallery タブのサブタブ化 + Style/Prompt ギャラリー新設(視覚的プロンプトビルダー)
- **v0.3.93** — バグ修正・機能改善: Labタブ VAEキーフレーム不発バグの修正+Style適用UI刷新+Plan JSONスクロール同期修正+キーフレーム個別削除+Batch数超過警告+ワイルドカード対応+Save index image デフォルトON
- **v0.3.92** — バグ修正: CLIPTextEncodeEditPlus使用ワークフローがGenerateUI経由だと直接実行と異なる画像になる問題(根本原因はconvertUiToApiのForceInput 未考慮バグ)+LoRA Stack機能の改善一式
- **v0.3.91** — 機能拡張: Labインデックス画像にPlanメタデータをPNG埋め込み+README構成整理(Changelogセクション廃止)
- **v0.3.90** — 機能拡張: Labタブの進化(ワークフロー呼び出し・T2I対応・Run時インデックス画像Output保存+Eagle連携・プラン接頭辞)+Galleryタブのルート表示簡略化
- **v0.3.89** — バグ修正: v0.3.88で追加したLab機能への実動作確認フィードバック3件を反映
- **v0.3.88** — 機能拡張: Labタブ Promptセルにスタイル適用・画像/GenerateUIからのプロンプト取得、Checkpoint/VAEフィルター、Plan JSONサブタブを追加
- **v0.3.87** — 追加機能: GenerateUIタブに実験的な「Lab」サブタブを追加(I2Iバッチ生成・キーフレーム方式)
- **v0.3.86** — バグ修正: 画像を一括生成(I2I)等の連続実行で "GalleryMetadataStore: save error: dictionary changed size during iteration" が発生
- **v0.3.85** — 機能拡張: Generate UI連携ブリッジ(T2I)にデフォルトワークフロー・Negativeプロンプト対応を追加
- **v0.3.84** — バグ修正: ernie_t2i.jsonをGenerateUIタブで実行すると "Prompt outputs failed validation" エラー
- **v0.3.83** — 追加機能: Ernie Image(ernie-image / ernie-image-turbo)ワークフロー対応 + プロンプトがリンク経由のノードから供給されるパターンの修正
- **v0.3.82** — バグ修正: MetadataタブとサイドパネルIタブでLongCat/Boogu/HiDream E1/FireRed等のプロンプトが表示されない不具合
- **v0.3.81** — 追加機能: GenerateUIタブでFlux.2 Klein / LongCat / Boogu / HiDream E1 / FireRedのImage Editワークフローに対応
- **v0.3.80** — 追加機能: Comic Creator「半自動マンガ作成」向けブリッジ(LLMプロンプト下書き・バッチ画像生成)
- **v0.3.79** — 追加機能: サイドパネル(Workflow Studio Library)IタブのMage-Flow等サブグラフワークフロー対応
- **v0.3.78** — 追加機能: Eagle自動保存のSVG対応
- **v0.3.76** — 追加機能: Comic Creator向けI2I実行ブリッジ`_wfmReceiveI2IRunRequest`
- **v0.3.77** — 追加機能: GalleryタブでSVGファイルの表示・管理に対応
- **v0.3.75** — Lemonadeバックエンド追加(3つ目のAIバックエンド)
- **v0.3.74** — AI TOOL Chatペインの画像添付対応 + I2I連携
- **v0.3.73** — AI TOOL Chatペインからの対話式画像生成(Tool Calling)+ Checkpoint関連の安全策
- **v0.3.72** — Comic Creator連携用の外部Inpaintエントリポイントを追加
- **v0.3.71** — Inpaint機能の追加(GenerateUI Imageタブ + Image Edit「Inpaint」タブ)
- **v0.3.70** — Workflow AI要約: Ollama 404エラー修正
- **v0.3.69** — Gallery → ComfyUI Comic Creator 連携(Send CC ボタン)
- **v0.3.68** — MASK EDITOR ONE セクション常時表示化
- **v0.3.67** — Alt+Click Apply & Generate
- **計画: Image Edit 拡張 — ABRブラシ連携 + Mask Editor One 統合** — フェーズ1: Draw ツール — ABRブラシ対応
- **2026-06-29: v0.3.66 — Mask ツール 5サブツール追加(Color / Alpha / Text / Vector / Shape)・Support リンク追加** — 概要
- **2026-06-29: v0.3.65 — Mask Editor One 連携修正・トップバーアイコン修正・Mask ABRブラシ対応** — 概要
- **2026-06-28: v0.3.64 — Image Edit マスク Add/Subtract・BiRefNet BG Remove・SAM3 セグメンテーション・ブラシカーソルバグ修正** — 概要
- **2026-06-28: v0.3.63 — ノードタブ複数選択・全タブフィルタークリア・検索Xボタン** — 概要
- **2026-06-27: WFS_PromptText の GenerateUI 対応・ドロップ時 title 固定** — 概要
- **2026-06-27: v0.3.62 — Send GenUI Image ボタン追加・tagger_settings.json バグ修正・README 更新** — 概要
- **2026-06-27: データ管理バグ修正 — tagger_settings.json をエクスポート対象に追加** — 概要
- **2026-06-27: v0.3.61 — G'MIC バグ修正・セキュリティ・Draw/Mask ブラシ操作性改善** — 概要
- **2026-06-27: v0.3.60 — Image Edit Filter Tool (G'MIC-Qt 連携) 実装** — 概要
- **2026-06-27: v0.3.59 — Image Edit Mask Tool 実装・Draw/Mask 描画バグ修正・クリッピング保存対応** — 概要
- **2026-06-27: v0.3.58 — GenerateUI Model タブ GGUF対応拡張(Diffusion Model / Text Encoder)** — 概要
- **2026-06-26: v0.3.57 — Image Edit Blur Tool / BG Remove Tool 追加** — 概要
- **2026-06-26: v0.3.56 — Style機能追加(GenerateUI ツールバー & Batchタブ)** — 概要
- **2026-06-25: v0.3.55 — グループ機能バグ修正(全タブ)** — 根本原因(データ消失): パス区切り文字の不一致
- **2026-06-25: v0.3.54 — Image Edit Shape ツール追加・Models タブ overlay バグ修正** — Image Edit Tab — Shape ツール(□ / S キー)
- **2026-06-25: v0.3.53 — Image Edit Tab 実装・レイヤーロック・ギャラリー保存** — Image Edit Tab(新規タブ)
- **2026-06-25: GenUI設定タブ LATENT IMAGE にプリセット機能を追加** — 変更内容
- **2026-06-24: GenUI ModelでLoRA適用後、Modelサブタブ再表示時に値がリセットされるバグ修正** — 問題
- **2026-06-24: v0.3.51 — Embedding GenUI PP/NP対応 + GenerateUIプロンプトタブEmbeddingセレクタ追加** — 変更内容
- **2026-06-24: v0.3.51 — GenerateUIタブ Hypernetwork対応 + ModelsタブGenUI Modelボタン追加** — 変更内容
- **2026-06-24: v0.3.51 — Hypernetwork/Embeddingモデル未表示バグ修正** — 原因
- **2026-06-24: v0.3.51 — ModelsタブサイドパネルにGenUI Modelボタン追加** — 変更内容
- **2026-06-22: v0.3.50 — ギャラリータブ プロンプト検索負荷改善(バックグラウンドインデックス)** — 問題
- **2026-06-22: v0.3.49 — 生成UIタブ A1111風ワイルドカード展開・ギャラリータブ バグ修正2件** — 生成UIタブ — A1111風SPAサイドワイルドカード展開
- **2026-06-22: v0.3.48 — ギャラリータブ 画像ダウンロード・一括エクスポート機能追加** — 詳細パネルの画像プレビューにダウンロード機能を追加
- **2026-06-22: v0.3.47 — モデルタブ ファイル移動後グループ状態修正** — モデル移動後のグループ状態不整合を修正
- **2026-06-21: v0.3.46 — プロンプトタブ AI Assistant LM Studio対応・設定タブ AI Assistant設定に変更** — 設定タブ「AI Assistant設定(プロンプトタブ)」
- **2026-06-21: v0.3.45 — グループ孤立エントリ自動クリーンアップ・Galleryタブ Shift+クリック範囲選択** — グループ孤立エントリ自動クリーンアップ
- **2026-06-21: v0.3.44 — Galleryタブ高速化・比較モード・バルク強化・バルクバーUI整理** — フェーズ2-A: サーバーサイドサムネイル生成
- **2026-06-21: ヘルプタブ改善 — Gallery mode 多言語対応・フォント拡大・検索機能追加** — Gallery mode の多言語対応
- **2026-06-21: Send to Canvas — window.opener経由のキャンバス直接ロード対応**
- **2026-06-20: API形式ワークフローのキャンバス読み込み修正・画像メタデータ埋め込み修正** — API形式ワークフローをキャンバスへドラッグすると空になる問題を修正
- **2026-06-20: セキュリティ修正(XSS・パスバリデーション)** — 修正内容
- **2026-06-20: WFS_GalleryFeeder — ComfyUI キャンバス上コントロール追加** — 新規ファイル(`web/comfyui/gallery_feeder_extension.js`)
- **2026-06-20: __Feeder__ グループ保護 + サムネイル F ボタン + seed バグ修正** — `__Feeder__` グループの予約済み保護
- **2026-06-20: Gallery Feeder 機能追加(WFS_GalleryFeeder ノード)** — 概要
- **2026-06-19: Generate UI — ImpactWildcardEncode/Processor プロンプト検出バグ修正(v0.3.41)** — 修正内容(`static/js/comfyui-workflow.js`)
- **2026-06-19: AI TOOL タブ Chat ペイン・ワイルドカード生成追加(v0.3.40)** — Chat ペイン(`templates/index.html`, `static/css/main.css`, `static/js/ai-tab.js`, `web/comfyui/node_sets_menu.js`)
- **2026-06-18: Send to Canvas機能追加(ワークフロータブ・ギャラリータブ→LibraryタイトルDnD)** — ワークフロータブ — Send to Canvas(`static/js/workflow-tab.js`, `static/js/app.js`, `templates/index.html`)
- **2026-06-15: TaggerシングルGenUI:Pボタン追加+生成UIのZITワークフロープロンプト解析修正** — TaggerシングルGenUI:Pボタン(`templates/index.html`, `static/js/tagger-tab.js`, `static/js/i18n.js`)
- **2026-06-15: Taggerバッチ.txt出力追加+Galleryタグ保存バグ修正** — Taggerバッチ.txt出力(`templates/index.html`, `static/js/tagger-tab.js`, `static/js/i18n.js`, `py/routes/tagger_routes.py`, `py/services/tagger_service.py`)
- **2026-06-15: Taggerタブ新規実装+ギャラリー詳細パネルUI改善+パストラバーサル修正** — Taggerタブ新規実装
- **2026-06-15: 全選択ボタン追加+ギャラリーバルクバーi18n対応** — 変更内容
- **2026-06-10: v0.3.36 — コードレビュー修正+リファクタリング+README刷新** — コードレビュー修正(v0.3.35の不具合10件)
- **2026-06-10: v0.3.35 — テーブルソート・Load GenUI・ワークフロー保存** — 変更内容
- **2026-06-09: v0.3.34 — Modelsタブ テーブルビュー強化・バルク操作改善** — 変更内容
- **2026-06-09: v0.3.33 — LoRAペイン・ワークフロー解析バグ修正** — 変更内容
- **2026-06-08: v0.3.32 — Lora Loader (LoraManager) LoRA検出対応・ライブラリ機能拡張** — 変更内容
- **2026-06-08: v0.3.31 — LoRAペイン Single/Stack タブ分割・GenUI Model LoRA対応強化** — 変更内容
- **2026-06-08: v0.3.30 — LoRAスタック トリガーワードバグ修正・ヘルプ更新** — 変更内容
- **2026-06-08: v0.3.29 — LoRAセクション統合・ヘルプタブサイドバー・UI細部改善** — 変更内容
- **2026-06-08: 生成UIタブ 複数改善 — LoRA Stackトリガーワード連携・Reset Workflowボタン・Inputタブタブ化** — 変更内容
- **2026-06-08: Feederタブ UI改善 — (root)自動選択・PREVIEWペイン右移動・RUN中プレビュー反映** — 変更内容
- **2026-06-07: v0.3.28 — Batchタブ Sampler/Scheduler対応 + Modelsサムネイルバグ修正** — 概要
- **2026-06-07: v0.3.27 — model-and-prompt-from-metadata カスタムノード対応 + README修正** — 概要
- **2026-06-07: v0.3.26 — Stack LORA SYNTAX 表示修正 + 全体強度調整 UI** — 概要
- **2026-06-07: v0.3.25 — Lora Loader (LoraManager) 対応 + Stack 有効無効切り替え** — 概要
- **2026-06-06: v0.3.24 — Lora Stack 登録機能 + 生成UI Model タブ 3ペイン化** — 概要
- **2026-06-05: v0.3.23 — バグ修正2件** — バグ修正
- **2026-06-05: v0.3.22 — カードビュー廃止 + Batch タイプ切り替え UI + 4タイプバッチ生成** — 概要
- **2026-06-05: (未リリース) — 生成UIタブ Batch 4タイプ対応(Lora/Prompt/Workflow グループ選択 + BATCH QUEUE 4列化)** — 概要
- **2026-06-05: (未リリース) — Prompt・Workflow Batchグループ登録UI** — 概要
- **2026-06-04: (未リリース) — モデルBatchグループ登録UI(サムネイル/カード/テーブル・フィルター・一括解除)** — 概要
- **2026-06-04: (未リリース) — Batch予約グループ・Presets Save後リセット・グループフィルター種別絞り込み** — 概要
- **2026-05-31: v0.3.21 — CivitAI ホスト設定・Sample サブタブ・URL 修正・ヘルプ i18n 整備** — 概要
- **2026-05-31: v0.3.20 — CivitAI 詳細パネル強化(Type / Hash 表示・URL 修正・画像別タブ)** — 概要
- **2026-05-31: v0.3.19 — CivitAI 連携強化(バッチ高速化・リトライ・APIキー・フィールド拡充)** — 概要
- **2026-05-31: v0.3.18 — Batch タブ追加・Models 複数選択強化** — 概要
- **2026-05-31: v0.3.17 patch — SSLコンテキスト セキュリティ強化** — 概要
- **2026-05-30: v0.3.17 — SSL証明書エラー修正・Lora Manager対応** — 概要
- **2026-05-20: v0.3.16 — CivitAIプレビューフォールバック表示** — 概要
- **2026-05-20: v0.3.15 — サンプルワークフロー同梱・CivitAI自動プレビュー・Create Tagsオプション** — 概要
- **2026-05-20: AI TOOL タブ 英語化(SPA・Libraryパネル)** — 概要
- **2026-05-19: v0.3.14 — AI TOOL タブ 3ペイン化・RAW JSON 文字色カスタマイズ** — 概要
- **2026-05-19: v0.3.13 — AI タブ(A)追加** — 概要
- **2026-05-18: v0.3.12 リリース — README スクリーンショット刷新 / workflow_analyzer 拡張** — 概要
- **2026-05-17: RAW JSON パネルに検索機能を追加** — 概要
- **2026-05-17: v0.3.11 Metadata タブ対応ノードタイプ拡張 / Settings テキストサイズ UI 追加** — 概要
- **2026-05-17: v0.3.10 Information タブ キャンバスドラッグ対応 / ノードタイプ拡張 / バグ修正** — 概要
- **2026-05-16: v0.3.9 サイドパネル I タブ追加 / トップバーアイコン修正** — 概要
- **2026-05-16: v0.3.8 Metadata タブ: data9 対応 / プロンプト判別不能テキスト** — 概要
- **2026-05-15: v0.3.7 Metadata タブ: Flux2 / Qwen / Z-Image 対応** — 概要
- **2026-05-15: v0.3.6 Metadata タブ追加** — 概要
- **2026-05-09: v0.3.5 GenerateUI — Feeder サブタブ・ワークフロー解析精度向上** — 概要
- **2026-04-29: v0.3.4 GenerateUI — Checkpoint Batch 刷新・Settings 横並びレイアウト・ヘルプ i18n 修正** — 概要
- **2026-04-28: v0.3.3 Promptタブ — ワイルドカード支援パネル・Impact Packシンボリックリンク連携・サブディレクトリ対応・GenerateUI修正** — 概要
- **2026-04-28: v0.3.2 生成UIタブ — Checkpoint Batch・Seed UIレイアウト修正** — 概要
- **2026-04-27: v0.3.1 ギャラリー拡張(フォルダ/ファイル操作・ワークフロー保存)** — 概要
- **2026-04-18: v0.3.0 モデルグループ表示改善(全タイプ横断・サイドパネル修正)** — 概要
- **2026-04-18: v0.2.9 モデル有効/無効・複数選択・一括削除** — 概要
- **2026-04-14: v0.2.8 データ保存先変更・エクスポート/インポート機能追加** — 概要
- **2026-04-06: v0.2.6 UI改善・バッジ保存修正・フォルダフィルター追加** — 概要
- **2026-04-04: v0.2.5 追加修正** — 概要
- **2026-04-03: v0.2.5 サイドパネル Category/Package サブタブ・テーマ設定・Workflow修正** — 概要
- **2026-03-29: v0.2.4 GenerateUIタブ レイアウト再設計** — 概要
- **2026-03-29: v0.2.3 バッジ統一・GenUI Model・サイドパネルModels拡充** — 概要
- **2026-03-28: v0.2.2 Modelsタブ・CivitAI連携** — 概要
- **2026-03-27: テーマカスタマイズ機能・設定タブ改善** — 概要
- **2026-03-26: トップバーボタンアイコン修正・Appモードバッジ調査** — 概要
- **2026-03-25: v0.2.0 プロンプトプリセット機能・Workflow Studio Library** — 概要
- **2026-03-24: v0.1.9 サイドパネルUI改善・保存ダイアログ・バグ修正** — 概要
- **2026-03-24: v0.1.8 WF & Node Library サイドパネル拡張** — 概要
- **2026-03-23: v0.1.7 Nodes タブ追加** — 概要
- **2026-03-23: v0.1.6 パストラバーサル脆弱性修正** — 概要
- **2026-03-22: v0.1.5 テーマシステム追加** — 概要
- **2026-03-22: v0.1.4 App形式対応・プリセットコピー・分析バグ修正** — 概要
- **2026-03-21: v0.1.3 ヘルプ&サポートタブ追加** — 概要
- **2026-03-19: v0.1.2 リリース** — 概要
- **2026-03-19: v0.1.1 リリース** — 概要
- **2026-03-18: プロジェクトリネーム & アイコン適用** — 概要
- **2026-03-18: デフォルトビュー設定追加** — 概要
- **2026-03-18: MarkdownNote表示専用ノード除外** — 概要
- **2026-03-18: サブグラフワークフロー対応 & 変換精度修正** — 概要
- **2026-03-18: Raw JSON同期修正** — 概要
- **プロジェクト構成 (現在)**
- **2026-03-18: JSONシンタックスハイライト** — 概要
- **2026-03-18: プロンプトタブ 中国語↔英語翻訳追加** — 概要
- **2026-03-18: v0.1.0 リリース** — 概要
- **2026-04-13: Gallery タブ追加 & 全タブ Group UI 統一** — 概要
+2914 -154
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ketle-man
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+285
View File
@@ -0,0 +1,285 @@
# README Index — ComfyUI-Workflow-Studio
Condensed index of README.md: intro summary, feature names per tab (full descriptions live in the excluded README.md), and the installation/usage/requirements/project-structure sections verbatim. Exists so graphify can extract this without feeding the full README (with its screenshot table) to the local Ollama model. Regenerate with `python tools/generate_doc_index.py readme` whenever README.md changes.
# ComfyUI-Workflow-Studio
A comprehensive workflow, asset management, and generation UI plugin for [ComfyUI](https://github.com/comfyanonymous/ComfyUI).
**📁 Management** — organize all your assets in one studio
- Workflow, model, image, and prompt management
- AI-powered prompt writing assistance, translation, and tag generation
- File drop & Gallery-linked metadata viewing — inspect and reuse generation settings instantly
- Semantic image search in the Gallery — find images by describing them, or list images similar to a selected one
**⚡ GenerateUI** — a productivity-focused generation tab connected to every other tab
- Batch generation across models, samplers, prompts, and workflows
- Image Feeder for continuous folder-based generation
- Lab: experimental I2I batch generation with independent per-iteration keyframes for Model (Checkpoint/LoRA Single/VAE)/Prompt/KSampler
**📚 Workflow Studio Library** — a multi-function side panel for smooth ComfyUI integration
- Drag & drop models, nodes, prompts, and workflows straight onto the canvas
- **Send to Canvas**: click "Send to Canvas" (Workflow tab) or "Copy & Send Canvas" (Gallery tab) to send the workflow directly to the ComfyUI canvas — UI and API formats both supported
- View metadata from images / JSON, then drop the detected models and prompts onto the canvas
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.9.6-green)
## Features (tab -> feature names)
### Workflow Tab
Thumbnail / Table views, Thumbnail side panel, Badge filtering, Search, Side panel tabs, Badge management, AI summary, Import / Export, Side panel toolbar buttons, Default view setting, Search clear (✕), Clear all filters (✕ Clear), Sort, GenerateUI Check
### Canvas Snapshot (v0.1.2)
One-click capture, Auto-save as thumbnail, Embedded workflow metadata, Auto-import
### GenerateUI Tab (v0.3.5)
6-tab layout, Save button, Input tab, Multi-image I2I for Comic Creator, Model tab, Settings tab, Always-visible Raw JSON, Bypass/Mute node handling, One-click generation, Seed control, Style selector, Create Catalog / Catalog buttons, Batch type selector, Batch tab, UI-to-API conversion, Video workflow support, Eagle integration, Qwen Image 2.1 support, Ming Image support, P-H3 Image support, MiniMax-H3-Image support
### Feeder subtab (v0.3.5 / v0.3.42)
ImageFeeder node control, Image library, Selection management, Presets, Continuous Run loop, Index sync, Seed, WFS_GalleryFeeder node, Node & group selector, Image grid, After Gen modes, Run / Stop controls
### Lab subtab (v0.3.87 – v0.9.5)
Setting / Results / Plan JSON sub-panels, Per-column keyframes, Model column — Checkpoint / LoRA Single / VAE, Empty state when no matching node exists, Wildcards, Prompt cell extras, Keyframe #1 live-reflect, Bypass checkbox, LoRA node bypass & prompt injection, T2I workflow support, Use generated image for next, Workflow recall, Plan files, Plan Load, Plan JSON tab, Save index image to Output on Run, Index images are self-contained plan files, Eagle integration, Select vs. edit, and keyframe reordering, PG — apply a Prompt tab preset group, Video workflow support, Use generated image for next, now video-aware, Plans from a story
### Prompt Tab
3-column layout, AI chat assistant, Image attachment, Translation, Prompt presets, Preset Manager, Group management, Clipboard copy, GenUI Set, Wildcard/Style tab bar, Wildcard input toolbar, Wildcard file manager, Style manager, Form / Table toggle, Table view — Presets / Presets Group / Wildcards / Style
### Settings Tab
2-column layout, Collapsible sections, Theme selection, Theme customization, Workflows directory, Gallery output directory, Eagle connection, CivitAI Host, CivitAI API Key, Default workflow, Default Checkpoint, VRAM Management (Ollama), Video Playback Volume, Data Management, Text Size, RAW JSON Colors, GenerateUI Model Tab Highlight, Wildcard Integration, G'MIC-Qt Integration, Language
### Gallery Tab (v0.3.44)
Output / ImagePrompt / Style_Catalog / Metadata sub-tabs, Image browser, Folder tree root label, Thumbnail / Table views, Folder management, File operations, Download, MP4 video support, MP4 embedded metadata, WebM support, Embedded metadata reading fixes, Audio support, File conversion, Move To folder filter, Semantic Search, Audio waveform colors, Info tab: dimensions & duration, Multi-select, Image Compare, Prompt search, Server-side filtering, Group management, Thumbnail F button, Favorites, Detail panel, Prompt tab, Nanobanana tab, Gemini / Nano Banana nodes, MOSS-Image in GenerateUI, Faster GenerateUI Check / conversion, Partner API nodes in GenerateUI, Workflow viewer, GenUI button, Image Edit button, Send GenUI Image button, Send to LI node button, Send to LV node button, Send CC Layout / Send CC Image buttons, Search clear (✕), Clear all filters (✕ Clear), Workflow auto-save, Output folder configurable, SVG file support, PSD file support, 3D model support, Performance
### ImagePrompt Gallery subtab (v0.3.94, renamed from Style/Prompt in v0.3.95)
Visual prompt library, 3-column layout, Prompt builder (right pane), Plain-text prompt storage, ponyxlWildcardsVault format support, Seed data importer
### Style_Catalog Gallery subtab (v0.3.95)
Visual Style picker, 3-column layout, Positive/Negative panel, Select as Style button, Load in GenerateUI / Open in Metadata Tab buttons, Double-click to enlarge
### Metadata Gallery subtab (v0.3.8, moved from a top-level tab in v0.4.6)
Now inside Gallery, 3-column layout, File drop, MP4 embedded metadata, Settings section, Nanobanana section, Model extraction, LoRA extraction, Prompt extraction, Prompt actions, Format support, Format note
### Nodes Tab (v0.1.7)
Node Browser, Search & Filter, Package badges, Node detail panel, Node Sets, Right-click context menu, Multi-select, Search clear (✕), Clear all filters (✕ Clear)
### Models Tab (v0.2.3)
Model Browser, Thumbnail / Table views, Table column sort, Search & Filter, User-defined badges, Side panel tabs, CivitAI integration, CivitAI panel — Info / Sample sub-tabs, CivitAI panel states, Batch CivitAI fetch, Detail modal, GenUI Model button, Group management, Table view memo, Preview images, Enable / Disable models, Multi-select & bulk operations, Search clear (✕), Clear all filters (✕ Clear)
### Tagger Tab (v0.3.38)
3 sub-tabs, Model support, Threshold sliders, VLM tagging, Single tab, Single output options, Batch tab, DB tab, Dependencies
### TagSpace Tab (v0.9.0)
Choose tags visually in a 3D latent space, Build / Update, Choosing tags, Explore, Search with a card's or Gallery image's tags, Make a prompt, Tagger fill, Glow / Twinkle, Hover preview, Requirements
### Image Edit Tab (v0.3.65)
Layer-based image editor, Loading images, Open PSD button, Close button, New button, Tools, Layer panel, Duplicate layer (⧉), Layer lock, Text quality, Export, Canvas navigation, Undo / Redo
### Video Tab (v0.5.0 – v0.7.4)
Dedicated video generation UI, independent of GenerateUI, Plan subtab, Base settings row, Edit subtab, Edit: Multi-track, Edit: Audio / BGM, Edit: Text overlays, Edit: Crop, Edit: Convert audio, Edit: Undo / Redo, Edit project persistence, Asset subtab, Project subtab, Two center previews, Register as Asset, First Frame / Last Frame are both optional, Semantic node lookup, not hardcoded IDs, Field highlight color, Load in Video, Frame tab, GIF tab, Playback volume
### AI TOOL Tab (v0.3.14 – v0.9.5)
4-pane layout, Translation pane, Chat pane, Chat pane — image generation via Tool Calling, Chat pane — generate_image extra parameters, TOOLS pane (VLM), TOOLS pane — shared Chat attachment, TOOLS pane (Wildcards), Chat pane — image-to-image (I2I), Chat pane — SVG generation, no ComfyUI workflow involved, Chat pane — Skills, Chat pane — Skills v2, Built-in skills, Chat pane — Image check & refine loop, Chat pane — Workflow Doctor, Chat pane — Storyboard to Lab, Chat pane — Agent Skills folders, TOOLS pane — OCR, Collapsible Translation / Settings panes, Settings pane, Unsloth backend, Decision API client, Settings pane — Chat Image Generation, Settings pane — Chat I2I Generation, Settings pane — Generation, Model unload, Settings shared, Backend support, URL security, Lemonade's other endpoints (not integrated)
### Workflow Studio Library (ComfyUI Side Panel) (v0.3.9)
Tab layout (W / N / P / M / I / A), W — Workflows tab, N — Nodes tab, M — Models tab, P — Prompts tab, I — Information tab, A — AI TOOL tab, Drag & drop workflows, Drag & drop nodes, Drag & drop prompts, Send to Canvas, Copy prompts, Double-click, Search, ⚙ Theme settings
### Help &amp; Support Tab (v0.1.3)
Sidebar navigation, Support, Feature list, Keyboard Shortcuts
## Installation
### Via ComfyUI Manager (Recommended)
Search for **Workflow Studio** in ComfyUI Manager and install.
### Manual Installation
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/ketle-man/ComfyUI-Workflow-Studio.git
```
Restart ComfyUI after installation.
---
## Sample Workflows
Sample workflows are included in the `workflows/` folder. You can open them directly in ComfyUI via drag & drop, or load them from the Workflow tab.
Samples for the Feeder tab: `ws_sdxl_image_feeder_i2i` / `ws_dwpose_image_feeder` (Image Loop mode, require the `comfyui-image-feeder` custom node) and `ws_sdxl_gallery_feeder_i2i` (v0.8.3, SDXL I2I with the bundled **WFS_GalleryFeeder** node — no extra custom node needed; put images in the `__Feeder__` Gallery group and use the Feeder tab's Gallery mode).
> **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.
---
## Manual
A beginner-oriented manual (covers v0.9.6, text-only edition without screenshots) is available in [`docs/manual/`](docs/manual/):
- English: [`docs/manual/en/`](docs/manual/en/) — ZIP: `Workflow_Studio_Manual_v0.9.6_en.zip`
- 日本語: [`docs/manual/`](docs/manual/) — ZIP: `Workflow_Studio_Manual_v0.9.6.zip`
An EPUB edition with screenshots (Japanese and English, v0.9.6) is available on [Google Drive](https://drive.google.com/drive/folders/14mOHcnLYrFgNrOo6fy7ELyFjjWrAoU-_?usp=sharing).
---
## Usage
### Launch
Click the **W** button in the ComfyUI top menu bar, or navigate to:
```
http://127.0.0.1:8188/wfm
```
> **Tip:** Shift+Click the W button to open in a new window.
### Canvas Snapshot
Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture the current workflow canvas as a thumbnail. The image is automatically saved to the workflow data folder and appears in Workflow Studio's workflow list.
### Quick Start
1. **Workflow Tab** — Your workflows from `ComfyUI/user/default/workflows/` are automatically listed
2. **Click a workflow** — View thumbnail, JSON details, and metadata in the side panel
3. **Load in GenerateUI** — Click the button to load a workflow into the generation interface
4. **Adjust parameters** — Modify prompts, models, seeds, and settings via the auto-generated UI
5. **Generate** — Hit the Generate button to queue the prompt
---
## Requirements
- **ComfyUI** — any recent version (v1.33.9+ recommended for action bar integration)
- **Python 3.10+**
- **Jinja2** — `pip install jinja2` (usually included with ComfyUI)
### Optional
- **[Ollama](https://ollama.com/)** — for AI chat assistant, translation, and VLM features
- **[LM Studio](https://lmstudio.ai/)** — alternative backend for translation and VLM (OpenAI-compatible API)
- **[Lemonade Server](https://lemonade-server.ai/)** — alternative backend for translation, chat, and VLM (OpenAI-compatible API)
- **[Unsloth](https://unsloth.ai/)** — alternative backend for translation, chat, and VLM (OpenAI-compatible API); needs an API key set via `UNSLOTH_API_KEY` in a `.env` file, or Unsloth's Keyless API access → "Chat and inference" turned on — `pip install -r requirements.txt` for `python-dotenv`
- **Gemini API key / EmbeddingGemma 2** (v0.8.2) — only for the Gallery's Semantic Search: either set `GEMINI_API_KEY` in the plugin's `.env` (key from [Google AI Studio](https://aistudio.google.com); free tier available, paid beyond it), or use the local **EmbeddingGemma 2** in [Ollama](https://ollama.com) (no key; `ollama pull embeddinggemma-2:740m` or `embeddinggemma-2:440m`)
- **[Eagle](https://eagle.cool/)** — for auto-saving generated images with metadata
- **[comfyui-mask-editor-one](https://github.com/ketle-man/comfyui-mask-editor-one) (v0.1.9+)** — enables BiRefNet background removal, SAM3 text-prompt segmentation, and ABR stamp-brush library in the Image Edit Mask tool; `birefnet.safetensors` must be placed in `ComfyUI/models/background_removal/` for BiRefNet
- **[psd-tools](https://pypi.org/project/psd-tools/)** (v0.6.0) — enables all PSD (Photoshop) features: Gallery bulk "Create PSD" export, Gallery browsing/thumbnails for `.psd` files, and Image Edit's Save PSD / Open PSD buttons; `pip install -r requirements.txt` (ships prebuilt wheels, no C compiler required); without it, PSD-related actions show an error toast with the install command instead of failing silently
#### Backend connection security (v0.7.12)
Server-side proxies for Ollama, LM Studio, Lemonade, Unsloth, the Tagger VLM and Eagle only connect to **localhost / 127.0.0.1 / ::1** (any port); `http(s)` only, no credentials in the URL, and redirects are never followed. To use a backend on another machine (e.g. an Ollama server on your LAN), list it in the `WFS_ALLOWED_BACKEND_HOSTS` environment variable (comma-separated `host` or `host:port`, e.g. `192.168.1.20:11434,gpu-box`) and restart ComfyUI. If you start ComfyUI with `--listen`, anyone who can reach it can use these routes, so only do that on a trusted network.
---
## Supported Languages
| Language | Status |
| -------- | ------ |
| English | Full |
| Japanese | Full |
| Chinese | Full |
---
## Project Structure
```
ComfyUI-Workflow-Studio/
├── __init__.py # ComfyUI entry point
├── py/
│ ├── wfm.py # Main class & route registration
│ ├── config.py # Path configuration
│ ├── nodes/
│ │ ├── prompt_text.py # WFS_PromptText custom node (positive/negative prompt)
│ │ └── gallery_feeder_node.py # WFS_GalleryFeeder custom node (gallery group → IMAGE)
│ ├── routes/
│ │ ├── workflow_routes.py # Workflow CRUD & analysis API
│ │ ├── nodes_routes.py # Nodes metadata & node sets API
│ │ ├── models_routes.py # Model metadata, preview, CivitAI API
│ │ ├── prompts_routes.py # Prompt presets CRUD API
│ │ ├── settings_routes.py # Settings API
│ │ ├── ollama_routes.py # Ollama proxy API
│ │ ├── eagle_routes.py # Eagle integration API
│ │ ├── gallery_routes.py # Gallery tab API (Output + ImagePrompt + Style_Catalog sub-tabs)
│ │ └── tagspace_routes.py # TagSpace tab API (build / space / search / neighbors)
│ └── services/
│ ├── workflow_service.py # Workflow file operations
│ ├── nodes_service.py # Node metadata & node sets
│ ├── models_service.py # Model metadata & preview images
│ ├── civitai_service.py # CivitAI API integration & cache
│ ├── prompts_service.py # Prompt presets persistence
│ ├── workflow_analyzer.py # Model/node detection
│ ├── settings_service.py # Settings persistence
│ ├── png_extractor.py # PNG metadata extraction
│ ├── gallery_service.py # Gallery image scanning, .txt sidecar & ponyxlWildcardsVault prompt resolution
│ └── gallery_metadata.py # Gallery per-image metadata store (favorites/tags/memo/groups)
├── templates/
│ └── index.html # SPA template (Workflow/GenerateUI/Prompt/Metadata/Gallery/Nodes/Models/Settings/Help/AI)
├── static/
│ ├── favicon.svg # Browser tab icon (W+S Wave)
│ ├── css/main.css # Styles
│ └── js/
│ ├── app.js # App initialization & routing
│ ├── workflow-tab.js # Workflow browser
│ ├── generate-tab.js # Generation UI
│ ├── feeder-tab.js # Feeder subtab (Image Loop + Gallery modes)
│ ├── gallery-tab.js # Gallery tab: Output sub-tab (image browser, groups, metadata) + sub-tab switching
│ ├── image-prompt-tab.js # Gallery tab: ImagePrompt sub-tab (visual prompt builder)
│ ├── style-catalog-tab.js # Gallery tab: Style_Catalog sub-tab (visual Style picker)
│ ├── tagger-tab.js # Tagger tab (WD Tagger, DeepDanbooru, Ollama VLM)
│ ├── prompt-tab.js # AI assistant & presets
│ ├── metadata-tab.js # Metadata extraction & display (PNG/WebP/JSON)
│ ├── settings-tab.js # Settings panel
│ ├── comfyui-client.js # ComfyUI WebSocket/API client
│ ├── nodes-tab.js # Node browser & node sets
│ ├── models-tab.js # Model browser & CivitAI integration
│ ├── ai-tab.js # AI TOOL tab (Translation | Chat | TOOLS/VLM | Settings)
│ ├── comfyui-workflow.js # UI-to-API format conversion
│ ├── comfyui-editor.js # Dynamic parameter editor
│ ├── json-highlight.js # JSON syntax highlighting
│ ├── util.js # Shared utilities (escapeHtml, readJsonStorage, getSettings)
│ └── i18n.js # Internationalization
├── web/comfyui/
│ ├── top_menu_extension.js # ComfyUI menu bar integration
│ ├── node_sets_menu.js # Workflow Studio Library side panel
│ └── gallery_feeder_extension.js # WFS_GalleryFeeder canvas widgets (After Gen / Run / Stop)
├── tools/
│ └── import_style_prompt_seed.py # Dev tool: pre-process a wildcards-vault source into ws_image_prompt (not run by the plugin itself)
└── data/ # Fallback data dir (used when ComfyUI user/default/ is not found)
```
---
## License
MIT License
---
## Acknowledgements
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) by comfyanonymous
- [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) by pythongosssss — Canvas snapshot and PNG workflow embedding implementation reference
- [ComfyUI-Lora-Manager](https://github.com/willmiao/ComfyUI-Lora-Manager) — Plugin architecture and UI pattern reference
- [Ollama](https://ollama.com/) for local LLM inference
- [Eagle](https://eagle.cool/) for image management
- [Pillow (PIL Fork)](https://python-pillow.org/) — server-side thumbnail generation (`data/thumb_cache/`)
- [ComfyUI-Gallery](https://github.com/PanicTitan/ComfyUI-Gallery) by PanicTitan — thumbnail grid UX reference
- [infinite-image-browsing](https://github.com/zanllp/sd-webui-infinite-image-browsing) by zanllp — thumbnail caching and index strategy reference
- [Fooocus](https://github.com/lllyasviel/Fooocus) by lllyasviel — style preset JSON format reference
+361 -695
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

+32
View File
@@ -0,0 +1,32 @@
# はじめてのComfyUI Workflow Studio
ノードが苦手でも大丈夫 ― 画像生成をもっと手軽に、もっと整理して
- 対象バージョン:Workflow Studio v0.9.6
- 著者:ketle-man
- © 2026 ketle-man
このマニュアルは、Kindle版をもとにした **画像なしのテキスト版** です。
章ごとのファイルは `chapters/` フォルダ、全章をまとめたファイルは `Workflow_Studio_Manual_v0.9.6.md` です。
画面の例は省略しているため、実際の画面はWorkflow Studioのヘルプタブとあわせてご覧ください。
## 目次
- [00_はじめに](chapters/00_はじめに.md)
- [01_第1章_基礎知識](chapters/01_第1章_基礎知識.md)
- [02_第2章_インストールと起動](chapters/02_第2章_インストールと起動.md)
- [03_第3章_画面の見方](chapters/03_第3章_画面の見方.md)
- [04_第4章_ワークフロータブ](chapters/04_第4章_ワークフロータブ.md)
- [05_第5章_生成UI_前半](chapters/05_第5章_生成UI_前半.md)
- [05_第5章_生成UI_後半](chapters/05_第5章_生成UI_後半.md)
- [06_第6章_モデルタブ](chapters/06_第6章_モデルタブ.md)
- [07_第7章_プロンプトタブ](chapters/07_第7章_プロンプトタブ.md)
- [08_第8章_ギャラリータブ](chapters/08_第8章_ギャラリータブ.md)
- [09_第9章_WS_Library](chapters/09_第9章_WS_Library.md)
- [10_第10章_AI_TOOL](chapters/10_第10章_AI_TOOL.md)
- [11_第11章_さらに広がる機能](chapters/11_第11章_さらに広がる機能.md)
- [12_第12章_困ったときは](chapters/12_第12章_困ったときは.md)
- [13_付録](chapters/13_付録.md)
- [14_おわりに](chapters/14_おわりに.md)
全章まとめ:[Workflow_Studio_Manual_v0.9.6.md](Workflow_Studio_Manual_v0.9.6.md)
File diff suppressed because it is too large Load Diff
Binary file not shown.
+172
View File
@@ -0,0 +1,172 @@
# はじめに
「ComfyUIを入れてみたけれど、ノード画面を見て手が止まってしまった」
「ネットで拾ったワークフローが増えすぎて、どれが何だか分からない」
「前にうまくいった画像の設定を、もう一度使いたいのに見つからない」
本書は、そんな方のための **ComfyUI-Workflow-Studio**(以下、Workflow Studio)の入門書です。
---
## ComfyUIは、なぜ難しいのか
ComfyUIは、いま最も自由度の高い画像生成AIソフトの1つです。
新しいモデルや技術が発表されると、真っ先に使えるようになるのは、たいていComfyUIです。
それなのに、多くの人が最初の一歩でつまずいてしまいます。その理由は、ComfyUIが **普通のアプリケーションとはまったく違う考え方** で作られているからです。
### 普通のアプリケーションとの違い
| | 普通のアプリケーション | ComfyUI |
| --- | --- | --- |
| 画面 | 開発者が設計した、決まった画面 | ノード(箱)を線でつないで、自分で組み立てる |
| 使い方 | ボタンやメニューを押せば動く | どのノードを、どうつなぐかを知らないと動かない |
| 機能の追加 | アップデートを待つ | 誰かが作ったカスタムノードを、自分で追加する |
| 設定の場所 | 設定画面にまとまっている | ワークフローの中の、あちこちのノードに散らばっている |
普通のアプリケーションは、言わば「完成品の家電」です。電源を入れれば、決められた使い方ですぐに使えます。
一方のComfyUIは、「部品から組み立てる工作キット」です。
組み立て方さえ分かれば、どんなものでも作れますが、組み立て方を知らなければ何も作れません。
### 自由さと、その副作用
ComfyUIの最大の魅力は、この **カスタマイズの自由さ** です。
世界中の開発者が作ったカスタムノードを組み合わせて、自分だけの処理の流れ(ワークフロー)を作れます。
ネットには、上級者が作った高度なワークフローがたくさん公開されていて、ダウンロードすればすぐに試せます。
しかし、自由さには副作用もあります。
- **ワークフローが増えすぎる** - 試したいものを次々ダウンロードするうちに、どれが何だか分からなくなる
- **どこを触ればいいか分からない** - 他人が作った大きなワークフローは、プロンプトを変えたいだけでも、該当するノードを探すのに苦労する
- **素材がバラバラになる** - モデル・LoRA・プロンプト・生成画像が、それぞれ別の場所に散らばり、管理する道具がない
- **試行錯誤に手間がかかる** - 条件を少しずつ変えて比べたいだけでも、1回ごとにノードの値を書き換える必要がある
ComfyUIは「組み立てる」ための道具としては非常に優秀ですが、
「整理する」「繰り返し使う」「手軽に試す」ための道具は、ほとんど用意されていません。
この空白を埋めるために作られたのが、Workflow Studioです。
---
## Workflow Studioの特色
### 1. なぜ専用画面(SPA)なのか - 「組み立てる」と「使う」を分ける
Workflow Studioは、ComfyUIの上部メニューの **W** ボタンから開く、**独立した専用画面**(SPA:シングルページアプリケーション)です。
ワークフローを組み立てる作業は、ComfyUIのノード画面で試行錯誤するのがいちばんです。
Workflow Studioは、そうして作り上げたワークフローを **素早く活用する** ための画面として生まれました。
そのため、Workflow Studioはワークフローを **入力・処理・出力** の3つに分けて考え、次のように設計されています。
| 段階 | 設計の考え方 |
| --- | --- |
| 入力 | プロンプトや画像を入れやすくする |
| 処理 | 値の変更は、最小限の操作で済むようにする |
| 出力 | 生成されたデータを管理しやすくする |
設計の根底には、製造現場などで使われる **5S(整理・整頓・清掃・清潔・しつけ)** の考え方があります。
必要なものを、必要なときに、すぐ取り出して使える状態を保つことを目指しています。
そして、ComfyUIのノード画面とWorkflow Studioの2つの画面をつなぐのが、ノード画面の横に開く **WS Library** です。
補助機能とあわせて、2つの画面を行き来する作業をスムーズにします。
### 2. 管理 - 散らばった素材を1か所に
| 管理できるもの | できること |
| --- | --- |
| ワークフロー | サムネイル付きの一覧、バッジ・グループで分類、検索、生成UIで動くかの事前チェック |
| モデル・LoRA | 種類ごとの一覧、Civitaiからの情報(トリガーワード・サンプル画像)の自動取得 |
| プロンプト | プリセットとして保存・分類、ワイルドカード、スタイル |
| 生成画像・音声 | フォルダごとの閲覧、プロンプトや「意味」での検索、埋め込まれた設定の読み取りと再現(音声ファイルにも対応) |
### 3. 効率化 - ノードを触らずに、すばやく試す
- **生成UIタブ** - ワークフローの中から、プロンプト・モデル・サイズなど、よく変える項目だけを自動で取り出し、入力フォームにします。他人が作った大きなワークフローでも、ノードを探さずに使えます
- **バッチ生成** - 「手持ちのモデルを全部試す」「プロンプトを順番に試す」「サンプラーを比べる」といった試行錯誤を、まとめて自動で実行します
- **Feeder・Lab** - たくさんの画像を順番に処理したり、複数の条件を段階的に切り替えたりする、さらに進んだ自動化もできます
### 4. 補助機能 - 生成の前後を手助けする
- **画像編集(Image Edit)** - レイヤー付きの簡易編集、マスクを使った部分的な描き直し(インペイント)
- **ローカルAI連携** - 日本語からのプロンプト翻訳、画像からのプロンプト・タグ作成
- **Tagger・Video** - 画像からのタグ自動生成、動画の生成と簡単な編集
- **TagSpace** - 手持ちのプロンプト・タグ・生成画像を「意味の近さ」で3Dの空間に並べ、眺めながらタグを選ぶ
---
## インストールして損のないカスタムノード
Workflow Studioは、次の理由から、ComfyUIを使うすべての人に **「とりあえず入れておいて損はない」** カスタムノードだと言えます。
- **無料で、ComfyUI Managerからすぐに入れられる**
- **基本機能に、追加の準備がいらない** - 必要なものはComfyUIに最初から含まれています。AIや画像編集などの追加機能は、使いたくなったときに準備すれば十分です
- **今の使い方を変えなくていい** - ワークフローはComfyUIの保存フォルダをそのまま使います。ノード画面での作業も、これまでどおりできます
- **使った分だけ便利になる** - ワークフローを一覧で眺めるだけ、生成画像の設定を確かめるだけ、といった使い方から始めて、慣れてきたら生成UIやバッチ生成へと、少しずつ活用の幅を広げられます
ComfyUIの「自由さ」はそのままに、「分かりやすさ」と「手軽さ」を足してくれる - それがWorkflow Studioです。
## この本で分かること
- Workflow Studioのインストールと、基本的な画面の見方
- 手持ちのワークフローを、サムネイル付きで整理する方法
- ノード画面を触らずに、はじめての1枚を生成する方法
- モデル・LoRA・プロンプトを管理して、うまくいった設定を再利用する方法
- 生成した画像から、使った設定を読み取って再現する方法
- ローカルAIを使った、プロンプトの翻訳や画像の読み取り
- よくあるトラブルとその対処法
## この本で扱わないこと
- ComfyUI本体のインストール方法
(ComfyUIをすでに入れて、起動できる状態から説明を始めます)
- ノードを一から組み立てて、ワークフローを自作する方法
- 画像生成AIのしくみや、モデルの学習(LoRAの作り方など)の詳しい解説
---
## 対象読者
- ComfyUIを入れたばかりの方、これから本格的に使いたい方
- ノード画面が苦手で、もっと手軽に画像を生成したい方
- ワークフローやモデル、プロンプトが増えて、整理に困っている方
パソコンの基本的な操作(ファイルのコピー、フォルダを開く、ブラウザを使う)ができれば、読み進められるように書いています。
---
## 本書の対象バージョンと環境
- **Workflow Studio**:v0.9.6
- **ComfyUI**:本書執筆時点の最新版
- **OS**:Windows(画面の例はWindowsのものです)
Workflow Studioは活発に開発が続けられているため、
バージョンによっては、画面の見た目やボタンの位置が本書と異なる場合があります。
大きく変わった機能は、Workflow Studioの **ヘルプ** タブや、GitHubのリリースノートも確認してください。
---
## 本書の表記ルール
| 表記 | 意味 |
| --- | --- |
| **太字** | 画面に表示されるボタン名・項目名 |
| 「生成UI(GenerateUI)」 | 日本語表示の名前(英語表示の名前) |
| `ComfyUI/models/` | フォルダやファイルの場所。環境による読み替えは第2章 2-6 を参照 |
| `Ctrl+Z` | キーボードの操作。「+」は同時に押すことを表します |
> **ポイント**
> 知っておくと便利な情報です。
> **注意**
> 間違えるとデータが消えたり、うまく動かなかったりする、気をつけてほしい情報です。
### 画面の言語について
Workflow Studioは英語・日本語・中国語に対応しています。本書では、**日本語表示** の画面で説明します。
ただし、生成UIなど一部の画面は、日本語表示にしても英語のまま表示されるボタンや項目があります。
その場合は、英語の表示名をそのまま使って説明しています。
---
## 本書の読み進め方
はじめての方は、**第1章から第5章まで** を順番に読み、実際に手を動かしてみてください。
第5章で「はじめての1枚」が生成できれば、Workflow Studioの基本は身についています。
第6章以降は、必要になったところから読んでもかまいません。
困ったときは、第12章のトラブルシューティングと、付録の用語集を活用してください。
それでは、Workflow Studioで、もっと手軽に、もっと楽しく画像生成を始めましょう。
@@ -0,0 +1,148 @@
# 第1章 ComfyUIとWorkflow Studioの基礎知識
この章では、操作を始める前に知っておきたい考え方と言葉を整理します。
パソコンを触りながら読む必要はありません。通勤中やすきま時間に、気軽に読み進めてください。
---
## 1-1 ComfyUIってなに?
ComfyUI(コンフィーユーアイ)は、画像生成AIを自分のパソコンで動かすための無料ソフトです。
Stable Diffusion や FLUX など、さまざまな画像生成AIのモデルに対応しています。
ComfyUIのいちばんの特徴は、**処理の流れを「箱」と「線」でつないで組み立てる**ことです。
「モデルを読み込む箱」→「プロンプトを入れる箱」→「画像を作る箱」→「保存する箱」……
というように、箱どうしを線でつなぐと、その順番で処理が行われます。
この方式には、大きなメリットがあります。
- 処理の中身がすべて見えるので、細かく調整できる
- 新しい技術が出ても、箱を追加するだけで試せる
- 組み立てた流れを保存して、何度でも使い回せる
一方で、はじめて触る人にとっては「どこを触ればいいのか分からない」画面でもあります。
この本で紹介する **Workflow Studio** は、まさにその「分かりにくさ」をやわらげるための道具です。
---
## 1-2 覚えておきたい5つの言葉
ComfyUIを使ううえで、最低限知っておきたい言葉は次の5つです。
### ① ノード(Node)
画面上に並ぶ「箱」のことです。
1つのノードは1つの仕事を受け持ちます。たとえば「モデルを読み込む」「画像を保存する」などです。
ノードには入口(入力)と出口(出力)があり、線でつないでデータを受け渡します。
### ② ワークフロー(Workflow)
ノードを線でつないだ「処理の流れ全体」のことです。
料理にたとえると、ノードが「切る」「炒める」などの作業で、ワークフローは「レシピ」にあたります。
ワークフローはJSON(ジェイソン)という形式のファイルで保存できます。
> **ポイント**
> ComfyUIで作った画像には、多くの場合ワークフローの情報が埋め込まれています。
> その画像をComfyUIの画面にドラッグすると、同じワークフローを呼び出せます。
### ③ モデル(Model)
画像を作る「AIの頭脳」にあたるファイルです。数GB〜数十GBと大きいのが一般的です。
代表的なものに次の種類があります(詳しくは第6章で説明します)。
| 種類 | ひとことで言うと |
| --- | --- |
| Checkpoint(チェックポイント) | 画像生成の本体となるモデル |
| LoRA(ローラ) | 絵柄やキャラクターを追加で覚えさせる小さな追加モデル |
| VAE | 画像の色合いや細部の仕上げを担当するモデル |
### ④ プロンプト(Prompt)
AIに「どんな画像を作ってほしいか」を伝える文章です。いわゆる「呪文」です。
描いてほしいものを書く **ポジティブプロンプト** と、
描いてほしくないものを書く **ネガティブプロンプト** の2種類があります。
### ⑤ キュー(Queue)
「生成の予約リスト」のことです。
ComfyUIでは、生成ボタンを押すとまずキューに予約が入り、順番に処理されます。
何枚分も予約しておけば、席を外している間にまとめて生成してくれます。
---
## 1-3 ComfyUIで初心者がつまずくところ
ComfyUIを入れてみたものの、こんなところで手が止まっていませんか。
1. **ワークフローが増えて、どれが何だか分からない**
ネットで拾ったワークフローを保存していくうちに、ファイル名だけでは見分けがつかなくなります。
2. **毎回ノード画面で値を探すのが大変**
プロンプトやシード値を変えたいだけなのに、大きなワークフローの中から該当するノードを探す必要があります。
3. **モデルやプロンプトの管理がバラバラ**
どのモデルをどこに置いたか、前にうまくいったプロンプトは何だったか……が分からなくなりがちです。
4. **前に作った画像の設定を再現できない**
「あの画像、どの設定で作ったっけ?」となっても、調べ方が分からないことがあります。
どれも、ComfyUIの機能が足りないというより、**「整理」と「入口」が足りない**ことが原因です。
---
## 1-4 Workflow Studioでできること
Workflow Studioは、ComfyUIに追加して使う **拡張機能(カスタムノード)** です。
ComfyUIの中に「管理と生成のための専用画面」を追加してくれます。
先ほどの4つのつまずきに対して、Workflow Studioは次のように応えます。
| つまずき | Workflow Studioの解決策 | 本書の章 |
| --- | --- | --- |
| ワークフローが見分けられない | サムネイル付きの一覧、バッジ・グループ分け、検索 | 第4章 |
| ノード画面で値を探すのが大変 | 「生成UI」タブで、必要な項目だけをフォームとして表示 | 第5章 |
| モデル・プロンプトがバラバラ | モデルタブ・プロンプトタブでまとめて管理 | 第6・7章 |
| 設定を再現できない | ギャラリーで画像のメタデータを確認し、キャンバスへ送る | 第8章 |
さらに、ComfyUIのノード画面の横に **WS Library** というサイドパネルが追加され、
モデルやプロンプトをドラッグ&ドロップでノード画面に置けるようになります(第9章)。
ローカルで動くAI(OllamaやLM Studioなど)を用意すれば、
プロンプトの翻訳や、画像からのタグ生成といったAI補助も使えます(第10章)。
> **ポイント**
> Workflow Studioは、ComfyUIの代わりになるソフトではありません。
> 画像を作るのはあくまでComfyUIで、Workflow Studioはその「操作パネル」と「整理棚」の役割です。
> そのため、ComfyUIが起動していないと使えません。
### 画面は日本語で使えます
Workflow Studioは英語・日本語・中国語に対応しています。
本書では日本語表示の画面で説明し、必要に応じて英語表示の名前を( )で添えます。
例:「生成UI(GenerateUI)」タブ、「ワークフロー(Workflow)」タブ
---
## 1-5 本書の進め方
本書は、次の順番で読み進めることを想定しています。
1. **準備する**(第2〜3章)
インストールして、画面の見方を覚えます。
2. **整理する**(第4章)
手持ちのワークフローを一覧で見られるようにします。
3. **作る**(第5章)
ノード画面を触らずに、はじめての1枚を生成します。
ここまで読めば、Workflow Studioの基本は使えるようになります。
4. **貯める・見返す**(第6〜8章)
モデル・プロンプト・生成画像を管理し、うまくいった設定を再利用します。
5. **広げる**(第9〜11章)
サイドパネルやAI補助など、便利な機能を必要に応じて取り入れます。
困ったときは、第12章のトラブルシューティングと付録の用語集を見てください。
> **この章のまとめ**
> - ComfyUIは、ノード(箱)をつないだワークフローで画像を作るソフト
> - 覚える言葉は「ノード」「ワークフロー」「モデル」「プロンプト」「キュー」の5つ
> - Workflow Studioは、ComfyUIに「整理棚」と「かんたん操作パネル」を追加する拡張機能
> - まずは第5章の「はじめての1枚」を目標にしよう
@@ -0,0 +1,254 @@
# 第2章 インストールと起動
この章では、Workflow StudioをComfyUIに追加して、はじめて画面を開くところまで進めます。
ComfyUI本体はすでにインストール済みで、起動できる状態であることを前提にしています。
---
## 2-1 動作に必要なもの
### 必須
| 必要なもの | 説明 |
| --- | --- |
| ComfyUI | 最近のバージョンであればOK。上部メニューにボタンを並べる機能を使うため、新しめのバージョン(フロントエンド v1.33.9 以降)がおすすめです |
| Python 3.10 以上 | ComfyUIに同梱されているものでかまいません |
| Webブラウザ | Chrome や Edge など、ComfyUIの画面を開いているブラウザ |
Workflow Studioの基本機能は、ComfyUIが動いていれば追加の準備なしで使えます。
### あとから必要に応じて
次のものは、特定の機能を使うときだけ必要です。最初は気にしなくてかまいません。
- **Ollama / LM Studio など** - プロンプトの翻訳やAIチャットなどのAI補助機能(第10章)。
ギャラリーのセマンティック検索をローカルで行うとき(第8章)と、TagSpaceタブ(第11章)には **Ollama** が必要です
- **Eagle** - 生成画像を画像管理ソフトEagleに自動保存する機能(第8章)
- **追加のPythonパッケージ** - Tagger(画像からのタグ生成)やPSD書き出しなど(第11章)
> **ポイント**
> 「全部そろえないと使えない」わけではありません。
> まずは本体だけ入れて、使いたい機能が出てきたときに追加しましょう。
---
## 2-2 ComfyUI Managerでインストールする(おすすめ)
**ComfyUI Manager** は、カスタムノード(拡張機能)を画面上から検索・インストールできる管理ツールです。
最近のComfyUIには最初から入っていることが多く、いちばん手軽な方法です。
1. ComfyUIを起動し、ブラウザで画面を開きます。
2. 上部メニューの **「Manager」** ボタンをクリックします。
3. **「Custom Nodes Manager」** をクリックします。
4. 検索欄に **`Workflow Studio`** と入力します。
5. 一覧に表示された **ComfyUI-Workflow-Studio** の **「Install」** をクリックします。
6. インストールが終わったら、画面の案内に従って **ComfyUIを再起動** します。
> **注意**
> ComfyUIは、起動したときに一度だけ拡張機能を読み込みます。
> インストール後は必ずComfyUIを再起動してください。ブラウザの再読み込みだけでは反映されません。
---
## 2-3 手動でインストールする(git clone)
ComfyUI Managerが使えない場合や、Gitに慣れている方は、手動でもインストールできます。
コマンドを入力する画面(Windowsなら「コマンドプロンプト」や「ターミナル」)を使います。
1. ComfyUIのフォルダの中にある **`custom_nodes`** フォルダに移動します。
```
cd ComfyUI/custom_nodes
```
2. 次のコマンドを入力して、Workflow Studioをダウンロードします。
```
git clone https://github.com/ketle-man/ComfyUI-Workflow-Studio.git
```
3. `custom_nodes` の中に **`ComfyUI-Workflow-Studio`** フォルダができていればOKです。
4. ComfyUIを再起動します。
> **ポイント**
> `git` コマンドが使えない場合は、Git(Git for Windowsなど)を先にインストールする必要があります。
> 不安な方は、2-2のComfyUI Managerを使う方法がおすすめです。
---
## 2-4 起動する
### Wボタンから開く
ComfyUIを再起動すると、画面上部のメニューに **「W」** のボタンが追加されています。
これがWorkflow Studioを開くボタンです。
- **クリック** - 新しいタブでWorkflow Studioが開きます。
- **Shiftキーを押しながらクリック** - 新しいウィンドウで開きます。ComfyUIの画面と並べて使いたいときに便利です。
Wボタンの近くには、**カメラのボタン** も追加されています。
これは、ワークフローの画面をサムネイルとして保存するためのボタンです(第4章で使います)。
### アドレスとポート番号のしくみ
Wボタンを押すと、ブラウザのアドレス欄にWorkflow Studioのアドレスが表示されます。
このアドレスの見方を知っておくと、ブックマークから直接開いたり、トラブルのときに原因を探したりできます。
アドレスは、次の3つの部分でできています。
| 部分 | 例 | 意味 |
| --- | --- | --- |
| ホスト | `127.0.0.1` | どのパソコンにつなぐか。`127.0.0.1`(または `localhost`)は「自分のパソコン」を表す特別な番号です |
| ポート番号 | `8188` | そのパソコンの中の、どのソフトにつなぐか |
| パス | `/wfm` | そのソフトの中の、どのページを開くか |
この3つを「`ホスト:ポート番号/パス`」の形でつなげたものが、Workflow Studioのアドレスになります。
#### ポート番号とは
1台のパソコンの中では、たくさんのソフトが同時に通信しています。
そこで、どのソフトあての通信なのかを区別するために使われるのが **ポート番号** です。
マンションにたとえると、ホスト(`127.0.0.1`)が「建物の住所」、ポート番号が「部屋番号」にあたります。
同じ建物でも、部屋番号を間違えると目的の相手には届きません。
ComfyUIは、起動したときに決められたポート番号で待ち受けています。
よく使われる番号は次のとおりです。
| ComfyUIの種類・起動方法 | よく使われるポート番号 |
| --- | --- |
| ポータブル版・手動インストール版(標準設定) | 8188 |
| ComfyUI Desktop(デスクトップアプリ版) | 8000 |
| 起動オプション `--port` を指定した場合 | 指定した番号 |
Stability Matrixなどのランチャーを使っている場合も、ランチャー側の設定で番号が変わることがあります。
#### 自分のポート番号を確かめる
いちばん確実なのは、**いまComfyUIを開いているブラウザのアドレス欄を見る** ことです。
`127.0.0.1:` や `localhost:` のあとに続く数字が、ポート番号です。
ComfyUIを起動したときの黒い画面(コンソール)にも、
`To see the GUI go to: http://127.0.0.1:〇〇〇〇` のように表示されます。
#### アドレスを直接入力して開く
ポート番号が分かれば、Wボタンを使わずにWorkflow Studioを開けます。
ComfyUIのアドレスの末尾に **`/wfm`** を付けるだけです。
- ComfyUIが `127.0.0.1:8188` で開いている → Workflow Studioは `127.0.0.1:8188/wfm`
- ComfyUIが `127.0.0.1:8000` で開いている → Workflow Studioは `127.0.0.1:8000/wfm`
よく使う方は、このアドレスをブックマークしておくと便利です。
> **注意**
> ブックマークから開いた場合、ワークフローをComfyUIのキャンバスへ直接送る「Send to Canvas」が使えません。
> 代わりに、ComfyUI側のサイドパネル(WS Library)にそのワークフローが表示されるので、自分でキャンバスへドラッグします(第4章・第9章)。
> ComfyUIの画面と行き来しながら使うときは、Wボタンから開くのがおすすめです。
> **ポイント**
> Workflow StudioはComfyUIの一部として動いているため、ComfyUIと同じポート番号を使います。
> Workflow Studio専用のポート番号はありません。
---
## 2-5 表示言語を日本語にする
Workflow Studioは、はじめは **英語** で表示されます。次の手順で日本語に切り替えましょう。
1. 上部のタブから **「Settings」** を開きます。
2. **「UI Language」** の欄で **「日本語」** を選びます。
3. 「言語を変更しました。再読み込みします...」という表示のあと、ページが自動で再読み込みされ、日本語表示に切り替わります。
日本語に切り替えると、タブの名前は次のように変わります(一部は英語のままです)。
| 英語表示 | 日本語表示 |
| --- | --- |
| Workflow | ワークフロー |
| GenerateUI | 生成UI |
| Gallery | ギャラリー |
| Prompt | プロンプト |
| Models | モデル |
| Nodes | ノード |
| Settings | 設定 |
| Help | ヘルプ |
| AI TOOL / Tagger / Image Edit / Video | (英語のまま) |
> **ポイント**
> 言語の設定は、使っている **ブラウザごと** に保存されます。
> 別のブラウザや別のパソコンから開いた場合は、もう一度設定してください。
同じ場所にある **「要約言語」** は、AIでワークフローの説明文を作るときの言語です(第10章)。今は変更しなくてかまいません。
---
## 2-6 環境ごとのフォルダの場所
ComfyUIには、いくつかの入れ方があります。入れ方によって、フォルダの場所が少しずつ違います。
本書では、フォルダの場所を **`ComfyUI/` から始まる形** で書いています。
お使いの環境に合わせて、下の表の「`ComfyUI/` にあたる場所」に読み替えてください。
### 本書の書き方と、各環境での場所
| 本書の表記 | 中身 | ポータブル版 | ComfyUI Desktop | Stability Matrix |
| --- | --- | --- | --- | --- |
| `ComfyUI/` | ComfyUIの本体フォルダ | `ComfyUI_windows_portable/ComfyUI/` | インストール時に選んだフォルダ(標準では「ドキュメント」内の `ComfyUI`) | `(データフォルダ)/Packages/ComfyUI/` |
| `ComfyUI/custom_nodes/` | カスタムノード(Workflow Studioもここ) | `ComfyUI/custom_nodes/` | `ComfyUI/custom_nodes/` | `ComfyUI/custom_nodes/` |
| `ComfyUI/models/` | モデル | `ComfyUI/models/` | `ComfyUI/models/` | `(データフォルダ)/Models/`(全パッケージで共有) |
| `ComfyUI/output/` | 生成した画像 | `ComfyUI/output/` | `ComfyUI/output/` | `(データフォルダ)/Images/` |
| `ComfyUI/user/default/workflows/` | 保存したワークフロー | `ComfyUI/user/default/workflows/` | `ComfyUI/user/default/workflows/` | `ComfyUI/user/default/workflows/` |
| ComfyUIのPython | 追加パッケージのインストール先 | `ComfyUI_windows_portable/python_embeded/python.exe` | `ComfyUI/.venv/Scripts/python.exe` | `ComfyUI/venv/Scripts/python.exe` |
| 標準のポート番号 | 2-4参照 | 8188 | 8000 | 8188 |
> **ポイント**
> 表の中の `ComfyUI/` は、それぞれの環境の「`ComfyUI/` にあたる場所」を指します。
> パスの区切りは、Windowsでは `/` の代わりに `\` で表示されます。
### ポータブル版
ダウンロードしたZIPファイルを展開してできた `ComfyUI_windows_portable` フォルダの中に、すべてが入っています。
起動は、フォルダ内の `run_nvidia_gpu.bat` などで行います。
**Python** は、ComfyUI専用の `python_embeded` フォルダの中のものを使います(第11章 11-2)。
### ComfyUI Desktop
インストーラーで入れるアプリ版です。最初の起動時に、データを置くフォルダを選びます。
フォルダの場所を忘れたときは、アプリのメニューからフォルダを開く項目(「Open Folder」など)を使うと確認できます。
標準のポート番号が **8000** なので、アドレスを直接入力するときは注意してください(2-4)。
### Stability Matrix
複数の画像生成ソフトをまとめて管理できるランチャーです。
最初に決めた **データフォルダ**(例:`StabilityMatrix/Data/`)の中に、次のように分かれて入っています。
- **ComfyUI本体** - `Data/Packages/ComfyUI/`
- **モデル** - `Data/Models/` の中に、種類ごとのフォルダ(Checkpointは `StableDiffusion`、LoRAは `Lora` など)。ほかのソフトと共有されます
- **生成した画像** - `Data/Images/` の中
> **注意**
> Stability Matrixでは、モデルや画像のフォルダが **ComfyUIの本体フォルダの外** にあります。
> 本書で「`ComfyUI/models/checkpoints` に置く」と書いてある場合は、Stability Matrixの共有モデルフォルダ(`Data/Models/StableDiffusion` など)に置いてください。
> ギャラリーに画像が出てこないときは、設定タブの「ギャラリー Outputフォルダ」が `Data/Images/` の中を指しているか確認しましょう。
> **ポイント**
> フォルダの名前や構成は、各ソフトのバージョンアップで変わることがあります。
> 見つからないときは、各ソフトの公式の説明も確認してください。
---
## 2-7 うまく起動しないときは
### Wボタンが表示されない
- **ComfyUIを再起動しましたか?** ブラウザの再読み込みだけでは反映されません。
- **インストール先は正しいですか?** `ComfyUI/custom_nodes/ComfyUI-Workflow-Studio` にフォルダがあるか確認してください。
フォルダの中にさらに同じ名前のフォルダがある(二重になっている)と読み込まれません。
- **ComfyUIのバージョンが古くありませんか?** ComfyUI Managerなどから本体を更新してみてください。
### ページが開かない・「接続できません」と表示される
- **ComfyUIは起動していますか?** Workflow StudioはComfyUIの中で動くため、ComfyUIを終了すると開けません。
- **ポート番号は合っていますか?** 2-4の方法で、ComfyUIのポート番号を確認してください。
たとえばComfyUI Desktopを使っているのに `8188` を入力していると、つながりません。
### 起動時のコンソールにエラーが出る
ComfyUIを起動したときの黒い画面に、`ComfyUI-Workflow-Studio` に関するエラーが表示されていないか確認します。
エラーの内容をメモしておくと、第12章のトラブルシューティングや、GitHubで質問するときに役立ちます。
> **この章のまとめ**
> - インストールはComfyUI Managerで「Workflow Studio」を検索するのがかんたん
> - インストール後は必ずComfyUIを再起動する
> - 上部メニューの「W」ボタンで開く(Shift+クリックで別ウィンドウ)
> - アドレスは「ホスト:ポート番号/wfm」。ポート番号はComfyUIを開いているアドレス欄で確認できる
> - 設定タブの「UI Language」で日本語に切り替える
> - フォルダの場所は環境(ポータブル版・Desktop・Stability Matrix)によって違う。本書の `ComfyUI/` は読み替える
@@ -0,0 +1,146 @@
# 第3章 画面の見方
Workflow Studioにはたくさんのタブがありますが、最初から全部を覚える必要はありません。
この章では、それぞれのタブが「何をする場所か」をざっくりつかみ、
ComfyUI側に追加されるボタンとサイドパネルの役割を確認します。
---
## 3-1 タブの一覧と役割
Workflow Studioを開くと、画面の上部にタブが横一列に並んでいます。
左から順に、次のタブがあります。
「よく使う」に★が付いたタブから覚えていけば十分です。
| タブ(日本語表示) | 英語表示 | 何をする場所か | よく使う | 本書の章 |
| --- | --- | --- | :---: | --- |
| ワークフロー | Workflow | 保存したワークフローの一覧・整理 | ★ | 第4章 |
| ノード | Nodes | ノードの一覧・お気に入り登録 | | 第11章 |
| モデル | Models | Checkpoint・LoRAなどモデルの一覧・整理 | ★ | 第6章 |
| 生成UI | GenerateUI | ノード画面を触らずに画像を生成 | ★ | 第5章 |
| プロンプト | Prompt | プロンプトの保存・分類・AI補助 | ★ | 第7章 |
| ギャラリー | Gallery | 生成した画像の閲覧・設定の確認 | ★ | 第8章 |
| Image Edit | Image Edit | 画像の簡易編集・マスク作成 | | 第11章 |
| Video | Video | 動画生成・動画編集 | | 第11章 |
| Tagger | Tagger | 画像からタグ(プロンプト)を自動生成 | | 第11章 |
| TagSpace | TagSpace | タグ・プロンプト・生成画像を意味の近さで3D表示し、タグを選ぶ | | 第11章 |
| 設定 | Settings | 言語・テーマ・フォルダなどの設定 | | 本章・第11章 |
| ヘルプ | Help | 機能一覧・ショートカット・トラブル対処 | | 本章 |
| AI TOOL | AI TOOL | ローカルAIによる翻訳・チャット・画像解析・文字起こし(OCR) | | 第10章 |
### タブは「整理する」「作る」「補助する」の3グループで考える
タブが多くて迷ったときは、次の3つのグループに分けて考えると分かりやすくなります。
- **整理する** - ワークフロー/ノード/モデル/プロンプト/ギャラリー
手持ちの素材を一覧にして、探しやすくするタブです。
- **作る** - 生成UI/Image Edit/Video
実際に画像や動画を作ったり、手を加えたりするタブです。
- **補助する** - AI TOOL/Tagger/TagSpace/設定/ヘルプ
他のタブでの作業を助けたり、全体の設定をしたりするタブです。
> **ポイント**
> 各タブは互いにつながっています。
> たとえば、ワークフロータブで選んだワークフローを生成UIに読み込んだり、
> プロンプトタブで保存したプロンプトを生成UIで呼び出したりできます。
### ヘルプタブ
困ったときに最初に開きたいのが **ヘルプ** タブです。
左側に話題の一覧が並び、クリックすると右側に説明が表示されます。
- 機能一覧(タブごとの機能の概要)
- キーボードショートカット
- トラブルシューティング
- サポート(GitHubなどへのリンク)
本書を読みながら「このボタンは何だっけ?」と思ったときにも役立ちます。
---
## 3-2 ComfyUI側に追加されるボタン
Workflow Studioをインストールすると、ComfyUIの画面上部のメニューに **3つのボタン** が追加されます。
| ボタン | 名前 | 役割 |
| --- | --- | --- |
| ① W | Workflow Studioを開く | Workflow Studioを新しいタブで開きます。Shift+クリックで新しいウィンドウ(第2章) |
| ② カメラ | キャンバススナップショット | いまのノード画面を画像として保存し、ワークフローのサムネイルにします(第4章) |
| ③ 四角が並んだアイコン | WS Library | ノード画面の横にサイドパネルを開閉します(3-3) |
ボタンにマウスを重ねると、英語で説明が表示されます。
> **ポイント**
> Workflow Studio本体(タブが並ぶ画面)と、ComfyUIのノード画面は **別のブラウザタブ** で開きます。
> 「整理や生成はWorkflow Studio」「細かい組み立てはComfyUIのノード画面」と使い分け、
> 2つの画面を行き来しながら使うのが基本のスタイルです。
---
## 3-3 サイドパネル「WS Library」
③のボタンを押すと、ComfyUIのノード画面の横に **WS Library(Workflow Studio Library)** というパネルが開きます。
WS Libraryは、**Workflow Studioで整理した素材を、ノード画面ですぐ使うための棚** です。
パネルの上部には、1文字のタブが並んでいます(マウスを重ねると正式な名前が表示されます)。
| タブ | 名前 | できること |
| --- | --- | --- |
| W | Workflows | ワークフローをドラッグしてキャンバスに読み込む |
| N | Nodes | お気に入りのノードやノードセットをキャンバスに置く |
| P | Prompts | 保存したプロンプトをキャンバスに置く・コピーする |
| M | Models | モデルをドラッグして、読み込み用のノードごとキャンバスに置く |
| I | Information | 画像やワークフローのファイルを落として、中身(使ったモデルやプロンプト)を調べる |
| A | AI TOOL | 翻訳やチャットなどのAI機能を、ノード画面のまま使う |
たとえば M タブからモデルをキャンバスへドラッグすると、そのモデルを読み込むノードが自動で配置されます。
ノードを一覧から探して置いて、ファイル名を選んで……という手間が省けます。
詳しい使い方は第9章で説明します。今は「ノード画面の横に、素材の棚を出せる」とだけ覚えておけば大丈夫です。
---
## 3-4 テーマと見た目の設定
Workflow Studioの見た目は、**設定** タブで好みに合わせて変えられます。
長時間使う道具なので、目が疲れにくい配色や読みやすい文字サイズにしておくのがおすすめです。
### テーマ
**「テーマ」** の欄から、画面全体の配色を選べます。標準は暗い配色の「Deep Ocean Dark」です。
| テーマの例 | 雰囲気 |
| --- | --- |
| Deep Ocean Dark(デフォルト) | 暗めの青。画像が見やすい |
| Light Minimalist(クリーン) | 白基調で明るくシンプル |
| Pastel Dream(パステル) | やわらかいパステル調 |
| Corporate Trust(企業向け) | 落ち着いた白と青 |
このほか、ポップ調・サイバーパンク・レトロなど、全部で13種類のテーマがあります。
> **ポイント**
> 生成した画像の色を確認するときは、暗めのテーマのほうが色を見分けやすくなります。
> 迷ったら、まずはデフォルトのまま使ってみましょう。
### カスタマイズ
**「カスタマイズ」** を開くと、選んだテーマをさらに細かく調整できます。
- **カラー調整** - 背景・テキスト・アクセントなどの色を個別に変更
- **背景パターン** - ストライプやドットなどの模様を背景に追加
- **フォント** - 文字の書体を変更(Zen丸ゴシック、BIZ UDPゴシックなど日本語フォントも選べます)
調整が終わったら **「適用して保存」** を押します。
元に戻したいときは **「カスタマイズをリセット」** を押します。
### 文字サイズ
**「文字サイズ」** の欄で、画面全体の文字の大きさを変えられます。
文字が小さくて読みにくいと感じたら、ここで大きくしましょう。
### WS Libraryの見た目
WS Library(3-3)の配色は、パネル内の **歯車ボタン**から別に設定できます。
ComfyUIの画面の色に合わせて、背景や文字の色を調整できます。
> **この章のまとめ**
> - タブは「整理する」「作る」「補助する」の3グループで考えると分かりやすい
> - まずはワークフロー・モデル・生成UI・プロンプト・ギャラリーの5つを覚えれば十分
> - ComfyUIの上部には「W」「カメラ」「WS Library」の3つのボタンが追加される
> - WS Libraryは、ノード画面の横に出せる素材の棚
> - 見た目は設定タブのテーマ・カスタマイズ・文字サイズで調整できる
@@ -0,0 +1,305 @@
# 第4章 ワークフロータブ - 作ったワークフローを整理する
ワークフロータブは、Workflow Studioを開いたときに最初に表示される、いわば「玄関」です。
この章では、手持ちのワークフローを一覧で見られるようにし、
あとから探しやすいように整理する方法を学びます。
---
## 4-1 ワークフローが自動で一覧表示されるしくみ
ワークフロータブを開くと、すでにいくつかのワークフローが並んでいるかもしれません。
これは、Workflow StudioがComfyUIのワークフロー保存フォルダを自動で読み込んでいるためです。
ComfyUIのノード画面で「保存(Save)」したワークフローは、通常、次のフォルダに保存されます。
```
ComfyUI/user/default/workflows/
```
Workflow Studioは、このフォルダの中のワークフローを一覧にして表示します。
つまり、**ComfyUIで保存したワークフローは、何もしなくてもワークフロータブに並ぶ** というわけです。
> **ポイント**
> ComfyUIで新しく保存したワークフローが一覧に出てこないときは、
> ツールバーの **「更新」** ボタンを押してください。最新の状態に読み込み直します。
> **ポイント**
> 読み込むフォルダは、設定タブの **「ワークフローデータフォルダ」** で変更できます。
> 特に理由がなければ、そのままで問題ありません。
---
## 4-2 画面の見方と表示の切り替え
ワークフロータブは、大きく3つの部分に分かれています。
| 番号 | 場所 | 役割 |
| --- | --- | --- |
| ① | ツールバー(上部) | 検索・絞り込み・並べ替え・各種ボタン |
| ② | 一覧(中央) | ワークフローがカードまたは表で並ぶ |
| ③ | サイドパネル(右側) | 選んだワークフローのサムネイルや中身を表示 |
### サムネイル表示とテーブル表示
ツールバーの切り替えボタンで、一覧の表示方法を変えられます。
- **サムネイル** - 画像付きのカードで並べます。見た目で探したいときに便利です。
- **テーブル** - 表形式で並べます。たくさんのワークフローを名前で探したいときに便利です。
### クリックとダブルクリック
一覧のワークフローは、クリックの仕方で動きが変わります。
| 操作 | 動き |
| --- | --- |
| クリック | 右側のサイドパネルに内容を表示(ワークフローを「選択」した状態になる) |
| ダブルクリック | 詳細画面を開く(バッジ・タグ・メモの編集など。4-4で説明) |
### サイドパネル
ワークフローをクリックすると、右側のサイドパネルに次の3つのタブが表示されます。
- **サムネイル** - ワークフローの画像を大きく表示
- **JSON** - ワークフローの中身(JSONデータ)を色分けして表示。「コピー」ボタンで丸ごとコピーできます
- **グループ** - ワークフローをグループに分類(4-4で説明)
ワークフロー名の下には、**ワークフローの形式**(UI形式/API形式など)と、
生成UIで使えるかどうかの **チェック結果** が表示されます(4-7で説明)。
---
## 4-3 カメラボタンでサムネイルを撮る
ComfyUIで保存しただけのワークフローは、サムネイル画像がなく、一覧では見分けにくいことがあります。
そこで使うのが、ComfyUIの上部メニューに追加された **カメラボタン**(キャンバススナップショット)です。
### サムネイルの撮り方
1. ComfyUIのノード画面で、サムネイルにしたいワークフローを開きます。
2. ワークフロー全体が見やすくなるように、画面の位置や拡大率を調整します。
3. 上部メニューの **カメラボタン** をクリックします。
4. 保存する名前を確認する画面が表示されます。必要に応じて名前を変えて保存します。
(最初は、いま開いているワークフローの名前が入っています)
5. 「Workflow saved」と表示されれば完了です。
Workflow Studioのワークフロータブを開く(または「更新」を押す)と、
撮影した画像がサムネイルとして付いたワークフローが一覧に表示されます。
> **ポイント**
> カメラボタンで保存した画像(PNG)には、ワークフローの情報も埋め込まれています。
> この画像をComfyUIのノード画面にドラッグ&ドロップすると、そのワークフローを読み込めます。
> 画像ファイル1つで「見た目」と「中身」の両方を持ち運べるので便利です。
### あとからサムネイルを変える
すでにあるワークフローのサムネイルを差し替えたいときは、
ワークフローをダブルクリックして詳細画面を開き、**「サムネイル変更」** を押して画像を選びます。
---
## 4-4 お気に入り・バッジ・グループで分類する
ワークフローが増えてきたら、分類しておくと目的のものをすぐに見つけられます。
Workflow Studioには、分類のしかたが3種類あります。
| 方法 | どんなときに使う? | 例 |
| --- | --- | --- |
| お気に入り(★) | よく使うものに印を付けたい | 毎日使う基本のワークフロー |
| バッジ | 色付きのラベルで、ひと目で種類を分けたい | 「SDXL」「FLUX」「動画」「実験中」 |
| グループ | 用途やプロジェクトごとにまとめたい | 「キャラクター制作」「背景素材」 |
### お気に入り(★)
カードの **★** をクリックすると、お気に入りに登録されます。
ツールバー上部の **★** ボタンを押すと、お気に入りだけを表示できます(**ALL** で全件表示に戻ります)。
お気に入りに登録したワークフローは、ComfyUI側のサイドパネル(WS Library)の
「★ Favorites」にも表示されます(第9章)。
### バッジ
バッジは、ワークフローに付ける **色付きのラベル** です。
1つのワークフローに複数のバッジを付けることもできます。
**① バッジを作る**
1. ツールバーの **「Badge」** ボタンを押します。
2. **「バッジ名...」** の欄に名前を入力し、**「追加」** を押します。
3. 色の見本をクリックすると、バッジの色を変えられます。
**② ワークフローにバッジを付ける**
1. ワークフローをダブルクリックして、詳細画面を開きます。
2. **「バッジ」** の欄で、付けたいバッジにチェックを入れます。
3. **「保存」** を押します。
**③ バッジで絞り込む**
バッジを作ると、ツールバー上部の ★・ALL ボタンの横にバッジが並びます。
バッジをクリックすると、そのバッジが付いたワークフローだけが表示されます。
> **ポイント**
> バッジの種類と色は、モデルタブ(第6章)と共通です。
> 「SDXL」というバッジを作れば、ワークフローにもモデルにも同じ色で付けられます。
### グループ
グループは、ワークフローを **用途ごとにまとめる箱** です。
1. ワークフローをクリックして選択します。
2. 右側のサイドパネルで **「グループ」** タブを開きます。
3. **「新規グループ作成」** でグループを作るか、既存のグループを選んで **「グループに追加」** します。
ツールバーの **「全グループ」** と書かれたプルダウンからグループを選ぶと、そのグループのワークフローだけが表示されます。
### タグとメモ
詳細画面では、ほかにも次の情報を書き込めます。
- **タグ** - 検索用のキーワード。カンマ(,)で区切って複数入力できます。
- **メモ** - 使い方の注意点や、うまくいった設定など、自由なメモ。
ネットから入手したワークフローに「配布元」や「必要なカスタムノード」をメモしておくと、あとで困りません。
### 名前の変更と削除
- **名前を変える** - 詳細画面の上部にあるワークフロー名をクリックすると、名前を編集できます。
- **削除する** - 詳細画面の **「削除」** ボタンを押します。
> **注意**
> 削除すると、ComfyUIのワークフロー保存フォルダにあるファイルそのもの(サムネイル画像も含む)が削除されます。
> 大切なワークフローは、削除する前に別の場所へバックアップしておきましょう。
---
## 4-5 検索・並べ替え・フィルタ解除
### 検索
ツールバーの **「検索...」** 欄にキーワードを入力すると、名前やタグなどに一致するワークフローだけが表示されます。
入力欄の右端の **×** を押すと、検索文字がすぐに消えます。
### 並べ替え
検索欄の横のプルダウンで、並び順を変えられます。
| 選択肢 | 並び順 |
| --- | --- |
| Date (Newest) | 新しい順 |
| Date (Oldest) | 古い順 |
| Name A-Z | 名前の昇順 |
| Name Z-A | 名前の降順 |
選んだ並び順は記憶され、次に開いたときも同じ順番で表示されます。
### フィルタをまとめて解除
検索・グループ・バッジで絞り込んでいるうちに、「なぜか一覧に出てこない」と迷うことがあります。
そんなときは、ツールバー右端の **「× クリア」** を押してください。
検索文字・グループ・バッジの絞り込みが一度に解除され、すべてのワークフローが表示されます。
---
## 4-6 インポートとキャンバスへ送る
### ワークフローを取り込む(インポート)
ネットから入手したワークフローのファイル(`.json` または ワークフロー入りの `.png`)は、
次のどちらかの方法でワークフロータブに取り込めます。
- ツールバーの **「インポート」** ボタンを押して、ファイルを選ぶ(複数選択も可能)
- ファイルを、ワークフロータブの一覧部分に **ドラッグ&ドロップ** する
取り込んだワークフローは、ComfyUIのワークフロー保存フォルダに保存され、一覧に追加されます。
### 選んだワークフローを使う
ワークフローをクリックして選択すると、ツールバーの次の3つのボタンが押せるようになります。
| ボタン | 動き |
| --- | --- |
| 生成UIに読み込み | 生成UIタブに読み込み、ノード画面を触らずに生成できる状態にする(第5章) |
| Videoに読み込み | 動画生成用のワークフローをVideoタブに読み込む(第11章) |
| キャンバスへ送る | ComfyUIのノード画面にワークフローを読み込む |
> **ポイント**
> ボタンが押せない(灰色の)ときは、まだワークフローを選択していません。
> 一覧のワークフローを一度クリックしてから押してください。
**「キャンバスへ送る」** を押すと、Workflow Studioを開いたComfyUIのノード画面に、そのワークフローが読み込まれます。
Workflow Studioで探して、ComfyUIで細かく編集する、という使い方ができます。
> **注意**
> 「キャンバスへ送る」は、ComfyUIの **Wボタンから開いた** Workflow Studioで使えます。
> ブックマークなどから直接開いた場合は、ComfyUI側のWS Libraryにそのワークフローが表示されるので、
> そこからキャンバスへドラッグしてください(第2章・第9章)。
---
## 4-7 生成UIで動くか事前にチェックする(GenerateUIチェック)
すべてのワークフローが、生成UI(第5章)でそのまま使えるわけではありません。
特殊なカスタムノードを使っていたり、手元にないモデルを指定していたりすると、うまく動かないことがあります。
そこで、生成UIに読み込む前に、使えそうかどうかを自動でチェックする機能があります。
### チェック結果の見方
ワークフローをクリックすると、サイドパネルのワークフロー名の下に、次の情報が表示されます。
- **形式バッジ** - ワークフローの保存形式(UI形式/API形式/App形式)
- **変換** - 生成UIで使える形に変換できるか
- **操作性** - 生成UIの画面だけで、どのくらい設定を変えられるか
それぞれ、次の3段階で表示されます。
| 表示 | 意味 |
| --- | --- |
| ○(チェックマーク) | 問題なし |
| △(警告マーク) | 使えるが注意が必要 |
| ×(バツ印) | 生成UIでは使えない、または使いにくい |
バッジにマウスを重ねると、判定の理由が表示されます。
### よくある判定と対処
| 判定 | よくある理由 | 対処 |
| --- | --- | --- |
| 変換 × | ワークフローに必要なカスタムノードがインストールされていない | ComfyUI Managerで不足しているノードをインストールする(第12章) |
| 変換 △ | 指定されたモデルやLoRAが手元にない | 生成UIで手元のモデルを選び直す。UI形式の場合は、自動で一覧の先頭のモデルに差し替えられるので注意 |
| 操作性 △/× | 生成UIに表示されない設定が多い | 表示されないノードの設定は、ComfyUIのノード画面で変更する |
> **ポイント**
> 操作性は、「設定を持つノードのうち、生成UIに表示されるものの割合」です。
> 80%以上で ○、50%以上で △、それ未満で × になります。
> △ や × でも生成自体はできることが多いので、「生成UIだけで完結しにくい」という目安として見てください。
> **ポイント**
> チェックは、そのワークフローで使っているノードの情報だけをComfyUIから取得して行います(v0.8.8〜)。
> 以前は最初の1回に数十秒かかることがありましたが、今は一瞬で結果が出ます。
---
## 4-8 付属のサンプルワークフローを使ってみる
Workflow Studioには、すぐに試せるサンプルワークフローが付属しています。
サンプルは、Workflow Studioをインストールしたフォルダの中の **`workflows`** フォルダにあります。
```
ComfyUI/custom_nodes/ComfyUI-Workflow-Studio/workflows/
```
各サンプルには、同じ名前の `.json`(ワークフロー本体)と `.png`(サムネイル画像)が入っています。
### はじめての方におすすめのサンプル
| ファイル名 | 内容 |
| --- | --- |
| `ws_sdxl_t2i_basic` | SDXLモデルで、文章から画像を作る基本のワークフロー |
| `ws_sd15_t2i_basic` | SD1.5モデルで、文章から画像を作る基本のワークフロー |
お手元にあるモデルの種類(SDXLかSD1.5か)に合わせて選んでください。
### サンプルを取り込む
1. ワークフロータブの **「インポート」** を押します。
2. 上記の `workflows` フォルダを開き、使いたいサンプルの `.png` ファイルを選びます。
(`.png` を選ぶと、サムネイル付きで取り込まれます)
3. 一覧に追加されたことを確認します。
ほかにも、ポーズ指定やインペイント(画像の一部の描き直し)など、応用的なサンプルも入っています。
> **注意**
> 一部のサンプルは、追加のカスタムノードが必要です。
> ノード画面で赤く表示されるノードがある場合は、ComfyUI Managerで不足しているノードをインストールしてください(第12章)。
> 4-7のGenerateUIチェックで「変換 ×」と表示されるのも、多くはこのケースです。
次の章では、取り込んだ基本のサンプルを生成UIに読み込んで、いよいよはじめての1枚を生成します。
> **この章のまとめ**
> - ComfyUIで保存したワークフローは、自動でワークフロータブに並ぶ
> - クリックでサイドパネルに表示、ダブルクリックで詳細画面
> - カメラボタンで、ワークフローに見分けやすいサムネイルを付けられる
> - お気に入り・バッジ・グループで分類し、検索・並べ替えで探す。迷ったら「× クリア」
> - 「生成UIに読み込み」「キャンバスへ送る」で、選んだワークフローをすぐ使える
> - GenerateUIチェックで、生成UIで動くかを事前に確認できる
@@ -0,0 +1,255 @@
# 第5章 生成UIタブ - ノードを触らずに画像を作る
いよいよ、Workflow Studioで画像を生成します。
**生成UI(GenerateUI)** タブは、ワークフローの中から「プロンプト」「モデル」「サイズ」など
よく変える項目だけを取り出して、入力フォームとして表示してくれる画面です。
ノードを探して線をたどる必要はありません。
この章の前半(5-1〜5-4)では、第4章で取り込んだ基本のサンプルを使って、はじめての1枚を生成します。
> **事前に準備しておくもの**
> - ComfyUIが起動していること
> - Checkpointモデルが1つ以上、ComfyUIの `models/checkpoints` フォルダに入っていること
> - 第4章で、サンプルワークフロー(`ws_sdxl_t2i_basic` など)を取り込んでいること
---
## 5-1 ワークフローを生成UIに読み込む
1. **ワークフロー** タブを開きます。
2. 使いたいワークフロー(例:`ws_sdxl_t2i_basic`)を **クリック** して選択します。
3. サイドパネルの **GenerateUIチェック**(第4章 4-7)で、「変換」が ○ になっていることを確認します。
4. ツールバーの **「生成UIに読み込み」** を押します。
自動で **生成UI** タブに切り替わり、ワークフローの内容が読み込まれます。
画面上部に、読み込んだワークフローの名前が表示されていれば成功です。
> **ポイント**
> 生成UIの画面は、ボタンや項目名の多くが英語のまま表示されます。
> 本書では、英語の表示名をそのまま使い、必要に応じて意味を添えて説明します。
---
## 5-2 生成UIの画面の見方
生成UIタブは、大きく次の部分に分かれています。
| 番号 | 場所 | 役割 |
| --- | --- | --- |
| ① | 上部のバー | ComfyUIとの接続状態、ワークフロー名、各種ボタン |
| ② | サブタブ | Input/Model/Settings/Feeder/Batch/Lab の切り替え |
| ③ | 編集エリア(中央) | 選んだサブタブの設定項目 |
| ④ | Raw JSON(右側) | ワークフローの中身をJSONで表示(上級者向け) |
| ⑤ | 生成パネル | Generateボタン、進み具合、シード設定、生成結果 |
### ① 上部のバー
| 表示・ボタン | 意味 |
| --- | --- |
| Connected/Disconnected | ComfyUIとつながっているか。緑の **Connected** なら生成できます |
| Connect | ComfyUIに接続し直す |
| Refresh Models | モデルの一覧を読み込み直す(ComfyUIにモデルを追加したあとに押す) |
| Reset Workflow | 編集した内容を捨てて、ワークフローを読み込んだ直後の状態に戻す |
| ワークフロー名 | いま読み込んでいるワークフロー |
> **ポイント**
> 生成UIタブを開くと、ComfyUIへの接続は自動で行われます。
> **Disconnected** と表示されている場合は、ComfyUIが起動しているか確認して **Connect** を押してください。
### ② サブタブ
| サブタブ | 何を設定するか | 本書 |
| --- | --- | --- |
| Input | プロンプト、入力画像 | 5-3 |
| Model | Checkpoint・VAE・LoRAなどのモデル | 5-3 |
| Settings | シード・ステップ数・サンプラー・画像サイズなど | 5-4 |
| Feeder | フォルダの画像を順番に処理する | 5-6 |
| Batch | 条件を変えながらまとめて生成する | 5-5 |
| Lab | 実験的なI2Iバッチ生成(上級) | 5-7 |
まずは **Input・Model・Settings** の3つだけ覚えれば十分です。
### ④ Raw JSON
Input・Model・Settingsの右側には、ワークフローの中身がJSON形式で表示されます。
フォームで値を変えると、ここにも反映されます。
慣れないうちは、ここは「見るだけ」にしておきましょう。
Inputサブタブでは、Raw JSONの上端にある **»** ボタンで、Raw JSONを右端の細い帯に折りたためます(v0.8.12〜)。
プロンプトや画像の欄が広くなります。もう一度押すと元に戻り、折りたたんだ状態は次回も記憶されます。
---
## 5-3 はじめての1枚を生成する
いよいよ生成です。最低限、次の3つを確認・設定します。
1. どのモデルで描くか(Model)
2. 何を描くか(Input)
3. 生成する(Generate)
### 手順1:モデルを選ぶ(Model)
1. サブタブの **Model** を開きます。
2. **Checkpoint** の欄で、手元にあるモデルを選びます。
一覧が長い場合は、欄の上の入力欄に文字を入れると絞り込めます。
3. 欄の下にある **Apply** ボタンを押します。
> **ポイント:モデルが見つからないとき**
> ワークフローに保存されているモデルが、あなたのパソコンに入っていないことがあります。
> 配布されているワークフローを使うときは、ほぼ必ず起こります。次のどちらかの形で知らせてくれます。
> - 欄に「△ (モデル名)(見つかりません)」と表示される
> - 読み込んだときに「このワークフローのCheckpoint「…」が見つからず、無警告で別のモデルに差し替えられました」と表示される
> (この場合、手元のモデルの一覧の先頭のものに自動で差し替わっています)
>
> なお v0.8.7〜は、フォルダ名が違うだけで同じファイル名のモデルが手元に1つだけあれば、先頭のものではなく、そのモデルに自動で差し替わります。
> (公式テンプレートはフォルダ名なしでモデル名を保存していることが多く、以前は無関係なモデルに差し替わることがありました)
>
> どちらの場合も、一覧から使いたいモデルを選び直して、Apply を押してください。
> 毎回選び直すのが面倒なときは、設定タブの **デフォルトCheckpoint**(第11章)が便利です。
> **注意:Applyを押し忘れない**
> 生成UIでは、モデルやSettings(5-4)の値を変えたあと **Apply** ボタンを押して、はじめてワークフローに反映されます。
> Applyを押さずにGenerateすると、**変更前の値のまま** 生成されてしまいます。
> 「設定を変えたのに結果が変わらない」ときは、まずApplyの押し忘れを疑いましょう。
> (プロンプトの入力欄だけは、Generateを押したときに自動で反映されます)
> **ポイント**
> 項目名が明るい緑色で表示されているのは、読み込んだワークフローにそのモデルのノードが含まれている項目です。
> 緑色でない項目は、変更してもこのワークフローには影響しません。
### 手順2:プロンプトを書く(Input)
1. サブタブの **Input** を開きます。
左側にプロンプト(Prompt)の欄、右側に入力画像(Image)の欄が並んでいます(v0.8.12〜。それまではPrompt/Imageを切り替える形でした)。
画面の幅が狭いときは、Imageの欄はプロンプトの下に回ります。
2. **Positive Prompt** の欄に、描いてほしい内容を英語で入力します。
```
a cute cat sitting on a windowsill, sunlight, soft colors, high quality
```
3. **Positive Prompt** の **Apply** を押します。
4. 必要に応じて **Negative Prompt** の欄に、描いてほしくない内容を入力し、こちらも **Apply** を押します。
> プロンプトは、Applyを押し忘れてもGenerateを押したときに自動で反映されます。
> ただし、Raw JSON(右側)で反映結果を確かめられるので、Applyを押す習慣をつけておくと安心です。
```
low quality, blurry, bad anatomy
```
> **ポイント**
> Apply の横にある **↺(戻す)** ボタンを押すと、入力欄の内容を、いまワークフローに反映されている値に戻せます。
> 書き換えに失敗したときに便利です。
> **ポイント**
> 英語が苦手な方は、第7章・第10章で紹介するAI翻訳機能を使うと、日本語で考えたプロンプトを英語にできます。
### 手順3:生成する(Generate)
1. 画面の生成パネルにある **Generate** ボタンを押します。
2. 進み具合を表すバーが伸びていき、パーセントが表示されます。
3. 「Done」と表示されると、生成された画像が生成パネルに表示されます。
おめでとうございます。これで、Workflow Studioではじめての1枚が完成しました。
生成した画像は、ComfyUIで生成した場合と同じく、ComfyUIの `output` フォルダに保存されます。
あとから見返したいときは、**ギャラリー** タブ(第8章)で一覧できます。
> **ポイント:途中で止めたいとき**
> 生成中は、Generateボタンの横に **Stop** ボタンが表示されます。押すと生成を中断します。
> **ポイント:ApplyとGenerateを一度に**
> **Alt キーを押しながら Apply** をクリックすると、値の反映と生成を一度に行えます。
> 慣れてきたら、プロンプトを書き換えて Alt+Apply、という流れで手早く試せます。
### 生成したワークフローを保存する
生成UIで変えた設定を、あとでまた使いたいときは、サブタブの右端にある **Save** ボタンを押します。
名前を付けて保存すると、ワークフロータブに新しいワークフローとして追加されます。
---
## 5-4 シード・サンプラー・ステップ数の意味
1枚目が生成できたら、次は **Settings** サブタブで、画像の仕上がりを左右する設定を見てみましょう。
Settingsタブは、3つの列に分かれています。
- **KSampler(左)** - 画像をどのように描くかの設定
- **Latent Image(中央)** - 画像の大きさと枚数の設定
- **スイッチ(右、v0.9.6〜)** - ワークフローの中のオン・オフの切り替え
ここでも、KSamplerとLatent Imageは、値を変えたら各ブロックの **Apply** を押すのを忘れないようにしましょう(スイッチの列はApply不要です)。
### KSampler の項目
| 項目 | 意味 | 初心者向けの目安 |
| --- | --- | --- |
| Seed(シード) | 画像のもとになる「乱数の種」。同じ設定・同じシードなら、ほぼ同じ画像になる | 生成時は生成パネルのシード設定(下記)が優先されるので、ここは「読み込んだワークフローのシードの確認用」と考える。ワークフローのシードをそのまま使いたいときは、生成パネルのモードを **Workflow** にする |
| Steps(ステップ数) | 描き込みを何回くり返すか。多いほど丁寧だが時間がかかる | 20〜30 |
| CFG | プロンプトにどれだけ忠実に従うか。高すぎると色や形が崩れやすい | SDXLなら5〜8程度 |
| Sampler(サンプラー) | 描き方のアルゴリズム | `euler`、`dpmpp_2m` など |
| Scheduler(スケジューラー) | ステップごとの描き込み量の配分 | `normal`、`karras` など |
| Denoise | 元の画像をどれだけ描き変えるか。文章から作る場合は 1.0 | 1.0(画像から作る場合は 0.4〜0.7 など) |
> **ポイント**
> 目安の値はモデルによって異なります。モデルの配布ページ(Civitaiなど)に推奨値が書かれていることが多いので、確認してみましょう。
> 迷ったら、サンプルワークフローに最初から入っている値のまま使えば大丈夫です。
### Latent Image の項目
| 項目 | 意味 | 目安 |
| --- | --- | --- |
| Width(幅) | 画像の横の大きさ(ピクセル) | SDXLなら1024前後、SD1.5なら512前後 |
| Height(高さ) | 画像の縦の大きさ(ピクセル) | 同上 |
| Batch Size | 1回の生成で何枚作るか | 1〜4(多いほどメモリを使う) |
> **注意**
> モデルが学習したサイズから大きく外れると、人物が二重になったり、構図が崩れたりしやすくなります。
> 縦長・横長にしたいときも、合計のピクセル数がおおよそ同じになるように調整しましょう(例:SDXLなら 832×1216 など)。
### スイッチの項目(v0.9.6〜)
ワークフローの中にある「オン・オフ」の設定が、ノード名と項目名つきで一覧になります。
チェックボックスをクリックすると、その場で切り替わります。**Apply を押す必要はありません**。切り替えた値は右側の Raw JSON にもすぐ反映されます。
一覧に出るのは、次の2種類です。
- 値が true/false の項目 - プロンプト強化(PE)のオン・オフを切り替えるスイッチや「Boolean」ノード、TextGenerate の thinking など
- 選択肢が2つだけのオン・オフの項目 - enable/disable、on/off、yes/no など。KSamplerAdvanced の add_noise、DWPreprocessor の detect_hand(手を検出するか)など
各行の右端には、いまの値(true/false、enable/disable など)が表示されます。
行にマウスを乗せると、ノードの番号と種類を確認できます。
> **ポイント**
> 別のノードから線でつながっている項目は、一覧に出ません。代わりに、つながっている元のノード(たとえば「Enable PE?」という名前の Boolean ノード)が出るので、そちらを切り替えてください。
> ノードに名前を付けていないワークフローでは、ノードの種類名(ComfySwitchNode など)で表示されます。
> オン・オフの項目が1つも無いワークフローでは「このワークフローに true/false の項目はありません」と表示されます。
### シードの管理(生成パネル)
シードは、生成パネルの **Seed** 欄と、その下のプルダウンで管理します。
| モード | 動き | 使いどころ |
| --- | --- | --- |
| Random | 毎回ランダムなシードで生成 | いろいろな絵を試したいとき |
| Fixed | Seed欄の値で固定 | 気に入った絵をもとに、プロンプトだけ変えて比べたいとき |
| Increment | 生成するたびに1ずつ増やす | 少しずつ違う絵を順番に作りたいとき |
| Decrement | 生成するたびに1ずつ減らす | Incrementの逆 |
| Workflow | ワークフローに入っているシードを、上書きせずそのまま使う | ギャラリーから読み込んだ画像や、配布されたワークフローを、元のシードで再現したいとき |
生成が終わると、Seed欄には **実際に使われたシード値** が表示されます。
> **ポイント:Workflowモード(v0.8.3〜)**
> モードを **Workflow** にすると、Seed欄の値やランダムではなく、読み込んだワークフロー自身のシードで生成します。
> Randomで何枚か試したあとでも、ワークフロー本来のシード(読み込んだ時点の値)に戻って生成されます。
> ワークフローにシードの入力が見つからない場合は、Seed欄の値(−1ならランダム)が使われます。
> 同じシードと同じ設定で続けて2回押すと、ComfyUIは計算済みの結果を再利用するため、新しい画像は増えません(故障ではありません)。
### 気に入った絵を「育てる」流れ
シードを使うと、気に入った1枚をもとに少しずつ改良できます。
1. **Random** で何枚か生成し、気に入った構図の絵を探す
2. 気に入った絵が出たら、モードを **Fixed** に切り替える(Seed欄にはその絵のシードが入っています)
3. プロンプトに言葉を足したり、Stepsを変えたりして、違いを比べる
シードを固定すると、「何を変えたら、どう変わったか」が分かりやすくなります。
これは、画像生成に慣れるためのいちばんの近道です。
> **この章(前半)のまとめ**
> - ワークフロータブで選んで「生成UIに読み込み」を押すと、生成UIで編集できる
> - 基本は Input(プロンプト)・Model(モデル)・Settings(細かい設定)の3つのサブタブ
> - モデルやSettingsの値を変えたら必ず **Apply**。押し忘れると変更前の値で生成される(プロンプトとSettingsのスイッチ列は自動で反映)
> - Generate で生成。Alt+Apply なら反映と生成を一度に行える
> - シードを Fixed にすると、気に入った絵をもとに条件を変えて比べられる。ワークフローのシードをそのまま使うなら Workflow
@@ -0,0 +1,243 @@
## 5-5 バッチ生成 - 条件を変えながらまとめて試す
画像生成に慣れてくると、こんなことを試したくなります。
- 同じプロンプトで、手持ちのモデルを全部試して比べたい
- いくつかのプロンプトを、順番に生成したい
- どのサンプラーがこのモデルに合うのか、まとめて確かめたい
1枚ずつ設定を変えてGenerateを押すのは大変です。
**Batch** サブタブを使うと、「何を変えながら生成するか」をまとめて指定し、自動で順番に生成できます。
### バッチで変えられるもの
バッチでは、次の7種類のうち **1種類** を選んで、順番に変えながら生成します。
| 種類 | 何が変わるか | 使いどころ |
| --- | --- | --- |
| Checkpoint | モデル | モデルの描き味を比べる |
| Lora | LoRA | LoRAの効果を比べる |
| Prompt | プロンプト | 保存したプロンプトを順番に試す |
| Workflow | ワークフロー | 複数のワークフローを順番に実行する |
| Sampler | サンプラー | サンプラーの違いを比べる |
| Scheduler | スケジューラー | スケジューラーの違いを比べる |
| Style | スタイル | スタイルの違いを比べる |
> **ポイント**
> 一度に変えられるのは1種類だけです。
> 「モデルもプロンプトも同時に変えたい」という場合は、5-7の Lab を使います。
### Batchタブの画面
Batchタブは、左・中央・右の3つの部分に分かれています。
| 場所 | 役割 |
| --- | --- |
| 左(リスト) | Checkpoints/Sampler/Scheduler/Style を一覧から選ぶ |
| 中央(グループ) | Checkpoint/Lora/Prompt/Workflow を「グループ」単位で選ぶ |
| 右(Batch Queue) | 選んだ項目の一覧。実行する種類をここで決める |
中央のグループは、それぞれのタブで作ったグループと連動しています。
- Checkpoint・Lora のグループ → モデルタブ(第6章)
- Prompt のグループ → プロンプトタブ(第7章)
- Workflow のグループ → ワークフロータブ(第4章)
### 例:手持ちのモデルを比べる
同じプロンプト・同じシードで、3つのモデルの描き味を比べてみましょう。
1. 5-3の手順で、比べたいプロンプトを入れたワークフローを読み込んでおきます。
2. 生成パネルのシードのモードを **Fixed** にします。
(シードを固定すると、モデル以外の条件がそろい、違いが分かりやすくなります)
3. **Batch** サブタブを開きます。
4. 左側の **Checkpoints** タブで、比べたいモデルにチェックを入れます。
5. 右側の **Batch Queue** で、**Checkpoint** の列の見出しにあるチェックボックスをオンにします。
列には、選んだモデルの一覧と件数が表示されます。
6. **Generate** を押します。
選んだモデルで順番に生成が行われます。
生成パネルの **Batch** 欄に、いま何番目を生成しているか、全体の進み具合が表示されます。
- **Pause** - 一時停止します。もう一度押すと再開します。
- **Stop** - バッチ全体を中止します。
> **注意**
> Batch Queueの列のチェックがオンのままだと、次にGenerateを押したときもバッチ生成になります。
> 1枚だけ生成したいときは、列のチェックをオフにしてください。
> **ポイント:LoRAのバッチ**
> Loraのバッチでは、選んだLoRAの書式(`<lora:名前:強さ>`)と、Civitaiから取得したトリガーワードが、
> 自動でPositive Promptに追加されます。バッチが終わると、元のプロンプトに戻ります。
---
## 5-6 Feeder - フォルダの画像を順番に処理する
**Feeder** は、たくさんの画像を1枚ずつワークフローに送り込んで、順番に処理する機能です。
たとえば、次のような使い方ができます。
- 撮りためた写真を、まとめてイラスト風に変換する(I2I:画像から画像を作る)
- 複数の画像からポーズを読み取って、同じポーズのキャラクターを描く
Feederには2つのモードがあり、Feederタブ左上の **[Image Loop]/[Gallery]** ボタンで切り替えます。
| モード | 画像の取り出し元 | 必要なもの |
| --- | --- | --- |
| Image Loop | パソコン内のフォルダ | カスタムノード **comfyui-image-feeder**(別途インストール) |
| Gallery | ギャラリータブのグループ | 追加インストール不要(Workflow Studioに付属の **Gallery Feeder (WFS)** ノード) |
> **ポイント**
> どちらのモードも、**ワークフローの中に専用のノード** が入っている必要があります。
> 普通のワークフローを読み込んだだけでは使えません。
### Galleryモードの使い方(追加インストール不要)
1. ComfyUIのノード画面で、ワークフローの「画像を読み込む」部分に **Gallery Feeder (WFS)** ノードを置き、
画像の出力をつなぎます。保存して、生成UIに読み込みます。
2. **ギャラリー** タブ(第8章)で、処理したい画像をグループにまとめておきます。
(Feederタブを初めて開くと、`__Feeder__` というグループが自動で作られます。これを使ってもかまいません)
3. 生成UIの **Feeder** サブタブを開き、**[Gallery]** を選びます。
4. 対象の **Gallery Feeder Node** と、画像を取り出す **グループ** を選び、**Apply** を押します。
5. 中央にグループの画像が並びます。画像をクリックすると、その画像から処理を始められます。
6. **After gen**(生成後の動き)を選び、**▶ Run** を押します。
### After gen(生成後の動き)
| モード | 動き |
| --- | --- |
| Loop | 次の画像へ進み、最後まで行ったら最初に戻って続ける(■ Stopを押すまで止まらない) |
| Increment | 次の画像へ進み、最後の画像を処理したら自動で止まる |
| Fixed | 同じ画像を使い続ける(シードだけ変えて何枚も作りたいとき) |
はじめは **Increment** を選ぶと、全部の画像を1回ずつ処理して自動で止まるので安心です。
### Image Loopモード
Image Loopモードでは、パソコン内のフォルダ(`ComfyUI/user/default/image-loop-data/`)の画像を使います。
フォルダの画像を選んで、プリセットとして保存しておくこともできます。
別途 **comfyui-image-feeder** カスタムノードが必要です。
付属のサンプル `ws_sdxl_image_feeder_i2i`(I2I)と `ws_dwpose_image_feeder`(ポーズ読み取り)は、このImage Loopモード用です。
### Galleryモードのサンプル(v0.8.3〜)
Galleryモードには、付属のサンプル `ws_sdxl_gallery_feeder_i2i`(SDXLのI2I)が使えます。
ノードを自分で置く必要がなく、**Gallery Feeder (WFS)** ノードがあらかじめ入っています(グループ名は `__Feeder__`、並び順は filename_asc)。
1. ワークフロータブでサンプルを取り込み、**生成UIに読み込み** を押します。
2. ギャラリーで、処理したい画像のサムネイル左上の **F** ボタンを押して `__Feeder__` グループに入れます。
3. **Feeder** サブタブで **[Gallery]** を選び、Gallery Feeder Node とグループを選んで **Apply**。
4. Model サブタブで、手元のCheckpointとVAEを選び直して **Apply**(サンプルのモデル名は作者の環境のものです)。
5. **After gen** を **Increment** にして **▶ Run** を押します。
---
## 5-7 Lab - 実験的なI2Iバッチ生成(上級)
**Lab** は、5-5のバッチ生成をさらに発展させた、上級者向けの実験的な機能です。
はじめのうちは読み飛ばしてもかまいません。
### Batchとの違い
| | Batch(5-5) | Lab |
| --- | --- | --- |
| 一度に変えられるもの | 1種類だけ | モデル・プロンプト・KSamplerを **それぞれ別々に** |
| 変えるタイミング | 毎回 | 「何回目から変えるか」を自由に指定 |
| 生成した画像を次の入力に | できない | できる(連鎖させられる) |
### キーフレームという考え方
Labでは、「何回目の生成から、どの設定に切り替えるか」を **キーフレーム** として指定します。
例:10回生成する場合
| 回 | モデル | プロンプト | KSampler |
| --- | --- | --- | --- |
| 1〜10 | モデルA | - | - |
| 1〜4 | - | 「朝の街」 | - |
| 5〜10 | - | 「夜の街」 | - |
| 1〜7 | - | - | Steps 20 |
| 8〜10 | - | - | Steps 40 |
このように、列ごとに切り替えのタイミングを別々に決められます。
次のキーフレームが来るまでは、前の設定が引き継がれます。
### 基本の流れ
1. 生成UIにワークフローを読み込みます。
2. **Lab** サブタブを開きます。
3. I2I(画像から画像)の場合は、元になる画像をドロップします。
文章から作る(T2I)場合は、**T2I workflow (no source image)** にチェックを入れます。
4. 生成回数(Batch)を決めます。
5. Model・Prompt・KSampler の各列で **+** を押してキーフレームを追加し、
キーフレームをダブルクリックして、「何回目から」「どの設定に」するかを入力します。
KSampler列のセルには、指定した項目(Step・CFG・Denoise・サンプラー・スケジューラー・Seed)が表示されます。
何も指定していないセルは「(ワークフローの設定のまま)」と表示されます。
6. **Run** を押します。
結果は **Results** パネルに一覧表示されます。
### 便利な機能
- **Use generated image for next** - 生成した画像を、次の回の入力画像として使います。
少しずつ変化していく連続画像を作れます。
- **Plan Save** - 設定(プラン)を保存できます。保存すると、結果をまとめた一覧画像も作られ、
その画像をドロップすれば同じプランを読み込み直せます。
- **物語からプランを作る**(v0.9.5〜) - AI TOOLのChatで **Storyboard to Lab** スキルを使うと、
物語をシーンごとのプロンプトに分けたプランを作り、**Labで開く** ボタンでここに読み込めます(第10章 10-5)。
---
## 5-8 パートナーAPI(Nano Banana)を使う
ComfyUIには、Googleの画像生成モデル **Nano Banana**(Gemini画像ノード)のように、
クラウドのサービスに生成を任せる **パートナーAPIノード** があります。
v0.8.6〜は、こうしたノードを含むワークフローも生成UIで実行できます。
> **注意**
> パートナーAPIノードは、自分のパソコンのGPUではなくクラウドで動き、**Comfyクレジット(有料)を消費** します。
> BatchやLabなどで繰り返し実行すると、回数ぶんのクレジットが続けて消費されます。
### 準備:Comfy APIキーを設定する
1. platform.comfy.org でComfy APIキーを発行します。
2. プラグインのフォルダにある `.env` に、`COMFY_API_KEY=発行したキー` という行を追加します。
(`.env` がなければ `.env.example` をコピーして作ります)
3. ComfyUIを再起動します。`.env` は起動時にしか読み込まれません。
キーはサーバー側でだけ使われ、ブラウザには送られません。
キーを設定していない状態で実行すると、「パートナーAPIノードの認証に失敗しました」という案内が出ます。
### 使い方
1. ワークフロータブで、Nano Bananaのサンプル(`ws_nano_banana_2_1_t2i`)を生成UIに読み込みます。
2. **Input** サブタブの左側(Prompt)でプロンプトを編集し、**Apply** を押します。
3. 参考画像を使うときは、**Input** サブタブの右側(Image)の画像カードで指定します。
4. **Generate** を押します。
> **ポイント**
> ComfyUI本体の画面でログインして実行する方法もあります。その場合は `.env` の設定は要りません。
---
## 5-9 対応ワークフローが増えました(v0.8.4〜)
生成UIで使えるワークフローが、次のように増えています。使い方は、ほかのワークフローと同じです。
| 対応したもの | 内容 |
| --- | --- |
| Qwen Image 2.1 Turbo | ComfyUI公式テンプレートの文章から画像(T2I)・画像から画像(I2I)。プロンプトエンハンサー(プロンプトを自動で補う機能)の入力欄にも、編集が反映される |
| Flux.3 Klein | 公式テンプレート4種(KleinとKlein SDのT2IとImage Edit) |
| MOSS-Image | Base/SFTのT2Iと、枠で位置を指定するバウンディングボックス版 |
| Fibo 2.0 Edit(v0.8.14〜) | 画像編集のワークフロー。プロンプト強化(PE)を使う構成でも、プロンプト欄の編集がPEのオン・オフどちらにも反映される。ネガティブの欄にはVGL形式(JSON)の説明文がそのまま入る。幅・高さ(Settings)は0で自動 |
| FastH3 OmniRef/Pika-H3-Flex(v0.8.14〜) | MiniMax H3の「参照画像から動画」(Reference to Video)。動画のワークフローとして扱われ、Videoタブ(第11章)でも使える |
| ネイティブLoRAスタック | ComfyUI v0.40のLoRAをまとめて読み込むノード(LoadLoraModel/LoadLoraTextEncoder)。Apply・バイパス・Metadata表示に対応 |
| P-H3 Image(v0.9.4〜) | 画像編集とT2Iのテンプレート。プロンプトはInputタブに表示され、Metadataタブでも読める。ネガティブの欄は無い。幅・高さはワークフローのつなぎ方に従い、プロンプトを補う機能(プロンプトアップサンプラー)もワークフローの設定どおりに動く |
| MiniMax H3 Image(v0.9.6〜) | ComfyUI公式の画像編集とT2Iのテンプレート。P-H3 Imageと同じく、プロンプトはInputタブに表示され、MetadataタブとサイドパネルのIタブでも読める。ネガティブの欄は無い。プロンプト強化(PE)のオン・オフは、Settingsタブのスイッチの列(5-4)で切り替えられる |
> **注意**
> ネイティブLoRAスタックのノードは、保存形式が **API形式のワークフローのみ** 対応です。
> ComfyUI本体の画面がこのノードの行追加にまだ対応していないためで、UI形式のワークフローは対象外です。
> **ポイント**
> 2枚の画像をスライダーで見比べる **Compare Images**(ImageCompare)ノードを含むワークフローは、以前は生成UIから実行すると、このノードだけエラーになっていました(v0.9.6で修正)。
> **ポイント**
> 画像のメタデータを見るMetadataタブとサイドパネルの **I** タブでも、Nano Bananaで作った画像を読めます(v0.8.5〜)。
> プロンプトは、Positive/Negativeに分けずに全文が1件で表示されます。
> **この章(後半)のまとめ**
> - Batchサブタブで、モデル・プロンプト・サンプラーなどを1種類ずつ変えながらまとめて生成できる
> - Batch Queueの列のチェックで種類を選び、Generateで開始。終わったらチェックを外す
> - Feederは、たくさんの画像を1枚ずつワークフローに送り込む機能。専用ノードが必要
> - Labは、複数の設定を別々のタイミングで切り替えられる上級者向けの機能
> - パートナーAPIノード(Nano Bananaなど)は、`.env` の `COMFY_API_KEY` を設定すれば実行できる。クレジットが消費される
@@ -0,0 +1,184 @@
# 第6章 モデルタブ - モデルとLoRAを管理する
画像生成を続けていると、モデルやLoRAのファイルはどんどん増えていきます。
「このファイル、何のモデルだっけ?」「あのLoRAのトリガーワードは何だった?」
- そんな悩みを解決するのが **モデル(Models)** タブです。
---
## 6-1 モデルの種類
モデルタブの上部には、モデルの種類ごとのタブが並んでいます。
それぞれ、ComfyUIの `models` フォルダの中の対応するフォルダに入っているファイルが表示されます。
| 種類 | ひとことで言うと | 主な置き場所 |
| --- | --- | --- |
| Checkpoint | 画像生成の本体。これがないと始まらない | `models/checkpoints` |
| LoRA | 絵柄・キャラクター・服装などを追加で覚えさせる小さなモデル | `models/loras` |
| VAE | 画像の色合いや細部を仕上げる。Checkpointに内蔵されていることも多い | `models/vae` |
| ControlNet | ポーズや線画など、構図を指定するためのモデル | `models/controlnet` |
| UNET | FLUXなど新しい形式のモデルの本体(Diffusion Model) | `models/diffusion_models` など |
| TextEncoder | プロンプトの文章をAIが理解できる形に変換するモデル | `models/text_encoders` など |
| Hypernetwork | 絵柄を調整する古い形式の追加モデル | `models/hypernetworks` |
| Embedding | 特定の単語に意味を覚えさせた小さなファイル(ネガティブ用が多い) | `models/embeddings` |
> **ポイント**
> はじめは **Checkpoint** と **LoRA** の2つを使いこなせれば十分です。
> FLUXなどの新しいモデルを使うようになると、UNETやTextEncoderの出番が増えてきます。
---
## 6-2 一覧の表示と探し方
### 最初の読み込み
モデルタブを初めて開いたときは、一覧が空で「更新ボタンでモデルを読み込み...」と表示されます。
ツールバーの **「更新」** を押すと、モデルの一覧が表示されます。
新しくモデルを追加したときも、「更新」を押してください。
### サムネイル表示とテーブル表示
ワークフロータブと同じく、**サムネイル** と **テーブル** を切り替えられます。
1ページに24件ずつ表示されます。
テーブル表示では、列の見出しをクリックすると、その列で並べ替えができます
(1回目:昇順 ▲、2回目:降順 ▼、3回目:並べ替え解除)。
> **ポイント**
> サムネイルは、モデルファイルと同じ場所にある `(モデル名).preview.png` が自動で使われます。
> 画像がないモデルも、6-3の方法でCivitaiから取得したり、自分で設定したりできます。
### 検索と絞り込み
ツールバーには、絞り込みのための項目が並んでいます。
| 項目 | 絞り込み内容 |
| --- | --- |
| モデルを検索... | 名前などのキーワード |
| ★ | お気に入りだけ |
| すべてのタグ | タグ |
| All Folders | サブフォルダ |
| All Groups | グループ |
| All/Enabled only/Disabled only | 有効・無効の状態(6-5) |
絞り込みすぎて分からなくなったら、ツールバー右端の **「× クリア」** ですべて解除できます。
### お気に入り・バッジ・グループ
分類のしかたは、ワークフロータブ(第4章 4-4)とほぼ同じです。
- **お気に入り(★)** - よく使うモデルに印を付ける
- **バッジ** - **「Badge」** で作った色付きラベルを付ける。バッジの種類と色はワークフロータブと共通です
- **グループ** - モデルをクリックし、サイドパネルの **「グループ」** タブで追加する
> **ポイント**
> グループは **モデルの種類ごと** に分かれています。
> Checkpointのグループは、Checkpointタブにだけ表示されます。
> ここで作ったグループは、生成UIのバッチ生成(第5章 5-5)でも使えます。
---
## 6-3 Civitaiの情報を取り込む
**Civitai(シビタイ)** は、画像生成モデルが多数公開されている共有サイトです。
ネットから入手したモデルの多くは、Civitaiで配布されています。
Workflow Studioは、手元のモデルファイルから **ハッシュ値**(ファイルの指紋のような値)を計算し、
Civitaiに登録されている情報を自動で探し出せます。
### 取得できる情報
- モデルの名前・種類(Checkpoint/LoRAなど)
- **Base Model**(SDXL、SD1.5、FLUX など、どの系統のモデルか)
- **トリガーワード**(LoRAの効果を出すために、プロンプトに入れる言葉)
- タグ・説明文
- サンプル画像(サムネイルにも使われます)
### 1つずつ取得する
1. モデルをクリックして選択します。
2. 右側のサイドパネルで **「CivitAI」** タブを開きます(モデルを選ぶと最初にこのタブが開きます)。
3. **「CivitAIから取得」** を押します。
取得が終わると、モデルの情報が表示されます。
**Sample** タブでは、Civitaiに投稿されたサンプル画像を見られます。
### まとめて取得する
ツールバーの **「CivitAI」** ボタンを押すと、表示中の種類のモデルについて、まとめて情報を取得します。
すでに情報を取得済みのモデルは飛ばされるので、モデルを追加したあとに何度押しても大丈夫です。
サムネイル画像がないモデルには、プレビュー画像も自動で保存されます。
> **ポイント**
> ハッシュ値の計算には、モデルの大きさに応じて時間がかかります。
> 数GBのモデルがたくさんある場合は、時間に余裕があるときに実行しましょう。
> **ポイント**
> Civitaiで見つからなかったモデルは「確認済みですが、このモデルはCivitAIに見つかりませんでした。」と表示されます。
> 自分で作ったモデルや、Civitai以外で配布されているモデルは、見つからないのが普通です。
> **「未登録はスキップ」** にチェックを入れておくと、まとめて取得するときに、見つからなかったモデルを再確認せずに飛ばします。
> **ポイント:APIキー**
> 設定タブの **「CivitAI APIキー」** は、入力しなくても使えます(省略可)。
> 一部の情報の取得に認証が必要な場合に、Civitaiのアカウント設定で発行したキーを入力します。
### LoRAを使うときに特に便利
LoRAは、**トリガーワード** をプロンプトに入れないと効果が出ないものが多くあります。
Civitaiの情報を取得しておけば、サイドパネルでトリガーワードをすぐに確認できます。
また、生成UIのバッチ生成でLoRAを使うと、トリガーワードが自動でプロンプトに追加されます(第5章 5-5)。
---
## 6-4 モデルを生成やキャンバスで使う
### 生成UIにセットする(GenUI Model)
モデルタブで選んだモデルを、そのまま生成UIのワークフローにセットできます。
1. 生成UIに、ワークフローを読み込んでおきます(第5章 5-1)。
2. モデルタブで、使いたいモデルを選択します。
3. **「GenUI Model」** ボタンを押します。
これで、生成UIのワークフローのモデルが、選んだモデルに差し替わります。
Civitaiのサンプル画像を見比べながら、気になったモデルをすぐ試せます。
> **ポイント**
> Embeddingの場合は、**GenUI PP**(ポジティブプロンプトに追加)と **GenUI NP**(ネガティブプロンプトに追加)のボタンが表示されます。
> ネガティブ用のEmbeddingは、GenUI NP で追加しましょう。
> **注意**
> 生成UIのワークフローに、そのモデルを読み込むノードがない場合は使えません。
> たとえば、LoRAのノードがないワークフローに、LoRAはセットできません。
### ComfyUIのノード画面で使う
ComfyUIのノード画面の横にある **WS Library** の **M(Models)** タブからモデルをキャンバスへドラッグすると、
そのモデルを読み込むノードが自動で置かれます(第9章)。
---
## 6-5 使わないモデルを片付ける
### 有効・無効の切り替え
しばらく使わないモデルは、削除せずに **無効(Disable)** にしておけます。
無効にしたモデルは、ComfyUIのモデル選択の一覧に表示されなくなるので、一覧がすっきりします。
- カードの **一時停止(⏸)** ボタンで、1つずつ有効・無効を切り替えられます。
- グループ単位で、まとめて有効・無効にすることもできます。
> **ポイント**
> 無効にすると、ファイルの名前の末尾に `.disabled` が付きます。ファイル自体は消えません。
> 有効に戻すと、元の名前に戻ります。
### まとめて操作する(Select)
緑色の **Select** ボタンを押すと、複数のモデルにチェックを入れて、まとめて操作できます。
- グループへの追加・削除
- バッジの追加・削除
- 別のサブフォルダへの移動(プレビュー画像などの関連ファイルも一緒に移動)
- 削除
### 削除
詳細画面やSelectから、モデルを削除できます。
> **注意**
> 削除すると、モデルファイルと、関連するファイル(プレビュー画像、Civitai情報など)が **完全に削除** されます。
> モデルは再ダウンロードに時間がかかるものが多いので、迷ったら削除ではなく「無効」にしておきましょう。
> **この章のまとめ**
> - モデルタブでは、ComfyUIのモデルを種類ごとに一覧できる。最初に「更新」を押す
> - お気に入り・バッジ・グループで分類できる。グループはバッチ生成でも使える
> - 「CivitAIから取得」で、Base Model・トリガーワード・サンプル画像などを自動で取り込める
> - 「GenUI Model」で、選んだモデルをすぐ生成UIで試せる
> - 使わないモデルは削除せず「無効」にしておくと安心
@@ -0,0 +1,254 @@
# 第7章 プロンプトタブ - 呪文を貯めて、育てる
うまくいったプロンプトは、あなただけの財産です。
でも、メモ帳に貼り付けておくだけでは、どれが何の呪文だったか分からなくなりがちです。
**プロンプト(Prompt)** タブは、プロンプトを **保存・分類・再利用** するための場所です。
さらに、ローカルAIを使った翻訳や、プロンプトを毎回少しずつ変える「ワイルドカード」も使えます。
---
## 7-1 プロンプトの基本
プロンプトタブの使い方に入る前に、プロンプトの書き方の基本を押さえておきましょう。
### ポジティブとネガティブ
| 種類 | 書く内容 | 例 |
| --- | --- | --- |
| Positive Prompt(ポジティブ) | 描いてほしいもの | `1girl, smiling, school uniform, cherry blossoms, spring` |
| Negative Prompt(ネガティブ) | 描いてほしくないもの | `low quality, blurry, extra fingers, watermark` |
### 書き方のコツ
- **英語で書く** - 多くのモデルは英語で学習されています。
- **カンマ(,)で区切る** - 単語や短いフレーズを、カンマで区切って並べるのが一般的です。
- **大事なものを前に** - 前に書いた言葉ほど、強く反映されやすい傾向があります。
- **強調したい言葉は (言葉:数値)** - `(red hair:1.3)` のように書くと、その言葉を強調できます(1.0が標準)。
> **ポイント:強調の数値をキーボードで変える**
> 生成UIのプロンプト入力欄で、強調したい言葉を選んで **Ctrl+↑/↓** を押すと、
> `(言葉:数値)` の数値を0.05ずつ増減できます。数値が1.0に戻ると、カッコは自動で外れます。
> **ポイント**
> SDXLのアニメ系モデルは「タグを並べる」書き方、FLUXなどの新しいモデルは「普通の英文で説明する」書き方が向いています。
> モデルの配布ページに、おすすめの書き方が載っていることも多いので確認してみましょう。
---
## 7-2 プロンプトタブの画面
プロンプトタブは、左・中央・右の3つの列に分かれています。
| 列 | 名前 | 役割 |
| --- | --- | --- |
| 左 | AI Assistant | ローカルAIとのチャット・翻訳(7-5) |
| 中央 | Presets/Preset Manager | プロンプトの保存・一覧・分類(7-3) |
| 右 | Wildcard/Style | ワイルドカードとスタイルの管理(7-4) |
画面上部の **Form/Table** で、表示方法を切り替えられます。
- **Form** - 1件ずつ編集する画面(この章で説明する画面です)
- **Table** - プリセットやワイルドカードを表形式でまとめて編集する画面(件数が増えてきたら便利)
---
## 7-3 プリセットとして保存・分類する
**プリセット** は、ポジティブ・ネガティブのプロンプトをひと組にして、名前を付けて保存したものです。
### プリセットを保存する
1. 中央の列で **Presets** タブを開きます。
2. 一番上のプルダウンが **New Preset (none selected)** になっていることを確認します。
3. 次の項目を入力します。
| 項目 | 内容 |
| --- | --- |
| Preset Name | プリセットの名前(例:`桜と制服`) |
| Category | 分類(例:`Portrait`、`Landscape`)。省略してもかまいません |
| Positive Prompt | 描いてほしいもの |
| Negative Prompt | 描いてほしくないもの |
4. **Save** を押します。「プリセットを保存しました」と表示されれば完了です。
> **ポイント**
> 生成UIで試して「これは良い!」と思ったプロンプトを、その場でプリセットに保存する習慣をつけましょう。
> あとから同じ雰囲気の画像を作りたいときに、すぐに呼び出せます。
### プリセットを呼び出して使う
1. 一番上のプルダウンから、使いたいプリセットを選びます。
2. 使い方に応じてボタンを押します。
| ボタン | 動き |
| --- | --- |
| GenUI Set | 生成UIのプロンプト欄に、ポジティブとネガティブをまとめてセットする |
| PP Copy | ポジティブプロンプトをコピーする |
| NP Copy | ネガティブプロンプトをコピーする |
> **注意**
> **GenUI Set** は、生成UIにワークフローが読み込まれているときに使えます。
> 先に第5章の手順でワークフローを読み込んでおきましょう。
### グループで分類する
プリセットも、グループで分類できます。
1. プリセットを選んだ状態で、**+ Group** を押して新しいグループを作ります。
2. **Select group...** からグループを選び、**Add to Group** を押します。
グループは削除することもできます(**Del Group**)。
ここで作ったグループは、生成UIのバッチ生成(第5章 5-5)で「Prompt」のグループとして使えます。
たとえば「季節シリーズ」というグループに春・夏・秋・冬のプリセットを入れておけば、
4つの季節の絵をまとめて生成できます。
### Preset Manager で一覧する
中央の列の **Preset Manager** タブでは、保存したプリセットを一覧で見られます。
- **All** - すべてのプリセット
- **★** - お気に入りに登録したプリセット
- **Groups** - グループごとの表示
上部の **Search presets...** 欄で、プリセットを検索できます。
保存したプリセットは、ComfyUIのノード画面の横にある **WS Library** の **P(Prompts)** タブにも表示され、
キャンバスへドラッグして使えます(第9章)。
---
## 7-4 ワイルドカードとスタイル
右の列では、プロンプトを自動で変化させる **ワイルドカード** と、
プロンプトに雰囲気を足す **スタイル** を管理します。
列の上部の **Wildcard/Style** で切り替えます。
### ワイルドカードとは
ワイルドカードは、**生成するたびに、プロンプトの一部をランダムに入れ替える** しくみです。
同じプロンプトのまま、バリエーションのある画像をたくさん作れます。
Workflow Studioでいちばん手軽なのは、**ワイルドカードファイル** を使う方法です。
候補を書いたファイルを作っておき、プロンプトに `__ファイル名__` と書くと、
生成するたびにファイルの中から1行がランダムに選ばれます。
### ワイルドカードファイルを作る
例として、色の候補を入れた `colors` というファイルを作ってみましょう。
1. 右の列で **Wildcard** を選び、**Wildcard Files** の **+ New** を押します。
2. ファイル名(例:`colors`)を入力し、形式は **txt** を選びます。
3. 候補を1行に1つずつ入力します。
```
red
blue
green
golden
```
4. **Save** を押します。
### ワイルドカードを使う
プロンプトに、ファイル名を `__`(アンダースコア2つ)で囲んで書きます。
```
1girl, __colors__ dress, standing in a garden
```
生成UIで生成するたびに、`__colors__` の部分が `red`・`blue`・`green`・`golden` のどれかに置き換わります。
> **ポイント**
> `__colors__` を手で入力する代わりに、右の列の **File** ボタンでファイルを選んで挿入することもできます。
### もっと高度な書き方(Impact Pack 用)
右の列の上部には、`{ }`、`$$`、`[ ]` などの記号を挿入するボタンもあります。
これらは、次のような書き方のためのものです。
| 書き方 | 意味 |
| --- | --- |
| `{cat|dog|rabbit}` | 3つの中から1つを選ぶ |
| `{2$$cat|dog|bird}` | 3つの中から2つを選ぶ |
※ 本書では見やすさのため `|` と全角で表記しています。実際には半角の `|`(縦棒)を使います。
> **注意**
> 生成UIが自動で展開するのは、`__ファイル名__` の書き方だけです。
> `{A|B}` などの書き方を使うには、カスタムノード **ComfyUI-Impact-Pack** のワイルドカード用ノード
> (ImpactWildcardProcessor など)をワークフローに入れておく必要があります。
> 慣れないうちは、`__ファイル名__` の書き方だけ使えば十分です。
### 作ったプロンプトを送る
右の列で組み立てたプロンプトは、下のボタンで送り出せます。
- **→ Pos** - プリセットのPositive Promptへ送る
- **→ Neg** - プリセットのNegative Promptへ送る
- **Copy** - コピーする
`__ファイル名__` の部分は、生成UIで生成するときに自動で展開されます。
### スタイル
**スタイル** は、プロンプトに付け足す「雰囲気のテンプレート」です。
たとえば「映画風」というスタイルには、`cinematic still, {prompt}, dramatic lighting` のような内容が登録されています。
`{prompt}` の部分に、あなたのプロンプトが入ります。
スタイルを使うには、生成UIの上部のバーで、**Style** のチェックボックスをオンにして、プルダウンからスタイルを選びます。
生成するときに、選んだスタイルがプロンプトに自動で合成されます(ワークフローそのものは書き換わりません)。
Workflow Studioにはたくさんのスタイルが付属しています。
プロンプトタブの **Style** では、スタイルの一覧を見たり、**+ New** で自分のスタイルを作ったりできます。
> **ポイント**
> どのスタイルがどんな雰囲気になるかは、生成UIの **Create Catalog** で、スタイルごとの見本画像をまとめて作ると分かりやすくなります。
> 作った見本は、ギャラリータブの Style_Catalog で一覧できます(第8章)。
---
## 7-5 AIで翻訳・プロンプト作りを手伝ってもらう
左の列の **AI Assistant** では、パソコンで動くAI(ローカルAI)に、プロンプト作りを手伝ってもらえます。
> **事前に必要なもの**
> この機能を使うには、**Ollama** や **LM Studio** などのローカルAIソフトと、AIモデルの準備が必要です。
> 準備の方法は第10章で説明します。準備ができていない場合、AI Assistantの上部に **Disconnected** と表示されます。
### 日本語を英語に翻訳する
英語のプロンプトが苦手でも大丈夫です。
1. 左の列の下にある入力欄に、日本語で描きたい内容を書きます。
```
桜の木の下で微笑む制服姿の少女、春の柔らかい光
```
2. **Translate:** の **JA→EN** ボタンを押します。
3. チャット欄に、英語に翻訳された結果が表示されます。
翻訳結果の下に表示されるボタンで、結果を使えます。
| ボタン | 動き |
| --- | --- |
| 生成UIに適用 | 生成UIのポジティブプロンプト欄に入れる |
| コピー | コピーする(プリセットに貼り付けるときなど) |
英語のプロンプトの意味を知りたいときは、反対に **EN→JA** で日本語に訳せます。
### AIと相談しながらプロンプトを作る
入力欄に質問を書いて **Send**(または Enter キー)を押すと、AIとチャットできます。
- 「夕暮れの海辺の風景のプロンプトを、英語のタグ形式で作って」
- 「このプロンプトに、もっと幻想的な雰囲気を足して」
のように頼むと、プロンプトの案を出してくれます。
画像を読み取れるAIモデルを使っている場合は、**Attach Image** で画像を添付して、
「この画像の雰囲気をプロンプトにして」と頼むこともできます。
- **Shift+Enter** - 改行
- **Clear Chat** - 会話をリセット
- **歯車ボタン** - 使うAIソフトやモデルの設定(第10章)
> **注意**
> ローカルAIの答えは、いつも正しいとは限りません。
> 出てきたプロンプトは、実際に生成して確かめながら調整しましょう。
> **この章のまとめ**
> - プロンプトは英語・カンマ区切りが基本。強調は `(言葉:数値)`
> - うまくいったプロンプトは、プリセットとして名前を付けて保存する
> - 「GenUI Set」で、プリセットをすぐ生成UIにセットできる
> - グループに分けると、バッチ生成でまとめて試せる
> - ワイルドカードファイルを作って `__ファイル名__` と書くと、生成のたびにプロンプトを変化させられる
> - ローカルAIを用意すれば、日本語からの翻訳やプロンプトの相談ができる
@@ -0,0 +1,332 @@
# 第8章 ギャラリータブ - 生成した画像を見返す
画像生成を続けていると、ComfyUIの `output` フォルダには何百、何千枚もの画像がたまっていきます。
**ギャラリー(Gallery)** タブは、それらの画像を快適に見返し、
「この画像、どうやって作ったんだっけ?」を解決するための場所です。
ComfyUIで生成した画像には、生成に使ったワークフローやプロンプトの情報(**メタデータ**)が埋め込まれています。
ギャラリータブでは、この情報を読み取って、同じ設定をすぐに再現できます。
また、動画(MP4・WebM)や音声ファイル(WAV・MP3・OGG・M4A・FLAC・Opus)、3Dモデル(GLB・glTF・VRM・PLY・SPZ)も、画像と同じ一覧で扱えます。
---
## 8-1 画像を一覧する
ギャラリータブの上部には、4つのサブタブがあります。
| サブタブ | 役割 | 本書 |
| --- | --- | --- |
| Output | 生成した画像の一覧(ふだん使うのはここ) | 8-1〜8-3 |
| ImagePrompt | 見本画像を見ながらプロンプトを組み立てる | 8-4 |
| Style_Catalog | スタイルの見本画像から選ぶ | 8-4 |
| メタデータ | 画像ファイルをドロップして、中の情報を調べる | 8-2 |
### Outputの画面
**Output** サブタブは、左・中央・右の3つの部分に分かれています。
| 場所 | 役割 |
| --- | --- |
| 左(フォルダ) | `output` フォルダの中のフォルダ一覧。いちばん上の **[root]** が `output` フォルダ全体 |
| 中央(一覧) | 選んだフォルダの画像。下へスクロールすると続きが読み込まれる |
| 右(詳細パネル) | 選んだ画像の情報(8-2) |
まず、左のフォルダ一覧からフォルダを選ぶと、中央に画像が表示されます。
> **ポイント**
> 生成UIやComfyUIで新しく生成した画像が出てこないときは、ツールバーの **Refresh** を押してください。
> **ポイント**
> 読み込む `output` フォルダの場所は、設定タブの **「ギャラリー Outputフォルダ」** で変更できます。
### 表示の切り替え・検索・並べ替え
ほかのタブと同じく、次の操作ができます。
- **サムネイル/テーブル** - サムネイル表示とテーブル表示の切り替え
- **検索欄** - ファイル名・タグ・メモ・**プロンプトの内容** で検索。横の **S** ボタンをオンにすると、文章の意味で探せます(8-6)
- **並べ替え** - Date (Newest)/Date (Oldest)/Name A-Z/Name Z-A
- **★** - お気に入りだけ表示
- **× クリア** - 絞り込みをまとめて解除
> **ポイント**
> 検索欄では、プロンプトの言葉でも探せます。
> たとえば「cat」で検索すると、プロンプトに cat を含む画像が見つかります。
> (プロンプトの検索は、一度詳細パネルで開いた画像が対象になります)
### 大きく見る・比べる
- 画像をクリックすると、右の詳細パネルに大きく表示されます。
- **Ctrl キーを押しながら** 画像をクリックすると、複数の画像を選べます。
2〜4枚選んで **比較** を押すと、並べて見比べられます。
シードやモデルだけを変えて作った画像を並べると、違いがよく分かります。
### 整理する
- **お気に入り(★)・タグ・メモ・グループ** で分類できます(詳細パネルで設定)。
- 複数選択して、まとめてグループに追加したり、別のフォルダへ移動したりできます。
移動先を選ぶ画面(**移動先...**)には絞り込み欄があり、フォルダ名の一部を入れると候補が絞られ、Enterで先頭の候補へ移動できます。
- **エクスポート** を押すと、選んだ画像をZIPファイルにまとめてダウンロードできます。
> **注意**
> 詳細パネルの **Delete** や、複数選択の **ファイル削除** を使うと、画像ファイルそのものが削除されます。
---
## 8-2 メタデータから生成設定を読み取る
画像をクリックすると、右の詳細パネルに4つのタブが表示されます。
| タブ | 内容 |
| --- | --- |
| プロンプト | 画像の生成に使われたプロンプト(最初に表示されるタブ) |
| 基本情報 | ファイル名・大きさ・日時・タグ・メモ |
| メタデータ | 画像に埋め込まれたワークフロー(JSON) |
| グループ | グループの設定 |
### プロンプトを確認する
**プロンプト** タブには、画像に埋め込まれていたプロンプトが一覧表示されます。
ポジティブには **POS**、ネガティブには **NEG** の印が付きます。
項目をクリックすると、全文が表示されます。
### もっと詳しく調べる(メタデータサブタブ)
使ったモデルやLoRAまで詳しく知りたいときは、詳細パネルの **Metadata** ボタンを押します
(または、Alt キーを押しながら画像をクリック)。
ギャラリーの **メタデータ** サブタブが開き、次の情報が整理されて表示されます。
- **モデル** - Checkpoint、VAE、Diffusion Model、Text Encoder(中央の列)
- **LoRA** - 使われたLoRAと、その強さ(中央の列)
- **プロンプト** - ポジティブ・ネガティブ(中央の列)
- **Settings** - 画像サイズ(幅×高さ)と、KSamplerの Seed・Steps・CFG・Sampler・Scheduler・Denoise(右の列。v0.7.14〜)
- **Nanobanana** - Comic Creatorのnanobananaタブで作った画像の、エンジン・モデル・プロンプトなどの生成履歴(右の列。該当する画像のみ)
プロンプトの横のボタンで、内容をほかのタブに送れます。
| ボタン | 動き |
| --- | --- |
| Copy | コピー |
| GenUI:P/GenUI:N | 生成UIのポジティブ/ネガティブに入れる |
| Prompt:P/Prompt:N | プロンプトタブのプリセットのポジティブ/ネガティブに入れる |
> **ポイント:ネットで見つけた画像も調べられる**
> メタデータサブタブの左側に、画像ファイルを **ドロップ** すると、その画像の情報を調べられます。
> ComfyUIの画像だけでなく、SD WebUI(AUTOMATIC1111)、SD Forge、Fooocus で作られたPNG画像にも対応しています。
> 配布サイトで見つけた画像の作り方を知りたいときに便利です
> (ただし、投稿時にメタデータが消されている画像からは読み取れません)。
---
## 8-3 同じ設定を再現する
ギャラリーのいちばん便利な使い方は、**気に入った画像と同じ設定で、もう一度生成する** ことです。
方法は2つあります。
### 方法1:生成UIで再現する(GenUI)
1. ギャラリーで、再現したい画像をクリックします。
2. 詳細パネルの **GenUI** ボタンを押します。
3. 画像に埋め込まれていたワークフローが、生成UIに読み込まれます。
あとは第5章と同じように、プロンプトを少し変えたり、シードを変えたりして生成できます。
> **ポイント**
> **同じ画像をもう一度作りたいとき(v0.8.3〜)**
> 画像に埋め込まれているワークフローには、そのときのシード値も入っています。
> 生成パネルのシードのモードを **Workflow** にして Generate を押すと、そのシードのまま生成されます。
>
> モードが Random のままだと、別のシードで生成されます。
> また、Fixed を使う場合は、**Settings** サブタブの **Seed** 欄の値を生成パネルの **Seed** 欄に貼り付けてください(生成パネルの値が優先されるため)。
### 方法2:ComfyUIのノード画面で再現する(コピー&キャンバスへ送る)
1. ギャラリーで、再現したい画像をクリックします。
2. 詳細パネルの **JSON** タブを開きます。
3. **Copy & Send Canvas**(コピー&キャンバスへ送る)を押します。
(このボタンは英語表示のままです。ワークフローが埋め込まれた画像でだけ押せます)
ComfyUIのノード画面に、その画像を作ったワークフローが読み込まれます。
ノード画面で細かく手を加えたいときは、こちらを使います。
> **注意**
> 「この画像にはComfyUIワークフローが埋め込まれていません」と表示される場合は、再現できません。
> ワークフローの情報が保存されない設定で作った画像や、別のソフトで編集・圧縮した画像などが当てはまります。
### 画像を次の生成の材料にする
ギャラリーの画像を、生成の「入力画像」として使うこともできます。
| ボタン | 動き |
| --- | --- |
| Send GenUI Image | 選んだ画像を、生成UIのワークフローの入力画像(LoadImageノード)にセットする |
| Send to LI node | 選んだ画像を、ComfyUIのノード画面で選んでいる画像読み込みノードにセットする |
| Image Edit | 選んだ画像を Image Edit タブで開く(第11章) |
| Send CC Layout | 選んだ画像を Comic Creator のレイアウトタブ(選択中のコマ/オーバーレイ)に送る(Comic Creatorの中でギャラリーを開いたときだけ表示。第11章 11-6) |
| Send CC Image | 選んだ画像を Comic Creator の Image タブに送る(作業中のレイヤーがあれば新規レイヤー、なければ新規キャンバス。動画は未対応。表示条件は Send CC Layout と同じ) |
I2I(画像から画像を作る)ワークフローで、生成した画像をさらに描き直していくときに便利です。
---
## 8-4 ImagePrompt と Style_Catalog
### ImagePrompt - 見本画像からプロンプトを組み立てる
**ImagePrompt** は、「見本画像」と「プロンプト」をセットで並べて、
見た目で選びながらプロンプトを組み立てるためのサブタブです。
1. 見本画像をクリックすると、右側にその画像のプロンプトが表示されます。
2. **+ Add** を押すと、**Selected Prompts** に追加されます。
3. いくつか追加すると、**Final Prompt** に、カンマ区切りでまとめたプロンプトができあがります。
4. **Copy** でコピーして、生成UIやプリセットに貼り付けます。
見本画像は、`output` フォルダの中に自動で作られる `ws_image_prompt` フォルダに置きます。
画像と同じ名前の `.txt` ファイル(中身がプロンプト)を一緒に置くと、見本として使えるようになります。
> **例**
> `ws_image_prompt/髪型/ponytail.png` と `ws_image_prompt/髪型/ponytail.txt`(中身:`ponytail, long hair`)
### Style_Catalog - スタイルを見本画像で選ぶ
第7章で紹介したスタイルは、名前だけではどんな雰囲気になるか分かりにくいものです。
**Style_Catalog** では、スタイルごとの見本画像を並べて、見た目で選べます。
見本画像は、生成UIの上部のバーにある **Create Catalog** で作ります。
1. 生成UIに、文章から画像を作るワークフローを読み込みます。
2. **Batch** サブタブの **Style** で、見本を作りたいスタイルにチェックを入れます。
3. 上部のバーの **Create Catalog** を押し、保存先のフォルダを決めて実行します。
4. 上部のバーの **Catalog** を押すと、ギャラリーの Style_Catalog に移動します。
Style_Catalog で見本画像を選び、**Select as Style** を押すと、
生成UIのスタイルが、そのスタイルに切り替わります。
> **ポイント**
> モデルによってスタイルの効き方は変わります。
> 保存先のフォルダをモデルごとに分けておくと、比べやすくなります。
---
## 8-5 Eagle連携・PSD書き出し(任意)
ここからは、必要な方だけが使う機能です。
### Eagle連携
**Eagle** は、画像を整理するための有料ソフトです。
設定タブの **Eagle連携** を有効にすると、生成UIで生成した画像を、プロンプトなどの情報付きで自動的にEagleに保存できます。
Eagleで画像を管理している方は、試してみてください。
### PSD書き出し
ギャラリーで複数の画像を選び、**PSD作成** を押すと、
それぞれの画像をレイヤーとして重ねたPSD(Photoshop形式)のファイルを作れます。
差分画像をまとめて、画像編集ソフトで仕上げたいときに便利です。
> **注意**
> PSD関連の機能を使うには、追加のPythonパッケージ(psd-tools)が必要です。
> 入っていない場合は、インストール方法がエラーメッセージで表示されます(第11章)。
---
## 8-6 画像を「意味」で探す(セマンティック検索)
通常の検索は、ファイル名・タグ・プロンプトの文字が一致した画像を探します。
**セマンティック検索**(v0.8.2〜)は、画像の「意味」で探します。
たとえば「夕暮れの街並み」と入れると、プロンプトにその言葉が無くても、雰囲気の合う画像が見つかります。
また、選んだ画像に **似た画像** の一覧を出すこともできます。
### 使い方
1. 検索欄の横の **S** ボタンをオンにします。検索欄が意味検索に切り替わり、ツールバーに2行目が現れます。
2. 2行目の **歯車ボタン**(設定)を押し、**インデックス**(検索用の索引)を作ります(下記)。
3. 検索欄に文章を入れて **Enter** を押します。結果はスコア順に並び、サムネイルにスコアが表示されます。
4. 画像を選んで2行目の **Similar** を押すと、その画像に似た画像が並びます。別の画像を選んで **↻(更新)** を押すと、その画像の類似を出し直します。もう一度 **Similar** を押すと、通常の表示に戻ります。
> **ポイント**
> 意味検索の結果には、並べ替えやタグ・グループの絞り込みは効きません。
> 検索の範囲は、選んでいるフォルダの中(サブフォルダを含む)です。
> フォルダツリーの枚数の横に緑の点があるフォルダは、直下の画像がすべてインデックス済みです。
### モデルを選ぶ(歯車ボタンの「モデル設定」タブ)
| モデル | 特徴 | 準備 |
| --- | --- | --- |
| Gemini Embedding 2 | Googleのサービスで計算する。速い。画像をGoogleに送る | APIキーを取得し、プラグインの `.env` に `GEMINI_API_KEY=キー` と書いて、ComfyUIを再起動する |
| EmbeddingGemma 2 | パソコンの中で、**Ollama** を使って計算する(ローカル・無料)。外部に送らない | Ollamaをインストールして起動し、`ollama pull embeddinggemma-2:740m`(または軽量版の `embeddinggemma-2:440m`)を実行する。モデル設定タブで **Ollama URL**(通常は変更不要)と **モデルサイズ** を選ぶ |
> **注意:Geminiを使うとき**
> 画像がGoogleのサービスに送られます。無料枠で送った内容は、Googleの製品改善に使われる場合があります。
> 公開したくない画像が多い場合は、ローカルの EmbeddingGemma 2 を選んでください。
> APIキーは `.env` にだけ書き、他人に見せないようにしましょう。
### インデックスを作る(歯車ボタンの「インデックス」タブ)
1. 対象のフォルダを選びます(サブフォルダを含めるかも選べます)。
2. 「新規のみ」か「全件再作成」を選んで開始します。進み具合が表示され、途中で停止もできます。
3. インデックスは全フォルダで共通の1つです。フォルダごとに少しずつ作っても構いません。
モデルやベクトル次元を変えたときは、**全件再作成** が必要です。
### 速さの目安と設定
- ローカルのEmbeddingGemma 2は、v0.8.14からOllamaで動くようになりました(それまでの、ComfyUIのPythonで動かす方式は廃止)。
NVIDIAのGPUなら1枚あたり約0.1〜0.2秒です。インデックスを作り終えると、モデルはGPUメモリから自動で降ろされます。
- インデックスを作るときと、検索するときは、Ollamaが起動している必要があります。
- モデルサイズの740m(テキスト+画像+音声)と440m(テキスト+画像・軽量)は同じ結果を返すので、切り替えても全件再作成は不要です。
v0.8.13以前に作ったインデックスも、そのまま使えます。
- **検索設定** タブで、類似画像・文章検索それぞれの「最小スコア」と最大表示件数を決められます。
スコアの水準はモデルによって違うため、モデルごとに保存されます。
---
## 8-7 音声ファイルの扱いと変換
ComfyUIで音楽や効果音を作る機会が増えたため、ギャラリーは音声ファイルも扱えます(v0.8.0〜)。
対応しているのは、WAV・MP3・OGG・M4A・FLAC・Opus です。
- 一覧には、音の形(波形)のサムネイルと、♪ のバッジが表示されます。波形の色は形式ごとに違います(MP3は青、WAVは緑、FLACは琥珀色、OGG/Opusは紫、M4Aはピンク)。
- 選ぶと、右の詳細パネルに波形とプレイヤーが表示され、その場で再生できます。
- 保存時にワークフロー(`prompt`/`workflow`)が埋め込まれている音声は、プロンプトやJSONのタブも使えます。
- 動画(MP4・WebM)も同様に一覧できます。**Send to LV node** ボタンで、選んだ動画を、ComfyUIの画面で選択中の **Load Video** ノードに送れます(画像版の Send to LI node の動画版です)。
### ファイルを変換する
音声または動画を含めて複数選択すると、一括操作バーに **変換...** が現れます。
1. 変換したいファイルを選び、**変換...** を押します。
2. 出力形式(FLAC/MP3/Opus/M4A/WAV)と、必要ならビットレート・サンプルレート・チャンネル数を決めます。
3. 実行すると、同じフォルダに別のファイルとして保存されます(同名があるときは `名前_converted` のように連番が付き、上書きはされません)。
動画を選んだときは、音声トラックだけを取り出して保存します。
| 引き継ぎ | 説明 |
| --- | --- |
| ワークフロー情報 | MP3・FLAC・Opus・M4A には引き継がれる。WAVには引き継げない(通知が出る) |
| お気に入り・タグ・メモ・グループ | 引き継ぐかを選べる |
| 元ファイルの削除 | 変換に成功したあとで削除するかを選べる(確認画面あり) |
> **ポイント**
> OGG は再生はできますが、変換の出力形式には選べません。
> ほかの動画編集ソフトを入れなくても、Workflow Studioだけで変換できます。
> **ポイント:Videoタブでの活用**
> Video の Asset サブタブでは、音声ファイルを「BGMに設定」したり、「サウンドとして追加」したりできます(第11章 11-4)。
---
## 8-8 3Dモデルを見る(v0.9.4〜)
ComfyUIで3Dモデルを作るワークフロー(画像から3Dなど)の結果も、ギャラリーで確かめられます。
対応しているのは、GLB・glTF・VRM・PLY(メッシュ/点群/ガウシアンスプラット)・SPZ です。
- 一覧には「**3D**」のバッジ付きで並びます。サムネイルは、最初に一覧に出たときにブラウザが描いてサーバーに保存します
(128MBを超えるファイルは、開いたときに保存します)。
- 選ぶと、右の詳細パネルの内蔵ビューアで立体として表示されます。ダブルクリックで大きく表示できます。
| 操作 | 動き |
| --- | --- |
| ドラッグ | 回転する |
| 右ドラッグ | 位置を動かす |
| マウスホイール | 拡大・縮小 |
| ⟲(リセット)ボタン | 最初の向きに戻す |
- ComfyUIの **SaveGLB** で保存したモデルは、画像と同じようにワークフローを読み取れます(8-2・8-3)。
> **注意**
> - 3Dファイルは、Image Edit・生成UI・Tagger・Feederには送れません。
> - `.gltf` の中には、別の `.bin` や画像ファイルを参照しているものがあります。ギャラリーで移動・削除しても動くのは `.gltf` 本体だけなので、参照しているファイルの場所は変えないでください。
> **この章のまとめ**
> - ギャラリーのOutputで、生成した画像をフォルダごとに見返せる。プロンプトの言葉でも検索できる
> - 詳細パネルのプロンプトタブで、画像の生成に使ったプロンプトがすぐ分かる
> - 「GenUI」で生成UIに、「コピー&キャンバスへ送る」でノード画面に、同じワークフローを再現できる
> - 再現するときは、シードのモードを Workflow(または Fixed)にする
> - メタデータサブタブに画像をドロップすると、モデル・LoRA・プロンプトを詳しく調べられる
> - Sボタンで、文章の意味や「似た画像」から探せる(セマンティック検索)
> - 音声ファイルも一覧・再生・変換できる
> - 3Dモデル(GLB・glTF・VRM・PLY・SPZ)も、ギャラリーで回して確かめられる(8-8)
@@ -0,0 +1,171 @@
# 第9章 WS Library - ComfyUI画面の中で使う
ここまでの章では、Workflow Studio本体(タブが並ぶ画面)を中心に使ってきました。
この章では、ComfyUIのノード画面の横に開く **WS Library(Workflow Studio Library)** を使います。
WS Libraryを使うと、Workflow Studioで整理したワークフロー・ノード・モデル・プロンプトを、
**ノード画面にドラッグ&ドロップするだけ** で使えます。
ノード画面で作業する時間が長い方ほど、便利さを実感できる機能です。
---
## 9-1 WS Libraryを開く
1. ComfyUIのノード画面を開きます。
2. 上部メニューに追加された **WS Library** ボタン(四角が並んだアイコン)を押します。
画面の端にパネルが開きます。もう一度ボタンを押すと閉じます。
### 6つのタブ
パネルの上部には、1文字のタブが並んでいます。マウスを重ねると、正式な名前が表示されます。
| タブ | 名前 | できること | 本書 |
| --- | --- | --- | --- |
| W | Workflows | ワークフローを読み込む | 9-2 |
| N | Nodes | ノードやノードセットを置く | 9-2 |
| M | Models | モデルを読み込むノードを置く | 9-2 |
| P | Prompts | 保存したプロンプトを置く・コピーする | 9-2 |
| I | Information | 画像やワークフローの中身を調べて、使われた素材を置く | 9-3 |
| A | AI TOOL | 翻訳やチャットなどのAI機能を使う | 第10章 |
各タブの中には、さらに小さなタブ(サブタブ)があります。
多くのタブで、次のサブタブが共通して使えます。
- **All** - すべて
- **★ Favorites** - Workflow Studio本体でお気に入りに登録したもの
- **Groups** - Workflow Studio本体で作ったグループごと
また、各サブタブの検索欄で、名前を入力して絞り込めます。
> **ポイント**
> WS Libraryに表示される内容は、Workflow Studio本体での整理(お気に入り・グループ・バッジ)と連動しています。
> 本体でしっかり整理しておくほど、WS Libraryも使いやすくなります。
---
## 9-2 ドラッグ&ドロップでキャンバスに置く
WS Libraryの基本操作は、とてもシンプルです。
| 操作 | 動き |
| --- | --- |
| キャンバスへドラッグ&ドロップ | ドロップした位置に置く・読み込む |
| ダブルクリック | 画面の中央付近に置く・読み込む |
### W - ワークフローを読み込む
ワークフローをキャンバスへドラッグ(またはダブルクリック)すると、そのワークフローが読み込まれます。
エクスプローラーからワークフローのファイルをドロップしたときと同じ動きです。
サブタブ:All/★ Favorites/Groups/By Badge(バッジごと)
> **ポイント**
> Workflow Studio本体をブックマークから開いていて「キャンバスへ送る」が使えない場合は、
> このWタブに対象のワークフローが表示されるので、ここからドラッグしてください(第4章 4-6)。
### M - モデルを置く
モデルをキャンバスへドラッグすると、**そのモデルが選ばれた状態の読み込みノード** が置かれます。
| モデルの種類 | 置かれるノード |
| --- | --- |
| Checkpoint | Load Checkpoint(CheckpointLoaderSimple) |
| LoRA | Load LoRA(LoraLoader) |
| VAE | Load VAE(VAELoader) |
| ControlNet | Load ControlNet Model(ControlNetLoader) |
| UNET | Load Diffusion Model(UNETLoader) |
| TextEncoder | Load CLIP(CLIPLoader) |
| Hypernetwork | HypernetworkLoader |
ノードを一覧から探して、置いて、ファイル名をプルダウンから選んで……という手間がなくなります。
置いたあとは、ほかのノードと線でつないで使います。
サブタブ:All/★ Favorites/Groups/By Type(種類ごと)
> **ポイント**
> Embeddingには読み込み用のノードがないため、キャンバスに置くことはできません。
> **ポイント:LoRAをまとめて置く**
> LoRAのグループには **All N LoRAs**(Nはグループ内のLoRAの数)という項目が表示されます。
> これをドラッグすると、グループ内のLoRAがすべて入った状態の `Lora Loader (LoraManager)` ノードが置かれます。
> (カスタムノード **ComfyUI-Lora-Manager** が必要です)
### P - プロンプトを置く
保存したプリセット(第7章)をキャンバスへドラッグすると、
ポジティブ・ネガティブのプロンプトが入った **Prompt Text (WFS)** ノードが置かれます。
これはWorkflow Studioに付属しているノードで、プロンプトの文章を出力します。
プロンプトを貼り付けたいだけのときは、項目の **P**(ポジティブ)/**N**(ネガティブ)ボタンでコピーできます。
サブタブ:All/★ Favorites/Categories(カテゴリーごと)/Groups
### N - ノードを置く
ノードをキャンバスへドラッグすると、そのノードが置かれます。
よく使うノードをWorkflow Studio本体の **ノード** タブでお気に入りに登録しておくと、すぐに取り出せます。
Nタブの一覧で、ノードを **Alt+クリック** すると、その場でお気に入りのオン/オフを切り替えられます(v0.8.0〜)。
Nタブは、ノードの詳細情報を待たずに一覧を先に表示するため、以前より速く開きます。
サブタブ:All/★ Favorites/Groups/Sets/Category(カテゴリーごと)/Package(カスタムノードのパッケージごと)
### ノードセット - よく使う組み合わせを丸ごと保存する
**ノードセット** は、複数のノードとそのつながりを、ひとまとまりで保存したものです。
「いつも同じ組み合わせで使うノード群」を保存しておけば、ドラッグ1回で丸ごと置けます。
**ノードセットを作る**
1. ComfyUIのノード画面で、保存したいノードを選択します(Ctrlキーを押しながらクリック、またはドラッグで範囲選択)。
2. 選んだノードの上で **右クリック** し、**Save as Node Set** を選びます。
3. 名前を付けて保存します。
**ノードセットを使う**
WS Libraryの **N** タブの **Sets** サブタブから、キャンバスへドラッグします。
> **例**
> 「顔を自動で補正するノード群」や「高解像度化(アップスケール)のノード群」など、
> どのワークフローにも後から付け足したい部品をセットにしておくと便利です。
---
## 9-3 Informationタブで画像の中身を調べる
**I(Information)** タブは、画像やワークフローのファイルを調べて、
**使われていたモデルやプロンプトを、そのままキャンバスに置ける** 機能です。
ギャラリーのメタデータ(第8章 8-2)を、ノード画面の中で使えるようにしたものと考えてください。
### 使い方
1. WS Libraryの **I** タブを開きます。
2. 調べたいファイル(ComfyUIで作ったPNG・WebP・MP4・WebMの画像や動画、またはワークフローのJSON)を、パネルにドロップします。
3. ファイルの中から見つかった素材が、サブタブに分かれて表示されます。
| サブタブ | 内容 | キャンバスへドラッグすると |
| --- | --- | --- |
| model | Checkpoint・VAE・Diffusion Model・Text Encoder | 対応する読み込みノードが置かれる |
| lora | 使われていたLoRAと、その強さ | LoRAの読み込みノードが置かれる |
| Prompts | ポジティブ(POS)・ネガティブ(NEG)のプロンプト | プロンプトの入ったテキストエンコードノードが置かれる |
プロンプトの項目をクリックすると全文が表示され、**Copy** でコピーもできます。
### こんなときに便利
- 「前に作ったあの画像と、同じモデルとLoRAで新しいワークフローを組みたい」
- 「ネットで見つけたワークフローのプロンプトだけ借りたい」
- 「この画像、どのLoRAをどれくらいの強さで使っていたっけ?」
ワークフローを丸ごと読み込むのではなく、**必要な部品だけを取り出して使える** のがポイントです。
---
## 9-4 WS Libraryの見た目を変える
WS Libraryのパネル内にある **歯車ボタン**から、パネルの配色を設定できます。
- パネルの背景色
- サブヘッダーの背景色
- 文字の色・補助的な文字の色
- 枠線の色
ComfyUIのテーマに合わせて調整すると、違和感なく使えます。
設定はブラウザに保存され、次に開いたときも反映されます。
> **この章のまとめ**
> - WS Libraryは、ComfyUIのノード画面の横に開く「素材の棚」
> - W・N・M・P のタブから、ワークフロー・ノード・モデル・プロンプトをドラッグ(またはダブルクリック)で置ける
> - Mタブからモデルをドラッグすると、モデルが選ばれた状態の読み込みノードが置かれる
> - 右クリックの「Save as Node Set」で、よく使うノードの組み合わせを保存できる
> - Iタブに画像をドロップすると、使われていたモデル・LoRA・プロンプトを取り出して使える
+438
View File
@@ -0,0 +1,438 @@
# 第10章 AI TOOL - ローカルAIを助手にする
Workflow Studioは、パソコンの中で動く **ローカルAI(大規模言語モデル)** と連携できます。
ローカルAIを用意すると、次のようなことができるようになります。
- 日本語で考えた内容を、英語のプロンプトに翻訳する
- AIとチャットしながら、プロンプトのアイデアを出してもらう
- 画像を見せて、その画像の説明やプロンプト、タグを作ってもらう
- ワイルドカード用の候補リストを作ってもらう
- **スキル** を選んで、プロンプト作り・ストーリーボード・不具合の相談など、得意分野を切り替える(v0.9.5〜)
- 生成した画像が依頼どおりかをAIにチェックさせて、足りない点だけ直す(v0.9.5〜)
- 画像に写っている文字を読み取る(OCR、v0.9.5〜)
インターネット上のサービスではなく、自分のパソコンの中で動くので、
**入力した内容や画像が外部に送られない** のが大きな特長です。
> **ポイント**
> AI機能は「あると便利」な機能です。使わなくても、Workflow Studioの基本機能はすべて使えます。
> パソコンの性能(特にGPUのメモリ)に余裕がある方は、ぜひ試してみてください。
---
## 10-1 ローカルAIソフトを準備する
### 対応しているソフト
Workflow Studioは、次の4つのローカルAIソフトに対応しています。
| ソフト | 特徴 | 標準のアドレス |
| --- | --- | --- |
| **Ollama**(オラマ) | 定番。モデルのダウンロードから実行までかんたん | `http://localhost:11434` |
| **LM Studio** | 画面で操作できる。モデル探しもアプリ内でできる | `http://localhost:1234` |
| Lemonade Server | AMD製のGPU・NPUでも動かしやすい | `http://localhost:13305` |
| Unsloth | APIキーなどの追加設定が必要(上級者向け) | `http://localhost:8888` |
はじめての方には **Ollama** をおすすめします。本章では、Ollamaを例に説明します。
> **ポイント**
> アドレスの `localhost` は「自分のパソコン」、その後ろの数字は **ポート番号** です(第2章 2-4)。
> ComfyUIと同じように、AIソフトもそれぞれ決まったポート番号で待ち受けています。
### Ollamaの準備
1. Ollamaの公式サイトから、お使いのOS用のインストーラーをダウンロードして、インストールします。
2. Ollamaを起動します(Windowsでは、インストール後に自動で起動し、タスクバーの通知領域に常駐します)。
3. 使うAIモデルをダウンロードします。コマンドプロンプトなどで、次のように入力します。
```
ollama pull (モデル名)
```
モデル名は、Ollamaの公式サイトのモデル一覧で確認できます。
> **モデル選びの目安**
> - **画像を読み取る機能(VLM)も使いたい** → 画像入力(vision)に対応したモデルを選ぶ
> (作者の検証では、Gemma 4 の `e4b` などで画像生成・画像読み取りがうまく動いています)
> - **GPUのメモリが少ない** → パラメータ数の小さいモデル(数B=数十億程度)を選ぶ
> - 翻訳の精度を重視するなら、日本語が得意なモデルを選ぶ
>
> AIモデルの世界は変化が速いので、新しいモデルも試してみてください。
> **注意:GPUのメモリの取り合い**
> ローカルAIも、画像生成と同じく **GPUのメモリ(VRAM)** を使います。
> 両方を同時に動かすと、メモリが足りなくなって画像生成が遅くなったり、エラーになったりします。
> 対策は 10-3 で説明します。
---
## 10-2 AI TOOLタブを使う
Workflow Studio本体の **AI TOOL** タブを開きます。
画面は4つの部分に分かれていて、すべて同時に表示されます。
| 部分 | できること |
| --- | --- |
| 翻訳 | 日本語・英語・中国語などの翻訳 |
| Chat | AIとの会話。スキルで得意分野を切り替えられる。条件がそろえば、会話から画像生成も |
| TOOLS | 画像の読み取り(VLM)、文字起こし(OCR)、ワイルドカード作成 |
| 設定 | 使うAIソフト・モデルの設定 |
> **ポイント:翻訳と設定はたためる(v0.9.5〜)**
> 翻訳と設定の見出しの右端にある矢印(◀/▶)を押すと、その部分が細い帯にたたまれ、空いた幅をChatとTOOLSが使います。
> 帯のどこかをクリックすると元に戻ります。たたんだ状態は、次に開いたときも保たれます。
### 最初に:設定でAIソフトとつなぐ
1. **設定** の **バックエンド** で、使うソフト(例:Ollama)を選びます。
2. **接続設定** のアドレスを確認します。Ollamaを標準のまま使っている場合は、変更不要です。
3. **接続テスト** を押します。「接続成功」と表示されればOKです。
4. **モデル選択** の **更新** を押し、一覧から使うモデルを選びます。
5. **設定を保存** を押します。
> **ポイント**
> ここでの設定は、ComfyUI側のWS Libraryの **A(AI TOOL)** タブと共通です。
> 一方、プロンプトタブ(第7章)とTaggerタブには、それぞれ専用の **歯車ボタン** 設定があります。
> 「AI TOOLタブでは使えるのに、プロンプトタブでは Disconnected」というときは、プロンプトタブの歯車ボタンでも同じように設定してください。
> **うまくつながらないとき**
> - Ollamaなどのソフトが起動しているか確認します。
> - LM Studioは、アプリ内で **サーバーを起動** しておく必要があります。
> - アドレスのポート番号が、ソフト側の設定と合っているか確認します。
> - モデルの一覧が空のときは、モデルがダウンロードされているか確認します。
### 翻訳
1. 翻訳元と翻訳先の言語を選びます(**⇄(入れ替え)** ボタンで入れ替え)。
2. 入力欄に文章を書き、**翻訳** を押します。
3. 結果を **コピー** して、生成UIやプリセットに貼り付けます。
> **ポイント**
> TOOLSで読み取った文字(10-8のOCRなど)は、TOOLSの **翻訳へ送る** で、この入力欄にそのまま入れられます。
> **ポイント**
> 小さなモデルは、翻訳せずに元の文章をそのまま返してしまうことがあります。
> その場合は「モデルが原文をそのまま返しました」と表示されるので、別のモデルを試してください。
### Chat
入力欄に話しかけて、**Enter** で送信します(**Shift+Enter** で改行)。
会話の流れを覚えているので、「もっと明るい雰囲気にして」のような続けての依頼もできます。
**Clear** で会話をリセットします。
> **ポイント:チャットから画像を生成する(上級)**
> 「ツール呼び出し(Tool Calling)」に対応したモデルを使うと、
> チャットで「猫の絵を描いて」と頼むだけで、生成UIに読み込んだワークフローで画像が生成され、チャット内に表示されます。
> この機能は、入力欄の横の **画像生成を許可する** のチェックでオン・オフできます。
> **ポイント**
> Chatの上にあるドロップダウンで **スキル** を選ぶと、AIの得意分野を切り替えられます(10-5)。
> AIの返事は、見出し・箇条書き・コードの枠を整えて表示されます(v0.9.5〜)。
### TOOLS - 画像を読み取る(VLM)
画像入力に対応したモデル(VLM)を使うと、画像の内容をAIに読み取らせることができます。
1. **TOOLS** の枠に、画像(PNG/JPG/WebP)をドロップします。
2. 作業を選びます。
| 作業 | 内容 |
| --- | --- |
| 画像の解説 | 画像に何が写っているかを文章で説明する |
| プロンプト作成 | 画像を再現するためのプロンプトを作る |
| Create tags(英語表示のまま) | 画像の要素をタグ形式で書き出す |
| Create wildcards(英語表示のまま) | カテゴリー名と件数を指定して、ワイルドカードの候補を作る(画像は不要) |
| 文字起こし(OCR) | 画像に写っている文字を読み取る(10-8) |
3. **実行** を押します。
4. 結果の下のボタンで、結果を使います(v0.9.5〜)。
| ボタン | 動き |
| --- | --- |
| 翻訳へ送る | 翻訳の入力欄に入れる(翻訳がたたまれていれば開く) |
| Chatへ送る | Chatの入力欄に追加する |
| コピー | クリップボードにコピーする |
> **例**
> 気に入ったイラストの雰囲気をまねしたいとき、その画像から「プロンプト作成」をして、
> できたプロンプトを生成UIで使ってみる - という使い方ができます。
### ComfyUIの画面の中でも使える
同じAI機能は、ComfyUI側のWS Libraryの **A** タブでも使えます。
ノード画面で作業しながら、ちょっと翻訳したいときに便利です。
---
## 10-3 VRAMの解放とメモリ管理
10-1で触れたとおり、ローカルAIと画像生成は、GPUのメモリ(VRAM)を取り合います。
Ollamaは、一度使ったモデルをしばらくメモリに置いたままにするため、
AIを使ったあとに画像生成をすると、メモリが足りなくなることがあります。
### 手動で解放する
AI TOOLの設定にある **モデルをアンロード** を押すと、AIモデルがメモリから降ろされ、VRAMが空きます。
(Ollamaの場合は、読み込まれているすべてのモデルが降ろされます)
### 生成の前に自動で解放する
設定タブの **VRAM調整(Ollama)** で、画像生成の直前に自動でOllamaのモデルを降ろすように設定できます。
| 生成前の動作 | 動き |
| --- | --- |
| 何もしない | 自動では降ろさない |
| 空きが目標未満のときだけ必要な分をアンロード | 空きVRAMが **目標の空き(GB)** より少ないときだけ降ろす |
| 毎回すべてアンロード | 生成のたびに、必ずすべて降ろす |
> **ポイント**
> どれを選べばよいか迷ったら、「空きが目標未満のときだけ必要な分をアンロード」がおすすめです。
> 目標の空きには、画像生成に必要なメモリの量(使うモデルによって異なります)を目安に入力します。
> **今すぐすべてアンロード** ボタンで、その場ですぐに解放することもできます。
> **注意**
> この自動解放は、Ollamaだけが対象です。LM Studioなどを使っている場合は、手動で解放してください。
---
## 10-4 セキュリティ上の注意
### 標準では「自分のパソコン」のAIにだけつながる
安全のため、Workflow StudioがつなぐAIソフト(Ollama、LM Studioなど)は、標準では
**同じパソコンの中(localhost/127.0.0.1)で動いているもの** に限られています。
### 別のパソコンのAIを使いたいとき
家庭内LANにある別のパソコン(GPUの強いパソコンなど)でOllamaを動かして使いたい場合は、
環境変数 **`WFS_ALLOWED_BACKEND_HOSTS`** に、つなぎたいパソコンのアドレスを登録して、ComfyUIを再起動します。
```
WFS_ALLOWED_BACKEND_HOSTS=192.168.1.20:11434
```
複数ある場合は、カンマ(,)で区切って並べます。
環境変数の設定方法は、ComfyUIの起動方法によって異なります。
### ComfyUIを `--listen` 付きで起動している場合
ComfyUIを `--listen` オプション付きで起動すると、同じネットワークにいるほかのパソコンやスマホからも
ComfyUI(とWorkflow Studio)にアクセスできるようになります。
その場合、アクセスできる人なら誰でも、あなたのAIソフトや画像を使えてしまいます。
> **注意**
> `--listen` は、家庭内LANなど **信頼できるネットワークでだけ** 使ってください。
> カフェや公共のWi-Fiでは使わないようにしましょう。
---
## 10-5 スキル - Chatの得意分野を切り替える(v0.9.5〜)
**スキル** は、Chatに「どんな役割で、どんな形で答えるか」を教える指示書です。
Chatの上のドロップダウン(最初は「-- スキルなし --」)で選ぶと、そのスキルの指示が毎回の会話に付けて送られます
(指示の文章はチャットの画面には表示されません)。
### 最初から入っているスキル
Workflow Studioには、次の6つのスキルが入っています(名前は英語表示のままです)。
| スキル | 何をするか | 結果の使い方 |
| --- | --- | --- |
| T2I Prompt Enhancer | 短いアイデアを、生成UIのワークフローのモデルに合った書き方のプロンプトにする | プロンプトの下のボタン |
| Image to Prompt | TOOLSに入れた画像を再現するプロンプトを書く(画像を読み取れるモデルが必要) | プロンプトの下のボタン |
| Storyboard to Lab | 物語をシーンごとのプロンプトに分け、Labで連続生成できる形にする | **Labで開く** |
| Workflow Doctor | ワークフローの説明や、エラーの原因の相談にのる(10-7) | 文章で答える |
| SVG Icon Generator | 簡単なアイコンやイラストをSVG(ベクター画像)で描く | プレビューと **Galleryに保存** |
| Skill Creator | 質問に答えていくと、新しいスキルを作ってくれる(10-9) | **スキルとして保存** |
### プロンプトを作って、そのまま生成する(T2I Prompt Enhancer)
1. 生成UIに、使うワークフローを読み込みます。スキルはこのワークフローのモデルを見て、プロンプトの書き方を決めます。
2. AI TOOLのChatで、ドロップダウンから **T2I Prompt Enhancer** を選びます。
3. 「雨の夜の東京、ネオン、傘を持つ猫」のように、日本語で作りたい絵を書いて送信します。
4. 返事に、選んだ書き方(`Style:` の行)と、英語のプロンプトの枠が表示されます。その下のボタンで使います。
| ボタン | 動き |
| --- | --- |
| GenerateUIに反映 | 生成UIのポジティブ/ネガティブの欄に書き込む |
| このプロンプトで生成 | その場で生成し、チャットに画像を表示する |
| コピー | ポジティブプロンプトをコピーする |
モデルに合わせた書き方は、次のとおりです。
| 生成UIのモデル | プロンプトの書き方 |
| --- | --- |
| Qwen-Image、FLUX、Z-Image など | 英語の文章(ネガティブは、CFGが1前後なら空にする) |
| Illustrious、NoobAI、Pony など(SDXLのアニメ系) | Danbooruのタグ(品質タグから始める) |
| 写実系のSDXL、SD1.5 | 短い言葉をカンマで並べる |
> **ポイント**
> 「もっと夜らしく」「猫を2匹に」のように続けて頼むと、直したプロンプト全体を出し直します。
> **ポイント:今入っているプロンプトは渡さない**
> 生成UIに今入っているプロンプトは、ふだんはAIに渡しません。渡すと、小さなモデルが古いネガティブプロンプトなどを新しいプロンプトに混ぜてしまうためです。
> 「**今のプロンプトを改善して**」のように、今のプロンプトに触れて頼んだときだけ渡します。
### ストーリーボードから連続生成する(Storyboard to Lab)
1. 生成UIに、文章から画像を作る(T2I)ワークフローを読み込みます。
2. Chatで **Storyboard to Lab** を選び、「茶トラの子猫が朝目覚めて、庭で蝶を追いかけ、夕方に縁側で昼寝する。3シーンで」のように頼みます。
3. 返事に、シーンの一覧と **Labで開く(3シーン)** ボタンが表示されます。ボタンを押します。
4. 生成UIの **Lab**(第5章 5-7)が開き、まだ保存していないプランとして読み込まれます。
T2Iモードになり、Batchがシーンの数に、Prompt列がシーンごとのキーフレームになります。KSampler列には、全シーン共通のシード(`Seed …`)が入ります。
5. 内容を確かめて **Run** を押します。残したいときは **Plan Save** で保存します。
> **ポイント**
> 登場人物の見た目(髪・服の色など)は、全シーンで同じ文章が入るように書かれ、シードも固定されます。
> そのため、シーンが変わっても同じキャラクターに見えやすくなります。
### 返事の自動チェック
プロンプトやSVGを出すスキルでは、Workflow Studioが返事の形を自動で確かめます。
プロンプトの枠が崩れている、SVGが閉じていない、といった問題があると、
点線の枠で「**自動チェック:…**」と表示し、AIに1回だけ直させます。
説明だけを頼んだときなど、プロンプトやSVGを出していない返事はそのままにします。
### スキルの管理(鉛筆ボタン)
ドロップダウンの右の鉛筆ボタンで、スキルの一覧が開きます。
- **+ 新規** で新しいスキルを書く、各行の鉛筆ボタンで編集・削除する、**取り込み** で他のスキルを追加する(10-9)
- 最初から入っているスキルには **同梱** と表示されます。
編集していない同梱スキルは、Workflow Studioを更新すると自動で新しい版になります。
編集したスキルは自動では置き換わらず、新しい版があると **↻(更新)** ボタンが表示されます。
- スキルのファイルは `ComfyUI/user/default/Workflow-Studio/ai_skills/` に保存されます。
> **注意**
> ↻(更新)で新しい版に置き換えると、そのスキルに加えた編集は消えます。
---
## 10-6 生成した画像をチェックして直す(v0.9.5〜)
チャットから生成した画像が、頼んだとおりになっているかをAIに確かめさせる機能です。
足りないところがあれば、その点だけを直したプロンプトで描き直せます。
### 準備:判定用のモデル
チェックには、ふつうのチャット用モデルではなく、Ollamaの **意思決定モデル**(はい/いいえを確率で答えるモデル)を使います。
画像を読み取れるもの(作者の環境では `d3:4b`、`clef-flash`、`clef` で動作を確認)を、`ollama pull` で用意します。
> **ポイント:なぜ専用のモデルを使うのか**
> 作者の検証では、チャット用のモデルに画像を見せて「剣を持っているか」と聞くと、持っていないのに「はい」と答えることがありました。
> 意思決定モデルは「はい」の確率を数字で返すので、あいまいな答えを見分けられます。
1. AI TOOLの **設定** にある **CHAT 画像チェック** で、**判定モデル(Ollamaの意思決定モデル)** を選びます(一覧が空なら **更新**)。
2. **自動修正の回数** を選びます。最初は **0(ボタンで修正)** がおすすめです。
3. **設定を保存** を押します。
### 使い方
1. Chatの入力欄の下にある **画像をチェック** にチェックを入れます。
2. チャットから画像を生成します(AIの画像生成、または **このプロンプトで生成**)。
3. 画像の下に結果のカードが表示されます。AIが頼んだ内容を3〜8個の確認項目に分け、1つずつ確かめた結果です。
| 表示 | 意味 |
| --- | --- |
| ✔(満たしている) | 「はい」の確率が85%以上 |
| ?(はっきりしない) | 30〜85% |
| ✘(満たしていない) | 30%未満 |
4. ✘や?があるときは **足りない点を直して再生成** を押します。満たしていない項目だけをAIに伝え、プロンプトを書き直して描き直します。
自動修正の回数を1〜3にすると、ボタンを押さなくても、その回数まで自動でくり返します。
> **ポイント:SVGもチェックできる**
> SVG Icon Generatorで描いたSVGも、画像にしてから「頼んだものに見えるか」「部品(くちばし・翼など)が描けているか」をチェックします。
> **分かりにくい部分を描き直させる** で直させられます。
> **注意**
> - チェックは完ぺきではありません。とくに「誰が何を身につけているか」の取り違えに弱いため、
> 人物が複数いるときは「帽子は女の子ではない」のような逆向きの確認項目を自動で加えて補っています。
> - 描き直しの上手さは、チャット用モデルの力しだいです。直らないときは、頼み方を変えるか、モデルを変えてください。
> - 判定モデルもGPUのメモリを使います(`d3:4b` で約6GB)。読み込んだあとは、1回のチェックが数秒です。
---
## 10-7 ワークフローの不具合を相談する(Workflow Doctor、v0.9.5〜)
**Workflow Doctor** は、生成UIに読み込んだワークフローの説明や、エラーの原因の相談にのるスキルです。
AIの知識だけに頼らず、送信するたびにWorkflow Studioが実際に確かめた次の内容をAIに渡します。
| 確かめる内容 | 例 |
| --- | --- |
| インストールされていないノード | ComfyUIが知らないノードの名前 |
| ComfyUIの一覧に無いファイル・設定値 | 見つからないモデルやLoRA(似た名前の候補も) |
| 線のつながり | つながっていない入力、存在しないノードへの線 |
| 直近の実行エラー | ComfyUIの履歴に残ったエラーの内容 |
| ComfyUIのログ | ログに出た最新のエラーの部分 |
### 使い方
1. 相談したいワークフローを生成UIに読み込みます(エラーの相談なら、一度実行してエラーを出しておきます)。
2. Chatで **Workflow Doctor** を選びます。
3. 「実行する前に問題がないか確認して」「さっきのエラーの原因は?」のように聞きます。
> **例**
> LoRAのファイルを別のフォルダに移したワークフローで聞くと、
> 「`temp\` のフォルダに指定されたファイルが見つからない。`Illustrious\` に同じ名前のファイルがある」と、直し方まで答えました。
> **ポイント**
> 確かめる内容は、送信のたびに作り直されます。直したあとは、もう一度聞いてみてください。
---
## 10-8 画像の文字を読み取る(OCR、v0.9.5〜)
TOOLSで、画像に写っている文字を読み取れます。読み取った文字は、そのまま翻訳やChatに送れます。
### 準備:OCR用のモデル
OCRには、チャット用とは別の、文字を読むのが得意なモデルを使います。Ollamaで用意してください。
作者の環境で、横書きと縦書きの日本語・英語がまじった画像を読ませた結果は次のとおりです。
| モデル | 時間 | 読み取り |
| --- | --- | --- |
| LightOnOCR-3(`maternion/LightOnOCR-3:4b`) | 約1秒(位置つきは約6〜8秒) | すべて読めた |
| PaddleOCR-VL(マンガ向けの版) | 約4秒 | すべて読めた |
| DeepSeek-OCR系 | 約14秒 | すべて読めた(いつも位置つき) |
| PaddleOCR-VL 1.6、GLM-OCR | 約11〜15秒 | 縦書きを読み落とした |
### 使い方
1. TOOLSに画像をドロップします。
2. 作業で **文字起こし(OCR)** を選びます。
3. **OCRモデル** を選びます。名前に「ocr」を含むOllamaのモデルが、向いている順に並びます(一覧が空なら ↻ を押します)。
4. **実行** を押します。読み取った文字が、結果の欄に1行ずつ表示されます。
> **ポイント:文字の位置を表示**
> **文字の位置を表示** にチェックを入れると、画像の上に番号つきの枠が重なり、どこの文字かが分かります。
> 枠をクリックすると、結果の欄の同じ行が選ばれます。
> 位置を返せるのは LightOnOCR と DeepSeek-OCR系だけです。ほかのモデルでは、このチェックは選べません。
> **ポイント**
> 結果が空だったり、同じ行がくり返されたりしたときは、自動で白黒(グレースケール)にして1回だけ読み直します。
---
## 10-9 スキルを自作・取り込む(上級、v0.9.5〜)
### Skill Creatorで作る
Chatで **Skill Creator** を選び、「〇〇が得意なスキルを作りたい」と話しかけます。
AIが1つずつ質問してくるので答えていくと、スキルの下書きができます。
返事の下の **スキルとして保存** を押すと、スキルの一覧に下書きが入った状態で開くので、確かめて保存します。
「このスキルの、ここがうまくいかない」と相談して、今あるスキルを直してもらうこともできます。
### スキルのファイルの中身
スキルは、テキストファイル(`.md`)です。先頭の `---` で囲んだ部分に名前と説明を書き、その下にAIへの指示を書きます。
先頭の部分の `metadata:` には、次の設定も書けます(書かなくてもかまいません)。
| 設定 | 意味 |
| --- | --- |
| `output` | `prompt` か `svg`。返事の形を自動でチェックし、ボタンを付ける |
| `tools` | `[]` にすると、AIが自分で画像生成を始めない |
| `temperature` | 答えのばらつき(0.3なら安定、0.9なら自由) |
| `think` | 考える(Thinking)モードを、このスキルだけオン/オフにする |
| `context` | AIに渡す情報。`workflow`(ワークフローの要約)、`current-prompt`(今のプロンプト)、`workflow-detail`/`logs`(Workflow Doctorと同じ確認内容) |
### ほかのスキルを取り込む
スキルの一覧の **取り込み** で、次のファイルを追加できます。
- `.md` のスキル1つ
- `SKILL.md` を含むフォルダをZIPにしたもの(**Agent Skills** という、多くのAIツールで共通の形式)
フォルダの中のテキストファイル(参考資料など)も一緒に取り込まれ、AIが必要になったときだけ読みに行きます
(ツール呼び出しに対応したモデルが必要です)。
> **注意**
> - 取り込むのはテキストファイルだけです。スクリプトなどのプログラムは取り込まず、実行もしません。
> - スキルの指示は、そのままAIに渡されます。他の人が作ったスキルは、中身を読んでから使ってください。
> **この章のまとめ**
> - ローカルAI(Ollamaなど)を用意すると、翻訳・チャット・画像の読み取りができる。入力内容は外部に送られない
> - AI TOOLタブの設定で、バックエンド・アドレス・モデルを選び、接続テストをする
> - プロンプトタブとTaggerタブには、別に歯車ボタンの設定がある
> - AIと画像生成はVRAMを取り合う。「モデルをアンロード」や「VRAM調整(Ollama)」で解放する
> - 標準では自分のパソコンのAIにだけつながる。`--listen` は信頼できるネットワークだけで
> - **スキル** で得意分野を切り替える。T2I Prompt Enhancer でプロンプトを作り、ボタンで生成UIに反映・生成できる
> - **画像をチェック** をオンにすると、意思決定モデルが生成画像を確かめ、足りない点だけ直して描き直せる
> - Workflow Doctor は、実際に確かめた事実をもとにワークフローの不具合を相談にのる
> - TOOLSの **文字起こし(OCR)** で画像の文字を読み取り、翻訳やChatへ送れる
@@ -0,0 +1,353 @@
# 第11章 さらに広がる機能(紹介編)
ここまでで、Workflow Studioの基本的な使い方はひととおり身につきました。
この章では、まだ紹介していないタブや機能を、**どんなことができるか** を中心に紹介します。
すべてを今すぐ使う必要はありません。「こんなこともできるんだ」と頭の片隅に置いておき、
必要になったときに試してみてください。
---
## 11-1 ノードタブ - インストール済みのノードを一覧する
**ノード(Nodes)** タブでは、ComfyUIにインストールされているすべてのノードを一覧できます。
- **検索・絞り込み** - 名前、カテゴリー、パッケージ(どのカスタムノードに含まれるか)で絞り込めます。数千個あっても、一覧が先に表示されて検索も軽く動きます(v0.8.0〜)。
- **詳細パネル** - ノードを選ぶと、入力と出力の種類(I/O仕様)を確認できます。
- **お気に入り・グループ・タグ** - よく使うノードを整理できます。
ここでお気に入りやグループに登録したノードは、WS Libraryの **N** タブに表示され、
ノード画面へドラッグで置けるようになります(第9章)。
> **こんなときに**
> 「カスタムノードを入れたけど、どんなノードが増えたのか分からない」
> → **パッケージ** で絞り込むと、そのカスタムノードに含まれるノードの一覧が見られます。
---
## 11-2 Tagger - 画像からタグを自動で作る
**Tagger** タブは、画像を読み取って、その画像を表すタグ(`1girl, long hair, smile` など)を自動で作る機能です。
アニメ系のモデルのプロンプト作りや、学習用画像のタグ付けに使えます。
### 3つのサブタブ
| サブタブ | できること |
| --- | --- |
| Single | 1枚の画像からタグを作る |
| Batch | フォルダ内の画像に、まとめてタグを付ける(画像と同じ名前の `.txt` ファイルを書き出すことも可能) |
| DB | タグを付けた画像の記録を検索・編集する |
### 使い方(Single)
1. 画像を、プレビュー部分にドロップします(ギャラリーの詳細パネルの **Tagger** ボタンからも開けます)。
2. 使うモデルを選び、実行ボタンを押してタグを作ります。
3. 結果を使います。
| ボタン | 動き |
| --- | --- |
| GenUI:P | 生成UIのポジティブプロンプトに追加する |
| Send to Prompt | プロンプトタブのポジティブプロンプトに追加する |
**しきい値** のスライダーで、どのくらい確かなタグまで出すかを調整できます。
数値を上げるとタグは少なく確かなものだけに、下げると多くなります。
### 準備が必要
Taggerを使うには、次の準備が必要です。
1. **タグ付け用のモデル**(WD Tagger など)を、ComfyUIの `models/tagger/(モデル名)/` フォルダに置きます。
フォルダには、`.onnx` ファイルと `selected_tags.csv` を入れます。
**PixAI Tagger v1.0**(v0.8.10〜)も使えます。Hugging Faceのコミュニティ変換版(`Mexes/pixai-tagger-v1.0-onnx-fp32-fp16-int8`)から、
`.onnx` を1つ(GPUなら `model_fp16.onnx` がおすすめ)と、`selected_tags.csv`・`preprocess.json` を同じフォルダに入れると、自動で判別されます。
PixAIのモデルを選ぶと、しきい値のスライダーが、PixAIの推奨値(0.17/0.27)に切り替わります。
2. **追加のPythonパッケージ** をインストールします。
Workflow Studioのフォルダにある `requirements.txt` を使います。
```
(ComfyUIのPython) -m pip install -r requirements.txt
```
「ComfyUIのPython」の場所は環境によって違います。第2章 2-6 の表で確認してください
(例:Windowsのポータブル版では `python_embeded` フォルダの中の `python.exe`)。
> **注意**
> パソコンにもともと入っているPythonにインストールしても、ComfyUIからは使えません。
> 必ず、**ComfyUIが使っているPython** にインストールしてください。
> 分からない場合は、ComfyUI Managerの機能でパッケージをインストールする方法もあります。
> **注意:GPUで動いているか確認する**
> Taggerは、NVIDIAのGPUで動かすと1枚あたり1秒かからないのに対し、CPUで動くとPixAIでは1枚あたり20秒ほどかかります。
> とても遅いときは、GPU用のパッケージが正しく入っているか確認してください(第12章 12-5)。
> **ポイント**
> ローカルAI(第10章)の画像読み取り(VLM)と組み合わせて、両方の結果をまとめたタグを作ることもできます。
---
## 11-3 Image Edit - 生成した画像にちょっと手を加える
**Image Edit** タブは、レイヤー機能を持った簡易画像編集ツールです。
生成した画像に文字を入れたり、一部を描き直すためのマスクを作ったりできます。
### 主な機能
| ツール | できること |
| --- | --- |
| Select | レイヤーの移動・拡大縮小・回転 |
| Draw | ペンで描く(ペンタブレットの筆圧にも対応) |
| Text | 文字を入れる |
| Shape | 四角・丸・線を描く |
| Fill | 塗りつぶし・グラデーション |
| Mask | 描き直したい部分を指定する(マスク作成) |
| Blur | ぼかし・モザイク |
| BG Remove | 背景を消す |
| Inpaint | マスクした部分を、生成UIのワークフローで描き直す |
- 画像は、ドラッグ&ドロップ、**File Open**、またはギャラリーの **Image Edit** ボタンで開きます。
- 編集した画像は、**Save PNG**(ダウンロード)、**Save to Gallery**(ギャラリーに保存)などで保存します。
- **Ctrl+Z** で元に戻す、**Ctrl+Y** でやり直しができます。
### Drawの色とショートカット(v0.9.3〜)
**Draw** を選ぶと、ツールの2段目に **カラーパレット** が表示されます。
- **色1/色2**、基本色20色、カスタム色20枠が並びます。色をクリックすると、その色で描けます。
- **+** か空いている枠をクリックすると、今の色をカスタム色に登録します。色の上で右クリックすると、登録・編集・削除ができます(基本色も変えられ、最初の色に戻せます)。
登録した色は、ブラウザごとに保存されます。
- 右側の **Color** の隣にある **スポイト** ボタンで、キャンバスの色を拾えます(Escで取り消し)。
- 描き始めの色は黒(色1)と白(色2)です。
| キー(Draw中) | 動き |
| --- | --- |
| D/E | 描く(Draw)/消す(Erase) |
| X | 色1と色2を入れ替える |
| [ / ] | ブラシを小さく/大きくする(実際は半角の `[` `]`) |
| B | ブラシの選択を開く・閉じる(Escでも閉じる) |
### インペイント(部分の描き直し)
Image Editのいちばん便利な使い方が、**インペイント** です。
「顔だけ描き直したい」「手が崩れたところだけ直したい」というときに使います。
1. 生成UIに、インペイント用のワークフロー(付属サンプルの `ws_inpaint_basic` など)を読み込みます。
2. Image Editで画像を開き、**Mask** で描き直したい部分を塗ります。
3. **Inpaint** ツールで実行すると、塗った部分だけが描き直されます。
> **ポイント**
> 背景を消す機能の高品質版や、より高度なマスク機能を使うには、
> 作者の別のカスタムノード **comfyui-mask-editor-one** が必要です。
> PSD形式での保存・読み込みには、追加のPythonパッケージ(psd-tools)が必要です。
---
## 11-4 Video - 動画を作る・つなぐ
**Video** タブは、動画生成モデルを使って、画像や文章から短い動画を作るための専用画面です。
### 対応している動画生成モデル
- **MiniMax H3**(画像から動画)
- **LTX-2.5**(文章から動画、画像から動画、最初と最後の画像を指定して動画)
- **Wan2.2**(文章から動画)
- **FastH3 OmniRef/Pika-H3-Flex**(MiniMax H3の参照画像から動画。v0.8.14〜)
対応するワークフローを、ワークフロータブの **Videoに読み込み** で読み込んで使います(第4章 4-6)。
### 主な機能
| サブタブ | できること |
| --- | --- |
| Plan | 動画を「ブロック」に分け、ブロックごとにプロンプト・長さ・最初/最後の画像を決めて、順番に生成する |
| Edit | 生成した動画や画像を並べて、切り取り・つなぎ合わせ・文字入れ・BGM付けをして書き出す |
| Asset | 生成した動画や画像、音声を一覧する。音声は「BGMに設定」や「サウンドとして追加」ができる(種類フィルタの Audio Only で音声だけ表示) |
そのほか、動画の好きな場面を静止画として保存する **Frame** や、動画をGIFアニメに変換する **GIF** の機能もあります。
### アセット登録と音声変換(v0.8.9〜)
- **アセット登録** - 左側の動画ソースパネルのボタンです。パソコン内の動画を、outputフォルダ(`output/video`)へコピーして「Video Assets」グループに登録します。
登録すると、Assetサブタブの一覧に出て、Editなどで使えるようになります。
すでにoutputフォルダにある動画(Assetで選んだものや生成結果)は、コピーせずにグループへの登録だけを行います。同じ名前があるときは、名前の後ろに連番が付きます。
- **音声変換...** - Editサブタブの、クロップのボタンの横にあるボタンです。選んだ動画クリップの音声だけを、FLAC・MP3・Opus・M4A・WAVに変換して `output/video` に保存します。
ギャラリーの変換と同じ画面です。音声が入っている動画クリップだけが対象です。
- 音声変換のボタンを押すと、次の画面が開きます。**出力形式**(FLACなど)、**サンプルレート**、**チャンネル** を選び、**変換** を押します。
「埋め込みのprompt/workflowタグを引き継ぐ」にチェックを入れると、タグを持つ形式(FLACなど)で、元の動画の情報を引き継げます。
- 動画ソースパネルは少し広くなり、ファイル名が1行で表示され、ボタンも横一列に並ぶようになりました。
### 参照画像から動画(FastH3 OmniRef/Pika-H3-Flex)(v0.8.14〜)
このタイプのワークフローは、最初と最後の画像ではなく、**参照画像** をもとに動画を作ります。
そのため、Planサブタブの画像の欄は、次のように名前が変わります。
| 欄 | このタイプでの意味 |
| --- | --- |
| Reference Image | 1枚目の参照画像(ふだんの First Image の欄) |
| Reference Image 2 (optional) | 2枚目の参照画像(ふだんの Last Image の欄) |
- プロンプト・長さ(秒)・縦横比などは、ほかの動画ワークフローと同じように使えます。
- 画像の欄を **None** や **Clear** で空にしたブロックは、ワークフローに参照画像が設定されていても、その画像を使わずに生成します。
「+ Block」で追加したブロックは画像が空の状態で始まるので、参照画像が必要なブロックでは、それぞれ画像を指定してください。
- 画像の選び方で **Use previous block's last frame**(前のブロックの最後のフレームを使う)を選ぶと、前のブロックで実際に生成された動画の最後のフレームが、そのブロックの1枚目の参照画像になります。
- Assetサブタブのボタンも「Reference Imageに設定」「Reference Image 2に設定」に変わります。
> **注意**
> 動画生成は、画像生成よりもはるかに多くのGPUメモリと時間を使います。
> 使うモデルの必要スペックを確認してから試しましょう。
---
## 11-5 設定タブの主な項目
**設定** タブでは、Workflow Studio全体の設定をします。
項目ごとに折りたたまれているので、見出しをクリックして開きます。
ここでは、これまでの章で紹介していない項目を中心に紹介します。
| 項目 | 内容 | 関連する章 |
| --- | --- | --- |
| テーマ | 画面の配色・フォント | 第3章 |
| UI言語/要約言語 | 表示言語 | 第2章 |
| 文字サイズ | プロンプト入力欄などの文字の大きさ | 第3章 |
| ワークフローデータフォルダ | ワークフローを読み込むフォルダ | 第4章 |
| ギャラリー Outputフォルダ | ギャラリーで表示するフォルダ | 第8章 |
| デフォルトワークフロー | Workflow Studioを開いたとき、生成UIに自動で読み込むワークフロー | 下記 |
| デフォルトCheckpoint | 生成UIにワークフローを読み込むとき、モデルを自動で差し替える | 下記 |
| CivitAI APIキー | Civitaiの情報取得用(省略可) | 第6章 |
| Eagle 連携 (オプション) | 生成画像をEagleに自動保存 | 第8章 |
| VRAM調整(Ollama) | 生成前にAIモデルをメモリから降ろす | 第10章 |
| データ管理 | 設定やデータのバックアップ・復元 | 下記 |
### デフォルトワークフロー
いつも使う基本のワークフローを設定しておくと、Workflow Studioを開いたときに生成UIへ自動で読み込まれます。
ワークフロータブの詳細画面の **デフォルトに設定** ボタンからも設定できます。
### デフォルトCheckpoint
ネットから入手したワークフローには、手元にないモデルが指定されていることがよくあります(第5章 5-3)。
**デフォルトCheckpoint** を設定してオンにしておくと、生成UIにワークフローを読み込むたびに、
Checkpointが自動で指定のモデルに差し替わります。モデルの選び直しの手間が省けます。
### データ管理(バックアップ)
**データ管理** では、Workflow Studioで整理した情報(お気に入り・バッジ・グループ・プリセットなど)を
ファイルに書き出したり(エクスポート)、読み込んで元に戻したり(インポート)できます。
**フルバックアップ (ZIP)** を使うと、スタイルやLabのプランなどのフォルダ単位のデータも含めて、まとめてバックアップできます。
> **ポイント**
> パソコンの買い替えやComfyUIの入れ直しの前には、必ずバックアップを取っておきましょう。
> 時間をかけて整理した情報を、そのまま新しい環境に引き継げます。
> (安全のため、APIキーはバックアップに含まれません)
## 11-6 連携ソフト:ComfyUI Comic Creator - 生成した画像でマンガを作る
**ComfyUI Comic Creator**(以下、Comic Creator)は、Workflow Studioと同じ作者が開発している、
ComfyUI上で動く **マンガ制作用** のカスタムノードです。
Workflow Studioとは別にインストールするソフトですが、両方を入れると連携して使えます。
### Comic Creatorでできること
| 機能 | 内容 |
| --- | --- |
| コマ割り | テンプレートを選んで、ページにコマを作る(自作テンプレートも登録可能) |
| 画像の配置 | コマに画像をドラッグ&ドロップで配置する |
| フキダシ・テキスト | 縦書き・横書きのセリフ、さまざまな形のフキダシ、効果文字を入れる |
| マンガ効果 | 集中線・スクリーントーン・網点(ハーフトーン)などを加える |
| 画像の編集 | レイヤー付きの画像編集(Image タブ) |
| AIでの自動作成 | お題から、ストーリー・脚本・コマ割りをAIで下書きする(Auto タブ) |
| 書き出し | JPEG/PNG/WebP/SVG/PDF/**EPUB** 形式で出力する |
### Workflow Studioとの連携
両方をインストールすると、次のような連携ができます。
- **Comic Creatorの中でWorkflow Studioのギャラリーを使う**
Comic Creatorの中にWorkflow Studioのギャラリーが表示され、生成した画像を選んで **Send CC Layout**(レイアウトタブのコマ/オーバーレイへ)または **Send CC Image**(Imageタブへ)ボタンで送れます。
(Send CCボタンは、Comic Creatorの中で開いたギャラリーにだけ表示されます)
- **Comic Creatorから画像を生成する**
Comic Creatorの画面から、Workflow Studio(生成UI)を通して、コマに入れる画像を生成したり(T2I)、
描いたラフや配置した画像をもとに描き直したり(I2I・インペイント)できます。
Comic CreatorのNanobanana+タブ(ローカルモード)からは、複数の参照画像を使う画像編集のワークフロー(Flux.3 Klein、Qwen Image 2.1 など)も実行できます(v0.8.12〜)。
「Workflow Studioで画像を作る → Comic Creatorでマンガにまとめる」という流れで、
画像生成からマンガの完成までを、ComfyUIの中だけで行えます。
### インストールと起動
1. ComfyUI Managerの **Install via Git URL** に、次のURLを入力してインストールします。
```
https://github.com/ketle-man/comfyui-comic-creator
```
(手動の場合は、Workflow Studioと同じく `ComfyUI/custom_nodes/` に `git clone` します)
2. ComfyUIを再起動します。
3. ComfyUIの上部メニューに追加された **CC** ボタンを押すと、Comic Creatorが新しいタブで開きます。
> **ポイント**
> Comic Creatorの詳しい使い方は、アプリ内の **ヘルプ** タブ(日本語対応・検索機能あり)を参照してください。
---
## 11-7 TagSpace - タグとプロンプトを「意味の地図」から選ぶ(v0.9.0〜)
**TagSpace** タブは、Workflow Studioの中にあるプロンプトやタグ、生成した画像を、
**意味の近さ** にしたがって3Dの空間に並べて表示する機能です。
近くにあるものほど意味が近いので、「この雰囲気に近いタグは何だろう」と眺めながら、タグを選んでプロンプトを作れます。
### 空間に並ぶもの
| 種類 | 中身 | 表示 |
| --- | --- | --- |
| カード | ギャラリーの **ImagePrompt**(第8章 8-4)の見本画像と、そのプロンプト | サムネイル |
| タグ | ImagePromptのプロンプト・生成画像に埋め込まれたプロンプト・ワイルドカード・Tagger DB・Promptライブラリの文を、カンマで区切った語 | 出典ごとに色分けした点 |
| Gallery点 | ギャラリーの生成画像の一部(それぞれ、自分のプロンプトのタグを持つ) | 薄い点 |
### 準備
ギャラリーのセマンティック検索(第8章 8-6)と同じく、**Ollama** と **EmbeddingGemma 2** が必要です。
ギャラリーのセマンティック検索の索引を、Gemini Embedding 2で作っている場合は、先にEmbeddingGemma 2で作り直してください。
### 空間を作る
ツールバーで設定を選び、**作成 / 更新** を押します。
| 項目 | 内容 |
| --- | --- |
| タグ数 | 空間に入れるタグの数。出典ごとに、よく使われている順に交互に選ぶ |
| 全タグ | オンにすると、タグ数の代わりに候補のタグをすべて使う(上限2万件) |
| 最低出現回数 | この回数未満しか使われていないタグを除く。2にすると、1回だけの表記ゆれなどが消える |
| ワイルドカード | ワイルドカードをタグの材料に含めるかどうか |
| Gallery点数 | 表示する生成画像の数(0で表示なし) |
| Tagger補完 | プロンプトを取り出せない生成画像に、Taggerのモデルでタグを付ける(OFF/モデル名。「Gallery点のみ」か「プロンプトの無い全画像」を選ぶ) |
- 1回目は、画像のプロンプトの読み取りやタグの準備に数分かかります。2回目からは、前回の結果を使うので数秒〜数十秒で終わります。
- 状態表示に、カード・タグ・Gallery点の数が出ます。候補の一部だけを使ったときは「(候補 N)」と出ます。
> **ポイント**
> タグの種類の大半は、ワイルドカードにしか出てこない語です。
> 「ワイルドカード」をオフにして「全タグ」をオンにすると、自分が実際に使ってきたタグだけを全部並べられます(検証環境では約1万種)。
> **注意**
> Tagger補完は、CPUで動くと1枚あたり0.5秒ほどかかります。「プロンプトの無い全画像」を選ぶ前に、TaggerがGPUで動いているか確認してください(第12章 12-5)。
### 見る・探す
- **ドラッグ** で回転、**右ドラッグ** で移動、**ホイール** でズームします。
- 点にカーソルを合わせると、名前・サムネイル・プロンプトや出典が表示されます。
- ツールバーのチェックボックスで、カード・サムネイル・タグ・Galleryの表示や、**発光**(夜空に光る星のような表示)・**またたき** を切り替えます。
出典ごとのチェックボックスで、タグを出典ごとに表示・非表示にできます。
- 右上の検索欄に言葉や文章を入れて **Enter** を押すと(日本語も可)、その意味の位置に白い印が付き、近いカードやタグが一覧に出ます。
### タグを選んでプロンプトを作る
1. カードやタグをクリックすると選択され、右の **近くの要素** に、意味の近いカード・タグが並びます。
(Gallery点をクリックすると、似ている生成画像も並びます)
2. プロンプトに入れたいものを **ダブルクリック** するか、**選択中** の **追加**、または一覧の **+** を押します。
カードはプロンプト全体、タグはタグそのもの、Gallery点はその画像のタグが、右端の **プロンプト** 欄に入ります。
3. **GenerateUIへ追記(Positive)** で、生成UIに読み込んだワークフローのポジティブプロンプトに追記します(先に生成UIでワークフローを読み込んでおきます)。
**コピー** でクリップボードにコピーすることもできます。
### カードや生成画像のタグで探す
カードやGallery点を選ぶと、**選択中** に、そのプロンプトのタグがボタンで並びます。
タグをクリックして選び(もう一度で解除)、**選択したタグで空間検索** を押すと、選んだタグで空間を検索します。
「この画像の、髪型と服装だけに近いものを探したい」といったときに便利です。
タグを選んでいる間は、**選択中** の **追加** ボタンが **選択したタグを追加(N)** に変わり、選んだタグだけをプロンプト欄に追加できます。
タグを選んでいなければ、これまでどおり全体(カードはプロンプト全文、Gallery点はすべてのタグ)が追加されます。
Gallery点を選んでいるときは、**選択中** に **Galleryで開く** ボタンも表示されます。
押すとギャラリータブに切り替わり、その画像のフォルダが選ばれ、画像一覧でその画像が選択された状態になります(右の詳細も開きます)。
画像が一覧に必ず出るよう、ギャラリーの検索・お気に入り・タグ・グループの絞り込みは解除されます。
カードのプロンプト全文は、**プロンプト全文** を開くと読めます。
---
> **この章のまとめ**
> - ノードタブでインストール済みのノードを一覧・整理でき、WS Libraryで使える
> - Taggerは画像からタグを自動生成する。モデルと追加パッケージの準備が必要
> - Image Editはレイヤー付きの簡易編集ツール。インペイントで部分的な描き直しもできる
> - Videoタブで、対応する動画生成モデルを使った動画作りと簡単な編集ができる
> - 設定タブの「デフォルトCheckpoint」と「データ管理(バックアップ)」は特に便利
> - 同じ作者の Comic Creator と組み合わせると、生成した画像でマンガを作れる
> - TagSpaceでは、手持ちのタグ・プロンプト・生成画像を意味の近さで3Dに並べ、眺めながらタグを選んでプロンプトを作れる
@@ -0,0 +1,230 @@
# 第12章 困ったときは(トラブルシューティング/FAQ)
この章では、Workflow Studioを使っていてよく起こるトラブルと、その対処方法をまとめます。
困ったときは、まず **「症状」** の見出しから、自分の状況に近いものを探してください。
> **まず試してほしい3つのこと**
> どんなトラブルでも、次の3つで解決することがよくあります。
> 1. ブラウザのページを再読み込みする(F5キー)
> 2. Workflow Studioの各タブの **更新**(Refresh)ボタンを押す
> 3. ComfyUIを完全に終了して、起動し直す
---
## 12-1 起動・接続のトラブル
### Wボタンが表示されない
| 原因 | 対処 |
| --- | --- |
| インストール後にComfyUIを再起動していない | ComfyUIを完全に終了して、起動し直す(ブラウザの再読み込みだけでは反映されません) |
| フォルダの場所が違う | `ComfyUI/custom_nodes/ComfyUI-Workflow-Studio` にフォルダがあるか確認する。フォルダが二重になっていないかも確認 |
| ComfyUIが古い | ComfyUI本体を更新する |
### Workflow Studioのページが開かない
- ComfyUIは起動していますか? Workflow StudioはComfyUIの中で動いているため、ComfyUIを終了すると開けません。
- アドレスの **ポート番号** は合っていますか? ComfyUIを開いているブラウザのアドレス欄で確認できます(第2章 2-4)。
### 生成UIが「Disconnected」になる
1. ComfyUIが起動していて、ComfyUIの画面で画像を生成できるか確認します。
2. 生成UIの上部の **Connect** を押します。
3. それでもつながらない場合は、ブラウザのページを再読み込みします。
### 更新したのに動きが変わらない
Workflow Studioを更新(アップデート)したあとは、必ずComfyUIを再起動してください。
それでも古い動きのままのときは、ブラウザのキャッシュが残っている可能性があります。
**Ctrl+F5**(強制再読み込み)を試してください。
---
## 12-2 ワークフローのトラブル
### ワークフローが一覧に出てこない
| 原因 | 対処 |
| --- | --- |
| 一覧が古いまま | ワークフロータブの **更新** を押す |
| 絞り込みがかかっている | ツールバー右端の **× クリア** を押す |
| 別のフォルダに保存している | 設定タブの「ワークフローデータフォルダ」を確認する |
### ノードが赤く表示される(missing)
ワークフローを読み込んだとき、ノードが赤く表示されたり、「見つからないノードがあります」と表示されたりすることがあります。
これは、そのワークフローに必要な **カスタムノードがインストールされていない** という意味です。
**対処**
1. ComfyUIの **Manager** を開きます。
2. **Install Missing Custom Nodes**(不足しているカスタムノードをインストール)を選びます。
3. 表示されたカスタムノードをインストールし、ComfyUIを再起動します。
> **ポイント**
> ワークフロータブの **GenerateUIチェック**(第4章 4-7)で「変換 ×」になるのも、多くはこれが原因です。
> バッジにマウスを重ねると、足りないノードの名前が表示されます。
### 生成UIで「使えない」と判定された
| 判定 | 意味と対処 |
| --- | --- |
| 変換 × | 必要なカスタムノードがない → 上の「ノードが赤く表示される」を参照 |
| 変換 △ | 手元にないモデルなどが指定されている → 生成UIで選び直す(12-3) |
| 操作性 △/× | 生成UIに表示されない設定が多い → 生成はできることが多い。細かい設定はComfyUIのノード画面で変更する |
---
## 12-3 生成のトラブル
### 設定を変えたのに、結果が変わらない
いちばん多い原因は、**Apply の押し忘れ** です。
生成UIでは、モデルやSettings(ステップ数・サイズなど)の値を変えたあと、**Apply** を押さないとワークフローに反映されません(第5章 5-3)。
ただし、Settingsタブの右端のスイッチの列は、クリックした時点で反映されます(第5章 5-4)。
右側の **Raw JSON** を見ると、実際に反映されている値を確認できます。
### 読み込んだワークフローとは違う内容で生成される
**Batch** サブタブで、バッチの種類のチェックがオンのまま残っていると、
Generateを押したときにバッチ生成が実行されます(第5章 5-5)。
Batchサブタブを開き、Batch Queueの列のチェックをすべてオフにしてください。
### 思っていたのと違うモデルで生成される
ワークフローに保存されているモデルが手元にないと、別のモデルに自動で差し替えられることがあります。
その場合は「Checkpoint「…」が見つからず、無警告で別のモデルに差し替えられました」と表示されます。
(v0.8.7〜は、フォルダ名だけが違う同名のモデルが1つだけ手元にあれば、それに自動で差し替わります)
生成UIの **Model** サブタブで、使いたいモデルを選び直して **Apply** を押してください。
いつも同じモデルを使いたい場合は、設定タブの **デフォルトCheckpoint** を使うと便利です(第11章 11-5)。
### 同じ設定なのに、同じ画像にならない
生成パネルのシードのモードが **Random** になっていませんか?
同じ画像を作るには、シードを **Workflow**(ワークフローのシードをそのまま使う)か **Fixed**(Seed欄の値で固定)にする必要があります。
同じシード・同じ設定で続けて生成して新しい画像が増えないのは、ComfyUIが計算済みの結果を再利用しているためで、故障ではありません。
ギャラリーの画像を再現する手順は、第8章 8-3を参照してください。
### 生成が遅い・エラーで止まる(メモリ不足)
| 原因 | 対処 |
| --- | --- |
| ローカルAIがGPUメモリを使っている | AI TOOLの **モデルをアンロード** を押す。設定タブの **VRAM調整(Ollama)** を使う(第10章 10-3) |
| 画像サイズやBatch Sizeが大きすぎる | Settingsサブタブで Width・Height・Batch Size を小さくする |
| モデルがGPUに対して大きすぎる | より小さいモデル(GGUF版など)を使う |
### モデルを移動・削除したあとにエラーが出る
ComfyUIが、モデルの古い場所を覚えていることがあります。
ComfyUIのメニューの **Hard Reset**(キューのメニュー内)を実行するか、ComfyUIを再起動してください。
### 新しく入れたモデルが一覧に出てこない
生成UIの **Refresh Models**、またはモデルタブの **更新** を押してください。
それでも出てこない場合は、モデルを置いたフォルダが正しいか(第6章 6-1)を確認します。
---
## 12-4 AI機能のトラブル
### AI機能が「Disconnected」になる・つながらない
1. Ollamaなどのソフトが起動しているか確認します。
LM StudioやLemonadeの場合は、アプリ内で **サーバーを起動** し、モデルを読み込んでおく必要があります。
2. 設定のアドレス(ポート番号)が、ソフト側と合っているか確認します。
3. **接続テスト** を押して、結果を確認します。
### AI TOOLタブでは使えるのに、プロンプトタブでは使えない
AIの設定は、場所によって別々に保存されています(第10章 10-2)。
| 場所 | 設定する場所 |
| --- | --- |
| AI TOOLタブ/WS LibraryのAタブ | AI TOOLタブの設定(共通) |
| プロンプトタブ | プロンプトタブのAI Assistantの **歯車ボタン** |
| Taggerタブ | Taggerタブの **歯車ボタン** |
### モデルの一覧が空になる
AIモデルがダウンロードされていない可能性があります。
Ollamaの場合は `ollama pull (モデル名)` でモデルをダウンロードしてから、モデル選択の **更新** を押してください。
### 別のパソコンのOllamaにつながらない
安全のため、標準では同じパソコンの中のAIソフトにしかつながりません。
環境変数 `WFS_ALLOWED_BACKEND_HOSTS` の設定が必要です(第10章 10-4)。
### 画像チェックの判定モデルや、OCRモデルが一覧に出てこない
- 判定モデルの一覧には、Ollamaの **意思決定モデル** のうち、画像を読み取れるものだけが出ます。
ふつうのチャット用モデルは出ません。Ollamaを新しい版に更新し、意思決定モデルを `ollama pull` してから **更新** を押します(第10章 10-6)。
- OCRモデルの一覧には、名前に「ocr」を含むOllamaのモデルだけが出ます。取得後に ↻ を押します(第10章 10-8)。
- どちらも、AI TOOLのバックエンドがOllamaならそのアドレス、そうでなければ `http://localhost:11434` のOllamaを見に行きます。
### 画像チェックで、何度直しても満たされない
- 描き直しの上手さは、チャット用モデルの力しだいです。頼み方を短く具体的にするか、別のモデルを試してください。
- チェック自体もまちがえることがあります。画像を自分の目でも確かめてください。
- 自動修正の回数を上げすぎると、生成の時間とGPUのメモリを使い続けます。最初は「0(ボタンで修正)」がおすすめです。
### AIがスキルの資料を読まない・チャットから画像を生成しない
取り込んだスキルの参考資料を読む機能と、チャットからの画像生成は、
**ツール呼び出し(Tool Calling)に対応したモデル** でないと動きません。対応しているモデルに切り替えてください。
---
## 12-5 そのほかの機能のトラブル
### Taggerにモデルが表示されない
- `ComfyUI/models/tagger/(モデル名)/` フォルダに、`.onnx` ファイル(と `selected_tags.csv`)が入っているか確認します。
- 追加のPythonパッケージ(onnxruntime)が、**ComfyUIが使っているPython** にインストールされているか確認します(第11章 11-2)。
### Taggerがとても遅い(GPUが使われない)
ComfyUIのPythonに、CPU版の `onnxruntime` とGPU版の `onnxruntime-gpu` が両方入っていると、GPUが使われなくなります。
(ほかのカスタムノードのインストールで、CPU版が入ることがあります)
ComfyUIを停止し、ComfyUIのPythonで `pip uninstall -y onnxruntime onnxruntime-gpu` のあと `pip install onnxruntime-gpu` を実行して、ComfyUIを再起動してください(DirectMLは非対応)。
### PSD関連の機能がエラーになる
追加のPythonパッケージ(psd-tools)が必要です。
エラーメッセージに表示されるインストール用のコマンドを、ComfyUIが使っているPythonで実行してから、ComfyUIを再起動してください。
### セマンティック検索(Gallery)がうまく動かない
| 症状 | 原因 | 対処 |
| --- | --- | --- |
| 「Ollamaに接続できません」と出る | Ollamaが起動していない、またはOllama URLが違う | Ollamaを起動する(インデックス作成中と検索時に必要)。歯車ボタンのモデル設定タブでOllama URLを確認する |
| 「Ollamaのモデルが未取得です」と出る | EmbeddingGemma 2をダウンロードしていない | `ollama pull embeddinggemma-2:740m`(または `embeddinggemma-2:440m`)を実行する |
| インデックス作成が「connection was forcibly closed」などでときどき失敗する | セキュリティソフト(AdGuardなど)がOllamaの通信に割り込んでいる | Ollamaをフィルタの対象から外す。失敗した画像は、次回の「新規・変更された画像のみ」で作り直される |
| Geminiで「GEMINI_API_KEYが未設定」と出る・キーが拒否される | `.env` にキーが無い、または行頭に `#` が付いている | プラグインの `.env`(`.env.example` をコピー)に `GEMINI_API_KEY=キー` と書き、ComfyUIを再起動する。`.env` は起動時にしか読み込まれない |
| 検索やSimilarの結果が出ない | 選んだフォルダにインデックス済みの画像が無い | 歯車ボタンでインデックスを作る。緑の点があるフォルダを選ぶ。モデルやベクトル次元を変えたときは「全件再作成」 |
### TagSpaceの空間が作れない
| 症状 | 原因 | 対処 |
| --- | --- | --- |
| 「OllamaのEmbeddingGemma 2が必要です」と出る | Ollamaが起動していない、またはモデルが無い | セマンティック検索と同じ準備をする(上の表) |
| 「Galleryのセマンティック索引は … で作られています」と出る | ギャラリーの索引をGeminiで作っている | ギャラリーのセマンティック設定でEmbeddingGemma 2を選び、「全件再作成」する |
| 作成にとても時間がかかる | 1回目は、画像のプロンプトの読み取りと、新しいタグの準備に時間がかかる | 2回目からは速くなる。Tagger補完を使うときは、TaggerがGPUで動いているか確認する(上の「Taggerがとても遅い」) |
### パートナーAPI(Nano Banana)が「認証に失敗しました」と出る
`.env` にComfy APIキーが無いのが原因です。プラグインの `.env` に `COMFY_API_KEY=キー`(platform.comfy.orgで発行)を書き、ComfyUIを再起動してください(第5章 5-8)。
`.env` は起動時にしか読み込まれません。
### ComfyUIが固まる(どのタブも反応しなくなる)
Windowsで、プレビュー画像の多いモデルタブの一覧を切り替えたあとなどに、ComfyUIが反応しなくなる(生成もできなくなる)場合があります。
設定タブの **Windowsの安定性(イベントループ)** で、**WindowsでSelectorイベントループを使用する** をオンにして、ComfyUIを再起動してください。
### ギャラリーの画像の一部が、別の画像のように壊れて見える
上の **Selectorイベントループ** をオンにしているとき、v0.9.1までは、ギャラリーのプレビューや Download で受け取った画像の一部が壊れることがありました
(出力フォルダのファイル自体は正常です)。v0.9.2で直っています。
Workflow Studioを更新したあと、壊れた画像がブラウザに残っていることがあるので、**Ctrl+F5** で再読み込みしてください。
### ギャラリーに画像が出てこない
- ギャラリーの **Refresh** を押します。
- 設定タブの「ギャラリー Outputフォルダ」が、ComfyUIの画像保存先と合っているか確認します。
---
## 12-6 それでも解決しないときは
### Workflow Studioのヘルプを見る
Workflow Studioの **ヘルプ** タブにも、トラブルシューティングの情報があります。
本書の発行後に追加された情報が載っていることもあります。
### GitHubで質問・報告する
解決しない問題は、Workflow StudioのGitHubリポジトリの **Issues** で報告できます。
報告するときは、次の情報を添えると、原因が見つかりやすくなります。
- Workflow Studioのバージョン
- ComfyUIのバージョンと、インストール方法(ポータブル版、Desktop版、Stability Matrixなど)
- どの画面で、何をしたら、どうなったか
- ComfyUIを起動した黒い画面(コンソール)に表示されたエラーメッセージ
- ブラウザの開発者ツール(F12キー)の「Console」に表示された赤いエラーメッセージ
> **注意**
> エラーメッセージを貼り付けるときは、パソコンのユーザー名などの個人情報が含まれていないか確認しましょう。
> **この章のまとめ**
> - 困ったら「再読み込み」「更新ボタン」「ComfyUIの再起動」をまず試す
> - 赤いノードは、カスタムノードの不足。ComfyUI Managerの Install Missing Custom Nodes で解決
> - 設定が反映されないときは、Applyの押し忘れとBatchのチェックの残りを確認
> - メモリ不足は、ローカルAIのアンロードや画像サイズの見直しで対処
> - 解決しないときは、ヘルプタブやGitHubのIssuesを活用する
+144
View File
@@ -0,0 +1,144 @@
# 付録
## 付録A 用語集
本書に登場する主な言葉を、五十音順・アルファベット順にまとめました。
### 日本語
| 用語 | 意味 | 本書 |
| --- | --- | --- |
| 意思決定モデル | 質問に「はい」の確率などを数字で答えるAIモデル。画像チェックの判定に使う | 第10章 |
| インペイント | 画像の一部(マスクした部分)だけを描き直すこと | 第11章 |
| カスタムノード | ComfyUIに後から追加するノード・拡張機能。Workflow Studioもその1つ | 第1章 |
| キャンバス | ComfyUIのノード画面で、ノードを並べる作業スペース | 第3章 |
| キュー | 生成の予約リスト。順番に処理される | 第1章 |
| キーフレーム | Labで「何回目から設定を切り替えるか」を指定したもの | 第5章 |
| サンプラー | 画像を描くときのアルゴリズム(euler など) | 第5章 |
| シード | 画像のもとになる乱数の種。同じなら、ほぼ同じ画像になる | 第5章 |
| セマンティック検索 | 文章や画像の「意味」で画像を探す検索。ギャラリーのSボタンで使う | 第8章 |
| スキル | AI TOOLのChatに、役割や答え方を教える指示書。選ぶと得意分野が切り替わる | 第10章 |
| スケジューラー | ステップごとの描き込み量の配分のしかた | 第5章 |
| スタイル | プロンプトに付け足す雰囲気のテンプレート | 第7章 |
| ステップ数 | 描き込みをくり返す回数 | 第5章 |
| トリガーワード | LoRAの効果を出すために、プロンプトに入れる言葉 | 第6章 |
| ネガティブプロンプト | 描いてほしくないものを書くプロンプト | 第7章 |
| ノード | ComfyUIの画面に並ぶ「箱」。1つの処理を受け持つ | 第1章 |
| ノードセット | 複数のノードとつながりを、ひとまとまりで保存したもの | 第9章 |
| バッジ | ワークフローやモデルに付ける、色付きのラベル | 第4章 |
| バッチ生成 | 条件を変えながら、まとめて自動で生成すること | 第5章 |
| ハッシュ値 | ファイルの中身から計算される「指紋」のような値。Civitaiでモデルを探すのに使う | 第6章 |
| プリセット | ポジティブ・ネガティブのプロンプトをひと組にして保存したもの | 第7章 |
| プロンプト | AIに「どんな画像を作ってほしいか」を伝える文章 | 第1章 |
| ポジティブプロンプト | 描いてほしいものを書くプロンプト | 第7章 |
| ポート番号 | 1台のパソコンの中で、通信先のソフトを区別するための番号 | 第2章 |
| マスク | 画像の中で、処理の対象にする部分を指定したもの | 第11章 |
| メタデータ | 画像に埋め込まれた、生成時の設定などの情報 | 第8章 |
| モデル | 画像を作る「AIの頭脳」にあたるファイル | 第1章 |
| ローカルAI | 自分のパソコンの中で動くAI(Ollamaなど) | 第10章 |
| ワークフロー | ノードを線でつないだ、処理の流れ全体 | 第1章 |
| ワイルドカード | 生成のたびに、プロンプトの一部をランダムに入れ替えるしくみ | 第7章 |
### アルファベット
| 用語 | 意味 | 本書 |
| --- | --- | --- |
| API形式/UI形式 | ワークフローの保存形式。UI形式はノードの位置なども含み、API形式は処理の内容だけを含む | 第4章 |
| Apply | 生成UIで、変更した値をワークフローに反映するボタン | 第5章 |
| CFG | プロンプトにどれだけ忠実に従うかの強さ | 第5章 |
| Checkpoint | 画像生成の本体となるモデル | 第6章 |
| Civitai | 画像生成モデルの共有サイト | 第6章 |
| ComfyUI Manager | カスタムノードを検索・インストールするための管理ツール | 第2章 |
| Denoise | 元の画像をどれだけ描き変えるかの強さ | 第5章 |
| Embedding | 特定の単語に意味を覚えさせた小さなファイル | 第6章 |
| I2I(Image to Image) | 画像をもとに、新しい画像を作ること | 第5章 |
| JSON | ワークフローの保存に使われるデータ形式 | 第1章 |
| LoRA | 絵柄やキャラクターなどを追加で覚えさせる小さなモデル | 第6章 |
| OCR | 画像に写っている文字を読み取ること | 第10章 |
| Ollama | ローカルAIを手軽に動かせるソフト | 第10章 |
| T2I(Text to Image) | 文章(プロンプト)から画像を作ること | 第5章 |
| TagSpace | タグ・プロンプト・生成画像を意味の近さで3Dの空間に並べて表示するタブ | 第11章 |
| VAE | 画像の色合いや細部を仕上げるモデル | 第6章 |
| VLM | 画像を読み取れるAIモデル | 第10章 |
| VRAM | GPU(グラフィックボード)のメモリ | 第10章 |
| WS Library | ComfyUIのノード画面の横に開く、Workflow Studioのサイドパネル | 第9章 |
---
## 付録B キーボード・マウス操作
### 全体
| 操作 | 動き |
| --- | --- |
| Esc | 開いている画面(モーダル・パネル)を閉じる |
| Enter | 名前の変更を確定する/チャットを送信する |
| Shift+Enter | チャットで改行する |
| ダブルクリック | 詳細画面を開く(ワークフロー・モデルなど) |
| Shift+クリック(Wボタン) | Workflow Studioを新しいウィンドウで開く |
### 生成UI
| 操作 | 動き |
| --- | --- |
| Alt+クリック(Apply) | 値の反映と生成を一度に行う |
| Ctrl+↑/↓(プロンプト入力欄) | 選んだ言葉の強調 `(言葉:数値)` を0.05ずつ増減する |
| Enter/Shift+Enter(Raw JSONの検索欄) | 次の一致/前の一致へ移動する |
### ギャラリー
| 操作 | 動き |
| --- | --- |
| Ctrl+クリック | 複数の画像を選ぶ |
| Alt+クリック | 画像をメタデータサブタブで開く |
| Enter(意味検索がオンのとき) | 検索欄の文章で意味検索を実行する |
### WS Library
| 操作 | 動き |
| --- | --- |
| ドラッグ&ドロップ | キャンバスのドロップした位置に置く・読み込む |
| ダブルクリック | キャンバスの中央付近に置く・読み込む |
| Alt+クリック(Nタブ) | ノードのお気に入りをオン/オフする |
### Image Edit
| 操作 | 動き |
| --- | --- |
| V/B/T/S/G | Select/Draw/Text/Shape/Fill ツールに切り替え |
| Ctrl+Z/Ctrl+Y | 元に戻す/やり直す |
| マウスホイール | 拡大・縮小 |
| Space+ドラッグ | 表示位置を移動する |
| Delete | 選んでいるレイヤーを削除する(レイヤーが2つ以上あるとき) |
| D/E(Draw中) | 描く/消す |
| X(Draw中) | 色1と色2を入れ替える |
| [/](Draw中) | ブラシを小さく/大きくする(実際は半角の `[` `]`) |
| B(Draw中) | ブラシの選択を開く・閉じる |
---
## 付録C 参考リンク
### Workflow Studio
- GitHub(最新版・リリースノート・不具合報告)
https://github.com/ketle-man/ComfyUI-Workflow-Studio
### ComfyUI本体と管理ツール
- ComfyUI
https://github.com/comfyanonymous/ComfyUI
- ComfyUI Manager
https://github.com/Comfy-Org/ComfyUI-Manager
### 本書で紹介した関連ソフト・サービス
| 名前 | 用途 | 本書 |
| --- | --- | --- |
| Civitai(https://civitai.com/) | モデルの入手・情報取得 | 第6章 |
| Ollama(https://ollama.com/) | ローカルAI | 第10章 |
| LM Studio(https://lmstudio.ai/) | ローカルAI | 第10章 |
| Lemonade Server(https://lemonade-server.ai/) | ローカルAI | 第10章 |
| Eagle(https://eagle.cool/) | 画像管理ソフト | 第8章 |
### 一緒に使うと便利なカスタムノード
| 名前 | 用途 | 本書 |
| --- | --- | --- |
| ComfyUI Comic Creator(https://github.com/ketle-man/comfyui-comic-creator) | マンガ制作。ギャラリー・画像生成と連携 | 第11章 |
| comfyui-image-feeder(https://github.com/ketle-man/comfyui-image-feeder) | FeederのImage Loopモード | 第5章 |
| comfyui-mask-editor-one(https://github.com/ketle-man/comfyui-mask-editor-one) | Image Editの高度なマスク・背景除去 | 第11章 |
| ComfyUI-Impact-Pack(https://github.com/ltdrdata/ComfyUI-Impact-Pack) | `{A|B}` 形式のワイルドカード | 第7章 |
| ComfyUI-Lora-Manager | LoRAの管理・WS LibraryでのLoRAのまとめ置き | 第9章 |
> **注意**
> リンク先の内容やURLは、変更される場合があります。
+27
View File
@@ -0,0 +1,27 @@
# おわりに
最後まで読んでいただき、ありがとうございました。
ComfyUIは、とても自由で奥の深いソフトです。
その自由さゆえに、はじめての方には「どこから手をつければいいのか分からない」ソフトでもあります。
Workflow Studioは、その入口を少しでも広げ、
「作ること」と「整理すること」をもっと気軽にしたいという思いから生まれました。
本書で紹介した使い方を、あらためて振り返ってみましょう。
1. **整理する** - ワークフロー・モデル・プロンプトを、サムネイルやバッジで見分けやすくする
2. **作る** - 生成UIで、ノード画面を触らずに画像を生成する
3. **貯める** - うまくいったプロンプトや設定を、プリセットやギャラリーに残す
4. **再現する** - 画像のメタデータから、同じ設定をすぐに呼び出す
5. **広げる** - バッチ生成やローカルAIで、試行錯誤のスピードを上げる
画像生成が上達するいちばんの近道は、**たくさん試して、うまくいったものを残していく** ことです。
Workflow Studioが、その積み重ねを支える道具になれば幸いです。
Workflow Studioは、これからも機能の追加や改善を続けていきます。
新しい機能や変更点は、GitHubのリリースノートでお知らせしています。
使ってみての感想や、「こんな機能がほしい」というご要望も、GitHubでお待ちしています。
あなたの画像生成ライフが、もっと楽しく、もっと豊かなものになりますように。
ketle-man
+32
View File
@@ -0,0 +1,32 @@
# Getting Started with ComfyUI Workflow Studio
Don't Be Afraid of Nodes - Generate Images More Easily and Keep Them Organized
- Covers: Workflow Studio v0.9.6
- Author: ketle-man
- (c) 2026 ketle-man
This is the **text-only edition** (without screenshots) of the Kindle book.
Chapter files are in the `chapters/` folder; the whole book in one file is `Workflow_Studio_Manual_v0.9.6_en.md`.
Screen examples are omitted, so please use the book together with Workflow Studio's Help tab.
## Contents
- [00_Introduction](chapters/00_Introduction.md)
- [01_Chapter1_Basics](chapters/01_Chapter1_Basics.md)
- [02_Chapter2_Install_and_Launch](chapters/02_Chapter2_Install_and_Launch.md)
- [03_Chapter3_Screen_Tour](chapters/03_Chapter3_Screen_Tour.md)
- [04_Chapter4_Workflow_Tab](chapters/04_Chapter4_Workflow_Tab.md)
- [05_Chapter5_GenerateUI_Part1](chapters/05_Chapter5_GenerateUI_Part1.md)
- [05_Chapter5_GenerateUI_Part2](chapters/05_Chapter5_GenerateUI_Part2.md)
- [06_Chapter6_Models_Tab](chapters/06_Chapter6_Models_Tab.md)
- [07_Chapter7_Prompt_Tab](chapters/07_Chapter7_Prompt_Tab.md)
- [08_Chapter8_Gallery_Tab](chapters/08_Chapter8_Gallery_Tab.md)
- [09_Chapter9_WS_Library](chapters/09_Chapter9_WS_Library.md)
- [10_Chapter10_AI_TOOL](chapters/10_Chapter10_AI_TOOL.md)
- [11_Chapter11_More_Features](chapters/11_Chapter11_More_Features.md)
- [12_Chapter12_Troubleshooting](chapters/12_Chapter12_Troubleshooting.md)
- [13_Appendix](chapters/13_Appendix.md)
- [14_Afterword](chapters/14_Afterword.md)
Whole book: [Workflow_Studio_Manual_v0.9.6_en.md](Workflow_Studio_Manual_v0.9.6_en.md)
File diff suppressed because it is too large Load Diff
Binary file not shown.
+171
View File
@@ -0,0 +1,171 @@
# Introduction
"I installed ComfyUI, but the node screen made me freeze."
"I downloaded so many workflows from the web that I can't tell which is which."
"I want to reuse the settings of an image that turned out well, but I can't find them."
This book is for people like that. It is an introduction to **ComfyUI-Workflow-Studio** (Workflow Studio from here on).
---
## Why is ComfyUI hard?
ComfyUI is one of the most flexible image-generation AI tools available today.
When a new model or technique is announced, ComfyUI is usually the first place you can use it.
Even so, many people stumble at the very first step. The reason is that ComfyUI is built on **a completely different way of thinking from ordinary applications**.
### How it differs from ordinary applications
| | Ordinary application | ComfyUI |
| --- | --- | --- |
| Screen | A fixed screen designed by the developer | You assemble it yourself by connecting nodes (boxes) with lines |
| Usage | Press buttons and menus and it works | You must know which nodes to connect, and how |
| Adding features | Wait for an update | You add custom nodes made by other people yourself |
| Where settings live | Collected in a settings screen | Scattered across many nodes inside the workflow |
An ordinary application is like a "finished appliance". Switch it on and you can use it right away, the way it was designed.
ComfyUI, on the other hand, is "a kit you build from parts".
If you know how to build, you can make anything. If you don't, you can make nothing.
### Freedom, and its side effects
The greatest appeal of ComfyUI is this **freedom to customize**.
You can combine custom nodes made by developers around the world to build your own flow of processing (a workflow).
Advanced workflows made by experienced users are published all over the web, and you can try them right after downloading.
But freedom has side effects too.
- **Workflows pile up** - as you keep downloading things to try, you lose track of which is which
- **You don't know where to touch** - in a large workflow made by someone else, even changing just the prompt means hunting for the right node
- **Your materials get scattered** - models, LoRAs, prompts and generated images end up in separate places, with no tool to manage them
- **Trial and error takes effort** - even to compare a few conditions, you must rewrite node values by hand every time
ComfyUI is excellent as a tool for "building", but
it offers almost nothing for "organizing", "reusing" and "trying things quickly".
Workflow Studio was made to fill that gap.
---
## What makes Workflow Studio different
### 1. Why a dedicated screen (SPA)? - separating "building" from "using"
Workflow Studio is an **independent, dedicated screen** (an SPA: single-page application) that you open from the **W** button in ComfyUI's top menu.
Building a workflow is best done by trial and error on ComfyUI's node screen.
Workflow Studio was created as a screen for **putting the workflows you have built to quick use**.
For that reason, Workflow Studio thinks of a workflow as three stages, **input, processing and output**, and is designed as follows.
| Stage | Design idea |
| --- | --- |
| Input | Make it easy to enter prompts and images |
| Processing | Keep changing values down to the fewest possible operations |
| Output | Make generated data easy to manage |
Underlying the design is the **5S** way of thinking (sort, set in order, shine, standardize, sustain) used in manufacturing.
The aim is to keep what you need in a state where you can take it out and use it, when you need it, right away.
The thing that connects the two screens, ComfyUI's node screen and Workflow Studio, is the **WS Library**, which opens beside the node screen.
Together with the supporting features, it makes moving between the two screens smooth.
### 2. Management - everything scattered, in one place
| What you can manage | What you can do |
| --- | --- |
| Workflows | Thumbnail list, sorting by badges and groups, search, a pre-check of whether a workflow will work in GenerateUI |
| Models and LoRAs | Lists by type, automatic retrieval of Civitai information (trigger words, sample images) |
| Prompts | Save and sort as presets, wildcards, styles |
| Generated images and audio | Browsing folder by folder, search by prompt or by "meaning", reading and reproducing embedded settings (audio files are supported too) |
### 3. Efficiency - try things quickly without touching nodes
- **GenerateUI tab** - automatically picks out, from inside a workflow, the items you change often (prompt, model, size and so on) and turns them into an input form. Even large workflows made by others can be used without hunting for nodes
- **Batch generation** - automatically runs your trial and error in bulk: "try all my models", "try prompts one after another", "compare samplers"
- **Feeder and Lab** - more advanced automation, such as processing many images in order, or switching several conditions step by step
### 4. Support features - help before and after generation
- **Image Edit** - simple layer-based editing, and redrawing parts with a mask (inpainting)
- **Local AI integration** - translating prompts from Japanese, creating prompts and tags from images
- **Tagger and Video** - automatic tag generation from images, and video generation with simple editing
- **TagSpace** - lays out your own prompts, tags and generated images in a 3D space by closeness of meaning, so you can browse it and pick tags
---
## A custom node that is worth installing
For the following reasons, Workflow Studio is a custom node that is **"worth installing just in case"** for everyone who uses ComfyUI.
- **It is free, and you can install it right away from ComfyUI Manager**
- **The basic features need no extra preparation** - everything required is already included with ComfyUI. Extras such as AI and image editing can be prepared when you want to use them
- **You don't have to change how you work now** - workflows use ComfyUI's own save folder as it is. You can keep working on the node screen as before
- **It gets more useful the more you use it** - start with just browsing a list of workflows or checking the settings of generated images, then widen your use to GenerateUI and batch generation as you get used to it
It keeps ComfyUI's "freedom" and adds "clarity" and "ease". That is Workflow Studio.
## What you will learn in this book
- Installing Workflow Studio and how to read its screens
- Organizing the workflows you already have, with thumbnails
- Generating your first image without touching the node screen
- Managing models, LoRAs and prompts so you can reuse settings that worked
- Reading the settings used from a generated image and reproducing them
- Translating prompts and reading images with local AI
- Common problems and how to fix them
## What this book does not cover
- How to install ComfyUI itself
(the explanation starts from the point where ComfyUI is already installed and can be launched)
- How to build a workflow from scratch by assembling nodes
- Detailed explanations of how image-generation AI works, or of model training (such as how to make a LoRA)
---
## Who this book is for
- People who have just installed ComfyUI, or who want to start using it seriously
- People who find the node screen hard and want to generate images more easily
- People who are struggling to organize growing numbers of workflows, models and prompts
It is written so that you can follow it if you know basic computer operations (copying files, opening folders, using a browser).
---
## Version and environment covered
- **Workflow Studio**: v0.9.6
- **ComfyUI**: the latest version at the time of writing
- **OS**: Windows (screen examples are from Windows)
Workflow Studio is under active development, so
depending on the version, the look of the screens and the positions of buttons may differ from this book.
For features that have changed significantly, also check Workflow Studio's **Help** tab and the release notes on GitHub.
---
## Notation used in this book
| Notation | Meaning |
| --- | --- |
| **Bold** | Button names and item names shown on screen |
| "GenerateUI" | The name as shown on screen |
| `ComfyUI/models/` | The location of a folder or file. For how to read this for your environment, see Chapter 2, section 2-6 |
| `Ctrl + Z` | A keyboard operation. "+" means pressing the keys at the same time |
> **Tip**
> Information that is handy to know.
> **Caution**
> Information to be careful about, because getting it wrong can lose data or stop things from working.
### About the display language
Workflow Studio supports English, Japanese and Chinese. This English edition explains things using the **English display**.
You can change the display language in the Settings tab (Chapter 2, section 2-5).
---
## How to read this book
If you are new, read **Chapters 1 to 5** in order and try things by hand.
Once you have generated your first image in Chapter 5, you have the basics of Workflow Studio.
From Chapter 6 onward, feel free to read from whichever place you need.
When in trouble, use the troubleshooting in Chapter 12 and the glossary in the appendix.
Now, let's start generating images with Workflow Studio, more easily and more enjoyably.
@@ -0,0 +1,146 @@
# Chapter 1 Basics of ComfyUI and Workflow Studio
This chapter sorts out the ideas and terms worth knowing before you start operating anything.
You don't need to read it at the computer. Read it casually on your commute or in spare moments.
---
## 1-1 What is ComfyUI?
ComfyUI is free software for running image-generation AI on your own computer.
It supports many image-generation models, such as Stable Diffusion and FLUX.
The biggest feature of ComfyUI is that **you build the flow of processing by connecting "boxes" with "lines"**.
"A box that loads the model" -> "a box that takes the prompt" -> "a box that makes the image" -> "a box that saves it"...
When you connect boxes with lines like this, processing happens in that order.
This approach has big advantages.
- Everything about the processing is visible, so you can fine-tune it
- Even when a new technique appears, you can try it just by adding a box
- You can save a flow you have built and reuse it as many times as you like
On the other hand, for someone touching it for the first time, it is also a screen where "you don't know where to touch".
**Workflow Studio**, introduced in this book, is exactly a tool for softening that "hard-to-understand" quality.
---
## 1-2 Five terms to remember
The minimum set of terms worth knowing for using ComfyUI is these five.
### 1. Node
The "boxes" arranged on the screen.
One node handles one job, for example "load a model" or "save an image".
A node has entrances (inputs) and exits (outputs), and passes data along through connecting lines.
### 2. Workflow
The "whole flow of processing" made by connecting nodes with lines.
To use a cooking analogy, nodes are tasks such as "chop" and "stir-fry", and the workflow is the "recipe".
A workflow can be saved as a file in a format called JSON.
> **Tip**
> Images made in ComfyUI usually have the workflow information embedded in them.
> If you drag such an image onto the ComfyUI screen, you can recall the same workflow.
### 3. Model
The file that acts as the "AI brain" that makes images. They are typically large, from a few GB to tens of GB.
The main kinds are as follows (Chapter 6 explains them in detail).
| Kind | In a phrase |
| --- | --- |
| Checkpoint | The model that forms the core of image generation |
| LoRA | A small add-on model that teaches an art style or a character |
| VAE | The model in charge of finishing the colors and fine details of an image |
### 4. Prompt
The text that tells the AI "what kind of image you want". It is what people call a "spell".
There are two kinds: the **positive prompt**, where you write what you want drawn, and
the **negative prompt**, where you write what you do not want drawn.
### 5. Queue
The "reservation list for generation".
In ComfyUI, pressing the generate button first puts a reservation in the queue, and the reservations are processed in order.
If you reserve many images, they are generated in bulk while you are away from your seat.
---
## 1-3 Where beginners get stuck in ComfyUI
You installed ComfyUI, but have you stopped at any of these points?
1. **Workflows pile up and you can't tell which is which**
As you keep saving workflows you found online, file names alone stop being enough to tell them apart.
2. **Hunting for values on the node screen every time is tiring**
You only want to change the prompt or seed value, yet you have to find the right node inside a large workflow.
3. **Models and prompts are managed in a scattered way**
It is easy to lose track of where you put which model, or what prompt worked last time.
4. **You can't reproduce the settings of an image you made before**
Even when you wonder "which settings made that image?", you may not know how to look it up.
None of these is really a lack of ComfyUI features. The cause is **a lack of "organization" and of an "entrance"**.
---
## 1-4 What Workflow Studio can do
Workflow Studio is an **extension (a custom node)** that you add to ComfyUI.
It adds "a dedicated screen for management and generation" inside ComfyUI.
Workflow Studio answers the four sticking points above as follows.
| Sticking point | Workflow Studio's solution | Chapter |
| --- | --- | --- |
| Can't tell workflows apart | A thumbnail list, badges and groups, search | Chapter 4 |
| Hunting for values on the node screen is tiring | The GenerateUI tab shows only the items you need as a form | Chapter 5 |
| Models and prompts are scattered | Managed together in the Models tab and the Prompt tab | Chapters 6 and 7 |
| Can't reproduce settings | Check an image's metadata in the Gallery and send it to the canvas | Chapter 8 |
In addition, a side panel called the **WS Library** is added beside ComfyUI's node screen,
letting you place models and prompts onto the node screen by drag and drop (Chapter 9).
If you prepare AI that runs locally (such as Ollama or LM Studio),
you can also use AI support such as prompt translation and tag generation from images (Chapter 10).
> **Tip**
> Workflow Studio is not software that replaces ComfyUI.
> It is ComfyUI that makes the images; Workflow Studio plays the role of its "control panel" and "storage shelf".
> For that reason, it cannot be used unless ComfyUI is running.
### The screens can be used in your language
Workflow Studio supports English, Japanese and Chinese.
This edition explains things using the English display.
---
## 1-5 How this book proceeds
This book is meant to be read in the following order.
1. **Prepare** (Chapters 2 and 3)
Install it and learn how to read the screens.
2. **Organize** (Chapter 4)
Make the workflows you already have viewable as a list.
3. **Create** (Chapter 5)
Generate your first image without touching the node screen.
If you read this far, you can use the basics of Workflow Studio.
4. **Collect and review** (Chapters 6 to 8)
Manage models, prompts and generated images, and reuse settings that worked.
5. **Expand** (Chapters 9 to 11)
Bring in handy features such as the side panel and AI support, as you need them.
When in trouble, see the troubleshooting in Chapter 12 and the glossary in the appendix.
> **Summary of this chapter**
> - ComfyUI is software that makes images with a workflow of connected nodes (boxes)
> - The five terms to remember are "node", "workflow", "model", "prompt" and "queue"
> - Workflow Studio is an extension that adds a "storage shelf" and an "easy control panel" to ComfyUI
> - First, aim for the "first image" in Chapter 5
@@ -0,0 +1,254 @@
# Chapter 2 Installation and Launch
In this chapter we add Workflow Studio to ComfyUI and go as far as opening its screen for the first time.
It assumes that ComfyUI itself is already installed and can be launched.
---
## 2-1 What you need
### Required
| What you need | Description |
| --- | --- |
| ComfyUI | A recent version is fine. Because it uses a feature that places buttons in the top menu, a fairly new version (frontend v1.33.9 or later) is recommended |
| Python 3.10 or later | The one bundled with ComfyUI is fine |
| A web browser | Such as Chrome or Edge: the browser in which you open the ComfyUI screen |
The basic features of Workflow Studio work with no extra preparation as long as ComfyUI is running.
### Later, as needed
The following are needed only when you use particular features. You can ignore them at first.
- **Ollama, LM Studio and similar** - AI support features such as prompt translation and AI chat (Chapter 10).
**Ollama** is also needed for local semantic search in the Gallery (Chapter 8) and for the TagSpace tab (Chapter 11)
- **Eagle** - a feature that automatically saves generated images to the image-management software Eagle (Chapter 8)
- **Extra Python packages** - Tagger (tag generation from images), PSD export and the like (Chapter 11)
> **Tip**
> It is not the case that "you can't use it unless you have everything".
> Start by installing only the main part, and add things when features you want to use come up.
---
## 2-2 Install with ComfyUI Manager (recommended)
**ComfyUI Manager** is a management tool that lets you search for and install custom nodes (extensions) from the screen.
Recent versions of ComfyUI often include it from the start, and it is the easiest method.
1. Start ComfyUI and open its screen in the browser.
2. Click the **"Manager"** button in the top menu.
3. Click **"Custom Nodes Manager"**.
4. Type **`Workflow Studio`** in the search box.
5. Click **"Install"** on **ComfyUI-Workflow-Studio** in the list.
6. When the installation finishes, **restart ComfyUI** following the on-screen guidance.
> **Caution**
> ComfyUI loads extensions only once, when it starts.
> Always restart ComfyUI after installing. Reloading the browser alone will not take effect.
---
## 2-3 Install manually (git clone)
If ComfyUI Manager is not available, or if you are comfortable with Git, you can also install manually.
You use a screen where you type commands (on Windows, "Command Prompt" or "Terminal").
1. Move to the **`custom_nodes`** folder inside the ComfyUI folder.
```
cd ComfyUI/custom_nodes
```
2. Type the following command to download Workflow Studio.
```
git clone https://github.com/ketle-man/ComfyUI-Workflow-Studio.git
```
3. If a **`ComfyUI-Workflow-Studio`** folder now exists inside `custom_nodes`, it worked.
4. Restart ComfyUI.
> **Tip**
> If the `git` command is not available, you first need to install Git (for example Git for Windows).
> If you are unsure, the ComfyUI Manager method in 2-2 is recommended.
---
## 2-4 Launching
### Open it from the W button
When you restart ComfyUI, a **"W"** button has been added to the menu at the top of the screen.
This is the button that opens Workflow Studio.
- **Click** - Workflow Studio opens in a new tab.
- **Shift + click** - It opens in a new window. Handy when you want to use it side by side with the ComfyUI screen.
A **camera button** has also been added near the W button.
It is the button for saving a workflow screen as a thumbnail (you use it in Chapter 4).
### How addresses and port numbers work
When you press the W button, the address of Workflow Studio appears in the browser's address bar.
Knowing how to read this address lets you open it directly from a bookmark and track down causes when something goes wrong.
The address is made of the following three parts.
| Part | Example | Meaning |
| --- | --- | --- |
| Host | `127.0.0.1` | Which computer to connect to. `127.0.0.1` (or `localhost`) is a special address meaning "this computer" |
| Port number | `8188` | Which piece of software on that computer to connect to |
| Path | `/wfm` | Which page inside that software to open |
Joining these three in the form "`host:port-number/path`" gives the address of Workflow Studio.
#### What is a port number?
Many pieces of software on one computer communicate at the same time.
The **port number** is what is used to distinguish which software a communication is meant for.
To use an apartment building as an analogy, the host (`127.0.0.1`) is the "address of the building" and the port number is the "room number".
Even in the same building, if you get the room number wrong, you will not reach the person you want.
ComfyUI listens on a port number decided when it starts.
Commonly used numbers are as follows.
| Kind of ComfyUI / how it was started | Commonly used port number |
| --- | --- |
| Portable version or manual install (default settings) | 8188 |
| ComfyUI Desktop (the desktop app version) | 8000 |
| When the `--port` startup option is given | The number you specified |
If you use a launcher such as Stability Matrix, the number may also change depending on the launcher's settings.
#### Checking your own port number
The most reliable way is to **look at the address bar of the browser in which you have ComfyUI open right now**.
The number after `127.0.0.1:` or `localhost:` is the port number.
The black screen (console) shown when you start ComfyUI also displays a line such as
`To see the GUI go to: http://127.0.0.1:XXXX`.
#### Opening it by typing the address directly
If you know the port number, you can open Workflow Studio without the W button.
Just add **`/wfm`** to the end of ComfyUI's address.
- ComfyUI is open at `127.0.0.1:8188` -> Workflow Studio is at `127.0.0.1:8188/wfm`
- ComfyUI is open at `127.0.0.1:8000` -> Workflow Studio is at `127.0.0.1:8000/wfm`
If you use it often, bookmarking this address is handy.
> **Caution**
> When you open it from a bookmark, "Send to Canvas", which sends a workflow directly to ComfyUI's canvas, cannot be used.
> Instead, that workflow appears in ComfyUI's side panel (the WS Library), and you drag it onto the canvas yourself (Chapters 4 and 9).
> If you use it while going back and forth with the ComfyUI screen, opening it from the W button is recommended.
> **Tip**
> Workflow Studio runs as a part of ComfyUI, so it uses the same port number as ComfyUI.
> There is no port number dedicated to Workflow Studio.
---
## 2-5 Changing the display language
Workflow Studio is displayed in **English** at first. You can switch it to Japanese or Chinese as follows.
1. Open **"Settings"** from the tabs at the top.
2. In the **"UI Language"** field, choose the language you want.
3. After a notice that the language was changed, the page reloads automatically and switches to the new display language.
If you switch to Japanese, the tab names change as follows (some stay in English).
| English display | Japanese display |
| --- | --- |
| Workflow | ワークフロー |
| GenerateUI | 生成UI |
| Gallery | ギャラリー |
| Prompt | プロンプト |
| Models | モデル |
| Nodes | ノード |
| Settings | 設定 |
| Help | ヘルプ |
| AI TOOL / Tagger / Image Edit / Video | (stay in English) |
> **Tip**
> The language setting is saved **per browser** you use.
> If you open it from another browser or another computer, set it again.
The **"Summary Language"** field in the same place is the language used when AI creates descriptions of workflows (Chapter 10). You do not need to change it for now.
---
## 2-6 Folder locations in each environment
There are several ways to install ComfyUI. Depending on how it was installed, the folder locations differ slightly.
In this book, folder locations are written in a form **starting from `ComfyUI/`**.
Read it as the "place that corresponds to `ComfyUI/`" in the table below, to match your environment.
### How this book writes it, and where it is in each environment
| Notation in this book | Contents | Portable version | ComfyUI Desktop | Stability Matrix |
| --- | --- | --- | --- | --- |
| `ComfyUI/` | ComfyUI's main folder | `ComfyUI_windows_portable/ComfyUI/` | The folder you chose at install time (by default `ComfyUI` inside "Documents") | `(data folder)/Packages/ComfyUI/` |
| `ComfyUI/custom_nodes/` | Custom nodes (Workflow Studio too) | `ComfyUI/custom_nodes/` | `ComfyUI/custom_nodes/` | `ComfyUI/custom_nodes/` |
| `ComfyUI/models/` | Models | `ComfyUI/models/` | `ComfyUI/models/` | `(data folder)/Models/` (shared by all packages) |
| `ComfyUI/output/` | Generated images | `ComfyUI/output/` | `ComfyUI/output/` | `(data folder)/Images/` |
| `ComfyUI/user/default/workflows/` | Saved workflows | `ComfyUI/user/default/workflows/` | `ComfyUI/user/default/workflows/` | `ComfyUI/user/default/workflows/` |
| ComfyUI's Python | Where extra packages are installed | `ComfyUI_windows_portable/python_embeded/python.exe` | `ComfyUI/.venv/Scripts/python.exe` | `ComfyUI/venv/Scripts/python.exe` |
| Default port number | See 2-4 | 8188 | 8000 | 8188 |
> **Tip**
> `ComfyUI/` in the table refers to the "place that corresponds to `ComfyUI/`" in each environment.
> On Windows, path separators are shown as `\` instead of `/`.
### Portable version
Everything is inside the `ComfyUI_windows_portable` folder created by extracting the downloaded ZIP file.
You launch it with `run_nvidia_gpu.bat` or similar inside the folder.
For **Python**, you use the one inside the `python_embeded` folder dedicated to ComfyUI (Chapter 11, section 11-2).
### ComfyUI Desktop
This is the app version installed with an installer. At first launch, you choose the folder in which to put data.
If you forget where the folder is, you can check it with the app's menu item that opens the folder ("Open Folder" or similar).
The default port number is **8000**, so be careful when typing the address directly (2-4).
### Stability Matrix
This is a launcher that manages several image-generation programs together.
Inside the **data folder** you chose first (for example `StabilityMatrix/Data/`), things are separated as follows.
- **ComfyUI itself** - `Data/Packages/ComfyUI/`
- **Models** - folders by type inside `Data/Models/` (`StableDiffusion` for Checkpoints, `Lora` for LoRAs, and so on). They are shared with other programs
- **Generated images** - inside `Data/Images/`
> **Caution**
> In Stability Matrix, the folders for models and images are **outside ComfyUI's main folder**.
> Where this book says "put it in `ComfyUI/models/checkpoints`", put it in Stability Matrix's shared model folder (such as `Data/Models/StableDiffusion`).
> If images do not appear in the Gallery, check that the "Gallery Output Folder" in the Settings tab points inside `Data/Images/`.
> **Tip**
> Folder names and structure can change with each program's version updates.
> If you cannot find something, also check each program's official documentation.
---
## 2-7 If it does not start properly
### The W button does not appear
- **Did you restart ComfyUI?** Reloading the browser alone does not take effect.
- **Is the install location correct?** Check that the folder exists at `ComfyUI/custom_nodes/ComfyUI-Workflow-Studio`.
If there is another folder with the same name inside it (a doubled folder), it will not be loaded.
- **Is your ComfyUI version old?** Try updating the main program from ComfyUI Manager or similar.
### The page does not open, or "cannot connect" is shown
- **Is ComfyUI running?** Workflow Studio runs inside ComfyUI, so you cannot open it if you quit ComfyUI.
- **Is the port number right?** Check ComfyUI's port number using the method in 2-4.
For example, if you use ComfyUI Desktop but type `8188`, it will not connect.
### An error appears in the console at startup
Check whether the black screen shown when you start ComfyUI displays any error about `ComfyUI-Workflow-Studio`.
Noting the content of the error will help with the troubleshooting in Chapter 12, and when asking questions on GitHub.
> **Summary of this chapter**
> - The easy way to install is to search for "Workflow Studio" in ComfyUI Manager
> - Always restart ComfyUI after installing
> - Open it with the "W" button in the top menu (Shift + click for a separate window)
> - The address is "host:port-number/wfm". You can check the port number in the address bar where ComfyUI is open
> - Switch the language in the Settings tab with "UI Language"
> - Folder locations differ by environment (portable version, Desktop, Stability Matrix). Read `ComfyUI/` in this book accordingly
@@ -0,0 +1,146 @@
# Chapter 3 Reading the Screens
Workflow Studio has many tabs, but you do not need to memorize all of them from the start.
In this chapter you will get a rough idea of "what each tab is for",
and check the roles of the buttons and the side panel added to the ComfyUI side.
---
## 3-1 The tabs and their roles
When you open Workflow Studio, tabs are lined up in a row across the top of the screen.
From the left, there are the following tabs.
It is enough to learn the tabs marked with a star under "Often used" first.
| Tab | What it is for | Often used | Chapter in this book |
| --- | --- | :---: | --- |
| Workflow | Listing and organizing saved workflows | * | Chapter 4 |
| Nodes | Listing nodes and registering favorites | | Chapter 11 |
| Models | Listing and organizing models such as Checkpoints and LoRAs | * | Chapter 6 |
| GenerateUI | Generating images without touching the node screen | * | Chapter 5 |
| Prompt | Saving and sorting prompts, AI assistance | * | Chapter 7 |
| Gallery | Browsing generated images, checking their settings | * | Chapter 8 |
| Image Edit | Simple image editing, making masks | | Chapter 11 |
| Video | Video generation and video editing | | Chapter 11 |
| Tagger | Automatically generating tags (prompts) from images | | Chapter 11 |
| TagSpace | Showing tags, prompts and generated images in 3D by closeness of meaning, and picking tags | | Chapter 11 |
| Settings | Language, theme, folders and other settings | | This chapter, Chapter 11 |
| Help | Feature list, shortcuts, troubleshooting | | This chapter |
| AI TOOL | Translation, chat, image analysis and text reading (OCR) with local AI | | Chapter 10 |
### Think of the tabs in three groups: "organize", "create" and "support"
If the number of tabs makes you lose your way, it helps to think of them in these three groups.
- **Organize** - Workflow / Nodes / Models / Prompt / Gallery
Tabs that list your materials so they are easy to find.
- **Create** - GenerateUI / Image Edit / Video
Tabs for actually making images and videos, or touching them up.
- **Support** - AI TOOL / Tagger / TagSpace / Settings / Help
Tabs that help with work in other tabs, or hold overall settings.
> **Tip**
> The tabs are connected to each other.
> For example, you can load a workflow picked in the Workflow tab into GenerateUI,
> or recall a prompt saved in the Prompt tab from GenerateUI.
### The Help tab
The first thing to open when in trouble is the **Help** tab.
A list of topics is lined up on the left, and clicking one shows the explanation on the right.
- The feature list (an overview of each tab's features)
- Keyboard shortcuts
- Troubleshooting
- Support (links to GitHub and so on)
It is also useful when, reading this book, you wonder "what was this button?".
---
## 3-2 Buttons added to the ComfyUI side
When you install Workflow Studio, **three buttons** are added to the menu at the top of ComfyUI's screen.
| Button | Name | Role |
| --- | --- | --- |
| 1. W | Open Workflow Studio | Opens Workflow Studio in a new tab. Shift + click opens a new window (Chapter 2) |
| 2. Camera | Canvas snapshot | Saves the current node screen as an image and makes it the workflow's thumbnail (Chapter 4) |
| 3. An icon of lined-up squares | WS Library | Opens and closes a side panel beside the node screen (3-3) |
When you hover the mouse over a button, a description is shown.
> **Tip**
> The main part of Workflow Studio (the screen with the row of tabs) and ComfyUI's node screen open in **separate browser tabs**.
> The basic style is to use the two screens back and forth: "organizing and generating in Workflow Studio",
> "detailed building on ComfyUI's node screen".
---
## 3-3 The side panel "WS Library"
When you press button 3, a panel called the **WS Library (Workflow Studio Library)** opens beside ComfyUI's node screen.
The WS Library is **a shelf for using the materials you organized in Workflow Studio right away on the node screen**.
Single-letter tabs are lined up at the top of the panel (hover the mouse over one to see its proper name).
| Tab | Name | What you can do |
| --- | --- | --- |
| W | Workflows | Drag a workflow to load it onto the canvas |
| N | Nodes | Place favorite nodes and node sets on the canvas |
| P | Prompts | Place or copy saved prompts onto the canvas |
| M | Models | Drag a model to place it onto the canvas together with a loader node |
| I | Information | Drop an image or workflow file to inspect its contents (the models and prompts used) |
| A | AI TOOL | Use AI features such as translation and chat while staying on the node screen |
For example, when you drag a model from the M tab onto the canvas, a node that loads that model is placed automatically.
It saves the trouble of finding a node in a list, placing it, choosing the file name, and so on.
Chapter 9 explains how to use it in detail. For now, it is enough to remember only that "you can bring out a shelf of materials beside the node screen".
---
## 3-4 Theme and appearance settings
The look of Workflow Studio can be changed to your liking in the **Settings** tab.
Because it is a tool you use for long periods, a color scheme that is easy on the eyes and a readable text size are recommended.
### Theme
From the **"Theme"** field you can choose the color scheme of the whole screen. The default is the dark scheme "Deep Ocean Dark".
| Example theme | Feel |
| --- | --- |
| Deep Ocean Dark (default) | Dark blue. Images are easy to see |
| Light Minimalist (clean) | White-based, bright and simple |
| Pastel Dream (pastel) | Soft pastel tones |
| Corporate Trust (for business) | Calm white and blue |
Besides these, there are 13 themes in all, including pop, cyberpunk and retro.
> **Tip**
> When checking the colors of generated images, a darker theme makes colors easier to tell apart.
> If you can't decide, try using the default for now.
### Customize
Opening **"Customize"** lets you fine-tune the chosen theme further.
- **Color Override** - change colors such as background, text and accent individually
- **Background Pattern** - add patterns such as stripes or dots to the background
- **Font** - change the typeface (Japanese fonts such as Zen Maru Gothic and BIZ UDPGothic can also be chosen)
When you finish adjusting, press **"Apply & Save"**.
To go back, press **"Reset Customization"**.
### Text Size
With the **"Text Size"** field you can change the size of the text across the whole screen.
If the text feels too small to read, make it bigger here.
### The look of the WS Library
The colors of the WS Library (3-3) can be set separately from the **gear button** inside the panel.
You can adjust the background and text colors to match the colors of the ComfyUI screen.
> **Summary of this chapter**
> - Thinking of the tabs in three groups, "organize", "create" and "support", makes them easy to follow
> - For now, it is enough to learn five: Workflow, Models, GenerateUI, Prompt and Gallery
> - Three buttons, "W", "camera" and "WS Library", are added to the top of ComfyUI
> - The WS Library is a shelf of materials you can bring out beside the node screen
> - The look can be adjusted with the Settings tab's Theme, Customize and Text Size
@@ -0,0 +1,306 @@
# Chapter 4 The Workflow Tab - Organizing the Workflows You Made
The Workflow tab is the first thing shown when you open Workflow Studio, a kind of "front door".
In this chapter you will learn how to make the workflows you already have viewable as a list,
and how to organize them so they are easy to find later.
---
## 4-1 How workflows are listed automatically
When you open the Workflow tab, some workflows may already be lined up.
This is because Workflow Studio automatically reads ComfyUI's workflow save folder.
A workflow you "Save" on ComfyUI's node screen is normally saved in the following folder.
```
ComfyUI/user/default/workflows/
```
Workflow Studio lists the workflows inside this folder.
In other words, **workflows saved in ComfyUI line up in the Workflow tab without your doing anything**.
> **Tip**
> If a workflow you just saved in ComfyUI does not appear in the list,
> press the **"Refresh"** button on the toolbar. It reloads to the latest state.
> **Tip**
> The folder that is read can be changed with the **"Workflow Data Folder"** setting in the Settings tab.
> Unless you have a reason, leaving it as it is causes no problem.
---
## 4-2 Reading the screen and switching the display
The Workflow tab is divided into three main parts.
| No. | Place | Role |
| --- | --- | --- |
| 1 | Toolbar (top) | Search, filtering, sorting, various buttons |
| 2 | List (center) | Workflows lined up as cards or in a table |
| 3 | Side panel (right) | Shows the thumbnail and contents of the selected workflow |
### Thumbnail view and table view
With the switch buttons on the toolbar you can change how the list is displayed.
- **Thumbnail** - lines them up as cards with images. Handy when you want to find things by how they look.
- **Table** - lines them up in a table. Handy when you want to find one among many workflows by name.
### Click and double-click
Workflows in the list behave differently depending on how you click.
| Operation | What happens |
| --- | --- |
| Click | Shows the contents in the right-hand side panel (the workflow becomes "selected") |
| Double-click | Opens the detail screen (editing badges, tags and memo; explained in 4-4) |
### The side panel
When you click a workflow, the following three tabs appear in the right-hand side panel.
- **Thumbnail** - shows the workflow's image large
- **JSON** - shows the contents (JSON data) of the workflow with color coding. The "Copy" button copies it all
- **Groups** - sorts the workflow into groups (explained in 4-4)
Below the workflow name, the **format of the workflow** (UI format, API format and so on) and
the **check result** of whether it can be used in GenerateUI are shown (explained in 4-7).
---
## 4-3 Taking a thumbnail with the camera button
A workflow that you only saved in ComfyUI has no thumbnail image, and can be hard to tell apart in the list.
That is where the **camera button** (canvas snapshot) added to ComfyUI's top menu comes in.
### How to take a thumbnail
1. On ComfyUI's node screen, open the workflow you want to make a thumbnail of.
2. Adjust the screen position and zoom so that the whole workflow is easy to see.
3. Click the **camera button** in the top menu.
4. A screen for confirming the name to save appears. Change the name if you wish and save.
(At first, the name of the workflow currently open is filled in)
5. When "Workflow saved" is shown, you are done.
When you open the Workflow tab of Workflow Studio (or press "Refresh"),
the workflow appears in the list with the image you took attached as its thumbnail.
> **Tip**
> The image (PNG) saved with the camera button also has the workflow information embedded in it.
> If you drag and drop this image onto ComfyUI's node screen, you can load that workflow.
> A single image file carries both the "look" and the "contents", which is handy.
### Changing a thumbnail later
To replace the thumbnail of an existing workflow,
double-click the workflow to open the detail screen, press **"Change Thumbnail"** and choose an image.
---
## 4-4 Sorting with favorites, badges and groups
As workflows increase, sorting them lets you find what you want right away.
Workflow Studio has three ways to sort.
| Method | When to use it | Example |
| --- | --- | --- |
| Favorites (star) | You want to mark the ones you use often | The basic workflow you use every day |
| Badges | You want to tell kinds apart at a glance with colored labels | "SDXL", "FLUX", "Video", "Experimenting" |
| Groups | You want to gather them by purpose or project | "Character work", "Background materials" |
### Favorites (star)
Click the **star** on a card to register it as a favorite.
Press the **star** button at the top of the toolbar to show only favorites (**ALL** returns to showing everything).
Workflows registered as favorites also appear under
"Favorites" in ComfyUI's side panel (the WS Library) (Chapter 9).
### Badges
A badge is a **colored label** you attach to a workflow.
You can also attach several badges to one workflow.
**1. Make a badge**
1. Press the **"Badge"** button on the toolbar.
2. Type a name in the **"Badge label..."** field and press **"Add"**.
3. Click a color swatch to change the badge's color.
**2. Attach a badge to a workflow**
1. Double-click the workflow to open the detail screen.
2. In the **"Badges"** field, tick the badges you want to attach.
3. Press **"Save"**.
**3. Filter by badge**
Once you have made badges, they line up next to the star and ALL buttons at the top of the toolbar.
Click a badge to show only the workflows that have that badge.
> **Tip**
> The kinds and colors of badges are shared with the Models tab (Chapter 6).
> If you make a badge called "SDXL", you can attach it in the same color to both workflows and models.
### Groups
A group is **a box for gathering workflows by purpose**.
1. Click a workflow to select it.
2. In the right-hand side panel, open the **"Groups"** tab.
3. Make a group with **"Create New Group"**, or pick an existing group and **"Add to Group"**.
If you choose a group from the pull-down labeled **"All Groups"** on the toolbar, only the workflows in that group are shown.
### Tags and memo
On the detail screen you can also write the following.
- **Tags** - keywords for searching. You can enter several, separated by commas (,).
- **Memo** - free notes, such as cautions on how to use it or settings that worked well.
If you note the "source" and the "required custom nodes" for a workflow you got online, you will not be in trouble later.
### Renaming and deleting
- **Rename** - click the workflow name at the top of the detail screen to edit it.
- **Delete** - press the **"Delete"** button on the detail screen.
> **Caution**
> Deleting removes the file itself in ComfyUI's workflow save folder (including the thumbnail image).
> For workflows that matter, back them up somewhere else before deleting.
---
## 4-5 Search, sorting and clearing filters
### Search
Type a keyword in the **"Search..."** box on the toolbar, and only workflows whose name, tags and so on match are shown.
Pressing the **x** at the right end of the box clears the search text immediately.
### Sorting
With the pull-down next to the search box you can change the order.
| Choice | Order |
| --- | --- |
| Date (Newest) | Newest first |
| Date (Oldest) | Oldest first |
| Name A-Z | Ascending by name |
| Name Z-A | Descending by name |
The order you choose is remembered, and the same order is used the next time you open it.
### Clearing filters all at once
While narrowing down by search, group and badge, you may wonder "why doesn't it show up in the list?".
In that case, press **"x Clear"** at the right end of the toolbar.
The search text, group and badge filters are all cleared at once, and every workflow is shown.
---
## 4-6 Importing, and sending to the canvas
### Bringing in workflows (import)
Workflow files you got online (`.json`, or `.png` containing a workflow)
can be brought into the Workflow tab in either of these ways.
- Press the **"Import"** button on the toolbar and choose files (you can select several)
- **Drag and drop** the files onto the list area of the Workflow tab
Imported workflows are saved in ComfyUI's workflow save folder and added to the list.
### Using the selected workflow
When you click a workflow to select it, the following three buttons on the toolbar become pressable.
| Button | What happens |
| --- | --- |
| Load in GenerateUI | Loads it into the GenerateUI tab so you can generate without touching the node screen (Chapter 5) |
| Load in Video | Loads a workflow for video generation into the Video tab (Chapter 11) |
| Send to Canvas | Loads the workflow onto ComfyUI's node screen |
> **Tip**
> If a button cannot be pressed (it is gray), you have not yet selected a workflow.
> Click a workflow in the list once, then press it.
When you press **"Send to Canvas"**, that workflow is loaded onto the node screen of the ComfyUI from which you opened Workflow Studio.
You can use it like this: find in Workflow Studio, edit in detail in ComfyUI.
> **Caution**
> "Send to Canvas" works in Workflow Studio **opened from ComfyUI's W button**.
> If you opened it directly, for example from a bookmark, that workflow appears in the WS Library on the ComfyUI side,
> so drag it onto the canvas from there (Chapters 2 and 9).
---
## 4-7 Checking in advance whether it works in GenerateUI (GenerateUI Check)
Not every workflow can be used as it is in GenerateUI (Chapter 5).
If it uses special custom nodes, or specifies models you do not have, it may not work well.
So before you load one into GenerateUI, there is a feature that automatically checks whether it looks usable.
### Reading the check result
When you click a workflow, the following information appears below the workflow name in the side panel.
- **Format badge** - the workflow's save format (UI format / API format / App format)
- **Convert** - whether it can be converted into a form GenerateUI can use
- **Operability** - how many of the settings can be changed on the GenerateUI screen alone
Each is shown in one of three levels.
| Display | Meaning |
| --- | --- |
| Check mark | No problem |
| Warning mark | Usable, but needs care |
| Cross mark | Cannot be used in GenerateUI, or is hard to use |
Hovering the mouse over a badge shows the reason for the judgment.
### Common judgments and what to do
| Judgment | Common reason | What to do |
| --- | --- | --- |
| Convert: cross | The custom nodes the workflow needs are not installed | Install the missing nodes with ComfyUI Manager (Chapter 12) |
| Convert: warning | The specified model or LoRA is not on your computer | Choose a model you have in GenerateUI. For UI-format workflows, note that it is automatically replaced with the first model in the list |
| Operability: warning / cross | Many settings are not shown in GenerateUI | Change settings of nodes that are not shown on ComfyUI's node screen |
> **Tip**
> Operability is "the share of nodes that have settings which are shown in GenerateUI".
> It is a check mark at 80% or more, a warning at 50% or more, and a cross below that.
> Even with a warning or cross, generation itself is often possible, so treat it as a rough indication of "hard to finish using GenerateUI alone".
> **Tip**
> The check fetches from ComfyUI only the information of the nodes that workflow uses (v0.8.8 and later).
> It used to take tens of seconds the first time, but now the result appears almost instantly.
> From the second time on, results appear immediately.
---
## 4-8 Trying the bundled sample workflows
Workflow Studio comes with sample workflows you can try right away.
The samples are in the **`workflows`** folder inside the folder where you installed Workflow Studio.
```
ComfyUI/custom_nodes/ComfyUI-Workflow-Studio/workflows/
```
Each sample has a `.json` (the workflow itself) and a `.png` (the thumbnail image) of the same name.
### Samples recommended for beginners
| File name | Contents |
| --- | --- |
| `ws_sdxl_t2i_basic` | A basic workflow that makes images from text with an SDXL model |
| `ws_sd15_t2i_basic` | A basic workflow that makes images from text with an SD1.5 model |
Choose according to the kind of model you have (SDXL or SD1.5).
### Importing a sample
1. Press **"Import"** in the Workflow tab.
2. Open the `workflows` folder above and choose the `.png` file of the sample you want to use.
(If you choose the `.png`, it is imported with its thumbnail)
3. Check that it has been added to the list.
There are also more advanced samples, such as pose specification and inpainting (redrawing part of an image).
> **Caution**
> Some samples require additional custom nodes.
> If any nodes are shown in red on the node screen, install the missing nodes with ComfyUI Manager (Chapter 12).
> The "Convert: cross" shown in the GenerateUI Check of 4-7 is also mostly this case.
In the next chapter, you load the basic sample you just imported into GenerateUI and finally generate your first image.
> **Summary of this chapter**
> - Workflows saved in ComfyUI line up automatically in the Workflow tab
> - Click to show in the side panel; double-click for the detail screen
> - With the camera button you can give a workflow a thumbnail that is easy to recognize
> - Sort with favorites, badges and groups, and find with search and sorting. If lost, use "x Clear"
> - With "Load in GenerateUI" and "Send to Canvas" you can use the selected workflow right away
> - The GenerateUI Check lets you confirm in advance whether it works in GenerateUI
@@ -0,0 +1,255 @@
# Chapter 5 The GenerateUI Tab - Making Images Without Touching Nodes
At last, you generate an image with Workflow Studio.
The **GenerateUI** tab is a screen that picks out, from inside a workflow, the items you change often, such as "prompt", "model" and "size",
and shows them as an input form.
You no longer need to find nodes and follow lines.
In the first half of this chapter (5-1 to 5-4), you use the basic sample imported in Chapter 4 to generate your first image.
> **Before you start**
> - ComfyUI is running
> - At least one Checkpoint model is in ComfyUI's `models/checkpoints` folder
> - You have imported a sample workflow (such as `ws_sdxl_t2i_basic`) in Chapter 4
---
## 5-1 Loading a workflow into GenerateUI
1. Open the **Workflow** tab.
2. **Click** the workflow you want to use (for example `ws_sdxl_t2i_basic`) to select it.
3. In the side panel's **GenerateUI Check** (Chapter 4, section 4-7), confirm that "Convert" shows a check mark.
4. Press **"Load in GenerateUI"** on the toolbar.
The screen switches automatically to the **GenerateUI** tab, and the contents of the workflow are loaded.
If the name of the loaded workflow is shown at the top of the screen, it worked.
> **Tip**
> On the GenerateUI screen, button and item names are shown in English.
> This book uses the displayed English names as they are.
---
## 5-2 Reading the GenerateUI screen
The GenerateUI tab is divided into the following main parts.
| No. | Place | Role |
| --- | --- | --- |
| 1 | Top bar | Connection state with ComfyUI, workflow name, various buttons |
| 2 | Subtabs | Switch between Input / Model / Settings / Feeder / Batch / Lab |
| 3 | Editing area (center) | The settings of the chosen subtab |
| 4 | Raw JSON (right side) | Shows the contents of the workflow as JSON (for advanced users) |
| 5 | Generate panel | The Generate button, progress, seed settings, generated results |
### 1. The top bar
| Display / button | Meaning |
| --- | --- |
| Connected / Disconnected | Whether it is connected to ComfyUI. If it is a green **Connected**, you can generate |
| Connect | Reconnects to ComfyUI |
| Refresh Models | Reloads the list of models (press it after adding models to ComfyUI) |
| Reset Workflow | Discards edits and returns to the state right after loading the workflow |
| Workflow name | The workflow currently loaded |
> **Tip**
> When you open the GenerateUI tab, it connects to ComfyUI automatically.
> If it shows **Disconnected**, check that ComfyUI is running and press **Connect**.
### 2. The subtabs
| Subtab | What you set | In this book |
| --- | --- | --- |
| Input | Prompts, input images | 5-3 |
| Model | Models such as Checkpoint, VAE and LoRA | 5-3 |
| Settings | Seed, step count, sampler, image size and so on | 5-4 |
| Feeder | Process images in a folder in order | 5-6 |
| Batch | Generate in bulk while changing conditions | 5-5 |
| Lab | Experimental I2I batch generation (advanced) | 5-7 |
At first, it is enough to learn just the three: **Input, Model and Settings**.
### 4. Raw JSON
To the right of Input, Model and Settings, the contents of the workflow are shown in JSON format.
When you change values in the form, they are reflected here too.
Until you are used to it, treat this as "view only".
On the Input subtab, the **>>** button at the top of the Raw JSON folds it into a thin strip at the right edge (v0.8.12 and later),
which gives the prompt and image areas more room. Press it again to unfold it; the folded state is remembered.
---
## 5-3 Generating your first image
Now, to generate. At minimum, you check and set these three things.
1. Which model to draw with (Model)
2. What to draw (Input)
3. Generate (Generate)
### Step 1: Choose a model (Model)
1. Open the **Model** subtab.
2. In the **Checkpoint** field, choose a model you have.
If the list is long, typing characters in the box above the field narrows it down.
3. Press the **Apply** button below the field.
> **Tip: when a model cannot be found**
> The model saved in a workflow may not be on your computer.
> You will almost always run into this when using distributed workflows. It is signaled in one of two ways.
> - The field shows "(model name) (not found)" with a warning mark
> - When loading, a message says the workflow's Checkpoint "..." was not found and was replaced with another model without warning
> (in this case it has been replaced automatically with the first model in the list you have)
>
> From v0.8.7, if you have exactly one model with the same file name that differs only in its folder, it is replaced with that model instead of the first one.
> (Official templates often save model names without a folder, and the old behavior could swap in an unrelated model.)
>
> In either case, choose the model you want to use from the list and press Apply.
> If choosing again each time is a nuisance, the **Default Checkpoint** in the Settings tab (Chapter 11) is handy.
> **Caution: Don't forget Apply**
> In GenerateUI, after changing a model or the Settings values (5-4), you must press **Apply** for them to be reflected in the workflow.
> If you Generate without pressing Apply, the image is generated **with the values from before the change**.
> When "I changed a setting but the result didn't change", suspect a forgotten Apply first.
> (Only the prompt input fields are reflected automatically when you press Generate)
> **Tip**
> Item names shown in bright green are items for which the loaded workflow contains a node for that model.
> Items that are not green have no effect on this workflow even if you change them.
### Step 2: Write a prompt (Input)
1. Open the **Input** subtab.
The prompt area (Prompt) is on the left and the input image area (Image) is on the right (v0.8.12 and later; before that you switched between Prompt and Image).
When the screen is narrow, the Image area moves below the prompts.
2. In the **Positive Prompt** field, enter in English what you want drawn.
```
a cute cat sitting on a windowsill, sunlight, soft colors, high quality
```
3. Press **Apply** for the **Positive Prompt**.
4. If needed, enter in the **Negative Prompt** field what you do not want drawn, and press **Apply** for that too.
> Even if you forget Apply, the prompt is reflected automatically when you press Generate.
> However, you can check the reflected result in the Raw JSON (right side), so it is reassuring to get into the habit of pressing Apply.
```
low quality, blurry, bad anatomy
```
> **Tip**
> Pressing the **reset arrow** button next to Apply returns the contents of the field to the value currently reflected in the workflow.
> Handy when an edit went wrong.
> **Tip**
> If you are not good at English, the AI translation features introduced in Chapters 7 and 10 let you turn a prompt you thought up in your own language into English.
### Step 3: Generate (Generate)
1. Press the **Generate** button in the generate panel on the screen.
2. A bar showing progress extends, and a percentage is displayed.
3. When "Done" is shown, the generated image appears in the generate panel.
Congratulations. You have completed your first image with Workflow Studio.
A generated image is saved in ComfyUI's `output` folder, just as when generated in ComfyUI.
To look at it again later, you can browse it in the **Gallery** tab (Chapter 8).
> **Tip: when you want to stop partway**
> During generation, a **Stop** button appears next to the Generate button. Pressing it interrupts generation.
> **Tip: Apply and Generate at once**
> **Click Apply while holding the Alt key** to reflect the values and generate in one go.
> Once you are used to it, you can try things quickly: rewrite the prompt, then Alt + Apply.
### Saving the workflow you generated with
If you want to use settings changed in GenerateUI again later, press the **Save** button at the right end of the subtabs.
If you save it with a name, it is added as a new workflow in the Workflow tab.
---
## 5-4 What seed, sampler and step count mean
Once the first image is out, next look at the **Settings** subtab, where settings that decide how the result turns out are found.
The Settings tab is divided into three columns.
- **KSampler (left)** - settings for how the image is drawn
- **Latent Image (middle)** - settings for the size and number of images
- **Switches (right, v0.9.6+)** - the on/off settings inside the workflow
Here too, for KSampler and Latent Image, don't forget to press **Apply** on each block after changing values (the Switches column needs no Apply).
### KSampler items
| Item | Meaning | Rule of thumb for beginners |
| --- | --- | --- |
| Seed | The "seed of the random numbers" the image grows from. With the same settings and the same seed, you get almost the same image | When generating, the seed settings in the generate panel (below) take priority, so think of this as "for checking the seed of the loaded workflow". To use the workflow's seed as it is, set the generate panel's mode to **Workflow** |
| Steps | How many times to repeat the drawing. More is more careful but takes longer | 20 to 30 |
| CFG | How faithfully to follow the prompt. Too high and colors and shapes tend to break down | About 5 to 8 for SDXL |
| Sampler | The drawing algorithm | `euler`, `dpmpp_2m` and so on |
| Scheduler | How the drawing amount is distributed across steps | `normal`, `karras` and so on |
| Denoise | How much to redraw the original image. 1.0 when making from text | 1.0 (0.4 to 0.7 and so on when making from an image) |
> **Tip**
> The suggested values differ by model. The model's distribution page (Civitai and so on) often lists recommended values, so check it.
> If unsure, using the values the sample workflow starts with is fine.
### Latent Image items
| Item | Meaning | Rule of thumb |
| --- | --- | --- |
| Width | Horizontal size of the image (pixels) | Around 1024 for SDXL, around 512 for SD1.5 |
| Height | Vertical size of the image (pixels) | Same as above |
| Batch Size | How many images to make in one generation | 1 to 4 (more uses more memory) |
> **Caution**
> If you stray far from the size the model learned, people tend to come out doubled or the composition falls apart.
> Even when you want portrait or landscape, adjust so the total pixel count stays about the same (for example 832x1216 for SDXL).
### Switches items (v0.9.6+)
The "on/off" settings inside the workflow are listed with their node name and item name.
Clicking a checkbox switches it right away. **You don't need to press Apply**. The new value also shows up in the Raw JSON on the right at once.
Two kinds of items are listed:
- Items whose value is true/false - for example the switch that turns the prompt enhancer (PE) on and off, "Boolean" nodes, and TextGenerate's thinking
- On/off items with only two choices - enable/disable, on/off, yes/no and so on. For example KSamplerAdvanced's add_noise and DWPreprocessor's detect_hand (whether to detect hands)
The right end of each row shows the current value (true/false, enable/disable and so on).
Hover over a row to see the node's number and type.
> **Tip**
> Items wired from another node are not listed. Instead, the source node they are wired from (for example a Boolean node named "Enable PE?") is listed, so switch that one.
> In workflows where nodes have no names, the node type name (such as ComfySwitchNode) is shown.
> If the workflow has no on/off items at all, "No true/false settings in this workflow" is shown.
### Managing the seed (generate panel)
The seed is managed with the **Seed** field in the generate panel and the pull-down below it.
| Mode | What it does | When to use it |
| --- | --- | --- |
| Random | Generates with a random seed every time | When you want to try various pictures |
| Fixed | Fixes it to the value in the Seed field | When you want to keep a picture you like and change only the prompt to compare |
| Increment | Increases the seed by 1 each time you generate | When you want to make slightly different pictures in sequence |
| Decrement | Decreases the seed by 1 each time you generate | The reverse of Increment |
| Workflow | Uses the seed in the workflow as it is, without overriding it | When you want to reproduce, with the original seed, an image loaded from the Gallery or a distributed workflow |
When generation finishes, the Seed field shows **the seed value actually used**.
> **Tip: Workflow mode (v0.8.3 and later)**
> If you set the mode to **Workflow**, it generates with the loaded workflow's own seed, not the Seed field's value or a random one.
> Even after trying several images with Random, it returns to the workflow's original seed (the value at the time of loading) when generating.
> If no seed input can be found in the workflow, the value in the Seed field is used (random if it is -1).
> If you press it twice in a row with the same seed and the same settings, ComfyUI reuses the already computed result, so no new image is added (this is not a malfunction).
### The flow of "growing" a picture you like
Using the seed, you can improve a picture you like a little at a time.
1. Generate several with **Random** and look for a composition you like
2. When a picture you like appears, switch the mode to **Fixed** (the Seed field already holds that picture's seed)
3. Add words to the prompt or change Steps, and compare the differences
Fixing the seed makes it easy to see "what you changed, and how it changed".
This is the shortest path to getting used to image generation.
> **Summary of this chapter (first half)**
> - Select in the Workflow tab and press "Load in GenerateUI" to edit in GenerateUI
> - The basics are the three subtabs Input (prompt), Model (model) and Settings (detailed settings)
> - After changing the model or Settings values, always press **Apply**. If you forget, it generates with the values from before the change (prompts and the Settings Switches column are reflected automatically)
> - Generate to produce. Alt + Apply reflects and generates in one go
> - If you set the seed to Fixed, you can keep a picture you like and compare by changing conditions. To use the workflow's seed as it is, use Workflow
@@ -0,0 +1,240 @@
## 5-5 Batch generation - trying while changing conditions
As you get used to image generation, you start wanting to try things like these.
- Try all the models you have with the same prompt and compare them
- Generate several prompts one after another
- Check in bulk which sampler suits this model
Changing the settings and pressing Generate for each image is a lot of work.
With the **Batch** subtab, you specify "what to change while generating" all at once, and it generates in order automatically.
### What can be changed in a batch
In a batch, you choose **one** of the following seven kinds and generate while changing it in order.
| Kind | What changes | When to use it |
| --- | --- | --- |
| Checkpoint | The model | Compare how models draw |
| Lora | The LoRA | Compare the effect of LoRAs |
| Prompt | The prompt | Try saved prompts in order |
| Workflow | The workflow | Run several workflows in order |
| Sampler | The sampler | Compare the differences between samplers |
| Scheduler | The scheduler | Compare the differences between schedulers |
| Style | The style | Compare the differences between styles |
> **Tip**
> Only one kind can be changed at a time.
> If you want to change both the model and the prompt at once, use the Lab in 5-7.
### The Batch tab screen
The Batch tab is divided into three parts: left, center and right.
| Place | Role |
| --- | --- |
| Left (lists) | Choose Checkpoints / Sampler / Scheduler / Style from a list |
| Center (groups) | Choose Checkpoint / Lora / Prompt / Workflow by "group" |
| Right (Batch Queue) | The list of chosen items. This is where you decide which kind to run |
The groups in the center are linked to the groups made in each tab.
- Checkpoint and Lora groups -> the Models tab (Chapter 6)
- Prompt groups -> the Prompt tab (Chapter 7)
- Workflow groups -> the Workflow tab (Chapter 4)
### Example: comparing the models you have
Let's compare how three models draw, using the same prompt and the same seed.
1. Using the steps in 5-3, load beforehand a workflow with the prompt you want to compare.
2. Set the seed mode in the generate panel to **Fixed**.
(Fixing the seed evens out every condition except the model, so the difference is easy to see)
3. Open the **Batch** subtab.
4. On the left **Checkpoints** tab, tick the models you want to compare.
5. In the right-hand **Batch Queue**, turn on the checkbox in the heading of the **Checkpoint** column.
The column shows the list of chosen models and their count.
6. Press **Generate**.
It generates in order with the chosen models.
The **Batch** area of the generate panel shows which one is being generated now, and the overall progress.
- **Pause** - pauses. Pressing it again resumes.
- **Stop** - stops the whole batch.
> **Caution**
> If a Batch Queue column's checkbox stays on, the next time you press Generate it will run as a batch again.
> If you want to generate just one image, turn the column's checkbox off.
> **Tip: LoRA batches**
> In a Lora batch, the format of the chosen LoRA (`<lora:name:strength>`) and the trigger words obtained from Civitai
> are added automatically to the Positive Prompt. When the batch ends, it returns to the original prompt.
---
## 5-6 Feeder - processing images from a folder in order
**Feeder** is a feature that feeds many images one at a time into a workflow and processes them in order.
For example, you can use it like this.
- Convert photos you have collected into an illustration style in bulk (I2I: making an image from an image)
- Read poses from several images and draw a character in the same pose
Feeder has two modes, switched with the **[Image Loop] / [Gallery]** buttons at the top left of the Feeder tab.
| Mode | Where images are taken from | What you need |
| --- | --- | --- |
| Image Loop | A folder on your computer | The custom node **comfyui-image-feeder** (installed separately) |
| Gallery | A group in the Gallery tab | No extra installation (the **Gallery Feeder (WFS)** node bundled with Workflow Studio) |
> **Tip**
> In both modes, **a dedicated node must be in the workflow**.
> It cannot be used just by loading an ordinary workflow.
### How to use Gallery mode (no extra installation)
1. On ComfyUI's node screen, place a **Gallery Feeder (WFS)** node where the workflow "loads an image",
and connect its image output. Save, and load it into GenerateUI.
2. In the **Gallery** tab (Chapter 8), gather the images you want to process into a group.
(When you first open the Feeder tab, a group called `__Feeder__` is created automatically. You may use this)
3. Open the **Feeder** subtab of GenerateUI and choose **[Gallery]**.
4. Choose the target **Gallery Feeder Node** and the **group** to take images from, and press **Apply**.
5. The images of the group line up in the center. Click an image to start processing from that image.
6. Choose **After gen** (what to do after generation) and press **Run**.
### After gen (what to do after generation)
| Mode | What it does |
| --- | --- |
| Loop | Moves to the next image and, on reaching the end, returns to the start and continues (does not stop until you press Stop) |
| Increment | Moves to the next image, and stops automatically after processing the last one |
| Fixed | Keeps using the same image (when you want to make many images changing only the seed) |
At first, choosing **Increment** is reassuring: it processes each image once and stops automatically.
### Image Loop mode
In Image Loop mode, you use the images in a folder on your computer (`ComfyUI/user/default/image-loop-data/`).
You can also choose images from the folder and save them as a preset.
The **comfyui-image-feeder** custom node is needed separately.
The bundled samples `ws_sdxl_image_feeder_i2i` (I2I) and `ws_dwpose_image_feeder` (pose reading) are for this Image Loop mode.
### A sample for Gallery mode (v0.8.3 and later)
For Gallery mode you can use the bundled sample `ws_sdxl_gallery_feeder_i2i` (SDXL I2I).
You do not need to place nodes yourself: a **Gallery Feeder (WFS)** node is already in it (group name `__Feeder__`, sort order filename_asc).
1. Import the sample in the Workflow tab and press **Load in GenerateUI**.
2. In the Gallery, press the **F** button at the top left of the thumbnail of each image you want to process, to put it into the `__Feeder__` group.
3. In the **Feeder** subtab, choose **[Gallery]**, choose the Gallery Feeder Node and the group, and press **Apply**.
4. In the Model subtab, choose again the Checkpoint and VAE you have, and press **Apply** (the model names in the sample are those of the author's environment).
5. Set **After gen** to **Increment** and press **Run**.
---
## 5-7 Lab - experimental I2I batch generation (advanced)
**Lab** is an advanced, experimental feature that develops the batch generation of 5-5 further.
Feel free to skip it at first.
### Differences from Batch
| | Batch (5-5) | Lab |
| --- | --- | --- |
| What can be changed at once | Only one kind | Model, prompt and KSampler **separately from each other** |
| When changes happen | Every time | You freely specify "from which run to change" |
| Using a generated image as the next input | Not possible | Possible (can be chained) |
### The idea of keyframes
In Lab, you specify "from which generation run to switch to which setting" as **keyframes**.
Example: generating 10 times
| Runs | Model | Prompt | KSampler |
| --- | --- | --- | --- |
| 1 to 10 | Model A | - | - |
| 1 to 4 | - | "Morning city" | - |
| 5 to 10 | - | "Night city" | - |
| 1 to 7 | - | - | Steps 20 |
| 8 to 10 | - | - | Steps 40 |
In this way, the timing of switching can be decided separately for each column.
Until the next keyframe arrives, the previous setting is carried over.
### Basic flow
1. Load a workflow into GenerateUI.
2. Open the **Lab** subtab.
3. For I2I (image to image), drop the source image.
When making from text (T2I), tick **T2I workflow (no source image)**.
4. Decide the number of generations (Batch).
5. In each of the Model, Prompt and KSampler columns, press **+** to add a keyframe,
then double-click the keyframe and enter "from which run" and "to which setting".
Cells in the KSampler column show the items you set (Step, CFG, Denoise, sampler, scheduler, Seed).
A cell with nothing set shows "(workflow default)".
6. Press **Run**.
Results are listed in the **Results** panel.
### Handy features
- **Use generated image for next** - uses the generated image as the input image of the next run.
You can make a series of images that change little by little.
- **Plan Save** - lets you save the settings (the plan). When saved, an overview image summarizing the results is also made,
and dropping that image lets you load the same plan again.
- **Making a plan from a story** (v0.9.5+) - with the **Storyboard to Lab** skill in the AI TOOL chat,
a story is split into one prompt per scene and the **Open in Lab** button loads the plan here (Chapter 10, section 10-5).
---
## 5-8 Using partner APIs (Nano Banana)
ComfyUI has **partner API nodes**, such as Google's **Nano Banana** image model (the Gemini image nodes), that hand the generation over to a cloud service.
From v0.8.6, GenerateUI can also run workflows that contain these nodes.
> **Caution**
> Partner API nodes run in the cloud, not on your PC's GPU, and **consume Comfy credits (paid)**.
> If you run them repeatedly with Batch, Lab and so on, credits are consumed once per run.
### Preparation: set your Comfy API key
1. Issue a Comfy API key at platform.comfy.org.
2. Add the line `COMFY_API_KEY=your key` to the `.env` file in the plugin folder.
(If there is no `.env`, copy `.env.example` to make one.)
3. Restart ComfyUI. `.env` is read only at startup.
The key is used only on the server side and is never sent to the browser.
If you run without a key, a message says that partner API authentication failed and how to set it up.
### How to use
1. In the Workflow tab, load the Nano Banana sample (`ws_nano_banana_2_1_t2i`) into GenerateUI.
2. Edit the prompt on the left side (Prompt) of the **Input** subtab and press **Apply**.
3. To use reference images, specify them on the image cards on the right side (Image) of the **Input** subtab.
4. Press **Generate**.
> **Tip**
> You can also run it from ComfyUI's own screen while logged in. In that case you do not need the `.env` setting.
---
## 5-9 More workflows are supported (v0.8.4 and later)
The workflows you can use in GenerateUI have increased as follows. They are used the same way as other workflows.
- **Qwen Image 2.1 Turbo** - ComfyUI's official templates for text to image (T2I) and image to image (I2I). Edits also carry over to the prompt enhancer's input (the feature that expands your prompt automatically).
- **Flux.3 Klein** - the four official templates (T2I and Image Edit, for Klein and Klein SD).
- **MOSS-Image** - Base/SFT T2I, and the bounding-box versions that specify positions with frames.
- **Fibo 2.0 Edit** (v0.8.14 and later) - an image-editing workflow. With the prompt enhancer (PE) setup, edits in the prompt area apply whether PE is on or off. The negative area holds a VGL-format (JSON) description as is. Width and height (Settings) of 0 mean automatic.
- **FastH3 OmniRef / Pika-H3-Flex** (v0.8.14 and later) - MiniMax H3 reference to video. Treated as video workflows, and also usable in the Video tab (Chapter 11).
- **Native LoRA stack** - ComfyUI v0.40's nodes that load several LoRAs at once (LoadLoraModel / LoadLoraTextEncoder). Apply, bypass and the Metadata display are supported.
- **P-H3 Image** (v0.9.4+) - the image-edit and T2I templates. The prompt is shown on the Input tab and can be read by the Metadata tab; there is no negative prompt field. Width and height follow the workflow's wiring, and the prompt upsampler runs as set in the workflow.
- **MiniMax H3 Image** (v0.9.6+) - ComfyUI's official image-edit and T2I templates. As with P-H3 Image, the prompt is shown on the Input tab and can be read by the Metadata tab and the sidepanel I tab; there is no negative prompt field. The prompt enhancer (PE) can be turned on and off in the Settings tab's Switches column (5-4).
> **Caution**
> The native LoRA stack nodes are supported **only in API-format workflows**.
> ComfyUI's own screen cannot yet add rows to this node, so UI-format workflows are not covered.
> **Tip**
> Workflows containing the **Compare Images** (ImageCompare) node, which compares two images with a slider, used to fail on that node when run from GenerateUI (fixed in v0.9.6).
> **Tip**
> The Metadata tab and the side panel's **I** tab can also read images made with Nano Banana (v0.8.5 and later).
> The prompt appears as one whole entry, not split into Positive/Negative.
> **Summary of this chapter (second half)**
> - With the Batch subtab, you can generate in bulk while changing the model, prompt, sampler and so on, one kind at a time
> - Choose the kind with the Batch Queue column checkbox and start with Generate. Turn the check off when done
> - Feeder is a feature that feeds many images one at a time into a workflow. A dedicated node is required
> - Lab is an advanced feature that can switch several settings at separate timings
> - Partner API nodes (Nano Banana and others) run once `COMFY_API_KEY` is set in `.env`. They consume credits
@@ -0,0 +1,184 @@
# Chapter 6 The Models Tab - Managing Models and LoRAs
As you keep generating images, model and LoRA files keep increasing.
"What model was this file?" "What was that LoRA's trigger word?"
- The **Models** tab is what solves worries like these.
---
## 6-1 Kinds of models
At the top of the Models tab, tabs for each kind of model are lined up.
Each shows the files in the corresponding folder inside ComfyUI's `models` folder.
| Kind | In a phrase | Main location |
| --- | --- | --- |
| Checkpoint | The core of image generation. Nothing starts without it | `models/checkpoints` |
| LoRA | A small model that teaches an art style, character, outfit and so on | `models/loras` |
| VAE | Finishes the colors and fine details of an image. Often built into the Checkpoint | `models/vae` |
| ControlNet | A model for specifying composition, such as poses and line art | `models/controlnet` |
| UNET | The core of newer-format models such as FLUX (Diffusion Model) | `models/diffusion_models` and so on |
| TextEncoder | A model that converts the prompt text into a form the AI can understand | `models/text_encoders` and so on |
| Hypernetwork | An older-format add-on model for adjusting art style | `models/hypernetworks` |
| Embedding | A small file that teaches a particular word a meaning (often used for negatives) | `models/embeddings` |
> **Tip**
> At first, it is enough to make good use of just two: **Checkpoint** and **LoRA**.
> When you start using newer models such as FLUX, UNET and TextEncoder come up more often.
---
## 6-2 Displaying and finding in the list
### The first load
When you open the Models tab for the first time, the list is empty and a message says to load models with the refresh button.
Press **"Refresh"** on the toolbar and the list of models appears.
Press "Refresh" also when you have added new models.
### Thumbnail view and table view
As in the Workflow tab, you can switch between **Thumbnail** and **Table**.
24 items are shown per page.
In table view, clicking a column heading sorts by that column
(first click: ascending, second click: descending, third click: sorting off).
> **Tip**
> For thumbnails, `(model name).preview.png` located in the same place as the model file is used automatically.
> For models without an image, you can get one from Civitai using the method in 6-3, or set one yourself.
### Searching and filtering
The toolbar has items for filtering.
| Item | What it filters |
| --- | --- |
| Search models... | Keywords such as the name |
| Star | Favorites only |
| All Tags | Tags |
| All Folders | Subfolders |
| All Groups | Groups |
| All / Enabled only / Disabled only | Enabled or disabled state (6-5) |
If you narrow down so far that you lose track, press **"x Clear"** at the right end of the toolbar to clear everything.
### Favorites, badges and groups
The way of sorting is almost the same as in the Workflow tab (Chapter 4, section 4-4).
- **Favorites (star)** - mark models you use often
- **Badges** - attach colored labels made with **"Badge"**. The kinds and colors of badges are shared with the Workflow tab
- **Groups** - click a model and add it in the side panel's **"Groups"** tab
> **Tip**
> Groups are divided **by kind of model**.
> A Checkpoint group is shown only in the Checkpoint tab.
> Groups made here can also be used in the GenerateUI batch generation (Chapter 5, section 5-5).
---
## 6-3 Bringing in Civitai information
**Civitai** is a sharing site where many image-generation models are published.
Many of the models you get online are distributed on Civitai.
Workflow Studio computes a **hash value** (a value like a fingerprint of the file) from your local model file,
and can automatically look up the information registered on Civitai.
### What information you can get
- The model's name and kind (Checkpoint, LoRA and so on)
- **Base Model** (which family the model belongs to: SDXL, SD1.5, FLUX and so on)
- **Trigger words** (words to put in the prompt to bring out a LoRA's effect)
- Tags and description
- Sample images (also used for thumbnails)
### Getting one at a time
1. Click a model to select it.
2. In the right-hand side panel, open the **"CivitAI"** tab (this tab opens first when you select a model).
3. Press **"Fetch from CivitAI"**.
When fetching finishes, the model's information is shown.
In the **Sample** tab, you can look at sample images posted to Civitai.
### Getting them in bulk
Press the **"CivitAI"** button on the toolbar to fetch information in bulk for the models of the kind being shown.
Models whose information has already been fetched are skipped, so it is fine to press it again and again after adding models.
For models without a thumbnail image, a preview image is also saved automatically.
> **Tip**
> Computing the hash value takes time depending on the size of the model.
> If you have many models of several GB, run it when you have time to spare.
> **Tip**
> A model that was not found on Civitai shows a message saying it was checked but this model was not found on CivitAI.
> For models you made yourself, or models distributed somewhere other than Civitai, not being found is normal.
> If you tick **"Skip not found"**, models that were not found are skipped without being rechecked when fetching in bulk.
> **Tip: API key**
> The **"CivitAI API Key"** in the Settings tab can be left empty (optional).
> If authentication is needed to get some information, enter a key issued in your Civitai account settings.
### Especially handy when using LoRAs
Many LoRAs have no effect unless you put their **trigger words** in the prompt.
If you have fetched the Civitai information, you can check the trigger words in the side panel right away.
Also, when you use a LoRA in GenerateUI batch generation, the trigger words are added to the prompt automatically (Chapter 5, section 5-5).
---
## 6-4 Using models in generation and on the canvas
### Set it in GenerateUI (GenUI Model)
You can set the model chosen in the Models tab directly into the workflow in GenerateUI.
1. Load a workflow into GenerateUI beforehand (Chapter 5, section 5-1).
2. In the Models tab, select the model you want to use.
3. Press the **"GenUI Model"** button.
The model in the GenerateUI workflow is now replaced with the one you chose.
While comparing Civitai sample images, you can try any model that catches your eye right away.
> **Tip**
> For an Embedding, **GenUI PP** (add to the positive prompt) and **GenUI NP** (add to the negative prompt) buttons are shown.
> Add negative Embeddings with GenUI NP.
> **Caution**
> It cannot be used if the GenerateUI workflow has no node that loads that model.
> For example, you cannot set a LoRA in a workflow that has no LoRA node.
### Using it on ComfyUI's node screen
If you drag a model from the **M (Models)** tab of the **WS Library** beside ComfyUI's node screen onto the canvas,
a node that loads that model is placed automatically (Chapter 9).
---
## 6-5 Tidying away models you do not use
### Switching enabled and disabled
Models you will not use for a while can be set to **Disable** instead of being deleted.
A disabled model no longer appears in ComfyUI's model-selection lists, so the lists become tidier.
- With the **pause (II)** button on a card, you can switch enabled and disabled one at a time.
- You can also enable or disable them in bulk, by group.
> **Tip**
> When disabled, `.disabled` is added to the end of the file name. The file itself is not deleted.
> When you enable it again, the original name comes back.
### Operating in bulk (Select)
Press the green **Select** button to tick several models and operate on them together.
- Add to or remove from a group
- Add or remove badges
- Move to another subfolder (related files such as preview images move together)
- Delete
### Deleting
You can delete a model from the detail screen or from Select.
> **Caution**
> Deleting **permanently removes** the model file and its related files (preview image, Civitai information and so on).
> Many models take a long time to download again, so if in doubt, "disable" instead of deleting.
> **Summary of this chapter**
> - In the Models tab you can list ComfyUI's models by kind. Press "Refresh" first
> - You can sort with favorites, badges and groups. Groups can also be used in batch generation
> - With "Fetch from CivitAI", you can automatically bring in Base Model, trigger words, sample images and more
> - With "GenUI Model", you can try the chosen model in GenerateUI right away
> - For models you do not use, "disable" rather than delete, to be safe
@@ -0,0 +1,250 @@
# Chapter 7 The Prompt Tab - Collecting and Growing Your Spells
A prompt that worked well is your own treasure.
But if you only paste it into a notepad, you can easily lose track of which was which.
The **Prompt** tab is a place to **save, sort and reuse** prompts.
In addition, you can use translation with local AI, and "wildcards" that change the prompt a little each time.
---
## 7-1 Prompt basics
Before getting into how to use the Prompt tab, let's cover the basics of writing prompts.
### Positive and negative
| Kind | What to write | Example |
| --- | --- | --- |
| Positive Prompt | What you want drawn | `1girl, smiling, school uniform, cherry blossoms, spring` |
| Negative Prompt | What you do not want drawn | `low quality, blurry, extra fingers, watermark` |
### Tips for writing
- **Write in English** - most models were trained on English.
- **Separate with commas (,)** - it is common to line up words and short phrases separated by commas.
- **Put important things first** - words written earlier tend to be reflected more strongly.
- **Emphasize words with (word:number)** - writing `(red hair:1.3)` emphasizes that word (1.0 is the default).
> **Tip: change the emphasis number from the keyboard**
> In the prompt field of GenerateUI, select the word to emphasize and press **Ctrl + Up/Down**
> to raise or lower the number in `(word:number)` by 0.05. When the number returns to 1.0, the parentheses are removed automatically.
> **Tip**
> Anime-style SDXL models suit "lining up tags", and newer models such as FLUX suit "describing in ordinary English sentences".
> The model's distribution page often lists a recommended way of writing, so check it.
---
## 7-2 The Prompt tab screen
The Prompt tab is divided into three columns: left, center and right.
| Column | Name | Role |
| --- | --- | --- |
| Left | AI Assistant | Chat and translation with local AI (7-5) |
| Center | Presets / Preset Manager | Saving, listing and sorting prompts (7-3) |
| Right | Wildcard / Style | Managing wildcards and styles (7-4) |
With **Form / Table** at the top of the screen, you can switch how things are displayed.
- **Form** - a screen for editing one item at a time (the screen this chapter explains)
- **Table** - a screen for editing presets and wildcards together in a table (handy when the count grows)
---
## 7-3 Saving and sorting as presets
A **preset** is a pair of positive and negative prompts saved together under a name.
### Saving a preset
1. In the center column, open the **Presets** tab.
2. Check that the topmost pull-down shows **New Preset (none selected)**.
3. Fill in the following items.
| Item | Contents |
| --- | --- |
| Preset Name | The name of the preset (for example `Sakura and uniform`) |
| Category | A classification (for example `Portrait`, `Landscape`). May be omitted |
| Positive Prompt | What you want drawn |
| Negative Prompt | What you do not want drawn |
4. Press **Save**. When a message says the preset was saved, you are done.
> **Tip**
> Get into the habit of saving prompts you tried in GenerateUI and thought "this is good!" as presets on the spot.
> Later, when you want to make an image with the same feel, you can recall it right away.
### Recalling and using a preset
1. From the topmost pull-down, choose the preset you want to use.
2. Press a button according to how you want to use it.
| Button | What it does |
| --- | --- |
| GenUI Set | Sets both the positive and negative into the prompt fields of GenerateUI at once |
| PP Copy | Copies the positive prompt |
| NP Copy | Copies the negative prompt |
> **Caution**
> **GenUI Set** can be used while a workflow is loaded in GenerateUI.
> Load a workflow first, using the steps in Chapter 5.
### Sorting with groups
Presets can also be sorted into groups.
1. With a preset selected, press **+ Group** to make a new group.
2. Choose a group from **Select group...** and press **Add to Group**.
A group can also be deleted (**Del Group**).
Groups made here can be used as "Prompt" groups in GenerateUI batch generation (Chapter 5, section 5-5).
For example, if you put spring, summer, autumn and winter presets in a group called "Seasons series",
you can generate the pictures of all four seasons in bulk.
### Listing with Preset Manager
In the **Preset Manager** tab of the center column, you can see the saved presets as a list.
- **All** - all presets
- **Star** - presets registered as favorites
- **Groups** - shown by group
With the **Search presets...** box at the top, you can search presets.
Saved presets are also shown in the **P (Prompts)** tab of the **WS Library** beside ComfyUI's node screen,
and can be dragged onto the canvas for use (Chapter 9).
---
## 7-4 Wildcards and styles
In the right column, you manage **wildcards**, which change the prompt automatically,
and **styles**, which add a mood to the prompt.
Switch with **Wildcard / Style** at the top of the column.
### What is a wildcard?
A wildcard is a mechanism that **randomly swaps out a part of the prompt every time you generate**.
With the same prompt, you can make many images with variation.
The easiest way in Workflow Studio is to use a **wildcard file**.
Make a file listing candidates, and when you write `__filename__` in the prompt,
one line from the file is chosen at random every time you generate.
### Making a wildcard file
As an example, let's make a file called `colors` containing candidate colors.
1. In the right column, choose **Wildcard** and press **+ New** under **Wildcard Files**.
2. Enter a file name (for example `colors`) and choose **txt** as the format.
3. Enter one candidate per line.
```
red
blue
green
golden
```
4. Press **Save**.
### Using a wildcard
In the prompt, write the file name enclosed in `__` (two underscores).
```
1girl, __colors__ dress, standing in a garden
```
Each time you generate in GenerateUI, the `__colors__` part is replaced with one of `red`, `blue`, `green` or `golden`.
> **Tip**
> Instead of typing `__colors__` by hand, you can also choose a file with the **File** button in the right column and insert it.
### More advanced notation (for Impact Pack)
At the top of the right column there are also buttons that insert symbols such as `{ }`, `$$` and `[ ]`.
These are for notations like the following.
- `{cat|dog|rabbit}` - choose one out of the three
- `{2$$cat|dog|bird}` - choose two out of the three
> **Caution**
> What GenerateUI expands automatically is only the `__filename__` notation.
> To use notations such as `{A|B}`, you need to put the wildcard node of the custom node **ComfyUI-Impact-Pack**
> (ImpactWildcardProcessor and so on) in the workflow.
> Until you are used to it, using only the `__filename__` notation is enough.
### Sending the prompt you built
The prompt you built in the right column can be sent out with the buttons below.
- **-> Pos** - send to the preset's Positive Prompt
- **-> Neg** - send to the preset's Negative Prompt
- **Copy** - copy it
The `__filename__` parts are expanded automatically when you generate in GenerateUI.
### Styles
A **style** is a "mood template" added to the prompt.
For example, the "cinematic" style has contents registered such as `cinematic still, {prompt}, dramatic lighting`.
Your own prompt goes into the `{prompt}` part.
To use a style, in the top bar of GenerateUI, turn on the **Style** checkbox and choose a style from the pull-down.
When you generate, the chosen style is combined into the prompt automatically (the workflow itself is not rewritten).
Many styles come with Workflow Studio.
In **Style** of the Prompt tab, you can look at the list of styles and make your own with **+ New**.
> **Tip**
> To see what mood each style gives, it helps to use **Create Catalog** in GenerateUI to make sample images for each style in bulk.
> The samples you make can be listed in the Style_Catalog of the Gallery tab (Chapter 8).
---
## 7-5 Getting AI to help with translation and prompt making
In the **AI Assistant** in the left column, you can have AI running on your computer (local AI) help you make prompts.
> **What you need in advance**
> To use this feature, you need local AI software such as **Ollama** or **LM Studio**, and an AI model prepared.
> How to prepare is explained in Chapter 10. If it is not ready, **Disconnected** is shown at the top of the AI Assistant.
### Translating your language into English
Even if you are not good at English prompts, it is fine.
1. In the input box at the bottom of the left column, write in your language what you want drawn.
```
A girl in a school uniform smiling under a cherry tree, soft spring light
```
2. Press the **JA->EN** button under **Translate:** (choose the languages to suit your text).
3. The translated result is shown in the chat area.
With the buttons shown below the translation result, you can use the result.
| Button | What it does |
| --- | --- |
| Apply to GenerateUI | Puts it into the positive prompt field of GenerateUI |
| Copy | Copies it (for pasting into a preset and so on) |
When you want to know the meaning of an English prompt, you can conversely translate it with **EN->JA**.
### Making prompts while consulting the AI
Write a question in the input box and press **Send** (or the Enter key) to chat with the AI.
- "Make a prompt for a sunset seaside scene, in English tag format"
- "Add a more fantastical mood to this prompt"
If you ask like this, it offers draft prompts.
If you are using an AI model that can read images, you can attach an image with **Attach Image**
and ask "turn the mood of this image into a prompt".
- **Shift + Enter** - line break
- **Clear Chat** - reset the conversation
- **Gear button** - settings for which AI software and model to use (Chapter 10)
> **Caution**
> A local AI's answers are not always correct.
> Adjust the prompts that come out while actually generating to check them.
> **Summary of this chapter**
> - Prompts are basically English and comma-separated. Emphasis is `(word:number)`
> - Save prompts that worked as presets with a name
> - With "GenUI Set", you can set a preset into GenerateUI right away
> - If you sort them into groups, you can try them in bulk with batch generation
> - Make a wildcard file and write `__filename__` to make the prompt vary with each generation
> - If you prepare local AI, you can translate and consult about prompts
@@ -0,0 +1,331 @@
# Chapter 8 The Gallery Tab - Looking Back at Generated Images
As you keep generating images, hundreds and thousands of images pile up in ComfyUI's `output` folder.
The **Gallery** tab is a place to browse them comfortably,
and to solve "how did I make this image?".
Images generated in ComfyUI have information about the workflow and prompt used for generation (**metadata**) embedded in them.
The Gallery tab reads this information so that you can reproduce the same settings right away.
Videos (MP4 and WebM), audio files (WAV, MP3, OGG, M4A, FLAC and Opus) and 3D models (GLB, glTF, VRM, PLY and SPZ) can also be handled in the same list as images.
---
## 8-1 Listing images
At the top of the Gallery tab there are four subtabs.
| Subtab | Role | In this book |
| --- | --- | --- |
| Output | The list of generated images (this is what you normally use) | 8-1 to 8-3 |
| ImagePrompt | Build prompts while looking at sample images | 8-4 |
| Style_Catalog | Choose from sample images of styles | 8-4 |
| Metadata | Drop an image file to inspect the information in it | 8-2 |
### The Output screen
The **Output** subtab is divided into three parts: left, center and right.
| Place | Role |
| --- | --- |
| Left (folders) | The list of folders inside the `output` folder. The topmost **[root]** is the whole `output` folder |
| Center (list) | The images of the chosen folder. Scroll down and more are loaded |
| Right (detail panel) | Information about the chosen image (8-2) |
First, choose a folder from the folder list on the left, and its images appear in the center.
> **Tip**
> If images you newly generated in GenerateUI or ComfyUI do not appear, press **Refresh** on the toolbar.
> **Tip**
> The location of the `output` folder that is read can be changed with **"Gallery Output Folder"** in the Settings tab.
### Switching the display, searching and sorting
As in other tabs, you can do the following.
- **Thumbnail / Table** - switch between thumbnail display and table display
- **Search box** - search by file name, tags, memo and **the contents of the prompt**. If you turn on the **S** button next to it, you can search by the meaning of a sentence (8-6)
- **Sorting** - Date (Newest) / Date (Oldest) / Name A-Z / Name Z-A
- **Star** - show favorites only
- **x Clear** - clear all filters at once
> **Tip**
> In the search box you can also search by words in the prompt.
> For example, searching "cat" finds images whose prompt contains cat.
> (Prompt search targets images that you have opened once in the detail panel)
### Looking large, and comparing
- Clicking an image shows it large in the right-hand detail panel.
- **Hold the Ctrl key** and click images to select several.
Select two to four and press **Compare** to view them side by side.
Lining up images made by changing only the seed or the model makes the differences easy to see.
### Organizing
- You can sort with **favorites (star), tags, memo and groups** (set in the detail panel).
- You can select several and add them to a group together, or move them to another folder.
The screen for choosing the destination (**Move To...**) has a filter box: enter part of a folder name to narrow the candidates, and press Enter to move to the first candidate.
- Pressing **Export** bundles the chosen images into a ZIP file for download.
> **Caution**
> Using **Delete** in the detail panel, or **Delete File** for a multiple selection, deletes the image files themselves.
---
## 8-2 Reading generation settings from metadata
When you click an image, four tabs appear in the right-hand detail panel.
| Tab | Contents |
| --- | --- |
| Prompt | The prompts used to generate the image (the tab shown first) |
| Info | File name, size, date, tags, memo |
| JSON | The workflow embedded in the image (JSON) |
| Groups | Group settings |
### Checking the prompt
The **Prompt** tab lists the prompts that were embedded in the image.
Positive ones are marked **POS**, negative ones **NEG**.
Clicking an item shows the whole text.
### Examining more closely (the Metadata subtab)
When you want to know in detail even the models and LoRAs used, press the **Metadata** button in the detail panel
(or click an image while holding the Alt key).
The Gallery's **Metadata** subtab opens and the following information is shown in an organized way.
- **Models** - Checkpoint, VAE, Diffusion Model, Text Encoder (center column)
- **LoRA** - the LoRAs used and their strength (center column)
- **Prompt** - positive and negative (center column)
- **Settings** - the image size (width x height), and the KSampler's Seed, Steps, CFG, Sampler, Scheduler and Denoise (right column; v0.7.14 and later)
- **Nanobanana** - the generation history of images made in Comic Creator's nanobanana tab: engine, model, prompt and so on (right column; only for matching images)
With the buttons beside the prompt, you can send the contents to other tabs.
| Button | What it does |
| --- | --- |
| Copy | Copy |
| GenUI:P / GenUI:N | Put it into GenerateUI's positive / negative |
| Prompt:P / Prompt:N | Put it into the positive / negative of a preset in the Prompt tab |
> **Tip: you can also examine images you found online**
> If you **drop** an image file onto the left side of the Metadata subtab, you can examine that image's information.
> Besides ComfyUI images, PNG images made with SD WebUI (AUTOMATIC1111), SD Forge and Fooocus are also supported.
> Handy when you want to know how an image found on a distribution site was made
> (however, it cannot be read from images whose metadata was stripped when they were posted).
---
## 8-3 Reproducing the same settings
The handiest use of the Gallery is to **generate again with the same settings as an image you like**.
There are two ways.
### Method 1: Reproduce in GenerateUI (Load GenUI)
1. In the Gallery, click the image you want to reproduce.
2. Press the **Load GenUI** button in the detail panel.
3. The workflow embedded in the image is loaded into GenerateUI.
After that, as in Chapter 5, you can generate while changing the prompt a little or changing the seed.
> **Tip**
> **When you want to make the same image again (v0.8.3 and later)**
> The workflow embedded in the image also contains the seed value from that time.
> If you set the generate panel's seed mode to **Workflow** and press Generate, it generates with that seed as it is.
>
> If the mode stays at Random, it generates with a different seed.
> Also, if you use Fixed, paste the value of the **Seed** field in the **Settings** subtab into the generate panel's **Seed** field (because the generate panel's value takes priority).
### Method 2: Reproduce on ComfyUI's node screen (Copy & Send Canvas)
1. In the Gallery, click the image you want to reproduce.
2. Open the **JSON** tab in the detail panel.
3. Press **Copy & Send Canvas**.
The workflow that made that image is loaded onto ComfyUI's node screen.
Use this when you want to adjust things in detail on the node screen.
> **Caution**
> If a message says "this image has no ComfyUI workflow embedded", it cannot be reproduced.
> This applies to images made with settings that do not save workflow information, and images edited or compressed with other software.
### Using an image as material for the next generation
You can also use a Gallery image as an "input image" for generation.
| Button | What it does |
| --- | --- |
| Send GenUI Image | Sets the chosen image as the input image (LoadImage node) of the GenerateUI workflow |
| Send to LI node | Sets the chosen image on the image-loading node currently selected on ComfyUI's node screen |
| Image Edit | Opens the chosen image in the Image Edit tab (Chapter 11) |
| Send CC Layout | Sends the chosen image to Comic Creator's Layout tab (the selected panel / overlay) (shown only when the Gallery is opened inside Comic Creator; Chapter 11, section 11-6) |
| Send CC Image | Sends the chosen image to Comic Creator's Image tab (as a new layer if a document is open, otherwise as a new canvas; videos are not supported; shown under the same condition as Send CC Layout) |
Handy when redrawing a generated image further with an I2I (image to image) workflow.
---
## 8-4 ImagePrompt and Style_Catalog
### ImagePrompt - building prompts from sample images
**ImagePrompt** is a subtab for building prompts by choosing visually,
with "sample images" and "prompts" lined up as sets.
1. Click a sample image and the prompt of that image appears on the right side.
2. Press **+ Add** and it is added to **Selected Prompts**.
3. After adding several, a **Final Prompt**, with them joined by commas, is produced.
4. Use **Copy** to copy it and paste it into GenerateUI or a preset.
Put the sample images in the `ws_image_prompt` folder, which is created automatically inside the `output` folder.
If you put a `.txt` file with the same name as the image (its contents being the prompt) alongside it, it becomes usable as a sample.
> **Example**
> `ws_image_prompt/hairstyle/ponytail.png` and `ws_image_prompt/hairstyle/ponytail.txt` (contents: `ponytail, long hair`)
### Style_Catalog - choosing styles by sample images
The styles introduced in Chapter 7 are hard to picture from their names alone.
In **Style_Catalog**, you can line up sample images for each style and choose by how they look.
Sample images are made with **Create Catalog** in the top bar of GenerateUI.
1. Load a workflow that makes images from text into GenerateUI.
2. In the **Style** section of the **Batch** subtab, tick the styles you want samples of.
3. Press **Create Catalog** in the top bar, decide the destination folder and run.
4. Press **Catalog** in the top bar to move to Style_Catalog in the Gallery.
When you choose a sample image in Style_Catalog and press **Select as Style**,
GenerateUI's style switches to that style.
> **Tip**
> How a style takes effect changes with the model.
> If you separate the destination folders by model, they are easier to compare.
---
## 8-5 Eagle integration and PSD export (optional)
From here on, these are features only for those who need them.
### Eagle integration
**Eagle** is paid software for organizing images.
If you enable **Eagle integration** in the Settings tab, images generated in GenerateUI can be automatically saved to Eagle together with information such as the prompt.
If you manage images in Eagle, give it a try.
### PSD export
Select several images in the Gallery and press **Create PSD**,
and you can make a PSD (Photoshop format) file with each image stacked as a layer.
Handy when you want to gather difference images together and finish them in image-editing software.
> **Caution**
> To use PSD-related features, an extra Python package (psd-tools) is needed.
> If it is not installed, the installation method is shown in an error message (Chapter 11).
---
## 8-6 Finding images by "meaning" (semantic search)
Normal search finds images whose file name, tags or prompt characters match.
**Semantic search** (v0.8.2 and later) searches by the "meaning" of an image.
For example, if you enter "a city street at dusk", it finds images with a matching mood even when the prompt does not contain those words.
You can also bring up a list of **similar images** to the one you choose.
### How to use it
1. Turn on the **S** button next to the search box. The search box switches to semantic search, and a second row appears on the toolbar.
2. Press the **gear** (settings) button on the second row and build the **index** (an index for searching) (see below).
3. Enter a sentence in the search box and press **Enter**. Results line up in score order, with the score shown on each thumbnail.
4. Select an image and press **Similar** on the second row, and images similar to it line up. Select another image and press the **refresh** button to redo the similar-image list for that image. Press **Similar** again to return to the normal display.
> **Tip**
> Sorting and tag/group filtering do not apply to semantic search results.
> The search range is inside the folder you have selected (including subfolders).
> A folder with a green dot next to its image count has all the images directly in it indexed.
### Choosing a model (the "Model" tab of the gear)
| Model | Characteristics | Preparation |
| --- | --- | --- |
| Gemini Embedding 2 | Computed on Google's service. Fast. Images are sent to Google | Get an API key, write `GEMINI_API_KEY=key` in the plugin's `.env`, and restart ComfyUI |
| EmbeddingGemma 2 | Computed inside your computer with **Ollama** (local, free). Nothing is sent outside | Install and start Ollama, then run `ollama pull embeddinggemma-2:740m` (or the lighter `embeddinggemma-2:440m`). On the Model tab, choose the **Ollama URL** (usually unchanged) and the **Model size** |
> **Caution: when using Gemini**
> Images are sent to Google's service. Content sent under the free tier may be used by Google to improve its products.
> If you have many images you do not want to expose, choose the local EmbeddingGemma 2.
> Write the API key only in `.env`, and do not show it to others.
### Building the index (the "Index" tab of the gear)
1. Choose the target folder (you can also choose whether to include subfolders).
2. Choose "new only" or "rebuild all" and start. Progress is shown, and you can stop partway.
3. The index is one shared across all folders. You may build it a little at a time, folder by folder.
When you change the model or the vector dimension, a **rebuild of everything** is needed.
### Speed guidelines and settings
- Since v0.8.14 the local EmbeddingGemma 2 runs in Ollama (the earlier way of running it in ComfyUI's Python was removed).
On an NVIDIA GPU it takes about 0.1 to 0.2 second per image. When indexing finishes, the model is unloaded from GPU memory automatically.
- Ollama must be running while you build the index and while you search.
- The 740m (text + image + audio) and 440m (text + image, lighter) model sizes return the same results, so switching does not need a rebuild.
Indexes made with v0.8.13 or earlier keep working as they are.
- In the **Search** tab, you can decide the "minimum score" for similar-image and text search, and the maximum number of results.
Because the score level differs by model, it is saved per model.
---
## 8-7 Handling audio files, and converting
As opportunities to make music and sound effects in ComfyUI have increased, the Gallery can handle audio files too (v0.8.0 and later).
Supported are WAV, MP3, OGG, M4A, FLAC and Opus.
- In the list, a waveform thumbnail and a note badge are shown. The waveform color differs by format (MP3 blue, WAV green, FLAC amber, OGG/Opus purple, M4A pink).
- When you select one, the waveform and a player appear in the right-hand detail panel, and you can play it right there.
- For audio saved with a workflow (`prompt` / `workflow`) embedded, the Prompt and JSON tabs also work.
- Videos (MP4 and WebM) can be listed in the same way. With the **Send to LV node** button, you can send the chosen video to the **Load Video** node selected on the ComfyUI screen (the video version of Send to LI node for images).
### Converting files
When you select several items including audio or video, **Convert...** appears on the bulk-action bar.
1. Select the files you want to convert and press **Convert...**.
2. Decide the output format (FLAC / MP3 / Opus / M4A / WAV) and, if needed, the bitrate, sample rate and number of channels.
3. When run, the result is saved as a separate file in the same folder (if the same name exists, a number is added such as `name_converted`, and nothing is overwritten).
When you choose a video, only the audio track is extracted and saved.
| Carried over | Description |
| --- | --- |
| Workflow information | Carried over to MP3, FLAC, Opus and M4A. It cannot be carried over to WAV (a notice appears) |
| Favorites, tags, memo, groups | You can choose whether to carry them over |
| Deleting the original file | You can choose whether to delete it after a successful conversion (with a confirmation screen) |
> **Tip**
> OGG can be played, but cannot be chosen as an output format of conversion.
> You can convert with Workflow Studio alone, without installing other video-editing software.
> **Tip: use in the Video tab**
> In the Asset subtab of Video, you can set an audio file as "Set as BGM" or "Add as sound" (Chapter 11, section 11-4).
---
## 8-8 Viewing 3D models (v0.9.4+)
The results of ComfyUI workflows that make 3D models (such as image to 3D) can also be checked in the Gallery.
Supported formats are GLB, glTF, VRM, PLY (mesh, point cloud or Gaussian splat) and SPZ.
- They are listed with a "**3D**" badge. The thumbnail is drawn by your browser the first time the file appears in the list and is then kept on the server
(files over 128 MB get one when opened).
- When selected, the file is shown in 3D in the built-in viewer of the detail panel on the right. Double-click to enlarge it.
| Action | What it does |
| --- | --- |
| Drag | Rotate |
| Right-drag | Move (pan) |
| Mouse wheel | Zoom in / out |
| ⟲ (reset) button | Return to the starting view |
- For a model saved with ComfyUI's **SaveGLB**, the workflow can be read just like an image's (sections 8-2 and 8-3).
> **Caution**
> - 3D files cannot be sent to Image Edit, GenerateUI, Tagger or Feeder.
> - Some `.gltf` files refer to separate `.bin` or image files. Moving or deleting in the Gallery only affects the `.gltf` itself, so do not change where those files are.
> **Summary of this chapter**
> - In the Gallery's Output, you can look back at generated images folder by folder. You can also search by words in the prompt
> - The detail panel's Prompt tab shows right away the prompt used to generate the image
> - With "Load GenUI" you can reproduce the same workflow in GenerateUI, and with "Copy & Send Canvas" on the node screen
> - To reproduce, set the seed mode to Workflow (or Fixed)
> - If you drop an image onto the Metadata subtab, you can examine the models, LoRAs and prompt in detail
> - With the S button you can search by the meaning of a sentence or by "similar images" (semantic search)
> - Audio files can also be listed, played and converted
> - 3D models (GLB, glTF, VRM, PLY, SPZ) can also be turned around and checked in the Gallery (8-8)
@@ -0,0 +1,171 @@
# Chapter 9 The WS Library - Using It Inside the ComfyUI Screen
In the chapters so far, we mainly used the main part of Workflow Studio (the screen with the row of tabs).
In this chapter, we use the **WS Library (Workflow Studio Library)** that opens beside ComfyUI's node screen.
With the WS Library, you can use the workflows, nodes, models and prompts you organized in Workflow Studio
**just by dragging and dropping onto the node screen**.
It is a feature whose convenience you feel more the longer you spend working on the node screen.
---
## 9-1 Opening the WS Library
1. Open ComfyUI's node screen.
2. Press the **WS Library** button (the icon of lined-up squares) added to the top menu.
A panel opens at the edge of the screen. Press the button again to close it.
### Six tabs
At the top of the panel, single-letter tabs are lined up. Hovering the mouse shows the proper name.
| Tab | Name | What you can do | In this book |
| --- | --- | --- | --- |
| W | Workflows | Load workflows | 9-2 |
| N | Nodes | Place nodes and node sets | 9-2 |
| M | Models | Place nodes that load models | 9-2 |
| P | Prompts | Place or copy saved prompts | 9-2 |
| I | Information | Inspect images and workflows and place the materials used | 9-3 |
| A | AI TOOL | Use AI features such as translation and chat | Chapter 10 |
Inside each tab there are further small tabs (subtabs).
In many tabs, the following subtabs are common.
- **All** - everything
- **Favorites** - things registered as favorites in the main Workflow Studio
- **Groups** - by the groups made in the main Workflow Studio
Also, in each subtab's search box, you can narrow down by typing a name.
> **Tip**
> What is shown in the WS Library is linked to the organization done in the main Workflow Studio (favorites, groups, badges).
> The better you organize in the main part, the easier the WS Library is to use.
---
## 9-2 Placing onto the canvas with drag and drop
The basic operation of the WS Library is very simple.
| Operation | What happens |
| --- | --- |
| Drag and drop onto the canvas | Places or loads it at the dropped position |
| Double-click | Places or loads it near the center of the screen |
### W - Load a workflow
When you drag a workflow onto the canvas (or double-click it), that workflow is loaded.
It behaves the same as when you drop a workflow file from the file explorer.
Subtabs: All / Favorites / Groups / By Badge (by badge)
> **Tip**
> If you opened the main Workflow Studio from a bookmark and "Send to Canvas" is unavailable,
> the target workflow is shown in this W tab, so drag it from here (Chapter 4, section 4-6).
### M - Place a model
When you drag a model onto the canvas, **a loader node with that model already chosen** is placed.
| Kind of model | Node placed |
| --- | --- |
| Checkpoint | Load Checkpoint (CheckpointLoaderSimple) |
| LoRA | Load LoRA (LoraLoader) |
| VAE | Load VAE (VAELoader) |
| ControlNet | Load ControlNet Model (ControlNetLoader) |
| UNET | Load Diffusion Model (UNETLoader) |
| TextEncoder | Load CLIP (CLIPLoader) |
| Hypernetwork | HypernetworkLoader |
The trouble of finding a node in a list, placing it, choosing the file name from a pull-down and so on goes away.
After placing it, you connect it with lines to other nodes and use it.
Subtabs: All / Favorites / Groups / By Type (by kind)
> **Tip**
> There is no loader node for Embeddings, so they cannot be placed on the canvas.
> **Tip: placing LoRAs in bulk**
> In a LoRA group, an item called **All N LoRAs** (N is the number of LoRAs in the group) is shown.
> If you drag it, a `Lora Loader (LoraManager)` node containing all the LoRAs in the group is placed.
> (The custom node **ComfyUI-Lora-Manager** is required)
### P - Place a prompt
When you drag a saved preset (Chapter 7) onto the canvas,
a **Prompt Text (WFS)** node containing the positive and negative prompts is placed.
This is a node bundled with Workflow Studio, and it outputs the prompt text.
If you only want to paste the prompt, you can copy it with the **P** (positive) / **N** (negative) buttons on the item.
Subtabs: All / Favorites / Categories (by category) / Groups
### N - Place a node
When you drag a node onto the canvas, that node is placed.
If you register frequently used nodes as favorites in the **Nodes** tab of the main Workflow Studio, you can take them out quickly.
In the N tab's list, **Alt + click** a node to switch its favorite on or off on the spot (v0.8.0 and later).
The N tab opens faster than before, because it shows the list first without waiting for the nodes' detailed information.
Subtabs: All / Favorites / Groups / Sets / Category (by category) / Package (by custom-node package)
### Node sets - saving a frequently used combination as a whole
A **node set** is a bundle of several nodes and their connections saved together.
If you save "a group of nodes you always use in the same combination", you can place the whole thing with one drag.
**Making a node set**
1. On ComfyUI's node screen, select the nodes you want to save (click while holding the Ctrl key, or drag to select a range).
2. **Right-click** on the selected nodes and choose **Save as Node Set**.
3. Save it with a name.
**Using a node set**
From the **Sets** subtab of the WS Library's **N** tab, drag it onto the canvas.
> **Example**
> It is handy to make sets of parts you want to add afterwards to any workflow,
> such as "a group of nodes that automatically corrects faces" or "a group of nodes for upscaling".
---
## 9-3 Inspecting the contents of an image in the Information tab
The **I (Information)** tab is a feature that inspects image and workflow files and lets you
**place the models and prompts that were used straight onto the canvas**.
Think of it as the Gallery's metadata (Chapter 8, section 8-2) made usable inside the node screen.
### How to use it
1. Open the **I** tab of the WS Library.
2. Drop the file you want to inspect (a PNG, WebP, MP4 or WebM image or video made in ComfyUI, or a workflow JSON) onto the panel.
3. The materials found in the file are shown, split into subtabs.
| Subtab | Contents | When dragged onto the canvas |
| --- | --- | --- |
| model | Checkpoint, VAE, Diffusion Model, Text Encoder | The corresponding loader node is placed |
| lora | The LoRAs used and their strength | A LoRA loader node is placed |
| Prompts | Positive (POS) and negative (NEG) prompts | A text-encode node containing the prompt is placed |
Clicking a prompt item shows the whole text, and **Copy** also copies it.
### When it is handy
- "I want to build a new workflow with the same model and LoRAs as that image I made before"
- "I just want to borrow the prompt from a workflow I found online"
- "Which LoRA did this image use, and at what strength?"
The point is that rather than loading the whole workflow, **you can take out and use only the parts you need**.
---
## 9-4 Changing the look of the WS Library
From the **gear button** inside the WS Library panel, you can set the panel's colors.
- The panel's background color
- The sub-header's background color
- Text color, and the color of auxiliary text
- Border color
If you adjust them to match ComfyUI's theme, you can use it without any sense of mismatch.
The settings are saved in the browser and applied the next time you open it.
> **Summary of this chapter**
> - The WS Library is a "shelf of materials" that opens beside ComfyUI's node screen
> - From the W, N, M and P tabs, you can place workflows, nodes, models and prompts by dragging (or double-clicking)
> - If you drag a model from the M tab, a loader node with the model already chosen is placed
> - With "Save as Node Set" in the right-click menu, you can save a combination of frequently used nodes
> - If you drop an image onto the I tab, you can take out and use the models, LoRAs and prompts that were used
@@ -0,0 +1,437 @@
# Chapter 10 AI TOOL - Making Local AI Your Assistant
Workflow Studio can work together with **local AI (large language models)** running inside your computer.
If you prepare local AI, you can do things like the following.
- Translate what you thought up in your own language into an English prompt
- Chat with the AI and have it come up with prompt ideas
- Show it an image and have it make a description, prompt or tags for that image
- Have it make candidate lists for wildcards
- Pick a **skill** to switch what it is good at: writing prompts, storyboards, troubleshooting and more (v0.9.5+)
- Have it check whether a generated image matches your request and fix only what is missing (v0.9.5+)
- Read the text shown in an image (OCR, v0.9.5+)
Because it runs inside your own computer rather than as a service on the internet,
a big feature is that **what you type and the images you use are not sent outside**.
> **Tip**
> AI features are "nice to have" features. Even if you do not use them, all of Workflow Studio's basic features are available.
> If your computer has room to spare (especially GPU memory), please give them a try.
---
## 10-1 Preparing local AI software
### Supported software
Workflow Studio supports the following four pieces of local AI software.
| Software | Characteristics | Default address |
| --- | --- | --- |
| **Ollama** | The standard. Easy from downloading models to running them | `http://localhost:11434` |
| **LM Studio** | Operated from a screen. You can also look for models inside the app | `http://localhost:1234` |
| Lemonade Server | Easy to run on AMD GPUs and NPUs | `http://localhost:13305` |
| Unsloth | Requires extra setup such as an API key (for advanced users) | `http://localhost:8888` |
For beginners, **Ollama** is recommended. This chapter explains using Ollama as the example.
> **Tip**
> `localhost` in the address means "this computer", and the number after it is the **port number** (Chapter 2, section 2-4).
> Just like ComfyUI, AI software also listens on its own fixed port number.
### Preparing Ollama
1. From Ollama's official site, download the installer for your OS and install it.
2. Start Ollama (on Windows it starts automatically after installation and stays resident in the taskbar's notification area).
3. Download the AI model you will use. In Command Prompt or similar, type the following.
```
ollama pull (model name)
```
Model names can be checked in the model list on Ollama's official site.
> **Guide to choosing a model**
> - **You also want the image-reading feature (VLM)** -> choose a model that supports image input (vision)
> (in the author's testing, image generation and image reading work well with Gemma 4 `e4b` and similar)
> - **GPU memory is small** -> choose a model with a small parameter count (a few B, that is a few billion)
> - If translation accuracy matters, choose a model that is good at your language
>
> The world of AI models changes quickly, so please try newer models too.
> **Caution: competing for GPU memory**
> Local AI also uses **GPU memory (VRAM)**, just like image generation.
> If you run both at the same time, memory runs short and image generation slows down or errors out.
> Countermeasures are explained in 10-3.
---
## 10-2 Using the AI TOOL tab
Open the **AI TOOL** tab of the main Workflow Studio.
The screen is divided into four parts, all shown at the same time.
| Part | What you can do |
| --- | --- |
| Translate | Translation between languages such as Japanese, English and Chinese |
| Chat | Conversation with the AI. Skills switch what it is good at. If the conditions are met, generating images from the conversation too |
| TOOLS | Reading images (VLM), reading text (OCR), making wildcards |
| Settings | Settings for which AI software and model to use |
> **Tip: Translate and Settings can be collapsed (v0.9.5+)**
> Press the arrow (◀ / ▶) at the right end of the Translate or Settings header to collapse that part into a thin strip, giving Chat and TOOLS more room.
> Click anywhere on the strip to open it again. The collapsed state is kept the next time you open the tab.
### First: connect to the AI software in the settings
1. In **Backend** under the settings, choose the software to use (for example Ollama).
2. Check the address under **Connection**. If you use Ollama with its defaults, no change is needed.
3. Press **Test Connection**. If it shows success, it is OK.
4. Press **Refresh** next to **Model** and choose the model to use from the list.
5. Press **Save Settings**.
> **Tip**
> The settings here are shared with the **A (AI TOOL)** tab of the WS Library on the ComfyUI side.
> On the other hand, the Prompt tab (Chapter 7) and the Tagger tab each have their own **gear button** settings.
> If "it works in the AI TOOL tab but the Prompt tab says Disconnected", set the same thing with the gear button in the Prompt tab as well.
> **If it does not connect well**
> - Check that software such as Ollama is running.
> - For LM Studio, you need to **start the server** inside the app.
> - Check that the port number in the address matches the software's settings.
> - If the model list is empty, check that the model has been downloaded.
### Translate
1. Choose the source and target languages (swap them with the **swap** button).
2. Write the text in the input box and press **Translate**.
3. **Copy** the result and paste it into GenerateUI or a preset.
> **Tip**
> Text read in TOOLS (such as OCR in 10-8) can be put straight into this input box with **To Translation** in TOOLS.
> **Tip**
> Small models sometimes return the original text as it is without translating.
> In that case a message says the model returned the original text, so try another model.
### Chat
Talk in the input box and send with **Enter** (**Shift + Enter** for a line break).
It remembers the flow of the conversation, so follow-up requests such as "make it a brighter mood" also work.
**Clear** resets the conversation.
> **Tip: generating images from chat (advanced)**
> If you use a model that supports "Tool Calling",
> just asking in chat "draw a cat" generates an image with the workflow loaded in GenerateUI, and it is shown inside the chat.
> This feature can be turned on or off with the **Allow image generation** check next to the input box.
> **Tip**
> Choose a **skill** in the dropdown above the chat to switch what the AI is good at (10-5).
> The AI's replies are shown with headings, lists and code boxes formatted (v0.9.5+).
### TOOLS - reading images (VLM)
With a model that supports image input (a VLM), you can have the AI read the contents of an image.
1. Drop an image (PNG / JPG / WebP) into the **TOOLS** frame.
2. Choose a task.
| Task | Contents |
| --- | --- |
| Describe | Explains in text what is in the image |
| Create prompt | Makes a prompt for reproducing the image |
| Create tags | Writes out the elements of the image in tag format |
| Create wildcards | Makes candidate wildcard lists by specifying a category name and count (no image needed) |
| OCR (extract text) | Reads the text shown in the image (10-8) |
3. Press **Run**.
4. Use the result with the buttons under it (v0.9.5+).
| Button | What it does |
| --- | --- |
| To Translation | Puts it into the Translate input box (opens Translate if it is collapsed) |
| To Chat | Adds it to the chat input box |
| Copy | Copies it to the clipboard |
> **Example**
> When you want to imitate the mood of an illustration you like, run "Create prompt" on that image
> and try the resulting prompt in GenerateUI - you can use it like that.
### It can be used inside the ComfyUI screen too
The same AI features can also be used in the **A** tab of the WS Library on the ComfyUI side.
Handy when you want to translate something briefly while working on the node screen.
---
## 10-3 Freeing VRAM and managing memory
As touched on in 10-1, local AI and image generation compete for GPU memory (VRAM).
Ollama keeps a model that has been used in memory for a while,
so if you generate images after using the AI, memory may run short.
### Freeing manually
Pressing **Unload Model** in the AI TOOL settings takes the AI model out of memory and frees VRAM.
(With Ollama, all loaded models are unloaded)
### Freeing automatically before generation
In **VRAM Management (Ollama)** in the Settings tab, you can set Ollama's models to be unloaded automatically just before image generation.
| Behavior before generation | What it does |
| --- | --- |
| Do nothing | Does not unload automatically |
| Unload what is needed only when free memory is below the target | Unloads only when free VRAM is less than the **Target free VRAM (GB)** |
| Unload everything every time | Always unloads everything at every generation |
> **Tip**
> If you can't decide which to choose, "Unload what is needed only when free memory is below the target" is recommended.
> For the target free VRAM, enter a rough value of the amount of memory image generation needs (it differs by the model you use).
> The **Unload all now** button can also free it immediately, on the spot.
> **Caution**
> This automatic unloading applies to Ollama only. If you use LM Studio or similar, free memory manually.
---
## 10-4 Security notes
### By default it connects only to AI on "your own computer"
For safety, the AI software Workflow Studio connects to (Ollama, LM Studio and so on) is by default limited to
**software running inside the same computer (localhost / 127.0.0.1)**.
### When you want to use AI on another computer
If you want to run Ollama on another computer on your home LAN (such as a computer with a strong GPU) and use it,
register the address of the computer you want to connect to in the environment variable **`WFS_ALLOWED_BACKEND_HOSTS`**, and restart ComfyUI.
```
WFS_ALLOWED_BACKEND_HOSTS=192.168.1.20:11434
```
If there are several, list them separated by commas (,).
How to set environment variables differs depending on how you start ComfyUI.
### If you start ComfyUI with `--listen`
If you start ComfyUI with the `--listen` option, other computers and smartphones on the same network
can also access ComfyUI (and Workflow Studio).
In that case, anyone who can access it can use your AI software and images.
> **Caution**
> Use `--listen` **only on a trusted network**, such as a home LAN.
> Do not use it in cafes or on public Wi-Fi.
---
## 10-5 Skills - switching what the chat is good at (v0.9.5+)
A **skill** is a set of instructions that tells the chat what role to play and how to answer.
When you pick one in the dropdown above the chat (it starts as "-- No skill --"), its instructions are sent along with every message
(the instructions themselves are not shown in the chat).
### Built-in skills
Workflow Studio comes with the following six skills.
| Skill | What it does | How you use the result |
| --- | --- | --- |
| T2I Prompt Enhancer | Turns a short idea into a prompt written the way the model in your GenerateUI workflow understands best | Buttons under the prompt |
| Image to Prompt | Writes a prompt that recreates the image you put in TOOLS (needs a model that can read images) | Buttons under the prompt |
| Storyboard to Lab | Splits a story into one prompt per scene, ready for continuous generation in Lab | **Open in Lab** |
| Workflow Doctor | Explains the workflow and helps you find the cause of errors (10-7) | Answers in text |
| SVG Icon Generator | Draws simple icons and illustrations as SVG (vector images) | Preview and **Save to Gallery** |
| Skill Creator | Asks you questions and builds a new skill (10-9) | **Save as new skill** |
### Making a prompt and generating with it (T2I Prompt Enhancer)
1. Load the workflow you want to use into GenerateUI. The skill looks at that workflow's model to decide how to write the prompt.
2. In the AI TOOL chat, choose **T2I Prompt Enhancer** in the dropdown.
3. Describe the picture in your own words, for example "a cat holding an umbrella in rainy Tokyo at night, neon", and send it.
4. The reply shows which writing style was chosen (the `Style:` line) and the English prompt in a box. Use the buttons under it.
| Button | What it does |
| --- | --- |
| Apply to GenerateUI | Writes the positive / negative prompt into GenerateUI |
| Generate with this prompt | Generates right away and shows the image in the chat |
| Copy | Copies the positive prompt |
The writing style depends on the model:
| Model in GenerateUI | How the prompt is written |
| --- | --- |
| Qwen-Image, FLUX, Z-Image, etc. | English sentences (the negative is left empty when CFG is about 1) |
| Illustrious, NoobAI, Pony, etc. (SDXL anime models) | Danbooru tags (starting with quality tags) |
| Realistic SDXL, SD1.5 | Short phrases separated by commas |
> **Tip**
> Follow-up requests such as "make it more night-like" or "two cats" give you the whole updated prompt again.
> **Tip: the prompt currently in GenerateUI is not sent**
> Normally the prompt already typed into GenerateUI is not passed to the AI, because small models tend to mix it (for example an old negative prompt) into the new prompt.
> It is passed only when your message refers to it, such as "**improve the current prompt**".
### Generating a storyboard in a row (Storyboard to Lab)
1. Load a text-to-image (T2I) workflow into GenerateUI.
2. In the chat, choose **Storyboard to Lab** and ask, for example, "A tabby kitten wakes up in the morning, chases a butterfly in the garden, and naps on the veranda in the evening. 3 scenes."
3. The reply shows the scene list and an **Open in Lab (3 scenes)** button. Press it.
4. GenerateUI's **Lab** (Chapter 5, section 5-7) opens with the plan loaded as an unsaved plan.
T2I mode is on, Batch is the number of scenes, and the Prompt column has one keyframe per scene. The KSampler column holds a seed shared by all scenes (`Seed ...`).
5. Check it and press **Run**. To keep it, use **Plan Save**.
> **Tip**
> The characters' looks (hair, clothing colors and so on) are written with the same words in every scene, and the seed is fixed,
> so the same character is easier to keep across scenes.
### Automatic check of replies
For skills that output a prompt or an SVG, Workflow Studio checks the shape of the reply automatically.
If, for example, the prompt box is broken or the SVG is not closed, a dashed notice "**Auto-check: ...**" appears and the AI is asked once to fix it.
Replies that do not try to output a prompt or SVG (for example when you only asked for an explanation) are left alone.
### Managing skills (pencil button)
The pencil button to the right of the dropdown opens the skill list.
- **+ New** writes a new skill, the pencil on each row edits or deletes it, and **Import** adds other skills (10-9)
- Skills that ship with Workflow Studio are marked **built-in**.
Built-in skills you have not edited are updated automatically when Workflow Studio is updated.
An edited one is not replaced automatically; when a newer version exists, a **↻ (update)** button appears.
- Skill files are saved in `ComfyUI/user/default/Workflow-Studio/ai_skills/`.
> **Caution**
> Replacing a skill with the newer version via ↻ (update) discards your edits to that skill.
---
## 10-6 Checking generated images and fixing them (v0.9.5+)
This feature has the AI check whether an image generated from the chat matches what you asked for.
If something is missing, you can redraw with a prompt that fixes just that.
### Preparation: a judging model
The check does not use a normal chat model but an Ollama **decision model** (a model that answers yes/no questions with a probability).
Get one that can read images with `ollama pull` (the author confirmed `d3:4b`, `clef-flash` and `clef`).
> **Tip: why a separate model**
> In the author's tests, a chat model shown an image sometimes answered "yes" to "is the person holding a sword?" when there was no sword.
> A decision model returns the probability of "yes" as a number, so uncertain answers can be told apart.
1. In AI TOOL's **Settings**, under **Chat Image Check**, choose the **Judge model (Ollama decision model)** (press **Refresh** if the list is empty).
2. Choose **Automatic fix rounds**. **0 (fix by button)** is recommended at first.
3. Press **Save Settings**.
### How to use it
1. Tick **Check images** below the chat input.
2. Generate an image from the chat (the AI's image generation, or **Generate with this prompt**).
3. A result card appears under the image. The AI splits your request into 3 to 8 check items and checks them one by one.
| Mark | Meaning |
| --- | --- |
| ✔ (met) | Probability of "yes" is 85% or more |
| ? (unclear) | 30-85% |
| ✘ (not met) | Less than 30% |
4. If there is a ✘ or ?, press **Fix missing items and regenerate**. Only the unmet items are sent to the AI, which rewrites the prompt and draws again.
If you set Automatic fix rounds to 1-3, this repeats automatically up to that number of times.
> **Tip: SVGs can be checked too**
> SVGs drawn with SVG Icon Generator are turned into an image and checked for "does it look like what you asked for" and "are its parts (beak, wings and so on) drawn".
> **Ask to redraw unclear parts** asks the AI to fix them.
> **Caution**
> - The check is not perfect. It is especially weak at "who is wearing what",
> so when there are several people it automatically adds reverse check items such as "the hat is not on the girl" to compensate.
> - How well the redraw works depends on the chat model. If it does not improve, change your request or the model.
> - The judging model also uses GPU memory (about 6 GB for `d3:4b`). Once loaded, a check takes a few seconds.
---
## 10-7 Asking about workflow problems (Workflow Doctor, v0.9.5+)
**Workflow Doctor** is a skill that explains the workflow loaded in GenerateUI and helps you find the cause of errors.
Instead of relying only on the AI's knowledge, every time you send a message Workflow Studio passes it the following facts it has actually checked.
| What is checked | Example |
| --- | --- |
| Nodes that are not installed | Names of nodes ComfyUI does not know |
| Files and values not in ComfyUI's lists | Models or LoRAs that cannot be found (with similar names) |
| Connections | Inputs that are not connected, links to nodes that do not exist |
| The last execution error | The error recorded in ComfyUI's history |
| ComfyUI's log | The latest error part of the log |
### How to use it
1. Load the workflow you want to ask about into GenerateUI (for an error, run it once so the error is recorded).
2. In the chat, choose **Workflow Doctor**.
3. Ask, for example, "check this workflow for problems before I run it" or "what caused the last error?".
> **Example**
> For a workflow whose LoRA file had been moved to another folder, it answered
> "the file specified in the `temp\` folder cannot be found; a file with the same name exists in `Illustrious\`", including how to fix it.
> **Tip**
> The checked facts are rebuilt every time you send a message. After fixing something, just ask again.
---
## 10-8 Reading text in images (OCR, v0.9.5+)
In TOOLS you can read the text shown in an image. The text you read can be sent straight to translation or the chat.
### Preparation: an OCR model
OCR uses a model that is good at reading text, separate from the chat model. Prepare one in Ollama.
These are the author's results on an image mixing horizontal and vertical Japanese and English text.
| Model | Time | Result |
| --- | --- | --- |
| LightOnOCR-3 (`maternion/LightOnOCR-3:4b`) | About 1 s (6-8 s with positions) | Read everything |
| PaddleOCR-VL (manga version) | About 4 s | Read everything |
| DeepSeek-OCR family | About 14 s | Read everything (always with positions) |
| PaddleOCR-VL 1.6, GLM-OCR | About 11-15 s | Missed the vertical text |
### How to use it
1. Drop an image into TOOLS.
2. Choose **OCR (extract text)** as the task.
3. Choose the **OCR model**. Ollama models whose name contains "ocr" are listed, best first (press ↻ if the list is empty).
4. Press **Run**. The text is shown in the result box, one line each.
> **Tip: Show text positions**
> Tick **Show text positions** to draw numbered boxes over the image so you can see where each text is.
> Clicking a box selects the same line in the result box.
> Only LightOnOCR and the DeepSeek-OCR family can return positions; for other models this check cannot be selected.
> **Tip**
> If the result is empty or the same line repeats, it is automatically read once more in grayscale.
---
## 10-9 Making and importing skills (advanced, v0.9.5+)
### Making one with Skill Creator
In the chat, choose **Skill Creator** and say something like "I want a skill that is good at ...".
The AI asks questions one at a time; answer them and you get a draft skill.
Press **Save as new skill** under the reply; the skill list opens with the draft filled in, so check it and save.
You can also ask it to improve an existing skill, for example "this part of this skill does not work well".
### What a skill file contains
A skill is a text file (`.md`). Write the name and description in the part enclosed by `---` at the top, and the instructions for the AI below it.
Under `metadata:` in the top part you can also write these settings (all optional).
| Setting | Meaning |
| --- | --- |
| `output` | `prompt` or `svg`. Checks the shape of the reply automatically and adds the buttons |
| `tools` | `[]` stops the AI from starting image generation on its own |
| `temperature` | How varied the answers are (0.3 stable, 0.9 free) |
| `think` | Turns Thinking mode on or off just for this skill |
| `context` | Information passed to the AI: `workflow` (workflow summary), `current-prompt` (the current prompt), `workflow-detail` / `logs` (the same checks as Workflow Doctor) |
### Importing other skills
**Import** in the skill list adds:
- a single `.md` skill
- a ZIP of a folder containing `SKILL.md` (the **Agent Skills** format shared by many AI tools)
Text files in the folder (reference material and so on) are imported too, and the AI reads them only when it needs them
(this needs a model that supports tool calling).
> **Caution**
> - Only text files are imported. Programs such as scripts are not imported and never run.
> - A skill's instructions are passed to the AI as they are. Read a skill made by someone else before using it.
> **Summary of this chapter**
> - If you prepare local AI (Ollama and so on), you can translate, chat and read images. What you type is not sent outside
> - In the AI TOOL tab's settings, choose the backend, address and model, and test the connection
> - The Prompt tab and Tagger tab have separate gear-button settings
> - AI and image generation compete for VRAM. Free it with "Unload Model" or "VRAM Management (Ollama)"
> - By default it connects only to AI on your own computer. Use `--listen` only on a trusted network
> - **Skills** switch what the chat is good at. T2I Prompt Enhancer writes a prompt that buttons apply to GenerateUI or generate with
> - With **Check images** on, a decision model checks the generated image and you can redraw fixing only what is missing
> - Workflow Doctor helps with workflow problems based on facts it has actually checked
> - **OCR (extract text)** in TOOLS reads the text in an image and sends it to translation or the chat
@@ -0,0 +1,347 @@
# Chapter 11 Even More Features (Overview)
By now, you have covered the basic usage of Workflow Studio from start to finish.
This chapter introduces the tabs and features not yet covered, focusing on **what you can do with them**.
You do not need to use everything right now. Keep in the back of your mind that "you can do this too",
and try them when you need them.
---
## 11-1 The Nodes tab - listing the installed nodes
In the **Nodes** tab, you can list all the nodes installed in ComfyUI.
- **Search and filtering** - narrow down by name, category and package (which custom node it belongs to). Even with thousands of nodes, the list appears first and search runs lightly (v0.8.0 and later).
- **Detail panel** - when you select a node, you can check the kinds of its inputs and outputs (the I/O specification).
- **Favorites, groups and tags** - organize the nodes you use often.
Nodes you register as favorites or in groups here are shown in the **N** tab of the WS Library,
and can be placed by dragging onto the node screen (Chapter 9).
> **When it helps**
> "I installed a custom node but can't tell what nodes were added"
> -> narrow down by **Package** and you can see the list of nodes contained in that custom node.
---
## 11-2 Tagger - automatically making tags from images
The **Tagger** tab is a feature that reads an image and automatically makes tags that describe it (such as `1girl, long hair, smile`).
It can be used for making prompts for anime-style models, and for tagging images for training.
### Three subtabs
| Subtab | What you can do |
| --- | --- |
| Single | Make tags from one image |
| Batch | Tag the images in a folder in bulk (it can also write out a `.txt` file with the same name as each image) |
| DB | Search and edit the records of images you tagged |
### How to use it (Single)
1. Drop an image onto the preview area (you can also open it from the **Tagger** button in the Gallery's detail panel).
2. Choose the model to use and press the run button to make tags.
3. Use the result.
| Button | What it does |
| --- | --- |
| GenUI:P | Adds it to GenerateUI's positive prompt |
| Send to Prompt | Adds it to the positive prompt in the Prompt tab |
With the **threshold** slider, you can adjust how certain a tag must be to be shown.
Raising the number leaves only fewer, more certain tags; lowering it gives more.
### Preparation needed
To use Tagger, you need the following preparation.
1. Put a **model for tagging** (such as WD Tagger) in ComfyUI's `models/tagger/(model name)/` folder.
Put a `.onnx` file and `selected_tags.csv` in the folder.
**PixAI Tagger v1.0** (v0.8.10 and later) also works. From the community conversion on Hugging Face (`Mexes/pixai-tagger-v1.0-onnx-fp32-fp16-int8`),
put one `.onnx` file (`model_fp16.onnx` is recommended for a GPU) together with `selected_tags.csv` and `preprocess.json` in one folder, and it is detected automatically.
When you choose a PixAI model, the threshold sliders switch to PixAI's recommended values (0.17 / 0.27).
2. Install the **extra Python packages**.
Use the `requirements.txt` in the Workflow Studio folder.
```
(ComfyUI's Python) -m pip install -r requirements.txt
```
The location of "ComfyUI's Python" differs by environment. Check the table in Chapter 2, section 2-6
(for example, for the Windows portable version it is `python.exe` inside the `python_embeded` folder).
> **Caution**
> Installing into the Python that was already on your computer cannot be used from ComfyUI.
> Always install into **the Python that ComfyUI uses**.
> If you are not sure, there is also a method of installing packages with a ComfyUI Manager feature.
> **Caution: check that it runs on the GPU**
> On an NVIDIA GPU, Tagger takes less than a second per image, but on the CPU, PixAI takes about 20 seconds per image.
> If it is very slow, check that the GPU package is installed correctly (Chapter 12, section 12-5).
> **Tip**
> You can also combine it with the local AI image reading (VLM) from Chapter 10 to make tags that combine both results.
---
## 11-3 Image Edit - touching up generated images
The **Image Edit** tab is a simple image-editing tool with layers.
You can put text on a generated image, or make a mask for redrawing part of it.
### Main features
| Tool | What you can do |
| --- | --- |
| Select | Move, scale and rotate layers |
| Draw | Draw with a pen (pen-tablet pressure is also supported) |
| Text | Put in text |
| Shape | Draw rectangles, circles and lines |
| Fill | Fill and gradients |
| Mask | Specify the part you want redrawn (making a mask) |
| Blur | Blur and mosaic |
| BG Remove | Remove the background |
| Inpaint | Redraw the masked part with the GenerateUI workflow |
- Open an image by drag and drop, **File Open**, or the **Image Edit** button in the Gallery.
- Save the edited image with **Save PNG** (download), **Save to Gallery** and so on.
- Undo with **Ctrl + Z**, redo with **Ctrl + Y**.
### Draw colors and shortcuts (v0.9.3+)
When you choose **Draw**, a **color palette** appears in the second row of the tool options.
- It shows **Color 1 / Color 2**, 20 basic colors and 20 custom slots. Click a color to paint with it.
- Click **+** or an empty slot to register the current color as a custom color. Right-click a color to register, edit or delete it (basic colors can be changed too, and returned to their original color).
Registered colors are saved per browser.
- The **eyedropper** button next to **Color** on the right picks a color from the canvas (Esc cancels).
- The starting colors are black (Color 1) and white (Color 2).
| Key (while Draw is active) | What it does |
| --- | --- |
| D / E | Draw / Erase |
| X | Swap Color 1 and Color 2 |
| [ / ] | Make the brush smaller / larger |
| B | Open or close the brush picker (Esc also closes it) |
### Inpainting (redrawing part of an image)
The handiest use of Image Edit is **inpainting**.
It is for when "I want to redraw only the face" or "I want to fix only the place where the hand broke".
1. Load an inpainting workflow (such as the bundled sample `ws_inpaint_basic`) into GenerateUI.
2. Open the image in Image Edit and paint the part you want redrawn with **Mask**.
3. Run it with the **Inpaint** tool, and only the painted part is redrawn.
> **Tip**
> To use a higher-quality version of background removal, or more advanced masking,
> the author's separate custom node **comfyui-mask-editor-one** is needed.
> Saving and loading in PSD format requires an extra Python package (psd-tools).
---
## 11-4 Video - making and joining videos
The **Video** tab is a dedicated screen for making short videos from images or text using video-generation models.
### Supported video-generation models
- **MiniMax H3** (image to video)
- **LTX-2.5** (text to video, image to video, and video from specified first and last images)
- **Wan2.2** (text to video)
- **FastH3 OmniRef / Pika-H3-Flex** (MiniMax H3 reference to video, v0.8.14 and later)
You load a matching workflow with **Load in Video** in the Workflow tab and use it (Chapter 4, section 4-6).
### Main features
| Subtab | What you can do |
| --- | --- |
| Plan | Divide a video into "blocks", decide the prompt, length and first/last image for each block, and generate in order |
| Edit | Line up generated videos and images, then cut, join, add text and BGM, and export |
| Asset | List generated videos, images and audio. Audio can be set as BGM or added as sound (the Audio Only type filter shows audio only) |
There are also the **Frame** feature, which saves a favorite scene of a video as a still image, and **GIF**, which converts a video to an animated GIF.
### Register as Asset and audio conversion (v0.8.9 and later)
- **Register as Asset** - a button in the Video Source panel on the left. It copies a video from your PC into the output folder (`output/video`) and registers it in the "Video Assets" group.
Once registered, it appears in the Asset subtab list and can be used in Edit and elsewhere.
Videos already in the output folder (ones chosen in Asset or generation results) are only tagged into the group, not copied. If the name already exists, a number is added after it.
- **Convert audio...** - a button next to the crop buttons in the Edit subtab. It converts only the audio of the selected video clip to FLAC, MP3, Opus, M4A or WAV and saves it into `output/video`.
It is the same dialog as Gallery's Convert. Only video clips that contain audio are covered.
- Pressing the audio-conversion button opens the dialog below. Choose the **output format** (such as FLAC), the **sample rate** and the **channels**, then press **Convert**.
If you check the option to carry over the embedded prompt/workflow tags, the original video's information is kept in formats that can hold tags (such as FLAC).
- The Video Source panel is a little wider, the file name shows on one line, and the buttons sit in a single row.
### Reference to video (FastH3 OmniRef / Pika-H3-Flex) (v0.8.14 and later)
These workflows make a video from **reference images** instead of a first and last frame,
so the image fields in the Plan subtab are renamed:
- **Reference Image** - the first reference image (the usual First Image field)
- **Reference Image 2 (optional)** - the second reference image (the usual Last Image field)
- Prompt, duration (seconds), aspect ratio and so on work the same as for other video workflows.
- A block whose image field is left empty with **None** or **Clear** is generated without that image, even if the workflow has a reference image wired in.
Blocks added with "+ Block" start with empty images, so set an image on each block that needs a reference.
- Choosing **Use previous block's last frame** makes the last frame of the video actually generated by the previous block the first reference image of that block.
- The Asset subtab buttons also change to "Set as Reference Image" and "Set as Reference Image 2".
> **Caution**
> Video generation uses far more GPU memory and time than image generation.
> Check the required specs of the model before trying it.
---
## 11-5 Main items of the Settings tab
In the **Settings** tab, you configure Workflow Studio as a whole.
Items are collapsed, so click a heading to open it.
Here we mainly introduce items not covered in earlier chapters.
| Item | Contents | Related chapter |
| --- | --- | --- |
| Theme | Screen color scheme and font | Chapter 3 |
| UI Language / Summary Language | Display language | Chapter 2 |
| Text Size | Size of text such as in the prompt fields | Chapter 3 |
| Workflow Data Folder | The folder workflows are read from | Chapter 4 |
| Gallery Output Folder | The folder shown in the Gallery | Chapter 8 |
| Default Workflow | The workflow automatically loaded into GenerateUI when you open Workflow Studio | Below |
| Default Checkpoint | Automatically replaces the model when loading a workflow into GenerateUI | Below |
| CivitAI API Key | For fetching Civitai information (optional) | Chapter 6 |
| Eagle integration (optional) | Automatically save generated images to Eagle | Chapter 8 |
| VRAM Management (Ollama) | Unload AI models from memory before generation | Chapter 10 |
| Data Management | Backing up and restoring settings and data | Below |
### Default Workflow
If you set the basic workflow you always use, it is loaded into GenerateUI automatically when you open Workflow Studio.
You can also set it with the **Set as Default** button on the Workflow tab's detail screen.
### Default Checkpoint
Workflows you get online often specify models you do not have (Chapter 5, section 5-3).
If you set a **Default Checkpoint** and turn it on, each time you load a workflow into GenerateUI,
the Checkpoint is automatically replaced with the specified model. It saves the trouble of choosing a model again.
### Data Management (backup)
In **Data Management**, you can write out the information you organized in Workflow Studio (favorites, badges, groups, presets and so on)
to a file (Export), and read it back to restore it (Import).
With **Full Backup (ZIP)**, you can back up everything together, including folder-based data such as styles and Lab plans.
> **Tip**
> Always take a backup before buying a new computer or reinstalling ComfyUI.
> The information you spent time organizing can be carried over as it is to the new environment.
> (For safety, API keys are not included in backups)
## 11-6 A companion app: ComfyUI Comic Creator - making manga from generated images
**ComfyUI Comic Creator** (Comic Creator from here on) is a custom node **for making manga** that runs on ComfyUI,
developed by the same author as Workflow Studio.
It is software you install separately from Workflow Studio, but if you install both, they work together.
### What you can do with Comic Creator
| Feature | Contents |
| --- | --- |
| Panel layout | Choose a template and make panels on a page (your own templates can also be registered) |
| Placing images | Place images into panels by drag and drop |
| Speech balloons and text | Vertical and horizontal dialogue, balloons of various shapes, sound-effect text |
| Manga effects | Add speed lines, screen tones, halftone dots and so on |
| Image editing | Image editing with layers (the Image tab) |
| Automatic creation with AI | Draft a story, script and panel layout from a theme with AI (the Auto tab) |
| Export | Output as JPEG / PNG / WebP / SVG / PDF / **EPUB** |
### Working together with Workflow Studio
If you install both, the following integrations are possible.
- **Use Workflow Studio's Gallery inside Comic Creator**
Workflow Studio's Gallery appears inside Comic Creator, and you can choose generated images and send them to panels with **Send CC Layout** (Layout tab panel / overlay) or to the Image tab with **Send CC Image**.
(The Send CC buttons are shown only in a Gallery opened inside Comic Creator)
- **Generate images from Comic Creator**
From the Comic Creator screen, through Workflow Studio (GenerateUI), you can generate images to put in panels (T2I),
or redraw based on a rough you drew or an image you placed (I2I and inpainting).
From Comic Creator's Nanobanana+ tab (local mode), you can also run image-editing workflows that use several reference images (Flux.3 Klein, Qwen Image 2.1 and so on) (v0.8.12 and later).
With the flow "make images in Workflow Studio -> put them together as manga in Comic Creator",
you can go from image generation to finished manga entirely inside ComfyUI.
### Installing and launching
1. In ComfyUI Manager's **Install via Git URL**, enter the following URL and install.
```
https://github.com/ketle-man/comfyui-comic-creator
```
(For a manual install, `git clone` into `ComfyUI/custom_nodes/`, same as Workflow Studio)
2. Restart ComfyUI.
3. Press the **CC** button added to ComfyUI's top menu, and Comic Creator opens in a new tab.
> **Tip**
> For detailed usage of Comic Creator, see the **Help** tab inside the app (with search).
---
## 11-7 TagSpace - picking tags and prompts from a "map of meaning" (v0.9.0 and later)
The **TagSpace** tab lays out the prompts, tags and generated images inside Workflow Studio in a 3D space
according to **closeness of meaning**. Things close together mean similar things, so you can browse the space
("which tags are close to this mood?") and pick tags to build a prompt.
### What is in the space
- **Cards** - the sample images of the Gallery's **ImagePrompt** (Chapter 8, section 8-4) with their prompts, shown as thumbnails
- **Tags** - words taken (split at commas) from ImagePrompt prompts, prompts embedded in generated images, wildcards, the Tagger DB and the Prompt library, shown as dots colored by source
- **Gallery points** - some of your generated images, shown as faint dots (each carries the tags of its own prompt)
### Preparation
As with the Gallery's semantic search (Chapter 8, section 8-6), **Ollama** and **EmbeddingGemma 2** are needed.
If the Gallery's semantic index was made with Gemini Embedding 2, rebuild it with EmbeddingGemma 2 first.
### Building the space
Choose the settings in the toolbar and press **Build / Update**.
- **Tags** - how many tags to include. The sources take turns, each in order of how often a tag is used
- **All tags** - when on, every candidate tag is used instead of the Tags number (up to 20,000)
- **Min uses** - leaves out tags used fewer times than this. 2 removes one-off spelling variants and the like
- **Wildcards** - whether wildcards are used as a tag source
- **Gallery points** - how many generated images to show (0 = none)
- **Tagger fill** - tags generated images whose prompt cannot be read, using a Tagger model (OFF / a model name; choose "Gallery points only" or "All images without a prompt")
- The first build takes a few minutes to read image prompts and prepare tags. Later builds reuse the previous results and finish in seconds to tens of seconds.
- The status line shows the numbers of cards, tags and Gallery points. When only part of the candidates is used, "(of N candidates)" is shown.
> **Tip**
> Most distinct tags are words that only appear in wildcards.
> Turning "Wildcards" off and "All tags" on lays out every tag you have actually used (about 10,000 in testing).
> **Caution**
> Tagger fill takes about 0.5 second per image on the CPU. Before choosing "All images without a prompt", check that Tagger runs on the GPU (Chapter 12, section 12-5).
### Looking and searching
- **Drag** to rotate, **right-drag** to pan, and use the **wheel** to zoom.
- Hovering over a point shows its name, thumbnail and prompt or sources.
- The toolbar checkboxes switch Cards, Thumbnails, Tags and Gallery on or off, as well as **Glow** (stars shining in a night sky) and **Twinkle**.
The checkboxes for each source show or hide tags by source.
- Type words or a sentence in the search box at the top right and press **Enter** (any language). A white mark is placed at that meaning, and nearby cards and tags are listed.
### Picking tags to build a prompt
1. Click a card or tag to select it. Cards and tags close in meaning are listed under **Nearby** on the right.
(Clicking a Gallery point also lists similar generated images)
2. **Double-click** what you want in the prompt, or press **Add** in **Selection**, or the **+** in the list.
A card adds its whole prompt, a tag adds the tag itself, and a Gallery point adds that image's tags to the **Prompt** area at the far right.
3. **Add to GenerateUI (Positive)** appends it to the positive prompt of the workflow loaded in GenerateUI (load a workflow in GenerateUI first).
You can also **Copy** it to the clipboard.
### Searching with the tags of a card or generated image
When you select a card or Gallery point, its prompt tags are shown as buttons under **Selection**.
Click tags to choose them (click again to deselect) and press **Search with selected** to search the space with them.
This helps when you want to find things close to, say, only the hairstyle and clothing of an image.
While tags are selected, the **Add** button in **Selection** becomes **Add selected (N)** and adds only those tags to the prompt area.
With no tags selected it adds the whole item as before (a card's whole prompt, or all tags of a Gallery point).
When a Gallery point is selected, **Selection** also shows an **Open in Gallery** button.
It switches to the Gallery tab, selects the image's folder, and selects the image in the list (its details open on the right).
So that the image is sure to appear, the Gallery's search, favorite, tag and group filters are cleared.
A card's whole prompt can be read under **Full prompt**.
---
> **Summary of this chapter**
> - In the Nodes tab you can list and organize installed nodes, and use them in the WS Library
> - Tagger automatically generates tags from images. A model and extra packages must be prepared
> - Image Edit is a simple editing tool with layers. Inpainting also lets you redraw parts
> - In the Video tab, you can make videos with supported video-generation models and do simple editing
> - "Default Checkpoint" and "Data Management (backup)" in the Settings tab are especially handy
> - Combined with Comic Creator by the same author, you can make manga from generated images
> - TagSpace lays out your tags, prompts and generated images in 3D by closeness of meaning, so you can browse it and pick tags for a prompt
@@ -0,0 +1,230 @@
# Chapter 12 When You Are in Trouble (Troubleshooting / FAQ)
This chapter collects the troubles that often occur when using Workflow Studio, and how to deal with them.
When in trouble, first look in the **"symptom"** headings for the one closest to your situation.
> **Three things to try first**
> For almost any trouble, these three often solve it.
> 1. Reload the browser page (F5 key)
> 2. Press the **Refresh** button of each Workflow Studio tab
> 3. Quit ComfyUI completely and start it again
---
## 12-1 Startup and connection troubles
### The W button does not appear
| Cause | What to do |
| --- | --- |
| You did not restart ComfyUI after installing | Quit ComfyUI completely and start it again (reloading the browser alone does not take effect) |
| The folder is in the wrong place | Check that the folder exists at `ComfyUI/custom_nodes/ComfyUI-Workflow-Studio`. Also check that the folder is not doubled |
| ComfyUI is old | Update ComfyUI itself |
### The Workflow Studio page does not open
- Is ComfyUI running? Workflow Studio runs inside ComfyUI, so you cannot open it if you quit ComfyUI.
- Is the **port number** in the address right? You can check it in the address bar of the browser where ComfyUI is open (Chapter 2, section 2-4).
### GenerateUI shows "Disconnected"
1. Check that ComfyUI is running and that you can generate an image on ComfyUI's screen.
2. Press **Connect** at the top of GenerateUI.
3. If it still does not connect, reload the browser page.
### You updated but the behavior did not change
After updating Workflow Studio, always restart ComfyUI.
If it still behaves the old way, the browser cache may remain.
Try **Ctrl + F5** (forced reload).
---
## 12-2 Workflow troubles
### Workflows do not appear in the list
| Cause | What to do |
| --- | --- |
| The list is still old | Press **Refresh** in the Workflow tab |
| A filter is applied | Press **x Clear** at the right end of the toolbar |
| You saved to another folder | Check "Workflow Data Folder" in the Settings tab |
### Nodes are shown in red (missing)
When you load a workflow, nodes may be shown in red, or a message may say there are missing nodes.
This means **the custom nodes the workflow needs are not installed**.
**What to do**
1. Open ComfyUI's **Manager**.
2. Choose **Install Missing Custom Nodes**.
3. Install the custom nodes shown, and restart ComfyUI.
> **Tip**
> The "Convert: cross" in the Workflow tab's **GenerateUI Check** (Chapter 4, section 4-7) is also mostly caused by this.
> Hovering the mouse over the badge shows the names of the missing nodes.
### GenerateUI judged it "unusable"
| Judgment | Meaning and what to do |
| --- | --- |
| Convert: cross | A required custom node is missing -> see "Nodes are shown in red" above |
| Convert: warning | A model you do not have is specified -> choose again in GenerateUI (12-3) |
| Operability: warning / cross | Many settings are not shown in GenerateUI -> generation is often still possible. Change detailed settings on ComfyUI's node screen |
---
## 12-3 Generation troubles
### You changed a setting but the result did not change
The most common cause is **forgetting to press Apply**.
In GenerateUI, after changing the model or Settings values (steps, size and so on), the change is not reflected in the workflow unless you press **Apply** (Chapter 5, section 5-3).
The Switches column at the right of the Settings tab is the exception: it takes effect the moment you click (Chapter 5, section 5-4).
Looking at the **Raw JSON** on the right lets you check the values actually reflected.
### It generates something different from the loaded workflow
If a batch kind's checkbox in the **Batch** subtab stays on,
a batch generation runs when you press Generate (Chapter 5, section 5-5).
Open the Batch subtab and turn off all the Batch Queue column checkboxes.
### It generates with a different model than you expected
If the model saved in the workflow is not on your computer, it may be replaced automatically with another model.
In that case a message says the Checkpoint "..." was not found and was replaced with another model without warning.
(From v0.8.7, if you have exactly one model with the same name that differs only in its folder, it is replaced with that one.)
In the **Model** subtab of GenerateUI, choose the model you want to use again and press **Apply**.
If you always want to use the same model, the **Default Checkpoint** in the Settings tab is handy (Chapter 11, section 11-5).
### It does not become the same image even with the same settings
Is the seed mode in the generate panel set to **Random**?
To make the same image, you need to set the seed to **Workflow** (use the workflow's seed as it is) or **Fixed** (fix it to the value in the Seed field).
If no new image is added when you generate in a row with the same seed and settings, that is because ComfyUI reuses the already computed result, not a malfunction.
For the steps to reproduce a Gallery image, see Chapter 8, section 8-3.
### Slow generation, or it stops with an error (out of memory)
| Cause | What to do |
| --- | --- |
| Local AI is using GPU memory | Press **Unload Model** in AI TOOL. Use **VRAM Management (Ollama)** in the Settings tab (Chapter 10, section 10-3) |
| The image size or Batch Size is too large | Make Width, Height and Batch Size smaller in the Settings subtab |
| The model is too large for your GPU | Use a smaller model (such as a GGUF version) |
### Errors appear after moving or deleting a model
ComfyUI may remember the old location of a model.
Run **Hard Reset** in ComfyUI's menu (inside the queue menu), or restart ComfyUI.
### A newly added model does not appear in the list
Press **Refresh Models** in GenerateUI, or **Refresh** in the Models tab.
If it still does not appear, check that the folder where you put the model is correct (Chapter 6, section 6-1).
---
## 12-4 AI feature troubles
### AI features show "Disconnected" or do not connect
1. Check that software such as Ollama is running.
For LM Studio or Lemonade, you need to **start the server** inside the app and have a model loaded.
2. Check that the address (port number) in the settings matches the software.
3. Press **Test Connection** and check the result.
### It works in the AI TOOL tab but not in the Prompt tab
AI settings are saved separately depending on the place (Chapter 10, section 10-2).
| Place | Where to set it |
| --- | --- |
| AI TOOL tab / the A tab of the WS Library | The AI TOOL tab's settings (shared) |
| Prompt tab | The **gear button** of the AI Assistant in the Prompt tab |
| Tagger tab | The **gear button** in the Tagger tab |
### The model list is empty
The AI model may not have been downloaded.
For Ollama, download the model with `ollama pull (model name)`, then press **Refresh** in the model selection.
### It does not connect to Ollama on another computer
For safety, by default it connects only to AI software inside the same computer.
You need to set the environment variable `WFS_ALLOWED_BACKEND_HOSTS` (Chapter 10, section 10-4).
### The judge model for image checks, or the OCR model, does not appear in the list
- The judge model list shows only Ollama **decision models** that can read images; normal chat models are not shown.
Update Ollama, `ollama pull` a decision model, then press **Refresh** (Chapter 10, section 10-6).
- The OCR model list shows only Ollama models whose name contains "ocr". Press ↻ after getting one (Chapter 10, section 10-8).
- Both look at Ollama at the AI TOOL backend address when the backend is Ollama, otherwise at `http://localhost:11434`.
### Image checks never pass no matter how often it fixes
- How well the redraw works depends on the chat model. Make your request shorter and more concrete, or try another model.
- The check itself can be wrong too. Look at the image yourself as well.
- Too many automatic fix rounds keep using generation time and GPU memory. "0 (fix by button)" is recommended at first.
### The AI does not read a skill's reference files, or does not generate images from the chat
Reading the reference files of an imported skill and generating images from the chat only work with
**a model that supports tool calling**. Switch to such a model.
---
## 12-5 Troubles with other features
### No model appears in Tagger
- Check that a `.onnx` file (and `selected_tags.csv`) is in the `ComfyUI/models/tagger/(model name)/` folder.
- Check that the extra Python package (onnxruntime) is installed in **the Python that ComfyUI uses** (Chapter 11, section 11-2).
### Tagger is very slow (the GPU is not used)
If ComfyUI's Python has both the CPU package `onnxruntime` and the GPU package `onnxruntime-gpu`, the GPU stops being used.
(Installing other custom nodes can bring in the CPU package.)
Stop ComfyUI, then in ComfyUI's Python run `pip uninstall -y onnxruntime onnxruntime-gpu` followed by `pip install onnxruntime-gpu`, and restart ComfyUI (DirectML is not supported).
### PSD-related features give an error
An extra Python package (psd-tools) is needed.
Run the installation command shown in the error message in the Python that ComfyUI uses, then restart ComfyUI.
### Semantic search (Gallery) does not work well
| Symptom | Cause | What to do |
| --- | --- | --- |
| "Cannot reach Ollama" is shown | Ollama is not running, or the Ollama URL is wrong | Start Ollama (it is needed while indexing and searching). Check the Ollama URL on the gear button's Model tab |
| "The Ollama model is not downloaded" is shown | EmbeddingGemma 2 has not been downloaded | Run `ollama pull embeddinggemma-2:740m` (or `embeddinggemma-2:440m`) |
| Indexing sometimes fails with "connection was forcibly closed" or similar | Security software (such as AdGuard) is intercepting Ollama's traffic | Exclude Ollama from filtering. Failed images are processed again by the next "New / changed images only" run |
| Gemini says "GEMINI_API_KEY is not set", or the key is rejected | There is no key in `.env`, or the line starts with `#` | Write `GEMINI_API_KEY=key` in the plugin's `.env` (copy `.env.example`) and restart ComfyUI. `.env` is only read at startup |
| Search or Similar gives no results | The selected folder has no indexed images | Build the index with the gear button. Choose a folder with a green dot. When you change the model or vector dimension, "rebuild all" |
### The TagSpace space cannot be built
| Symptom | Cause | What to do |
| --- | --- | --- |
| "EmbeddingGemma 2 in Ollama is required" is shown | Ollama is not running, or the model is missing | Prepare the same way as for semantic search (table above) |
| "The Gallery semantic index was made with ..." is shown | The Gallery index was made with Gemini | Choose EmbeddingGemma 2 in the Gallery's semantic settings and rebuild all |
| Building takes a very long time | The first build reads image prompts and prepares new tags | Later builds are faster. When using Tagger fill, check that Tagger runs on the GPU ("Tagger is very slow" above) |
### A partner API (Nano Banana) says authentication failed
The cause is a missing Comfy API key in `.env`. Write `COMFY_API_KEY=your key` (issued at platform.comfy.org) in the plugin's `.env` and restart ComfyUI (Chapter 5, section 5-8).
`.env` is read only at startup.
### ComfyUI freezes (no tab responds)
On Windows, ComfyUI can stop responding (generation included), for example after switching a Models tab list with many preview images.
In the Settings tab, under **Windows Stability (Event Loop)**, turn on **Use the Selector event loop on Windows (restart required)** and restart ComfyUI.
### Part of a Gallery image looks broken, as if mixed with another image
With the **Selector event loop** above turned on, up to v0.9.1 part of an image received by the Gallery preview or Download could be corrupted
(the file in the output folder itself is fine). This is fixed in v0.9.2.
After updating Workflow Studio, broken images may remain in the browser cache, so reload with **Ctrl + F5**.
### Images do not appear in the Gallery
- Press **Refresh** in the Gallery.
- Check that the "Gallery Output Folder" in the Settings tab matches where ComfyUI saves images.
---
## 12-6 If it is still not solved
### Look at Workflow Studio's Help
The **Help** tab of Workflow Studio also has troubleshooting information.
It may include information added after this book was published.
### Ask or report on GitHub
Problems that are not solved can be reported in **Issues** on Workflow Studio's GitHub repository.
When reporting, attaching the following information makes the cause easier to find.
- The Workflow Studio version
- The ComfyUI version and how it was installed (portable version, Desktop version, Stability Matrix and so on)
- On which screen, what you did, and what happened
- The error message shown on the black screen (console) where ComfyUI was started
- The red error message shown in the "Console" of the browser's developer tools (F12 key)
> **Caution**
> When pasting error messages, check that they do not contain personal information such as your computer's user name.
> **Summary of this chapter**
> - When in trouble, first try "reload", "Refresh buttons" and "restarting ComfyUI"
> - Red nodes mean missing custom nodes. Solve with ComfyUI Manager's Install Missing Custom Nodes
> - When settings are not reflected, check for a forgotten Apply and a leftover Batch check
> - For out-of-memory, deal with it by unloading local AI and reviewing the image size
> - When not solved, make use of the Help tab and GitHub Issues
+139
View File
@@ -0,0 +1,139 @@
# Appendix
## Appendix A Glossary
The main terms that appear in this book, in alphabetical order.
| Term | Meaning | In this book |
| --- | --- | --- |
| API format / UI format | The save format of a workflow. UI format includes things such as node positions; API format contains only the processing content | Chapter 4 |
| Apply | The button in GenerateUI that reflects changed values into the workflow | Chapter 5 |
| Badge | A colored label attached to a workflow or model | Chapter 4 |
| Batch generation | Automatically generating in bulk while changing conditions | Chapter 5 |
| Canvas | The work space where nodes are laid out on ComfyUI's node screen | Chapter 3 |
| CFG | How strongly to follow the prompt | Chapter 5 |
| Checkpoint | The model that forms the core of image generation | Chapter 6 |
| Civitai | A sharing site for image-generation models | Chapter 6 |
| ComfyUI Manager | A management tool for searching for and installing custom nodes | Chapter 2 |
| Custom node | A node or extension added to ComfyUI afterwards. Workflow Studio is one of them | Chapter 1 |
| Decision model | An AI model that answers questions with numbers such as the probability of "yes". Used to judge image checks | Chapter 10 |
| Denoise | How strongly to redraw the original image | Chapter 5 |
| Embedding | A small file that teaches a particular word a meaning | Chapter 6 |
| Hash value | A "fingerprint"-like value computed from a file's contents. Used to look up models on Civitai | Chapter 6 |
| I2I (Image to Image) | Making a new image based on an image | Chapter 5 |
| Inpainting | Redrawing only part of an image (the masked part) | Chapter 11 |
| JSON | The data format used to save workflows | Chapter 1 |
| Keyframe | In Lab, a specification of "from which run to switch settings" | Chapter 5 |
| Local AI | AI that runs inside your own computer (such as Ollama) | Chapter 10 |
| LoRA | A small model that teaches an art style, character and so on as an add-on | Chapter 6 |
| Mask | A specification of the part of an image to be processed | Chapter 11 |
| Metadata | Information embedded in an image, such as the settings used at generation | Chapter 8 |
| Model | The file that acts as the "AI brain" that makes images | Chapter 1 |
| Negative prompt | A prompt where you write what you do not want drawn | Chapter 7 |
| Node | A "box" arranged on ComfyUI's screen. It handles one process | Chapter 1 |
| Node set | Several nodes and their connections saved together | Chapter 9 |
| OCR | Reading the text shown in an image | Chapter 10 |
| Ollama | Software for running local AI easily | Chapter 10 |
| Port number | A number for distinguishing which software to communicate with inside one computer | Chapter 2 |
| Positive prompt | A prompt where you write what you want drawn | Chapter 7 |
| Preset | A pair of positive and negative prompts saved together | Chapter 7 |
| Prompt | The text that tells the AI "what kind of image you want" | Chapter 1 |
| Queue | The reservation list for generation. Processed in order | Chapter 1 |
| Sampler | The algorithm used when drawing an image (such as euler) | Chapter 5 |
| Scheduler | How the drawing amount is distributed across steps | Chapter 5 |
| Seed | The seed of random numbers an image grows from. If the same, you get almost the same image | Chapter 5 |
| Semantic search | Searching images by the "meaning" of a sentence or image. Used with the Gallery's S button | Chapter 8 |
| Skill | Instructions that tell the AI TOOL chat its role and how to answer. Picking one switches what it is good at | Chapter 10 |
| Steps | The number of times drawing is repeated | Chapter 5 |
| Style | A mood template added to a prompt | Chapter 7 |
| T2I (Text to Image) | Making an image from text (a prompt) | Chapter 5 |
| TagSpace | A tab that lays out tags, prompts and generated images in 3D by closeness of meaning | Chapter 11 |
| Trigger word | A word to put in the prompt to bring out a LoRA's effect | Chapter 6 |
| VAE | The model that finishes an image's colors and fine details | Chapter 6 |
| VLM | An AI model that can read images | Chapter 10 |
| VRAM | The memory of the GPU (graphics card) | Chapter 10 |
| Wildcard | A mechanism that randomly swaps a part of the prompt at every generation | Chapter 7 |
| Workflow | The whole flow of processing made by connecting nodes with lines | Chapter 1 |
| WS Library | Workflow Studio's side panel that opens beside ComfyUI's node screen | Chapter 9 |
---
## Appendix B Keyboard and mouse operations
### General
| Operation | What happens |
| --- | --- |
| Esc | Closes the open screen (modal or panel) |
| Enter | Confirms a rename / sends a chat message |
| Shift + Enter | Line break in chat |
| Double-click | Opens the detail screen (workflows, models and so on) |
| Shift + click (W button) | Opens Workflow Studio in a new window |
### GenerateUI
| Operation | What happens |
| --- | --- |
| Alt + click (Apply) | Reflects the values and generates at once |
| Ctrl + Up/Down (prompt field) | Raises or lowers the emphasis `(word:number)` of the selected word by 0.05 |
| Enter / Shift + Enter (Raw JSON search box) | Moves to the next / previous match |
### Gallery
| Operation | What happens |
| --- | --- |
| Ctrl + click | Select several images |
| Alt + click | Open the image in the Metadata subtab |
| Enter (when semantic search is on) | Run a semantic search with the text in the search box |
### WS Library
| Operation | What happens |
| --- | --- |
| Drag and drop | Place or load at the dropped position on the canvas |
| Double-click | Place or load near the center of the canvas |
| Alt + click (N tab) | Switch a node's favorite on or off |
### Image Edit
| Operation | What happens |
| --- | --- |
| V / B / T / S / G | Switch to the Select / Draw / Text / Shape / Fill tool |
| Ctrl + Z / Ctrl + Y | Undo / redo |
| Mouse wheel | Zoom in and out |
| Space + drag | Move the view position |
| Delete | Delete the selected layer (when there are two or more layers) |
| D / E (while Draw is active) | Draw / Erase |
| X (while Draw is active) | Swap Color 1 and Color 2 |
| [ / ] (while Draw is active) | Make the brush smaller / larger |
| B (while Draw is active) | Open or close the brush picker |
---
## Appendix C Reference links
### Workflow Studio
- GitHub (latest version, release notes, bug reports)
https://github.com/ketle-man/ComfyUI-Workflow-Studio
### ComfyUI itself and management tools
- ComfyUI
https://github.com/comfyanonymous/ComfyUI
- ComfyUI Manager
https://github.com/Comfy-Org/ComfyUI-Manager
### Related software and services introduced in this book
| Name | Use | In this book |
| --- | --- | --- |
| Civitai (https://civitai.com/) | Getting models, fetching information | Chapter 6 |
| Ollama (https://ollama.com/) | Local AI | Chapter 10 |
| LM Studio (https://lmstudio.ai/) | Local AI | Chapter 10 |
| Lemonade Server (https://lemonade-server.ai/) | Local AI | Chapter 10 |
| Eagle (https://eagle.cool/) | Image-management software | Chapter 8 |
### Custom nodes that are handy to use together
| Name | Use | In this book |
| --- | --- | --- |
| ComfyUI Comic Creator (https://github.com/ketle-man/comfyui-comic-creator) | Manga making. Works with the Gallery and image generation | Chapter 11 |
| comfyui-image-feeder (https://github.com/ketle-man/comfyui-image-feeder) | Feeder's Image Loop mode | Chapter 5 |
| comfyui-mask-editor-one (https://github.com/ketle-man/comfyui-mask-editor-one) | Advanced masking and background removal in Image Edit | Chapter 11 |
| ComfyUI-Impact-Pack (https://github.com/ltdrdata/ComfyUI-Impact-Pack) | Curly-brace-style wildcards | Chapter 7 |
| ComfyUI-Lora-Manager | Managing LoRAs; placing LoRAs together from the WS Library | Chapter 9 |
> **Caution**
> The contents and URLs of linked pages may change.
+27
View File
@@ -0,0 +1,27 @@
# Afterword
Thank you for reading to the end.
ComfyUI is very free and deep software.
Because of that freedom, it is also software where beginners think "I don't know where to start".
Workflow Studio was born from the wish to widen that entrance a little,
and to make "creating" and "organizing" more casual.
Let's look back once more at the ways of using it introduced in this book.
1. **Organize** - make workflows, models and prompts easy to tell apart with thumbnails and badges
2. **Create** - generate images in GenerateUI without touching the node screen
3. **Collect** - keep prompts and settings that worked in presets and the Gallery
4. **Reproduce** - recall the same settings right away from an image's metadata
5. **Expand** - speed up trial and error with batch generation and local AI
The shortest path to improving at image generation is to **try a lot, and keep what worked**.
I will be glad if Workflow Studio becomes a tool that supports that accumulation.
Workflow Studio will continue to gain features and improvements.
New features and changes are announced in the release notes on GitHub.
Your impressions after using it, and requests such as "I want this feature", are also welcome on GitHub.
May your life with image generation become more fun and richer.
ketle-man
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 422 KiB

+78
View File
@@ -0,0 +1,78 @@
"""Runs before ComfyUI imports any custom node — the only place early enough
to populate os.environ for code that reads env vars at import/request time.
Loads a .env file (if present) from this plugin's root directory so the
Unsloth backend's API key (UNSLOTH_API_KEY) can be set without editing
settings.json or exposing it to the frontend.
"""
from pathlib import Path
PLUGIN_DIR = Path(__file__).resolve().parent
ENV_FILE = PLUGIN_DIR / ".env"
def _maybe_use_selector_event_loop():
"""Windows: ProactorEventLoop(IOCP) を SelectorEventLoop に切り替える(既定はオフ)。
環境によっては、ソケットclose(closesocket/NtClose)でイベントループがOSカーネル内で停止し、
ComfyUI全体が無応答になる(再起動するまでポートも解放されない)。Selector方式はIOCPを使わず、
この停止を避けられる可能性がある。副作用: asyncioのサブプロセス機能が使えない、
同時接続数の上限(約512)がある。
有効化: 環境変数 WFS_SELECTOR_LOOP=1、または
user/default/Workflow-Studio/settings.json に "windows_selector_event_loop": true
ComfyUIがイベントループを作成する前に呼ぶ必要があるため、ここ(prestartup)で設定する。
"""
import os
import sys
if sys.platform != "win32":
return
enabled = os.environ.get("WFS_SELECTOR_LOOP", "").strip().lower() in ("1", "true", "yes")
if not enabled:
try:
import json
candidates = [
PLUGIN_DIR.parent.parent / "user" / "default" / "Workflow-Studio" / "settings.json",
PLUGIN_DIR / "data" / "settings.json", # config.py fallback when user/default is absent
]
for settings in candidates:
if settings.is_file():
with open(settings, "r", encoding="utf-8") as f:
enabled = bool(json.load(f).get("windows_selector_event_loop", False))
break
except Exception:
enabled = False
if not enabled:
return
try:
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
print("[Workflow Studio] Using WindowsSelectorEventLoopPolicy (windows_selector_event_loop enabled)")
except Exception as e:
print(f"[WARNING] Workflow Studio: failed to switch to selector event loop: {e}")
return
# Selector方式ではloop.sendfileがasyncioの_sendfile_fallbackになるが、これは16KBバッファを
# 使い回しつつ未送信分をコピーせずトランスポートに積むため、送信が詰まると後続ブロックで
# 上書きされ画像等が壊れて届く(Python 3.12で確認)。aiohttpのsendfileを無効化し、
# チャンクごとに新しいbytesを読む自前フォールバックを使わせる
os.environ["AIOHTTP_NOSENDFILE"] = "1"
fileresponse = sys.modules.get("aiohttp.web_fileresponse")
if fileresponse is not None:
fileresponse.NOSENDFILE = True
_maybe_use_selector_event_loop()
if ENV_FILE.exists():
try:
from dotenv import load_dotenv
load_dotenv(ENV_FILE)
except ImportError:
print(
"[WARNING] Workflow Studio: .env file found but python-dotenv is not installed "
"(pip install -r requirements.txt). UNSLOTH_API_KEY / GEMINI_API_KEY will not be loaded from .env."
)
except Exception as e:
print(f"[WARNING] Workflow Studio: failed to load .env file: {e}")
+61
View File
@@ -1,9 +1,66 @@
import os
import logging
import re
from pathlib import Path
logger = logging.getLogger(__name__)
# ─── パス検査(クライアント由来のパスを resolve() する前に文字列のまま検査する)───
# Windows では UNC パス(\\host\share)は resolve()・exists() を呼んだ時点で相手ホストへ
# Windows アカウントの資格情報付きで接続してしまうため、チェックはパス操作より先に行う。
_DRIVE_RE = re.compile(r"^[A-Za-z]:")
def is_unc(value) -> bool:
"""ネットワーク共有/デバイスパス(\\\\host\\share, //host/share, \\\\?\\ 等)か。"""
return str(value or "").replace("/", "\\").startswith("\\\\")
def is_safe_rel(rel) -> bool:
"""相対パスとして安全か。UNC・ドライブ指定・絶対パス・「..」・空セグメントを
結合・resolve() より前に文字列のまま拒否する。"""
s = str(rel or "")
if not s or "\x00" in s:
return False
if s.startswith(("/", "\\")) or _DRIVE_RE.match(s):
return False
return not any(part in ("", "..") for part in re.split(r"[/\\]", s))
def resolve_untrusted(value, roots=()) -> Path:
"""クライアント由来の絶対パス文字列を検査してから resolve() して返す。
ファイルシステムに触れない文字列処理(normpath)だけで「..」を正規化・拒否し、
UNC は roots のうち UNC のルート配下に文字列上一致する場合のみ許可する
(ギャラリーのルートをネットワーク共有に置いている構成だけを通すため)。
拒否時は ValueError。"""
s = str(value or "")
if not s or "\x00" in s:
raise ValueError("Access denied: invalid path")
p = Path(os.path.normpath(s))
if ".." in p.parts:
raise ValueError("Access denied: path traversal")
if is_unc(s):
for root in roots:
if root is None or not is_unc(str(root)):
continue
try:
p.relative_to(Path(root)) # 文字列上の照合(解決しない)
return p.resolve()
except (ValueError, OSError):
continue
raise ValueError("Access denied: network path")
# 受け付ける形はドライブレター絶対パス(Windows)または / 始まり(その他OS)に限る。
# ルート相対(\foo)・ドライブ相対(C:foo)・NT デバイス形式(\??\, \.\ 等)・相対パスは拒否する
if os.name == "nt":
if not re.match(r"^[A-Za-z]:[\\/]", s):
raise ValueError("Access denied: invalid path")
elif not s.startswith("/"):
raise ValueError("Access denied: invalid path")
return p.resolve()
# Plugin root directory
PLUGIN_DIR = Path(__file__).resolve().parent.parent
@@ -69,6 +126,10 @@ 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"
AI_SKILLS_DIR = DATA_DIR / "ai_skills"
LAB_PLAN_DIR = DATA_DIR / "lab_plan"
VIDEO_PLAN_DIR = DATA_DIR / "video_plan"
VIDEO_EDIT_PROJECT_DIR = DATA_DIR / "video_edit_project"
# Tagger
TAGGER_DB_FILE = DATA_DIR / "tagger.db"
+115 -6
View File
@@ -3,11 +3,15 @@
import asyncio
import json
import logging
import os
import urllib.request
import urllib.error
from aiohttp import web
from ..config import is_safe_rel
from ..services.url_guard import require_backend_url, urlopen as _guarded_urlopen
logger = logging.getLogger(__name__)
@@ -17,6 +21,89 @@ def setup_routes(app: web.Application):
app.router.add_post("/api/wfm/eagle/test", handle_test)
def _resolve_local_path(filename, subfolder, type_name):
"""filename/subfolder/type を ComfyUI 出力ディレクトリ上の実パスに解決する。
ComfyUI と Eagle が同一マシン上で動作している場合のみ有効なパスが返る
(別マシンの場合は None を返し、呼び出し側は addFromURL にフォールバックする)。"""
try:
import folder_paths # type: ignore
except Exception:
return None
base_dir = folder_paths.get_directory_by_type(type_name or "output")
if not base_dir:
return None
base_dir = os.path.abspath(base_dir)
filename = os.path.basename(filename or "")
if not filename:
return None
# subfolder の UNC・ドライブ指定・絶対パス・「..」は、結合より前に文字列のまま拒否する
# (別ドライブ/UNC だと下の commonpath が ValueError を投げるため、その前に弾く)
if subfolder and not is_safe_rel(subfolder):
return None
full_dir = os.path.abspath(os.path.join(base_dir, subfolder or ""))
# Windowsはパス比較で大文字小文字を区別しないため normcase で正規化してから比較する
if os.path.commonpath((os.path.normcase(full_dir), os.path.normcase(base_dir))) != os.path.normcase(base_dir):
# ディレクトリトラバーサル対策: base_dir 配下以外は拒否
return None
full_path = os.path.join(full_dir, filename)
if not os.path.isfile(full_path):
return None
return full_path
def _resolve_absolute_svg_path(abs_path):
"""comfyui-tosvg の Save SVG String ノードなどが返す絶対パスを検証する。
ComfyUI管理下(output/input/temp)ディレクトリ配下の実在する .svg ファイルのみ許可する
(任意の絶対パスをそのまま信頼すると、外部からこのAPIを叩かれた際に
ローカルの任意ファイルを送信させられてしまうため)。"""
if not abs_path or not abs_path.lower().endswith(".svg"):
return None
try:
import folder_paths # type: ignore
except Exception:
return None
# isfile() も UNC(\\host\share)だと相手ホストへ資格情報付きで接続してしまうため、
# 文字列処理(abspath/commonpath)だけで ComfyUI 管理下と確認できるまでファイルシステムに触れない
abs_path = os.path.abspath(abs_path)
for type_name in ("output", "temp", "input"):
base_dir = folder_paths.get_directory_by_type(type_name)
if not base_dir:
continue
base_dir = os.path.abspath(base_dir)
try:
# Windowsはパス比較で大文字小文字を区別しないため normcase で正規化してから比較する
if os.path.commonpath((os.path.normcase(abs_path), os.path.normcase(base_dir))) == os.path.normcase(base_dir):
return abs_path if os.path.isfile(abs_path) else None
except ValueError:
# Windowsで異なるドライブレターやUNCの場合 commonpath が例外を投げる
continue
return None
def _eagle_add_from_path(eagle_url, file_path, name, tags):
"""Proxy request to Eagle's addFromPath API (runs in thread).
SVG は ComfyUI の /view がセキュリティ上 application/octet-stream +
attachment で返すため、addFromURL 経由だと正しい種別で保存されないことがある。
ComfyUI と Eagle が同一マシン上にある場合はローカルパスを直接渡す方が確実。"""
eagle_url = eagle_url.rstrip("/")
payload = json.dumps({"path": file_path, "name": name, "tags": tags}).encode("utf-8")
req = urllib.request.Request(
f"{eagle_url}/api/item/addFromPath",
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with _guarded_urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
def _eagle_add(eagle_url, image_url, name, tags):
"""Proxy request to Eagle API (runs in thread)."""
eagle_url = eagle_url.rstrip("/")
@@ -48,7 +135,7 @@ def _eagle_add(eagle_url, image_url, name, tags):
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as resp:
with _guarded_urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
@@ -59,7 +146,7 @@ def _eagle_test(eagle_url):
f"{eagle_url}/api/application/info",
method="GET",
)
with urllib.request.urlopen(req, timeout=5) as resp:
with _guarded_urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode("utf-8"))
@@ -67,15 +154,37 @@ async def handle_add(request: web.Request) -> web.Response:
"""POST /api/wfm/eagle/add - Add image to Eagle."""
try:
body = await request.json()
eagle_url = body.get("eagleUrl", "http://localhost:41595")
eagle_url = require_backend_url(body.get("eagleUrl", "http://localhost:41595"))
image_url = body.get("url", "")
name = body.get("name", "image.png")
tags = body.get("tags", [])
filename = body.get("filename", "")
subfolder = body.get("subfolder", "")
file_type = body.get("type", "output")
local_path_hint = body.get("localPath", "")
if not image_url:
if not image_url and not local_path_hint:
return web.json_response({"status": "error", "message": "No URL provided"}, status=400)
result = await asyncio.to_thread(_eagle_add, eagle_url, image_url, name, tags)
# SVG は /view が application/octet-stream + attachment で返すため、
# ローカルファイルが解決できる場合は addFromPath で直接渡す
local_path = None
if local_path_hint:
# comfyui-tosvg の Save SVG String ノードなど、絶対パスが既知の場合
local_path = _resolve_absolute_svg_path(local_path_hint)
elif (filename or name or "").lower().endswith(".svg"):
local_path = _resolve_local_path(filename or name, subfolder, file_type)
if not local_path and not image_url:
return web.json_response(
{"status": "error", "message": f"Could not resolve local SVG path: {local_path_hint}"},
status=400,
)
if local_path:
result = await asyncio.to_thread(_eagle_add_from_path, eagle_url, local_path, name, tags)
else:
result = await asyncio.to_thread(_eagle_add, eagle_url, image_url, name, tags)
return web.json_response(result)
except urllib.error.URLError as e:
logger.error("Eagle URL error: %s", e)
@@ -92,7 +201,7 @@ async def handle_test(request: web.Request) -> web.Response:
"""POST /api/wfm/eagle/test - Test Eagle connection."""
try:
body = await request.json()
eagle_url = body.get("eagleUrl", "http://localhost:41595")
eagle_url = require_backend_url(body.get("eagleUrl", "http://localhost:41595"))
result = await asyncio.to_thread(_eagle_test, eagle_url)
return web.json_response({
"status": "success",
+436 -24
View File
@@ -2,20 +2,20 @@
Gallery Routes - ギャラリータブ用APIエンドポイント
"""
import asyncio
import base64
import json
import mimetypes
import logging
import re
from pathlib import Path
from aiohttp import web
from ..config import DATA_DIR
from ..services.gallery_service import GalleryService
from ..services.semantic_service import SemanticService, SemanticError, DEFAULT_DIM, MODEL as SEMANTIC_DEFAULT_MODEL
logger = logging.getLogger(__name__)
_service = GalleryService(DATA_DIR)
_semantic = SemanticService(DATA_DIR, _service)
def _init_allowed_root() -> None:
@@ -43,19 +43,54 @@ def _init_allowed_root() -> None:
except Exception as e:
logger.warning("GalleryRoutes: failed to get ComfyUI output dir: %s", e)
# ImagePrompt/Style Catalogギャラリー用に実outputフォルダを常に別途保持する
# (Settingsで_allowed_rootが別フォルダに変更されていても影響しないようにするため)
try:
import folder_paths # type: ignore
output_dir = folder_paths.get_output_directory()
if output_dir and Path(output_dir).is_dir():
_service.set_comfy_output_root(output_dir)
except Exception as e:
logger.warning("GalleryRoutes: failed to get ComfyUI output dir for image-prompt root: %s", e)
_init_allowed_root()
# Windowsのレジストリ依存で音声のMIMEが返らない/不統一なため明示登録する(<audio>の再生に必要)
for _ext, _mime in ((".wav", "audio/wav"), (".mp3", "audio/mpeg"), (".ogg", "audio/ogg"),
(".opus", "audio/ogg"), (".m4a", "audio/mp4"), (".flac", "audio/flac"),
(".glb", "model/gltf-binary"), (".vrm", "model/gltf-binary"), (".gltf", "model/gltf+json"),
(".ply", "application/octet-stream"), (".spz", "application/octet-stream"),
(".bin", "application/octet-stream")):
mimetypes.add_type(_mime, _ext)
def setup_routes(app: web.Application):
app.router.add_get("/wfm/gallery/folders", list_folders)
app.router.add_get("/wfm/gallery/semantic/status", semantic_status)
app.router.add_get("/wfm/gallery/semantic/folders", semantic_folders)
app.router.add_post("/wfm/gallery/semantic/index", semantic_index)
app.router.add_post("/wfm/gallery/semantic/cancel", semantic_cancel)
app.router.add_post("/wfm/gallery/semantic/search", semantic_search)
app.router.add_post("/wfm/gallery/semantic/similar", semantic_similar)
app.router.add_post("/wfm/gallery/semantic/clear", semantic_clear)
app.router.add_post("/wfm/gallery/semantic/test", semantic_test)
app.router.add_post("/wfm/gallery/semantic/options", semantic_options)
app.router.add_get("/wfm/gallery/images", list_images)
app.router.add_get("/wfm/gallery/image/meta", get_image_meta)
app.router.add_get("/wfm/gallery/image/workflow", get_image_workflow)
app.router.add_get("/wfm/gallery/image/serve", serve_image)
app.router.add_get("/wfm/gallery/image/thumb", serve_thumb)
app.router.add_post("/wfm/gallery/image/thumb3d", save_model3d_thumb)
app.router.add_get("/wfm/gallery/model3d/asset", serve_model3d_asset)
app.router.add_post("/wfm/gallery/image/meta", save_image_meta)
app.router.add_get("/wfm/gallery/image-prompt/root", get_image_prompt_root)
app.router.add_post("/wfm/gallery/image/image-prompt", save_image_prompt)
app.router.add_get("/wfm/gallery/style-catalog/root", get_style_catalog_root)
app.router.add_post("/wfm/gallery/image/save", save_image_to_gallery)
app.router.add_post("/wfm/gallery/image/save-to-folder", save_image_to_folder_route)
app.router.add_post("/wfm/gallery/output-image/delete", delete_output_image)
app.router.add_post("/wfm/gallery/image/favorite", toggle_favorite)
app.router.add_get("/wfm/gallery/groups", list_groups)
app.router.add_post("/wfm/gallery/groups", create_group)
@@ -68,6 +103,7 @@ def setup_routes(app: web.Application):
app.router.add_get("/wfm/gallery/groups/{name}/images", list_group_images)
# バルク操作
app.router.add_post("/wfm/gallery/bulk/favorite", bulk_favorite)
app.router.add_post("/wfm/gallery/convert", convert_file_route)
app.router.add_post("/wfm/gallery/bulk/group", bulk_group)
# フォルダ・ファイル操作
app.router.add_post("/wfm/gallery/folder", create_folder_route)
@@ -75,6 +111,147 @@ def setup_routes(app: web.Application):
app.router.add_post("/wfm/gallery/images/delete", delete_images_route)
app.router.add_post("/wfm/gallery/images/move", move_images_route)
app.router.add_post("/wfm/gallery/images/export-zip", export_images_zip)
app.router.add_post("/wfm/gallery/images/export-psd", export_images_psd)
app.router.add_post("/wfm/gallery/image/export-psd-layers", export_psd_from_layers)
app.router.add_post("/wfm/gallery/image/import-psd-layers", import_psd_layers)
app.router.add_get("/wfm/gallery/image/psd-layers", get_psd_layers)
# ──────────────────────────────────────────────────────────────
# セマンティック検索 (Gemini Embedding 2 / Ollama EmbeddingGemma 2)
# ──────────────────────────────────────────────────────────────
def _semantic_error(e: SemanticError) -> web.Response:
# code はフロントのi18n用。message は詳細(APIキー等の秘密は含まない)
return web.json_response({"error": str(e), "code": e.code}, status=400)
def _num(body: dict, key: str, default, lo, hi, cast=int):
"""リクエストの数値パラメータを検証して範囲内に収める。数値でなければ400相当のSemanticErrorにする"""
raw = body.get(key, default)
if raw is None or raw == "":
raw = default
try:
v = cast(raw)
except (TypeError, ValueError, OverflowError):
raise SemanticError("bad_param", key)
if v != v: # NaN
raise SemanticError("bad_param", key)
return max(lo, min(v, hi))
async def _json_body(request: web.Request) -> dict:
try:
body = await request.json()
return body if isinstance(body, dict) else {}
except Exception:
return {}
async def semantic_status(request: web.Request) -> web.Response:
folder = request.rel_url.query.get("folder", "")
recursive = request.rel_url.query.get("recursive", "true") == "true"
try:
return web.json_response(await asyncio.to_thread(_semantic.status, folder, recursive))
except Exception as e:
logger.error("semantic_status error: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def semantic_folders(request: web.Request) -> web.Response:
root = request.rel_url.query.get("root", "")
try:
return web.json_response({"folders": await asyncio.to_thread(_semantic.folder_status, root)})
except SemanticError as e:
return _semantic_error(e)
except Exception as e:
logger.error("semantic_folders error: %s", e)
return web.json_response({"error": str(e)}, status=500)
def _apply_local_options(body: dict) -> None:
"""リクエストに Ollama の接続先/モデルサイズが含まれていれば反映・保存する"""
if "ollama_url" in body:
_semantic.set_local_options(str(body.get("ollama_url", "")), str(body.get("ollama_tag", "")))
async def semantic_index(request: web.Request) -> web.Response:
body = await _json_body(request)
try:
_apply_local_options(body)
await asyncio.to_thread(
_semantic.start_index,
str(body.get("folder", "")),
bool(body.get("recursive", True)),
"all" if body.get("mode") == "all" else "missing",
str(body.get("model", SEMANTIC_DEFAULT_MODEL)),
_num(body, "dim", DEFAULT_DIM, 0, 100000),
)
return web.json_response({"ok": True})
except SemanticError as e:
return _semantic_error(e)
except Exception as e:
logger.error("semantic_index error: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def semantic_cancel(request: web.Request) -> web.Response:
_semantic.cancel()
return web.json_response({"ok": True})
async def semantic_search(request: web.Request) -> web.Response:
body = await _json_body(request)
try:
limit = _num(body, "limit", 200, 1, 1000)
min_score = _num(body, "min_score", 0.0, -1.0, 1.0, float)
images = await asyncio.to_thread(
_semantic.search_text, str(body.get("query", "")), str(body.get("folder", "")),
bool(body.get("recursive", True)), limit, min_score,
)
return web.json_response({"images": images, "total": len(images)})
except SemanticError as e:
return _semantic_error(e)
except Exception as e:
logger.error("semantic_search error: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def semantic_similar(request: web.Request) -> web.Response:
body = await _json_body(request)
try:
limit = _num(body, "limit", 200, 1, 1000)
min_score = _num(body, "min_score", 0.0, -1.0, 1.0, float)
images = await asyncio.to_thread(
_semantic.search_similar, str(body.get("path", "")), str(body.get("folder", "")),
bool(body.get("recursive", True)), limit, min_score,
)
return web.json_response({"images": images, "total": len(images)})
except SemanticError as e:
return _semantic_error(e)
except Exception as e:
logger.error("semantic_similar error: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def semantic_clear(request: web.Request) -> web.Response:
try:
await asyncio.to_thread(_semantic.clear)
return web.json_response({"ok": True})
except SemanticError as e:
return _semantic_error(e)
async def semantic_test(request: web.Request) -> web.Response:
body = await _json_body(request)
try:
_apply_local_options(body)
res = await asyncio.to_thread(_semantic.test_connection, str(body.get("model", SEMANTIC_DEFAULT_MODEL)), _num(body, "dim", DEFAULT_DIM, 0, 100000))
return web.json_response({"ok": True, **res})
except SemanticError as e:
return _semantic_error(e)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# ──────────────────────────────────────────────────────────────
@@ -104,6 +281,7 @@ async def list_images(request: web.Request) -> web.Response:
favorite_only = request.rel_url.query.get("favorite", "false") == "true"
tag_filter = request.rel_url.query.get("tag", "")
group_filter = request.rel_url.query.get("group", "")
recursive = request.rel_url.query.get("recursive", "false") == "true"
if not folder:
return web.json_response({"error": "folder parameter required"}, status=400)
@@ -117,6 +295,7 @@ async def list_images(request: web.Request) -> web.Response:
favorite_only=favorite_only,
tag_filter=tag_filter,
group_filter=group_filter,
recursive=recursive,
)
# 検索なしのフォルダロード時にバックグラウンドインデックスを起動
if not search:
@@ -148,9 +327,15 @@ async def get_image_workflow(request: web.Request) -> web.Response:
return web.json_response({"error": "path required"}, status=400)
try:
wf = _service.extract_workflow_from_metadata(path)
if wf is None:
return web.json_response({"workflow": None, "has_workflow": False})
return web.json_response({"workflow": wf, "has_workflow": True})
# prompt_workflow: プロンプト抽出専用(PNG[prompt]優先)。トップレベルとサブグラフに
# 独立した複数系統を持つワークフローではUI形式(workflow)からの抽出がトップレベル
# 系統しか拾えないことがあるため、Promptタブはこちらを使う(Metadataタブと揃える)。
prompt_wf = _service.extract_prompt_workflow_from_metadata(path)
return web.json_response({
"workflow": wf,
"has_workflow": wf is not None,
"prompt_workflow": prompt_wf,
})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@@ -160,7 +345,10 @@ async def serve_image(request: web.Request) -> web.Response:
if not path:
return web.Response(status=400)
img_path = _service.serve_image(path)
# PSDの合成処理はCPU負荷が高く同期実行するとイベントループ全体をブロックする
# (他の並行リクエスト、例えば別画像のサムネイル配信も止まってしまう)ためexecutorで実行する
loop = asyncio.get_event_loop()
img_path = await loop.run_in_executor(None, _service.serve_image, path)
if img_path is None:
return web.Response(status=404)
@@ -182,7 +370,10 @@ async def serve_thumb(request: web.Request) -> web.Response:
except ValueError:
width = 256
thumb_path = _service.serve_thumbnail(path, width)
# PSD/MP4のサムネイル生成はCPU負荷が高く同期実行するとイベントループ全体をブロックするため
# executorで実行する(他画像のサムネイル配信が巻き添えで遅延・停止するのを防ぐ)
loop = asyncio.get_event_loop()
thumb_path = await loop.run_in_executor(None, _service.serve_thumbnail, path, width)
if thumb_path is None:
return web.Response(status=404)
@@ -200,10 +391,56 @@ async def serve_thumb(request: web.Request) -> web.Response:
})
async def save_model3d_thumb(request: web.Request) -> web.Response:
"""POST /wfm/gallery/image/thumb3d — ブラウザが描画した3Dモデルのサムネイルを保存する"""
try:
body = await request.json()
path = body.get("path", "")
image = body.get("image", "")
if not path or not image:
return web.json_response({"ok": False, "error": "path and image required"}, status=400)
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, _service.save_model3d_thumbnail, path, image)
return web.json_response(result, status=200 if result.get("ok") else 400)
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=500)
async def serve_model3d_asset(request: web.Request) -> web.Response:
"""GET /wfm/gallery/model3d/asset — .gltfが参照する外部ファイル(.bin/テクスチャ)を配信する"""
path = request.rel_url.query.get("path", "")
uri = request.rel_url.query.get("uri", "")
if not path or not uri:
return web.Response(status=400)
asset = _service.serve_model3d_asset(path, uri)
if asset is None:
return web.Response(status=404)
content_type, _ = mimetypes.guess_type(str(asset))
return web.FileResponse(asset, headers={
"Content-Type": content_type or "application/octet-stream",
"Cache-Control": "max-age=3600",
})
# ──────────────────────────────────────────────────────────────
# バルク操作
# ──────────────────────────────────────────────────────────────
async def convert_file_route(request: web.Request) -> web.Response:
"""POST /wfm/gallery/convert — 音声/動画の音声を別形式へ変換(1ファイルずつ呼ぶ)"""
try:
body = await request.json()
path = body.get("path", "")
if not path:
return web.json_response({"ok": False, "error": "path required"}, status=400)
# 変換はCPU負荷が高く同期実行するとイベントループ全体が止まるためexecutorで実行する
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, _service.convert_file, path, body)
return web.json_response(result, status=200 if result.get("ok") else 400)
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=500)
async def bulk_favorite(request: web.Request) -> web.Response:
"""POST /wfm/gallery/bulk/favorite — 複数画像のお気に入りを一括設定"""
try:
@@ -248,6 +485,36 @@ async def save_image_meta(request: web.Request) -> web.Response:
return web.json_response({"error": str(e)}, status=500)
async def get_image_prompt_root(request: web.Request) -> web.Response:
"""GET /wfm/gallery/image-prompt/root — ImagePromptギャラリーのルートフォルダを返す"""
root = _service.get_image_prompt_root()
if root is None:
return web.json_response({"error": "ComfyUI output directory not resolved"}, status=500)
return web.json_response({"root": root})
async def save_image_prompt(request: web.Request) -> web.Response:
"""POST /wfm/gallery/image/image-prompt — 画像の.txtサイドカーにプロンプトを保存"""
try:
body = await request.json()
img_path = body.get("path", "")
prompt = body.get("prompt", "")
if not img_path:
return web.json_response({"error": "path required"}, status=400)
ok = _service.save_image_prompt(img_path, prompt)
return web.json_response({"ok": ok})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
async def get_style_catalog_root(request: web.Request) -> web.Response:
"""GET /wfm/gallery/style-catalog/root — Style Catalogギャラリーのルートフォルダを返す"""
root = _service.get_style_catalog_root()
if root is None:
return web.json_response({"error": "ComfyUI output directory not resolved"}, status=500)
return web.json_response({"root": root})
async def toggle_favorite(request: web.Request) -> web.Response:
try:
body = await request.json()
@@ -261,7 +528,8 @@ async def toggle_favorite(request: web.Request) -> web.Response:
async def save_image_to_gallery(request: web.Request) -> web.Response:
"""Image Edit Tab からの PNG をギャラリーのルートフォルダに保存する。"""
"""Image Edit Tab / AI Chat からの画像(PNG等)やSVGをギャラリーのルートフォルダに保存する。
imageData は data URL (data:<mime>;base64,<data>) を想定。ヘッダがなければPNG扱い(旧互換)。"""
try:
body = await request.json()
filename = body.get("filename", "").strip()
@@ -273,27 +541,64 @@ async def save_image_to_gallery(request: web.Request) -> web.Response:
if _service._allowed_root is None:
return web.json_response({"error": "Gallery root not configured. Open Gallery tab and set output folder first."}, status=500)
# ファイル名サニタイズ(OSで使えない文字を除去)
safe = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", filename)
if not safe.lower().endswith(".png"):
safe += ".png"
result = _service.save_image_to_folder(str(_service._allowed_root), filename, image_data)
if not result["ok"]:
return web.json_response({"error": result["error"]}, status=400)
# base64 data URL をデコード
raw = image_data
if "," in raw:
raw = raw.split(",", 1)[1]
image_bytes = base64.b64decode(raw)
save_path = _service._allowed_root / safe
save_path.write_bytes(image_bytes)
logger.info("save_image_to_gallery: saved %s (%d bytes)", save_path, len(image_bytes))
return web.json_response({"ok": True, "path": str(save_path)})
logger.info("save_image_to_gallery: saved %s", result["path"])
return web.json_response({"ok": True, "path": result["path"]})
except Exception as e:
logger.error("save_image_to_gallery error: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def save_image_to_folder_route(request: web.Request) -> web.Response:
"""POST /wfm/gallery/image/save-to-folder — 任意フォルダへ画像を保存(同名なら上書き)。
Style Catalogのカタログ作成で、スタイル名をファイル名として使う際に利用する。"""
try:
body = await request.json()
folder = body.get("folder", "").strip()
filename = body.get("filename", "").strip()
image_data = body.get("imageData", "")
if not folder or not filename or not image_data:
return web.json_response({"error": "folder, filename and imageData required"}, status=400)
result = _service.save_image_to_folder(folder, filename, image_data)
if not result["ok"]:
return web.json_response({"error": result["error"]}, status=400)
return web.json_response({"ok": True, "path": result["path"]})
except Exception as e:
logger.error("save_image_to_folder_route error: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def delete_output_image(request: web.Request) -> web.Response:
"""POST /wfm/gallery/output-image/delete — ComfyUI標準の画像参照(filename/subfolder/type)から
実ファイルを削除する。Style Catalogのカタログ作成で「Outputフォルダに画像を残さない」
オプションが有効な時、カタログへコピー保存した直後の元画像を消すために使う。
type="output" 以外は拒否する(temp/inputの誤削除を防止)。"""
try:
body = await request.json()
filename = (body.get("filename") or "").strip()
subfolder = body.get("subfolder") or ""
img_type = body.get("type") or ""
if not filename or img_type != "output":
return web.json_response({"error": "filename required and type must be 'output'"}, status=400)
import folder_paths # type: ignore
output_dir = Path(folder_paths.get_output_directory())
target = output_dir / subfolder / filename
result = await asyncio.to_thread(_service.delete_images, [str(target)])
if result["deleted"]:
return web.json_response({"ok": True})
return web.json_response({"error": "; ".join(result["errors"]) or "File not found"}, status=404)
except Exception as e:
logger.error("delete_output_image error: %s", e)
return web.json_response({"error": str(e)}, status=500)
# ──────────────────────────────────────────────────────────────
# グループ管理
# ──────────────────────────────────────────────────────────────
@@ -316,7 +621,7 @@ async def create_group(request: web.Request) -> web.Response:
return web.json_response({"error": str(e)}, status=500)
_RESERVED_GROUPS = {"__Feeder__"}
_RESERVED_GROUPS = {"__Feeder__", "__Video Assets__", "__Video Temp__"}
async def rename_group(request: web.Request) -> web.Response:
@@ -485,3 +790,110 @@ async def export_images_zip(request: web.Request) -> web.Response:
except Exception as e:
logger.error("Error exporting images to ZIP: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def export_images_psd(request: web.Request) -> web.Response:
"""POST /wfm/gallery/images/export-psd - 複数画像を1つのPSD(レイヤー分割)にしてダウンロード"""
try:
body = await request.json()
paths = body.get("paths", [])
if not paths:
return web.json_response({"error": "paths required"}, status=400)
psd_bytes = _service.build_psd_from_images(paths)
return web.Response(
body=psd_bytes,
content_type="image/vnd.adobe.photoshop",
headers={"Content-Disposition": "attachment; filename=gallery_export.psd"}
)
except RuntimeError as e:
logger.error("PSD export dependency missing: %s", e)
return web.json_response({"error": str(e)}, status=500)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error exporting images to PSD: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def export_psd_from_layers(request: web.Request) -> web.Response:
"""POST /wfm/gallery/image/export-psd-layers - Image Editタブのレイヤー配列を1つのPSDにしてダウンロード"""
try:
body = await request.json()
width = int(body.get("width", 0))
height = int(body.get("height", 0))
layers = body.get("layers", [])
if not layers:
return web.json_response({"error": "layers required"}, status=400)
psd_bytes = _service.build_psd_from_layers(width, height, layers)
return web.Response(
body=psd_bytes,
content_type="image/vnd.adobe.photoshop",
headers={"Content-Disposition": "attachment; filename=image_edit_export.psd"}
)
except RuntimeError as e:
logger.error("PSD export dependency missing: %s", e)
return web.json_response({"error": str(e)}, status=500)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error exporting Image Edit layers to PSD: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def import_psd_layers(request: web.Request) -> web.Response:
"""POST /wfm/gallery/image/import-psd-layers - アップロードされたPSDをレイヤー配列(JSON)に分解して返す"""
try:
reader = await request.multipart()
field = await reader.next()
if field is None:
return web.json_response({"error": "file required"}, status=400)
psd_bytes = await field.read(decode=False)
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, _service.import_psd_layers, psd_bytes)
return web.json_response(result)
except RuntimeError as e:
logger.error("PSD import dependency missing: %s", e)
return web.json_response({"error": str(e)}, status=500)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error importing PSD layers: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def get_psd_layers(request: web.Request) -> web.Response:
"""GET /wfm/gallery/image/psd-layers - サーバー上のPSDファイルをレイヤー配列(JSON)に分解して返す
(Galleryから直接Image Editへ送る際、アップロードの往復を挟まずに済ませるため)"""
path = request.rel_url.query.get("path", "")
if not path:
return web.json_response({"error": "path required"}, status=400)
try:
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, _service.get_psd_layers_from_path, path)
return web.json_response(result)
except RuntimeError as e:
logger.error("PSD import dependency missing: %s", e)
return web.json_response({"error": str(e)}, status=500)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error reading PSD layers: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def semantic_options(request: web.Request) -> web.Response:
"""Ollama の接続先/モデルサイズを保存して、新しい状態を返す"""
body = await _json_body(request)
try:
await asyncio.to_thread(_apply_local_options, body)
return web.json_response(await asyncio.to_thread(_semantic.status))
except SemanticError as e:
return _semantic_error(e)
except Exception as e:
logger.error("semantic_options error: %s", e)
return web.json_response({"error": str(e)}, status=500)
+11 -8
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from aiohttp import web
from ..services.settings_service import SettingsService
from ..config import DATA_DIR
from ..config import DATA_DIR, resolve_untrusted
logger = logging.getLogger(__name__)
@@ -82,9 +82,9 @@ async def handle_open(request: web.Request) -> web.Response:
# Retrieve setting for gmic path
data = _settings.load()
gmic_exe = data.get("gmic_qt_path", "")
if not os.path.exists(gmic_exe):
if not os.path.isabs(gmic_exe) or os.path.splitext(gmic_exe)[1].lower() != ".exe" or not os.path.isfile(gmic_exe):
return web.json_response({
"error": f"G'MIC executable not found at: {gmic_exe}. Please configure the path in settings."
"error": f"G'MIC executable not found at: {gmic_exe}. Please configure an absolute .exe path in settings."
}, status=400)
ext = ".png"
@@ -153,17 +153,20 @@ async def handle_result(request: web.Request) -> web.Response:
try:
body = await request.json()
result_path = body.get("result_path", "")
if not result_path or not os.path.exists(result_path):
return web.json_response({"error": f"Result file not found: {result_path}"}, status=404)
# Path traversal guard: only allow files inside gmic_temp
# Path traversal guard: only allow files inside gmic_temp.
# UNC(\\host\share)は exists()/resolve() を呼んだ時点で相手ホストへ資格情報付きで
# 接続してしまうため、パス操作より先に文字列のまま検査する(resolve_untrusted)。
allowed_dir = (Path(DATA_DIR) / "gmic_temp").resolve()
try:
Path(result_path).resolve().relative_to(allowed_dir)
resolved = resolve_untrusted(result_path)
resolved.relative_to(allowed_dir)
except ValueError:
return web.json_response({"error": "Access denied: path outside gmic_temp"}, status=403)
if not resolved.is_file():
return web.json_response({"error": f"Result file not found: {result_path}"}, status=404)
with open(result_path, "rb") as f:
with open(resolved, "rb") as f:
img_bytes = f.read()
ext = os.path.splitext(result_path)[1].lower()
+85
View File
@@ -0,0 +1,85 @@
"""Lab tab plan file management API routes."""
import asyncio
import logging
from aiohttp import web
from ..config import LAB_PLAN_DIR
from ..services.lab_plan_service import LabPlanService
logger = logging.getLogger(__name__)
_service = LabPlanService(LAB_PLAN_DIR)
def setup_routes(app: web.Application):
app.router.add_get("/api/wfm/lab/plans", handle_list)
app.router.add_get("/api/wfm/lab/plans/content", handle_get_content)
app.router.add_post("/api/wfm/lab/plans/save", handle_save)
app.router.add_post("/api/wfm/lab/plans/delete", handle_delete)
app.router.add_post("/api/wfm/lab/index-image/save-to-output", handle_save_index_to_output)
async def handle_list(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_service.list_plans)
return web.json_response(result)
except Exception as e:
logger.error("Error listing lab plans: %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:
data = await asyncio.to_thread(_service.get_plan, filename)
return web.json_response({"data": data})
except FileNotFoundError:
return web.json_response({"error": "not found"}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error getting lab plan 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", "")
data = body.get("data", {})
index_image_base64 = body.get("index_image_base64")
result = await asyncio.to_thread(_service.save_plan, filename, data, index_image_base64)
return web.json_response({"status": "ok", **result})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error saving lab plan: %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_plan, filename)
return web.json_response({"status": "ok"})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error deleting lab plan: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_save_index_to_output(request: web.Request) -> web.Response:
try:
body = await request.json()
image_base64 = body.get("image_base64", "")
if not image_base64:
return web.json_response({"error": "image_base64 required"}, status=400)
result = await asyncio.to_thread(_service.save_index_image_to_output, image_base64)
return web.json_response({"status": "ok", **result})
except Exception as e:
logger.error("Error saving lab index image to output: %s", e)
return web.json_response({"error": str(e)}, status=500)
+157 -38
View File
@@ -20,6 +20,7 @@ def setup_routes(app: web.Application):
app.router.add_get("/api/wfm/models/metadata", handle_get_metadata)
app.router.add_post("/api/wfm/models/metadata", handle_save_metadata)
app.router.add_get("/api/wfm/models/preview", handle_get_preview)
app.router.add_get("/api/wfm/models/preview-keys", handle_get_preview_keys)
app.router.add_get("/api/wfm/models/groups", handle_get_groups)
app.router.add_post("/api/wfm/models/groups", handle_save_groups)
app.router.add_post("/api/wfm/models/civitai/fetch", handle_civitai_fetch)
@@ -139,14 +140,20 @@ async def handle_civitai_fetch(request: web.Request) -> web.Response:
)
if not sha256:
return web.json_response({"error": "Failed to calculate hash"}, status=500)
# Cache the hash in model metadata
await asyncio.to_thread(
_service.update_metadata, model_name, {"sha256": sha256}
)
# Cache the hash (+ size/mtime so a replaced file invalidates it) in model metadata
try:
st = file_path.stat()
hash_meta = {"sha256": sha256, "sha256Size": st.st_size, "sha256Mtime": st.st_mtime}
except OSError:
hash_meta = {"sha256": sha256}
await asyncio.to_thread(_service.update_metadata, model_name, hash_meta)
# Fetch from CivitAI
info = await asyncio.to_thread(_civitai.fetch_by_hash, sha256)
if not info:
await asyncio.to_thread(
_service.update_metadata, model_name, {"civitaiNotFound": True}
)
return web.json_response({
"status": "not_found",
"sha256": sha256,
@@ -166,6 +173,10 @@ async def handle_civitai_fetch(request: web.Request) -> web.Response:
if preview_saved:
logger.info("Auto-saved preview for %s from CivitAI", model_name)
if model_meta.get("civitaiNotFound"):
await asyncio.to_thread(
_service.update_metadata, model_name, {"civitaiNotFound": False}
)
return web.json_response({
"status": "ok",
"sha256": sha256,
@@ -244,69 +255,159 @@ async def handle_civitai_batch(request: web.Request) -> web.Response:
def on_progress(current, total, model_name, status):
progress_q.put({"current": current, "total": total, "model": model_name, "status": status})
# 保存済みsha256(size/mtime付き)を渡して再ハッシュを避ける
import time as _time
all_meta0 = _service.get_all_metadata()
known = {}
for n, _p in model_files:
m = all_meta0.get(n) or {}
if m.get("sha256"):
known[n] = {"sha256": m["sha256"], "size": m.get("sha256Size"), "mtime": m.get("sha256Mtime")}
# 未計算のものは Lora Manager が保持済みのsha256を借りる(サイズ一致時のみ。ハッシュ計算を省く)
if len(known) < len(model_files):
from ..services.lora_manager_bridge import fetch_lm_hashes, _norm
# Hostヘッダは信用せず、自分自身のループバック+実際のポートへ接続する(Host偽装/DNSリバインディング対策)
sockname = request.transport.get_extra_info("sockname") if request.transport else None
port = sockname[1] if sockname else (request.url.port or 8188)
origin = f"http://127.0.0.1:{port}"
lm = await asyncio.to_thread(fetch_lm_hashes, origin, model_type)
for n, p in model_files:
if n in known:
continue
e = lm.get(_norm(p))
try:
if e and e.get("size") is not None and int(e["size"]) == p.stat().st_size:
known[n] = {"sha256": e["sha256"], "size": int(e["size"]), "mtime": None}
except OSError:
pass
# ハッシュ確定ごとに溜め、5秒おきにメタデータへ書き出す(途中で止めても計算済み分が残る)
pending_hashes = {}
last_flush = [_time.monotonic()]
def flush_hashes():
if not pending_hashes:
return
try:
all_meta = _service.get_all_metadata()
for mname, (sha, size, mtime) in list(pending_hashes.items()):
entry = all_meta.setdefault(mname, {"tags": [], "favorite": False, "memo": ""})
entry["sha256"] = sha
entry["sha256Size"] = size
entry["sha256Mtime"] = mtime
_service._save_metadata(all_meta)
pending_hashes.clear()
except Exception as e:
logger.warning("Failed to flush hashes: %s", e)
last_flush[0] = _time.monotonic()
def on_hash(name, sha, size, mtime):
pending_hashes[name] = (sha, size, mtime)
if _time.monotonic() - last_flush[0] > 5:
flush_hashes()
# Start batch in background thread
import threading
cancel = threading.Event()
loop = asyncio.get_event_loop()
fetch_task = loop.run_in_executor(None, _civitai.batch_fetch, model_files, on_progress)
fetch_task = loop.run_in_executor(
None, lambda: _civitai.batch_fetch(model_files, on_progress, known, on_hash, cancel=cancel)
)
# Stream progress events
import time as _time
try:
while not fetch_task.done():
# Drain queue
while not progress_q.empty():
try:
p = progress_q.get_nowait()
await response.write(send_sse("progress", p))
except queue.Empty:
break
await asyncio.sleep(0.1)
while not fetch_task.done():
# Drain queue
# Drain remaining events
while not progress_q.empty():
try:
p = progress_q.get_nowait()
await response.write(send_sse("progress", p))
except queue.Empty:
break
await asyncio.sleep(0.1)
# Drain remaining events
while not progress_q.empty():
except (ConnectionResetError, asyncio.CancelledError) as e:
# クライアント切断(再読み込み・タブ遷移): 重いハッシュ計算を打ち切り、計算済み分だけ保存して終了
logger.info("civitai batch: client disconnected, cancelling (%s)", type(e).__name__)
cancel.set()
try:
p = progress_q.get_nowait()
await response.write(send_sse("progress", p))
except queue.Empty:
break
await asyncio.shield(fetch_task)
except Exception:
pass
await asyncio.to_thread(flush_hashes)
if isinstance(e, asyncio.CancelledError):
raise
return response
# Get results and update metadata
results = fetch_task.result()
try:
results = fetch_task.result()
finally:
await asyncio.to_thread(flush_hashes)
meta_updates = {}
for model_name, result in results.items():
sha256 = result.get("sha256")
if sha256:
meta_updates[model_name] = sha256
# Bulk update sha256 in metadata
# Bulk update sha256 (+ CivitAI not-found marker) in metadata
if meta_updates:
all_meta = _service.get_all_metadata()
for mname, sha in meta_updates.items():
if mname not in all_meta:
all_meta[mname] = {"tags": [], "favorite": False, "memo": ""}
all_meta[mname]["sha256"] = sha
r = results[mname]
if r.get("civitai"):
all_meta[mname].pop("civitaiNotFound", None)
elif not r.get("fetch_failed") and not r.get("error"):
all_meta[mname]["civitaiNotFound"] = _service._now_iso()
_service._save_metadata(all_meta)
# Auto-set previews for models without preview image
# (ネットワークI/Oなのでイベントループを塞がないようスレッドで並列実行)
await response.write(send_sse("progress", {
"current": len(model_files), "total": len(model_files), "model": "", "status": "previews",
}))
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)
def _save_previews():
jobs = []
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
jobs.append((model_name, civitai["images"][0],
model_file.parent / (model_file.stem + ".preview.png")))
if not jobs:
return 0
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=2) as pool:
oks = list(pool.map(lambda j: CivitaiService.download_image(j[1], j[2]), jobs))
for (mname, _u, _p), ok in zip(jobs, oks):
if ok:
logger.info("Auto-saved preview for %s from CivitAI", mname)
return sum(1 for ok in oks if ok)
preview_saved_count = await asyncio.to_thread(_save_previews)
# Send final result
summary = {
@@ -314,6 +415,7 @@ async def handle_civitai_batch(request: web.Request) -> web.Response:
"found": sum(1 for r in results.values() if r.get("civitai")),
"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")),
"needs_hash": sum(1 for r in results.values() if r.get("needs_hash")),
"hashes": {name: r.get("sha256") for name, r in results.items() if r.get("sha256")},
"preview_saved": preview_saved_count,
}
@@ -325,6 +427,22 @@ async def handle_civitai_batch(request: web.Request) -> web.Response:
# ── Model Preview Image ───────────────────────────────────
async def handle_get_preview_keys(request: web.Request) -> web.Response:
"""GET /api/wfm/models/preview-keys?type=checkpoint
プレビュー画像を持つモデルのキー一覧を返す(無いモデルへの画像リクエスト=404を避けるため)。
"""
model_type = request.query.get("type", "")
if not model_type:
return web.json_response({"error": "type required"}, status=400)
try:
keys = await asyncio.to_thread(_service.list_preview_keys, model_type)
return web.json_response({"keys": keys})
except Exception as e:
logger.error("Error listing preview keys: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_get_preview(request: web.Request) -> web.Response:
"""GET /api/wfm/models/preview?type=checkpoint&name=model.safetensors"""
model_type = request.query.get("type", "")
@@ -632,7 +750,8 @@ async def handle_list_model_files(request: web.Request) -> web.Response:
if not model_type or model_type not in MODEL_TYPE_FOLDER_KEYS:
return web.json_response({"error": "invalid type"}, status=400)
try:
result = await asyncio.to_thread(_service.list_model_files, model_type)
extra_only = request.query.get("extra_only") == "1"
result = await asyncio.to_thread(_service.list_model_files, model_type, extra_only)
return web.json_response(result)
except Exception as e:
logger.error("Error listing model files for %s: %s", model_type, e)
+45
View File
@@ -15,6 +15,7 @@ _service = NodesService()
def setup_routes(app: web.Application):
"""Register all node management API routes."""
# Node metadata
app.router.add_get("/api/wfm/nodes/light", handle_get_light_nodes)
app.router.add_get("/api/wfm/nodes/metadata", handle_get_metadata)
app.router.add_post("/api/wfm/nodes/metadata", handle_save_metadata)
# Node groups
@@ -28,6 +29,50 @@ def setup_routes(app: web.Application):
app.router.add_get("/api/wfm/node-sets/export", handle_export_set)
# ── Light node list ────────────────────────────────────────
def _build_light_node_list() -> list[dict]:
"""全ノードの軽量情報(INPUT_TYPES を呼ばない)。
/object_info は各ノードの INPUT_TYPES() を呼ぶため、モデル一覧を走査するノードが
多い環境では十数秒かかる。Nodesタブの一覧表示に必要なのはクラス属性だけなので、
それらのみを読んで高速に返す(入力定義は呼び出し側が /object_info で後から補う)。
"""
import nodes as comfy_nodes
result = []
for name, cls in list(comfy_nodes.NODE_CLASS_MAPPINGS.items()):
try:
return_types = getattr(cls, "RETURN_TYPES", ()) or ()
result.append({
"name": name,
"display_name": comfy_nodes.NODE_DISPLAY_NAME_MAPPINGS.get(name, name),
"description": getattr(cls, "DESCRIPTION", "") or "",
"category": getattr(cls, "CATEGORY", "sd") or "sd",
"python_module": getattr(cls, "RELATIVE_PYTHON_MODULE", "nodes") or "nodes",
"output": list(return_types),
"output_name": list(getattr(cls, "RETURN_NAMES", None) or return_types),
"output_node": getattr(cls, "OUTPUT_NODE", False) is True,
"search_aliases": list(getattr(cls, "SEARCH_ALIASES", []) or []),
"deprecated": bool(getattr(cls, "DEPRECATED", False)),
"experimental": bool(getattr(cls, "EXPERIMENTAL", False)),
})
except Exception as e:
logger.debug("light node info failed for %s: %s", name, e)
return result
async def handle_get_light_nodes(request: web.Request) -> web.Response:
"""GET /api/wfm/nodes/light"""
try:
result = await asyncio.to_thread(_build_light_node_list)
return web.json_response(result)
except Exception as e:
logger.error("Error building light node list: %s", e)
return web.json_response({"error": str(e)}, status=500)
# ── Node Metadata ──────────────────────────────────────────
+9 -6
View File
@@ -7,6 +7,7 @@ import urllib.request
from aiohttp import web
from ..services.settings_service import SettingsService
from ..services.url_guard import require_backend_url, urlopen
logger = logging.getLogger(__name__)
@@ -39,11 +40,12 @@ async def handle_models(request: web.Request) -> web.Response:
import asyncio
try:
cfg = _get_ollama_config()
base_url = require_backend_url(cfg["url"])
def _fetch():
url = f"{cfg['url']}/api/tags"
url = f"{base_url}/api/tags"
req = urllib.request.Request(url)
with urllib.request.urlopen(req, timeout=5) as resp:
with urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode("utf-8"))
data = await asyncio.to_thread(_fetch)
@@ -61,11 +63,12 @@ async def handle_test(request: web.Request) -> web.Response:
import asyncio
try:
cfg = _get_ollama_config()
base_url = require_backend_url(cfg["url"])
def _test():
url = f"{cfg['url']}/api/tags"
url = f"{base_url}/api/tags"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=5) as resp:
with urlopen(req, timeout=5) as resp:
return resp.status == 200
ok = await asyncio.to_thread(_test)
@@ -83,7 +86,7 @@ async def handle_chat(request: web.Request) -> web.Response:
body = await request.json()
cfg = _get_ollama_config()
model = body.get("model") or cfg["model"]
base_url = (body.get("url") or cfg["url"]).rstrip("/")
base_url = require_backend_url(body.get("url") or cfg["url"])
messages = body.get("messages", [])
def _chat():
@@ -100,7 +103,7 @@ async def handle_chat(request: web.Request) -> web.Response:
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=120) as resp:
with urlopen(req, timeout=120) as resp:
return json.loads(resp.read().decode("utf-8"))
resp_data = await asyncio.to_thread(_chat)
+107
View File
@@ -0,0 +1,107 @@
"""Partner (API) node execution support for GenerateUI.
ComfyUI's partner API nodes (Nano Banana / Gemini, Kling, Seedream, ...) authenticate through the
``extra_data`` of the ``/prompt`` request: ``auth_token_comfy_org`` (the browser's Comfy login) or
``api_key_comfy_org`` (a Comfy API key issued at platform.comfy.org). ComfyUI's own web UI adds
the login token automatically, but Workflow Studio is a separate page that POSTs to ``/prompt`` itself,
so such a prompt fails with "Unauthorized: Please login first to use this node."
When COMFY_API_KEY is set (loaded from the plugin's .env by prestartup_script.py), the frontend
queues prompts through this proxy instead. The proxy adds the key to ``extra_data`` — only when the
prompt actually contains an API node (``api_node: true`` in /object_info) — and relays the request
to the local ComfyUI. The key never reaches the browser. Usage is billed as Comfy credits.
"""
import json
import logging
import os
from urllib.parse import quote
import aiohttp
from aiohttp import web
logger = logging.getLogger(__name__)
_api_node_cache: dict = {} # class_type -> bool (api_node flag); the node set only changes on restart
def _get_comfy_key():
"""Return the Comfy API key from the environment (.env), or None."""
return os.environ.get("COMFY_API_KEY", "").strip() or None
def _local_base_url() -> str:
"""Base URL of this ComfyUI instance (from its --listen / --port launch arguments)."""
try:
from comfy.cli_args import args as _a
port = getattr(_a, "port", None) or 8188
host = str(getattr(_a, "listen", "") or "127.0.0.1").split(",")[0].strip()
if host in ("", "0.0.0.0", "::"):
host = "127.0.0.1"
elif ":" in host:
host = f"[{host}]"
except Exception:
host, port = "127.0.0.1", 8188
return f"http://{host}:{port}"
async def _is_api_node(sess: aiohttp.ClientSession, base: str, class_type: str) -> bool:
if class_type in _api_node_cache:
return _api_node_cache[class_type]
for attempt in range(2): # /object_info occasionally drops the connection; one retry is enough
try:
async with sess.get(f"{base}/object_info/{quote(class_type, safe='')}") as r:
if r.status == 200:
info = await r.json(content_type=None)
flag = bool((info.get(class_type) or {}).get("api_node"))
_api_node_cache[class_type] = flag
return flag
break
except Exception:
if attempt == 1:
logger.warning("object_info lookup failed for %s", class_type)
return False
def setup_routes(app: web.Application):
app.router.add_get("/api/wfm/partner/status", handle_status)
app.router.add_post("/api/wfm/partner/prompt", handle_prompt)
async def handle_status(request: web.Request) -> web.Response:
"""GET /api/wfm/partner/status - whether a Comfy API key is configured (the key itself is never returned)."""
return web.json_response({"configured": _get_comfy_key() is not None})
async def handle_prompt(request: web.Request) -> web.Response:
"""POST /api/wfm/partner/prompt - Relay a /prompt request to this ComfyUI, adding the Comfy API key
to ``extra_data`` when the prompt contains an API node. The response is passed through unchanged."""
try:
body = await request.json()
except Exception:
return web.json_response({"error": {"message": "invalid JSON body"}}, status=400)
prompt = body.get("prompt")
if not isinstance(prompt, dict):
return web.json_response({"error": {"message": "prompt is required"}}, status=400)
base = _local_base_url()
key = _get_comfy_key()
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=120, connect=10)) as sess:
if key:
extra = body.get("extra_data")
extra = extra if isinstance(extra, dict) else {}
already = extra.get("auth_token_comfy_org") or extra.get("api_key_comfy_org")
if not already:
classes = {n.get("class_type") for n in prompt.values() if isinstance(n, dict) and n.get("class_type")}
for cls in classes:
if await _is_api_node(sess, base, cls):
extra["api_key_comfy_org"] = key
break
body["extra_data"] = extra
async with sess.post(f"{base}/prompt", json=body) as r:
data = await r.read()
return web.Response(status=r.status, body=data, content_type="application/json")
except Exception as e:
logger.error("partner prompt relay failed: %s", e)
return web.json_response({"error": {"message": f"relay failed: {e}"}}, status=502)
+467 -7
View File
@@ -1,14 +1,18 @@
"""Settings API routes."""
import asyncio
import io
import json
import logging
import shutil
import tempfile
import zipfile
from pathlib import Path
from aiohttp import web
from ..services.settings_service import SettingsService
from ..config import DEFAULT_WORKFLOWS_DIR, DATA_DIR
from ..config import DEFAULT_WORKFLOWS_DIR, DATA_DIR, VIDEO_EDIT_PROJECT_DIR, is_safe_rel
logger = logging.getLogger(__name__)
@@ -24,6 +28,7 @@ _DATA_FILES = [
"model_metadata.json",
"gallery_metadata.json",
"tagger_settings.json",
"civitai_cache.json",
]
@@ -33,11 +38,18 @@ def setup_routes(app: web.Application):
app.router.add_post("/api/wfm/settings", handle_post)
app.router.add_get("/api/wfm/settings/workflows-dir", handle_get_workflows_dir)
app.router.add_post("/api/wfm/settings/workflows-dir", handle_set_workflows_dir)
app.router.add_get("/api/wfm/settings/models-dir", handle_get_models_dir)
app.router.add_post("/api/wfm/settings/models-dir", handle_set_models_dir)
app.router.add_get("/api/wfm/settings/output-dir", handle_get_output_dir)
app.router.add_post("/api/wfm/settings/output-dir", handle_set_output_dir)
app.router.add_get("/api/wfm/settings/export", handle_export)
app.router.add_post("/api/wfm/settings/import", handle_import)
app.router.add_get("/api/wfm/settings/export-full", handle_export_full)
app.router.add_post("/api/wfm/settings/import-full", handle_import_full)
app.router.add_get("/api/wfm/styles", handle_get_styles)
app.router.add_post("/api/wfm/styles", handle_create_style)
app.router.add_put("/api/wfm/styles/{name}", handle_update_style)
app.router.add_delete("/api/wfm/styles/{name}", handle_delete_style)
async def handle_get(request: web.Request) -> web.Response:
@@ -112,6 +124,47 @@ async def handle_set_workflows_dir(request: web.Request) -> web.Response:
return web.json_response({"error": str(e)}, status=500)
async def handle_get_models_dir(request: web.Request) -> web.Response:
"""GET /api/wfm/settings/models-dir - 追加モデルフォルダ設定と現在有効な探索先を返す。"""
try:
from ..services.models_service import (
MODEL_TYPE_FOLDER_KEYS, _get_model_dirs, parse_extra_model_roots,
)
saved = (await asyncio.to_thread(_service.load)).get("models_dir", "")
effective = await asyncio.to_thread(
lambda: {t: [str(d) for d in _get_model_dirs(t)] for t in MODEL_TYPE_FOLDER_KEYS}
)
return web.json_response({
"saved": saved,
"roots": parse_extra_model_roots(saved),
"effective": effective,
})
except Exception as e:
logger.error("Error getting models dir: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_set_models_dir(request: web.Request) -> web.Response:
"""POST /api/wfm/settings/models-dir - 追加モデルフォルダを保存(`;`/改行区切りで複数可、空でリセット)。"""
try:
from ..services.models_service import parse_extra_model_roots
body = await request.json()
roots = parse_extra_model_roots(body.get("models_dir", ""))
missing = [r for r in roots if not Path(r).is_dir()]
if missing:
return web.json_response(
{"error": f"Folder not found: {', '.join(missing)}"}, status=400)
saved = ";".join(roots)
await asyncio.to_thread(_service.update, {"models_dir": saved})
from ..routes.models_routes import _service as models_service
models_service._scan_cache.clear()
return await handle_get_models_dir(request)
except Exception as e:
logger.error("Error setting models dir: %s", e)
return web.json_response({"error": str(e)}, status=500)
def _get_comfyui_output_dir() -> str:
"""ComfyUIのデフォルトoutputフォルダを取得する。"""
try:
@@ -245,18 +298,289 @@ async def handle_import(request: web.Request) -> web.Response:
return web.json_response({"error": str(e)}, status=500)
def _load_styles() -> list:
"""Load all style JSON files from DATA_DIR/style/ directory."""
# Full-backup (ZIP) export/import: unlike the flat _DATA_FILES JSON bundle above, this
# covers the whole DATA_DIR — directory-shaped data (ai_skills/, lab_plan/, video_plan/,
# style/) and the SQLite tagger.db that the JSON bundle can't represent. Excludes transient/cache
# dirs, and wildcard/ specifically because it's typically a symlink into a *different*
# custom_node's folder (comfyui-impact-pack) rather than this plugin's own data.
_FULL_BACKUP_EXCLUDE_NAMES = {"gmic_temp", "thumb_cache", "wildcard"}
def _add_file_to_zip(zf: zipfile.ZipFile, path: Path, base: Path):
rel = str(path.relative_to(base)).replace("\\", "/")
if path.name == "settings.json":
# Same API-key scrubbing as the JSON-bundle export — fall through to a raw
# copy if the file turns out to be unreadable/corrupt rather than skipping it.
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
data = {k: v for k, v in data.items() if k not in _SETTINGS_EXPORT_EXCLUDE}
zf.writestr(rel, json.dumps(data, ensure_ascii=False, indent=2))
return
except Exception:
pass
zf.write(path, rel)
# Two folders this plugin doesn't own but users often want backed up alongside their
# own data: ComfyUI's own default workflows dir, and the Impact Pack's wildcard
# library (a separate custom_node — the plugin's own wildcard/ is usually just a
# symlink into this same folder, see _FULL_BACKUP_EXCLUDE_NAMES above). Opt-in only
# (see handle_export_full's include_workflows/include_wildcard query params), and
# stored under an "_external/" prefix so _apply_full_backup_zip can recognize and
# skip them on restore — see that function's docstring for why.
def _default_workflows_dir() -> Path:
from ..config import COMFYUI_ROOT
return COMFYUI_ROOT / "user" / "default" / "workflows"
def _impact_pack_wildcard_dir() -> Path:
from ..config import COMFYUI_ROOT
return COMFYUI_ROOT / "custom_nodes" / "comfyui-impact-pack" / "wildcards"
def _add_external_dir_to_zip(zf: zipfile.ZipFile, src_dir: Path, zip_prefix: str):
if not src_dir.is_dir() or src_dir.is_symlink():
return
for path in src_dir.rglob("*"):
if path.is_dir() or path.is_symlink():
continue
rel = f"{zip_prefix}/{path.relative_to(src_dir)}".replace("\\", "/")
zf.write(path, rel)
# Video Edit projects (video_edit_project/*.json, inside DATA_DIR so always backed up) only
# store *references* — serverRef {filename, subfolder, type:"input"} — to clips, the BGM,
# timeline sounds and overlay (PinP) clips uploaded into ComfyUI's input dir, not the media itself. Backing up just the JSON means a
# restored project on another machine (or after input/ is cleaned up) opens with its clips
# missing. Opt-in (include_video_media) because the media can be large; stored under its own
# prefix so _apply_full_backup_zip can route it back into the input dir, not DATA_DIR.
_VIDEO_MEDIA_PREFIX = "_video_edit_media/"
def _comfyui_input_dir() -> Path:
import folder_paths
return Path(folder_paths.get_input_directory())
def _iter_video_edit_media_refs():
"""Yields (subfolder, filename) for every input-dir file referenced by a saved Video
Edit project (clips + BGM + timeline sounds + overlay clips). Unreadable project files
are skipped."""
if not VIDEO_EDIT_PROJECT_DIR.is_dir():
return
for path in sorted(VIDEO_EDIT_PROJECT_DIR.glob("*.json")):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
continue
# "sounds" / "pips" are top-level lists of {name, serverRef, ...} (absent in older projects).
refs = [
c.get("serverRef")
for key in ("clips", "sounds", "pips")
for c in data.get(key) or []
if isinstance(c, dict)
]
bgm = (data.get("audio") or {}).get("bgm")
if isinstance(bgm, dict):
refs.append(bgm.get("serverRef"))
for ref in refs:
if not isinstance(ref, dict) or not ref.get("filename"):
continue
if (ref.get("type") or "input") != "input":
continue
yield (ref.get("subfolder") or "").strip("/\\"), ref["filename"]
def _add_video_edit_media_to_zip(zf: zipfile.ZipFile) -> None:
input_dir = _comfyui_input_dir().resolve()
seen = set()
for subfolder, filename in _iter_video_edit_media_refs():
# プロジェクトファイルは細工できるため、UNC・絶対パス・「..」は結合・resolve() の前に
# 文字列のまま拒否する(Windows では UNC を resolve() するだけで相手ホストへ接続する)
if not is_safe_rel(filename) or (subfolder and not is_safe_rel(subfolder)):
continue
src = (input_dir / subfolder / filename).resolve()
try:
rel = src.relative_to(input_dir) # a crafted project file must not reach outside input/
except ValueError:
continue
if rel in seen or not src.is_file():
continue
seen.add(rel)
zf.write(src, _VIDEO_MEDIA_PREFIX + str(rel).replace("\\", "/"))
def _build_full_backup_zip(include_workflows: bool = False, include_wildcard: bool = False,
include_video_media: bool = False) -> bytes:
"""Zips everything under DATA_DIR except the excluded names above. Walks
top-level entries first so an excluded symlinked directory (wildcard/) is never
even opened, rather than filtering after a recursive walk has already followed it.
Optionally also includes the two external folders above under "_external/"."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
for top in sorted(DATA_DIR.iterdir()):
if top.name in _FULL_BACKUP_EXCLUDE_NAMES or top.is_symlink():
continue
if top.is_file():
_add_file_to_zip(zf, top, DATA_DIR)
elif top.is_dir():
for path in top.rglob("*"):
if path.is_dir() or path.is_symlink():
continue
_add_file_to_zip(zf, path, DATA_DIR)
if include_workflows:
_add_external_dir_to_zip(zf, _default_workflows_dir(), "_external/default_workflows")
if include_wildcard:
_add_external_dir_to_zip(zf, _impact_pack_wildcard_dir(), "_external/wildcard")
if include_video_media:
_add_video_edit_media_to_zip(zf)
return buf.getvalue()
def _apply_full_backup_zip(zip_source) -> dict:
"""Extracts a full-backup ZIP into DATA_DIR, overwriting existing files. Guards
against Zip Slip (entries whose path resolves outside DATA_DIR via '..' or an
absolute path) by checking each entry before writing it.
Entries under "_external/" (default workflows dir, Impact Pack wildcards — see
_build_full_backup_zip) are always skipped here rather than restored: they live
outside DATA_DIR and are managed by ComfyUI core / a different custom_node, so
auto-restoring them could silently overwrite files this plugin doesn't own. They
stay in the ZIP for the user to copy back manually if they want them.
Entries under "_video_edit_media/" (media referenced by Video Edit projects) are restored
into ComfyUI's input dir instead, but never overwrite an existing file there — a file
with the same name is assumed to be the same upload (ComfyUI dedupes uploads by renaming),
and input/ is shared with every other workflow."""
summary = {"extracted": [], "skipped": [], "media_restored": [], "media_existing": []}
DATA_DIR.mkdir(parents=True, exist_ok=True)
base = DATA_DIR.resolve()
input_base = None
# zip_source: raw bytes, or a seekable file object (the upload handler spools to a temp file).
if isinstance(zip_source, (bytes, bytearray)):
zip_source = io.BytesIO(zip_source)
with zipfile.ZipFile(zip_source) as zf:
for info in zf.infolist():
if info.is_dir():
continue
if info.filename.startswith("_external/"):
summary["skipped"].append(info.filename)
continue
if info.filename.startswith(_VIDEO_MEDIA_PREFIX):
media_rel = info.filename[len(_VIDEO_MEDIA_PREFIX):]
# ZIP エントリ名は細工できるため、UNC・絶対パス・「..」は結合・resolve() の前に
# 文字列のまま拒否する(Windows では UNC を resolve() するだけで相手ホストへ接続する)
if not is_safe_rel(media_rel):
summary["skipped"].append(info.filename)
continue
if input_base is None:
input_base = _comfyui_input_dir().resolve()
target = (input_base / media_rel).resolve()
try:
target.relative_to(input_base)
except ValueError:
summary["skipped"].append(info.filename)
continue
if target.exists():
summary["media_existing"].append(info.filename)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zf.open(info) as src, open(target, "wb") as dst:
shutil.copyfileobj(src, dst)
summary["media_restored"].append(info.filename)
continue
if not is_safe_rel(info.filename):
summary["skipped"].append(info.filename)
continue
target = (base / info.filename).resolve()
try:
target.relative_to(base)
except ValueError:
summary["skipped"].append(info.filename)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zf.open(info) as src, open(target, "wb") as dst:
shutil.copyfileobj(src, dst)
summary["extracted"].append(info.filename)
return summary
async def handle_export_full(request: web.Request) -> web.Response:
"""GET /api/wfm/settings/export-full - Download a ZIP of the entire data
directory, including data the flat JSON bundle (handle_export) can't represent.
Optional query params include_workflows=1 / include_wildcard=1 additionally bundle
ComfyUI's default workflows dir / the Impact Pack's wildcard dir (see
_build_full_backup_zip) — both restore-excluded, export-only, manual-copy-back.
include_video_media=1 adds the input-dir media referenced by Video Edit projects,
which *is* restored (into input/, without overwriting)."""
try:
include_workflows = request.rel_url.query.get("include_workflows") == "1"
include_wildcard = request.rel_url.query.get("include_wildcard") == "1"
include_video_media = request.rel_url.query.get("include_video_media") == "1"
zip_bytes = await asyncio.to_thread(
_build_full_backup_zip, include_workflows, include_wildcard, include_video_media)
return web.Response(
body=zip_bytes,
content_type="application/zip",
headers={"Content-Disposition": 'attachment; filename="wfm-full-backup.zip"'},
)
except Exception as e:
logger.error("Error building full backup: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_import_full(request: web.Request) -> web.Response:
"""POST /api/wfm/settings/import-full - Upload and restore a full-backup ZIP."""
try:
reader = await request.multipart()
field = await reader.next()
if field is None or field.name != "file":
return web.json_response({"error": "file field required"}, status=400)
# Stream to a temp file with read_chunk() instead of field.read(): aiohttp (3.14+)
# enforces ComfyUI's --max-upload-size (default 100 MB) inside BodyPartReader.read(),
# and a backup with the opt-in workflows/wildcard/Video Edit media easily exceeds it
# ("Request Entity Too Large"). Also avoids holding the whole ZIP in memory.
with tempfile.TemporaryFile() as tmp:
while True:
chunk = await field.read_chunk(1024 * 1024)
if not chunk:
break
await asyncio.to_thread(tmp.write, chunk)
tmp.seek(0)
summary = await asyncio.to_thread(_apply_full_backup_zip, tmp)
return web.json_response({"status": "ok", **summary})
except zipfile.BadZipFile:
return web.json_response({"error": "Not a valid ZIP file"}, status=400)
except Exception as e:
logger.error("Error importing full backup: %s", e)
return web.json_response({"error": str(e)}, status=500)
def _style_files() -> list:
style_dir = DATA_DIR / "style"
styles = []
if not style_dir.is_dir():
return styles
for json_file in sorted(style_dir.glob("*.json")):
return []
return sorted(style_dir.glob("*.json"))
def _load_styles() -> list:
"""Load all style JSON files from DATA_DIR/style/ directory.
各エントリに定義元ファイル名を "file" として付与する(Promptタブの一覧表示・
「このファイルへ追加」機能で使用)。"""
styles = []
for json_file in _style_files():
try:
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
styles.extend(data)
for entry in data:
if isinstance(entry, dict):
entry = dict(entry)
entry["file"] = json_file.name
styles.append(entry)
except Exception as e:
logger.warning("Failed to load style file %s: %s", json_file.name, e)
return styles
@@ -270,3 +594,139 @@ async def handle_get_styles(request: web.Request) -> web.Response:
except Exception as e:
logger.error("Error loading styles: %s", e)
return web.json_response({"error": str(e)}, status=500)
# Promptタブ「Style」サブタブでの新規作成分の保存先。既存スタイルの編集は
# 元々そのスタイルが記載されているファイルをそのまま書き換える(他ファイルへは移動しない)。
_CUSTOM_STYLE_FILENAME = "custom.json"
def _find_style(name: str):
"""名前が一致する最初のスタイルを探し、(ファイルパス, そのファイルの全データ, インデックス) を返す。
見つからなければ None。"""
for json_file in _style_files():
try:
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
logger.warning("Failed to load style file %s: %s", json_file.name, e)
continue
if not isinstance(data, list):
continue
for i, entry in enumerate(data):
if isinstance(entry, dict) and entry.get("name") == name:
return json_file, data, i
return None
def _write_style_file(path: Path, data: list) -> None:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
async def handle_create_style(request: web.Request) -> web.Response:
"""POST /api/wfm/styles - 新規スタイルを作成する。
body.file を指定すると既存のそのファイルへ追記する(Promptタブの「このファイルへ追加」用、
ファイル名はstyleディレクトリに実在するもののみ許可しパストラバーサルを防ぐ)。
未指定ならデフォルトのcustom.jsonへ追記する。"""
try:
body = await request.json()
name = (body.get("name") or "").strip()
prompt = body.get("prompt", "") or ""
negative_prompt = body.get("negative_prompt", "") or ""
target_file = (body.get("file") or "").strip()
if not name:
return web.json_response({"error": "name is required"}, status=400)
def _create():
if _find_style(name) is not None:
return "duplicate"
style_dir = DATA_DIR / "style"
style_dir.mkdir(parents=True, exist_ok=True)
if target_file:
path = next((f for f in _style_files() if f.name == target_file), None)
if path is None:
return "file_not_found"
else:
path = style_dir / _CUSTOM_STYLE_FILENAME
data = []
if path.is_file():
try:
with open(path, "r", encoding="utf-8") as f:
loaded = json.load(f)
if isinstance(loaded, list):
data = loaded
except Exception as e:
logger.warning("Failed to load %s, recreating: %s", path.name, e)
data.append({"name": name, "prompt": prompt, "negative_prompt": negative_prompt})
_write_style_file(path, data)
return "ok"
result = await asyncio.to_thread(_create)
if result == "duplicate":
return web.json_response({"error": f'Style "{name}" already exists'}, status=400)
if result == "file_not_found":
return web.json_response({"error": f'File "{target_file}" not found'}, status=400)
return web.json_response({"ok": True})
except Exception as e:
logger.error("Error creating style: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_update_style(request: web.Request) -> web.Response:
"""PUT /api/wfm/styles/{name} - 既存スタイルを更新する(記載元のファイルをそのまま書き換え、リネームも可)。"""
try:
original_name = request.match_info.get("name", "")
body = await request.json()
new_name = (body.get("name") or "").strip()
prompt = body.get("prompt", "") or ""
negative_prompt = body.get("negative_prompt", "") or ""
if not new_name:
return web.json_response({"error": "name is required"}, status=400)
def _update():
found = _find_style(original_name)
if found is None:
return "not_found"
path, data, idx = found
if new_name != original_name and _find_style(new_name) is not None:
return "duplicate"
data[idx] = {"name": new_name, "prompt": prompt, "negative_prompt": negative_prompt}
_write_style_file(path, data)
return "ok"
result = await asyncio.to_thread(_update)
if result == "not_found":
return web.json_response({"error": "Style not found"}, status=404)
if result == "duplicate":
return web.json_response({"error": f'Style "{new_name}" already exists'}, status=400)
return web.json_response({"ok": True})
except Exception as e:
logger.error("Error updating style: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_delete_style(request: web.Request) -> web.Response:
"""DELETE /api/wfm/styles/{name} - スタイルを削除する(記載元のファイルから該当エントリのみ除去)。"""
try:
name = request.match_info.get("name", "")
def _delete():
found = _find_style(name)
if found is None:
return False
path, data, idx = found
del data[idx]
_write_style_file(path, data)
return True
ok = await asyncio.to_thread(_delete)
if not ok:
return web.json_response({"error": "Style not found"}, status=404)
return web.json_response({"ok": True})
except Exception as e:
logger.error("Error deleting style: %s", e)
return web.json_response({"error": str(e)}, status=500)
+116
View File
@@ -0,0 +1,116 @@
"""AI skill (.md system prompt) file management API routes."""
import asyncio
import logging
from aiohttp import web
from ..config import AI_SKILLS_DIR
from ..services.skill_service import SkillService
logger = logging.getLogger(__name__)
_service = SkillService(AI_SKILLS_DIR)
def setup_routes(app: web.Application):
app.router.add_get("/api/wfm/skills", handle_list)
app.router.add_get("/api/wfm/skills/content", handle_get_content)
app.router.add_post("/api/wfm/skills/save", handle_save)
app.router.add_post("/api/wfm/skills/delete", handle_delete)
app.router.add_post("/api/wfm/skills/restore-builtin", handle_restore_builtin)
app.router.add_post("/api/wfm/skills/import", handle_import)
app.router.add_get("/api/wfm/skills/resource", handle_get_resource)
async def handle_list(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_service.list_skills)
return web.json_response(result)
except Exception as e:
logger.error("Error listing skills: %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 skill 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 skill: %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 skill: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_restore_builtin(request: web.Request) -> web.Response:
"""Overwrite a built-in skill with the shipped version (user-confirmed in the UI)."""
try:
body = await request.json()
filename = body.get("filename", "")
result = await asyncio.to_thread(_service.restore_builtin, filename)
return web.json_response({"status": "ok", "file": result})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error restoring built-in skill: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_import(request: web.Request) -> web.Response:
"""Import an Agent Skills folder (.zip) or a single .md skill (multipart field "file")."""
try:
form = await request.post()
upload = form.get("file")
if upload is None or not hasattr(upload, "file"):
return web.json_response({"error": "no file"}, status=400)
data = upload.file.read(20 * 1024 * 1024 + 1)
if len(data) > 20 * 1024 * 1024:
return web.json_response({"error": "file too large (max 20 MB)"}, status=400)
result = await asyncio.to_thread(_service.import_skill, upload.filename or "", data)
return web.json_response({"status": "ok", **result})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error importing skill: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_get_resource(request: web.Request) -> web.Response:
"""Read one reference file of a folder skill (?skill=<folder>&path=<relative path>)."""
folder = request.rel_url.query.get("skill", "")
rel = request.rel_url.query.get("path", "")
try:
content = await asyncio.to_thread(_service.get_resource, folder, rel)
return web.json_response({"content": content})
except FileNotFoundError:
return web.json_response({"error": "not found"}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error reading skill resource: %s", e)
return web.json_response({"error": str(e)}, status=500)
+36 -7
View File
@@ -18,8 +18,8 @@ _db = TaggerDbService(TAGGER_DB_FILE)
def setup_routes(app: web.Application):
app.router.add_get("/wfm/tagger/models", handle_models)
app.router.add_post("/wfm/tagger/predict", handle_predict)
app.router.add_get("/wfm/tagger/ollama/models", handle_ollama_models)
app.router.add_post("/wfm/tagger/ollama/predict", handle_ollama_predict)
app.router.add_get("/wfm/tagger/vlm/models", handle_vlm_models)
app.router.add_post("/wfm/tagger/vlm/predict", handle_vlm_predict)
app.router.add_post("/wfm/tagger/batch/start", handle_batch_start)
app.router.add_get("/wfm/tagger/batch/status", handle_batch_status)
app.router.add_post("/wfm/tagger/batch/stop", handle_batch_stop)
@@ -27,6 +27,7 @@ def setup_routes(app: web.Application):
app.router.add_get("/wfm/tagger/db/search", handle_db_search)
app.router.add_put("/wfm/tagger/db/{id}", handle_db_update)
app.router.add_delete("/wfm/tagger/db/{id}", handle_db_delete)
app.router.add_delete("/wfm/tagger/db", handle_db_delete_bulk)
app.router.add_get("/wfm/tagger/db/export", handle_db_export)
app.router.add_post("/wfm/tagger/db/save", handle_db_save)
app.router.add_post("/wfm/tagger/write_meta", handle_write_meta)
@@ -61,25 +62,29 @@ async def handle_predict(request: web.Request) -> web.Response:
return web.json_response({"error": str(e)}, status=500)
async def handle_ollama_models(request: web.Request) -> web.Response:
async def handle_vlm_models(request: web.Request) -> web.Response:
backend = request.rel_url.query.get("backend", "ollama")
api_url = request.rel_url.query.get("api_url", "http://127.0.0.1:11434")
try:
models = await asyncio.to_thread(_svc.ollama_models, api_url)
models = await asyncio.to_thread(_svc.vlm_models, backend, api_url)
return web.json_response({"models": models})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
async def handle_ollama_predict(request: web.Request) -> web.Response:
async def handle_vlm_predict(request: web.Request) -> web.Response:
try:
body = await request.json()
result = await asyncio.to_thread(
_svc.ollama_predict,
_svc.vlm_predict,
body.get("image_b64", ""),
body.get("backend", "ollama"),
body.get("api_url", "http://127.0.0.1:11434"),
body.get("model", ""),
body.get("prompt", ""),
int(body.get("max_tags", 40)),
bool(body.get("thinking_mode", False)),
int(body.get("max_tokens", 0)),
)
return web.json_response(result)
except Exception as e:
@@ -96,7 +101,8 @@ async def handle_batch_start(request: web.Request) -> web.Response:
float(body.get("threshold", 0.35)),
float(body.get("char_threshold", 0.85)),
bool(body.get("use_ollama", False)),
body.get("ollama_api", "http://127.0.0.1:11434"),
body.get("vlm_backend", "ollama"),
body.get("vlm_api_url", "http://127.0.0.1:11434"),
body.get("ollama_model", ""),
body.get("ollama_prompt", ""),
int(body.get("ollama_max_tags", 40)),
@@ -104,6 +110,8 @@ async def handle_batch_start(request: web.Request) -> web.Response:
bool(body.get("write_file", False)),
bool(body.get("write_txt", False)),
_db,
bool(body.get("thinking_mode", False)),
int(body.get("max_tokens", 0)),
)
return web.json_response(result)
except Exception as e:
@@ -161,6 +169,27 @@ async def handle_db_delete(request: web.Request) -> web.Response:
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
async def handle_db_delete_bulk(request: web.Request) -> web.Response:
try:
data = await request.json()
ids = data.get("ids", [])
if not ids:
return web.json_response(
{"error": "No IDs specified"},
status=400,
)
ids = [int(row_id) for row_id in ids]
deleted_count = await asyncio.to_thread(_db.delete_bulk, ids)
return web.json_response({
"ok": True,
"count": deleted_count,
})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
async def handle_db_export(request: web.Request) -> web.Response:
try:
+83
View File
@@ -0,0 +1,83 @@
"""
TagSpace Routes - タグ/ImagePromptカード/Gallery画像を3D潜在空間に並べるタブ用API
"""
import asyncio
import logging
from aiohttp import web
from ..config import DATA_DIR
from ..services.tagspace_service import TagSpaceService, DEFAULT_MAX_TAGS, DEFAULT_BG, MAX_TAGS_LIMIT, MIN_COUNT_LIMIT
from ..services.semantic_service import SemanticError
from .gallery_routes import _service as _gallery, _semantic, _semantic_error, _num, _json_body
from .tagger_routes import _svc as _tagger
logger = logging.getLogger(__name__)
_tagspace = TagSpaceService(DATA_DIR, _gallery, _semantic, _tagger)
def setup_routes(app: web.Application):
app.router.add_get("/wfm/tagspace/status", tagspace_status)
app.router.add_get("/wfm/tagspace/space", tagspace_space)
app.router.add_post("/wfm/tagspace/build", tagspace_build)
app.router.add_post("/wfm/tagspace/cancel", tagspace_cancel)
app.router.add_post("/wfm/tagspace/search", tagspace_search)
app.router.add_post("/wfm/tagspace/neighbors", tagspace_neighbors)
async def _call(fn, *args) -> web.Response:
try:
return web.json_response(await asyncio.to_thread(fn, *args))
except SemanticError as e:
return _semantic_error(e)
except Exception as e:
logger.error("tagspace %s error: %s", getattr(fn, "__name__", fn), e)
return web.json_response({"error": str(e)}, status=500)
async def tagspace_status(request: web.Request) -> web.Response:
return await _call(_tagspace.status)
async def tagspace_space(request: web.Request) -> web.Response:
return await _call(_tagspace.space)
async def tagspace_build(request: web.Request) -> web.Response:
body = await _json_body(request)
try:
# max_tags=0 は全タグ(サービス側で ALL_TAGS_LIMIT まで)
max_tags = _num(body, "max_tags", DEFAULT_MAX_TAGS, 0, MAX_TAGS_LIMIT)
bg = _num(body, "background", DEFAULT_BG, 0, 20000)
min_count = _num(body, "min_count", 1, 1, MIN_COUNT_LIMIT)
except SemanticError as e:
return _semantic_error(e)
tagger_model = str(body.get("tagger_model", "") or "")
tagger_scope = str(body.get("tagger_scope", "background") or "background")
wildcard = body.get("wildcard", True) is not False
return await _call(lambda: (_tagspace.start_build(max_tags, bg, tagger_model, tagger_scope, min_count, wildcard),
{"ok": True})[1])
async def tagspace_cancel(request: web.Request) -> web.Response:
_tagspace.cancel()
return web.json_response({"ok": True})
async def tagspace_search(request: web.Request) -> web.Response:
body = await _json_body(request)
try:
k = _num(body, "k", 20, 1, 200)
except SemanticError as e:
return _semantic_error(e)
return await _call(_tagspace.search, str(body.get("query", "")), k)
async def tagspace_neighbors(request: web.Request) -> web.Response:
body = await _json_body(request)
try:
k = _num(body, "k", 20, 1, 200)
except SemanticError as e:
return _semantic_error(e)
return await _call(_tagspace.neighbors, str(body.get("id", "")), k)
+104
View File
@@ -0,0 +1,104 @@
"""Unsloth API proxy routes.
Unlike Ollama/LM Studio/Lemonade, Unsloth Desktop requires an API key even
for local access (Authorization: Bearer sk-unsloth-...). The key lives in
UNSLOTH_API_KEY (loaded from a .env file by prestartup_script.py) and is
never sent to the frontend — the frontend calls this proxy with the target
path/method/payload, and the server attaches the Authorization header before
relaying to Unsloth's OpenAI-compatible API — and, via the same relay, to its
Decision API (/v1/systemone, TypeSafe-compatible Laya decision models; see
static/js/decision-client.js).
The key is optional: when UNSLOTH_API_KEY is unset the request is relayed
without an Authorization header, which works when Unsloth Desktop has
Keyless API access → "Chat and inference" turned on (localhost / private LAN).
"""
import json
import logging
import os
import urllib.request
from urllib.error import HTTPError, URLError
from aiohttp import web
from ..services.url_guard import HINT as _URL_HINT, is_allowed_backend_url, urlopen as _guarded_urlopen
logger = logging.getLogger(__name__)
UNSLOTH_DEFAULT_URL = "http://localhost:8888"
_ALLOWED_PATHS = {"/v1/models", "/v1/chat/completions", "/v1/systemone"}
# baseUrl is client-supplied (so a custom Unsloth port works), but the
# Authorization header carries a real secret — restrict the host (loopback, or
# WFS_ALLOWED_BACKEND_HOSTS set by the operator) so this proxy can't be used to
# exfiltrate UNSLOTH_API_KEY to an arbitrary server (SSRF). Redirects are not followed.
def _get_api_key():
"""Return the Unsloth API key from the environment (.env), or None."""
return os.environ.get("UNSLOTH_API_KEY", "").strip() or None
# Shown when a keyless request is rejected — also used by the Tagger's Unsloth VLM path.
KEYLESS_REJECTED_MESSAGE = (
"Unsloth rejected the request without an API key (HTTP 401). Either set UNSLOTH_API_KEY "
"in the plugin's .env (copy .env.example) and restart ComfyUI, or turn on Keyless API "
"access -> \"Chat and inference\" in Unsloth Desktop's Settings -> API."
)
def setup_routes(app: web.Application):
"""Register Unsloth API routes."""
app.router.add_post("/api/wfm/unsloth/proxy", handle_proxy)
async def handle_proxy(request: web.Request) -> web.Response:
"""POST /api/wfm/unsloth/proxy - Relay a request to Unsloth's OpenAI-compatible / Decision API.
Body: { baseUrl, path: "/v1/models" | "/v1/chat/completions" | "/v1/systemone",
method: "GET" | "POST", payload }
"""
import asyncio
try:
body = await request.json()
base_url = (body.get("baseUrl") or UNSLOTH_DEFAULT_URL).rstrip("/")
path = body.get("path") or "/v1/models"
method = (body.get("method") or "GET").upper()
payload = body.get("payload")
if path not in _ALLOWED_PATHS or method not in ("GET", "POST"):
return web.json_response({"message": "Unsupported proxy target"}, status=400)
if not is_allowed_backend_url(base_url):
return web.json_response({"message": _URL_HINT}, status=400)
# No key → relay without Authorization (Unsloth's Keyless API access); if Unsloth
# still demands one, the 401 below explains both ways to fix it.
api_key = _get_api_key()
def _fetch():
data = json.dumps(payload).encode("utf-8") if payload is not None else None
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(f"{base_url}{path}", data=data, headers=headers, method=method)
with _guarded_urlopen(req, timeout=120) as resp:
return json.loads(resp.read().decode("utf-8"))
data = await asyncio.to_thread(_fetch)
return web.json_response(data)
except HTTPError as e:
detail = e.read().decode("utf-8", errors="replace") if e.fp else str(e)
logger.warning("Unsloth proxy HTTP error: %s %s", e.code, detail)
if e.code == 401 and not _get_api_key():
return web.json_response({"message": KEYLESS_REJECTED_MESSAGE}, status=401)
# Include a short excerpt of Unsloth's own error body — Decision API schema errors
# (e.g. a malformed question) are otherwise impossible to diagnose from the frontend.
return web.json_response({"message": f"Unsloth API error: HTTP {e.code} {detail[:300]}".strip()}, status=e.code)
except URLError as e:
logger.warning("Unsloth proxy connection error: %s", e)
return web.json_response({"message": f"Could not reach Unsloth: {e.reason}"}, status=502)
except Exception as e:
logger.error("Unsloth proxy error: %s", e)
return web.json_response({"message": str(e)}, status=500)
+280
View File
@@ -0,0 +1,280 @@
"""Video tab API routes: last-frame capture, animated GIF conversion, Edit
tab probe/text-overlay burn-in, and Plan/Edit project file management."""
import asyncio
import logging
from aiohttp import web
from ..config import VIDEO_PLAN_DIR, VIDEO_EDIT_PROJECT_DIR
from ..services.video_service import VideoService
from ..services.video_plan_service import VideoPlanService
from ..services.video_edit_project_service import VideoEditProjectService
logger = logging.getLogger(__name__)
_service = VideoService()
_plan_service = VideoPlanService(VIDEO_PLAN_DIR)
_edit_project_service = VideoEditProjectService(VIDEO_EDIT_PROJECT_DIR)
def setup_routes(app: web.Application):
app.router.add_post("/api/wfm/video/frame/save-to-output", handle_save_frame)
app.router.add_post("/api/wfm/video/to-gif", handle_to_gif)
app.router.add_post("/api/wfm/video/asset/register", handle_register_asset)
app.router.add_post("/api/wfm/video/edit/convert-audio", handle_convert_audio)
app.router.add_get("/api/wfm/video/edit/probe", handle_probe_video)
app.router.add_post("/api/wfm/video/edit/overlay-text", handle_overlay_text)
app.router.add_get("/api/wfm/video/plans", handle_list_plans)
app.router.add_get("/api/wfm/video/plans/content", handle_get_plan_content)
app.router.add_post("/api/wfm/video/plans/save", handle_save_plan)
app.router.add_post("/api/wfm/video/plans/delete", handle_delete_plan)
app.router.add_post("/api/wfm/video/index-image/save-to-output", handle_save_index_to_output)
app.router.add_get("/api/wfm/video/edit/projects", handle_list_edit_projects)
app.router.add_get("/api/wfm/video/edit/projects/content", handle_get_edit_project_content)
app.router.add_post("/api/wfm/video/edit/projects/save", handle_save_edit_project)
app.router.add_post("/api/wfm/video/edit/projects/delete", handle_delete_edit_project)
async def handle_save_frame(request: web.Request) -> web.Response:
try:
body = await request.json()
image_base64 = body.get("image_base64", "")
if not image_base64:
return web.json_response({"error": "image_base64 required"}, status=400)
result = await asyncio.to_thread(_service.save_frame_to_output, image_base64)
return web.json_response({"status": "ok", **result})
except Exception as e:
logger.error("Error saving video frame to output: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_register_asset(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
if not filename:
return web.json_response({"error": "filename required"}, status=400)
result = await asyncio.to_thread(
_service.register_asset, filename, body.get("subfolder", ""), body.get("type", "input")
)
return web.json_response({"status": "ok", **result})
except FileNotFoundError as e:
return web.json_response({"error": str(e)}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error registering video asset: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_convert_audio(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
if not filename:
return web.json_response({"error": "filename required"}, status=400)
# PyAVの変換はCPU負荷が高いのでスレッドで実行する(イベントループを止めない)
result = await asyncio.to_thread(
_service.convert_audio_to_output, filename, body.get("subfolder", ""), body.get("type", "input"), body
)
return web.json_response({"ok": True, **result})
except FileNotFoundError as e:
return web.json_response({"ok": False, "error": str(e)}, status=404)
except ValueError as e:
return web.json_response({"ok": False, "error": str(e)}, status=400)
except Exception as e:
logger.error("Error converting clip audio: %s", e)
return web.json_response({"ok": False, "error": str(e)}, status=500)
async def handle_to_gif(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
subfolder = body.get("subfolder", "")
type_ = body.get("type", "output")
start_time = float(body.get("start_time") or 0)
end_time_raw = body.get("end_time")
end_time = float(end_time_raw) if end_time_raw not in (None, "") else None
fps = float(body.get("fps") or 10)
max_width_raw = body.get("max_width")
max_width = int(max_width_raw) if max_width_raw not in (None, "") else None
if not filename:
return web.json_response({"error": "filename required"}, status=400)
result = await asyncio.to_thread(
_service.convert_to_gif, filename, subfolder, type_, start_time, end_time, fps, max_width
)
return web.json_response({"status": "ok", **result})
except FileNotFoundError as e:
return web.json_response({"error": str(e)}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error converting video to GIF: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_probe_video(request: web.Request) -> web.Response:
filename = request.rel_url.query.get("filename", "")
subfolder = request.rel_url.query.get("subfolder", "")
type_ = request.rel_url.query.get("type", "input")
if not filename:
return web.json_response({"error": "filename required"}, status=400)
try:
result = await asyncio.to_thread(_service.probe_video, filename, subfolder, type_)
return web.json_response(result)
except FileNotFoundError as e:
return web.json_response({"error": str(e)}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error probing video: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_overlay_text(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
subfolder = body.get("subfolder", "")
type_ = body.get("type", "output")
overlays = body.get("overlays") or []
delete_source = bool(body.get("delete_source"))
pips = body.get("pips") or []
if not filename:
return web.json_response({"error": "filename required"}, status=400)
if not isinstance(overlays, list) or not isinstance(pips, list):
return web.json_response({"error": "overlays and pips must be lists"}, status=400)
# Decode+re-encode of every frame — run off the event loop so other
# requests aren't stalled for the duration of the burn-in.
result = await asyncio.to_thread(
_service.overlay_text_on_video, filename, subfolder, type_, overlays, "video/wfm_edit", delete_source, pips
)
return web.json_response({"status": "ok", **result})
except FileNotFoundError as e:
return web.json_response({"error": str(e)}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error burning text overlay into video: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_list_plans(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_plan_service.list_plans)
return web.json_response(result)
except Exception as e:
logger.error("Error listing video plans: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_get_plan_content(request: web.Request) -> web.Response:
filename = request.rel_url.query.get("filename", "")
try:
data = await asyncio.to_thread(_plan_service.get_plan, filename)
return web.json_response({"data": data})
except FileNotFoundError:
return web.json_response({"error": "not found"}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error getting video plan content: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_save_plan(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
data = body.get("data", {})
index_image_base64 = body.get("index_image_base64")
result = await asyncio.to_thread(_plan_service.save_plan, filename, data, index_image_base64)
return web.json_response({"status": "ok", **result})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error saving video plan: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_delete_plan(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
await asyncio.to_thread(_plan_service.delete_plan, filename)
return web.json_response({"status": "ok"})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error deleting video plan: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_list_edit_projects(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_edit_project_service.list_projects)
return web.json_response(result)
except Exception as e:
logger.error("Error listing video edit projects: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_get_edit_project_content(request: web.Request) -> web.Response:
filename = request.rel_url.query.get("filename", "")
try:
data = await asyncio.to_thread(_edit_project_service.get_project, filename)
return web.json_response({"data": data})
except FileNotFoundError:
return web.json_response({"error": "not found"}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error getting video edit project content: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_save_edit_project(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
data = body.get("data", {})
result = await asyncio.to_thread(_edit_project_service.save_project, filename, data)
return web.json_response({"status": "ok", **result})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error saving video edit project: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_delete_edit_project(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
await asyncio.to_thread(_edit_project_service.delete_project, filename)
return web.json_response({"status": "ok"})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error deleting video edit project: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_save_index_to_output(request: web.Request) -> web.Response:
try:
body = await request.json()
image_base64 = body.get("image_base64", "")
if not image_base64:
return web.json_response({"error": "image_base64 required"}, status=400)
result = await asyncio.to_thread(_plan_service.save_index_image_to_output, image_base64)
return web.json_response({"status": "ok", **result})
except Exception as e:
logger.error("Error saving video index image to output: %s", e)
return web.json_response({"error": str(e)}, status=500)
+222
View File
@@ -0,0 +1,222 @@
"""VRAM management: unload Ollama models before image generation to free VRAM.
Ported from ComfyUI Comic Creator (/api/ccc/vram/prepare), which follows ComfyUI-LiveChatStream's
vram_prepare. ComfyUI's own free-VRAM figure (get_free_memory / system_stats vram_free) may not react
to Ollama loading/unloading models (measured on ComfyUI_5: 8462MB before and after loading a 9B model
while nvidia-smi went 6530 -> 469MB), so free VRAM is measured with nvidia-smi plus ComfyUI's own
unused torch cache, falling back to get_free_memory when nvidia-smi is unavailable.
"""
import asyncio
import json
import logging
import math
import subprocess
import time
import urllib.request
from urllib.parse import urlparse
from aiohttp import web
from ..services.url_guard import is_allowed_backend_url, LOOPBACK_HOSTS, urlopen
logger = logging.getLogger(__name__)
_MB = 1024 * 1024
_OLLAMA_DEFAULT_URL = "http://127.0.0.1:11434"
_MAX_URLS = 6 # incl. the default; each unreachable URL costs a connect timeout
_lock = asyncio.Lock() # never run concurrently (unloading and measuring interfere)
def setup_routes(app: web.Application):
app.router.add_post("/api/wfm/vram/prepare", handle_prepare)
def _smi_free_mb(ident):
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
out = subprocess.run(
["nvidia-smi", "-i", str(ident), "--query-gpu=memory.free", "--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=5, creationflags=flags,
)
if out.returncode != 0:
raise RuntimeError(out.stderr.strip() or "nvidia-smi failed")
return float(out.stdout.strip().splitlines()[0])
def _free_vram_mb():
"""Free VRAM (MB) usable for image generation, and its source ('nvidia-smi' / 'comfy')."""
import torch
import comfy.model_management as mm
dev = mm.get_torch_device()
if getattr(dev, "type", "cpu") == "cuda":
try:
st = torch.cuda.memory_stats(dev)
cached = max(0, st["reserved_bytes.all.current"] - st["active_bytes.all.current"])
try:
free = _smi_free_mb(f"GPU-{torch.cuda.get_device_properties(dev).uuid}")
except Exception:
free = _smi_free_mb(dev.index or 0)
return free + cached / _MB, "nvidia-smi"
except Exception as e:
logger.warning("[WFS] nvidia-smi free VRAM unavailable, falling back to ComfyUI: %s", e)
return mm.get_free_memory(dev) / _MB, "comfy"
def _wait_settled(prev_mb, timeout_s=6.0):
"""After an unload, wait until free VRAM grows and levels off (Ollama runner exit); return it (MB)."""
now = prev_mb
last = None
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
time.sleep(0.4)
now, _ = _free_vram_mb()
if last is not None and abs(now - last) < 32 and now > prev_mb + 32:
break
last = now
return now
def _ollama_json(base, path, payload=None, timeout=60):
data = json.dumps(payload).encode("utf-8") if payload is not None else None
req = urllib.request.Request(base + path, data=data, headers={"Content-Type": "application/json"},
method="POST" if data is not None else "GET")
with urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read()
def _ollama_loaded_names(base):
_, raw = _ollama_json(base, "/api/ps", timeout=5)
return {m.get("name") for m in json.loads(raw.decode("utf-8")).get("models", [])}
def _ollama_unload(base, name):
"""Unload with keep_alive=0. Models that reject /api/generate (e.g. decision models) are retried
with /api/chat. Ollama sometimes unloads a little later even after an error response, so finally
check /api/ps to see whether it is still loaded."""
errors = []
for path, payload in (("/api/generate", {"model": name, "keep_alive": 0}),
("/api/chat", {"model": name, "messages": [], "keep_alive": 0})):
try:
status, _ = _ollama_json(base, path, payload)
if status == 200:
return True
errors.append(f"{path}: HTTP {status}")
except Exception as e:
errors.append(f"{path}: {e}")
for _ in range(5):
time.sleep(0.5)
try:
if name not in _ollama_loaded_names(base):
return True
except Exception:
pass
logger.warning("[WFS] vram: unload failed for %s: %s", name, "; ".join(errors))
return False
def _ollama_bases(urls):
"""Deduplicated, permitted Ollama base URLs. localhost / 127.0.0.1 / ::1 count as one server.
URLs the backend guard rejects are skipped (logged) rather than failing the whole request."""
if urls is None:
urls = []
if not isinstance(urls, list):
raise ValueError("urls must be an array")
bases, seen = [], set()
for raw in [_OLLAMA_DEFAULT_URL, *urls]:
if not isinstance(raw, str) or not raw.strip():
continue
base = raw.strip().rstrip("/")
if not is_allowed_backend_url(base):
logger.warning("[WFS] vram: skipped backend URL not permitted by the guard: %s", base)
continue
p = urlparse(base)
host = (p.hostname or "").lower()
key = ("loopback" if host in LOOPBACK_HOSTS else host, p.scheme, p.port or (443 if p.scheme == "https" else 80))
if key in seen:
continue
seen.add(key)
bases.append(base)
if len(bases) >= _MAX_URLS:
break
return bases
def _prepare(mode, target_mb, bases):
free_before, free_src = _free_vram_mb()
cands, unreachable = [], []
for base in bases:
try:
_, raw = _ollama_json(base, "/api/ps", timeout=5)
for m in json.loads(raw.decode("utf-8")).get("models", []):
if m.get("name"):
cands.append({"base": base, "name": m["name"], "vram_mb": int(m.get("size_vram", 0) / _MB)})
except Exception as e:
# Ollama not running etc. Normal for the default URL, so not a failure; keep the reason in the
# log only (the response doesn't reveal the state of caller-chosen local ports)
logger.info("[WFS] vram: %s/api/ps failed: %r", base, e)
unreachable.append({"url": base})
unloaded, failed = [], []
free_after = free_before
if mode == "auto":
# Ollama's size_vram excludes the KV cache etc. and is smaller than the VRAM actually freed.
# Choosing by estimate could free too much, so unload the biggest first, one at a time, and stop
# as soon as the measured free VRAM reaches the target
cands.sort(key=lambda c: c["vram_mb"], reverse=True)
for c in cands:
if free_after >= target_mb:
break
if _ollama_unload(c["base"], c["name"]):
unloaded.append(c)
free_after = _wait_settled(free_after)
else:
failed.append(c)
else:
for c in cands:
(unloaded if _ollama_unload(c["base"], c["name"]) else failed).append(c)
if unloaded:
free_after = _wait_settled(free_after)
strip = lambda cs: [{"name": c["name"], "vram_mb": c["vram_mb"]} for c in cs]
return {
"status": "ok", "mode": mode, "target_gb": target_mb / 1024, "free_source": free_src,
"free_before_mb": round(free_before), "free_after_mb": round(free_after),
"unloaded": strip(unloaded), "failed": [c["name"] for c in failed],
"unreachable": unreachable,
"reached": free_after >= target_mb,
}
def _error(message, status):
return web.json_response({"status": "error", "message": message}, status=status)
async def handle_prepare(request: web.Request) -> web.Response:
"""POST {mode:'auto'|'all', target_gb, urls:[Ollama URLs]}.
auto: only when free VRAM is below target_gb, unload the models using the most VRAM until it is reached.
all: unload every loaded model.
The default 127.0.0.1:11434 is always included; other URLs must pass the backend URL guard."""
# JSON only, so another site can't unload models with a simple (preflight-free) text/plain request
if request.content_type != "application/json":
return _error("Content-Type must be application/json", 415)
try:
data = await request.json()
if not isinstance(data, dict):
raise ValueError("body must be a JSON object")
mode = data.get("mode")
if mode not in ("auto", "all"):
raise ValueError("mode must be auto or all")
target_gb = float(data.get("target_gb", 8) or 0)
if not math.isfinite(target_gb):
raise ValueError("target_gb must be a finite number")
target_mb = min(max(target_gb, 0.0), 64.0) * 1024
bases = _ollama_bases(data.get("urls"))
except Exception as e:
return _error(str(e), 400)
if _lock.locked():
return _error("VRAM management is already running", 409)
try:
async with _lock:
result = await asyncio.get_running_loop().run_in_executor(None, _prepare, mode, target_mb, bases)
return web.json_response(result)
except Exception as e:
logger.error("[WFS] vram prepare error: %s", e)
return _error(str(e), 500)
@@ -0,0 +1,45 @@
---
name: Image to Prompt
description: Looks at an attached image and writes a prompt that recreates it, in the style the current GenerateUI model understands best.
metadata:
wfs-version: 2
output: prompt
tools: []
temperature: 0.3
think: false
context: [workflow]
---
You reverse-engineer images into text-to-image prompts. The user attaches an image (in the TOOLS pane, which also attaches it to the chat) and you write a prompt that would recreate it with the model in their current workflow. This only works with a vision-capable chat model.
If no image is attached to the user's message, do not invent one: reply in one or two sentences asking them to drop an image into the TOOLS pane (PNG / JPG / WebP) and send their message again.
## Step 1 - look carefully, describe only what is visible
Go through the image in this order and note only what you can actually see - never guess names of characters, people, brands or artists:
1. Main subject(s): count, kind, apparent age group, hair, eyes, clothing and accessories with colors.
2. Pose, action, expression, gaze direction.
3. Setting and background, important objects.
4. Lighting (direction, softness, time of day), color palette.
5. Framing: shot size (close-up, upper body, full body, wide), camera angle, lens feel (shallow depth of field, wide angle).
6. Medium and style: photo, anime illustration, 3D render, oil painting, flat vector, etc.
7. Any readable text in the image - copy it exactly.
## Step 2 - pick the prompt style from the "Current workflow context"
- NATURAL style (Qwen-Image, FLUX, Z-Image, HiDream, SD3 and other T5 / LLM text-encoder models): fluent English sentences in the order above, 40-120 words, no quality tags; text to render goes in double quotes. Leave negative empty when CFG is about 1.
- TAG style (SDXL anime checkpoints: Illustrious, NoobAI, Animagine, Pony): Danbooru tags in the order quality -> count -> appearance -> expression/pose -> background -> lighting -> composition. Quality tags: Pony `score_9, score_8_up, score_7_up`; others `masterpiece, best quality, very aesthetic, absurdres`. Negative: `worst quality, low quality, lowres, bad anatomy, bad hands, jpeg artifacts, watermark, signature`.
- MIXED style (realistic SDXL / SD1.5): short comma-separated descriptive phrases plus camera and lighting terms.
- No context: use NATURAL.
- Write every positive and negative prompt from scratch from the user's request and the style rules here. Only when the context contains "Prompts currently typed into the workflow" (it appears when the user refers to the current prompt) edit or extend those as the user asks; even then, keep only the parts the user wants to keep.
## Output format (always)
1. One line starting with "Style:" naming the style and why (e.g. "Style: TAG - workflow uses an Illustrious checkpoint").
2. The prompt in a fenced block labeled `prompt`:
```prompt
positive: <the full prompt on one line>
negative: <negative prompt on one line, or empty>
```
3. One line "Aspect: W:H" matching the image's orientation (e.g. "Aspect: 2:3 (portrait)").
Reply to the user in their language outside the block; the prompt itself is English. If the user asks for changes ("make it night", "remove the hat"), output the complete updated block again.
@@ -0,0 +1,55 @@
---
name: Skill Creator
description: Interactively interviews you and drafts a new WFS chat skill (.md system prompt) ready to save.
metadata:
wfs-version: 3
tools: []
temperature: 0.5
---
You are a skill-design assistant embedded in ComfyUI Workflow Studio's AI TOOL Chat pane. Your job is to interactively help the user design a NEW "skill" - a markdown file with frontmatter followed by system-prompt instructions. Once saved, a skill can be selected from a dropdown and is silently prepended to future chat messages as a system prompt.
## How to conduct the interview
- Ask ONE focused question at a time, never a bulleted list of questions. Wait for the user's answer before asking the next one.
- Cover, in this order, only what is not already clear from context:
1. What task or domain should the skill specialize in (what should the model do differently once this skill is active)?
2. What output format is expected - plain prose, a specific fenced code block, a fixed structure, a length limit? If the result is an image prompt, an SVG, or another skill, suggest the matching Workflow Studio output type below so the user gets action buttons for free.
3. Any hard constraints or things to avoid (tone, forbidden content, security concerns, things past attempts got wrong)?
4. Would a short worked example help smaller/local models follow the pattern more reliably? If yes, ask what a good example input/output looks like, or offer to draft one.
- Keep it short - 3 to 5 questions is usually enough. If the user says "just generate it", "that's enough", or similar at any point, stop asking and produce the best skill you can from what you already have.
## Workflow Studio output types (the fenced blocks the app recognizes)
- ```prompt block - an image-generation prompt in this exact shape (the app shows "Apply to GenerateUI" / "Generate with this prompt" buttons):
positive: <prompt text>
negative: <negative prompt text, may be empty>
- ```svg block - one complete `<svg>` element (the app previews it, can save it to the Gallery, and auto-checks it).
- ```skill block - a complete skill file (the app shows a "Save as new skill" button).
## Optional metadata a skill can declare
All keys are optional and go under `metadata:` in the frontmatter. Only include the ones that matter for this skill.
- `output`: `prompt` or `svg` - the app checks that the reply contains that block and automatically asks the model to fix it once if it is missing or broken.
- `tools`: `[]` to never offer image generation to the model, or `[generate_image]` to let it start ComfyUI generation by itself (only when the user turned on the image-generation toggle).
- `temperature`: a number such as `0.3` (precise, repeatable) to `0.9` (creative).
- `think`: `true` / `false` to force the model's thinking mode on or off for this skill.
- `context`: a list of facts sent with every message:
- `workflow`: a summary of the workflow loaded in GenerateUI (model files, prompt nodes, CFG, resolution). The prompt text currently in the workflow is added only when the user's message refers to it ("improve the current prompt").
- `current-prompt`: always include the prompt text currently in the workflow (for skills whose job is to edit that prompt).
- `workflow-detail`: every node with its settings, checked against this ComfyUI install (missing nodes / files, broken links) - for troubleshooting skills.
- `logs`: the last execution error and the latest error block of the ComfyUI log.
## Output format for the finished skill
- Once you have enough information, output the complete skill file in a single fenced code block labeled `skill` (```skill ... ```). Never mix prose inside that block - explanation goes outside it.
- The block must start with frontmatter in this shape (drop the `metadata:` part entirely if no key is needed):
---
name: <short display name, shown in the skill dropdown>
description: <one sentence, shown as a tooltip in the dropdown>
metadata:
output: <prompt or svg>
temperature: <number>
---
- After the frontmatter, write clear, imperative system-prompt instructions for the target skill: what role the model should take, concrete output-format rules, hard constraints, and - if useful - one short worked example following a "request -> ideal output" shape.
- Write the instructions in English unless the user asks otherwise (small local models follow English instructions most reliably), but tell the skill to reply in the user's language.
- After the code block, tell the user in one line that they can click the "Save as new skill" button below the message to add it, or copy the block manually into a new skill.
## Improving an existing skill
If the user pastes an existing skill and describes what went wrong, do not interview from scratch: identify the cause in one or two sentences, then output the complete revised skill in a ```skill block, keeping its name unless asked to change it.
@@ -0,0 +1,53 @@
---
name: Storyboard to Lab
description: Turns a story or scene list into a Lab plan - one prompt per scene with consistent characters - that opens in GenerateUI > Lab with one click.
metadata:
wfs-version: 2
output: labplan
tools: []
temperature: 0.6
think: false
context: [workflow]
---
You are a storyboard artist for ComfyUI Workflow Studio. The user describes a story, a sequence, or a list of shots (in any language). You split it into scenes and write one text-to-image prompt per scene, packaged as a Lab plan: the Lab tab runs the scenes as consecutive iterations of the workflow loaded in GenerateUI.
## Before writing
- If the user did not say how many scenes, use 4-8 that cover the story's key beats. Never exceed 32.
- If an essential detail is missing and would change every scene (e.g. who the main character is), ask ONE short question first. Otherwise just make sensible choices.
## Consistency (the most important part)
- Write a fixed "character sheet" phrase for each recurring character (hair, eyes, age range, outfit with colors) and paste it IDENTICALLY into every scene where that character appears. Do the same for a recurring place or style.
- Keep one art style / medium for the whole plan and repeat it in every scene.
- Use the same "seed" for the whole plan (any integer, e.g. 123456789) so the look stays stable; use null only if the user wants variety.
## Prompt style per scene
Use the "Current workflow context" to choose the style:
- NATURAL (Qwen-Image, FLUX, Z-Image, HiDream, SD3, other T5 / LLM-encoder models): English sentences - character sheet, action in this scene, setting, lighting, camera framing, style. 40-100 words, no quality tags. Negative empty when CFG is about 1.
- TAG (Illustrious, NoobAI, Animagine, Pony): Danbooru tags - quality tags (Pony: `score_9, score_8_up, score_7_up`; others: `masterpiece, best quality, very aesthetic, absurdres`), count tag, character sheet tags, scene action, background, lighting, framing. Negative: `worst quality, low quality, lowres, bad anatomy, bad hands, jpeg artifacts, watermark, signature`.
- MIXED (realistic SDXL / SD1.5): short descriptive phrases plus camera and lighting terms.
- No context: NATURAL.
- Write every positive and negative prompt from scratch from the user's request and the style rules here. Only when the context contains "Prompts currently typed into the workflow" (it appears when the user refers to the current prompt) edit or extend those as the user asks; even then, keep only the parts the user wants to keep.
Vary the camera framing across scenes (wide establishing shot, medium shot, close-up) like a real storyboard.
## Output format (always)
1. A short numbered list of the scenes in the user's language (one line each).
2. The plan in one fenced block labeled `labplan` containing valid JSON only (double quotes, no comments, no trailing commas):
```labplan
{
"name": "short_plan_name",
"note": "one-line summary of the story",
"seed": 123456789,
"negative": "default negative prompt for every scene, written from the style rules above (empty string for CFG about 1)",
"scenes": [
{ "title": "Scene title", "positive": "full prompt for this scene" },
{ "title": "Scene title", "positive": "full prompt", "negative": "only if this scene needs a different negative" }
]
}
```
3. One line telling the user to press "Open in Lab" below the message, check the plan, and press Run in the Lab tab (GenerateUI must have the workflow loaded).
When the user asks for changes ("make scene 3 at night", "add a scene"), output the complete updated `labplan` block again, keeping the character sheets identical.
+95
View File
@@ -0,0 +1,95 @@
---
name: SVG Icon Generator
description: Produces clean, minimal, valid SVG icons and simple illustrations.
metadata:
wfs-version: 2
output: svg
tools: []
temperature: 0.4
---
You are an expert SVG icon and illustration designer. When the user asks for an icon, logo, pictogram, diagram, character, animal, or any vector graphic, respond with valid, clean SVG markup.
## Composition process (do this before writing markup - skipping this step is the main cause of unrecognizable output)
- Break the subject down into 3-8 distinct parts that make it recognizable (e.g. a bird: body, head, beak, eye, wing, tail; a house: wall, roof, door, window). Drop parts that are not essential.
- Decide each part's approximate position, size, and draw order within the viewBox (elements listed later in the markup are drawn on top).
- Pick the simplest shape that captures each part's silhouette: `<circle>`/`<ellipse>` for round parts, `<rect>` for rectangular parts, `<polygon>` for angular parts (beaks, ears, leaves, roofs). For a rounded organic outline that no primitive can approximate well (a body silhouette, a wing, a leaf edge), use a short `<path>` with 2-4 smooth curve commands (`Q` or `C`) instead of a plain circle - a few deliberate curve points beat a distorted primitive.
- Never fall back to drawing an unrelated blob plus a few disconnected decorative lines when a subject is hard to render - always decompose it into the named parts above instead, even if each part is crude.
- Write the part list in one short line before the code block (e.g. "Parts: body, head, beak, eye, wing").
## Style presets
If the user names a style, follow it; otherwise use "flat".
- **line**: outlines only - `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, `stroke-linecap="round"`, `stroke-linejoin="round"`. No filled areas except tiny dots (eyes).
- **flat**: solid fills, 2-4 explicit hex colors, no strokes or only thin dark outlines.
- **duotone**: exactly two colors - one main hex color and the same color with `opacity="0.4"` for secondary parts.
- **mono**: a single `fill="currentColor"` for everything, shapes separated by negative space.
## Output rules
- Always wrap the SVG markup in a fenced code block labeled `svg`, e.g. ```svg ... ```.
- Keep any explanation brief and place it OUTSIDE the code block, never mixed inside the SVG.
- Output exactly one `<svg>...</svg>` element per code block.
- When the user asks for a change, output the complete updated SVG again (never a partial diff).
## Structure rules
- Always include `xmlns="http://www.w3.org/2000/svg"` and a `viewBox` attribute (e.g. `viewBox="0 0 24 24"`). Do not hardcode large pixel `width`/`height`; if you set them, make them match the viewBox aspect ratio.
- Use a small, round coordinate system (0-24 or 0-100). Keep every coordinate inside the viewBox.
- Round coordinate values to at most 2 decimal places.
- Keep every `<path>` short (at most about 6 commands) - long, intricate paths are where quality breaks down most often.
- Group related elements with `<g>` when it clarifies structure, but do not over-nest.
## Color rules
- For single-color icons, use `fill="currentColor"` (or `stroke="currentColor"` for line icons) so the icon inherits the surrounding text color.
- For multi-color illustrations, use explicit hex colors (e.g. `#4A90D9`) - never named CSS colors like `"blue"`.
- Avoid gradients, filters, and blend modes unless the user explicitly asks for a glossy/gradient style - they rarely render correctly from generated coordinates.
## Accessibility
- Include a `<title>` element as the first child of `<svg>` describing what the icon depicts.
## Security (mandatory - do not violate)
- Never include `<script>`, `<foreignObject>`, event handler attributes (`onload`, `onclick`, etc.), or `<a>`/`href` links to external URLs.
- Never reference external resources (no `xlink:href` to remote URLs, no `<image>` with external `src`).
## Self-check before you answer
Silently verify: every tag is closed; `viewBox` and `xmlns` are present; every part from your part list appears as its own element; nothing lies outside the viewBox; no forbidden element or attribute is used. Workflow Studio validates the SVG automatically and will send you the exact error if it is broken - if you receive such a message, reply with the full corrected SVG only.
## Example 1 - simple geometric icon (single color, primitives only)
Request: "Sun icon"
Parts: disc (circle), 8 rays (lines).
```svg
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Sun</title>
<circle cx="12" cy="12" r="5" fill="currentColor"/>
<g stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="12" y1="1" x2="12" y2="4"/>
<line x1="12" y1="20" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/>
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="4" y2="12"/>
<line x1="20" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/>
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
</g>
</svg>
```
## Example 2 - organic subject (multi-color, decomposed into parts)
Request: "Bird icon"
Parts: body (rounded outline via a short curved path), head (circle), beak (small triangle), eye (dot), wing (small curved path) - each drawn as its own element, layered head/beak/eye on top of the body.
```svg
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Bird</title>
<path d="M4 16 Q4 10 11 9 Q18 8 20 13 Q20 17 14 18 Q7 19 4 16 Z" fill="#4A90D9"/>
<path d="M8 14 Q5 12 3 14 Q6 16 9 16 Z" fill="#3576B8"/>
<circle cx="17" cy="10" r="3.2" fill="#4A90D9"/>
<polygon points="20,10 23,9.3 20,11.4" fill="#F2B807"/>
<circle cx="18" cy="9" r="0.6" fill="#1A1A1A"/>
</svg>
```
Follow this pattern for every request: list the parts first, represent each with the simplest adequate shape, then assemble - never skip straight to unstructured shapes.
@@ -0,0 +1,63 @@
---
name: T2I Prompt Enhancer
description: Turns a short idea into a prompt written the way the model in the current GenerateUI workflow understands best.
metadata:
wfs-version: 2
output: prompt
tools: []
temperature: 0.5
think: false
context: [workflow]
---
You are a prompt engineer for text-to-image models in ComfyUI. The user gives a short or vague idea (in any language); you rewrite it into one prompt that the model in their current workflow will follow well.
## Step 1 - pick the prompt style
Use the "Current workflow context" section (provided by Workflow Studio when a workflow is loaded) to decide the style. If the user names a model, that wins. If there is no context and the user did not say, use NATURAL style.
- NATURAL style - for DiT / LLM-encoder models: Qwen-Image, FLUX (1, 2, Kontext, Klein), Z-Image, HiDream, Ernie Image, LongCat, MOSS-Image, Ming, Boogu, SD3/3.5, Lumina, and anything that loads a T5 / Qwen / Llama / Gemma text encoder.
- Write fluent English sentences, most important first: subject and its key attributes -> action or pose -> setting -> lighting and time of day -> composition and camera (shot size, angle, lens) -> art style or medium.
- 40-120 words. Every phrase must add visual information - no filler such as "masterpiece", "best quality", "8k", "highly detailed" (these models ignore them, and long prompts get truncated: FLUX's T5 stops reading at 512 tokens).
- Any text that must appear in the image goes in double quotes, e.g. a sign reading "OPEN".
- Negative: these models usually run at CFG about 1, where the negative prompt has no effect. Leave `negative:` empty unless the context shows CFG above 1.5; then add a short list of real defects to avoid.
- TAG style - for SDXL anime checkpoints (Illustrious, NoobAI, Animagine, Pony, and checkpoints whose names contain "illu", "noob", "anime", "pony"):
- Comma-separated Danbooru tags in this order: quality tags -> count tag (1girl, 2boys, no humans) -> character / series if named -> appearance (hair, eyes, clothing) -> expression and pose -> background and setting -> lighting -> composition (from above, cowboy shot, close-up).
- Quality tags: Pony models start with `score_9, score_8_up, score_7_up`; other anime SDXL models start with `masterpiece, best quality, very aesthetic, absurdres`.
- Use underscores only where the tag itself has them; use spaces otherwise (e.g. `long hair`, `looking at viewer`). Optional emphasis: `(tag:1.2)`, never above 1.4.
- Negative: `worst quality, low quality, lowres, bad anatomy, bad hands, extra digits, jpeg artifacts, watermark, signature, text` (Pony: add `score_4, score_5, score_6`).
- MIXED style - for realistic SDXL or SD1.5 checkpoints: short comma-separated descriptive phrases (not single words, not long sentences), subject first, then camera and lighting terms (e.g. `85mm portrait, soft window light, shallow depth of field`). Negative: a short defect list.
## Step 2 - rewrite
- Keep every element the user explicitly asked for, exactly. Never contradict them or swap them for something "better".
- Fill only what is missing and matters for the picture (lighting, framing, background) with plausible, concrete choices that fit the user's mood. Do not invent extra characters, objects, or text.
- If the idea is in Japanese or another language, translate the meaning faithfully - never transliterate (夕焼け -> "sunset glow", not "yuuyake").
- Write every positive and negative prompt from scratch from the user's request and the style rules here. Only when the context contains "Prompts currently typed into the workflow" (it appears when the user refers to the current prompt) edit or extend those as the user asks; even then, keep only the parts the user wants to keep.
- If the request is truly ambiguous in a way that changes the image (e.g. "a bat" - animal or baseball bat?), ask one short question instead of guessing.
## Output format (always)
1. One line starting with "Style:" naming the style you chose and why in a few words (e.g. "Style: NATURAL - workflow uses Qwen-Image 2.1, CFG 1").
2. The prompt in a fenced block labeled `prompt`, exactly in this shape:
```prompt
positive: <the full prompt on one line>
negative: <negative prompt on one line, or leave empty after the colon>
```
3. Optionally one line suggesting a fitting aspect ratio (e.g. "Suggested aspect: 9:16 (portrait)").
Reply to the user in their language outside the block, but the prompt itself is always English (TAG style uses English tags).
When the user asks for changes ("make it night", "add a cat"), apply them to your previous prompt and output the complete updated block again - never only the changed part.
## Example
User: 雨の夜の東京で傘をさす猫
Context: model qwen_image_2512_fp8.safetensors, CFG 1.0
Style: NATURAL - Qwen-Image workflow, CFG 1 so no negative prompt.
```prompt
positive: A small tabby cat standing upright on a rain-soaked Tokyo side street at night, holding a tiny transparent umbrella above its head with one paw. Glowing neon signs in pink and cyan reflect on the wet asphalt, raindrops visible in the light. Low-angle medium shot, shallow depth of field with blurred city lights behind, cinematic and slightly melancholic mood.
negative:
```
Suggested aspect: 2:3 (portrait)
@@ -0,0 +1,35 @@
---
name: Workflow Doctor
description: Explains the workflow loaded in GenerateUI and diagnoses errors from checked facts (missing nodes and files, last execution error, ComfyUI log).
metadata:
wfs-version: 1
tools: []
temperature: 0.2
context: [workflow-detail, logs]
---
You are a ComfyUI troubleshooting assistant inside Workflow Studio. Every message comes with two fact sections generated by Workflow Studio right before the message was sent:
- "Workflow facts": the workflow loaded in GenerateUI, checked against this ComfyUI install - node types that are NOT INSTALLED, values that are NOT in ComfyUI's list (usually model files that do not exist, with the closest existing names), broken links, missing required inputs, and every node with its settings.
- "ComfyUI runtime facts": the last execution error from ComfyUI's history and the most recent error block of the ComfyUI log.
## Ground rules (most important)
- Base every statement on those facts or on text the user pasted. Quote the exact node (#id and class name), file name, or error line you rely on.
- Your general knowledge of ComfyUI, models and custom nodes may be outdated or wrong. When you use it, say so ("generally", "usually") and never invent node names, file names, settings, menu paths or package names.
- If the facts do not show the cause, say that plainly and tell the user exactly what to check or paste next (e.g. "run the workflow once and send me a message again so the error is captured").
- The facts are rebuilt on every message, so after the user fixes something, re-check from the new facts instead of repeating your earlier answer.
## When the user asks why something fails
1. Cause - one or two sentences, quoting the decisive fact.
2. Fix - numbered, concrete steps using Workflow Studio / ComfyUI terms:
- Not installed node type -> install the custom node pack that provides it with ComfyUI Manager ("Install Missing Custom Nodes"), then restart ComfyUI. Only name the pack if the error text or node name makes it certain.
- Value not in ComfyUI's list -> pick an existing file in that node (offer the "closest existing" names from the facts), or put the missing file into the matching models folder and press Refresh Models in GenerateUI.
- Out of memory (CUDA out of memory / allocation errors) -> lower resolution or batch size, use a smaller / quantized model, unload other models (Workflow Studio's VRAM settings can unload Ollama models before generation).
- Shape / dtype / "mat1 and mat2" errors -> usually a model and an encoder/VAE/LoRA from different model families; point at the two loader nodes involved.
3. Check - how the user can confirm it is fixed.
## When the user asks what the workflow does
Describe the pipeline in order (load models -> encode prompt -> sample -> decode -> save), naming the key nodes (#id) and the important settings (model files, resolution, steps, CFG, sampler, LoRAs). Point out anything in "Detected problems" even if the user did not ask.
## Style
- Reply in the user's language. Use short headings and numbered steps; keep it compact.
- Never output a full workflow JSON. If a node value must change, name the node (#id), the input and the new value.
+320 -32
View File
@@ -56,6 +56,38 @@ _MAX_RETRIES = 3
_BATCH_CHUNK_SIZE = 100
# サイドカーJSON(.metadata.json / .cm-info.json)の最大サイズ。巨大ファイルでメモリを使い切らないための上限
_SIDECAR_MAX_BYTES = 2 * 1024 * 1024
def _as_int(v):
"""数値IDだけを受け付ける(文字列に細工されたIDをURL等に埋め込まないため)。不正ならNone。"""
try:
if isinstance(v, bool):
return None
n = int(v)
return n if n > 0 else None
except (TypeError, ValueError):
return None
# プレビュー画像のダウンロード許可ホスト(サイドカーJSON由来のURLでSSRF/ローカルファイル読み取りをさせない)
_IMAGE_HOST_SUFFIXES = (".civitai.com", ".civitai.red", ".civitai.green")
_IMAGE_HOST_EXACT = ("civitai.com", "civitai.red", "civitai.green")
_IMAGE_MAX_BYTES = 50 * 1024 * 1024
def _is_allowed_image_url(url):
"""https かつ CivitAI 系ホストのURLだけ許可する(file:// や内部アドレス、他ホストは拒否)。"""
try:
from urllib.parse import urlparse
u = urlparse(str(url).strip())
host = (u.hostname or "").lower()
return u.scheme == "https" and (host in _IMAGE_HOST_EXACT or host.endswith(_IMAGE_HOST_SUFFIXES))
except Exception:
return False
class CivitaiService:
"""Fetch and cache CivitAI model metadata."""
@@ -117,14 +149,16 @@ class CivitaiService:
# ── ハッシュ計算 ──────────────────────────────────────────
@staticmethod
def calculate_sha256(file_path, chunk_size=65536):
"""Calculate SHA256 hash of a file."""
def calculate_sha256(file_path, chunk_size=512 * 1024, cancel=None):
"""Calculate SHA256 hash of a file. cancel(threading.Event)がセットされたらNoneを返して中断。"""
h = hashlib.sha256()
path = Path(file_path)
if not path.is_file():
return None
with open(path, "rb") as f:
while True:
if cancel is not None and cancel.is_set():
return None
chunk = f.read(chunk_size)
if not chunk:
break
@@ -142,8 +176,11 @@ class CivitaiService:
"""
sha256_lower = sha256_hash.lower()
cache = self._load_cache()
if sha256_lower in cache:
return cache[sha256_lower]
cached = cache.get(sha256_lower)
# Stability Matrix(.cm-info.json)由来の簡易情報は画像一覧を持たないため、
# 明示的な取得/更新ではAPIから完全な情報で上書きする
if cached is not None and cached.get("source") != "cm-info":
return cached
url = f"{CIVITAI_API_BASE}/model-versions/by-hash/{sha256_hash.upper()}"
@@ -198,6 +235,7 @@ class CivitaiService:
url = f"{CIVITAI_API_BASE}/model-versions/by-hash"
results = {h.lower(): None for h in sha256_hashes}
cache = self._load_cache()
failed = set()
for chunk_start in range(0, len(sha256_hashes), _BATCH_CHUNK_SIZE):
chunk = sha256_hashes[chunk_start:chunk_start + _BATCH_CHUNK_SIZE]
@@ -239,6 +277,7 @@ class CivitaiService:
time.sleep(wait)
continue
logger.warning("CivitAI batch POST error: %s %s", e.code, e.reason)
failed.update(chunk_lower)
break
except (URLError, Exception) as e:
if attempt < _MAX_RETRIES - 1:
@@ -247,8 +286,11 @@ class CivitaiService:
time.sleep(wait)
continue
logger.warning("CivitAI batch POST failed: %s", e)
failed.update(chunk_lower)
break
# 通信失敗したチャンクは「CivitAIに存在しない」と区別するため記録する
self.last_failed_hashes = failed
self._save_cache()
return results
@@ -338,64 +380,295 @@ class CivitaiService:
# ── バッチフェッチ ────────────────────────────────────────
def batch_fetch(self, model_files, progress_callback=None):
@staticmethod
def _known_hash(known, st):
"""メタデータに保存済みのsha256が、現在のファイルに対して有効ならそれを返す。
size/mtime が記録されていれば一致を確認する(ファイル差し替え検知)。
記録の無い旧データは信頼する。
"""
sha = (known or {}).get("sha256")
if not sha:
return None
try:
ks, km = known.get("size"), known.get("mtime")
if ks is not None and int(ks) != st.st_size:
return None
if km is not None and abs(float(km) - st.st_mtime) > 2:
return None
except (TypeError, ValueError):
return None
return str(sha).lower()
def _read_sidecar(self, file_path, size):
"""サイドカーからsha256とCivitAI情報を読む(ハッシュ計算・通信不要)。
Lora Manager の `<stem>.metadata.json` → Stability Matrix の `<stem>.cm-info.json` の順に試す。
Returns: (sha256_lower or None, extracted_info or None)
"""
sha, info = self._read_lm_sidecar(file_path, size)
if sha:
return sha, info
return self._read_cm_info(file_path)
@staticmethod
def _parse_iso_ts(value):
"""ISO8601文字列→epoch秒。.NETの7桁小数秒にも対応。失敗時はNone。"""
try:
import re
from datetime import datetime
s = str(value).replace("Z", "+00:00")
s = re.sub(r"(\.\d{6})\d+", r"\1", s)
return datetime.fromisoformat(s).timestamp()
except Exception:
return None
def _read_cm_info(self, file_path):
"""Stability Matrix の `<stem>.cm-info.json` から sha256 とCivitAI簡易情報を読む。
ファイルサイズ情報が無いため、ImportedAt より後にモデルファイルが更新されていたら不採用。
画像一覧は持たないので info には "source": "cm-info" を付け、個別取得で完全情報に置き換えられる。
"""
sc = file_path.with_name(file_path.stem + ".cm-info.json")
try:
if sc.stat().st_size > _SIDECAR_MAX_BYTES: # 巨大ファイルによるメモリ消費を避ける
return None, None
with open(sc, "r", encoding="utf-8-sig") as f:
d = json.load(f)
if not isinstance(d, dict):
return None, None
sha = str((d.get("Hashes") or {}).get("SHA256", "")).lower()
if len(sha) != 64 or any(c not in "0123456789abcdef" for c in sha):
return None, None
imported = self._parse_iso_ts(d.get("ImportedAt"))
if imported is None or file_path.stat().st_mtime > imported + 2:
return None, None
except Exception:
return None, None
info = None
try:
if d.get("Source", 0) == 0 and _as_int(d.get("VersionId")): # 0 = Civitai
info = self._info_from_cm_info(d)
except Exception:
info = None
return sha, info
@staticmethod
def _info_from_cm_info(d):
"""cm-info.json を _extract_info と同じ形式のdictに変換する。"""
model_id, version_id = _as_int(d.get("ModelId")), _as_int(d.get("VersionId"))
stats = d.get("Stats") or {}
return {
"versionId": version_id,
"modelId": model_id,
"modelName": d.get("ModelName", ""),
"versionName": d.get("VersionName", ""),
"type": d.get("ModelType", ""),
"description": d.get("VersionDescription") or d.get("ModelDescription", ""),
"tags": [str(x) for x in (d.get("Tags") or []) if isinstance(x, (str, int, float))][:100],
"nsfw": bool(d.get("Nsfw", False)),
"nsfwLevel": 0,
"air": "",
"creator": d.get("AuthorUsername") or "",
"images": [],
"imageDetails": [],
"trainedWords": [str(x) for x in (d.get("TrainedWords") or []) if isinstance(x, (str, int, float))][:200],
"baseModel": d.get("BaseModel") or "",
"fileSize": 0,
"fileMeta": d.get("FileMetadata") or {},
"fileHashes": d.get("Hashes") or {},
"downloadUrl": "",
"modelUrl": (
f"https://civitai.com/models/{model_id}?modelVersionId={version_id}"
if model_id else f"https://civitai.com/model-versions/{version_id}"
),
"stats": {
"downloadCount": stats.get("downloadCount", 0),
"thumbsUpCount": stats.get("thumbsUpCount", 0),
"thumbsDownCount": stats.get("thumbsDownCount", 0),
},
"updatedAt": "",
"publishedAt": "",
"source": "cm-info",
}
def _read_lm_sidecar(self, file_path, size):
"""`<stem>.metadata.json` (Lora Manager) サイドカーからsha256とCivitAI情報を読む。
サイドカーのsizeがファイルサイズと一致する場合のみ採用する。
Returns: (sha256_lower or None, extracted_info or None)
"""
sc = file_path.with_name(file_path.stem + ".metadata.json")
try:
if sc.stat().st_size > _SIDECAR_MAX_BYTES:
return None, None
with open(sc, "r", encoding="utf-8") as f:
d = json.load(f)
if not isinstance(d, dict):
return None, None
sha = str(d.get("sha256", "")).lower()
if len(sha) != 64 or any(c not in "0123456789abcdef" for c in sha):
return None, None
if d.get("size") is None or int(float(d["size"])) != size:
return None, None
except Exception:
return None, None
info = None
civ = d.get("civitai")
if isinstance(civ, dict) and civ.get("id"):
# 別ファイルの情報を取り込まないよう、files[].hashes.SHA256 に一致するものだけ採用
matched = any(
str(f.get("hashes", {}).get("SHA256", "")).lower() == sha
for f in civ.get("files", []) if isinstance(f, dict)
)
if matched:
try:
info = self._extract_info(civ)
except Exception:
info = None
return sha, info
def batch_fetch(self, model_files, progress_callback=None, known=None, on_hash=None, workers=2, cancel=None, hash_missing=False):
"""Batch fetch CivitAI info for multiple model files.
Phase 1: SHA256 計算("hashing")
Phase 0: 保存済みsha256 / サイドカーから解決(ハッシュ計算・通信なし)
Phase 1: 残りのSHA256を並列計算("hashing")— hash_missing=True のときのみ。
既定では計算せず needs_hash を返す
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
known: { model_name: {"sha256", "size", "mtime"} } メタデータ保存済みハッシュ
on_hash: fn(model_name, sha256, size, mtime) ハッシュが新たに確定するたびに呼ばれる
workers: ハッシュ計算の並列数
cancel: threading.Event。セットされると未処理分を打ち切って戻る(クライアント切断時)
Returns: dict of { model_name: { sha256, civitai_info_or_none } }
"""
results = {}
total = len(model_files)
cache = self._load_cache()
known = known or {}
finished = 0
cache_dirty = False
# Phase 1: ハッシュ計算
hashes_needed = [] # [(model_name, sha256_lower, original_index)]
hashes_needed = [] # [(model_name, sha256_lower)]
to_hash = [] # [(model_name, file_path, stat)]
for i, (model_name, file_path) in enumerate(model_files):
if progress_callback:
progress_callback(i, total, model_name, "hashing")
sha256 = self.calculate_sha256(file_path)
if not sha256:
results[model_name] = {"sha256": None, "civitai": None, "error": "hash_failed"}
def _resolved(name, sha, st, new_hash):
"""sha256確定後の共通処理。キャッシュにあれば完了、無ければ取得待ちへ。"""
nonlocal finished
results[name] = {"sha256": sha, "civitai": None}
if new_hash and on_hash:
on_hash(name, sha, st.st_size, st.st_mtime)
if sha in cache:
results[name]["civitai"] = cache[sha]
finished += 1
if progress_callback:
progress_callback(i + 1, total, model_name, "not_found")
progress_callback(finished, total, name, "cached")
else:
hashes_needed.append((name, sha))
# Phase 0: ハッシュ計算なしで解決できるものを先に処理
for model_name, file_path in model_files:
try:
st = Path(file_path).stat()
except OSError:
results[model_name] = {"sha256": None, "civitai": None, "error": "hash_failed"}
finished += 1
if progress_callback:
progress_callback(finished, total, model_name, "not_found")
continue
sha256_lower = sha256.lower()
results[model_name] = {"sha256": sha256_lower, "civitai": None}
sha = self._known_hash(known.get(model_name), st)
if sha:
_resolved(model_name, sha, st, new_hash=False)
continue
if sha256_lower in cache:
results[model_name]["civitai"] = cache[sha256_lower]
sha, info = self._read_sidecar(Path(file_path), st.st_size)
if sha:
if info and sha not in cache:
cache[sha] = info
cache_dirty = True
_resolved(model_name, sha, st, new_hash=True)
continue
to_hash.append((model_name, file_path, st))
if cache_dirty:
self._save_cache()
logger.info("CivitAI batch: total=%d resolved_without_hash=%d need_hash=%d hash_missing=%s",
total, len(results), len(to_hash), hash_missing)
# ハッシュ未確定のものは、hash_missing=False(既定)なら計算せず "needs_hash" として返す
# (個別取得でハッシュ計算する運用。全件ハッシュ計算は負荷が高いため行わない)
if to_hash and not hash_missing:
for model_name, _fp, _st in to_hash:
results[model_name] = {"sha256": None, "civitai": None, "needs_hash": True}
finished += 1
if progress_callback:
progress_callback(i + 1, total, model_name, "cached")
else:
hashes_needed.append((model_name, sha256_lower, i))
progress_callback(finished, total, model_name, "needs_hash")
to_hash = []
# Phase 1: 残りを並列でハッシュ計算(hashlibは大きなチャンクでGILを解放する)
if to_hash:
from concurrent.futures import ThreadPoolExecutor, as_completed
def _hash(item):
return self.calculate_sha256(item[1], cancel=cancel)
with ThreadPoolExecutor(max_workers=max(1, workers)) as pool:
futures = {pool.submit(_hash, item): item for item in to_hash}
for fut in as_completed(futures):
if cancel is not None and cancel.is_set():
for f in futures:
f.cancel()
break
model_name, _fp, st = futures[fut]
try:
sha = fut.result()
except Exception:
sha = None
if not sha:
results[model_name] = {"sha256": None, "civitai": None, "error": "hash_failed"}
finished += 1
if progress_callback:
progress_callback(finished, total, model_name, "not_found")
continue
if progress_callback:
progress_callback(finished, total, model_name, "hashing")
_resolved(model_name, sha.lower(), st, new_hash=True)
if cancel is not None and cancel.is_set():
return results
# Phase 2: POST で一括取得(未キャッシュ分)
if hashes_needed:
if progress_callback:
progress_callback(len(results), total, "", "fetching")
progress_callback(finished, total, "", "fetching")
# 同一ハッシュが複数モデルに対応する場合を考慮
sha256_to_entries: dict[str, list] = {}
for name, sha256_lower, idx in hashes_needed:
sha256_to_entries.setdefault(sha256_lower, []).append((name, idx))
sha256_to_names: dict[str, list] = {}
for name, sha256_lower in hashes_needed:
sha256_to_names.setdefault(sha256_lower, []).append(name)
batch_results = self._batch_fetch_post(list(sha256_to_entries.keys()))
logger.info("CivitAI batch: POST %d hashes", len(sha256_to_names))
batch_results = self._batch_fetch_post(list(sha256_to_names.keys()))
logger.info("CivitAI batch: POST done")
failed = getattr(self, "last_failed_hashes", set())
for sha256_lower, info in batch_results.items():
for name, idx in sha256_to_entries.get(sha256_lower, []):
for name in sha256_to_names.get(sha256_lower, []):
results[name]["civitai"] = info
if not info and sha256_lower in failed:
results[name]["fetch_failed"] = True
finished += 1
if progress_callback:
status = "found" if info else "not_found"
progress_callback(idx + 1, total, name, status)
progress_callback(finished, total, name, status)
if progress_callback:
progress_callback(total, total, "", "done")
@@ -407,10 +680,25 @@ class CivitaiService:
@staticmethod
def download_image(url, save_path, timeout=15):
"""Download an image from URL and save to save_path. Returns True on success."""
if not _is_allowed_image_url(url):
logger.warning("Refusing to download preview from non-CivitAI URL: %s", str(url)[:120])
return False
try:
from urllib.request import HTTPRedirectHandler, HTTPSHandler, build_opener
class _SafeRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
if not _is_allowed_image_url(newurl): # リダイレクト先も検証
raise ValueError("redirect to disallowed host")
return super().redirect_request(req, fp, code, msg, headers, newurl)
opener = build_opener(_SafeRedirect, HTTPSHandler(context=_get_ssl_context()))
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 opener.open(req, timeout=timeout) as resp:
data = resp.read(_IMAGE_MAX_BYTES + 1)
if len(data) > _IMAGE_MAX_BYTES:
logger.warning("Preview too large, skipped: %s", str(url)[:120])
return False
with open(save_path, "wb") as f:
f.write(data)
return True
+93
View File
@@ -0,0 +1,93 @@
"""Audio conversion (and audio extraction from video) using PyAV.
Gallery の「Convert」機能のコア。PyAV(av)だけで完結し、システムの ffmpeg には依存しない。
"""
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
# 出力フォーマット -> (拡張子, エンコーダ, 標準サンプル形式, ビットレート指定可否, タグ保持可否)
# .ogg は PyAV 同梱ビルドに Vorbis エンコーダが無いため出力対象外(入力は読める)。
# wav は任意キー(prompt/workflow)のタグを持てないため、メタデータ引き継ぎの対象外。
OUTPUT_FORMATS = {
"flac": {"ext": ".flac", "codec": "flac", "sample_fmt": "s16", "bitrate": False, "tags": True},
"mp3": {"ext": ".mp3", "codec": "libmp3lame", "sample_fmt": "fltp", "bitrate": True, "tags": True},
"opus": {"ext": ".opus", "codec": "libopus", "sample_fmt": "flt", "bitrate": True, "tags": True},
"m4a": {"ext": ".m4a", "codec": "aac", "sample_fmt": "fltp", "bitrate": True, "tags": True},
"wav": {"ext": ".wav", "codec": "pcm_s16le", "sample_fmt": "s16", "bitrate": False, "tags": False},
}
# 引き継ぐタグ(小文字で比較)。ComfyUIの prompt/workflow と一般的な曲情報のみ。
_CARRY_TAGS = {"prompt", "workflow", "title", "artist", "album", "date", "genre", "comment"}
_OPUS_RATES = (8000, 12000, 16000, 24000, 48000)
def supports_tags(fmt: str) -> bool:
return bool(OUTPUT_FORMATS.get(fmt, {}).get("tags"))
def _channel_layout(opt: str, src_channels: int) -> str:
if opt == "mono":
return "mono"
if opt == "stereo":
return "stereo"
return "mono" if src_channels == 1 else "stereo"
def convert_audio(src: Path, dst: Path, fmt: str, *, bitrate_kbps: int = 0,
sample_rate: int = 0, channels: str = "keep",
inherit_metadata: bool = True) -> dict:
"""src(音声または音声トラックを持つ動画)から dst へ変換する。
sample_rate=0 / channels="keep" は元の設定を維持。opusは48kHz固定扱い。
戻り値: {"tags_written": bool}(タグを持てない形式や元にタグが無い場合は False)
"""
import av
spec = OUTPUT_FORMATS[fmt]
tags_written = False
with av.open(str(src)) as in_c:
if not in_c.streams.audio:
raise ValueError("No audio stream in the source file")
in_stream = in_c.streams.audio[0]
rate = sample_rate or in_stream.rate or 44100
if fmt == "opus":
rate = min(_OPUS_RATES, key=lambda r: abs(r - rate)) if sample_rate else 48000
elif fmt == "mp3" and rate not in (8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000):
rate = 44100
layout = _channel_layout(channels, in_stream.channels)
options = {"movflags": "use_metadata_tags"} if fmt == "m4a" else {}
with av.open(str(dst), "w", options=options) as out_c:
# タグ: 引き継ぎ対象かつ形式がタグを持てる場合のみ
if inherit_metadata and spec["tags"]:
src_tags = dict(in_c.metadata or {})
for k, v in (in_stream.metadata or {}).items(): # Opus等はストリーム側にタグを持つ
src_tags.setdefault(k, v)
for k, v in src_tags.items():
if k.lower() in _CARRY_TAGS and isinstance(v, str) and v:
out_c.metadata[k.lower()] = v
tags_written = True
out_stream = out_c.add_stream(spec["codec"], rate=rate)
out_stream.layout = layout
if spec["bitrate"] and bitrate_kbps:
out_stream.bit_rate = int(bitrate_kbps) * 1000
resampler = av.AudioResampler(format=out_stream.format.name, layout=layout, rate=rate)
def _encode(frames):
for f in frames:
for pkt in out_stream.encode(f):
out_c.mux(pkt)
for frame in in_c.decode(in_stream):
_encode(resampler.resample(frame))
_encode(resampler.resample(None))
for pkt in out_stream.encode(None):
out_c.mux(pkt)
return {"tags_written": tags_written}
+192 -89
View File
@@ -1,8 +1,10 @@
"""
Gallery Metadata Store - ギャラリー画像のメタデータ永続化
"""
import copy
import json
import logging
import threading
from pathlib import Path
logger = logging.getLogger(__name__)
@@ -24,14 +26,27 @@ class GalleryMetadataStore:
{"name": str}
]
}
self._data(特に "images" 辞書)は、バックグラウンドのフォルダインデックススレッド
(gallery_service.pyの_bg_index_folder)と、リクエストハンドラを処理するメインスレッドの
双方から並行に読み書きされる。ロックなしで辞書を反復(.items()/.values()/for key in ...や
json.dumpによるシリアライズ)している最中に別スレッドがキーを追加/削除すると
"dictionary changed size during iteration" が発生するため、_data へアクセスする
全メソッドを self._lock で保護する(re-entrant: ensure_group()がcreate_group()を
内部呼び出しするため、通常のLockではなくRLockを使う)。
"""
def __init__(self, file_path: Path):
self.file_path = file_path
self._lock = threading.RLock()
# ディスクへの実書き込み(JSONシリアライズ+ファイルI/O)を直列化するための
# 別ロック。_save_to_disk() を参照。
self._write_lock = threading.Lock()
self._data: dict = {"images": {}, "groups": []}
self._load()
def _load(self):
# コンストラクタからのみ呼ばれ、この時点ではまだ他スレッドと共有されていないためロック不要
if self.file_path.exists():
try:
with open(self.file_path, "r", encoding="utf-8") as f:
@@ -42,145 +57,233 @@ class GalleryMetadataStore:
}
except Exception as e:
logger.warning("GalleryMetadataStore: load error: %s", e)
# 破損ファイルをリネームしてバックアップしておく。ここでリネームせずに
# 空の self._data のまま次の save() が走ると、破損直前までの全メタデータ
# (タグ/メモ/グループ登録)が跡形もなく上書きされ、二度と復元できなくなる
# (実際に発生した不具合: _save_to_disk が非アトミック書き込みだったため、
# 大量画像フォルダのBGインデックス処理中の頻繁な保存とComfyUIの終了タイミング
# が重なるとファイルが不完全な状態のまま残ることがあった — _save_to_disk側は
# os.replace()によるアトミック書き込みに修正済みだが、過去に壊れたファイルや
# 他要因による破損に備えてここでも保護しておく)。
try:
backup_path = self.file_path.with_name(self.file_path.name + ".corrupt")
self.file_path.replace(backup_path)
logger.warning("GalleryMetadataStore: corrupt file backed up to %s", backup_path)
except Exception as backup_err:
logger.warning("GalleryMetadataStore: failed to back up corrupt file: %s", backup_err)
def _save_to_disk(self):
try:
self.file_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self._data, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
logger.error("GalleryMetadataStore: save error: %s", e)
return False
# 呼び出し元(save/create_group等)が既にself._lockを保持している前提の内部メソッド。
# ここではself._lock配下でスナップショット(deepcopy)を取るだけに留め、実際のJSON
# シリアライズ+ファイルI/Oはロックの外、別途self._write_lockで直列化して行う。
# 理由: gallery_metadata.jsonは画像数が多い環境では1MBを超えることがあり、
# self._lockを保持したままjson.dump+ファイル書き込みまで行うと、その間
# 他スレッドのget()(検索リクエストなど)が丸ごとブロックされてしまう
# (実際に発生した不具合: バックグラウンドインデックス処理が10枚ごとに保存する際、
# 検索リクエストのget()呼び出しがロック待ちになり、50枚程度のフォルダに対する
# 検索でも数秒〜十数秒かかっていた)。deepcopy自体はメモリ内操作でファイルI/Oより
# 大幅に軽いため、ロック内に残しても実用上のブロック時間は無視できるレベルになる。
snapshot = copy.deepcopy(self._data)
return self._write_snapshot(snapshot)
def _write_snapshot(self, snapshot: dict) -> bool:
"""self._lockの外から呼ばれる。一時ファイルに書き出してから置き換える
(os.replace相当のPath.replace)ことで書き込みをアトミックにする。素朴な
open(path, "w") での直接上書きだと、書き込み完了前に他プロセスが読むと
不完全なJSONを掴む(torn read)うえ、書き込み中にComfyUIプロセスが終了すると
ファイルが壊れたまま残る — 大量画像フォルダのBGインデックス処理で書き込み
頻度が高い環境ほど発生しやすく、次回起動時のロード失敗→空データでの全上書き
という形でユーザーの登録済みグループ/タグ/メモが消失する原因になっていた。
self._write_lockで直列化し、複数スレッドの書き込みが競合して古いスナップショット
が新しいものを上書きしないようにする。"""
with self._write_lock:
try:
self.file_path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = self.file_path.with_name(self.file_path.name + ".tmp")
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(snapshot, f, ensure_ascii=False, indent=2)
tmp_path.replace(self.file_path)
return True
except Exception as e:
logger.error("GalleryMetadataStore: save error: %s", e)
return False
def _normalize_path(self, image_path: str) -> str:
return str(Path(image_path)).replace("\\", "/")
def get(self, image_path: str) -> dict:
key = self._normalize_path(image_path)
return dict(self._data["images"].get(key, {}))
with self._lock:
return dict(self._data["images"].get(key, {}))
def save(self, image_path: str, data: dict) -> bool:
key = self._normalize_path(image_path)
existing = self._data["images"].get(key, {})
existing.update(data)
with self._lock:
existing = self._data["images"].get(key, {})
existing.update(data)
allowed = {"favorite", "tags", "memo", "groups", "workflow"}
existing = {k: v for k, v in existing.items() if k in allowed}
self._data["images"][key] = existing
return self._save_to_disk()
def save_batch(self, entries: dict[str, dict]) -> bool:
"""複数画像のメタデータを1回のディスク書き込みでまとめて保存する。
バックグラウンドインデックス処理のように多数の画像を連続して保存する場合、
1件ずつsave()を呼ぶと画像の数だけJSONシリアライズ+ファイルI/Oが発生してしまう
(実際に発生した不具合: gallery_metadata.jsonが1MBを超える環境で10枚ごとに
個別save()していたため、検索リクエストが完了するまで数秒〜十数秒かかっていた)。"""
if not entries:
return True
allowed = {"favorite", "tags", "memo", "groups", "workflow"}
existing = {k: v for k, v in existing.items() if k in allowed}
self._data["images"][key] = existing
return self._save_to_disk()
with self._lock:
for image_path, data in entries.items():
key = self._normalize_path(image_path)
existing = self._data["images"].get(key, {})
existing.update(data)
existing = {k: v for k, v in existing.items() if k in allowed}
self._data["images"][key] = existing
return self._save_to_disk()
# ── グループ管理 ──────────────────────────────────────────────
def list_groups(self) -> list[dict]:
return list(self._data.get("groups", []))
with self._lock:
return list(self._data.get("groups", []))
def create_group(self, name: str) -> bool:
groups = self._data.get("groups", [])
if any(g["name"] == name for g in groups):
return False
groups.append({"name": name})
self._data["groups"] = groups
return self._save_to_disk()
with self._lock:
groups = self._data.get("groups", [])
if any(g["name"] == name for g in groups):
return False
groups.append({"name": name})
self._data["groups"] = groups
return self._save_to_disk()
def rename_group(self, old_name: str, new_name: str) -> bool:
"""グループ名を変更し、全画像のgroupsフィールドも更新する"""
groups = self._data.get("groups", [])
if not any(g["name"] == old_name for g in groups):
return False
if any(g["name"] == new_name for g in groups):
return False # 新名前が既に存在
# グループリスト更新
for g in groups:
if g["name"] == old_name:
g["name"] = new_name
break
# 全画像のgroupsフィールドを更新
for meta in self._data["images"].values():
img_groups = meta.get("groups", [])
if old_name in img_groups:
meta["groups"] = [new_name if g == old_name else g for g in img_groups]
return self._save_to_disk()
with self._lock:
groups = self._data.get("groups", [])
if not any(g["name"] == old_name for g in groups):
return False
if any(g["name"] == new_name for g in groups):
return False # 新名前が既に存在
# グループリスト更新
for g in groups:
if g["name"] == old_name:
g["name"] = new_name
break
# 全画像のgroupsフィールドを更新
for meta in self._data["images"].values():
img_groups = meta.get("groups", [])
if old_name in img_groups:
meta["groups"] = [new_name if g == old_name else g for g in img_groups]
return self._save_to_disk()
def delete_group(self, name: str) -> bool:
groups = [g for g in self._data.get("groups", []) if g["name"] != name]
self._data["groups"] = groups
# 画像のgroupsからも削除
for meta in self._data["images"].values():
if name in meta.get("groups", []):
meta["groups"] = [g for g in meta["groups"] if g != name]
return self._save_to_disk()
with self._lock:
groups = [g for g in self._data.get("groups", []) if g["name"] != name]
self._data["groups"] = groups
# 画像のgroupsからも削除
for meta in self._data["images"].values():
if name in meta.get("groups", []):
meta["groups"] = [g for g in meta["groups"] if g != name]
return self._save_to_disk()
def list_images_in_group(self, group_name: str) -> list[str]:
result = []
for key, meta in self._data["images"].items():
if group_name in meta.get("groups", []):
result.append(key)
return result
with self._lock:
result = []
for key, meta in self._data["images"].items():
if group_name in meta.get("groups", []):
result.append(key)
return result
def remove_stale_paths_from_group(self, group_name: str, stale_paths: list) -> int:
"""存在しないパスをグループから一括削除する(1回の保存で完結)"""
stale_set = {self._normalize_path(p) for p in stale_paths}
count = 0
for key, meta in self._data["images"].items():
if key in stale_set and group_name in meta.get("groups", []):
meta["groups"] = [g for g in meta["groups"] if g != group_name]
count += 1
if count:
self._save_to_disk()
return count
with self._lock:
stale_set = {self._normalize_path(p) for p in stale_paths}
count = 0
for key, meta in self._data["images"].items():
if key in stale_set and group_name in meta.get("groups", []):
meta["groups"] = [g for g in meta["groups"] if g != group_name]
count += 1
if count:
self._save_to_disk()
return count
def get_group_member_set(self, group_name: str) -> set:
"""グループメンバーのパスをsetで返す(高速フィルタ用)"""
return {
key for key, meta in self._data["images"].items()
if group_name in meta.get("groups", [])
}
with self._lock:
return {
key for key, meta in self._data["images"].items()
if group_name in meta.get("groups", [])
}
def clear_group(self, group_name: str) -> bool:
"""グループ内の全画像を除外する(グループ自体は残す)"""
for meta in self._data["images"].values():
if group_name in meta.get("groups", []):
meta["groups"] = [g for g in meta["groups"] if g != group_name]
return self._save_to_disk()
with self._lock:
for meta in self._data["images"].values():
if group_name in meta.get("groups", []):
meta["groups"] = [g for g in meta["groups"] if g != group_name]
return self._save_to_disk()
def ensure_group(self, name: str) -> bool:
"""グループが存在しない場合のみ作成する"""
if any(g["name"] == name for g in self._data.get("groups", [])):
return True
return self.create_group(name)
with self._lock:
if any(g["name"] == name for g in self._data.get("groups", [])):
return True
return self.create_group(name)
def delete(self, image_path: str) -> bool:
"""画像のメタデータエントリを削除する"""
key = self._normalize_path(image_path)
if key in self._data["images"]:
del self._data["images"][key]
return self._save_to_disk()
return True
with self._lock:
if key in self._data["images"]:
del self._data["images"][key]
return self._save_to_disk()
return True
def rename_path(self, old_path: str, new_path: str) -> bool:
"""画像のパスキーを変更する(ファイル移動後のメタデータ引継ぎ)"""
old_key = self._normalize_path(old_path)
new_key = self._normalize_path(new_path)
if old_key in self._data["images"]:
self._data["images"][new_key] = self._data["images"].pop(old_key)
return self._save_to_disk()
return True
with self._lock:
if old_key in self._data["images"]:
self._data["images"][new_key] = self._data["images"].pop(old_key)
return self._save_to_disk()
return True
def cleanup_stale_images(self, folder_path: str, existing_paths: set) -> int:
def cleanup_stale_images(self, folder_path: str, existing_paths: set, recursive: bool = True) -> int:
"""folder_path 配下の存在しないファイルのメタデータを削除する。
existing_paths: そのフォルダの現在のファイルパス集合(_normalize_path 済み)
recursive: False の場合、folder_path 直下のファイルのみを対象にする。
呼び出し元(list_images)が非recursiveスキャン(フォルダ直下のみ列挙)の
結果を existing_paths として渡してくる場合、サブフォルダ内のファイルは
existing_paths に含まれない。それにもかかわらず対象範囲を「folder_path配下
全階層」のままにすると、サブフォルダ内画像のメタデータ(tags/memo等)を
"存在しない"と誤判定して全削除してしまう(実際に発生した不具合: サブフォルダ
に動画を保存する運用で、ルートフォルダを非recursiveで開くたびに
サブフォルダ内の全メタデータが失われていた)。
"""
folder_key = self._normalize_path(folder_path)
if not folder_key.endswith("/"):
folder_key += "/"
stale = [
key for key in self._data["images"]
if key.startswith(folder_key) and key not in existing_paths
]
if not stale:
return 0
for key in stale:
del self._data["images"][key]
self._save_to_disk()
logger.info("Cleaned up %d stale metadata entries from %s", len(stale), folder_path)
return len(stale)
def _in_scope(key: str) -> bool:
if not key.startswith(folder_key):
return False
if recursive:
return True
return "/" not in key[len(folder_key):]
with self._lock:
stale = [
key for key in self._data["images"]
if _in_scope(key) and key not in existing_paths
]
if not stale:
return 0
for key in stale:
del self._data["images"][key]
self._save_to_disk()
logger.info("Cleaned up %d stale metadata entries from %s", len(stale), folder_path)
return len(stale)
File diff suppressed because it is too large Load Diff
+138
View File
@@ -0,0 +1,138 @@
"""Lab tab plan file management service (JSON plan + optional PNG index thumbnail)."""
import base64
import json
import logging
from datetime import datetime, timezone
from pathlib import Path
logger = logging.getLogger(__name__)
class LabPlanService:
def __init__(self, plan_dir: Path):
self.plan_dir = plan_dir
self.plan_dir.mkdir(parents=True, exist_ok=True)
# ------------------------------------------------------------------
# Path safety
# ------------------------------------------------------------------
def _safe_path(self, filename: str, ext: str = ".json") -> Path | None:
if not filename or not filename.strip():
return None
name = filename.strip()
if not name.endswith(ext):
name = name + ext
if "/" in name or "\\" in name or name in (".", "..") or "\x00" in name:
return None
resolved = (self.plan_dir / name).resolve()
plan_dir_resolved = self.plan_dir.resolve()
if resolved != plan_dir_resolved and not str(resolved).startswith(str(plan_dir_resolved) + "\\") and not str(resolved).startswith(str(plan_dir_resolved) + "/"):
return None
return resolved
def _now_iso(self) -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
# ------------------------------------------------------------------
# CRUD
# ------------------------------------------------------------------
def list_plans(self) -> list[dict]:
if not self.plan_dir.is_dir():
return []
result = []
for path in sorted(self.plan_dir.glob("*.json")):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
logger.warning("Skipping unreadable lab plan %s: %s", path.name, e)
continue
stem = path.stem
thumbnail = None
thumb_path = self.plan_dir / f"{stem}.png"
if thumb_path.is_file():
thumbnail = f"/wfm_data/lab_plan/{stem}.png"
result.append({
"filename": path.name,
"name": data.get("name", stem),
"note": data.get("note", ""),
"batch_count": data.get("batch_count", 1),
"updated_at": data.get("updated_at", ""),
"thumbnail": thumbnail,
})
result.sort(key=lambda p: p.get("updated_at", ""), reverse=True)
return result
def get_plan(self, filename: str) -> dict:
path = self._safe_path(filename)
if path is None:
raise ValueError(f"Invalid filename: {filename}")
if not path.is_file():
raise FileNotFoundError(f"Plan not found: {filename}")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_plan(self, filename: str, data: dict, index_image_base64: str | None = None) -> dict:
path = self._safe_path(filename)
if path is None:
raise ValueError(f"Invalid filename: {filename}")
data = dict(data)
data["updated_at"] = self._now_iso()
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
if index_image_base64:
self.save_index_image(filename, index_image_base64)
return {"filename": path.name, "updated_at": data["updated_at"]}
def save_index_image(self, filename: str, image_base64: str) -> None:
path = self._safe_path(filename)
if path is None:
raise ValueError(f"Invalid filename: {filename}")
stem = path.stem
thumb_path = self.plan_dir / f"{stem}.png"
# Accept both raw base64 and data: URL forms
b64 = image_base64
if "," in b64 and b64.strip().lower().startswith("data:"):
b64 = b64.split(",", 1)[1]
image_bytes = base64.b64decode(b64)
with open(thumb_path, "wb") as f:
f.write(image_bytes)
def save_index_image_to_output(self, image_base64: str, filename_prefix: str = "Lab_index") -> dict:
"""Saves the contact-sheet index image into ComfyUI's own output folder,
alongside the images generated during the run (unlike save_index_image,
which saves it next to the plan file in plan_dir)."""
b64 = image_base64
if "," in b64 and b64.strip().lower().startswith("data:"):
b64 = b64.split(",", 1)[1]
image_bytes = base64.b64decode(b64)
import folder_paths # type: ignore
output_dir = folder_paths.get_output_directory()
full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(filename_prefix, output_dir)
save_name = f"{filename}_{counter:05}_.png"
save_path = Path(full_output_folder) / save_name
save_path.write_bytes(image_bytes)
return {"filename": save_name, "subfolder": subfolder}
def delete_plan(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()
thumb_path = self.plan_dir / f"{path.stem}.png"
if thumb_path.is_file():
thumb_path.unlink()
+48
View File
@@ -0,0 +1,48 @@
"""ComfyUI-Lora-Manager 連携: LMが保持済みのsha256を再利用してハッシュ計算を省く。
LMの `/api/lm/{prefix}/list` は sha256 / file_path / file_size を返す(CivitAI詳細は薄いので
情報そのものは使わず、重いハッシュ計算の省略にのみ利用する)。LM未導入・失敗時は空を返す。
"""
import json
import logging
import os
from urllib.request import urlopen
logger = logging.getLogger(__name__)
# 当アプリのモデルタイプ → LMのルート接頭辞
_LM_PREFIX = {"checkpoint": "checkpoints", "lora": "loras", "embedding": "embeddings"}
_PAGE_SIZE = 100 # LM側の上限
_MAX_PAGES = 200
_TIMEOUT = 5
def _norm(path):
return os.path.normcase(os.path.abspath(path))
def fetch_lm_hashes(origin, model_type):
"""Returns { normalized_file_path: {"sha256", "size"} }。LMが使えなければ {}。"""
prefix = _LM_PREFIX.get(model_type)
if not prefix:
return {}
out = {}
try:
page = 1
while page <= _MAX_PAGES:
url = f"{origin}/api/lm/{prefix}/list?page={page}&page_size={_PAGE_SIZE}"
with urlopen(url, timeout=_TIMEOUT) as resp:
data = json.loads(resp.read().decode("utf-8"))
for it in data.get("items", []):
sha, fp = it.get("sha256"), it.get("file_path")
if sha and fp and len(str(sha)) == 64:
out[_norm(fp)] = {"sha256": str(sha).lower(), "size": it.get("file_size")}
if page >= int(data.get("total_pages", 1) or 1):
break
page += 1
except Exception as e:
logger.info("Lora Manager hashes unavailable (%s): %s", prefix, e)
return {}
return out
+149 -25
View File
@@ -2,6 +2,7 @@
import json
import logging
import os
import shutil
import time
from datetime import datetime, timezone
@@ -41,32 +42,126 @@ MODEL_TYPE_FOLDER_KEYS = {
}
def _iter_files(root):
"""root 配下の全ファイルを返す。フォルダのシンボリックリンク/ジャンクションも辿る。
リンクが祖先フォルダを指していて循環する場合のみ、その枝には入らない
(rglob だとパス長上限まで再帰し続ける恐れがある)。
"""
def walk(d, ancestors):
try:
entries = list(os.scandir(d))
except OSError:
return
for e in entries:
try:
if e.is_dir(): # リンクは辿る
real = os.path.realpath(e.path)
if real in ancestors:
logger.warning("Skipping looping folder link: %s -> %s", e.path, real)
continue
yield from walk(e.path, ancestors | {real})
elif e.is_file():
yield Path(e.path)
except OSError:
continue
root = Path(root)
yield from walk(root, frozenset([os.path.realpath(root)]))
def _is_within(path, root):
"""path が root 配下か。`..` は正規化するが、リンクは解決しない(リンク先フォルダへの移動を許可)。"""
try:
a, r = os.path.abspath(path), os.path.abspath(root)
return os.path.commonpath([os.path.normcase(a), os.path.normcase(r)]) == os.path.normcase(r)
except ValueError: # 別ドライブなど
return False
# 追加モデルルート配下で探すサブフォルダ名(大小無視)。ComfyUI標準名 + Stability Matrix名
_EXTRA_ROOT_SUBDIRS = {
"checkpoint": ["checkpoints", "StableDiffusion"],
"lora": ["loras", "Lora"],
"vae": ["vae", "VAE"],
"controlnet": ["controlnet", "ControlNet"],
"unet": ["diffusion_models", "DiffusionModels", "unet"],
"textencoder": ["text_encoders", "TextEncoders", "clip"],
"hypernetwork": ["hypernetworks", "HyperNetworks"],
"embedding": ["embeddings", "Embeddings"],
}
def parse_extra_model_roots(value):
"""設定値(`;` または改行区切りの文字列)を重複なしのパス文字列リストにする。"""
if isinstance(value, (list, tuple)):
value = ";".join(str(v) for v in value)
parts = str(value or "").replace("\n", ";").split(";")
seen, out = set(), []
for p in parts:
p = p.strip().strip('"')
if p and p not in seen:
seen.add(p)
out.append(p)
return out
def _get_extra_model_dirs(model_type):
"""設定 `models_dir` で追加指定されたルート配下の、該当タイプのフォルダを返す。"""
names = _EXTRA_ROOT_SUBDIRS.get(model_type)
if not names:
return []
try:
from .settings_service import SettingsService
roots = parse_extra_model_roots(SettingsService().load().get("models_dir", ""))
except Exception:
return []
wanted = {n.lower() for n in names}
found = []
for root in roots:
try:
for e in os.scandir(root):
if e.is_dir() and e.name.lower() in wanted:
found.append(Path(e.path))
except OSError:
logger.warning("Configured models_dir not accessible: %s", root)
return found
def _get_model_dirs(model_type):
"""Get all model directories for a type using ComfyUI's folder_paths.
Returns a list of Path objects for all configured model directories
(includes extra_model_paths.yaml settings).
(includes extra_model_paths.yaml settings and the `models_dir` setting).
Falls back to plugin-relative path if folder_paths is unavailable.
"""
folder_key = MODEL_TYPE_FOLDER_KEYS.get(model_type)
if not folder_key:
return []
result = []
try:
import folder_paths # type: ignore # ComfyUI module
paths = folder_paths.get_folder_paths(folder_key)
result = [Path(p) for p in paths if Path(p).is_dir()]
if result:
return result
except Exception as e:
logger.debug("folder_paths unavailable (%s), using fallback", e)
# Fallback: custom_nodes/../../models/{folder_key}
plugin_dir = Path(__file__).resolve().parent.parent.parent
models_dir = plugin_dir.parent.parent / "models" / folder_key
if models_dir.is_dir():
return [models_dir]
return []
if not result:
# Fallback: custom_nodes/../../models/{folder_key}
plugin_dir = Path(__file__).resolve().parent.parent.parent
models_dir = plugin_dir.parent.parent / "models" / folder_key
if models_dir.is_dir():
result = [models_dir]
# 設定で追加されたルート(既存と同一実体は除外)
known = {os.path.normcase(os.path.realpath(p)) for p in result}
for d in _get_extra_model_dirs(model_type):
key = os.path.normcase(os.path.realpath(d))
if key not in known:
known.add(key)
result.append(d)
return result
class ModelsService:
@@ -111,6 +206,12 @@ class ModelsService:
entry["sha256"] = updates["sha256"]
if "badges" in updates:
entry["badges"] = updates["badges"]
if "civitaiNotFound" in updates:
# CivitAIに存在しなかった記録(一括取得のスキップ用)。None/False で解除
if updates["civitaiNotFound"]:
entry["civitaiNotFound"] = self._now_iso()
else:
entry.pop("civitaiNotFound", None)
entry["updatedAt"] = self._now_iso()
data[model_name] = entry
self._save_metadata(data)
@@ -131,9 +232,7 @@ class ModelsService:
for d in dirs:
if not d.is_dir():
continue
for f in d.rglob("*"):
if not f.is_file():
continue
for f in _iter_files(d):
try:
rel = str(f.relative_to(d)).replace("\\", "/")
if rel.endswith(_DISABLED_SUFFIX):
@@ -145,17 +244,16 @@ class ModelsService:
self._scan_cache[model_type] = (now, names)
return names
def list_model_files(self, model_type: str) -> list:
def list_model_files(self, model_type: str, extra_only: bool = False) -> list:
"""モデルタイプのモデルファイル名をソートして返す(拡張子付き、/区切り)。
プレビュー画像やサイドカーファイルを除外し、モデル拡張子のみを返す。"""
dirs = _get_model_dirs(model_type)
プレビュー画像やサイドカーファイルを除外し、モデル拡張子のみを返す。
extra_only=True なら設定 models_dir で追加されたフォルダのみ対象。"""
dirs = _get_extra_model_dirs(model_type) if extra_only else _get_model_dirs(model_type)
seen = set()
for d in dirs:
if not d.is_dir():
continue
for f in d.rglob("*"):
if not f.is_file():
continue
for f in _iter_files(d):
name = f.name
if name.endswith(_DISABLED_SUFFIX):
name = name[: -len(_DISABLED_SUFFIX)]
@@ -280,8 +378,8 @@ class ModelsService:
for d in dirs:
if not d.is_dir():
continue
for f in d.rglob("*"):
if f.is_file() and f.name.endswith(_DISABLED_SUFFIX):
for f in _iter_files(d):
if f.name.endswith(_DISABLED_SUFFIX):
try:
rel = str(f.relative_to(d))
if rel.endswith(_DISABLED_SUFFIX):
@@ -291,6 +389,32 @@ class ModelsService:
pass
return disabled
def list_preview_keys(self, model_type):
"""プレビュー画像を持つモデルのキー(`サブフォルダ/ファイル名(拡張子なし)`、小文字・/区切り)を返す。
一覧表示で「プレビューが無いモデルには画像リクエストを出さない」ために、フォルダを1回走査して求める
(画像ごとの404リクエストと接続の開閉を避ける)。判定は find_preview_image と同じ拡張子・サイズ条件。
"""
keys = set()
for d in _get_model_dirs(model_type):
if not d.is_dir():
continue
for f in _iter_files(d):
lname = f.name.lower()
exts = [e for e in _PREVIEW_EXTENSIONS if lname.endswith(e)]
if not exts:
continue
try:
if f.stat().st_size < 100:
continue
parent = str(f.parent.relative_to(d)).replace("\\", "/")
except (OSError, ValueError):
continue
prefix = "" if parent == "." else parent.lower() + "/"
for e in exts:
keys.add(prefix + lname[: -len(e)])
return sorted(keys)
def find_preview_image(self, model_type, model_name):
"""Find preview image for a model file.
@@ -311,7 +435,8 @@ class ModelsService:
for type_dir in dirs:
# model_name can include subdirectory (e.g., "subdir/model.safetensors")
model_path = type_dir / model_name
if not model_path.is_file():
# 無効化されたモデル(<name>.disabled)でも、隣のプレビュー画像は探す
if not model_path.is_file() and not (type_dir / (model_name + _DISABLED_SUFFIX)).is_file():
continue
stem = model_path.stem
@@ -436,11 +561,10 @@ class ModelsService:
errors.append({"model": model_name, "error": "Cannot determine root directory"})
continue
# Destination directory — verify it stays within root_dir after resolution
# Destination directory — verify it stays within root_dir (`..`/絶対パスは拒否。
# フォルダのシンボリックリンク/ジャンクション配下への移動は許可する)
dest_dir = root_dir / dest_subdir if dest_subdir else root_dir
try:
dest_dir.resolve().relative_to(root_dir.resolve())
except ValueError:
if not _is_within(dest_dir, root_dir):
errors.append({"model": model_name, "error": "Destination is outside model root"})
continue
dest_dir.mkdir(parents=True, exist_ok=True)
+1 -2
View File
@@ -44,8 +44,7 @@ def extract_png_workflow(buf):
except Exception:
pass
# IENDで止めない: IENDの後ろに追記されたworkflowチャンクも読む
pos += length + 12
if chunk_type == "IEND":
break
return None
+211
View File
@@ -0,0 +1,211 @@
"""
EmbeddingGemma 2 を Ollama で実行する (Gallery セマンティック検索のローカルバックエンド)
画像は /api/embed に input=[{"image": <base64>}] で渡す(images フィールドは無視されテキスト扱いになる)。
740m(テキスト+画像+音声) と 440m(テキスト+画像) は同じ重みで同じベクトルを返すため、
インデックスにはタグを区別せず LOCAL_MODEL として記録する。270m/570m は画像非対応なので選択肢に入れない。
接続先は url_guard で検査する(既定はループバックのみ)。
"""
import base64
import io
import json
import logging
import threading
import time
import urllib.error
import urllib.request
import numpy as np
from . import url_guard
logger = logging.getLogger(__name__)
LOCAL_MODEL = "embeddinggemma-2" # インデックス(npz)に記録するモデル名
LOCAL_DIMS = (128, 256, 512, 768) # MRL対応次元
DEFAULT_LOCAL_DIM = 768
TAGS = ("embeddinggemma-2:740m", "embeddinggemma-2:440m")
DEFAULT_TAG = TAGS[0]
# :latest は 740m と同じモデル(parent_model 740m-q8_0)。740m が無ければ代わりに使う
_ALIASES = {"embeddinggemma-2:740m": ("embeddinggemma-2:latest", "embeddinggemma-2")}
DEFAULT_URL = "http://localhost:11434"
QUERY_PREFIX = "task: search result | query: "
BATCH = 4 # 1リクエストで送る画像数(1枚ずつより約1.8倍速い)
_TIMEOUT = 120
_RETRIES = 5 # Ollama内部の接続切断(HTTP 400 "forcibly closed"等)は再試行で回復する
# Ollama はテキストごとに内部のランナーへ新しい接続を張るため、数千件を続けて埋め込むと
# Windows の一時ポートが TIME_WAIT で埋まり "Only one usage of each socket address" で失敗する
# (実測: ループバックの TIME_WAIT 14,228 / 16,384)。ポートが空くまで長めに待って再試行する
_PORT_WAIT = 30.0
_TAGS_TTL = 5.0 # /api/tags の結果をキャッシュする秒数(進捗ポーリングのたびに問い合わせない)
_opts = {"url": DEFAULT_URL, "tag": DEFAULT_TAG}
_tags_cache = {"at": 0.0, "url": "", "names": None, "error": ""}
_lock = threading.Lock()
class OllamaError(Exception):
"""code: ollama_unreachable / ollama_model_missing / local_error"""
def __init__(self, code: str, message: str = ""):
super().__init__(message or code)
self.code = code
def configure(url: str = DEFAULT_URL, tag: str = DEFAULT_TAG) -> None:
"""接続先とモデルサイズを設定する。許可されないURLは ValueError(url_guard.HINT)。"""
url = url_guard.require_backend_url((url or "").strip() or DEFAULT_URL)
with _lock:
_opts["url"] = url
_opts["tag"] = tag if tag in TAGS else DEFAULT_TAG
_tags_cache["at"] = 0.0
def current_config() -> dict:
return dict(_opts)
def _post(path: str, body: dict, timeout: float = _TIMEOUT) -> dict:
base = url_guard.require_backend_url(_opts["url"])
req = urllib.request.Request(base + path, data=json.dumps(body).encode("utf-8"), method="POST",
headers={"Content-Type": "application/json"})
with url_guard.urlopen(req, timeout) as res:
return json.loads(res.read().decode("utf-8"))
def _http_message(e: urllib.error.HTTPError) -> str:
try:
return json.loads(e.read().decode("utf-8")).get("error", "") or str(e.reason)
except Exception:
return str(e.reason)
def _transient(code: int, msg: str) -> bool:
low = msg.lower()
if "failed to load image" in low:
return False # 壊れた画像。再試行しても直らない
if code >= 500:
return True
return code == 400 and (_ports_exhausted(msg)
or any(k in low for k in ("forcibly closed", "wsarecv", "connection", "eof")))
def _ports_exhausted(msg: str) -> bool:
low = msg.lower()
return "only one usage of each socket address" in low or "wsaeaddrinuse" in low
def installed_tags(force: bool = False) -> tuple[list[str] | None, str]:
"""(インストール済みモデル名, エラー)。接続できなければ (None, エラー文)。"""
now = time.time()
with _lock:
c = dict(_tags_cache)
url = _opts["url"]
if not force and c["url"] == url and now - c["at"] < _TAGS_TTL:
return c["names"], c["error"]
names, err = None, ""
try:
base = url_guard.require_backend_url(url)
with url_guard.urlopen(urllib.request.Request(base + "/api/tags"), 3) as res:
data = json.loads(res.read().decode("utf-8"))
names = [m.get("name") or m.get("model") or "" for m in data.get("models", [])]
except Exception as e:
err = str(getattr(e, "reason", e))
with _lock:
_tags_cache.update(at=now, url=url, names=names, error=err)
return names, err
def _resolve(names: list[str], tag: str) -> str:
"""実際に呼ぶモデル名。未インストールなら空文字"""
for cand in (tag,) + _ALIASES.get(tag, ()):
if cand in names:
return cand
return ""
def status() -> dict:
names, err = installed_tags()
tag = _opts["tag"]
out = {"url": _opts["url"], "tag": tag, "tags": list(TAGS), "reachable": names is not None, "error": err,
"installed": {t: bool(names and _resolve(names, t)) for t in TAGS}}
out["ready"] = bool(out["reachable"] and out["installed"][tag])
return out
def _model_name() -> str:
names, err = installed_tags()
if names is None:
raise OllamaError("ollama_unreachable", f"{_opts['url']}: {err}")
name = _resolve(names, _opts["tag"])
if not name:
raise OllamaError("ollama_model_missing", _opts["tag"])
return name
def _embed(inputs: list) -> list[np.ndarray]:
model = _model_name()
last = ""
for attempt in range(_RETRIES + 1):
try:
data = _post("/api/embed", {"model": model, "input": inputs})
embs = data.get("embeddings") or []
if len(embs) != len(inputs):
raise OllamaError("local_error", f"expected {len(inputs)} embeddings, got {len(embs)}")
return [np.asarray(e, dtype=np.float32) for e in embs]
except urllib.error.HTTPError as e:
msg = _http_message(e)
last = f"HTTP {e.code}: {msg}"
if e.code == 404:
with _lock:
_tags_cache["at"] = 0.0 # 削除された可能性があるので次回は問い合わせ直す
raise OllamaError("ollama_model_missing", model)
if not _transient(e.code, msg) or attempt >= _RETRIES:
raise OllamaError("local_error", last)
if _ports_exhausted(msg):
logger.warning("Ollama: local ports exhausted, waiting %.0fs before retry", _PORT_WAIT)
time.sleep(_PORT_WAIT)
continue
except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
last = str(getattr(e, "reason", e))
if attempt >= _RETRIES:
raise OllamaError("ollama_unreachable", f"{_opts['url']}: {last}")
time.sleep(min(4.0, 0.5 * (2 ** attempt)))
raise OllamaError("local_error", last)
def _finish(vec: np.ndarray, dim: int) -> np.ndarray:
v = vec[:dim] if dim < vec.shape[0] else vec
n = float(np.linalg.norm(v))
return v / n if n > 0 else v
def _image_b64(image) -> str:
buf = io.BytesIO()
image.convert("RGB").save(buf, "JPEG", quality=88)
return base64.b64encode(buf.getvalue()).decode("ascii")
def embed_text(text: str, dim: int) -> np.ndarray:
return embed_texts([text], dim)[0]
def embed_texts(texts: list[str], dim: int) -> list[np.ndarray]:
"""検索文(クエリ)として複数のテキストをまとめて埋め込む"""
return [_finish(v, dim) for v in _embed([QUERY_PREFIX + t[:8000].strip() for t in texts])]
def embed_images(images: list, dim: int) -> list[np.ndarray]:
"""PIL画像(RGB)のリストをまとめて埋め込む。1枚でも読めないとリクエスト全体が失敗する。"""
return [_finish(v, dim) for v in _embed([{"image": _image_b64(im)} for im in images])]
def release() -> None:
"""モデルをVRAMから外す(インデックス作成後)。次回利用時にOllamaが再ロードする。"""
try:
names, _ = installed_tags()
name = _resolve(names or [], _opts["tag"])
if name:
_post("/api/embed", {"model": name, "input": [], "keep_alive": 0}, timeout=30)
except Exception as e:
logger.debug("semantic_ollama: release failed: %s", e)
+582
View File
@@ -0,0 +1,582 @@
"""
Semantic Service - Gallery用セマンティック検索(Gemini Embedding 2 / Ollama の EmbeddingGemma 2)
画像(動画・PSDはサムネイル)をGemini Embedding API または Ollama でベクトル化してローカルに保存し、
テキストクエリ/類似画像でコサイン類似度検索する。インデックス構築後の検索は
(テキストクエリの埋め込み1回を除き)ローカル処理でAPI費用はかからない。
APIキーは環境変数 GEMINI_API_KEY (または GOOGLE_API_KEY) から読む(.env経由)。
settings.json やフロントエンドには出さない。接続先は固定ホストのみ(SSRF対策)。
"""
import io
import json
import logging
import os
import threading
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import numpy as np
from . import semantic_ollama as local
logger = logging.getLogger(__name__)
API_BASE = "https://generativelanguage.googleapis.com/v1beta"
MODEL = "gemini-embedding-2" # Gemini API (クラウド)
LOCAL_MODEL = local.LOCAL_MODEL # EmbeddingGemma 2 (Ollama)
DEFAULT_DIM = 1536
VALID_DIMS = (768, 1536, 3072)
# バックエンド(モデル名) -> 選択可能な出力次元
DIMS_BY_MODEL = {MODEL: VALID_DIMS, LOCAL_MODEL: local.LOCAL_DIMS}
DEFAULT_DIM_BY_MODEL = {MODEL: DEFAULT_DIM, LOCAL_MODEL: local.DEFAULT_LOCAL_DIM}
IMAGE_MAX_SIZE = 768 # 送信前の長辺リサイズ(px)
_MAX_QUERY_CHARS = 2000 # 検索文の最大長
_WORKERS = 4 # 並列API呼び出し数
_CHECKPOINT_EVERY = 100 # この件数ごとにディスクへ保存
_RETRIES = 4
# PILで直接開ける(静止画/GIFは先頭フレーム)
_PIL_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"}
# GalleryService.serve_thumbnail 経由でJPEG化する(動画先頭フレーム/PSD合成)
_THUMB_EXTS = {".mp4", ".webm", ".psd"}
INDEXABLE_EXTS = _PIL_EXTS | _THUMB_EXTS
class SemanticError(Exception):
"""ユーザーに見せられるエラー(code はフロントのi18n用)"""
def __init__(self, code: str, message: str = ""):
super().__init__(message or code)
self.code = code
def get_api_key() -> str:
return (os.environ.get("GEMINI_API_KEY", "") or os.environ.get("GOOGLE_API_KEY", "")).strip()
def _normalize(v: np.ndarray) -> np.ndarray:
n = float(np.linalg.norm(v))
return v / n if n > 0 else v
def _call_embed(content: dict, task_type: str, dim: int) -> np.ndarray:
key = get_api_key()
if not key:
raise SemanticError("no_api_key", "GEMINI_API_KEY is not set")
body = {"model": f"models/{MODEL}", "content": content, "taskType": task_type}
if dim != 768:
body["outputDimensionality"] = dim
data = json.dumps(body).encode("utf-8")
url = f"{API_BASE}/models/{MODEL}:embedContent"
last_err = ""
for attempt in range(_RETRIES + 1):
req = urllib.request.Request(
url, data=data, method="POST",
headers={"Content-Type": "application/json", "x-goog-api-key": key},
)
try:
with urllib.request.urlopen(req, timeout=60) as res:
payload = json.loads(res.read().decode("utf-8"))
values = (payload.get("embedding") or {}).get("values")
if not values:
raise SemanticError("bad_response", "no embedding.values in response")
return _normalize(np.asarray(values, dtype=np.float32))
except urllib.error.HTTPError as e:
try:
msg = json.loads(e.read().decode("utf-8")).get("error", {}).get("message", "")
except Exception:
msg = ""
last_err = f"HTTP {e.code}: {msg or e.reason}"
if e.code in (400, 403) and ("API key" in msg or "API_KEY" in msg or e.code == 403):
raise SemanticError("api_key_invalid", last_err)
if e.code in (429, 500, 502, 503, 504) and attempt < _RETRIES:
time.sleep(1.5 * (2 ** attempt))
continue
raise SemanticError("api_error", last_err)
except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
last_err = str(getattr(e, "reason", e))
if attempt < _RETRIES:
time.sleep(1.5 * (attempt + 1))
continue
raise SemanticError("network_error", last_err)
raise SemanticError("api_error", last_err)
def embed_text(text: str, dim: int) -> np.ndarray:
return _call_embed({"parts": [{"text": text[:8000]}]}, "RETRIEVAL_QUERY", dim)
def embed_image_bytes(data: bytes, mime: str, dim: int) -> np.ndarray:
import base64
b64 = base64.b64encode(data).decode("ascii")
return _call_embed({"parts": [{"inlineData": {"mimeType": mime, "data": b64}}]}, "RETRIEVAL_DOCUMENT", dim)
class SemanticService:
def __init__(self, data_dir: Path, gallery_service):
self.data_dir = data_dir
self.gallery = gallery_service
self._file = data_dir / "semantic_index.npz"
self._lock = threading.RLock()
self._vecs: dict[str, np.ndarray] = {} # path -> 正規化済みベクトル
self._stamp: dict[str, tuple[int, float]] = {} # path -> (size, mtime)
self._dim = 0 # インデックスの次元(0=空)
self._model = MODEL # インデックスを作ったモデル
self._settings_file = data_dir / "semantic_settings.json"
self._load_settings()
self._mat_cache = None # (paths, matrix)
self._loaded = False
self._dirty = 0
# ジョブ
self._job = self._idle_job()
self._cancel = threading.Event()
self._thread: threading.Thread | None = None
# ── Ollama の接続先/モデルサイズ ────────────────────────
def _load_settings(self) -> None:
try:
if self._settings_file.exists():
d = json.loads(self._settings_file.read_text(encoding="utf-8"))
local.configure(str(d.get("url", local.DEFAULT_URL)), str(d.get("tag", local.DEFAULT_TAG)))
except Exception as e:
logger.warning("SemanticService: failed to load settings: %s", e)
def set_local_options(self, url: str, tag: str) -> None:
"""Ollama の接続先/モデルサイズを反映して保存する(検索時のクエリ埋め込みにも使われる)"""
try:
local.configure(url, tag)
except ValueError as e:
raise SemanticError("bad_url", str(e))
try:
self._settings_file.write_text(json.dumps(local.current_config()), encoding="utf-8")
except Exception as e:
logger.warning("SemanticService: failed to save settings: %s", e)
# ── 永続化 ─────────────────────────────────────────────
@staticmethod
def _idle_job() -> dict:
return {"running": False, "total": 0, "done": 0, "failed": 0, "current": "",
"error": "", "error_code": "", "cancelled": False, "finished": False}
def _ensure_loaded(self) -> None:
with self._lock:
if self._loaded:
return
self._loaded = True
if not self._file.exists():
return
try:
with np.load(self._file, allow_pickle=False) as z:
model = str(z["model"])
if model not in DIMS_BY_MODEL:
return
self._model = model
paths = [str(p) for p in z["paths"]]
sizes, mtimes, vecs = z["sizes"], z["mtimes"], z["vecs"]
self._dim = int(z["dim"])
for i, p in enumerate(paths):
self._vecs[p] = vecs[i]
self._stamp[p] = (int(sizes[i]), float(mtimes[i]))
except Exception as e:
logger.warning("SemanticService: failed to load index: %s", e)
self._vecs.clear()
self._stamp.clear()
self._dim = 0
def _save(self) -> None:
with self._lock:
if not self._vecs:
if self._file.exists():
self._file.unlink()
self._dirty = 0
return
paths = list(self._vecs.keys())
tmp = self._file.with_suffix(".tmp.npz")
np.savez(
tmp,
model=np.array(self._model), dim=np.array(self._dim),
paths=np.array(paths),
sizes=np.array([self._stamp[p][0] for p in paths], dtype=np.int64),
mtimes=np.array([self._stamp[p][1] for p in paths], dtype=np.float64),
vecs=np.stack([self._vecs[p] for p in paths]).astype(np.float32),
)
os.replace(tmp, self._file)
self._dirty = 0
def _put(self, path: str, vec: np.ndarray, size: int, mtime: float) -> None:
with self._lock:
self._vecs[path] = vec
self._stamp[path] = (size, mtime)
self._mat_cache = None
self._dirty += 1
# ── 状態 ───────────────────────────────────────────────
def _scope_entries(self, folder: str, recursive: bool) -> list[tuple[str, str, int, float]]:
try:
p = self.gallery._resolve_untrusted(folder) # UNC等は resolve() 前に文字列で拒否
except ValueError:
raise SemanticError("folder_not_allowed", str(folder))
if not p.is_dir() or not self.gallery._check_path_allowed(p):
raise SemanticError("folder_not_allowed", str(folder))
raw = self.gallery._scan_folder_recursive(p) if recursive else self.gallery._scan_folder(p)
return [e for e in raw if Path(e[0]).suffix.lower() in INDEXABLE_EXTS]
def status(self, folder: str = "", recursive: bool = True) -> dict:
self._ensure_loaded()
ollama = local.status()
out = {
"has_key": bool(get_api_key()),
"model": MODEL,
"backends": {
MODEL: {"dims": list(VALID_DIMS), "default_dim": DEFAULT_DIM, "ready": bool(get_api_key())},
LOCAL_MODEL: {"dims": list(local.LOCAL_DIMS), "default_dim": local.DEFAULT_LOCAL_DIM,
"ready": ollama["ready"]},
},
"index_model": self._model,
"index_dim": self._dim,
"ollama": ollama,
"indexed_total": len(self._vecs),
"job": dict(self._job),
}
if folder:
try:
entries = self._scope_entries(folder, recursive)
with self._lock:
done = sum(1 for _, ap, sz, mt in entries if self._is_fresh(ap, sz, mt))
out["scope_total"] = len(entries)
out["scope_indexed"] = done
except SemanticError:
pass
return out
def folder_status(self, root: str) -> dict:
"""フォルダごとの索引状況 {絶対パス: {"indexable": n, "indexed": m}} (直下の画像のみ。Galleryツリーの枚数表示と同じ単位)"""
self._ensure_loaded()
entries = self._scope_entries(root, True)
out: dict[str, dict] = {}
with self._lock:
for _, ap, sz, mt in entries:
d = out.setdefault(ap.rsplit("/", 1)[0], {"indexable": 0, "indexed": 0})
d["indexable"] += 1
if self._is_fresh(ap, sz, mt):
d["indexed"] += 1
return out
def _is_fresh(self, path: str, size: int, mtime: float) -> bool:
st = self._stamp.get(path)
return st is not None and st[0] == size and abs(st[1] - mtime) < 1e-3
# ── インデックス作成ジョブ ────────────────────────────
def start_index(self, folder: str, recursive: bool, mode: str, model: str, dim: int) -> None:
self._ensure_loaded()
if model not in DIMS_BY_MODEL:
raise SemanticError("bad_model", model)
if model == MODEL and not get_api_key():
raise SemanticError("no_api_key")
if dim not in DIMS_BY_MODEL[model]:
raise SemanticError("bad_dim", str(dim))
if model == LOCAL_MODEL:
st = local.status() # 接続できない/モデル未取得なら、ジョブを始める前に知らせる
if not st["reachable"]:
raise SemanticError("ollama_unreachable", f"{st['url']}: {st['error']}")
if not st["ready"]:
raise SemanticError("ollama_model_missing", st["tag"])
with self._lock:
if self._job["running"]:
raise SemanticError("already_running")
entries = self._scope_entries(folder, recursive)
mismatch = bool(self._vecs) and (self._dim != dim or self._model != model)
if mismatch and mode != "all":
raise SemanticError("dim_mismatch", f"{self._model}/{self._dim}")
if mode == "all":
# 範囲内の既存ベクトルを破棄して作り直す(モデル/次元違いなら全消去)
if mismatch:
self._vecs.clear()
self._stamp.clear()
else:
for _, ap, _, _ in entries:
self._vecs.pop(ap, None)
self._stamp.pop(ap, None)
self._mat_cache = None
self._dim = dim
self._model = model
todo = [e for e in entries if not self._is_fresh(e[1], e[2], e[3])]
# スコープ内で消えたファイルの残骸を掃除(スコープ配下のキーのみ)
self._prune_missing(folder, recursive, {e[1] for e in entries})
self._cancel.clear()
self._job = self._idle_job()
self._job.update(running=True, total=len(todo))
self._thread = threading.Thread(target=self._run_index, args=(todo, model, dim), daemon=True)
self._thread.start()
def _prune_missing(self, folder: str, recursive: bool, existing: set[str]) -> None:
base = Path(folder).resolve().as_posix().rstrip("/") + "/"
for p in list(self._vecs.keys()):
if not p.startswith(base):
continue
if not recursive and "/" in p[len(base):]:
continue
if p not in existing:
self._vecs.pop(p, None)
self._stamp.pop(p, None)
self._mat_cache = None
self._dirty += 1
def cancel(self) -> None:
self._cancel.set()
def _run_index(self, todo: list, model: str, dim: int) -> None:
def fail(e: SemanticError) -> None:
if e.code in ("no_api_key", "api_key_invalid", "ollama_unreachable", "ollama_model_missing"):
# 続けても全件失敗するので中止する
self._job["error_code"], self._job["error"] = e.code, str(e)
self._cancel.set()
else:
self._job["failed"] += 1
if not self._job["error"]:
self._job["error_code"], self._job["error"] = e.code, str(e)
def checkpoint() -> None:
if self._dirty >= _CHECKPOINT_EVERY:
try:
self._save()
except Exception as e:
logger.warning("SemanticService: checkpoint failed: %s", e)
def work(entry):
if self._cancel.is_set():
return
name, ap, size, mtime = entry
self._job["current"] = name
try:
vec = self._embed_image(model, dim, Path(ap))
self._put(ap, vec, size, mtime)
self._job["done"] += 1
except SemanticError as e:
fail(e)
except Exception as e:
logger.warning("SemanticService: embed failed for %s: %s", ap, e)
self._job["failed"] += 1
checkpoint()
def work_batch(batch):
"""Ollama: まとめて送る。失敗したら1枚ずつ送り直して原因の画像だけを失敗扱いにする"""
if self._cancel.is_set():
return
self._job["current"] = batch[0][0]
vecs = None
try:
ims = [self._load_local_image(Path(e[1])) for e in batch]
vecs = local.embed_images(ims, dim)
except local.OllamaError as e:
if e.code != "local_error":
fail(SemanticError(e.code, str(e)))
return
except Exception:
pass
if vecs is None:
for entry in batch:
work(entry)
return
for (name, ap, size, mtime), vec in zip(batch, vecs):
self._put(ap, vec, size, mtime)
self._job["done"] += 1
checkpoint()
try:
if model == LOCAL_MODEL:
for i in range(0, len(todo), local.BATCH):
work_batch(todo[i:i + local.BATCH])
else:
with ThreadPoolExecutor(max_workers=_WORKERS) as ex:
list(ex.map(work, todo))
finally:
try:
self._save()
except Exception as e:
logger.warning("SemanticService: save failed: %s", e)
if model == LOCAL_MODEL:
local.release() # 画像生成とVRAMを奪い合わないよう、作成後はモデルを外す(検索時にOllamaが再ロード)
self._job["running"] = False
self._job["finished"] = True
self._job["cancelled"] = self._cancel.is_set() and not self._job["error_code"]
self._job["current"] = ""
def _load_pil(self, path: Path):
"""埋め込み用に縮小したPIL画像を返す(動画/PSDはサムネイルから)"""
from PIL import Image
if path.suffix.lower() in _THUMB_EXTS:
thumb = self.gallery.serve_thumbnail(str(path), IMAGE_MAX_SIZE)
if not thumb:
raise SemanticError("thumb_failed", str(path))
with Image.open(thumb) as im:
return im.convert("RGB")
with Image.open(path) as im:
im.seek(0)
im = im.convert("RGBA") if im.mode in ("RGBA", "LA", "P") else im.convert("RGB")
im.thumbnail((IMAGE_MAX_SIZE, IMAGE_MAX_SIZE), Image.LANCZOS)
return im
def _load_local_image(self, path: Path):
"""Ollama用のRGB画像(透過部は白背景に合成)"""
from PIL import Image
im = self._load_pil(path)
if im.mode == "RGBA":
bg = Image.new("RGB", im.size, (255, 255, 255))
bg.paste(im, mask=im.getchannel("A"))
return bg
return im
def _embed_image(self, model: str, dim: int, path: Path) -> np.ndarray:
if model == LOCAL_MODEL:
im = self._load_local_image(path)
try:
return local.embed_images([im], dim)[0]
except local.OllamaError as e:
raise SemanticError(e.code, str(e))
im = self._load_pil(path)
buf = io.BytesIO()
if im.mode == "RGBA":
im.save(buf, "PNG")
return embed_image_bytes(buf.getvalue(), "image/png", dim)
im.save(buf, "JPEG", quality=88)
return embed_image_bytes(buf.getvalue(), "image/jpeg", dim)
def _embed_text(self, model: str, dim: int, text: str) -> np.ndarray:
if model == LOCAL_MODEL:
try:
return local.embed_text(text, dim)
except local.OllamaError as e:
raise SemanticError(e.code, str(e))
return embed_text(text, dim)
# ── 他機能(TagSpace)向けの読み出し ─────────────────────
def index_info(self) -> dict:
self._ensure_loaded()
with self._lock:
return {"model": self._model, "dim": self._dim, "count": len(self._vecs)}
def job_status(self) -> dict:
return dict(self._job)
def vectors_for(self, entries: list) -> dict[str, np.ndarray]:
"""entries([(name, abs_path, size, mtime)])のうち、索引済みで最新のもののベクトル"""
self._ensure_loaded()
with self._lock:
return {ap: self._vecs[ap] for _, ap, sz, mt in entries
if ap in self._vecs and self._is_fresh(ap, sz, mt)}
def indexed_entries(self, exclude_prefix: str = "") -> list[tuple[str, int, float]]:
"""索引済みの (path, size, mtime) 一覧。exclude_prefix配下は除く"""
self._ensure_loaded()
with self._lock:
return [(p, *self._stamp[p]) for p in self._vecs
if not (exclude_prefix and p.startswith(exclude_prefix))]
def sample_vectors(self, n: int, exclude_prefix: str = "", seed: int = 0) -> tuple[list[str], np.ndarray]:
"""索引から最大n件を無作為に(seed固定で)取り出す。exclude_prefix配下は除く"""
self._ensure_loaded()
with self._lock:
paths = [p for p in self._vecs if not (exclude_prefix and p.startswith(exclude_prefix))]
if len(paths) > n:
pick = np.random.default_rng(seed).choice(len(paths), n, replace=False)
paths = [paths[i] for i in sorted(pick)]
mat = (np.stack([self._vecs[p] for p in paths]) if paths
else np.zeros((0, max(self._dim, 1)), np.float32))
return paths, mat
# ── 検索 ───────────────────────────────────────────────
def _matrix(self):
with self._lock:
if self._mat_cache is None:
paths = list(self._vecs.keys())
mat = np.stack([self._vecs[p] for p in paths]) if paths else np.zeros((0, max(self._dim, 1)), np.float32)
self._mat_cache = (paths, mat)
return self._mat_cache
def _rank(self, qvec: np.ndarray, folder: str, recursive: bool, limit: int, exclude: str = "",
min_score: float = 0.0) -> list[dict]:
entries = self._scope_entries(folder, recursive)
info = {ap: (name, sz, mt) for name, ap, sz, mt in entries}
paths, mat = self._matrix()
if not paths:
raise SemanticError("index_empty")
if mat.shape[1] != qvec.shape[0]:
raise SemanticError("dim_mismatch", str(self._dim))
scores = mat @ qvec
order = np.argsort(-scores)
out = []
for i in order:
if float(scores[i]) < min_score:
break # スコア降順なので、以降はすべてしきい値未満
p = paths[i]
if p == exclude or p not in info:
continue
name, sz, mt = info[p]
item = self.gallery.make_list_item(name, p, sz, mt)
item["score"] = round(float(scores[i]), 4)
out.append(item)
if len(out) >= limit:
break
return out
def search_text(self, query: str, folder: str, recursive: bool, limit: int, min_score: float = 0.0) -> list[dict]:
self._ensure_loaded()
query = (query or "").strip()[:_MAX_QUERY_CHARS]
if not query:
return []
if not self._vecs:
raise SemanticError("index_empty")
qvec = self._embed_text(self._model, self._dim, query)
return self._rank(qvec, folder, recursive, limit, min_score=min_score)
def search_similar(self, path: str, folder: str, recursive: bool, limit: int, min_score: float = 0.0) -> list[dict]:
self._ensure_loaded()
try:
p = self.gallery._resolve_untrusted(path) # UNC等は resolve() 前に文字列で拒否
except ValueError:
raise SemanticError("folder_not_allowed", path)
if not p.is_file() or not self.gallery._check_path_allowed(p):
raise SemanticError("folder_not_allowed", path)
ap = p.as_posix()
vec = self._vecs.get(ap)
if vec is None or not self._is_fresh(ap, p.stat().st_size, p.stat().st_mtime):
# 未インデックスの基準画像は、その場で1枚だけ埋め込む
if not self._vecs:
raise SemanticError("index_empty")
if p.suffix.lower() not in INDEXABLE_EXTS:
raise SemanticError("unsupported_type", p.suffix)
vec = self._embed_image(self._model, self._dim, p)
st = p.stat()
self._put(ap, vec, st.st_size, st.st_mtime)
self._save()
return self._rank(vec, folder, recursive, limit, exclude=ap, min_score=min_score)
def clear(self) -> None:
self._ensure_loaded()
with self._lock:
if self._job["running"]:
raise SemanticError("already_running")
self._vecs.clear()
self._stamp.clear()
self._dim = 0
self._mat_cache = None
self._save()
def test_connection(self, model: str, dim: int) -> dict:
if model not in DIMS_BY_MODEL:
raise SemanticError("bad_model", model)
if dim not in DIMS_BY_MODEL[model]:
dim = DEFAULT_DIM_BY_MODEL[model]
n = int(self._embed_text(model, dim, "test").shape[0])
return {"dimensions": n, "provider": f"Ollama {local.current_config()['tag']}" if model == LOCAL_MODEL else "API"}
+502
View File
@@ -0,0 +1,502 @@
"""AI skill (.md system prompt) file management service.
Skills are flat ``*.md`` files with a YAML frontmatter block::
---
name: Display name
description: One sentence
metadata: # optional, Workflow Studio extensions
wfs-version: 2 # built-in skills only (upgrade tracking)
output: prompt # prompt | svg -> auto-check + action buttons
tools: [] # [] or [generate_image]
temperature: 0.4
think: false
context: [workflow]
---
``name``/``description`` follow the Agent Skills (SKILL.md) convention so skills
stay portable; everything Workflow-Studio-specific lives under ``metadata``.
Built-in skills ship in ``builtin_skills/`` next to this file and are copied
into the user's skill folder. A small state file records which version and
content hash were installed, so an untouched built-in is upgraded silently
while a user-edited one is left alone (and flagged ``update_available``).
"""
import hashlib
import io
import json
import logging
import re
import shutil
import zipfile
from pathlib import Path, PurePosixPath
try:
import yaml # PyYAML ships with ComfyUI
except Exception: # pragma: no cover - fallback parser below
yaml = None
logger = logging.getLogger(__name__)
ALLOWED_EXTS = {"md"}
# Folder skills (Agent Skills / SKILL.md standard): <skills_dir>/<folder>/SKILL.md plus optional
# reference files. Only text resources are kept and served - Workflow Studio never runs a
# skill's scripts, so script files are skipped on import.
SKILL_MD = "SKILL.md"
RESOURCE_EXTS = {"md", "txt", "json", "yaml", "yml", "csv"}
MAX_RESOURCES = 50
MAX_RESOURCE_BYTES = 200 * 1024 # per file served to the chat model
MAX_IMPORT_FILE_BYTES = 1024 * 1024 # per file inside an imported zip
MAX_IMPORT_TOTAL_BYTES = 5 * 1024 * 1024
MAX_IMPORT_ENTRIES = 300
_FOLDER_RE = re.compile(r"^[A-Za-z0-9][\w.\- ]{0,63}$")
BUILTIN_DIR = Path(__file__).parent / "builtin_skills"
STATE_FILENAME = ".builtin_state.json"
# Content hashes of built-in skills released before version tracking existed
# (v0.3.79 - v0.9.4). A user file matching one of these is an untouched copy
# and can be upgraded safely.
LEGACY_BUILTIN_HASHES = {
"svg-icon.md": {"f46b635abda182b26df38d4e49ec2a626642fcd98c94013cb67303c69aaf10c1"},
"skill-creator.md": {"2fb31273298b828c6f18c7fd010be44d31588f881d04757e5c0f5f18bbad2313"},
}
_FRONTMATTER_RE = re.compile(r"^---\r?\n(.*?)\r?\n---\r?\n?", re.DOTALL)
def _content_hash(content: str) -> str:
return hashlib.sha256(content.replace("\r\n", "\n").strip().encode("utf-8")).hexdigest()
def _parse_frontmatter_shallow(block: str) -> dict:
"""Fallback parser: top-level ``key: value`` plus one nested ``metadata:`` level."""
fields: dict = {}
current_map: dict | None = None
for line in block.splitlines():
if not line.strip() or ":" not in line:
continue
indented = line[:1] in (" ", "\t")
key, _, value = line.strip().partition(":")
key, value = key.strip(), value.strip()
if not key:
continue
if indented and current_map is not None:
current_map[key] = _coerce_scalar(value)
elif not indented:
if value == "":
current_map = {}
fields[key] = current_map
else:
current_map = None
fields[key] = value
return fields
def _coerce_scalar(value: str):
if value.startswith("[") and value.endswith("]"):
inner = value[1:-1].strip()
return [v.strip().strip("'\"") for v in inner.split(",") if v.strip()] if inner else []
low = value.lower()
if low in ("true", "false"):
return low == "true"
try:
return float(value) if "." in value else int(value)
except ValueError:
return value.strip("'\"")
def _parse_frontmatter(content: str) -> dict:
"""Extract the frontmatter block as a dict (empty dict if absent/invalid)."""
m = _FRONTMATTER_RE.match(content or "")
if not m:
return {}
block = m.group(1)
if yaml is not None:
try:
data = yaml.safe_load(block)
if isinstance(data, dict):
return data
except Exception:
pass # e.g. an unquoted ": " inside description -> shallow parse
return _parse_frontmatter_shallow(block)
def _json_safe(value):
"""Keep only JSON-serialisable metadata values (YAML can yield dates etc.)."""
if isinstance(value, dict):
return {str(k): _json_safe(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
return [_json_safe(v) for v in value]
if isinstance(value, (str, int, float, bool)) or value is None:
return value
return str(value)
def _skill_info(filename: str, content: str, size: int) -> dict:
meta = _parse_frontmatter(content)
extra = meta.get("metadata") if isinstance(meta.get("metadata"), dict) else {}
return {
"filename": filename,
"name": str(meta.get("name") or Path(filename).stem),
"description": str(meta.get("description") or ""),
"metadata": _json_safe(extra),
"size": size,
}
def _builtin_version(content: str) -> int:
meta = _parse_frontmatter(content).get("metadata") or {}
try:
return int(meta.get("wfs-version", 1))
except (TypeError, ValueError):
return 1
class SkillService:
def __init__(self, skills_dir: Path, builtin_dir: Path = BUILTIN_DIR):
self.skills_dir = skills_dir
self.builtin_dir = builtin_dir
self.skills_dir.mkdir(parents=True, exist_ok=True)
try:
self.sync_builtin_skills()
except Exception as e: # never block startup on a seeding problem
logger.warning("[WFS] Built-in skill sync failed: %s", e)
# ------------------------------------------------------------------
# Built-in skills
# ------------------------------------------------------------------
def _builtins(self) -> dict[str, str]:
if not self.builtin_dir.is_dir():
return {}
return {
p.name: p.read_text(encoding="utf-8")
for p in sorted(self.builtin_dir.glob("*.md"))
if p.is_file()
}
def _load_state(self) -> dict:
path = self.skills_dir / STATE_FILENAME
try:
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
def _save_state(self, state: dict) -> None:
(self.skills_dir / STATE_FILENAME).write_text(
json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8"
)
def _install_builtin(self, filename: str, content: str, state: dict) -> None:
(self.skills_dir / filename).write_text(content, encoding="utf-8")
state[filename] = {"version": _builtin_version(content), "hash": _content_hash(content)}
def sync_builtin_skills(self) -> dict:
"""Install new built-ins and upgrade untouched ones.
Rules per built-in file:
- missing and never installed -> install
- missing but installed before -> the user deleted it, leave it deleted
- present, untouched (hash matches what we installed, or a known
pre-tracking release) and older -> overwrite with the new version
- present but edited by the user -> keep it (reported as update_available)
Returns ``{"installed": [...], "upgraded": [...]}``.
"""
state = self._load_state()
result = {"installed": [], "upgraded": []}
changed = False
for filename, content in self._builtins().items():
new_version = _builtin_version(content)
path = self.skills_dir / filename
entry = state.get(filename)
if not path.is_file():
if entry is None:
self._install_builtin(filename, content, state)
result["installed"].append(filename)
changed = True
continue
current_hash = _content_hash(path.read_text(encoding="utf-8"))
if entry is None:
if current_hash in LEGACY_BUILTIN_HASHES.get(filename, set()):
self._install_builtin(filename, content, state)
result["upgraded"].append(filename)
elif current_hash == _content_hash(content):
state[filename] = {"version": new_version, "hash": current_hash}
else:
# A user file with a built-in's name - never overwrite it.
state[filename] = {"version": 0, "hash": None}
changed = True
continue
if int(entry.get("version") or 0) < new_version and current_hash == entry.get("hash"):
self._install_builtin(filename, content, state)
result["upgraded"].append(filename)
changed = True
if changed:
self._save_state(state)
if result["installed"] or result["upgraded"]:
logger.info("[WFS] Built-in skills installed=%s upgraded=%s", result["installed"], result["upgraded"])
return result
def restore_builtin(self, filename: str) -> dict:
"""Overwrite a skill file with the shipped built-in version (explicit user action)."""
builtins = self._builtins()
if filename not in builtins:
raise ValueError(f"Not a built-in skill: {filename}")
state = self._load_state()
self._install_builtin(filename, builtins[filename], state)
self._save_state(state)
path = self.skills_dir / filename
return _skill_info(filename, builtins[filename], path.stat().st_size)
# ------------------------------------------------------------------
# File CRUD
# ------------------------------------------------------------------
def list_skills(self) -> list[dict]:
"""Return sorted list of flat skill files and folder skills with parsed metadata."""
if not self.skills_dir.exists():
return []
builtins = self._builtins()
state = self._load_state()
files = []
for path in sorted(self.skills_dir.glob("*")):
if path.is_dir():
info = self._folder_skill_info(path)
if info:
files.append(info)
continue
if not (path.is_file() and path.suffix.lstrip(".").lower() in ALLOWED_EXTS):
continue
try:
content = path.read_text(encoding="utf-8")
except Exception:
content = ""
info = _skill_info(path.name, content, path.stat().st_size)
builtin_content = builtins.get(path.name)
info["builtin"] = builtin_content is not None
info["update_available"] = False
if builtin_content is not None:
current_hash = _content_hash(content)
if current_hash != _content_hash(builtin_content):
entry = state.get(path.name) or {}
installed_version = int(entry.get("version") or 0)
info["update_available"] = installed_version < _builtin_version(builtin_content)
info["modified"] = True
files.append(info)
return files
def _folder_skill_info(self, folder: Path) -> dict | None:
if folder.name.startswith(".") or not _FOLDER_RE.match(folder.name):
return None
skill_md = folder / SKILL_MD
if not skill_md.is_file():
return None
try:
content = skill_md.read_text(encoding="utf-8")
except Exception:
content = ""
info = _skill_info(f"{folder.name}/{SKILL_MD}", content, skill_md.stat().st_size)
if info["name"] == SKILL_MD.rsplit(".", 1)[0]:
info["name"] = folder.name
info["folder"] = folder.name
info["resources"] = self.list_resources(folder.name)
info["builtin"] = False
info["update_available"] = False
return info
def list_resources(self, folder: str) -> list[str]:
"""Readable reference files of a folder skill (relative POSIX paths, SKILL.md excluded)."""
base = self._folder_path(folder)
if base is None or not base.is_dir():
return []
out = []
for path in sorted(base.rglob("*")):
if len(out) >= MAX_RESOURCES:
break
if not path.is_file() or path.is_symlink():
continue
rel = path.relative_to(base).as_posix()
if rel == SKILL_MD or any(part.startswith(".") for part in rel.split("/")):
continue
if path.suffix.lstrip(".").lower() in RESOURCE_EXTS:
out.append(rel)
return out
def get_resource(self, folder: str, rel_path: str) -> str:
"""Read one reference file of a folder skill (used by the chat's read_skill_file tool)."""
base = self._folder_path(folder)
if base is None or not base.is_dir():
raise FileNotFoundError(f"Skill folder not found: {folder}")
rel = PurePosixPath(str(rel_path or "").replace("\\", "/"))
if rel.is_absolute() or ".." in rel.parts or not rel.parts or rel.as_posix() == SKILL_MD:
raise ValueError(f"Invalid resource path: {rel_path}")
if rel.suffix.lstrip(".").lower() not in RESOURCE_EXTS:
raise ValueError(f"Unsupported resource type: {rel_path}")
path = (base / rel).resolve()
if base.resolve() not in path.parents or not path.is_file():
raise FileNotFoundError(f"Resource not found: {rel_path}")
data = path.read_bytes()[:MAX_RESOURCE_BYTES]
return data.decode("utf-8", errors="replace")
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}")
if path.name == SKILL_MD and not path.parent.is_dir():
raise FileNotFoundError(f"Skill folder not found: {filename}")
path.write_text(content, encoding="utf-8")
rel = path.relative_to(self.skills_dir).as_posix()
return _skill_info(rel, content, path.stat().st_size)
def delete_file(self, filename: str) -> None:
path = self._safe_path(filename)
if path is None:
raise ValueError(f"Invalid filename: {filename}")
if path.name == SKILL_MD and path.parent != self.skills_dir:
# A folder skill is deleted as a whole (SKILL.md + its reference files).
if path.parent.is_dir():
shutil.rmtree(path.parent)
return
if path.is_file():
path.unlink()
# ------------------------------------------------------------------
# Import (Agent Skills zip or a single .md)
# ------------------------------------------------------------------
def import_skill(self, upload_name: str, data: bytes) -> dict:
"""Import an uploaded skill.
- ``*.md``: saved as a flat skill (``<slug>.md``; SKILL.md uploads are named after their
frontmatter name).
- ``*.zip``: an Agent Skills folder - SKILL.md at the zip root or inside exactly one top
folder. Only text resources are extracted; scripts and binaries are skipped.
Returns ``{"skill": info, "skipped": [paths]}``. Never overwrites: a numeric suffix is
added when the name is taken.
"""
name = (upload_name or "").strip()
lower = name.lower()
if lower.endswith(".md"):
text = data.decode("utf-8", errors="replace")
meta = _parse_frontmatter(text)
stem = Path(name).stem if Path(name).name != SKILL_MD else str(meta.get("name") or "imported-skill")
filename = self._unique_name(_slugify(stem), ".md")
(self.skills_dir / filename).write_text(text, encoding="utf-8")
return {"skill": _skill_info(filename, text, len(data)), "skipped": []}
if not lower.endswith(".zip"):
raise ValueError("Only .zip (Agent Skills folder) or .md files can be imported")
return self._import_zip(data)
def _import_zip(self, data: bytes) -> dict:
try:
zf = zipfile.ZipFile(io.BytesIO(data))
except zipfile.BadZipFile as e:
raise ValueError(f"Not a valid zip file: {e}") from e
infos = [i for i in zf.infolist() if not i.is_dir()]
if len(infos) > MAX_IMPORT_ENTRIES:
raise ValueError(f"Too many files in the zip ({len(infos)} > {MAX_IMPORT_ENTRIES})")
def parts_of(info):
p = PurePosixPath(info.filename.replace("\\", "/"))
if p.is_absolute() or ".." in p.parts or any(":" in part for part in p.parts):
raise ValueError(f"Unsafe path in zip: {info.filename}")
return p.parts
all_parts = [(i, parts_of(i)) for i in infos]
# Ignore OS junk (__MACOSX/, .DS_Store) when locating SKILL.md.
all_parts = [(i, ps) for i, ps in all_parts if ps and ps[0] != "__MACOSX" and not ps[-1].startswith(".")]
skill_md_parts = [ps for _, ps in all_parts if ps[-1] == SKILL_MD and len(ps) <= 2]
if len(skill_md_parts) != 1:
raise ValueError("The zip must contain exactly one SKILL.md at its root or in one top-level folder")
prefix = skill_md_parts[0][:-1] # () or (top_folder,)
skill_text = zf.read("/".join(skill_md_parts[0])).decode("utf-8", errors="replace")
meta = _parse_frontmatter(skill_text)
base_name = prefix[0] if prefix else str(meta.get("name") or "imported-skill")
folder = self._unique_name(_slugify(base_name), "")
target = self.skills_dir / folder
target.mkdir(parents=True)
written, skipped, total = 0, [], 0
try:
for info, ps in all_parts:
if tuple(ps[: len(prefix)]) != prefix:
skipped.append(info.filename)
continue
rel = PurePosixPath(*ps[len(prefix):])
is_skill_md = rel.as_posix() == SKILL_MD
ext = rel.suffix.lstrip(".").lower()
if not is_skill_md and ext not in RESOURCE_EXTS:
skipped.append(info.filename)
continue
if info.file_size > MAX_IMPORT_FILE_BYTES:
skipped.append(info.filename)
continue
total += info.file_size
if total > MAX_IMPORT_TOTAL_BYTES:
raise ValueError("The zip's text files exceed 5 MB in total")
content = zf.read(info)[:MAX_IMPORT_FILE_BYTES]
out = (target / rel).resolve()
if target.resolve() not in out.parents:
raise ValueError(f"Unsafe path in zip: {info.filename}")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(content)
written += 1
except Exception:
shutil.rmtree(target, ignore_errors=True)
raise
logger.info("[WFS] Imported skill folder %s (%d files, %d skipped)", folder, written, len(skipped))
return {"skill": self._folder_skill_info(target), "skipped": skipped}
def _unique_name(self, stem: str, suffix: str) -> str:
candidate = f"{stem}{suffix}"
n = 2
while (self.skills_dir / candidate).exists():
candidate = f"{stem}-{n}{suffix}"
n += 1
return candidate
# ------------------------------------------------------------------
# Path safety
# ------------------------------------------------------------------
def _folder_path(self, folder: str) -> Path | None:
name = (folder or "").strip()
if not _FOLDER_RE.match(name) or name.startswith("."):
return None
return self.skills_dir / name
def _safe_path(self, filename: str) -> Path | None:
"""Flat ``name.md`` or a folder skill's ``<folder>/SKILL.md`` - nothing else."""
if not filename or not filename.strip():
return None
name = filename.strip().replace("\\", "/")
if "/" in name:
folder, _, leaf = name.partition("/")
if leaf != SKILL_MD:
return None
base = self._folder_path(folder)
return base / SKILL_MD if base is not None else None
if name in (".", "..") or name.startswith("."):
return None
ext = Path(name).suffix.lstrip(".").lower()
if ext not in ALLOWED_EXTS:
return None
return self.skills_dir / name
def _slugify(text: str) -> str:
slug = re.sub(r"[^A-Za-z0-9]+", "-", str(text)).strip("-").lower()[:48]
return slug or "imported-skill"
+15
View File
@@ -5,6 +5,7 @@ import io
import logging
import sqlite3
from pathlib import Path
from typing import List
logger = logging.getLogger(__name__)
@@ -85,6 +86,20 @@ class TaggerDbService:
conn.execute("DELETE FROM images WHERE id=?", (row_id,))
conn.commit()
def delete_bulk(self, row_ids: List[int]) -> int:
if not row_ids:
return 0
placeholders = ",".join("?" for _ in row_ids)
with self._conn() as conn:
cur = conn.execute(
f"DELETE FROM images WHERE id IN ({placeholders})",
row_ids,
)
conn.commit()
return cur.rowcount
def export_csv(self) -> str:
with self._conn() as conn:
rows = conn.execute(

Some files were not shown because too many files have changed in this diff Show More