Compare commits

...
21 Commits
Author SHA1 Message Date
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
31 changed files with 2500 additions and 95510 deletions
+6 -8
View File
@@ -6,11 +6,9 @@ __pycache__/
*.egg-info/
data/
# graphify: keep the knowledge graph itself under version control,
# but exclude the run-by-run cost/token log, the local extraction
# cache (both churn every rebuild and have no value to other clones),
# and the machine-local interpreter/scan-root pointers.
graphify-out/cost.json
graphify-out/cache/
graphify-out/.graphify_python
graphify-out/.graphify_root
# 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
+4
View File
@@ -2,6 +2,10 @@
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.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のレイヤーパネル改善
+117
View File
@@ -2,6 +2,122 @@
---
## v0.7.2(2026-09-18)
### Workflowタブにソート機能追加(Date/Name 昇順降順)
Workflowタブのグループフィルター右隣に、Galleryタブと同じパターンのソートドロップダウン(Date Newest/Oldest、Name A-Z/Z-A)を追加した。`wf.mtime`(バックエンドが既に返していたファイル更新時刻)を使ったクライアント側ソートで、選択状態は`localStorage`(`wfm_workflow_sort`)に永続化する。
**変更ファイル**: `templates/index.html`(`#wfm-sort` select追加)、`static/js/workflow-tab.js`(`sortWorkflows()`追加、`renderGrid()`で`filterWorkflows()`の結果に適用)
### Videoタブ左ペイン幅調整・Editトリムパネルのバグ修正とUI調整
**左ペイン幅**: PlanサブタブのフォームパネルをGenerateUI寄りの380px固定から253px(約2/3)に縮小し、中央プレビュー/タイムラインを広げた(`.wfm-video-form-panel`の`width`のみの変更、`flex:1`の中央パネルが自動で広がる)。
**「現在位置を使用」ボタンでトリムスクラバーの見た目が更新されないバグを修正**: トリムのStart/End数値入力とスクラバー(ドラッグハンドル・ハイライト範囲)は同じ`commit()`関数を経由する設計だが、`commit()`はスクラバー自身の`updateRange()`(ハンドル位置・ハイライト帯の再描画)を呼んでいなかった。ハンドルをドラッグした場合のみ呼び出し元が直接`updateRange()`を叩いていたため気付かれておらず、数値入力や「現在位置を使用」ボタンではclipのtrimStart/trimEndは正しく更新されるのにスクラバーの表示だけ古いままになっていた。`_wireTrimScrubber()`が`updateRange`関数自体を返すようにし、`_renderTrimPanel()`側で`commit()`から必ずそれを呼べるようにして解消。
**トリムスクラバーのバッジ重なりを解消**: 開始/終了/再生位置の時刻バッジ(ハンドルから浮き上がるラベル)がクリップ名やルーラー目盛りと重なる、また再生位置がトリム開始/終了に近いとStart/EndバッジとPlayheadバッジ同士が重なって読めなくなる不具合をユーザーが発見。クリップ名とスクラバーの間隔を拡大、スクラバー上部にバッジ専用の余白(`padding-top`)を確保、トラック高さを36px→52pxに拡大した上で、Playheadバッジのみ`top`をStart/Endバッジより高い位置(2段目)にずらして重なりを回避した。
**変更ファイル**: `static/css/video-tab.css`(`.wfm-video-form-panel`, `.wfm-video-trim-scrubber/-track/-badge/-badge-playhead`)、`static/js/video-edit-tab.js`(`_wireTrimScrubber()`が`updateRange`を返すよう変更、`_renderTrimPanel()`の`commit()`から呼び出し、クリップ名/トリム行の余白調整)
**How to apply**: 同じ状態変更を複数の入力経路(数値入力・ボタン・ドラッグ)から行える場合、各経路が同じ「見た目を再描画する関数」を実際に呼んでいるかを確認すること——1経路(ドラッグ)だけがたまたま直接呼んでいたために、他の経路のバグが長く気付かれずに残っていた。
### Video Assetタブに動画/画像フィルタドロップダウン追加
Asset切替ドロップダウン(`__Video Assets__`/`__Video Temp__`/All Video Assets)の右隣に、幅を約半分にした動画/画像タイプフィルタ(All Types / Video Only / Image Only)を追加。既存の`isVideoFile()`判定を再利用してクライアント側でリストを絞り込む。
**変更ファイル**: `templates/index.html`(`#wfm-video-asset-kind` select追加)、`static/js/video-asset-tab.js`(`_s.kindFilter`追加、`_filteredImages()`で適用)
### Video Edit プロジェクト永続化(Phase 5)とサイドバーProjectのPlan/Editモード切替
[VIDEO_EDIT_TAB_PLAN.md](VIDEO_EDIT_TAB_PLAN.md)のPhase 5(永続化)を実装。他の未着手フェーズ(クロップ/テキストオーバーレイ/BGM合成)と比べ、`video_plan_service.py`と同型のCRUDパターンをほぼそのまま流用できるため最軽量(見積もり1〜1.5日相当)と判断し、最初に着手した。
**バックエンド**: `video_plan_service.py`と同じ形(パス安全性チェック付きlist/get/save/delete、`updated_at`自動付与)の`VideoEditProjectService`を新設し、`video_edit_project/`ディレクトリに`ws_videoeditproj_*.json`として保存する。`video_plan_service.py`との共通base化は行わず独立クラスのまま(両者のスキーマは無関係なので、無理に共通化しても行数が減るだけで得るものがないという既存の`VideoPlanService`のドキュメント方針を踏襲)。
**保存対象**: タイムラインのクリップ順序・トリム区間・各クリップが参照するアップロード済みサーバーファイル(`serverRef`)のみ。アップロード未完了のクリップは保存対象から除外する。
**読込方式**: 保存されたクリップをComfyUIの`/view`エンドポイントからBlobとして再取得し、`File`化した上で既存の「クリップ追加」経路(`addClipFromFile()`→アップロード→probe)にそのまま通してから、保存済みのトリム値を上書き適用する方式にした。`serverRef`とメタデータを信頼してプレビュー生成ロジックを別に書く(再アップロードを省く)選択肢もあったが、既存の十分テスト済みの1経路だけを通ることを優先し、実装量とリスクを抑えた。
**サイドバーProjectタブの切替**: 中央のPlan/Editどちらのサブタブがアクティブかを`video-tab.js`が`_centerSubtabMode`として記憶し、サイドバーのProjectパネルを開く/切り替えるたびにそのモードを渡すようにした。`video-project-tab.js`は元々Video Plan専用だったリスト表示・削除ロジックを、モードごとの設定テーブル(取得/削除エンドポイント、開く関数、件数ラベル等)に切り出して汎用化。Editモードでのアイテムクリックは新設した`video-edit-tab.js`の`openSavedVideoEditProject()`(`video-plan-tab.js`の`openSavedVideoPlan()`と同型)に委譲する。
**検証**: 全JS/Pythonファイルを`node --check`/`py_compile`で構文確認、開発元リポジトリと実行時`custom_nodes`フォルダ間でファイルのdiffが空になることを確認して同期。Python側(新規サービス・ルート追加)はComfyUI完全再起動が必須(インポートは起動時一度きりのため)である旨をユーザーに案内済み。
**変更ファイル**: `py/services/video_edit_project_service.py`(新規)、`py/config.py`(`VIDEO_EDIT_PROJECT_DIR`)、`py/routes/video_routes.py`(`/api/wfm/video/edit/projects`系4エンドポイント)、`static/js/video-edit-tab.js`(`_buildProjectData()`/`_saveProject()`/`_restoreClipFromSaved()`/`_loadProjectData()`/`openSavedVideoEditProject()`、Save/Save As/Loadボタン配線)、`static/js/video-project-tab.js`(`_MODES`設定テーブルによるPlan/Edit汎用化)、`static/js/video-tab.js`(`_centerSubtabMode`追跡)、`templates/index.html`(Save/Save As/Load UI)、`static/js/i18n.js`(新規メッセージキー・ヘルプ本文更新、英/日/中3言語)
**How to apply**: 新機能の永続化を設計する際、対象データを直接シリアライズしてサーバー参照込みで復元しようとすると(プレビュー生成・probe結果の整合性など)別経路のロジックを新たに書く必要が出やすい。既存の「新規追加」経路がテスト済みで十分軽ければ、保存データを最小限(サーバー参照+差分パラメータ)に絞り、読込時はその経路をそのまま再利用する方が実装・保守コストが低い。
関連: [[project_video_edit_tab_feature_branch]]
---
## v0.7.1(2026-09-17)
### セキュリティ修正: Tagger VLM の Unsloth 経路でAPIキーがSSRFにより外部送信され得る不備を修正
セキュリティレビューで、Unsloth Desktop連携の2つの経路のうちAI TOOLタブ用プロキシ(`unsloth_routes.py`)にはリクエスト先ホストを`localhost`/`127.0.0.1`/`::1`に制限するallowlistが入っていたが、Taggerタブ VLM経路(`tagger_service.py`の`vlm_models`/`vlm_predict`、`backend=="unsloth"`時)には同等の検証が無く、クライアントがリクエストごとに指定する`api_url`へそのまま`Authorization: Bearer <UNSLOTH_API_KEY>`を送信してしまう実装になっていた。ComfyUIを`--listen 0.0.0.0`等で外部公開している環境では、設定変更なしにこの経路経由でAPIキーが任意の外部ホストへ流出し得る。`unsloth_routes.py`と同じallowlistロジックを`tagger_service.py`側にも追加し、loopback以外の`api_url`はキーを付与せずエラーを返すようにした。
副次的に、G'MIC-Qt起動(`gmic_routes.py`)の`gmic_qt_path`検証も、存在チェックのみだったのを絶対パス・`.exe`拡張子・実ファイルであることの検証に強化した(`shell=True`は使用しておらずコマンドインジェクションではないが、外部公開環境で設定変更と組み合わさった際のローカル実行リスクを下げる防御的対応)。
**How to apply**: 同一サービス(Unslothなど認証キーを要する外部API)へ複数の呼び出し経路がある場合、ホスト制限などのセキュリティ検証はルート/サービス単位ではなく呼び出し経路ごとに漏れなく入れる必要がある。片方だけ実装して安心しないこと。
## v0.7.0(2026-09-17)
### プレビュー枠の高さ固定・AssetサブタブをPlan/Editと並ぶ中央下部へ移設・トリムパネルにClipchamp風スクラバー追加・Frame保存のVideo Assets登録とAsset→Plan画像連携
Edit サブタブMVP実装後、実際にユーザーが使う中で見つかった調整要望をまとめて対応した。
**プレビュー枠の高さ固定**: 中央ペインのSource/Resultプレビュー枠が`min-height`指定のみだったため、動画ソースのネイティブサイズ(特に縦長)によって枠が伸縮し、下のPlan/Editサブタブの表示位置がソースを選ぶたびに動いてしまっていた。枠を`height:260px`固定にし、動画/画像側は`width/height:auto; max-width/max-height:100%`(ネイティブサイズが枠より大きい場合のみ縮小、小さい場合は拡大しない)に変更して解消。
**AssetサブタブをPlan/Editと並ぶ中央下部の第3サブタブへ移設**: 左サイドバーにあったAsset一覧・詳細パネルを中央ペイン下部(Plan/Editと同じ場所)へ移設し、左=一覧(サムネイル/テーブル切替可)・右=選択ファイルの詳細という2カラムレイアウトに再構成。一覧上部にグループ切替・ファイル名検索・表示切替(▦グリッド/☰テーブル)ボタンを追加した。左サイドバーはProjectのみのシンプルなパネルになった。
**トリムパネルにClipchamp風ビジュアルスクラバーを追加**: 動画クリップのトリム編集がテキスト入力(開始/終了秒)のみだったのを、時間目盛り(ルーラー)・Sourceプレビューの`currentTime`と同期するドラッグ可能な再生ヘッド・時刻バッジ付きのドラッグ可能なイン/アウトハンドルを備えたビジュアルなスクラバーに拡張した。全位置を`clip.duration`基準の%指定にすることでpx/秒換算の管理を持たずに済ませている。既存のStart/End数値入力・「現在位置を使用」ボタンとは同じ`commit`関数を通して連動する。ハンドルドラッグ中は`_renderTimeline()`の再描画を避け、pointerup時のみ確定してパフォーマンスに配慮。playheadとトリムハンドルが同じ位置に重なってもハンドル操作を優先できるようz-indexを調整した。
**Frame保存のVideo Assets登録とAsset→Plan画像連携**: 右ペインFrameタブの「Save to Output」に「Add to Video Assets group」チェックボックスを追加し、チェック時は保存したフレームをGalleryの`__Video Assets__`グループへ自動登録できるようにした。またAssetタブの画像アセット詳細に「Set as First Image」「Set as Last Image」ボタンを追加し、PlanタブのFirst/Last Imageへ直接画像をアップロードできるようにした(動画クリップでは非表示、選択中のPlanブロックへ反映後Planサブタブへ自動切り替え)。
**検証**: いずれもKaptureで実機ComfyUI_5(ポート8189)に接続し、正方形/縦長動画でのプレビュー枠固定確認、Asset新レイアウトでのグリッド/テーブル切替・検索・選択、トリムスクラバーのplayhead同期・トラッククリックシーク・ハンドルドラッグ(playhead重なり時含む)・複数クリップ切替時のリスナー管理、Frame保存後のGalleryグループ登録をAPI経由で確認、Asset画像からFirst/Last Imageへの反映まで一通り実機確認済み。
**How to apply**: プレビュー枠のようにコンテンツ依存でサイズが変わる要素が、隣接するUIの表示位置に影響する場合は、コンテンツ側ではなく枠側を固定サイズにしてコンテンツを内側で縮小表示する方が安全([[project_video_edit_tab_feature_branch]])。ドラッグ可能なUI要素同士が同じ座標に重なる可能性がある場合は、操作頻度の高い方(今回はトリムハンドル)のz-indexを優先し、もう一方の当たり判定に埋もれないようにする。
関連: [[project_video_edit_tab_feature_branch]]
### Video タブ Edit サブタブ: 簡易動画編集機能(クリップタイムライン・トリム・結合・書き出し)を新規実装
これまで「Editing tools coming soon」のプレースホルダーのみだったVideoタブのEditサブタブに、Adobe Premiereライクな簡易動画編集機能を実装した。詳細設計・調査結果は[VIDEO_EDIT_TAB_PLAN.md](VIDEO_EDIT_TAB_PLAN.md)にまとめてある。
**MVP機能**: クリップのタイムライン配置(左詰め、実秒数×固定px/秒スケール)、トリム(開始/終了秒)、複数クリップの結合書き出し、タイムライン全体の疑似連続プレビュー、静止画/アニメーション画像クリップ(表示時間指定)、Clear/複製/削除/並べ替え(◀▶ボタンおよびネイティブドラッグ&ドロップ)。新規ファイル`static/js/video-edit-tab.js`が中心実装。
**Phase 0実機検証での重要な発見(当初の設計を訂正)**:
- ComfyUI Core 0.35〜0.36には`VideoSlice`という名前のノードは存在せず、正しいclass_typeは`"Video Slice"`(スペース込み)。`VideoTrim`(experimental)の`trim`入力はフラット値`{"start_time":..}`を渡すと**エラーにならず黙って何もトリムしない**(`.get("trim")`で一段余計にネストを剥がす実装のため)。実際に出力duration をprobeして初めて発覚し、「`execution_success`だけでは検証不十分」という教訓を得た。
- `ConcatenateVideo`のAutogrow入力`videos`は、ネストしたJSONオブジェクトではなく`"videos.video0"`のようなドット結合フラットキーがinputsのトップレベルに必要(ComfyUI Core `Autogrow.TemplatePrefix`のドット結合ロジックに由来)。
- `ConcatenateVideo`は解像度不一致で実行時ハードエラーになるため、クライアント側で事前チェックし、動画クリップ同士の不一致は書き出しをブロック、画像クリップは`ImageScale`(crop=center)で自動フィットして回避。
- 動画クリップ(音声あり)と画像由来クリップ(無音)を結合すると`ConcatenateVideo`が音声レイアウト不一致でエラーになる。複数クリップ結合時のみ、全クリップを`GetVideoComponents`→`CreateVideo`で無音統一する対策を実装(単一クリップ書き出しは音声を保持。**複数クリップ結合時は音声が失われるのが既知の限界**)。
**静止画/アニメーション画像クリップ対応**: 既存のAsset一覧(`__Video Assets__`/`__Video Temp__`グループ)には元々ビデオ専用フィルタが無く画像も一覧されていたが、プレビューが常に`<video>`要素前提で画像では何も表示されなかった。`video-preview.js`のSource/Resultプレビュー枠に`<img>`コンパニオン要素を追加し、`mediaType`引数で切り替えるよう拡張。画像クリップは`LoadImage`→(必要時)`ImageScale`→`RepeatImageBatch(amount=表示秒数×24fps)`→`CreateVideo`で正確な長さの動画セグメントに変換して書き出しグラフに組み込む。
**UI設計はPlanサブタブに合わせて再設計**: 当初は縦リスト+各行ボタンだったが、ユーザー指示によりPlanタブの横トラック+共有ツールバー方式(◀▶/複製/削除/クリアが選択中クリップに対して働く)に変更。タイムライン自体はPlanのブロックのようにflex-growで幅一杯に引き伸ばさず、実秒数の固定幅で左詰め(空きトラックはそのまま空く)。この過程で、Edit実装以前の「Editing tools coming soon」プレースホルダー時代の重複CSSルール(`align-items:center`等)が残存しており、実装後もタイムライン全体が中央に縮んで表示される不具合を発見・修正した。
**タイムライン連続プレビュー**: 新規`<video>`要素を追加せず、Export結果が最終的に書き出される「生成された動画」(Result)プレビュー枠を共有し、全クリップ(トリム区間反映)を順番に連続再生する機能を追加(「▶ プレビュー / ■ 停止」トグル)。ユーザー要望「このプレビューの動画=書き出しされる動画」に対応。
**検証**: 全JS/Pythonファイルを`node --check`/構文チェックで確認。開発元リポジトリと実行時`custom_nodes`は別実体のため、変更のたびに両フォルダへ同期しmd5ハッシュ比較で完全一致を確認。Kapture(ブラウザDevTools連携)で実機ComfyUI_5(ポート8189)に接続し、クリップ追加・トリム・並べ替え(ドラッグ含む)・複製・削除・クリア・連続プレビュー・単一/複数クリップ書き出し・静止画クリップの書き出し・動画+画像混在結合を一通り実機確認済み。
**How to apply**: ComfyUI Coreの新しいノード(特にexperimental扱いのもの)をAPIプロンプト形式で直接呼び出す際は、`/object_info`のクラス名・入力スキーマを鵜呑みにせず、実際に`/prompt`で実行して**出力を実測**するまで検証を終えたとみなさない——`execution_success`は型検証を通っただけで、パラメータが意図通り効いたかは別問題(今回`VideoTrim`のトリムが黙って無視される形で顕在化)。Autogrow(`COMFY_AUTOGROW_V3`)型の動的入力は、フラットなドット結合キー(`"prefix{name}"`を`.`区切りでinputsトップレベルに並べる)で渡す。
関連: [[project_dev_deploy_sync]]
## v0.6.3
### Send CCボタンでMP4動画をComic Creatorの新設「動画ツール」へ送れるように対応
Comic Creator(`eagle_comic_creator_spa/comfyui-comic-creator`、別リポジトリ)側にレイアウトタブの新機能「動画ツール」(MP4をコマに配置して再生・停止・音量/ミュート調整・指定フレームでの静止画キャプチャができる実験的機能)が追加されたのに伴い、Galleryタブの「Send CC」ボタンをこれに対応させた。
**問題**: 既存の`sendCcBtn`クリックハンドラ(`gallery-tab.js`)は、選択中ファイルのURLを`window.parent.insertImageFromUrl()`(レイアウトタブ宛て)または`window.parent._ccImageTab.loadFromUrl()`(Imageタブ宛て)へ渡すだけで、`.mp4`を判別する分岐が無かった。Comic Creator側の受け口は画像専用ロジックしか持たないため、動画を選んでSend CCを押しても正しく処理されなかった。
**対応**: `gallery-tab.js`に既にあった`isVideoFile()`ヘルパー(拡張子判定、Video Assetsグループのバッジ表示等で使用中)で選択中ファイルが動画かどうかを判定し、レイアウトタブ宛て(デフォルト経路)で動画の場合はComic Creator側が今回新設した受け口`window.parent.insertVideoFromUrl(url, filename)`を呼ぶよう分岐した。Imageタブ宛て(`_ccI2ITargetMode === 'image'`)で動画を選んだ場合は、Comic Creator側が今回Imageタブの動画編集には対応していないため、トースト(新規i18nキー`galleryVideoNotSupportedInImageTab`、英/日/中3言語)で案内して送信を中断するようにした。
**検証**: 双方の変更点を構文チェック(`node --check`)のみで確認。ComfyUI実機でのSend CC往復動作(Gallery側でmp4選択→送信→Comic Creator側でのオーバーレイ表示・再生確認)はユーザー側での実機確認待ち。
**How to apply**: Comic Creator側とWorkflow Studio側は別リポジトリだが、Send CC・Send to LI nodeのような`window.parent`/`window.opener`経由のクロスウィンドウ連携APIは、どちらか一方だけを変更すると壊れる。片方(今回はComic Creator側)に新しい受け口(`insertVideoFromUrl`)を追加したら、呼び出し元であるこちらのGalleryタブ側も同時に更新する必要がある。
## v0.6.2
### Image Editタブにペンタブレット筆圧対応とFillツール(バケツ塗りつぶし)を追加
@@ -57,6 +173,7 @@
**Gallery: PSDファイルの一覧・サムネイル・プレビュー表示** — `.psd`を`IMAGE_EXTENSIONS`に追加し、Galleryのフォルダ一覧・検索・グループ機能に自動的に乗るようにした。ブラウザは`<img>`でPSDを直接レンダリングできないため、`serve_image`/`serve_thumb`ルートで`.psd`の場合は`psd_tools`で全レイヤーを合成(非表示レイヤーは除外)した上でPNG/JPEGに変換し、mtimeベースのキャッシュキーでディスクキャッシュして配信する方式にした。情報パネルの幅・高さもPSDヘッダーから取得するようにした。
**実機テストで発覚した2件のバグとその修正**:
1. **サムネイル表示が遅く、他の無関係なPNGサムネイルまで真っ黒になる**——原因は`serve_image`/`serve_thumb`ルートがPSD合成という重いCPU処理を`async def`ハンドラ内で直接同期実行しており、aiohttpの単一スレッドイベントループ全体をブロックしていたこと。PSD1枚の合成中は他の並行リクエスト(別画像のサムネイル配信など)も完全に止まり、結果的に無関係な画像まで表示が固まって見えた。`serve_image`・`serve_thumb`・`import_psd_layers`・新設`get_psd_layers`の4ルートを`loop.run_in_executor(None, ...)`でデフォルトのThreadPoolExecutorに逃がすことで解消した。この教訓は今後の重い処理全般に適用すべき一般原則として別途メモリに記録した。
2. **単一画像「Image Edit」送信ボタンでPSDを開くと1レイヤーしかない**(「Open PSD」ボタンでは正しく全レイヤー開ける)——原因はこのボタンが常に`loadFromUrl()`(1枚のフラット画像として読み込む経路)を使っており、PSD専用のレイヤー分解経路を通っていなかったこと。サーバー上のファイルを再アップロードせず直接レイヤー分解できる新規`GET /wfm/gallery/image/psd-layers`(`GalleryService.get_psd_layers_from_path()`)を追加し、既存の`import_psd_layers()`とロジックを`_extract_psd_layers()`に共通化した上で、PSDファイルの場合はこの新エンドポイントを使うよう分岐させた。
+4 -4
View File
@@ -26,12 +26,12 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.6.2-green)
![Version](https://img.shields.io/badge/version-0.7.2-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)
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
### Canvas Snapshot (v0.1.2)
One-click capture, Auto-save as thumbnail, Embedded workflow metadata, Auto-import
@@ -75,8 +75,8 @@ Model Browser, Thumbnail / Table views, Table column sort, Search & Filter, User
### 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.5.7)
Dedicated video generation UI, independent of GenerateUI, Plan / Edit subtabs, Base settings row, Asset / Project subtabs, Two center previews, 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
### Video Tab (v0.5.0 – v0.7.2)
Dedicated video generation UI, independent of GenerateUI, Plan subtab, Base settings row, Edit subtab, Edit project persistence, Asset subtab, Project subtab, Two center previews, 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)
4-pane layout, Translation pane, Chat pane, Chat pane — image generation via Tool Calling, Chat pane — generate_image extra parameters, TOOLS pane (VLM), TOOLS pane — shared Chat attachment, TOOLS pane (Wildcards), Chat pane — image-to-image (I2I), Chat pane — SVG generation, no ComfyUI workflow involved, Chat pane — Skills, Settings pane, Unsloth backend, Settings pane — Chat Image Generation, Settings pane — Chat I2I Generation, Settings pane — Generation, Model unload, Settings shared, Backend support, URL security, Lemonade's other endpoints (not integrated)
+15 -7
View File
@@ -22,7 +22,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- Built-in AI tools (translation and more)
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.6.2-green)
![Version](https://img.shields.io/badge/version-0.7.2-green)
## Screenshots
@@ -46,6 +46,10 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
| :----------------------------------: | :--------------------------------------: |
| ![Video Tab](docs/14_videotab.png) | ![AI TOOL Tab](docs/17_aitooltab.png) |
| Video Tab - Edit | Video Tab - Asset |
| :----------------------------------: | :-----------------------------------: |
| ![Video Tab - Edit](docs/18_videotab_Edit.png) | ![Video Tab - Asset](docs/19_videotab_Asset.png) |
| Lab Tab | Lab Index Image |
| :--------------------------------: | :------------------------------------------: |
| ![Lab Tab](docs/15_labtab.png) | ![Lab Index Image](docs/16_lab_index.png) |
@@ -69,6 +73,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- **Default view setting** — persist your preferred view mode (Thumbnail / Table)
- **Search clear (✕)** — inline ✕ button appears inside the search box whenever text is entered; click to clear immediately
- **Clear all filters (✕ Clear)** — rightmost toolbar button; resets search text, group filter, and badge filter (ALL) in one click
- **Sort** (v0.7.2) — Date (Newest/Oldest) / Name (A-Z/Z-A) dropdown next to the group filter, persisted via localStorage — same pattern as the Gallery tab's sort control
</details>
@@ -268,7 +273,7 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
- **Image Edit button** — toolbar button; sends the selected image directly to the Image Edit tab as the base layer; PSD files (v0.6.0) are fully decomposed into their original layers instead of being flattened into one image
- **Send GenUI Image button** — toolbar button (next to Image Edit); uploads the selected image to ComfyUI and sets it as the input for the first LoadImage node in the GenerateUI tab; automatically switches to GenerateUI → Input → Image; requires a workflow with at least one LoadImage node to be loaded
- **Send to LI node button** (v0.5.2) — toolbar button (next to Send GenUI Image); uploads the selected image and writes its filename into the `image` widget of the node currently selected on the ComfyUI canvas (falling back to the first LoadImage-type node found) — same destination and cross-window bridge as the Image Edit tab's **Send to LI node** button (renamed from "Send to Workflow" in this same release), so both entry points now share one consistent name; requires Workflow Studio to have been opened from ComfyUI's top menu button
- **Send CC button** (v0.3.69) — toolbar button, shown only when this Gallery tab is embedded as an iframe inside ComfyUI Comic Creator (a separate custom node); sends the selected image directly into Comic Creator's currently selected panel/overlay
- **Send CC button** (v0.3.69) — toolbar button, shown only when this Gallery tab is embedded as an iframe inside ComfyUI Comic Creator (a separate custom node); sends the selected image directly into Comic Creator's currently selected panel/overlay; when the selected file is an MP4 video (v0.6.3), it is routed to Comic Creator's Layout-tab Video tool instead — inserted as a new video object in the selected panel (playable, capturable to a still frame) rather than as a flattened image; sending a video while the Image tab is the intended destination is not yet supported and shows an info toast instead
- **Search clear (✕)** — inline ✕ button appears inside the search box whenever text is entered; click to clear immediately
- **Clear all filters (✕ Clear)** — toolbar button next to the view-mode (Thumbnail/Table) switch (v0.5.2, moved from the toolbar's rightmost position so it sits with the other display-affecting controls); resets search text, tag filter, group filter, and favorites filter in one click (sort order is preserved)
- **Workflow auto-save** — images generated from the Generate UI tab have their workflow automatically saved to gallery metadata
@@ -409,18 +414,21 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
</details>
<details>
<summary><h3>Video Tab (v0.5.0 – v0.5.7)</h3></summary>
<summary><h3>Video Tab (v0.5.0 – v0.7.2)</h3></summary>
- **Dedicated video generation UI, independent of GenerateUI** — a purpose-built tab for MiniMax H3 (Image to Video), LTX-2.5 (Text / Image / First & Last Frame to Video), and Wan2.2 (Text to Video) workflows (LTX-2.5/Wan2.2: v0.5.4); does not share state with the GenerateUI tab, and model selection (UNet/CLIP/VAE/Audio VAE) is fixed to the loaded workflow's own defaults rather than editable here
- **Plan / Edit subtabs** (center panel, batch redesign in v0.5.5) — a **plan** is a sequence of timeline blocks, each a fully self-contained generation job with its own Prompt, Duration, and First/Last Image; split the timeline with **+ Split** / **+ Block** / **Delete**, then **Run** to generate every block in order (Pause/Cancel work mid-run) — a plan with a single block behaves exactly like the old one-shot form. First Image can be None, an explicit image, or "use previous block's last frame" (chaining — an explicit Last Image on the previous block always wins over extracting one from its generated output). Save / Save As / Clear a plan (`ws_videoplan_*.json`, with a PNG contact-sheet thumbnail that embeds the full plan data so dropping the image back on works too); Edit is reserved for future video-editing tools
- **Plan subtab** (center panel, batch redesign in v0.5.5) — a **plan** is a sequence of timeline blocks, each a fully self-contained generation job with its own Prompt, Duration, and First/Last Image; split the timeline with **+ Split** / **+ Block** / **Delete**, then **Run** to generate every block in order (Pause/Cancel work mid-run) — a plan with a single block behaves exactly like the old one-shot form. First Image can be None, an explicit image, or "use previous block's last frame" (chaining — an explicit Last Image on the previous block always wins over extracting one from its generated output). Save / Save As / Clear a plan (`ws_videoplan_*.json`, with a PNG contact-sheet thumbnail that embeds the full plan data so dropping the image back on works too)
- **Base settings row** (topbar, v0.5.7) — Aspect Ratio / Megapixels / Multiple / Duration (seconds) / Noise Seed on one row — Duration always reflects the currently selected block (each block keeps its own value), the rest are plan-wide — plus a second row of model-specific fields: Width / Height (become directly editable for workflows with no Resolution Selector node, e.g. Wan2.2 and LTX-2.5's First & Last Frame to Video), Turbo Mode / Strength / Steps (MiniMax H3's turbo LoRA — Turbo Mode doubles as Wan2.2's own turbo toggle, sharing one field), and Prompt Enhance (LTX-2.5 only). Every field in both rows is enabled only when the loaded workflow actually has it — see Field highlight color below (one exception: Wan2.2's Text-to-Video variant doesn't expose Noise Seed at all, so it alone leaves that field disabled)
- **Asset / Project subtabs** (sidebar, v0.5.5) — **Asset** browses generated videos through three groups: **Video Assets** (a manually-curated keeper group you organize yourself, on your own schedule), **Video Temp** (every plan run's output lands here automatically regardless of whether the plan itself was saved — a **Clear Video Temp** button in the Gallery tab's detail panel empties it in one click), and **All Video Assets** (the union of both); selecting a video shows it in the center panel's left preview and lets you tag/memo it or jump straight to the full Gallery tab. **Project** lists every saved plan file (thumbnail, block count, last updated) — click one to load it into the Plan editor, or delete it with the × button; either sidebar panel collapses back down when its own tab is clicked again
- **Two center previews** (v0.5.5) — the left preview shows either the video currently selected in the Asset subtab or whatever's dropped/picked in the **Video Source** panel to its left (**Clear** resets it to empty); the right preview always shows the most recently generated clip. The Frame/GIF tools act on whichever preview was touched most recently
- **Edit subtab** (v0.7.0) — a lightweight clip timeline for trimming and concatenating video/image clips outside of generation: add clips via the Video Source panel's **Add to Edit** button or the Asset subtab's **Send to Edit** button, drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, Clear to empty the timeline. The trim panel for a selected video clip shows a Clipchamp-style scrubber — a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges (image clips get a simple hold-duration field instead); the playhead's own time badge sits a row above the in/out handle badges (v0.7.2) so it stays legible even when the playhead lands right on a trim point, and the scrubber has extra headroom/height and spacing from the clip name above it (v0.7.2). **▶ Preview** plays every ready clip back-to-back (trimmed to its in/out points) in the result preview, and **Export** builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into Video Temp automatically — exporting more than one clip currently drops audio, since ComfyUI's concatenation node requires uniform audio across all inputs
- **Edit project persistence** (v0.7.2) — **Save** / **Save As** / **Load** buttons in the Edit subtab's export panel persist the timeline itself (clip order, trim points, and which uploaded server-side file each clip refers to) as a `ws_videoeditproj_*.json` project file; Load re-fetches and re-probes each clip from its saved server reference, so the original files need to still exist
- **Asset subtab** (center panel, next to Plan/Edit — moved here from the sidebar in v0.7.0) — browses generated videos/images through three groups: **Video Assets** (a manually-curated keeper group you organize yourself, on your own schedule), **Video Temp** (every plan run's output lands here automatically regardless of whether the plan itself was saved — a **Clear Video Temp** button in the Gallery tab's detail panel empties it in one click), and **All Video Assets** (the union of both); a filename search box, a video/image type filter dropdown (All Types / Video Only / Image Only, v0.7.2), and a thumbnail-grid/table (▦/☰) view toggle sit above the list (v0.7.0). Selecting an item shows it in the center preview and lets you tag/memo it, **Send to Edit** (queues it as a clip in the Edit subtab), jump straight to the full Gallery tab, or — for images only — **Set as First Image** / **Set as Last Image** (v0.7.0, uploads it straight into the currently-selected Plan block and switches to the Plan subtab)
- **Project subtab** (sidebar) — lists saved plan files while the center Plan subtab is active, or saved Edit projects while the center Edit subtab is active (v0.7.2 — thumbnail/block-or-clip-count/last-updated) — click one to load it into the matching editor, or delete it with the × button; the panel collapses back down when its own tab is clicked again, and switching Plan↔Edit while it's open refreshes the list in place
- **Two center previews** (v0.5.5) — the left preview shows either the video currently selected in the Asset subtab or whatever's dropped/picked in the **Video Source** panel to its left (**Clear** resets it to empty); the right preview always shows the most recently generated clip; both preview frames are a fixed height (v0.7.0) so switching sources never shifts the Plan/Edit/Asset panel below them. The Frame/GIF tools act on whichever preview was touched most recently
- **First Frame / Last Frame are both optional** — leaving First Image empty runs the workflow text-only (the node is simply left unconnected — a "bypass"); workflows that don't even include a LoadImage node for First Frame are supported out of the box (Wan2.2's Text-to-Video graph has neither slot at all), and a LoadImage node is only injected into the graph on the fly if an image is actually supplied
- **Semantic node lookup, not hardcoded IDs** (generalized in v0.5.4) — detects which of the three families is loaded from its subgraph's internal node types (or, for Wan2.2, the subgraph's own name — it has no distinctive internal node type), then locates its Resolution Selector and LoadImage node(s) by the subgraph's *public input label* (`prompt`, `duration`, `width`, `first_frame`, `last_frame`) rather than a fixed slot index — so it keeps working even though the three families expose those inputs under different internal names; a workflow no longer needs to match any bundled reference file's exact node IDs, and no fallback test workflow is auto-loaded on tab open
- **Field highlight color** — Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple / Noise Seed / Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance labels turn the same highlight color as the GenerateUI Model tab (shared setting, customizable in the Settings tab — see below) when the loaded workflow actually exposes that field; e.g. a Wan2.2 workflow (no Resolution Selector node) leaves Aspect Ratio / Megapixels / Multiple unhighlighted while Width / Height light up instead
- **Load in Video** (Workflow tab) — load any workflow containing a MiniMax H3 Image-to-Video node, an LTX-2.5 Text/Image/First&Last-Frame-to-Video node, or a Wan2.2 Text-to-Video node straight into this tab from the Workflow tab's side-panel toolbar (next to Load in GenerateUI) — starts a fresh single-block plan seeded from the workflow's own current values; unsupported workflows show an error toast instead of loading
- **Frame tab** (right panel) — captures the active preview's current playback position as a still frame: play/seek to the position you want, click **Capture Frame** (client-side canvas, no server round-trip), then **Save to Output** to write it as a PNG into ComfyUI's own Output folder (auto-numbered, same pattern as other generated images)
- **Frame tab** (right panel) — captures the active preview's current playback position as a still frame: play/seek to the position you want, click **Capture Frame** (client-side canvas, no server round-trip), then **Save to Output** to write it as a PNG into ComfyUI's own Output folder (auto-numbered, same pattern as other generated images); an optional **Add to Video Assets group** checkbox (v0.7.0) also tags the saved frame into Gallery's Video Assets group
- **GIF tab** (right panel) — converts the active preview to an animated GIF; set Start/End (seconds), FPS, and an optional Max Width, then **Convert to GIF** — the server decodes the video (PyAV) and encodes an animated GIF (Pillow), both dependencies ComfyUI itself already requires, so no ffmpeg binary or extra install is needed; the result is saved into ComfyUI's Output folder and previewed inline
- **Playback volume** — the preview player's volume is saved to the Settings tab and shared across every video player in the plugin (Video tab, Gallery tab), so it no longer resets to maximum each time a different video is opened
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

-183
View File
@@ -1,183 +0,0 @@
{
"0": "escapeHtml",
"1": "prompt-table.js",
"2": "models_routes.py",
"3": "generate-tab.js",
"4": "_openCellModal",
"5": "gallery_routes.py",
"6": "gallery-tab.js",
"7": "ImageEditTab",
"8": "ai-tab.js",
"9": "eagle_routes.py",
"10": "feeder-tab.js",
"11": "NodesService",
"12": "settings_routes.py",
"13": "metadata-tab.js",
"14": "node_sets_menu.js",
"15": "GalleryService",
"16": "settings-tab.js",
"17": "tagger_routes.py",
"18": "WildcardService",
"19": "tagger-tab.js",
"20": "renderContent",
"21": "comfyui-editor.js",
"22": "ComfyUI-Workflow-Studio",
"23": "TaggerService",
"24": "top_menu_extension.js",
"25": "nodes-tab.js",
"26": "setupAiHandlers",
"27": "workflow-tab.js",
"28": "workflow_routes.py",
"29": "GalleryMetadataStore",
"30": "WorkflowService",
"31": "LabPlanService",
"32": "skill_routes.py",
"33": "LayerManager",
"34": "SelectTool",
"35": "CivitaiService",
"36": "DrawTool",
"37": "MaskTool",
"38": "index.html Template",
"39": "gallery_service.py",
"40": "FileExport",
"41": "image-prompt-tab.js",
"42": "KSampler Node",
"43": "image-edit-tab.js",
"44": "style-catalog-tab.js",
"45": "add",
"46": "comfyui-workflow.js",
"47": "BlurTool",
"48": "MaskShapeTool",
"49": "MaskVectorTool",
"50": "ShapeTool",
"51": "gmic_routes.py",
"52": "ollama_routes.py",
"53": "TextTool",
"54": "analyze_workflow",
"55": "MaskTextTool",
"56": "lab-tab.js",
"57": ".list_images",
"58": "MaskAlphaTool",
"59": "MaskColorTool",
"60": "gallery_feeder_extension.js",
"61": "createPanel",
"62": "_runLabBatch",
"63": "BgRemove",
"64": "Workflow JSON",
"65": "WFS_GalleryFeeder",
"66": "initGenerateTab",
"67": "InpaintI2IActions",
"68": "Sam3Segmentation",
"69": "fromWorkflow",
"70": "_extractPromptsAPI",
"71": "generate_doc_index.py",
"72": "WS Inpaint Basic Workflow",
"73": "._bg_index_folder",
"74": "aiohttp",
"75": "import_style_prompt_seed.py",
"76": "Image Edit (FireRed Image Edit 1.1) node",
"77": "Version 0.3.9",
"78": "DWPose Image Feeder Workflow",
"79": "wfm.py",
"80": "README.index.md",
"81": "WFS_PromptText",
"82": "workflow_service.py",
"83": "SPA",
"84": "prompt-ai-chat.js",
"85": "Tagger Tab requirements.txt",
"86": "data directory",
"87": "t",
"88": "Chinese Language Support",
"89": "Version 0.3.95",
"90": "Windows",
"91": "add_routes",
"92": "Bounded Cache",
"93": "Business Logic",
"94": "check_lazy_status",
"95": "HF_HOME",
"96": "IS_CHANGED",
"97": "Lab Subtab",
"98": "lab_routes.py",
"99": "PowerShell",
"100": "prestartup_script.py",
"101": ".get_image_prompt_root",
"102": ".get_style_catalog_root",
"103": ".start_background_index",
"104": ".update_workflows_dir",
"105": "Version 0.4.2",
"106": "Workflow Studio Banner",
"107": "Workflow Studio Icon",
"108": "BiRefNet Model",
"109": "Chinese",
"110": "SDXL 2D Pose Editor I2I Workflow",
"111": "ComfyUI Manager",
"112": "Tagger Tab Screenshot (WD Tagger/DeepDanbooru)",
"113": "Models Tab UI Screenshot",
"114": "Workflow Studio Topbar Icon Strip",
"115": "GenerateUI LoRA Stack Tab Screenshot",
"116": "Workflow Studio Favicon Icon",
"117": "2D Pose Editor to Image Edit (FireRed) Workflow",
"118": "3D Pose Editor to Image Edit (Flux.2 Klein) Workflow",
"119": "SDXL DWPose ControlNet Workflow",
"120": "SDXL From Metadata T2I Workflow",
"121": "SDXL Img2Img + FaceDetailer Workflow",
"122": "SDXL Image Feeder Img2Img Workflow",
"123": "SDXL T2I + FaceDetailer Workflow",
"124": "GenerateUI Batch Tab Screenshot",
"125": "Multiple Select Menu Screenshot",
"126": "Image Edit Tab Screenshot",
"127": "Workflow Tab Screenshot",
"128": "Prompt Tab Input Assistance Screenshot",
"129": "GenerateUI Feeder Tab Screenshot",
"130": "Workflow Studio Library Panel (Lora Info View)",
"131": "Library Panel I Tab (Prompts) Screenshot",
"132": "Settings Tab Screenshot (Language, Theme)",
"133": "ComfyUI Workflow Studio Thumbnail",
"134": "English",
"135": "Feeder Subtab",
"136": "folder_paths",
"137": "Help & Support Tab",
"138": "i18n",
"139": "Japanese",
"140": "Metadata Tab",
"141": "NODE_CLASS_MAPPINGS",
"142": "OrderedDict",
"143": "get_output_directory",
"144": "comfyui-workflow-studio",
"145": "ComfyUI PromptServer",
"146": "py/__init__.py",
"147": "nodes/__init__.py",
"148": "routes/__init__.py",
"149": "services/__init__.py",
"150": "python-dotenv",
"151": "Silent Failure Diagnosis",
"152": "get_temp_directory",
"153": "prompts_routes.py",
"154": "VALIDATE_INPUTS",
"155": "SD1.5 Basic Text-to-Image Workflow",
"156": "SDXL Text-to-Image Basic Workflow Screenshot",
"157": "SettingsService",
"158": "on_doc_edit.py",
"159": "ComfyUI-Custom-Scripts",
"160": "ComfyUI-Gallery",
"161": "ComfyUI-Lora-Manager",
"162": "Eagle",
"163": "Fooocus",
"164": "Gallery Tab",
"165": "GenerateUI Tab",
"166": "Image Edit Tab",
"167": "infinite-image-browsing",
"168": "Lemonade Server",
"169": "LM Studio",
"170": "Ollama",
"171": "Settings Tab",
"172": "Tagger Tab",
"173": "Unsloth",
"174": "app.js",
"175": "PromptsService",
"176": "util.js",
"177": "civitai_service.py",
"178": "config.py",
"179": "createModelItem",
"180": "_renderBatchPreview"
}
-605
View File
@@ -1,605 +0,0 @@
# Graph Report - ComfyUI-Workflow-Studio (2026-08-22)
## Corpus Check
- 113 files · ~567,985 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 2314 nodes · 5664 edges · 181 communities (87 shown, 94 thin omitted)
- Extraction: 95% EXTRACTED · 5% INFERRED · 0% AMBIGUOUS · INFERRED: 302 edges (avg confidence: 0.88)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `ad9d06d3`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation)
- escapeHtml
- prompt-table.js
- models_routes.py
- generate-tab.js
- _openCellModal
- gallery_routes.py
- gallery-tab.js
- ImageEditTab
- ai-tab.js
- eagle_routes.py
- feeder-tab.js
- NodesService
- settings_routes.py
- metadata-tab.js
- node_sets_menu.js
- GalleryService
- settings-tab.js
- tagger_routes.py
- WildcardService
- tagger-tab.js
- renderContent
- comfyui-editor.js
- ComfyUI-Workflow-Studio
- TaggerService
- top_menu_extension.js
- nodes-tab.js
- setupAiHandlers
- workflow-tab.js
- workflow_routes.py
- GalleryMetadataStore
- WorkflowService
- LabPlanService
- skill_routes.py
- LayerManager
- SelectTool
- CivitaiService
- DrawTool
- MaskTool
- index.html Template
- gallery_service.py
- FileExport
- image-prompt-tab.js
- KSampler Node
- image-edit-tab.js
- style-catalog-tab.js
- add
- comfyui-workflow.js
- BlurTool
- MaskShapeTool
- MaskVectorTool
- ShapeTool
- gmic_routes.py
- ollama_routes.py
- TextTool
- analyze_workflow
- MaskTextTool
- lab-tab.js
- .list_images
- MaskAlphaTool
- MaskColorTool
- gallery_feeder_extension.js
- createPanel
- _runLabBatch
- BgRemove
- Workflow JSON
- WFS_GalleryFeeder
- initGenerateTab
- InpaintI2IActions
- Sam3Segmentation
- fromWorkflow
- _extractPromptsAPI
- generate_doc_index.py
- WS Inpaint Basic Workflow
- ._bg_index_folder
- aiohttp
- import_style_prompt_seed.py
- Image Edit (FireRed Image Edit 1.1) node
- Version 0.3.9
- DWPose Image Feeder Workflow
- wfm.py
- README.index.md
- WFS_PromptText
- workflow_service.py
- SPA
- prompt-ai-chat.js
- Tagger Tab requirements.txt
- data directory
- t
- Chinese Language Support
- Version 0.3.95
- Windows
- add_routes
- Bounded Cache
- Business Logic
- check_lazy_status
- HF_HOME
- IS_CHANGED
- Lab Subtab
- lab_routes.py
- PowerShell
- prestartup_script.py
- .get_image_prompt_root
- .get_style_catalog_root
- .start_background_index
- .update_workflows_dir
- Version 0.4.2
- Workflow Studio Banner
- Workflow Studio Icon
- BiRefNet Model
- Chinese
- SDXL 2D Pose Editor I2I Workflow
- ComfyUI Manager
- Tagger Tab Screenshot (WD Tagger/DeepDanbooru)
- Models Tab UI Screenshot
- Workflow Studio Topbar Icon Strip
- GenerateUI LoRA Stack Tab Screenshot
- Workflow Studio Favicon Icon
- 2D Pose Editor to Image Edit (FireRed) Workflow
- 3D Pose Editor to Image Edit (Flux.2 Klein) Workflow
- SDXL DWPose ControlNet Workflow
- SDXL From Metadata T2I Workflow
- SDXL Img2Img + FaceDetailer Workflow
- SDXL Image Feeder Img2Img Workflow
- SDXL T2I + FaceDetailer Workflow
- GenerateUI Batch Tab Screenshot
- Multiple Select Menu Screenshot
- Image Edit Tab Screenshot
- Workflow Tab Screenshot
- Prompt Tab Input Assistance Screenshot
- GenerateUI Feeder Tab Screenshot
- Workflow Studio Library Panel (Lora Info View)
- Library Panel I Tab (Prompts) Screenshot
- Settings Tab Screenshot (Language, Theme)
- ComfyUI Workflow Studio Thumbnail
- English
- Feeder Subtab
- folder_paths
- Help & Support Tab
- i18n
- Japanese
- Metadata Tab
- NODE_CLASS_MAPPINGS
- OrderedDict
- get_output_directory
- comfyui-workflow-studio
- ComfyUI PromptServer
- python-dotenv
- Silent Failure Diagnosis
- get_temp_directory
- prompts_routes.py
- VALIDATE_INPUTS
- SD1.5 Basic Text-to-Image Workflow
- SDXL Text-to-Image Basic Workflow Screenshot
- SettingsService
- on_doc_edit.py
- ComfyUI-Custom-Scripts
- ComfyUI-Gallery
- ComfyUI-Lora-Manager
- Eagle
- Fooocus
- Gallery Tab
- GenerateUI Tab
- Image Edit Tab
- infinite-image-browsing
- Lemonade Server
- LM Studio
- Ollama
- Settings Tab
- Tagger Tab
- Unsloth
- app.js
- PromptsService
- util.js
- civitai_service.py
- config.py
- createModelItem
- _renderBatchPreview
## God Nodes (most connected - your core abstractions)
1. `t()` - 233 edges
2. `showToast()` - 192 edges
3. `escapeHtml()` - 73 edges
4. `ImageEditTab` - 53 edges
5. `GalleryService` - 45 edges
6. `setup_routes()` - 32 edges
7. `initSettingsTab()` - 31 edges
8. `add()` - 31 edges
9. `renderModelGrid()` - 29 edges
10. `TaggerService` - 26 edges
## Surprising Connections (you probably didn't know these)
- `beforeRegisterNodeDef()` --indirect_call--> `saveSelectedAsNodeSet()` [INFERRED]
web/comfyui/top_menu_extension.js → web/comfyui/node_sets_menu.js
- `Publish to Comfy Registry Workflow` --conceptually_related_to--> `index.html Template` [INFERRED]
.github/workflows/publish.yml → templates/index.html
- `Version 0.3.9` --references--> `Models Tab` [EXTRACTED]
DEVLOG.index.md → README.index.md
- `Version 0.3.9` --references--> `Nodes Tab` [EXTRACTED]
DEVLOG.index.md → README.index.md
- `Version 0.3.9` --references--> `Prompt Tab` [EXTRACTED]
DEVLOG.index.md → README.index.md
## Import Cycles
- 3-file cycle: `static/js/app.js -> static/js/generate-tab.js -> static/js/feeder-tab.js -> static/js/app.js`
- 3-file cycle: `static/js/feeder-tab.js -> static/js/gallery-tab.js -> static/js/generate-tab.js -> static/js/feeder-tab.js`
- 3-file cycle: `static/js/app.js -> static/js/prompt-tab.js -> static/js/prompt-presets.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/prompt-tab.js -> static/js/prompt-ai-chat.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/prompt-tab.js -> static/js/prompt-styles.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/prompt-tab.js -> static/js/prompt-wildcards.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/models-tab.js -> static/js/models/badges.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/gallery-tab.js -> static/js/image-prompt-tab.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/image-edit/Sam3Segmentation.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/image-edit/GmicIntegration.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/image-edit/FileExport.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/image-edit/MaskEditorOneBridge.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/models-tab.js -> static/js/comfyui-editor.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/comfyui-editor.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/image-edit/BgRemove.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/image-edit/BlurTool.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/image-edit-tab.js -> static/js/image-edit/InpaintI2IActions.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/gallery-tab.js -> static/js/style-catalog-tab.js -> static/js/app.js`
- 3-file cycle: `static/js/app.js -> static/js/generate-tab.js -> static/js/comfyui-editor.js -> static/js/app.js`
- 3-file cycle: `static/js/ai-tab.js -> static/js/app.js -> static/js/gallery-tab.js -> static/js/ai-tab.js`
## Hyperedges (group relationships)
- **Architecture Layers** — workflowstudio, add_routes, py_wfm, py_routes, py_services, business_logic, web_comfyui, static, templates, data, settings_json, metadata_json, node_metadata_json [EXTRACTED 0.80]
- **Graphify Index Generation** — readme_index, devlog_index, generate_doc_index, graphifyignore, readme, devlog, claude_hooks, git_hooks [EXTRACTED 0.85]
- **Workflow Studio UI Navigation Tabs** — templates_index_html_workflow, templates_index_html_nodes, templates_index_html_models, templates_index_html_generate, templates_index_html_prompt, templates_index_html_gallery, templates_index_html_image_edit, templates_index_html_tagger, templates_index_html_settings, templates_index_html_help, templates_index_html_ai [EXTRACTED 0.85]
- **Workflow JSON Consistency Group** — last_node_id, last_link_id, links, workflow_json [EXTRACTED 0.85]
- **i18n Languages** — i18n, english, japanese, chinese [EXTRACTED 0.90]
- **Mask Editor One Inpainting Pipeline** — comfyui-workflow-studio_workflows_ws_inpaint_mask_editor_one_load_checkpoint_node, comfyui-workflow-studio_workflows_ws_inpaint_mask_editor_one_mask_editor_one_node, comfyui-workflow-studio_workflows_ws_inpaint_mask_editor_one_clip_text_encode_positive_node, comfyui-workflow-studio_workflows_ws_inpaint_mask_editor_one_clip_text_encode_negative_node, comfyui-workflow-studio_workflows_ws_inpaint_mask_editor_one_vae_encode_for_inpainting_node, comfyui-workflow-studio_workflows_ws_inpaint_mask_editor_one_save_image_node [INFERRED]
## Communities (181 total, 94 thin omitted)
### Community 0 - "escapeHtml"
Cohesion: 0.09
Nodes (80): openModal(), comfyUI, badgeHtml(), bindBadgeModalEvents(), bindBadgeRowEvents(), getBadgePalette(), modelBadgesHtml(), _onGridChange() (+72 more)
### Community 1 - "prompt-table.js"
Cohesion: 0.06
Nodes (99): closeModal(), refreshStylesList(), apiCreatePreset(), apiDeletePreset(), apiUpdatePreset(), clearBatchPresets(), createPmItem(), esc() (+91 more)
### Community 2 - "models_routes.py"
Cohesion: 0.06
Nodes (55): handle_change_preview(), handle_civitai_batch(), handle_civitai_cache(), handle_civitai_fetch(), handle_delete_models(), handle_get_disabled(), handle_get_filepath(), handle_get_groups() (+47 more)
### Community 3 - "generate-tab.js"
Cohesion: 0.09
Nodes (41): _applyNamedStyle(), _applyStyleToWorkflow(), _batchGroupState, _batchStyleSelected, _blobToDataUrl(), _buildFolderTree(), _ckptBatch, _ckptState (+33 more)
### Community 4 - "_openCellModal"
Cohesion: 0.24
Nodes (12): _applyStyleToText(), _cellHtml(), _cellLabel(), _effectiveDisplayValue(), _isEmptyValue(), _isLiveDisplay(), _liveValueFor(), _openCellModal() (+4 more)
### Community 5 - "gallery_routes.py"
Cohesion: 0.10
Nodes (54): add_to_group(), bulk_favorite(), bulk_group(), clear_group_images(), create_folder_route(), create_group(), delete_folder_route(), delete_group() (+46 more)
### Community 6 - "gallery-tab.js"
Cohesion: 0.10
Nodes (53): addTag(), API, apiFetch(), _appendNextPage(), _applySelectionToDOM(), _attachScrollSentinel(), bindEvents(), bulkAddToGroup() (+45 more)
### Community 8 - "ai-tab.js"
Cohesion: 0.09
Nodes (52): appendChatBubble(), _appendSkillSaveButton(), _appendSvgPreview(), _applyGenOptions(), buildTranslationMessages(), callChat(), callLLM(), callVLM() (+44 more)
### Community 9 - "eagle_routes.py"
Cohesion: 0.14
Nodes (20): _eagle_add(), _eagle_add_from_path(), _eagle_test(), handle_add(), handle_test(), Application, Request, Response (+12 more)
### Community 10 - "feeder-tab.js"
Cohesion: 0.10
Nodes (50): _applyGalToWorkflow(), _applyPreset(), _applyToGalNode(), _applyToNode(), _applyToWorkflow(), _deletePreset(), _deselectAll(), _feederNodes() (+42 more)
### Community 11 - "NodesService"
Cohesion: 0.09
Nodes (27): handle_create_set(), handle_delete_set(), handle_export_set(), handle_get_groups(), handle_get_metadata(), handle_list_sets(), handle_save_groups(), handle_save_metadata() (+19 more)
### Community 12 - "settings_routes.py"
Cohesion: 0.09
Nodes (44): _apply_import_bundle(), _build_export_bundle(), _find_style(), _get_comfyui_output_dir(), handle_create_style(), handle_delete_style(), handle_export(), handle_get() (+36 more)
### Community 13 - "metadata-tab.js"
Cohesion: 0.11
Nodes (40): buildLoRAItem(), buildModelItem(), buildPromptItem(), collectAllNodes(), collectUnique(), extractAllMetadata(), fromWorkflow(), extractCheckpoints() (+32 more)
### Community 14 - "node_sets_menu.js"
Cohesion: 0.08
Nodes (38): AI_BACKEND_DEFAULT_URLS, AI_LANG_NAMES, convertApiToUiWorkflow(), _extractAllMetadata(), _extractWorkflowFromEXIF(), fetchGroups(), fetchMetadata(), fetchNodeSets() (+30 more)
### Community 15 - "GalleryService"
Cohesion: 0.10
Nodes (13): GalleryService, Path, 許可するルートパスを更新する(Settings変更時に呼ぶ), ComfyUI実outputフォルダの不変ルートを設定する(起動時に一度だけ呼ぶ), パスが許可ルート配下かチェック(パストラバーサル防止)。 Output ギャラリーの _allowed_root、またはComfyUI実outputフォルダの…, outputフォルダのフォルダツリーを返す, 画像と同名の.txtサイドカーにプロンプトテキストを保存する, ワークフローを抽出する。 優先順位: PNG[workflow] > PNG[prompt] > gallery_metadata.json[workflow] (+5 more)
### Community 16 - "settings-tab.js"
Cohesion: 0.09
Nodes (40): getLang(), getLanguageOptions(), getSummaryLang(), getSummaryLanguageOptions(), getSummaryPrompt(), LANGUAGE_OPTIONS, LANGUAGES, setLang() (+32 more)
### Community 17 - "tagger_routes.py"
Cohesion: 0.14
Nodes (24): Connection, handle_batch_start(), handle_batch_status(), handle_batch_stop(), handle_db_delete(), handle_db_export(), handle_db_list(), handle_db_save() (+16 more)
### Community 18 - "WildcardService"
Cohesion: 0.12
Nodes (21): handle_create_link(), handle_delete(), handle_get_content(), handle_link_status(), handle_list(), handle_remove_link(), handle_save(), Application (+13 more)
### Community 19 - "tagger-tab.js"
Cohesion: 0.14
Nodes (29): _applyI18n(), _batchStart(), _batchStop(), _dbDelete(), _dbExport(), _dbLoad(), _dbSave(), _dbSearch() (+21 more)
### Community 20 - "renderContent"
Cohesion: 0.16
Nodes (28): createDraggableItem(), createDraggablePromptItem(), createDraggableWfItem(), esc(), extractPackageName(), getNodeCategory(), getNodePackage(), getWfBadges() (+20 more)
### Community 21 - "comfyui-editor.js"
Cohesion: 0.08
Nodes (28): _applyLoraToNode(), _attachPromptWeightControl(), _buildLoraManagerSyntax(), _buildLoraSyntax(), _buildPresetOptions(), _compositeImageWithMask(), _I2I_PLACEHOLDER_DEFAULT, _imageInputToDataURL() (+20 more)
### Community 22 - "ComfyUI-Workflow-Studio"
Cohesion: 0.50
Nodes (4): CivitAI, ComfyUI-Workflow-Studio, Mask Editor One, Version 0.3.65
### Community 23 - "TaggerService"
Cohesion: 0.14
Nodes (5): Image, Path, WD Tagger / DeepDanbooru / Ollama VLM 推論サービス。, パストラバーサル防止: セパレータ・NUL・相対参照を拒否, TaggerService
### Community 24 - "top_menu_extension.js"
Cohesion: 0.14
Nodes (27): ../../scripts/ui/components/button.js, ../../scripts/ui/components/buttonGroup.js, getNodeSetsIcon(), NODE_SETS_TOOLTIP, attachTopMenuButton(), beforeRegisterNodeDef(), captureCanvasSnapshot(), compareVersions() (+19 more)
### Community 25 - "nodes-tab.js"
Cohesion: 0.11
Nodes (40): _applyNodeSelectionToDOM(), bulkNodeAddToGroup(), bulkNodeCreateAndAddToGroup(), bulkNodeRemoveFromGroup(), bulkNodeSetFavorite(), categoryBadgeHtml(), createNodeCard(), createNodeSet() (+32 more)
### Community 26 - "setupAiHandlers"
Cohesion: 0.11
Nodes (28): _aiApplyGenOptions(), aiCallChat(), aiCallLLM(), aiCallVLM(), aiFetchModels(), aiFileToBase64(), aiLooksUntranslated(), aiSkillDeleteFile() (+20 more)
### Community 27 - "workflow-tab.js"
Cohesion: 0.13
Nodes (34): readJsonStorage(), badgeHtml(), _buildStructuredSummaryText(), _buildSummarySourceText(), clearBatch(), closeSidePanel(), deleteWorkflow(), fetchWorkflows() (+26 more)
### Community 28 - "workflow_routes.py"
Cohesion: 0.15
Nodes (26): handle_analyze(), handle_change_thumbnail(), handle_delete(), handle_import(), handle_list(), handle_metadata(), handle_raw(), handle_reanalyze_all() (+18 more)
### Community 29 - "GalleryMetadataStore"
Cohesion: 0.13
Nodes (9): GalleryMetadataStore, Path, グループ名を変更し、全画像のgroupsフィールドも更新する, gallery_metadata.json の構造: { "images": { "<abs_path>": { "favorite": bool,…, 存在しないパスをグループから一括削除する(1回の保存で完結), グループメンバーのパスをsetで返す(高速フィルタ用), グループ内の全画像を除外する(グループ自体は残す), 画像のパスキーを変更する(ファイル移動後のメタデータ引継ぎ) (+1 more)
### Community 30 - "WorkflowService"
Cohesion: 0.13
Nodes (13): Get raw workflow JSON content., Save metadata (tags, memo, summary, etc.) for a workflow., Manages workflow files and metadata., Import workflow files. Returns list of results., Rename workflow and its associated thumbnail., Delete workflow JSON and associated thumbnail., Re-analyze a workflow and save results to metadata., Re-analyze all workflows and update metadata. (+5 more)
### Community 31 - "LabPlanService"
Cohesion: 0.23
Nodes (4): LabPlanService, Path, Lab tab plan file management service (JSON plan + optional PNG index thumbnail)., Saves the contact-sheet index image into ComfyUI's own output folder, alongside…
### Community 32 - "skill_routes.py"
Cohesion: 0.14
Nodes (16): handle_delete(), handle_get_content(), handle_list(), handle_save(), Application, Request, Response, AI skill (.md system prompt) file management API routes. (+8 more)
### Community 35 - "CivitaiService"
Cohesion: 0.15
Nodes (11): CivitaiService, Build request headers, optionally including Bearer token., Calculate SHA256 hash of a file., Fetch model version info from CivitAI by SHA256 hash (GET). 429/5xx…, POST /model-versions/by-hash でハッシュリストを一括取得(最大100件/リクエスト)。 レスポンスの…, Extract relevant fields from CivitAI API response., Batch fetch CivitAI info for multiple model files. Phase 1: SHA256…, Clear cache for a specific hash or all. (+3 more)
### Community 38 - "index.html Template"
Cohesion: 0.10
Nodes (22): actions/checkout@v4, Comfy-Org/publish-node-action@main, DEVLOG.md, Publish to Comfy Registry Workflow, graphifyignore, main Branch, pyproject.toml, README.md (+14 more)
### Community 39 - "gallery_service.py"
Cohesion: 0.10
Nodes (13): Gallery Metadata Store - ギャラリー画像のメタデータ永続化, _clean_vault_tags(), _decode_image_data_url(), _FolderCache, _parse_vault_yaml_leaves(), Gallery Service - outputフォルダの画像管理、メタデータ閲覧, フォルダ単位の画像スキャン結果キャッシュ。 フォルダのmtimeが変わった場合、またはTTL超過時に再スキャンする。, data URL (data:<mime>;base64,<data>) をデコードし (バイト列, 拡張子) を返す。 ヘッダが無ければPNG扱い(旧互換)。 (+5 more)
### Community 41 - "image-prompt-tab.js"
Cohesion: 0.21
Nodes (20): activateImagePromptTab(), addSelectedToChips(), API, apiFetch(), cleanPromptText(), clearAllChips(), copyFinalPrompt(), createThumbCard() (+12 more)
### Community 42 - "KSampler Node"
Cohesion: 0.19
Nodes (20): CLIP Text Encode Negative Prompt Node, CLIP Text Encode Positive Prompt Node, Inpaint with Mask Editor One (Workflow Screenshot), Load Checkpoint Node, Mask Editor One Node, Save Image Node, VAE Encode (for Inpainting) Node, 3D Pose Editor Node (comfyui-vrm-pose-editor) (+12 more)
### Community 43 - "image-edit-tab.js"
Cohesion: 0.12
Nodes (8): comfyEditor, comfyWorkflow, GmicIntegration, MaskEditorOneBridge, MASK_TEXT_FONTS, fitToCanvas(), TOOL_DEFS, TEXT_FONTS
### Community 44 - "style-catalog-tab.js"
Cohesion: 0.22
Nodes (18): selectStyleByName(), activateStyleCatalogTab(), API, apiFetch(), copyText(), createThumbCard(), debounce(), initStyleCatalogTab() (+10 more)
### Community 45 - "add"
Cohesion: 0.22
Nodes (19): add(), fetchWorkflowRaw(), getCanvasDropPos(), handleInfoFile(), installCanvasDropHandler(), loadWorkflowOnCanvas(), placeClipTextEncodeNode(), placeLoraMgrNode() (+11 more)
### Community 46 - "comfyui-workflow.js"
Cohesion: 0.20
Nodes (13): _CONTROL_AFTER_GENERATE, _findInjectedWidgetIndex(), _flattenSubgraphs(), _getDynamicComboSubNames(), _getWidgetInputNames(), _getWidgetInputTypes(), _isExtraWidgetValue(), _isLinkedWidgetName() (+5 more)
### Community 51 - "gmic_routes.py"
Cohesion: 0.21
Nodes (14): _gmic_run_gui(), handle_open(), handle_result(), handle_status(), Application, Request, Response, G'MIC-Qt integration API routes. (+6 more)
### Community 52 - "ollama_routes.py"
Cohesion: 0.23
Nodes (14): _get_ollama_config(), handle_chat(), handle_models(), handle_test(), Application, Request, Response, Ollama API proxy routes. (+6 more)
### Community 54 - "analyze_workflow"
Cohesion: 0.20
Nodes (13): analyze_workflow(), _clip_type_from_ui_node(), _collect_all_ui_nodes(), _detect_model_type_from_name(), _model_name_from_api_node(), _model_name_from_ui_node(), Workflow analyzer - detects model types and counts input/output nodes., Analyze workflow JSON and return model types, input/output node counts. (+5 more)
### Community 56 - "lab-tab.js"
Cohesion: 0.12
Nodes (41): _applyPlanData(), _buildIndexImageDataUrl(), _buildPlanData(), _clearPlan(), COLUMN_KEYS, _defaultValueFor(), _emptyColumns(), _emptyLabState() (+33 more)
### Community 57 - ".list_images"
Cohesion: 0.17
Nodes (6): os.scandir() で画像ファイルを1回のシステムコールで列挙。 キャッシュがあればそれを返す。 Returns: [(name,…, os.walk() でフォルダ配下を再帰的に列挙する(サブフォルダの画像も含む)。 ImagePrompt/Style…, 指定フォルダ内の画像一覧を返す(recursive=Trueならサブフォルダも含める), 画像のプロンプトテキストを返す。 1. 画像と同名の .txt サイドカーがあればそれを使う 2. 無ければ ponyxlWildcardsVault 形式…, image_path の祖先を辿り、直下に thumbnails(_option2) を持つフォルダ…, ponyxlWildcardsVault形式のフォールバック解決。 thumbnails_option2 の "{leaf}.preview3.ext"…
### Community 60 - "gallery_feeder_extension.js"
Cohesion: 0.36
Nodes (11): beforeRegisterNodeDef(), fetchGroupImages(), getInputWidget(), getOrCreate(), _ns, setWidgetValue(), startLoop(), stopLoop() (+3 more)
### Community 61 - "createPanel"
Cohesion: 0.22
Nodes (13): applyTheme(), buildThemePanel(), createPanel(), fetchModelGroups(), fetchModelMetadata(), fetchPrompts(), fetchWorkflows(), injectStyles() (+5 more)
### Community 62 - "_runLabBatch"
Cohesion: 0.20
Nodes (10): _annotatedImageRef(), _applyLabLoraToWorkflow(), _buildLoraPromptInjection(), _buildWorkflowForIteration(), _fetchLabLoraMetadataCache(), _labTriggerWordsFor(), _runLabBatch(), _setRunUiState() (+2 more)
### Community 64 - "Workflow JSON"
Cohesion: 0.20
Nodes (10): EXTRA_PNGINFO, last_link_id, last_node_id, links array, Position Index, PROMPT, widgets_values, workflow_analyzer.py (+2 more)
### Community 65 - "WFS_GalleryFeeder"
Cohesion: 0.27
Nodes (4): Path, WFS_GalleryFeeder – Feeds images from a gallery group into a workflow., Gallery グループ内の画像をインデックス順に1枚ずつ出力するノード。, WFS_GalleryFeeder
### Community 66 - "initGenerateTab"
Cohesion: 0.22
Nodes (7): initGenerateTab(), _loadStyles(), _renderStyleDropdown(), updateStatus(), highlightJSON(), syncJsonHighlight(), syncScroll()
### Community 69 - "fromWorkflow"
Cohesion: 0.36
Nodes (10): _collectAllNodes(), _collectUnique(), fromWorkflow(), _extractCheckpoints(), _extractDiffusionModels(), _extractLoRAs(), _extractMarkdownNoteModels(), addU() (+2 more)
### Community 70 - "_extractPromptsAPI"
Cohesion: 0.33
Nodes (10): _extractPrompts(), _extractPromptsAPI(), _extractPromptsFromNodeSet(), _extractPromptsLiteGraph(), _isPromptStylerNode(), _isSamplerNode(), _isTextEncoderNode(), _resolveEditPlusText() (+2 more)
### Community 71 - "generate_doc_index.py"
Cohesion: 0.53
Nodes (5): generate_devlog_index(), generate_readme_index(), main(), Path, Regenerate README.index.md / DEVLOG.index.md from README.md / DEVLOG.md.…
### Community 72 - "WS Inpaint Basic Workflow"
Cohesion: 0.53
Nodes (9): CLIP Text Encode (Prompt) - negative conditioning, CLIP Text Encode (Prompt) - positive conditioning, KSampler node (ddim sampler, 30 steps, cfg 4.0, denoise 1.00), Load Checkpoint node (hayochamixIDXL_v11+_bl checkpoint), Load Image node (clipspace painted mask input), Save Image node (ComfyUI filename prefix), VAE Decode node, VAE Encode (for Inpainting) node with grow_mask_by (+1 more)
### Community 73 - "._bg_index_folder"
Cohesion: 0.25
Nodes (5): Event, バックグラウンドスレッドで未キャッシュ画像のprompt_cacheを構築する。 10枚処理するたびに50msスリープしてメインスレッドへの影響を最小化。, PNG/JPEGからメタデータを抽出し、保存済みメタと合わせて返す, PNGのtEXtチャンクからメタデータを抽出, JPEGのEXIF/commentからメタデータを抽出(簡易)
### Community 74 - "aiohttp"
Cohesion: 0.16
Nodes (14): aiohttp, py/routes, _get_api_key(), handle_proxy(), _is_allowed_base_url(), Application, Request, Response (+6 more)
### Community 75 - "import_style_prompt_seed.py"
Cohesion: 0.36
Nodes (8): clean_tags(), import_category(), main(), parse_yaml_leaves(), Path, Style/Prompt ギャラリー用シードデータ投入スクリプト。 comfyui_prompt_gallery / prompt_builder_proto…, comfyui_prompt_gallery の parseYamlForImages と同じ簡易パーサ(PyYAML不要)。 「key:」行の直後が「-…, 1カテゴリ分を投入する。戻り値: (copied, images_without_prompt, leaves_total)
### Community 76 - "Image Edit (FireRed Image Edit 1.1) node"
Cohesion: 0.32
Nodes (8): 3D Pose Editor node, FireRed-Image-Edit-1.1_fp8mixed_comfy.safetensors, FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors (LoRA), Image Edit (FireRed Image Edit 1.1) node, qwen_2.5_vl_7b_fp8_scaled.safetensors (CLIP), qwen_image_vae.safetensors (VAE), Save Image node, 3D Pose Editor to Image Edit (Fire Red) Workflow
### Community 77 - "Version 0.3.9"
Cohesion: 0.18
Nodes (11): AI TOOL Tab, Models Tab, Nodes Tab, Prompt Tab, Version 0.1.2, Version 0.1.7, Version 0.2.3, Version 0.3.14 (+3 more)
### Community 78 - "DWPose Image Feeder Workflow"
Cohesion: 0.52
Nodes (7): DWPose Image Feeder Workflow, Apply ControlNet Node, DWPose Estimator Node (comfyui-controlnet-aux), Image Feeder Node (comfyui-image-feeder), KSampler Node, Load Checkpoint Node (animagineXLRealistic_v6), Load ControlNet Model Node (OpenPoseXL2)
### Community 79 - "wfm.py"
Cohesion: 0.21
Nodes (12): Jinja2, Request, Response, Serve the main SPA page., Serve files from the current workflows directory (dynamic path)., Serve files (index thumbnails) from the Lab plan directory., Main entry point for Workflow Studio plugin., Register all routes with ComfyUI's server. (+4 more)
### Community 80 - "README.index.md"
Cohesion: 0.40
Nodes (6): on_doc_edit.py, DEVLOG.index.md, generate_doc_index.py, pre-commit, graphify, README.index.md
### Community 81 - "WFS_PromptText"
Cohesion: 0.33
Nodes (3): WFS_PromptText – Prompt preset node with positive/negative STRING outputs., Outputs positive and negative prompt strings., WFS_PromptText
### Community 82 - "workflow_service.py"
Cohesion: 0.40
Nodes (4): extract_png_workflow(), Extract ComfyUI workflow JSON from PNG metadata., Extract ComfyUI workflow JSON from PNG byte data. Returns dict or None., Workflow CRUD and metadata management service.
### Community 83 - "SPA"
Cohesion: 0.40
Nodes (5): ComfyUI, SPA, static, templates, web/comfyui
### Community 84 - "prompt-ai-chat.js"
Cohesion: 0.34
Nodes (14): addChatMessage(), applyToGenerateUI(), attachFile(), chatWithAi(), clearChat(), fetchAiModels(), getPromptAiConfig(), initAiChatUI() (+6 more)
### Community 85 - "Tagger Tab requirements.txt"
Cohesion: 0.40
Nodes (5): Tagger Tab requirements.txt, onnxruntime (WD Tagger / SwinV2), piexif (JPEG EXIF tag writing), python-dotenv (Unsloth .env loading), tensorflow (optional, DeepDanbooru)
### Community 86 - "data directory"
Cohesion: 0.50
Nodes (4): data directory, metadata.json, node_metadata.json, settings.json
### Community 87 - "t"
Cohesion: 0.25
Nodes (20): showToast(), _applyDefaultCheckpointIfEnabled(), loadWorkflowIntoEditor(), saveCurrentWorkflow(), t(), _initPlanDropZone(), _loadPlanFromFile(), _loadPlanFromIndexImage() (+12 more)
### Community 88 - "Chinese Language Support"
Cohesion: 1.00
Nodes (3): Chinese Language Support, English Language Support, Japanese Language Support
### Community 89 - "Version 0.3.95"
Cohesion: 0.67
Nodes (3): ImagePrompt Gallery Subtab, Style_Catalog Subtab, Version 0.3.95
### Community 90 - "Windows"
Cohesion: 0.67
Nodes (3): LongPathsEnabled, MAX_PATH, Windows
### Community 98 - "lab_routes.py"
Cohesion: 0.42
Nodes (10): handle_delete(), handle_get_content(), handle_list(), handle_save(), handle_save_index_to_output(), Application, Request, Response (+2 more)
### Community 153 - "prompts_routes.py"
Cohesion: 0.25
Nodes (13): handle_create(), handle_delete(), handle_list(), handle_update(), Application, Request, Response, Prompt presets API routes. (+5 more)
### Community 157 - "SettingsService"
Cohesion: 0.28
Nodes (4): Return CivitAI API key. Env var CIVITAI_API_KEY takes priority over…, Merge updates into existing settings., Manages application settings (data/settings.json)., SettingsService
### Community 174 - "app.js"
Cohesion: 0.22
Nodes (8): applyI18nToHtml(), initModal(), _onHelpSearch(), initI18n(), applyJsonColors(), applyModelTabActiveColor(), applyTextareaFontSize(), getSettings()
### Community 176 - "util.js"
Cohesion: 0.43
Nodes (7): AI_BACKEND_DEFAULT_URLS, _bytesToDataUrl(), _dataUrlToBytes(), embedPngTextChunk(), _joinBytes(), _n2b(), _pngCrc32()
### Community 177 - "civitai_service.py"
Cohesion: 0.33
Nodes (5): _get_ssl_context(), _make_ssl_context(), CivitAI API integration service., Return an SSL context with CA verification. 1. certifi CA bundle (available in…, Download an image from URL and save to save_path. Returns True on success.
### Community 178 - "config.py"
Cohesion: 0.22
Nodes (5): Resolve workflows dir from settings, falling back to default., _resolve_workflows_dir(), Node metadata and node sets management service., Prompt presets management service., Settings management service.
### Community 179 - "createModelItem"
Cohesion: 0.57
Nodes (7): createModelItem(), fetchModelList(), matchesModelSearch(), renderModelAll(), renderModelByType(), renderModelFavorites(), renderModelGroups()
### Community 180 - "_renderBatchPreview"
Cohesion: 0.20
Nodes (18): _buildSimpleGroupList(), _getGroupSelCount(), _getGroupSelCountFrom(), initBatchTab(), _loadBatchCheckpointGroups(), _loadBatchLoraGroups(), _loadPromptGroupsForBatch(), _loadWorkflowGroupsForBatch() (+10 more)
## Knowledge Gaps
- **216 isolated node(s):** `comfyui-workflow-studio`, `LANG_NAMES`, `VLM_PROMPTS`, `IMAGE_GEN_TOOLS`, `skillFiles` (+211 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **94 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `showToast()` connect `t` to `escapeHtml`, `prompt-table.js`, `generate-tab.js`, `_openCellModal`, `gallery-tab.js`, `ImageEditTab`, `ai-tab.js`, `feeder-tab.js`, `settings-tab.js`, `tagger-tab.js`, `comfyui-editor.js`, `nodes-tab.js`, `workflow-tab.js`, `FileExport`, `image-prompt-tab.js`, `image-edit-tab.js`, `style-catalog-tab.js`, `app.js`, `BlurTool`, `lab-tab.js`, `_runLabBatch`, `BgRemove`, `initGenerateTab`, `InpaintI2IActions`, `Sam3Segmentation`, `prompt-ai-chat.js`?**
_High betweenness centrality (0.071) - this node is a cross-community bridge._
- **Why does `t()` connect `t` to `escapeHtml`, `prompt-table.js`, `generate-tab.js`, `_openCellModal`, `gallery-tab.js`, `ai-tab.js`, `feeder-tab.js`, `metadata-tab.js`, `settings-tab.js`, `tagger-tab.js`, `comfyui-editor.js`, `nodes-tab.js`, `workflow-tab.js`, `image-prompt-tab.js`, `style-catalog-tab.js`, `app.js`, `lab-tab.js`, `_runLabBatch`, `initGenerateTab`, `prompt-ai-chat.js`?**
_High betweenness centrality (0.058) - this node is a cross-community bridge._
- **Why does `ImageEditTab` connect `ImageEditTab` to `image-edit-tab.js`, `app.js`?**
_High betweenness centrality (0.028) - this node is a cross-community bridge._
- **What connects `comfyui-workflow-studio`, `LANG_NAMES`, `VLM_PROMPTS` to the rest of the system?**
_216 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `escapeHtml` be split into smaller, more focused modules?**
_Cohesion score 0.09288389513108614 - nodes in this community are weakly interconnected._
- **Should `prompt-table.js` be split into smaller, more focused modules?**
_Cohesion score 0.06366691560866318 - nodes in this community are weakly interconnected._
- **Should `models_routes.py` be split into smaller, more focused modules?**
_Cohesion score 0.0554954954954955 - nodes in this community are weakly interconnected._
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-890
View File
@@ -1,890 +0,0 @@
{
"CLAUDE.md": {
"mtime": 1787279658.5963283,
"seen": 1787380810.3754587,
"ast_hash": "b2c014309df240903aa90a26914c8c19",
"semantic_hash": "b2c014309df240903aa90a26914c8c19"
},
".claude/settings.json": {
"mtime": 1787279595.344592,
"seen": 1787380810.2925637,
"ast_hash": "cd2c4308b55c5d1aa1b647cdec814b79",
"semantic_hash": "cd2c4308b55c5d1aa1b647cdec814b79"
},
"__init__.py": {
"mtime": 1781939762.254596,
"seen": 1787279199.0011544,
"ast_hash": "72d9ef06026dff7be74da81ff98a306a",
"semantic_hash": "72d9ef06026dff7be74da81ff98a306a"
},
"prestartup_script.py": {
"mtime": 1787036267.332648,
"seen": 1787279199.0011563,
"ast_hash": "7b5d482ef21a480d1b38356e8d82970b",
"semantic_hash": "7b5d482ef21a480d1b38356e8d82970b"
},
"py/__init__.py": {
"mtime": 1773797715.629029,
"seen": 1787279199.0011575,
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"py/config.py": {
"mtime": 1786245317.3352194,
"seen": 1787279199.001159,
"ast_hash": "b013c27bb7cd9a79449961539fdcd30f",
"semantic_hash": "b013c27bb7cd9a79449961539fdcd30f"
},
"py/nodes/__init__.py": {
"mtime": 1774423045.6646452,
"seen": 1787279199.00116,
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"py/nodes/gallery_feeder_node.py": {
"mtime": 1781943269.5205867,
"seen": 1787279199.0011609,
"ast_hash": "d37c520b0f76e70e60ec7c72db904bf4",
"semantic_hash": "d37c520b0f76e70e60ec7c72db904bf4"
},
"py/nodes/prompt_text.py": {
"mtime": 1774423045.6669831,
"seen": 1787279199.0011618,
"ast_hash": "43cf16f182b9eda5fade2b52c2439372",
"semantic_hash": "43cf16f182b9eda5fade2b52c2439372"
},
"py/routes/__init__.py": {
"mtime": 1773797716.663036,
"seen": 1787279199.0011628,
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"py/routes/eagle_routes.py": {
"mtime": 1785464911.9023821,
"seen": 1787279199.0011635,
"ast_hash": "edd32039935a892317c55b434c9839ab",
"semantic_hash": "edd32039935a892317c55b434c9839ab"
},
"py/routes/gallery_routes.py": {
"mtime": 1786682156.6899943,
"seen": 1787279199.001165,
"ast_hash": "59e4c5d34ee2329ab9e0a66f841d1676",
"semantic_hash": "59e4c5d34ee2329ab9e0a66f841d1676"
},
"py/routes/gmic_routes.py": {
"mtime": 1782544784.03979,
"seen": 1787279199.0011659,
"ast_hash": "b909c65af6590c0ca058136cb0b9234e",
"semantic_hash": "b909c65af6590c0ca058136cb0b9234e"
},
"py/routes/lab_routes.py": {
"mtime": 1786322410.571655,
"seen": 1787279199.0011666,
"ast_hash": "48e94f71b11f2e494bb98bb7113e2baa",
"semantic_hash": "48e94f71b11f2e494bb98bb7113e2baa"
},
"py/routes/models_routes.py": {
"mtime": 1782289767.1073005,
"seen": 1787279199.0011675,
"ast_hash": "067aa9c89085fa3e077fdfacbb6be2cd",
"semantic_hash": "067aa9c89085fa3e077fdfacbb6be2cd"
},
"py/routes/nodes_routes.py": {
"mtime": 1774255599.0841086,
"seen": 1787279199.0011685,
"ast_hash": "dc5f7b12dce346631ff945c31bf03ac9",
"semantic_hash": "dc5f7b12dce346631ff945c31bf03ac9"
},
"py/routes/ollama_routes.py": {
"mtime": 1784126582.1613884,
"seen": 1787279199.0011694,
"ast_hash": "80552c0676f7bbd4209eb95cf7a03f5b",
"semantic_hash": "80552c0676f7bbd4209eb95cf7a03f5b"
},
"py/routes/prompts_routes.py": {
"mtime": 1774423045.6696556,
"seen": 1787279199.0011702,
"ast_hash": "53b5641f3ad6f65e8ae665601c4739d7",
"semantic_hash": "53b5641f3ad6f65e8ae665601c4739d7"
},
"py/routes/settings_routes.py": {
"mtime": 1786681377.025748,
"seen": 1787279199.001171,
"ast_hash": "bb2f332c04474ee39319c2d0b8107db9",
"semantic_hash": "bb2f332c04474ee39319c2d0b8107db9"
},
"py/routes/skill_routes.py": {
"mtime": 1785634791.033795,
"seen": 1787279199.0011723,
"ast_hash": "3318c506163c720c5412ec0f8ff3a28a",
"semantic_hash": "3318c506163c720c5412ec0f8ff3a28a"
},
"py/routes/tagger_routes.py": {
"mtime": 1781503496.8879485,
"seen": 1787279199.001173,
"ast_hash": "9c0559b594a69354882da0b1643e11f0",
"semantic_hash": "9c0559b594a69354882da0b1643e11f0"
},
"py/routes/unsloth_routes.py": {
"mtime": 1787036726.267709,
"seen": 1787279199.001174,
"ast_hash": "93d4de97937f472f9618a35a0b4211d6",
"semantic_hash": "93d4de97937f472f9618a35a0b4211d6"
},
"py/routes/wildcard_routes.py": {
"mtime": 1777370036.4121041,
"seen": 1787279199.0011756,
"ast_hash": "1d0ea981a44ffab77d62c214951f4536",
"semantic_hash": "1d0ea981a44ffab77d62c214951f4536"
},
"py/routes/workflow_routes.py": {
"mtime": 1773896239.095392,
"seen": 1787279199.0011766,
"ast_hash": "e64f3e7b5d0dfe3543ffbfeb137aacb3",
"semantic_hash": "e64f3e7b5d0dfe3543ffbfeb137aacb3"
},
"py/services/__init__.py": {
"mtime": 1773797717.739297,
"seen": 1787279199.0011773,
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"py/services/civitai_service.py": {
"mtime": 1780233122.7587051,
"seen": 1787279199.001178,
"ast_hash": "3b4a2a118bc56dd27e768a2819b401e7",
"semantic_hash": "3b4a2a118bc56dd27e768a2819b401e7"
},
"py/services/gallery_metadata.py": {
"mtime": 1786170828.9941547,
"seen": 1787279199.0011792,
"ast_hash": "9d02a71d56b7ca2a88cdf8c56fadf912",
"semantic_hash": "9d02a71d56b7ca2a88cdf8c56fadf912"
},
"py/services/gallery_service.py": {
"mtime": 1786638406.9058151,
"seen": 1787279199.0011802,
"ast_hash": "c8b9858f015f7f4a934c4b630e905107",
"semantic_hash": "c8b9858f015f7f4a934c4b630e905107"
},
"py/services/lab_plan_service.py": {
"mtime": 1786322396.0192728,
"seen": 1787279199.001181,
"ast_hash": "5d2c928c077ad7b788f15419b39ef51f",
"semantic_hash": "5d2c928c077ad7b788f15419b39ef51f"
},
"py/services/models_service.py": {
"mtime": 1782393318.5989842,
"seen": 1787279199.0011816,
"ast_hash": "0885cc6852199fa45271192ea0d6713a",
"semantic_hash": "0885cc6852199fa45271192ea0d6713a"
},
"py/services/nodes_service.py": {
"mtime": 1774255599.0862236,
"seen": 1787279199.0011828,
"ast_hash": "8819590ffdc136fb2912d9c5696e26e0",
"semantic_hash": "8819590ffdc136fb2912d9c5696e26e0"
},
"py/services/png_extractor.py": {
"mtime": 1773797967.5810304,
"seen": 1787279199.0011837,
"ast_hash": "1282b6032d78cba2a4bbbfb876126bc1",
"semantic_hash": "1282b6032d78cba2a4bbbfb876126bc1"
},
"py/services/prompts_service.py": {
"mtime": 1774423045.6711988,
"seen": 1787279199.0011845,
"ast_hash": "79a98e220d2b556106e8530054547155",
"semantic_hash": "79a98e220d2b556106e8530054547155"
},
"py/services/settings_service.py": {
"mtime": 1773798498.9270506,
"seen": 1787279199.0011854,
"ast_hash": "dade5ea667a2c6408434ad3061c76db7",
"semantic_hash": "dade5ea667a2c6408434ad3061c76db7"
},
"py/services/skill_service.py": {
"mtime": 1785637306.8654232,
"seen": 1787279199.0011866,
"ast_hash": "5ebe579995c1c674e4219e58b3bd5855",
"semantic_hash": "5ebe579995c1c674e4219e58b3bd5855"
},
"py/services/tagger_db_service.py": {
"mtime": 1781503496.8949444,
"seen": 1787279199.0011873,
"ast_hash": "de0ca007db5159484e8ff3b8a36a3b84",
"semantic_hash": "de0ca007db5159484e8ff3b8a36a3b84"
},
"py/services/tagger_service.py": {
"mtime": 1781503496.8969479,
"seen": 1787279199.0011883,
"ast_hash": "7bfde3ebe93f6fa0062a6aecba3f10d9",
"semantic_hash": "7bfde3ebe93f6fa0062a6aecba3f10d9"
},
"py/services/wildcard_service.py": {
"mtime": 1777380000.0054882,
"seen": 1787279199.0011892,
"ast_hash": "5814ed3cf2e31bf8b4abaa15c697b0cd",
"semantic_hash": "5814ed3cf2e31bf8b4abaa15c697b0cd"
},
"py/services/workflow_analyzer.py": {
"mtime": 1785474766.4736228,
"seen": 1787279199.00119,
"ast_hash": "937d6b02a6235795cb22ceb39e81f719",
"semantic_hash": "937d6b02a6235795cb22ceb39e81f719"
},
"py/services/workflow_service.py": {
"mtime": 1775439760.4805496,
"seen": 1787279199.0011911,
"ast_hash": "a2e47c0b8780ec7ca292310b16733a8c",
"semantic_hash": "a2e47c0b8780ec7ca292310b16733a8c"
},
"py/wfm.py": {
"mtime": 1787036294.0749958,
"seen": 1787279199.0011919,
"ast_hash": "cb1c96536f67edb380d1010b6165d74d",
"semantic_hash": "cb1c96536f67edb380d1010b6165d74d"
},
"pyproject.toml": {
"mtime": 1787378263.2900941,
"seen": 1787380810.3265483,
"ast_hash": "f514fae02a3b48726f5073e932cfa1b3",
"semantic_hash": "f514fae02a3b48726f5073e932cfa1b3"
},
"static/js/ai-tab.js": {
"mtime": 1787293482.4888227,
"seen": 1787380810.3265514,
"ast_hash": "960b474155386a36000e6961b0b2e0c3",
"semantic_hash": "960b474155386a36000e6961b0b2e0c3"
},
"static/js/app.js": {
"mtime": 1787377516.2436306,
"seen": 1787380810.3265538,
"ast_hash": "7df994a3e8f6c34d27a1ccd88b207a5c",
"semantic_hash": "7df994a3e8f6c34d27a1ccd88b207a5c"
},
"static/js/comfyui-client.js": {
"mtime": 1786676208.2330563,
"seen": 1787279199.0011961,
"ast_hash": "bb546a6ae7bd35d1c16fee311681be73",
"semantic_hash": "bb546a6ae7bd35d1c16fee311681be73"
},
"static/js/comfyui-editor.js": {
"mtime": 1787293462.7015421,
"seen": 1787380810.3274822,
"ast_hash": "fe27626a41d18f02bdea2b89aaaf740b",
"semantic_hash": "fe27626a41d18f02bdea2b89aaaf740b"
},
"static/js/comfyui-workflow.js": {
"mtime": 1787293410.2500806,
"seen": 1787380810.3274848,
"ast_hash": "255cfe0c2172639ed698a5e9397d7144",
"semantic_hash": "255cfe0c2172639ed698a5e9397d7144"
},
"static/js/feeder-tab.js": {
"mtime": 1781939989.0796032,
"seen": 1787279199.0011988,
"ast_hash": "dcd5977c5fe691b87e2379cd0e018a12",
"semantic_hash": "dcd5977c5fe691b87e2379cd0e018a12"
},
"static/js/gallery-tab.js": {
"mtime": 1787368748.1550386,
"seen": 1787380810.328425,
"ast_hash": "0144bbe48e21eccc498714404e29e29e",
"semantic_hash": "0144bbe48e21eccc498714404e29e29e"
},
"static/js/generate-tab.js": {
"mtime": 1787401222.9019487,
"seen": 1787401432.6967273,
"ast_hash": "dbfc96b6400bd780990725e390f275c9",
"semantic_hash": ""
},
"static/js/i18n.js": {
"mtime": 1787377822.448605,
"seen": 1787380810.32843,
"ast_hash": "d925b681cc485326659fbc24f5a87dda",
"semantic_hash": "d925b681cc485326659fbc24f5a87dda"
},
"static/js/image-edit-tab.js": {
"mtime": 1787199901.562169,
"seen": 1787279199.001202,
"ast_hash": "be7b15d85bb185e2fd3492f4d5657f21",
"semantic_hash": "be7b15d85bb185e2fd3492f4d5657f21"
},
"static/js/image-edit/BgRemove.js": {
"mtime": 1787126632.8601232,
"seen": 1787279199.0012033,
"ast_hash": "add6a58068bdadc0c7641202f36ddb27",
"semantic_hash": "add6a58068bdadc0c7641202f36ddb27"
},
"static/js/image-edit/BlurTool.js": {
"mtime": 1787126263.0542421,
"seen": 1787279199.0012043,
"ast_hash": "1e52bc3757110a271ec17d923784a91b",
"semantic_hash": "1e52bc3757110a271ec17d923784a91b"
},
"static/js/image-edit/DrawTool.js": {
"mtime": 1782717315.6436062,
"seen": 1787279199.001205,
"ast_hash": "a97a6a97ae28c804a7c5fa50448a2c87",
"semantic_hash": "a97a6a97ae28c804a7c5fa50448a2c87"
},
"static/js/image-edit/FileExport.js": {
"mtime": 1787192008.741717,
"seen": 1787279199.0012057,
"ast_hash": "cf645b775e537ad276c9e070c7849349",
"semantic_hash": "cf645b775e537ad276c9e070c7849349"
},
"static/js/image-edit/GmicIntegration.js": {
"mtime": 1787125777.3889306,
"seen": 1787279199.0012069,
"ast_hash": "023d9d19e57d8fa6ebedc3c489ecf7ca",
"semantic_hash": "023d9d19e57d8fa6ebedc3c489ecf7ca"
},
"static/js/image-edit/InpaintI2IActions.js": {
"mtime": 1787127273.4351766,
"seen": 1787279199.0012076,
"ast_hash": "9ea57922985b63d4ec8eb44219a8341b",
"semantic_hash": "9ea57922985b63d4ec8eb44219a8341b"
},
"static/js/image-edit/LayerManager.js": {
"mtime": 1782717943.772222,
"seen": 1787279199.0012085,
"ast_hash": "596cd1906208fe72e1c2953d4d7f7135",
"semantic_hash": "596cd1906208fe72e1c2953d4d7f7135"
},
"static/js/image-edit/MaskEditorOneBridge.js": {
"mtime": 1787199887.897264,
"seen": 1787279199.0012095,
"ast_hash": "db2ef8c024c7e72baa0dfdb79b8abd4d",
"semantic_hash": "db2ef8c024c7e72baa0dfdb79b8abd4d"
},
"static/js/image-edit/MaskEditorOneTools.js": {
"mtime": 1782719665.7021136,
"seen": 1787279199.0012105,
"ast_hash": "b6b16c43ce528f7c9921eab0f4eda335",
"semantic_hash": "b6b16c43ce528f7c9921eab0f4eda335"
},
"static/js/image-edit/MaskTool.js": {
"mtime": 1782699721.4711535,
"seen": 1787279199.0012114,
"ast_hash": "116469f33829c5b2e6270f4b278b2b9c",
"semantic_hash": "116469f33829c5b2e6270f4b278b2b9c"
},
"static/js/image-edit/Sam3Segmentation.js": {
"mtime": 1787126893.774897,
"seen": 1787279199.0012121,
"ast_hash": "98c3a330ae7ea3369c00168c54c66d53",
"semantic_hash": "98c3a330ae7ea3369c00168c54c66d53"
},
"static/js/image-edit/SelectTool.js": {
"mtime": 1782368193.2572634,
"seen": 1787279199.0012128,
"ast_hash": "cb8db6bd5ee742b15f3beee4a5173377",
"semantic_hash": "cb8db6bd5ee742b15f3beee4a5173377"
},
"static/js/image-edit/ShapeTool.js": {
"mtime": 1782375200.0253434,
"seen": 1787279199.001214,
"ast_hash": "36d00bf6b80ebea13314d2853528c44f",
"semantic_hash": "36d00bf6b80ebea13314d2853528c44f"
},
"static/js/image-edit/TextTool.js": {
"mtime": 1782368193.2592669,
"seen": 1787279199.0012147,
"ast_hash": "88f9e60251c2f78957e42c33ed5196f8",
"semantic_hash": "88f9e60251c2f78957e42c33ed5196f8"
},
"static/js/image-prompt-tab.js": {
"mtime": 1786638406.9362252,
"seen": 1787279199.0012157,
"ast_hash": "cf39285a30e3f8b79f6f51dfe13f86b5",
"semantic_hash": "cf39285a30e3f8b79f6f51dfe13f86b5"
},
"static/js/json-highlight.js": {
"mtime": 1773834925.1977189,
"seen": 1787279199.0012164,
"ast_hash": "e83e7a7a507d160b9cd503235a79135c",
"semantic_hash": "e83e7a7a507d160b9cd503235a79135c"
},
"static/js/lab-tab.js": {
"mtime": 1787310963.3056493,
"seen": 1787380810.343902,
"ast_hash": "11a5776c4163e2a97ef490e85de7db63",
"semantic_hash": "11a5776c4163e2a97ef490e85de7db63"
},
"static/js/metadata-tab.js": {
"mtime": 1787377525.9536967,
"seen": 1787380810.3439052,
"ast_hash": "5ac004dd3708aa4dc8890256232fd99e",
"semantic_hash": "5ac004dd3708aa4dc8890256232fd99e"
},
"static/js/models-tab.js": {
"mtime": 1787145102.2314196,
"seen": 1787279199.001219,
"ast_hash": "dcee8a68713cd300d0862b1c376d442f",
"semantic_hash": "dcee8a68713cd300d0862b1c376d442f"
},
"static/js/models/badges.js": {
"mtime": 1787143926.3653927,
"seen": 1787279199.00122,
"ast_hash": "0c1901a90a12f1b23df416d9eef11e1d",
"semantic_hash": "0c1901a90a12f1b23df416d9eef11e1d"
},
"static/js/models/detail-panel.js": {
"mtime": 1787144898.6748104,
"seen": 1787279199.0012212,
"ast_hash": "9907a4af0524b35d80e3b9fc7925324e",
"semantic_hash": "9907a4af0524b35d80e3b9fc7925324e"
},
"static/js/models/filters.js": {
"mtime": 1787144201.7602522,
"seen": 1787279199.001222,
"ast_hash": "afd754f68d01f64c51995929b3841e56",
"semantic_hash": "afd754f68d01f64c51995929b3841e56"
},
"static/js/models/grid-view.js": {
"mtime": 1787144745.5522888,
"seen": 1787279199.0012226,
"ast_hash": "492e91fff2b8743a5243f85a736eb689",
"semantic_hash": "492e91fff2b8743a5243f85a736eb689"
},
"static/js/models/helpers.js": {
"mtime": 1787144193.741004,
"seen": 1787279199.0012236,
"ast_hash": "1c7b796f2507f55403fa5d11f304580a",
"semantic_hash": "1c7b796f2507f55403fa5d11f304580a"
},
"static/js/models/selection-bulk.js": {
"mtime": 1787144787.0092065,
"seen": 1787279199.0012245,
"ast_hash": "d445ff4dc4e9810afea73ff53c76b05b",
"semantic_hash": "d445ff4dc4e9810afea73ff53c76b05b"
},
"static/js/models/state.js": {
"mtime": 1787143674.705312,
"seen": 1787279199.0012252,
"ast_hash": "172ced5fdc4c96095f2760d7ef12d77d",
"semantic_hash": "172ced5fdc4c96095f2760d7ef12d77d"
},
"static/js/nodes-tab.js": {
"mtime": 1782619197.3828287,
"seen": 1787279199.0012262,
"ast_hash": "3467d4682a8c20a2b209b5db224ce6e2",
"semantic_hash": "3467d4682a8c20a2b209b5db224ce6e2"
},
"static/js/prompt-tab.js": {
"mtime": 1787399045.1690862,
"seen": 1787399276.0028682,
"ast_hash": "184961647ceccdf98c8c031c4da96889",
"semantic_hash": ""
},
"static/js/settings-tab.js": {
"mtime": 1787227602.6323576,
"seen": 1787279199.001228,
"ast_hash": "85b55cac9901afc9e94e32f0aa220d2b",
"semantic_hash": "85b55cac9901afc9e94e32f0aa220d2b"
},
"static/js/style-catalog-tab.js": {
"mtime": 1786678976.6511285,
"seen": 1787279199.001229,
"ast_hash": "a2956544bf800ac34f8084b8acd0714b",
"semantic_hash": "a2956544bf800ac34f8084b8acd0714b"
},
"static/js/tagger-tab.js": {
"mtime": 1781503496.933307,
"seen": 1787279199.0012298,
"ast_hash": "5e1c858a13e95148eeac1d5b5be5e4f3",
"semantic_hash": "5e1c858a13e95148eeac1d5b5be5e4f3"
},
"static/js/util.js": {
"mtime": 1787036300.7369778,
"seen": 1787279199.001231,
"ast_hash": "29774ed440f2b60a708c70db1b3ae208",
"semantic_hash": "29774ed440f2b60a708c70db1b3ae208"
},
"static/js/workflow-tab.js": {
"mtime": 1785474064.011484,
"seen": 1787279199.001232,
"ast_hash": "65caea21e8e02d81f2e85c75c7231dca",
"semantic_hash": "65caea21e8e02d81f2e85c75c7231dca"
},
"tools/import_style_prompt_seed.py": {
"mtime": 1786616195.405381,
"seen": 1787279199.0012326,
"ast_hash": "02997888c17a6061b7910cddfdd4d89a",
"semantic_hash": "02997888c17a6061b7910cddfdd4d89a"
},
"web/comfyui/gallery_feeder_extension.js": {
"mtime": 1781943835.232655,
"seen": 1787279199.0012345,
"ast_hash": "0048b031e64a433e29a5f7112e82012d",
"semantic_hash": "0048b031e64a433e29a5f7112e82012d"
},
"web/comfyui/node_sets_menu.js": {
"mtime": 1787199930.9575465,
"seen": 1787279199.0012355,
"ast_hash": "a9d7ad5d194ac2f4b125978a7b048042",
"semantic_hash": "a9d7ad5d194ac2f4b125978a7b048042"
},
"web/comfyui/top_menu_extension.js": {
"mtime": 1782696309.9055538,
"seen": 1787279199.0012362,
"ast_hash": "2c74498722b46d42bcc15f61ec35dfc4",
"semantic_hash": "2c74498722b46d42bcc15f61ec35dfc4"
},
"workflows/ws_2d_pose_editor_to_Image_Edit( FireRed ).json": {
"mtime": 1779256606.5720088,
"seen": 1787279199.0012372,
"ast_hash": "52801ad2b42eca5e3a5f8fc51332d2c3",
"semantic_hash": "52801ad2b42eca5e3a5f8fc51332d2c3"
},
"workflows/ws_3d_pose_editor_to_Image_Edit( Fire Red).json": {
"mtime": 1779257129.3572881,
"seen": 1787279199.001238,
"ast_hash": "8260ca467693c21c497f98113abf37f9",
"semantic_hash": "8260ca467693c21c497f98113abf37f9"
},
"workflows/ws_3d_pose_editor_to_Image_Edit( Flux2 Klein ).json": {
"mtime": 1779257864.2496417,
"seen": 1787279199.001239,
"ast_hash": "7eba746ab9fdca0aefabdc7c7c7bd13e",
"semantic_hash": "7eba746ab9fdca0aefabdc7c7c7bd13e"
},
"workflows/ws_dwpose_image_feeder.json": {
"mtime": 1779255324.1402953,
"seen": 1787279199.0012398,
"ast_hash": "282ce14b11b8b88ddc91e1d318756c0d",
"semantic_hash": "282ce14b11b8b88ddc91e1d318756c0d"
},
"workflows/ws_inpaint_basic.json": {
"mtime": 1784801838.7836998,
"seen": 1787279199.0012407,
"ast_hash": "43e83496dec4856ab9d97c8b950148ec",
"semantic_hash": "43e83496dec4856ab9d97c8b950148ec"
},
"workflows/ws_inpaint_mask_editor_one.json": {
"mtime": 1784801946.0336668,
"seen": 1787279199.001242,
"ast_hash": "ac0a865c1dacd2b30c489f08f8b4df4d",
"semantic_hash": "ac0a865c1dacd2b30c489f08f8b4df4d"
},
"workflows/ws_sd15_t2i_basic.json": {
"mtime": 1779253098.1947012,
"seen": 1787279199.0012426,
"ast_hash": "cb1b5f84ac1b37af8715bf2ab0b38a51",
"semantic_hash": "cb1b5f84ac1b37af8715bf2ab0b38a51"
},
"workflows/ws_sdxl_2d_pose_editor_i2i.json": {
"mtime": 1779256158.4671156,
"seen": 1787279199.0012436,
"ast_hash": "cfe237775d6fb844ac726639e3ae928b",
"semantic_hash": "cfe237775d6fb844ac726639e3ae928b"
},
"workflows/ws_sdxl_3d pose editor_dwpose.json": {
"mtime": 1779255768.6302803,
"seen": 1787279199.0012448,
"ast_hash": "0e1955da91149ba6280d39db7a54db6f",
"semantic_hash": "0e1955da91149ba6280d39db7a54db6f"
},
"workflows/ws_sdxl_dwpose.json": {
"mtime": 1779255037.8698418,
"seen": 1787279199.0012455,
"ast_hash": "d670222d71df92e262ce8cb7299fd531",
"semantic_hash": "d670222d71df92e262ce8cb7299fd531"
},
"workflows/ws_sdxl_from_metadata_t2i.json": {
"mtime": 1779254360.9712114,
"seen": 1787279199.0012462,
"ast_hash": "463fca53dd0a4030555ff639661f2772",
"semantic_hash": "463fca53dd0a4030555ff639661f2772"
},
"workflows/ws_sdxl_i2i_facedetailer.json": {
"mtime": 1779253668.0067616,
"seen": 1787279199.0012474,
"ast_hash": "e2c677ee6670e37b466829c43ff9cd96",
"semantic_hash": "e2c677ee6670e37b466829c43ff9cd96"
},
"workflows/ws_sdxl_image_feeder_i2i.json": {
"mtime": 1779254714.0149894,
"seen": 1787279199.0012484,
"ast_hash": "8f14f2e0d1ebed40f95a8b728a5ecc79",
"semantic_hash": "8f14f2e0d1ebed40f95a8b728a5ecc79"
},
"workflows/ws_sdxl_t2i_basic.json": {
"mtime": 1779253180.3940487,
"seen": 1787279199.0012493,
"ast_hash": "beacbe857606b44d13aa2fc2f4872b76",
"semantic_hash": "beacbe857606b44d13aa2fc2f4872b76"
},
"workflows/ws_sdxl_t2i_facedetaller.json": {
"mtime": 1779253367.7975926,
"seen": 1787279199.00125,
"ast_hash": "0706c31d93f980b581e0e6eb376c0bcc",
"semantic_hash": "0706c31d93f980b581e0e6eb376c0bcc"
},
"DEVLOG.index.md": {
"mtime": 1787378424.0888915,
"seen": 1787399276.0029197,
"ast_hash": "f5e13974b9f8770e6f49bf74fc2c421e",
"semantic_hash": ""
},
"README.index.md": {
"mtime": 1787378423.9177885,
"seen": 1787399276.0029216,
"ast_hash": "4eaa4581c6f969c663176b216fde4596",
"semantic_hash": ""
},
"requirements.txt": {
"mtime": 1787036261.1817627,
"seen": 1787279199.0012598,
"ast_hash": "5942e1febf9611227a9145b3f9a91c6e",
"semantic_hash": "5942e1febf9611227a9145b3f9a91c6e"
},
"assets/banner.svg": {
"mtime": 1776081443.419675,
"seen": 1787279199.0012608,
"ast_hash": "059ec317eaba9e353d220d883477927a",
"semantic_hash": "059ec317eaba9e353d220d883477927a"
},
"assets/icon.svg": {
"mtime": 1776081443.4219253,
"seen": 1787279199.0012615,
"ast_hash": "9689aa0178236c19d5240250e08a303e",
"semantic_hash": "9689aa0178236c19d5240250e08a303e"
},
"docs/10_GenUI_Batch.png": {
"mtime": 1781057457.561953,
"seen": 1787279199.0012627,
"ast_hash": "351c1647aa5becfb4a2dbfa7da043b24",
"semantic_hash": "351c1647aa5becfb4a2dbfa7da043b24"
},
"docs/11_multiple_select_menu.png": {
"mtime": 1781057528.0140572,
"seen": 1787279199.0012636,
"ast_hash": "b3082324b8200a4eac8868f94ec4e6c7",
"semantic_hash": "b3082324b8200a4eac8868f94ec4e6c7"
},
"docs/12_Tagger.png": {
"mtime": 1782551031.7107866,
"seen": 1787279199.0012643,
"ast_hash": "42806369c4c08140c87daea348b6fc0f",
"semantic_hash": "42806369c4c08140c87daea348b6fc0f"
},
"docs/13_ImageEdit.png": {
"mtime": 1782550946.6263509,
"seen": 1787279199.0012655,
"ast_hash": "25a313806ba3c2396c9361b42599b063",
"semantic_hash": "25a313806ba3c2396c9361b42599b063"
},
"docs/1_workflowtab.png": {
"mtime": 1779078100.5980048,
"seen": 1787279199.0012662,
"ast_hash": "5aa53a79b8bd9e7b11a76f1ff89c9297",
"semantic_hash": "5aa53a79b8bd9e7b11a76f1ff89c9297"
},
"docs/2_modelstab.png": {
"mtime": 1779077971.8302422,
"seen": 1787279199.0012672,
"ast_hash": "2c89e877f01e8de3151e655f305df6aa",
"semantic_hash": "2c89e877f01e8de3151e655f305df6aa"
},
"docs/3_PromptInputAssistance.png": {
"mtime": 1779077804.0445502,
"seen": 1787279199.001268,
"ast_hash": "6634ec20d561648ce223807d2fb010cc",
"semantic_hash": "6634ec20d561648ce223807d2fb010cc"
},
"docs/4_GenUI_feeder.png": {
"mtime": 1779077729.1484556,
"seen": 1787279199.001269,
"ast_hash": "e6b3607b7c53a2f601aff487c278a3fb",
"semantic_hash": "e6b3607b7c53a2f601aff487c278a3fb"
},
"docs/5_topbar.png": {
"mtime": 1779076905.6012611,
"seen": 1787279199.0012698,
"ast_hash": "bafc74e7452f83ae9c585aa11f76f208",
"semantic_hash": "bafc74e7452f83ae9c585aa11f76f208"
},
"docs/6_ws_library.png": {
"mtime": 1781057870.2308166,
"seen": 1787279199.0012705,
"ast_hash": "6645a6c3d9ea09d75d34255c76cfc68b",
"semantic_hash": "6645a6c3d9ea09d75d34255c76cfc68b"
},
"docs/7_library_Infomation.png": {
"mtime": 1779076410.2629254,
"seen": 1787279199.0012715,
"ast_hash": "acb54f299c244fd41457f8fe4ccfc1d6",
"semantic_hash": "acb54f299c244fd41457f8fe4ccfc1d6"
},
"docs/8_Customize.png": {
"mtime": 1779077388.8494852,
"seen": 1787279199.0012722,
"ast_hash": "c9f979d40753ce41c015b2d594007658",
"semantic_hash": "c9f979d40753ce41c015b2d594007658"
},
"docs/9_GenUI_LoraStack.png": {
"mtime": 1781057821.3736062,
"seen": 1787279199.0012734,
"ast_hash": "e2b08e5f2059554d305563c63286bd74",
"semantic_hash": "e2b08e5f2059554d305563c63286bd74"
},
"docs/thumb.png": {
"mtime": 1778457744.7955222,
"seen": 1787279199.0012743,
"ast_hash": "92a0dcf2c3457777f2945f6df2dff017",
"semantic_hash": "92a0dcf2c3457777f2945f6df2dff017"
},
"static/favicon.svg": {
"mtime": 1773877378.0368707,
"seen": 1787279199.0012753,
"ast_hash": "0cd547c25c922d632890be287d6084ff",
"semantic_hash": "0cd547c25c922d632890be287d6084ff"
},
"workflows/ws_2d_pose_editor_to_Image_Edit( FireRed ).png": {
"mtime": 1779256606.562932,
"seen": 1787279199.0012763,
"ast_hash": "79b9d1e5e1303b523349b539d3dc1af3",
"semantic_hash": "79b9d1e5e1303b523349b539d3dc1af3"
},
"workflows/ws_3d_pose_editor_to_Image_Edit( Fire Red).png": {
"mtime": 1779257129.3453796,
"seen": 1787279199.001277,
"ast_hash": "02c572ab1c659d71dfd56e6b2b0334a5",
"semantic_hash": "02c572ab1c659d71dfd56e6b2b0334a5"
},
"workflows/ws_3d_pose_editor_to_Image_Edit( Flux2 Klein ).png": {
"mtime": 1779257864.2375357,
"seen": 1787279199.001278,
"ast_hash": "bb03ed86c7d4460a3568a4a8f90e5f40",
"semantic_hash": "bb03ed86c7d4460a3568a4a8f90e5f40"
},
"workflows/ws_dwpose_image_feeder.png": {
"mtime": 1779255324.1356418,
"seen": 1787279199.0012789,
"ast_hash": "e5db6889f59078e4ead6b89b8c07e91e",
"semantic_hash": "e5db6889f59078e4ead6b89b8c07e91e"
},
"workflows/ws_inpaint_basic.png": {
"mtime": 1784801838.7777002,
"seen": 1787279199.00128,
"ast_hash": "79321edb85aed7c0a8c777accc005486",
"semantic_hash": "79321edb85aed7c0a8c777accc005486"
},
"workflows/ws_inpaint_mask_editor_one.png": {
"mtime": 1784801946.027666,
"seen": 1787279199.001281,
"ast_hash": "bdd62facd6a8176c50e92264994dbbc2",
"semantic_hash": "bdd62facd6a8176c50e92264994dbbc2"
},
"workflows/ws_sd15_t2i_basic.png": {
"mtime": 1779253098.1897035,
"seen": 1787279199.0012817,
"ast_hash": "9582bb698439fff1a748315145538777",
"semantic_hash": "9582bb698439fff1a748315145538777"
},
"workflows/ws_sdxl_2d_pose_editor_i2i.png": {
"mtime": 1779256158.4621174,
"seen": 1787279199.001283,
"ast_hash": "bb7b9e26eae992abe62cd50299da9cb2",
"semantic_hash": "bb7b9e26eae992abe62cd50299da9cb2"
},
"workflows/ws_sdxl_3d pose editor_dwpose.png": {
"mtime": 1779255768.6191878,
"seen": 1787279199.0012836,
"ast_hash": "ac0fc8184ff7e42389fd0d562daa88c4",
"semantic_hash": "ac0fc8184ff7e42389fd0d562daa88c4"
},
"workflows/ws_sdxl_dwpose.png": {
"mtime": 1779255037.8606594,
"seen": 1787279199.0012846,
"ast_hash": "564f58d85a0b960154bd0a30ea111599",
"semantic_hash": "564f58d85a0b960154bd0a30ea111599"
},
"workflows/ws_sdxl_from_metadata_t2i.png": {
"mtime": 1779254360.9671092,
"seen": 1787279199.0012853,
"ast_hash": "98dfdb3ba374b0952c06995843f45171",
"semantic_hash": "98dfdb3ba374b0952c06995843f45171"
},
"workflows/ws_sdxl_i2i_facedetailer.png": {
"mtime": 1779253668.0007572,
"seen": 1787279199.0012865,
"ast_hash": "7285bb2a06aaf8dc7c5b37b84261be61",
"semantic_hash": "7285bb2a06aaf8dc7c5b37b84261be61"
},
"workflows/ws_sdxl_image_feeder_i2i.png": {
"mtime": 1779254714.0099926,
"seen": 1787279199.0012872,
"ast_hash": "13d4e7f87a87fd4c9a78427b38800cc6",
"semantic_hash": "13d4e7f87a87fd4c9a78427b38800cc6"
},
"workflows/ws_sdxl_t2i_basic.png": {
"mtime": 1779253180.3890488,
"seen": 1787279199.0012882,
"ast_hash": "9d8dda899602b8d74c6b23350e58a0d0",
"semantic_hash": "9d8dda899602b8d74c6b23350e58a0d0"
},
"workflows/ws_sdxl_t2i_facedetaller.png": {
"mtime": 1779253367.7925913,
"seen": 1787279199.0012891,
"ast_hash": "51cae742b39316023cf5332f1d794854",
"semantic_hash": "51cae742b39316023cf5332f1d794854"
},
".claude/hooks/on_doc_edit.py": {
"mtime": 1787279578.9362085,
"seen": 1787380810.2925577,
"ast_hash": "1a39a0e2ffaac441e8decccad0e1c42c",
"semantic_hash": "1a39a0e2ffaac441e8decccad0e1c42c"
},
"tools/generate_doc_index.py": {
"mtime": 1787279532.9148839,
"seen": 1787380810.3572273,
"ast_hash": "d0cdaf32b36fdef641a9396fd4ec403f",
"semantic_hash": "d0cdaf32b36fdef641a9396fd4ec403f"
},
".github/workflows/publish.yml": {
"mtime": 1778460176.5699222,
"seen": 1787380810.3754551,
"ast_hash": "a83dab23e8d09fdc8626689e96dfeb0e",
"semantic_hash": "a83dab23e8d09fdc8626689e96dfeb0e"
},
"templates/index.html": {
"mtime": 1787377487.6004047,
"seen": 1787380810.3754609,
"ast_hash": "639c4691eb3439d1707cd972bbed1402",
"semantic_hash": "639c4691eb3439d1707cd972bbed1402"
},
"static/js/prompt-table.js": {
"mtime": 1787400288.156035,
"seen": 1787400419.0870566,
"ast_hash": "bb722a8d63b1a7578f136bf99798f679",
"semantic_hash": ""
},
"static/js/prompt-ai-chat.js": {
"mtime": 1787398970.5236483,
"seen": 1787399276.0028632,
"ast_hash": "cdb87c58d22faa43d64b4f71a8d8e0f7",
"semantic_hash": ""
},
"static/js/prompt-presets.js": {
"mtime": 1787398926.1807213,
"seen": 1787399276.0028653,
"ast_hash": "7fd37f214f881138d21559c267241c5d",
"semantic_hash": ""
},
"static/js/prompt-styles.js": {
"mtime": 1787399033.0976152,
"seen": 1787399276.0028667,
"ast_hash": "800e1d042f6800d057b60801461d80aa",
"semantic_hash": ""
},
"static/js/prompt-wildcards.js": {
"mtime": 1787399711.3928645,
"seen": 1787399943.1509204,
"ast_hash": "0e1312911fa197ab5a1858fa449dfc69",
"semantic_hash": ""
}
}
+1
View File
@@ -72,6 +72,7 @@ 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"
+2 -2
View File
@@ -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"
+77 -1
View File
@@ -6,19 +6,22 @@ import logging
from aiohttp import web
from ..config import VIDEO_PLAN_DIR
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_get("/api/wfm/video/edit/probe", handle_probe_video)
app.router.add_get("/api/wfm/video/plans", handle_list_plans)
app.router.add_get("/api/wfm/video/plans/content", handle_get_plan_content)
@@ -26,6 +29,11 @@ def setup_routes(app: web.Application):
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:
@@ -69,6 +77,24 @@ async def handle_to_gif(request: web.Request) -> web.Response:
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_list_plans(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_plan_service.list_plans)
@@ -120,6 +146,56 @@ async def handle_delete_plan(request: web.Request) -> web.Response:
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()
+20
View File
@@ -12,6 +12,7 @@ import threading
import urllib.request
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse
import numpy as np
from PIL import Image, ImageOps, PngImagePlugin
@@ -20,6 +21,20 @@ logger = logging.getLogger(__name__)
_model_lock = threading.Lock()
# Unsloth requires an Authorization: Bearer <UNSLOTH_API_KEY> header even for
# local access. api_url is client-supplied per request, so without a host
# check a client could point it at an arbitrary server and exfiltrate the key
# (SSRF). Mirrors unsloth_routes.py's _is_allowed_base_url/_ALLOWED_HOSTS.
_UNSLOTH_ALLOWED_HOSTS = {"localhost", "127.0.0.1", "::1"}
def _is_allowed_unsloth_url(api_url: str) -> bool:
try:
parsed = urlparse(api_url)
return parsed.scheme in ("http", "https") and parsed.hostname in _UNSLOTH_ALLOWED_HOSTS
except Exception:
return False
class TaggerService:
def __init__(self, models_dir: Path, settings_file: Path):
@@ -263,6 +278,9 @@ class TaggerService:
headers = {}
if backend == "unsloth":
if not _is_allowed_unsloth_url(api_url):
logger.error("vlm_models: Unsloth backend URL must point to localhost/127.0.0.1/::1")
return []
key = self._unsloth_api_key()
if not key:
logger.error("vlm_models: UNSLOTH_API_KEY is not set")
@@ -303,6 +321,8 @@ class TaggerService:
else:
headers = {"Content-Type": "application/json"}
if backend == "unsloth":
if not _is_allowed_unsloth_url(api_url):
return {"error": "Unsloth backend URL must point to localhost/127.0.0.1/::1"}
key = self._unsloth_api_key()
if not key:
return {"error": "UNSLOTH_API_KEY is not set. Copy .env.example to .env in the "
+98
View File
@@ -0,0 +1,98 @@
"""Video tab Edit subtab project file management (JSON timeline persistence).
Same CRUD shape as VideoPlanService (list/get/save/delete with a safe-path
helper), kept as its own class rather than sharing a base because the two
JSON schemas are unrelated (batch keyframe blocks vs. an ordered clip
timeline) — see VideoPlanService's own docstring for the same reasoning.
No index-image thumbnail support (unlike VideoPlanService) since the Edit
project list is plain text/JSON only for now.
"""
import json
import logging
from datetime import datetime, timezone
from pathlib import Path
logger = logging.getLogger(__name__)
class VideoEditProjectService:
def __init__(self, project_dir: Path):
self.project_dir = project_dir
self.project_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.project_dir / name).resolve()
project_dir_resolved = self.project_dir.resolve()
if resolved != project_dir_resolved and not str(resolved).startswith(str(project_dir_resolved) + "\\") and not str(resolved).startswith(str(project_dir_resolved) + "/"):
return None
return resolved
def _now_iso(self) -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
# ------------------------------------------------------------------
# CRUD
# ------------------------------------------------------------------
def list_projects(self) -> list[dict]:
if not self.project_dir.is_dir():
return []
result = []
for path in sorted(self.project_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 video edit project %s: %s", path.name, e)
continue
result.append({
"filename": path.name,
"name": data.get("name", path.stem),
"clip_count": len(data.get("clips", [])),
"updated_at": data.get("updated_at", ""),
})
result.sort(key=lambda p: p.get("updated_at", ""), reverse=True)
return result
def get_project(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"Project not found: {filename}")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_project(self, filename: str, data: dict) -> 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)
return {"filename": path.name, "updated_at": data["updated_at"]}
def delete_project(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()
+32
View File
@@ -57,6 +57,38 @@ class VideoService:
save_path.write_bytes(image_bytes)
return {"filename": save_name, "subfolder": subfolder}
def probe_video(self, filename: str, subfolder: str, type_: str) -> dict:
"""Reads duration/dimensions/fps/audio presence via PyAV only (no ffmpeg
subprocess) — used by the Video Edit tab right after a clip is added, so
the timeline UI can show its length and the export-time resolution-match
check has real numbers to compare against."""
import av # type: ignore
src_path = self._resolve_media_path(filename, subfolder, type_)
container = av.open(str(src_path))
try:
stream = container.streams.video[0]
fps = float(stream.average_rate) if stream.average_rate else 0.0
if stream.duration is not None and stream.time_base is not None:
duration = float(stream.duration * stream.time_base)
elif container.duration is not None:
duration = float(container.duration) / 1_000_000
else:
duration = 0.0
width = stream.codec_context.width
height = stream.codec_context.height
has_audio = len(container.streams.audio) > 0
finally:
container.close()
return {
"duration": duration,
"width": width,
"height": height,
"fps": fps,
"has_audio": has_audio,
}
def convert_to_gif(
self,
filename: str,
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-workflow-studio"
description = "A comprehensive workflow, asset management, and generation UI plugin for ComfyUI. Manage workflows, models, images, and prompts; batch generation and Image Feeder in the GenerateUI tab; library side panel with canvas drag & drop and AI tools."
version = "0.6.2"
version = "0.7.2"
license = { file = "LICENSE" }
requires-python = ">= 3.10"
dependencies = ["jinja2"]
+296 -20
View File
@@ -21,7 +21,7 @@
/* ── 左ペイン: フォーム ── */
.wfm-video-form-panel {
width: 380px;
width: 253px;
flex: none;
display: flex;
flex-direction: column;
@@ -275,15 +275,85 @@
flex-direction: column;
}
/* ── Asset: 素材グリッド・詳細パネル ── */
/* ── Asset: 中央下部サブタブ。左=一覧(サムネイル/テーブル切替) / 右=詳細 ── */
.wfm-video-asset-content { padding: 0 16px 16px; }
.wfm-video-asset-layout {
display: flex;
gap: 16px;
align-items: flex-start;
}
.wfm-video-asset-list-col {
flex: 2;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.wfm-video-asset-detail-col {
flex: 1;
min-width: 220px;
max-width: 320px;
border-left: 1px solid var(--wfm-border);
padding-left: 16px;
}
.wfm-video-asset-toolbar {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.wfm-video-asset-view-btn.active {
background: var(--wfm-primary);
color: #fff;
}
.wfm-video-asset-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 8px;
max-height: 320px;
max-height: 420px;
overflow-y: auto;
}
.wfm-video-asset-table-wrap {
max-height: 420px;
overflow-y: auto;
border: 1px solid var(--wfm-border);
border-radius: 4px;
}
.wfm-video-asset-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.wfm-video-asset-table th,
.wfm-video-asset-table td {
padding: 6px 8px;
text-align: left;
border-bottom: 1px solid var(--wfm-border);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 240px;
}
.wfm-video-asset-table thead th {
position: sticky;
top: 0;
background: var(--wfm-bg-secondary);
}
.wfm-video-asset-row { cursor: pointer; }
.wfm-video-asset-row:hover { background: var(--wfm-bg-hover); }
.wfm-video-asset-row.selected { outline: 2px solid var(--wfm-primary); outline-offset: -2px; }
.wfm-video-asset-card {
cursor: pointer;
border: 1px solid var(--wfm-border);
@@ -315,8 +385,221 @@
display: flex;
flex-direction: column;
gap: 6px;
border-top: 1px solid var(--wfm-border);
padding-top: 10px;
}
/* ── Edit: クリップタイムライン(Planタブと同じ横トラック+共有ツールバー方式)・
トリムパネル・書き出しパネル ──
Planの.wfm-video-timeline-trackはブロックをflex-grow(duration比率)で幅一杯に
引き伸ばすのに対し、Editのブロックは実秒数×px/秒の固定幅で「左詰め」に並べる
(空きトラックはそのまま空く)ため、専用クラスに分けている。 */
.wfm-video-edit-panel { padding: 0 16px 16px; }
.wfm-video-edit-timeline-track {
display: flex;
align-items: stretch;
gap: 4px;
width: 100%;
box-sizing: border-box;
height: 48px;
padding: 4px;
border-radius: 4px;
border: 1px solid var(--wfm-border);
overflow-x: auto;
position: relative;
flex: none; /* stay a fixed-height full-width bar; never shrink/stretch with clip content */
}
.wfm-video-edit-timeline-placeholder {
display: flex;
align-items: center;
}
.wfm-video-edit-timeline-block {
flex: none;
display: flex;
flex-direction: column;
justify-content: center;
padding: 2px 6px;
border: 1px solid var(--wfm-border);
border-radius: 3px;
background: var(--wfm-bg-hover, rgba(255, 255, 255, 0.04));
cursor: grab;
overflow: hidden;
transition: outline 0.1s;
}
.wfm-video-edit-timeline-block:active { cursor: grabbing; }
.wfm-video-edit-timeline-block.selected { outline: 2px solid var(--wfm-primary); outline-offset: -2px; }
.wfm-video-edit-timeline-block-name {
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wfm-video-edit-timeline-block-meta {
font-size: 10px;
color: var(--wfm-text-secondary);
}
.wfm-video-edit-main-row {
display: flex;
gap: 16px;
align-items: flex-start;
margin-top: 10px;
}
.wfm-video-edit-trim-panel {
flex: 1;
min-width: 0;
}
.wfm-video-edit-trim-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 160px));
gap: 10px;
}
.wfm-video-edit-trim-row label {
display: block;
font-size: 12px;
color: var(--wfm-text-secondary);
margin-bottom: 4px;
}
/* 開始/終了の入力欄+「現在位置を使用」ボタンを縦積みにする。以前は横並びで
ボタンのラベルがはみ出し、かつ入力欄とボタンの見分けがつきにくかった。 */
.wfm-video-edit-trim-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.wfm-video-edit-trim-field input { width: 100%; box-sizing: border-box; }
.wfm-video-edit-playhead-btn {
align-self: flex-start;
font-size: 10px;
padding: 2px 6px;
}
.wfm-video-edit-total-duration {
font-size: 12px;
color: var(--wfm-text-secondary);
white-space: nowrap;
}
/* ── トリムスクラバー: clipchamp風の時間目盛り+シークバー+トリムハンドル。
すべて%位置指定(clip.duration基準)にすることでpx/秒換算を持たずに済ませている。 */
.wfm-video-trim-scrubber {
margin-top: 10px;
/* start/end/playheadのバッジ(track上端から浮く)が上のクリップ名や
目盛りと重ならないよう、あらかじめ専用の余白を確保しておく。
playheadバッジはstart/endバッジより高い位置に2段目として出すため
(再生位置がトリム開始/終了に近いとき同じ高さだと文字が重なるため)、
その分も含めた高さを確保する。 */
padding-top: 44px;
user-select: none;
}
.wfm-video-trim-ruler {
position: relative;
height: 16px;
font-size: 10px;
color: var(--wfm-text-secondary);
}
.wfm-video-trim-ruler-tick {
position: absolute;
top: 0;
transform: translateX(-50%);
white-space: nowrap;
}
.wfm-video-trim-track {
position: relative;
height: 52px;
margin-top: 4px;
background: var(--wfm-bg-hover, #111);
border: 1px solid var(--wfm-border);
border-radius: 4px;
cursor: pointer;
}
.wfm-video-trim-range {
position: absolute;
top: 0;
bottom: 0;
background: rgba(70, 130, 230, 0.3);
border-top: 2px solid var(--wfm-primary);
border-bottom: 2px solid var(--wfm-primary);
pointer-events: none;
}
.wfm-video-trim-handle {
position: absolute;
top: 0;
bottom: 0;
width: 10px;
margin-left: -5px;
background: var(--wfm-primary);
border-radius: 2px;
cursor: ew-resize;
z-index: 3;
}
.wfm-video-trim-playhead {
position: absolute;
top: -4px;
bottom: -4px;
width: 2px;
margin-left: -1px;
background: #fff;
/* Below the trim handles (z-index:3) so a handle sitting at the same
position (e.g. right after "Set from playhead") stays grabbable instead
of the playhead's hit area stealing the pointer. */
z-index: 2;
cursor: ew-resize;
}
.wfm-video-trim-playhead::before {
content: "";
position: absolute;
top: -4px;
left: 50%;
transform: translateX(-50%);
width: 10px;
height: 10px;
border-radius: 50%;
background: #fff;
}
.wfm-video-trim-badge {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
background: #111;
color: #fff;
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
white-space: nowrap;
pointer-events: none;
}
.wfm-video-trim-badge-playhead {
background: #000;
/* start/endバッジ(-24px)より上の2段目に出し、再生位置がトリム範囲の
端に重なっても文字同士がぶつからないようにする */
top: -42px;
border: 1px solid var(--wfm-primary);
}
.wfm-video-edit-export-panel {
width: 220px;
flex: none;
}
.wfm-video-asset-detail label {
@@ -383,7 +666,7 @@
.wfm-video-preview-frame {
flex: 1;
min-width: 0;
min-height: 260px;
height: 260px;
display: flex;
align-items: center;
justify-content: center;
@@ -391,11 +674,16 @@
border-radius: 6px;
background: var(--wfm-bg-hover, #111);
text-align: center;
overflow: hidden;
}
/* ソース動画/画像のネイティブサイズがフレームより大きい場合のみ縮小し、
小さい場合は拡大しない(枠のheightを固定にしてPlan/Editの位置がずれないようにするため) */
.wfm-video-preview-el {
max-width: 100%;
max-height: 60vh;
max-height: 100%;
width: auto;
height: auto;
border-radius: 6px;
}
@@ -417,18 +705,6 @@
margin: 0 0 8px 0;
}
/* 将来の動画編集機能(トリミング等)用のプレースホルダー領域 */
.wfm-video-edit-panel {
flex: 1;
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed var(--wfm-border);
border-radius: 6px;
color: var(--wfm-text-secondary);
font-size: 13px;
}
/* ── 右ペイン: プロパティ (Frame抽出 / GIF変換タブ) ── */
.wfm-video-properties-panel {
+1
View File
@@ -745,6 +745,7 @@ function applyI18nToHtml() {
"wfm-help-video-5": "helpVideo5", "wfm-help-video-6": "helpVideo6",
"wfm-help-video-7": "helpVideo7", "wfm-help-video-8": "helpVideo8",
"wfm-help-video-9": "helpVideo9", "wfm-help-video-10": "helpVideo10",
"wfm-help-video-11": "helpVideo11", "wfm-help-video-12": "helpVideo12",
};
for (const [id, key] of Object.entries(helpIdMap)) {
const el = document.getElementById(id);
+27 -1
View File
@@ -60,7 +60,7 @@ function _isReservedGroup(name) {
// 拡張子から動画ファイル(mp4)かどうかを判定する。
// 一覧アイテムは path/filename、詳細メタ(get_image_metadata)は ext フィールドを持つため両対応。
function isVideoFile(img) {
export function isVideoFile(img) {
const source = img?.ext || img?.filename || img?.path || "";
return source.toLowerCase().endsWith(".mp4");
}
@@ -1986,6 +1986,7 @@ function bindEvents() {
return;
}
const url = API.serveImage(state.selectedImage.path);
const isVideo = isVideoFile(state.selectedImage);
// I2I送信元がImageタブだった場合はそちらへ画像を読み込ませる(_ccI2ITargetModeはComic Creater側が
// 「I2Iへ送る」ボタン押下時にセットするフラグ。undefined/'layout'なら従来通りコマ/オーバーレイへ挿入)
@@ -1996,6 +1997,11 @@ function bindEvents() {
targetMode = null;
}
if (targetMode === "image") {
// Imageタブは動画未対応(Comic Creator側の動画ツールはレイアウトタブのみ対応)
if (isVideo) {
showToast(t("galleryVideoNotSupportedInImageTab"), "info");
return;
}
let imageTab = null;
try {
if (window.parent && window.parent !== window && typeof window.parent._ccImageTab?.loadFromUrl === "function") {
@@ -2013,6 +2019,26 @@ function bindEvents() {
return;
}
// レイアウトタブのコマ/オーバーレイへ(デフォルト経路)。動画(.mp4)はComic Creator側の
// 動画ツール専用の受け口(insertVideoFromUrl)へ、それ以外は従来通り画像として送る
if (isVideo) {
let insertVideoFn = null;
try {
if (window.parent && window.parent !== window && typeof window.parent.insertVideoFromUrl === "function") {
insertVideoFn = window.parent.insertVideoFromUrl;
}
} catch (e) {
insertVideoFn = null;
}
if (!insertVideoFn) {
showToast(t("galleryCCNotAvailable"), "info");
return;
}
insertVideoFn(url, state.selectedImage.filename || "video.mp4");
showToast(t("gallerySentCC"), "success");
return;
}
let insertFn = null;
try {
if (window.parent && window.parent !== window && typeof window.parent.insertImageFromUrl === "function") {
+147 -12
View File
@@ -586,6 +586,7 @@ const LANGUAGES = {
sendToLiNodeSuccess: (filename) => `Sent "${filename}" to LI node`,
galleryCCNotAvailable: "Open this Gallery from ComfyUI Comic Creator to use Send CC",
gallerySentCC: "Sent to ComfyUI Comic Creator",
galleryVideoNotSupportedInImageTab: "Sending a video to the Image tab is not supported yet",
galleryNoEmbeddedWorkflow: "No ComfyUI workflow embedded in this image.",
galleryTabOutput: "Output",
galleryTabImagePrompt: "ImagePrompt",
@@ -1344,7 +1345,37 @@ const LANGUAGES = {
// -- Video Tab (MiniMax H3 / LTX-2.5 / Wan2.2, independent of GenerateUI) --
tabVideo: "Video",
loadInVideo: "Load in Video",
videoEditPlaceholder: "Editing tools coming soon",
videoEditNoClipsHint: "No clips yet — select an Asset or drop a video into Video Source above, then \"Add to Edit\"",
videoEditNoClips: "Add at least one clip first",
videoEditStillProbing: "Please wait for clip analysis to finish",
videoEditResolutionMismatch: (a, b) => `Clip resolutions don't match (${a} vs ${b}). Trim/crop to the same size before exporting.`,
videoEditProbing: "Analyzing...",
videoEditSelectClipHint: "Select a clip to trim",
videoEditImageDuration: "Duration (s)",
videoEditTrimStart: "Start (s)",
videoEditTrimEnd: "End (s)",
videoEditSetFromPlayhead: "Use current position",
videoEditMoveLeft: "Move left",
videoEditMoveRight: "Move right",
videoEditClearBtn: "Clear",
videoEditTotalDuration: (time) => `Total: ${time}`,
videoEditPreviewPlay: "▶ Preview",
videoEditPreviewStop: "■ Stop",
videoEditConfirmClear: "Clear the entire edit timeline?",
videoSourceAddToEdit: "Add to Edit",
videoEditDuplicate: "Duplicate",
videoEditDelete: "Delete",
videoEditExportBtn: "Export",
videoEditExporting: "Exporting...",
videoEditExportDone: (name) => `Exported: ${name}`,
videoEditSendToEdit: "Send to Edit",
videoEditClipSent: (name) => `Added "${name}" to Edit timeline`,
videoAssetSetAsFirst: "Set as First Image",
videoAssetSetAsLast: "Set as Last Image",
videoAssetSetAsFirstDone: (name) => `Set "${name}" as First Image`,
videoAssetSetAsLastDone: (name) => `Set "${name}" as Last Image`,
videoNoBlockSelected: "No block selected in Plan",
videoBlockImageUploadFailed: "Failed to upload image to Plan block",
videoNoWorkflowLoaded: "No workflow loaded",
videoUnsupportedWorkflow: "This workflow does not contain a supported video model node (MiniMax H3, LTX-2.5 Text/Image/First&Last-Frame to Video, or Wan2.2 Text to Video)",
videoWorkflowLoadFailed: "Failed to load workflow",
@@ -1355,6 +1386,11 @@ const LANGUAGES = {
videoPlanSaveFailed: "Failed to save plan",
videoPlanLoaded: "Plan loaded",
videoPlanLoadFailed: "Failed to load plan",
videoEditProjectEnterName: "Enter a name for this project",
videoEditProjectSaved: "Project saved",
videoEditProjectSaveFailed: "Failed to save project",
videoEditProjectLoaded: "Project loaded",
videoEditProjectLoadFailed: "Failed to load project",
videoConfirmClearPlan: "Clear the current plan?",
videoPlanPause: "Pause",
videoPlanResume: "Resume",
@@ -1388,18 +1424,27 @@ const LANGUAGES = {
videoProjectDeleted: "Plan deleted",
videoProjectDeleteFailed: "Failed to delete plan",
videoProjectLoadListFailed: "Failed to load plan list",
videoEditProjectHint: "Saved edit projects",
videoEditProjectEmpty: "No saved edit projects yet",
videoEditProjectClipCount: (n) => `${n} clip${n === 1 ? "" : "s"}`,
videoEditProjectDeleteConfirm: (name) => `Delete project "${name}"?`,
videoEditProjectDeleted: "Project deleted",
videoEditProjectDeleteFailed: "Failed to delete project",
videoEditProjectLoadListFailed: "Failed to load project list",
helpVideoTitle: "Video Tab",
helpVideoDesc: "A dedicated video generation tab for MiniMax H3 (Image to Video), LTX-2.5 (Text / Image / First & Last Frame to Video), and Wan2.2 (Text to Video) workflows, independent of the GenerateUI tab. The center panel's Plan / Edit subtabs handle batch generation and (future) editing; the sidebar's Asset / Project subtabs handle browsing generated videos and saved plans.",
helpVideoDesc: "A dedicated video generation tab for MiniMax H3 (Image to Video), LTX-2.5 (Text / Image / First & Last Frame to Video), and Wan2.2 (Text to Video) workflows, independent of the GenerateUI tab. The center panel's Plan / Edit / Asset subtabs handle batch generation, clip editing, and browsing generated videos; the sidebar's Project subtab handles saved plans (while Plan is active) or saved Edit projects (while Edit is active).",
helpVideo1: "Plan subtab: the topbar's settings row holds Aspect Ratio / Megapixels / Multiple / Duration (seconds) / Noise Seed as this plan's base settings — Duration always reflects the currently selected block (each block keeps its own value), the rest are plan-wide. A second row below adds Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance, each enabled only for workflows that actually support it (see helpVideo2). Split the timeline into blocks with the + Split / + Block / Delete buttons — each block has its own Prompt, Duration, and First/Last Image — First Image can be None, an explicit image, or \"use previous block's last frame\" (an explicit Last Image on the previous block always wins over extracting one from its generated video). Click Run to generate every block in order; Pause/Cancel work mid-run.",
helpVideo2: "A plan with a single block behaves exactly like the old one-shot form. Model selection (UNet / CLIP / VAE / Audio VAE) is fixed to the loaded workflow's defaults. For workflows with no ResolutionSelector node (e.g. Wan2.2, LTX-2.5 First & Last Frame to Video), the Aspect Ratio / Megapixels / Multiple fields are disabled and the workflow's own Width / Height become directly editable instead. Turbo Mode / Strength / Steps only apply to MiniMax H3's turbo LoRA, and Prompt Enhance only to LTX-2.5 — both stay disabled for other model families. Noise Seed is disabled for the one workflow that doesn't expose it: Wan2.2 Text-to-Video.",
helpVideo3: "Use the \"Load in Video\" button in the Workflow tab's detail modal to load a workflow containing a MiniMax H3 Image-to-Video node, an LTX-2.5 Text/Image/First&Last-Frame-to-Video node, or a Wan2.2 Text-to-Video node — this starts a fresh single-block plan seeded from the workflow's own current values. Unsupported workflows show an error instead.",
helpVideo4: "Generated videos (mp4) automatically appear in the Asset subtab's Video Temp group, and can also be browsed and played back in the Gallery tab. Organizing keepers into Video Assets is entirely up to you, on your own schedule.",
helpVideo5: "The center panel shows two previews: the left one shows either the video you select in the Asset subtab or whatever you drop/pick in the Video Source panel on its left (Clear resets it to empty), letting you use the Frame/GIF tools without running the loaded video workflow first; the right one always shows the most recently generated clip.",
helpVideo6: "Frame tab (right panel): play/seek the preview video to the position you want, click Capture Frame to grab that frame client-side, then Save to Output to write it as a PNG into ComfyUI's output folder.",
helpVideo6: "Frame tab (right panel): play/seek the preview video to the position you want, click Capture Frame to grab that frame client-side, then Save to Output to write it as a PNG into ComfyUI's output folder — check \"Add to Video Assets group\" first to also tag the saved frame into Gallery's Video Assets group.",
helpVideo7: "GIF tab (right panel): set Start/End (seconds), FPS, and an optional Max Width, then Convert to GIF — the server decodes the video and encodes an animated GIF into ComfyUI's output folder.",
helpVideo8: "Chaining: setting a block's First Image to \"use previous block's last frame\" continues smoothly from the previous clip — if that previous block has its own explicit Last Image, that image is used directly; otherwise the actual last frame of its generated video is extracted automatically.",
helpVideo9: "Project tab (sidebar, next to Asset): browse every saved plan file (thumbnail, block count, last updated), click one to load it into the Plan editor, or delete it with the × button; clicking the same tab again collapses the panel.",
helpVideo9: "Project tab (sidebar): lists saved Video Plans while the center Plan subtab is active, or saved Edit projects while the center Edit subtab is active (thumbnail/block or clip count, last updated) — click one to load it, or delete it with the × button; clicking the same tab again collapses the panel.",
helpVideo10: "Highlight color: the Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple / Noise Seed / Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance labels turn the same highlight color as the GenerateUI Model tab (customizable in the Settings tab) when the loaded workflow actually supports that field — e.g. for a Wan2.2 workflow (no ResolutionSelector node), the Aspect Ratio / Megapixels / Multiple labels stay unhighlighted while Width / Height light up instead.",
helpVideo11: "Edit subtab: a lightweight clip timeline for trimming and concatenating video/image clips outside of generation. Add clips via the Video Source panel's \"Add to Edit\" button or the Asset subtab's \"Send to Edit\" button, then drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, and Clear to empty the timeline. The trim panel for the selected video clip shows a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges — image clips instead get a simple hold-duration field. ▶ Preview plays every ready clip back-to-back (trimmed to its in/out points) in the right-hand result preview; Export builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into the Video Temp group automatically. Exporting more than one clip currently drops audio, since ComfyUI's concatenation node requires uniform audio across all inputs. The export panel's Save/Save As/Load buttons persist the timeline itself (clip order, trim points, which uploaded file each clip refers to) as a JSON project file on the server — Load re-fetches each clip from its saved server reference and re-probes it, so the original files must still exist. These saved projects also show up in the sidebar's Project tab whenever the Edit subtab is active (see helpVideo9).",
helpVideo12: "Asset subtab (center panel, next to Plan/Edit): browses every video/image tagged into Video Assets or Video Temp (via the group dropdown, or \"All Video Assets\" for both). Search filters by filename; the ▦/☰ buttons switch between a thumbnail grid and a table (filename/updated/tags). Selecting an item shows its tags/memo, loads it into the center preview, and offers \"Send to Edit\" (queues it as a clip in the Edit subtab) plus, for images only, \"Set as First Image\"/\"Set as Last Image\" (uploads it straight into the currently-selected Plan block, switching to the Plan subtab).",
},
ja: {
@@ -1982,6 +2027,7 @@ const LANGUAGES = {
sendToLiNodeSuccess: (filename) => `"${filename}" をLIノードへ送信しました`,
galleryCCNotAvailable: "Send CCはComfyUI Comic Creator内のギャラリーから開いた場合のみ使用できます",
gallerySentCC: "ComfyUI Comic Creatorへ送信しました",
galleryVideoNotSupportedInImageTab: "動画のImageタブへの送信は未対応です",
galleryNoEmbeddedWorkflow: "この画像にはComfyUIワークフローが埋め込まれていません。",
galleryTabOutput: "Output",
galleryTabImagePrompt: "ImagePrompt",
@@ -2740,7 +2786,37 @@ const LANGUAGES = {
// -- Video タブ(MiniMax H3 / LTX-2.5 / Wan2.2対応、GenerateUIタブとは独立) --
tabVideo: "Video",
loadInVideo: "Videoに読み込み",
videoEditPlaceholder: "編集機能は今後追加予定",
videoEditNoClipsHint: "クリップがありません — 上の動画ソースでAssetを選択/動画をドロップし「Editに追加」してください",
videoEditNoClips: "まずクリップを1つ以上追加してください",
videoEditStillProbing: "クリップの解析が完了するまでお待ちください",
videoEditResolutionMismatch: (a, b) => `クリップの解像度が一致しません(${a} と ${b})。書き出し前に同じサイズにトリム/クロップしてください。`,
videoEditProbing: "解析中...",
videoEditSelectClipHint: "トリムするクリップを選択してください",
videoEditImageDuration: "表示時間(秒)",
videoEditTrimStart: "開始(秒)",
videoEditTrimEnd: "終了(秒)",
videoEditSetFromPlayhead: "現在位置を使用",
videoEditMoveLeft: "左へ移動",
videoEditMoveRight: "右へ移動",
videoEditClearBtn: "クリア",
videoEditTotalDuration: (time) => `合計: ${time}`,
videoEditPreviewPlay: "▶ プレビュー",
videoEditPreviewStop: "■ 停止",
videoEditConfirmClear: "編集タイムラインを全てクリアしますか?",
videoSourceAddToEdit: "Editに追加",
videoEditDuplicate: "複製",
videoEditDelete: "削除",
videoEditExportBtn: "書き出し",
videoEditExporting: "書き出し中...",
videoEditExportDone: (name) => `書き出し完了: ${name}`,
videoEditSendToEdit: "Editへ送る",
videoEditClipSent: (name) => `"${name}" をEditタイムラインに追加しました`,
videoAssetSetAsFirst: "First Imageに設定",
videoAssetSetAsLast: "Last Imageに設定",
videoAssetSetAsFirstDone: (name) => `"${name}" をFirst Imageに設定しました`,
videoAssetSetAsLastDone: (name) => `"${name}" をLast Imageに設定しました`,
videoNoBlockSelected: "Planでブロックが選択されていません",
videoBlockImageUploadFailed: "Planブロックへの画像アップロードに失敗しました",
videoNoWorkflowLoaded: "ワークフローが読み込まれていません",
videoUnsupportedWorkflow: "このワークフローには対応する動画モデルノード(MiniMax H3、LTX-2.5のText/Image/First&Last-Frame to Video、またはWan2.2のText to Video)が含まれていません",
videoWorkflowLoadFailed: "ワークフローの読み込みに失敗しました",
@@ -2751,6 +2827,11 @@ const LANGUAGES = {
videoPlanSaveFailed: "Planの保存に失敗しました",
videoPlanLoaded: "Planを読み込みました",
videoPlanLoadFailed: "Planの読み込みに失敗しました",
videoEditProjectEnterName: "プロジェクトの名前を入力してください",
videoEditProjectSaved: "プロジェクトを保存しました",
videoEditProjectSaveFailed: "プロジェクトの保存に失敗しました",
videoEditProjectLoaded: "プロジェクトを読み込みました",
videoEditProjectLoadFailed: "プロジェクトの読み込みに失敗しました",
videoConfirmClearPlan: "現在のPlanをクリアしますか?",
videoPlanPause: "一時停止",
videoPlanResume: "再開",
@@ -2784,18 +2865,27 @@ const LANGUAGES = {
videoProjectDeleted: "Planを削除しました",
videoProjectDeleteFailed: "Planの削除に失敗しました",
videoProjectLoadListFailed: "Plan一覧の読み込みに失敗しました",
videoEditProjectHint: "保存済みのEditプロジェクト",
videoEditProjectEmpty: "保存済みのEditプロジェクトはまだありません",
videoEditProjectClipCount: (n) => `${n}クリップ`,
videoEditProjectDeleteConfirm: (name) => `プロジェクト「${name}」を削除しますか?`,
videoEditProjectDeleted: "プロジェクトを削除しました",
videoEditProjectDeleteFailed: "プロジェクトの削除に失敗しました",
videoEditProjectLoadListFailed: "プロジェクト一覧の読み込みに失敗しました",
helpVideoTitle: "Video タブ",
helpVideoDesc: "MiniMax H3 (Image to Video)、LTX-2.5 (Text / Image / First & Last Frame to Video)、Wan2.2 (Text to Video) ワークフロー対応の動画生成タブ。GenerateUIタブとは独立して動作します。中央パネルのPlan/Editサブタブがバッチ生成と(将来の)編集を、左サイドバーのAsset/Projectサブタブが生成済み動画の閲覧と保存済みPlanの一覧を扱います。",
helpVideoDesc: "MiniMax H3 (Image to Video)、LTX-2.5 (Text / Image / First & Last Frame to Video)、Wan2.2 (Text to Video) ワークフロー対応の動画生成タブ。GenerateUIタブとは独立して動作します。中央パネルのPlan/Edit/Assetサブタブがバッチ生成・クリップ編集・生成済み動画の閲覧を扱い、左サイドバーのProjectサブタブは中央がPlan選択中なら保存済みPlan一覧、Edit選択中なら保存済みEditプロジェクト一覧を扱います。",
helpVideo1: "Planサブタブ: 上部の設定行にAspect Ratio/Megapixels/Multiple/Duration(秒)/Noise SeedがこのPlanの基本設定として並びます——Durationは常に現在選択中のブロックの値を表示・編集します(各ブロックが独自の値を持つ)が、それ以外はPlan全体で共通の値です。その下の2行目にはWidth/Height/Turbo Mode/Strength/Steps/Prompt Enhanceが並び、読み込んだワークフローが実際に対応する項目だけ入力可能になります(詳細はhelpVideo2)。「+ Split」「+ Block」「Delete」ボタンでタイムラインをブロックに分割すると、各ブロックは独自のPrompt・Duration・First/Last Imageを持ちます(First Imageは「なし」「画像を指定」「前ブロックの最終フレームを使用」から選択可能。前ブロックに明示的なLast Imageが設定されていれば、フレーム抽出よりそちらが優先されます)。Runを押すと全ブロックを順番に生成します。実行中もPause/Cancelが使えます。",
helpVideo2: "ブロックが1個だけのPlanは、以前の単発生成フォームと同じ動作になります。モデル選択(UNet/CLIP/VAE/Audio VAE)は読み込んだワークフローの既定値に固定されます。ResolutionSelectorノードを持たないワークフロー(Wan2.2、LTX-2.5のFirst & Last Frame to Videoなど)はAspect Ratio/Megapixels/Multiple欄が無効化される代わりに、ワークフロー自体のWidth/Heightを直接編集できるようになります。Turbo Mode/Strength/StepsはMiniMax H3のturbo LoRA専用、Prompt EnhanceはLTX-2.5専用で、他のモデルファミリーでは無効のままです。Noise SeedはWan2.2のText to Videoのみ公開入力を持たないため無効化されます。",
helpVideo3: "Workflowタブの詳細モーダルにある「Videoに読み込み」ボタンから、MiniMax H3のImage to Videoノード、LTX-2.5のText/Image/First&Last-Frame to Videoノード、またはWan2.2のText to Videoノードを含むワークフローを読み込めます。この操作でワークフローの現在値を反映したブロック1個の新規Planが作成されます。対応していないワークフローはエラー表示になります。",
helpVideo4: "生成した動画(mp4)はAssetサブタブのVideo Tempグループに自動的に反映されるほか、Galleryタブでも閲覧・再生できます。残したい動画をVideo Assetsへ整理するタイミングはユーザー任意です。",
helpVideo5: "中央パネルには2つのプレビューがあります。左側はAssetサブタブで選択した動画、または左隣の「Video Source」パネルにドロップ/選択した任意のローカル動画を表示し(Clearでリセット)、読み込んだワークフローを実行せずにFrame/GIF機能を使えます。右側は常に直近で生成されたクリップを表示します。",
helpVideo6: "Frameタブ(右パネル): プレビュー動画を再生・シークして止めたい位置で「Capture Frame」を押すとその場でフレームをキャプチャし、「Save to Output」でPNGとしてComfyUIのOutputフォルダに保存します。",
helpVideo6: "Frameタブ(右パネル): プレビュー動画を再生・シークして止めたい位置で「Capture Frame」を押すとその場でフレームをキャプチャし、「Save to Output」でPNGとしてComfyUIのOutputフォルダに保存します。保存前に「Add to Video Assets group」にチェックを入れると、保存したフレームをGalleryのVideo Assetsグループにも自動登録できます。",
helpVideo7: "GIFタブ(右パネル): Start/End(秒)・FPS・Max Width(任意)を指定して「Convert to GIF」を押すと、サーバー側で動画をデコードしアニメーションGIFとしてComfyUIのOutputフォルダに保存します。",
helpVideo8: "チェイン機能: ブロックのFirst Imageを「前ブロックの最終フレームを使用」に設定すると、前のクリップから連続した映像になります。前ブロックに明示的なLast Imageが設定されていればそれがそのまま使われ、無ければ生成された動画から実際の最終フレームが自動抽出されます。",
helpVideo9: "Projectタブ(左サイドバー、Assetの隣): 保存済みの全Planファイル(サムネイル・ブロック数・更新日時)を一覧表示し、クリックでPlanエディタへ読み込み、×ボタンで削除できます。同じタブをもう一度押すとパネルを閉じます。",
helpVideo9: "Projectタブ(左サイドバー): 中央がPlanサブタブのときは保存済みPlan一覧、Editサブタブのときは保存済みEditプロジェクト一覧(サムネイル/ブロック数またはクリップ数・更新日時)を表示します。クリックで読み込み、×ボタンで削除できます。同じタブをもう一度押すとパネルを閉じます。",
helpVideo10: "ハイライト表示: 読み込んだワークフローが実際にサポートしているフィールドに応じて、Prompt/Duration/First Image/Last Image/Aspect Ratio/Megapixels/Multiple/Noise Seed/Width/Height/Turbo Mode/Strength/Steps/Prompt Enhanceのラベルが、GenerateUI ModelタブのハイライトとSettingsタブで共通設定できる色に変わります。例えばWan2.2ワークフロー(ResolutionSelectorノードが無い)ではAspect Ratio/Megapixels/Multipleはハイライトされない代わりに、Width/Heightがハイライトされます。",
helpVideo11: "Editサブタブ: 生成とは独立して動画/画像クリップをトリム・結合できる簡易タイムラインです。Video Sourceパネルの「Editに追加」ボタン、またはAssetサブタブの「Editへ送る」ボタンでクリップを追加し、ブロックをドラッグ(または◀/▶ツールバーボタン)で並べ替え、選択中クリップをDuplicate/Delete、Clearでタイムラインを空にできます。選択中の動画クリップのトリムパネルには時間目盛り・Sourceプレビューと同期するドラッグ可能な再生ヘッド・時刻バッジ付きのドラッグ可能なイン/アウトハンドルが表示されます(画像クリップは代わりにシンプルな保持時間の入力欄)。「▶ プレビュー」は準備済みの全クリップをイン/アウト点でトリムして右側の結果プレビューで連続再生し、「書き出し」は実際のComfyUIグラフ(トリム→2クリップ以上ならConcatenate→保存)を構築・実行して、結果をVideo Tempグループへ自動登録します。複数クリップの書き出しは現状音声が失われます(ComfyUIの結合ノードが全入力で音声形式の統一を要求するため)。書き出しパネルのSave/Save As/Loadボタンは、タイムラインそのもの(クリップ順序・トリム区間・各クリップが参照するアップロード済みファイル)をサーバー上にJSONプロジェクトとして保存します。Loadは保存されたサーバー参照から各クリップを再取得・再プローブするため、元のファイルが存在している必要があります。保存済みプロジェクトは、中央がEditサブタブのときサイドバーのProjectタブにも一覧表示されます(helpVideo9参照)。",
helpVideo12: "Assetサブタブ(中央パネル、Plan/Editの隣): グループ切替(または「All Video Assets」で両方まとめて)でVideo AssetsまたはVideo Tempに登録された動画/画像を一覧表示します。検索欄はファイル名でフィルタし、▦/☰ボタンでサムネイルグリッドとテーブル表示(ファイル名・更新日時・タグ)を切り替えられます。項目を選択するとタグ・メモが表示され中央プレビューに読み込まれ、「Editへ送る」(Editサブタブのクリップとして追加)に加え、画像の場合のみ「First Imageに設定」「Last Imageに設定」(選択中のPlanブロックへ直接アップロードし、Planサブタブへ切り替え)が使えます。",
},
zh: {
@@ -3378,6 +3468,7 @@ const LANGUAGES = {
sendToLiNodeSuccess: (filename) => `已将"${filename}"发送到LI节点`,
galleryCCNotAvailable: "仅当从ComfyUI Comic Creator内的图库打开时才能使用Send CC",
gallerySentCC: "已发送至ComfyUI Comic Creator",
galleryVideoNotSupportedInImageTab: "暂不支持将视频发送到Image标签",
galleryNoEmbeddedWorkflow: "此图像中未嵌入ComfyUI工作流。",
galleryTabOutput: "Output",
galleryTabImagePrompt: "ImagePrompt",
@@ -4136,7 +4227,37 @@ const LANGUAGES = {
// -- Video 标签页(支持 MiniMax H3 / LTX-2.5 / Wan2.2,独立于 GenerateUI) --
tabVideo: "Video",
loadInVideo: "加载到Video",
videoEditPlaceholder: "编辑功能即将推出",
videoEditNoClipsHint: "暂无片段 — 请在上方选择素材或拖放视频后点击“添加到Edit”",
videoEditNoClips: "请先添加至少一个片段",
videoEditStillProbing: "请等待片段分析完成",
videoEditResolutionMismatch: (a, b) => `片段分辨率不一致(${a} 与 ${b})。导出前请裁剪为相同尺寸。`,
videoEditProbing: "分析中...",
videoEditSelectClipHint: "选择一个片段进行裁剪",
videoEditImageDuration: "显示时长(秒)",
videoEditTrimStart: "开始(秒)",
videoEditTrimEnd: "结束(秒)",
videoEditSetFromPlayhead: "使用当前位置",
videoEditMoveLeft: "左移",
videoEditMoveRight: "右移",
videoEditClearBtn: "清空",
videoEditTotalDuration: (time) => `总计: ${time}`,
videoEditPreviewPlay: "▶ 预览",
videoEditPreviewStop: "■ 停止",
videoEditConfirmClear: "确定清空整个编辑时间线吗?",
videoSourceAddToEdit: "添加到Edit",
videoEditDuplicate: "复制",
videoEditDelete: "删除",
videoEditExportBtn: "导出",
videoEditExporting: "导出中...",
videoEditExportDone: (name) => `已导出: ${name}`,
videoEditSendToEdit: "发送到Edit",
videoEditClipSent: (name) => `已将 "${name}" 添加到Edit时间线`,
videoAssetSetAsFirst: "设为First Image",
videoAssetSetAsLast: "设为Last Image",
videoAssetSetAsFirstDone: (name) => `已将 "${name}" 设为First Image`,
videoAssetSetAsLastDone: (name) => `已将 "${name}" 设为Last Image`,
videoNoBlockSelected: "Plan中未选择区块",
videoBlockImageUploadFailed: "上传图片到Plan区块失败",
videoNoWorkflowLoaded: "尚未加载工作流",
videoUnsupportedWorkflow: "该工作流不包含受支持的视频模型节点(MiniMax H3、LTX-2.5 的 Text/Image/First&Last-Frame to Video,或 Wan2.2 的 Text to Video)",
videoWorkflowLoadFailed: "工作流加载失败",
@@ -4147,6 +4268,11 @@ const LANGUAGES = {
videoPlanSaveFailed: "Plan 保存失败",
videoPlanLoaded: "Plan 已加载",
videoPlanLoadFailed: "Plan 加载失败",
videoEditProjectEnterName: "请输入该项目的名称",
videoEditProjectSaved: "项目已保存",
videoEditProjectSaveFailed: "项目保存失败",
videoEditProjectLoaded: "项目已加载",
videoEditProjectLoadFailed: "项目加载失败",
videoConfirmClearPlan: "要清空当前 Plan 吗?",
videoPlanPause: "暂停",
videoPlanResume: "继续",
@@ -4180,18 +4306,27 @@ const LANGUAGES = {
videoProjectDeleted: "Plan 已删除",
videoProjectDeleteFailed: "Plan 删除失败",
videoProjectLoadListFailed: "Plan 列表加载失败",
videoEditProjectHint: "已保存的 Edit 项目",
videoEditProjectEmpty: "暂无已保存的 Edit 项目",
videoEditProjectClipCount: (n) => `${n} 个片段`,
videoEditProjectDeleteConfirm: (name) => `确定删除项目「${name}」吗?`,
videoEditProjectDeleted: "项目已删除",
videoEditProjectDeleteFailed: "项目删除失败",
videoEditProjectLoadListFailed: "项目列表加载失败",
helpVideoTitle: "Video 标签页",
helpVideoDesc: "支持 MiniMax H3(Image to Video)、LTX-2.5(Text / Image / First & Last Frame to Video)、Wan2.2(Text to Video)工作流的视频生成标签页,独立于 GenerateUI 标签页运行。中央面板的 Plan/Edit 子标签负责批量生成和(未来的)编辑;侧边栏的 Asset/Project 子标签负责浏览已生成的视频和已保存的 Plan。",
helpVideoDesc: "支持 MiniMax H3(Image to Video)、LTX-2.5(Text / Image / First & Last Frame to Video)、Wan2.2(Text to Video)工作流的视频生成标签页,独立于 GenerateUI 标签页运行。中央面板的 Plan/Edit/Asset 子标签负责批量生成、片段剪辑和浏览已生成的视频;侧边栏的 Project 子标签在中央选中 Plan 时显示已保存的 Plan 列表,选中 Edit 时显示已保存的 Edit 项目列表。",
helpVideo1: "Plan 子标签:顶部设置行包含 Aspect Ratio/Megapixels/Multiple/Duration(秒)/Noise Seed,作为该 Plan 的基础设置——Duration 始终显示并可编辑当前选中区块的值(每个区块保留自己的值),其余字段则是整个 Plan 共用的。下方第二行是 Width/Height/Turbo Mode/Strength/Steps/Prompt Enhance,仅当所加载的工作流实际支持该项时才可输入(详见 helpVideo2)。用「+ Split」「+ Block」「Delete」按钮把时间线分割成多个区块后,每个区块都有自己的 Prompt、Duration 以及 First/Last Image(First Image 可选择“无”、“指定图片”或“使用前一个区块的最后一帧”——若前一个区块已明确设置了 Last Image,则优先使用该图片而非提取帧)。点击 Run 会按顺序生成所有区块,运行中也可以 Pause/Cancel。",
helpVideo2: "只有一个区块的 Plan 与旧版单次生成表单效果完全相同。模型选择(UNet/CLIP/VAE/Audio VAE)固定为所加载工作流的默认值。没有 ResolutionSelector 节点的工作流(如 Wan2.2、LTX-2.5 的 First & Last Frame to Video)会禁用 Aspect Ratio/Megapixels/Multiple 字段,改为可以直接编辑工作流自身的 Width/Height。Turbo Mode/Strength/Steps 仅适用于 MiniMax H3 的 turbo LoRA,Prompt Enhance 仅适用于 LTX-2.5——其他模型系列中这些字段保持禁用。Noise Seed 仅在 Wan2.2 的 Text to Video 中会被禁用,因为该变体未公开此输入。",
helpVideo3: "在 Workflow 标签页的详情弹窗中使用「加载到Video」按钮,可以加载包含 MiniMax H3 的 Image to Video 节点、LTX-2.5 的 Text/Image/First&Last-Frame to Video 节点、或 Wan2.2 的 Text to Video 节点的工作流——此操作会创建一个仅含一个区块、并以该工作流当前值为初始值的新 Plan。不受支持的工作流会显示错误提示。",
helpVideo4: "生成的视频(mp4)会自动出现在 Asset 子标签的 Video Temp 组中,也可以在 Gallery 标签页中浏览和播放。是否整理到 Video Assets,由您自行决定时机。",
helpVideo5: "中间面板有两个预览区:左侧显示您在 Asset 子标签中选择的视频,或拖放/选择到左边「Video Source」面板中的任意本地视频(点击 Clear 可重置),无需先运行已加载的视频工作流即可使用 Frame/GIF 工具;右侧始终显示最近一次生成的片段。",
helpVideo6: "Frame标签页(右侧面板):播放/拖动预览视频到想要的位置,点击「Capture Frame」在客户端截取该帧,再点击「Save to Output」将其保存为PNG到ComfyUI的输出文件夹。",
helpVideo6: "Frame标签页(右侧面板):播放/拖动预览视频到想要的位置,点击「Capture Frame」在客户端截取该帧,再点击「Save to Output」将其保存为PNG到ComfyUI的输出文件夹——保存前勾选「Add to Video Assets group」还可以把保存的帧同时标记进 Gallery 的 Video Assets 分组。",
helpVideo7: "GIF标签页(右侧面板):设置Start/End(秒)、FPS和可选的Max Width,点击「Convert to GIF」后,服务器会解码视频并将动画GIF保存到ComfyUI的输出文件夹。",
helpVideo8: "链接功能:将某个区块的 First Image 设置为“使用前一个区块的最后一帧”,即可与上一个片段流畅衔接——如果前一个区块已明确设置了 Last Image,将直接使用该图片;否则会自动从其生成的视频中提取实际的最后一帧。",
helpVideo9: "Project标签页(左侧边栏,与Asset并列):浏览所有已保存的 Plan 文件(缩略图、区块数、最后更新时间),点击即可加载到 Plan 编辑器,或用 × 按钮删除;再次点击同一标签会收起面板。",
helpVideo9: "Project标签页(左侧边栏):中央为 Plan 子标签时显示已保存的 Plan 列表,为 Edit 子标签时显示已保存的 Edit 项目列表(缩略图/区块数或片段数、最后更新时间),点击即可加载,或用 × 按钮删除;再次点击同一标签会收起面板。",
helpVideo10: "高亮颜色:当所加载的工作流实际支持该字段时,Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple / Noise Seed / Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance 标签会变为与 GenerateUI Model 标签页相同的高亮颜色(可在 Settings 标签页自定义)——例如 Wan2.2 工作流(没有 ResolutionSelector 节点)中,Aspect Ratio / Megapixels / Multiple 不会高亮,取而代之的是 Width / Height 会高亮。",
helpVideo11: "Edit 子标签:独立于生成流程的轻量剪辑时间线,用于裁剪和拼接视频/图片片段。通过 Video Source 面板的「Add to Edit」按钮,或 Asset 子标签的「Send to Edit」按钮添加片段,拖动区块(或使用 ◀/▶ 工具栏按钮)重新排序,对选中片段执行 Duplicate/Delete,Clear 可清空时间线。选中视频片段的裁剪面板会显示时间刻度尺、与 Source 预览同步的可拖动播放头,以及带时间标签的可拖动入/出点手柄(图片片段则只有一个简单的保持时长输入框)。「▶ Preview」会按入/出点依次裁剪并在右侧结果预览区连续播放所有就绪片段;「Export」会构建并运行实际的 ComfyUI 图(裁剪→若多于一个片段则拼接→保存),并自动把结果标记进 Video Temp 分组。目前导出多个片段会丢失音频,因为 ComfyUI 的拼接节点要求所有输入的音频格式一致。导出面板的 Save/Save As/Load 按钮会把时间线本身(片段顺序、裁剪区间、每个片段引用的已上传文件)作为 JSON 项目保存到服务器;Load 会根据保存的服务器引用重新获取并重新探测每个片段,因此原始文件必须仍然存在。已保存的项目在中央为 Edit 子标签时也会显示在侧边栏的 Project 标签中(参见 helpVideo9)。",
helpVideo12: "Asset 子标签(中央面板,位于 Plan/Edit 旁):浏览标记到 Video Assets 或 Video Temp 分组的所有视频/图片(通过分组下拉框选择,或选择「All Video Assets」同时浏览两者)。搜索框按文件名过滤,▦/☰ 按钮可在缩略图网格和表格视图(文件名/更新时间/标签)之间切换。选中一项会显示其标签/备注并加载到中央预览,此外还提供「Send to Edit」(作为片段加入 Edit 子标签);仅图片还额外提供「Set as First Image」「Set as Last Image」(直接上传到当前选中的 Plan 区块,并切换到 Plan 子标签)。",
},
};
+129 -10
View File
@@ -8,8 +8,12 @@
* to the real Gallery tab via the "Open in Gallery" button.
*/
import { VIDEO_GROUP, VTEMP_GROUP, ensureVideoGroup } from "./gallery-tab.js";
import { VIDEO_GROUP, VTEMP_GROUP, ensureVideoGroup, isVideoFile } from "./gallery-tab.js";
import { setSourcePreview } from "./video-preview.js";
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { addClipFromFile } from "./video-edit-tab.js";
import { setBlockImageFromFile } from "./video-plan-tab.js";
// Sentinel for the "All Video Assets" option — not a real backend group, since
// Gallery's group filter only ever matches one group at a time. Selecting it
@@ -27,8 +31,24 @@ const _s = {
images: [],
selectedPath: null,
loaded: false, // becomes true once the Asset subtab has been shown at least once
searchQuery: "",
viewMode: "grid", // "grid" | "table"
kindFilter: "all", // "all" | "video" | "image"
};
function _formatDate(mtime) {
return new Date(mtime * 1000).toLocaleString();
}
function _filteredImages() {
const q = _s.searchQuery.trim().toLowerCase();
let list = _s.images;
if (_s.kindFilter === "video") list = list.filter((img) => isVideoFile(img));
else if (_s.kindFilter === "image") list = list.filter((img) => !isVideoFile(img));
if (q) list = list.filter((img) => img.filename.toLowerCase().includes(q));
return list;
}
async function _fetchOutputDir() {
if (_s.outputDir) return;
try {
@@ -92,7 +112,7 @@ async function _loadImages() {
images = await _fetchGroupImages(_s.group);
}
_s.images = images;
_renderGrid();
_renderList();
if (statusEl) statusEl.textContent = `${_s.images.length} video(s)`;
} catch (err) {
_s.images = [];
@@ -101,15 +121,55 @@ async function _loadImages() {
}
}
// Renders whichever of grid/table is active for the current viewMode — called
// on load, on search input, and after switching view modes.
function _renderList() {
if (_s.viewMode === "table") _renderTable();
else _renderGrid();
}
function _renderGrid() {
const grid = document.getElementById("wfm-video-asset-grid");
if (!grid) return;
if (_s.images.length === 0) {
const images = _filteredImages();
if (images.length === 0) {
grid.innerHTML = `<div class="wfm-placeholder">No videos yet</div>`;
return;
}
grid.innerHTML = "";
for (const img of _s.images) grid.appendChild(_makeCard(img));
for (const img of images) grid.appendChild(_makeCard(img));
}
function _renderTable() {
const body = document.getElementById("wfm-video-asset-table-body");
if (!body) return;
const images = _filteredImages();
if (images.length === 0) {
body.innerHTML = `<tr><td colspan="3" class="wfm-placeholder">No videos yet</td></tr>`;
return;
}
body.innerHTML = "";
for (const img of images) body.appendChild(_makeRow(img));
}
function _makeRow(img) {
const row = document.createElement("tr");
row.className = "wfm-video-asset-row" + (img.path === _s.selectedPath ? " selected" : "");
row.dataset.path = img.path;
const nameCell = document.createElement("td");
nameCell.textContent = img.filename;
nameCell.title = img.filename;
const dateCell = document.createElement("td");
dateCell.textContent = _formatDate(img.mtime);
const tagsCell = document.createElement("td");
tagsCell.textContent = (img.tags || []).join(", ");
row.append(nameCell, dateCell, tagsCell);
row.addEventListener("click", () => _selectImage(img));
return row;
}
function _makeCard(img) {
@@ -135,27 +195,30 @@ function _makeCard(img) {
function _selectImage(img) {
_s.selectedPath = img.path;
document.querySelectorAll(".wfm-video-asset-card").forEach((c) => {
document.querySelectorAll(".wfm-video-asset-card, .wfm-video-asset-row").forEach((c) => {
c.classList.toggle("selected", c.dataset.path === img.path);
});
_renderDetail(img);
_loadIntoSourcePreview(img);
}
// Feeds the selected asset's video into the center panel's Asset/Source preview
// Feeds the selected asset's media into the center panel's Asset/Source preview
// pane (see video-preview.js) so it's visible without switching tabs, and so the
// Frame/GIF property tools can operate on it just like a Video Source drop.
// Fetched as a Blob and wrapped as a "local" source rather than trying to map
// Gallery's arbitrary absolute path onto ComfyUI's filename/subfolder/type
// triple — that keeps the GIF tool's existing upload-on-first-use path working
// unchanged for both cases.
// unchanged for both cases. Still/animated images (this group isn't exclusively
// videos — anything tagged into it shows up here) preview via the pane's <img>
// companion element instead of trying to play them as a <video>.
async function _loadIntoSourcePreview(img) {
try {
const res = await fetch(`/wfm/gallery/image/serve?path=${encodeURIComponent(img.path)}`);
if (!res.ok) throw new Error(String(res.status));
const blob = await res.blob();
const file = new File([blob], img.filename, { type: blob.type || "video/mp4" });
setSourcePreview(URL.createObjectURL(file), { kind: "local", file });
const isVideo = isVideoFile(img);
const file = new File([blob], img.filename, { type: blob.type || (isVideo ? "video/mp4" : "image/png") });
setSourcePreview(URL.createObjectURL(file), { kind: "local", file }, isVideo ? "video" : "image");
} catch (err) {
console.warn("[VideoAsset] failed to load preview:", err);
}
@@ -178,7 +241,12 @@ function _renderDetail(img) {
<label style="margin-top:10px;">Memo</label>
<textarea id="wfm-video-asset-memo" class="wfm-textarea" rows="3"></textarea>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-memo-save" style="margin-top:6px;">Save Memo</button>
<button type="button" class="wfm-btn wfm-btn-primary wfm-btn-sm" id="wfm-video-asset-open-gallery" style="width:100%;margin-top:12px;">Open in Gallery</button>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-send-to-edit" style="width:100%;margin-top:12px;">${t("videoEditSendToEdit")}</button>
<div id="wfm-video-asset-set-plan-image-row" style="display:${isVideoFile(img) ? "none" : "flex"};gap:6px;margin-top:6px;">
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-set-first" style="flex:1;">${t("videoAssetSetAsFirst")}</button>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-set-last" style="flex:1;">${t("videoAssetSetAsLast")}</button>
</div>
<button type="button" class="wfm-btn wfm-btn-primary wfm-btn-sm" id="wfm-video-asset-open-gallery" style="width:100%;margin-top:6px;">Open in Gallery</button>
`;
const nameEl = document.getElementById("wfm-video-asset-name");
@@ -205,6 +273,36 @@ function _renderDetail(img) {
img.memo = memo;
});
panel.querySelector("#wfm-video-asset-send-to-edit")?.addEventListener("click", async () => {
try {
const res = await fetch(`/wfm/gallery/image/serve?path=${encodeURIComponent(img.path)}`);
if (!res.ok) throw new Error(String(res.status));
const blob = await res.blob();
const file = new File([blob], img.filename, { type: blob.type || (isVideoFile(img) ? "video/mp4" : "image/png") });
addClipFromFile(file, img.filename);
document.querySelector('.wfm-video-center-panel .wfm-video-subtab-btn[data-video-subtab="edit"]')?.click();
showToast(t("videoEditClipSent", img.filename), "success");
} catch (err) {
showToast(t("errorWithMsg", err.message), "error");
}
});
const setAsPlanImage = async (which) => {
try {
const res = await fetch(`/wfm/gallery/image/serve?path=${encodeURIComponent(img.path)}`);
if (!res.ok) throw new Error(String(res.status));
const blob = await res.blob();
const file = new File([blob], img.filename, { type: blob.type || "image/png" });
await setBlockImageFromFile(which, file);
document.querySelector('.wfm-video-center-panel .wfm-video-subtab-btn[data-video-subtab="plan"]')?.click();
showToast(t(which === "first" ? "videoAssetSetAsFirstDone" : "videoAssetSetAsLastDone", img.filename), "success");
} catch (err) {
showToast(t("errorWithMsg", err.message), "error");
}
};
panel.querySelector("#wfm-video-asset-set-first")?.addEventListener("click", () => setAsPlanImage("first"));
panel.querySelector("#wfm-video-asset-set-last")?.addEventListener("click", () => setAsPlanImage("last"));
panel.querySelector("#wfm-video-asset-open-gallery")?.addEventListener("click", () => {
document.querySelector('.wfm-tab[data-tab="gallery"]')?.click();
const filterSel = document.getElementById("wfm-gallery-group-filter");
@@ -275,6 +373,17 @@ export async function refreshVideoAssetTab() {
await _loadImages();
}
function _setViewMode(mode) {
_s.viewMode = mode;
document.getElementById("wfm-video-asset-view-grid")?.classList.toggle("active", mode === "grid");
document.getElementById("wfm-video-asset-view-table")?.classList.toggle("active", mode === "table");
const grid = document.getElementById("wfm-video-asset-grid");
const tableWrap = document.getElementById("wfm-video-asset-table-wrap");
if (grid) grid.style.display = mode === "grid" ? "" : "none";
if (tableWrap) tableWrap.style.display = mode === "table" ? "" : "none";
_renderList();
}
export function initVideoAssetTab() {
document.getElementById("wfm-video-asset-group")?.addEventListener("change", (e) => {
_s.group = e.target.value;
@@ -284,6 +393,16 @@ export function initVideoAssetTab() {
_loadImages();
});
document.getElementById("wfm-video-asset-refresh")?.addEventListener("click", () => _loadImages());
document.getElementById("wfm-video-asset-kind")?.addEventListener("change", (e) => {
_s.kindFilter = e.target.value;
_renderList();
});
document.getElementById("wfm-video-asset-search")?.addEventListener("input", (e) => {
_s.searchQuery = e.target.value;
_renderList();
});
document.getElementById("wfm-video-asset-view-grid")?.addEventListener("click", () => _setViewMode("grid"));
document.getElementById("wfm-video-asset-view-table")?.addEventListener("click", () => _setViewMode("table"));
// Neither reserved group is ever auto-created by a batch run itself (see
// video-plan-tab.js's _ensureVideoAssetGroups, which only ensures
File diff suppressed because it is too large Load Diff
+23 -2
View File
@@ -290,10 +290,13 @@ function _wireBlockImageDropZone(which) {
});
}
// Returns true/false so setBlockImageFromFile (called from outside the Plan
// subtab, where this status text may not even be visible) can tell its own
// caller whether the upload actually succeeded instead of just assuming so.
async function _handleBlockImageUpload(which, file) {
if (!file || !file.type.startsWith("image/")) return;
if (!file || !file.type.startsWith("image/")) return false;
const block = _plan.blocks.find((b) => b.id === _selectedBlockId);
if (!block) return;
if (!block) return false;
const statusEl = document.getElementById(`wfm-video-block-${which}-status`);
const previewImg = document.getElementById(`wfm-video-block-${which}-preview`);
const wrap = document.getElementById(`wfm-video-block-${which}-wrap`);
@@ -309,11 +312,29 @@ async function _handleBlockImageUpload(which, file) {
if (which === "first") { block.first_image_filename = result.name; block.first_image_mode = "explicit"; }
else block.last_image_filename = result.name;
if (statusEl) { statusEl.textContent = `✓ ${result.name}`; statusEl.style.color = "var(--wfm-success)"; }
return true;
} catch (err) {
if (statusEl) { statusEl.textContent = `✗ ${err.message}`; statusEl.style.color = "var(--wfm-danger)"; }
return false;
}
}
// Exported so other modules (video-asset-tab.js's "Set as First/Last Image"
// buttons) can seed the currently-selected Plan block's First/Last Image from
// an already-fetched File — reuses the exact upload + first_image_mode
// bookkeeping the drop-zone UI itself uses, then re-renders the block editor
// so its dropdown/preview reflect the change even if the Plan subtab wasn't
// the one visible when this was called. _plan.blocks always has at least one
// entry (see _emptyBlock's initializer), so this only ever fails to find a
// block if _selectedBlockId itself has gone stale somehow.
export async function setBlockImageFromFile(which, file) {
const block = _plan.blocks.find((b) => b.id === _selectedBlockId);
if (!block) throw new Error(t("videoNoBlockSelected"));
const ok = await _handleBlockImageUpload(which, file);
_renderBlockEditor();
if (!ok) throw new Error(t("videoBlockImageUploadFailed"));
}
function _clearBlockImage(which) {
const block = _plan.blocks.find((b) => b.id === _selectedBlockId);
if (!block) return;
+45 -14
View File
@@ -8,25 +8,34 @@
* lets all three depend on it without needing to import each other —
* video-tab.js already imports both of the others for init/refresh, so a
* reverse import from either back into video-tab.js would create a cycle.
*
* Each pane also has a companion <img> element (wfm-video-*-preview-img) so a
* still/animated image (from Asset or a Video Source drop) can be previewed
* without forcing it through a <video> tag, which can't render image formats
* at all. mediaType controls which element is shown; Frame/GIF's
* getActivePreviewVideoElement() returns null while a pane is showing an
* image, since those tools only make sense against real video.
*/
const _PANES = {
source: { video: "wfm-video-source-preview-video", placeholder: "wfm-video-source-preview-placeholder" },
result: { video: "wfm-video-preview-video", placeholder: "wfm-video-preview-placeholder" },
source: { video: "wfm-video-source-preview-video", img: "wfm-video-source-preview-img", placeholder: "wfm-video-source-preview-placeholder" },
result: { video: "wfm-video-preview-video", img: "wfm-video-preview-img", placeholder: "wfm-video-preview-placeholder" },
};
const _objectUrls = { source: null, result: null };
// Whichever pane most recently received a video is what the Frame/GIF property
// Whichever pane most recently received media is what the Frame/GIF property
// tabs act on (see getActivePreviewSource/getActivePreviewVideoElement below) —
// an Asset selection or Video Source drop activates "source", a batch run
// finishing a block activates "result".
// finishing a block (or the Edit tab's export/preview) activates "result".
let _activePane = null; // "source" | "result" | null
let _activeSource = null;
let _activeMediaType = null; // "video" | "image" | null — mirrors _activePane's current content
function _setPreview(pane, url, source) {
function _setPreview(pane, url, source, mediaType = "video") {
const ids = _PANES[pane];
const video = document.getElementById(ids.video);
const img = document.getElementById(ids.img);
const placeholder = document.getElementById(ids.placeholder);
if (!video || !placeholder) return;
@@ -37,31 +46,44 @@ function _setPreview(pane, url, source) {
if (url && url.startsWith("blob:")) _objectUrls[pane] = url;
if (url) {
video.src = url;
video.style.display = "block";
if (mediaType === "image") {
video.pause();
video.removeAttribute("src");
video.load();
video.style.display = "none";
if (img) { img.src = url; img.style.display = "block"; }
} else {
if (img) { img.removeAttribute("src"); img.style.display = "none"; }
video.src = url;
video.style.display = "block";
}
placeholder.style.display = "none";
_activePane = pane;
_activeSource = source;
_activeMediaType = mediaType;
} else {
video.pause();
video.removeAttribute("src");
video.load();
video.style.display = "none";
if (img) { img.removeAttribute("src"); img.style.display = "none"; }
placeholder.style.display = "";
if (_activePane === pane) { _activePane = null; _activeSource = null; }
if (_activePane === pane) { _activePane = null; _activeSource = null; _activeMediaType = null; }
}
}
// Sets (or clears, with url=null) the Asset-selection/Video-Source-drop pane.
export function setSourcePreview(url, source) { _setPreview("source", url, source); }
// mediaType: "video" (default) or "image".
export function setSourcePreview(url, source, mediaType = "video") { _setPreview("source", url, source, mediaType); }
// Sets (or clears) the generated-result pane — called once per block as a
// batch run's video output comes in.
export function setResultPreview(url, source) { _setPreview("result", url, source); }
// batch run's video output comes in, and by the Edit tab's timeline preview
// (which may show either a video clip or a held still image).
export function setResultPreview(url, source, mediaType = "video") { _setPreview("result", url, source, mediaType); }
// What the Frame/GIF property tabs operate on: { kind: "output"|"input", filename,
// subfolder, type } for anything already on the server, or { kind: "local", file }
// for a picked/dropped file not yet uploaded. null if neither pane has a video.
// for a picked/dropped file not yet uploaded. null if neither pane has media.
export function getActivePreviewSource() { return _activeSource; }
// The GIF tab uploads a still-local source on first use and needs to remember
@@ -70,9 +92,10 @@ export function getActivePreviewSource() { return _activeSource; }
export function updateActivePreviewSourceRef(source) { _activeSource = source; }
// The <video> element the Frame/GIF tools should read/capture from, or null if
// neither pane currently has a video loaded.
// neither pane currently has a video loaded (including when the active pane is
// showing a still image instead).
export function getActivePreviewVideoElement() {
if (!_activePane) return null;
if (!_activePane || _activeMediaType === "image") return null;
return document.getElementById(_PANES[_activePane].video);
}
@@ -80,3 +103,11 @@ export function getActivePreviewVideoElement() {
export function getAllPreviewVideoElements() {
return Object.values(_PANES).map((ids) => document.getElementById(ids.video)).filter(Boolean);
}
// Direct access to the "result" pane's <video> element — used by
// video-edit-tab.js's timeline preview, which needs to drive playback
// (seek to a clip's trim-in, listen for the trim-out point, swap to the
// next clip) rather than just set a src via setResultPreview().
export function getResultPreviewVideoElement() {
return document.getElementById(_PANES.result.video);
}
+67 -18
View File
@@ -1,19 +1,57 @@
/**
* Video Tab - Sidebar "Project" subtab (next to Asset): browses saved Video
* Plan files (ws_videoplan_*.json in video_plan/), independent of which plan
* (if any) is currently loaded in the Plan editor.
* Plan files (ws_videoplan_*.json in video_plan/) OR saved Video Edit
* projects (ws_videoeditproj_*.json in video_edit_project/), depending on
* which center subtab (Plan/Edit) is currently active — video-tab.js passes
* that mode in on every refresh (Asset stays on whichever of the two was
* last active, since it has no saved-list concept of its own).
*
* Deliberately thin, mirroring video-asset-tab.js's own scope — clicking an
* item hands off to video-plan-tab.js's openSavedVideoPlan() rather than
* re-implementing plan loading here.
* item hands off to video-plan-tab.js's openSavedVideoPlan() or
* video-edit-tab.js's openSavedVideoEditProject() rather than
* re-implementing plan/project loading here.
*/
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./util.js";
import { openSavedVideoPlan } from "./video-plan-tab.js";
import { openSavedVideoEditProject } from "./video-edit-tab.js";
// Per-mode config: which endpoints to hit, which loader/toast strings to use,
// and how to read each item's count field — keeps _renderList/_makeItem/etc.
// mode-agnostic instead of branching throughout.
const _MODES = {
plan: {
listUrl: "/api/wfm/video/plans",
deleteUrl: "/api/wfm/video/plans/delete",
open: (filename) => openSavedVideoPlan(filename),
countOf: (item) => item.block_count ?? 0,
countLabel: (n) => t("videoProjectBlockCount", n),
hintKey: "videoProjectHint",
emptyKey: "videoProjectEmpty",
deleteConfirmKey: "videoProjectDeleteConfirm",
deletedKey: "videoProjectDeleted",
deleteFailedKey: "videoProjectDeleteFailed",
loadListFailedKey: "videoProjectLoadListFailed",
},
edit: {
listUrl: "/api/wfm/video/edit/projects",
deleteUrl: "/api/wfm/video/edit/projects/delete",
open: (filename) => openSavedVideoEditProject(filename),
countOf: (item) => item.clip_count ?? 0,
countLabel: (n) => t("videoEditProjectClipCount", n),
hintKey: "videoEditProjectHint",
emptyKey: "videoEditProjectEmpty",
deleteConfirmKey: "videoEditProjectDeleteConfirm",
deletedKey: "videoEditProjectDeleted",
deleteFailedKey: "videoEditProjectDeleteFailed",
loadListFailedKey: "videoEditProjectLoadListFailed",
},
};
const _s = {
mode: "plan",
plans: [],
activeFilename: null,
loaded: false, // becomes true once the Project subtab has been shown at least once
@@ -29,8 +67,9 @@ function _formatUpdatedAt(iso) {
function _renderList() {
const list = document.getElementById("wfm-video-project-list");
if (!list) return;
const cfg = _MODES[_s.mode];
if (_s.plans.length === 0) {
list.innerHTML = `<span class="wfm-placeholder">${t("videoProjectEmpty")}</span>`;
list.innerHTML = `<span class="wfm-placeholder">${t(cfg.emptyKey)}</span>`;
return;
}
list.innerHTML = "";
@@ -38,6 +77,7 @@ function _renderList() {
}
function _makeItem(plan) {
const cfg = _MODES[_s.mode];
const item = document.createElement("div");
item.className = "wfm-video-project-item" + (plan.filename === _s.activeFilename ? " active" : "");
item.dataset.filename = plan.filename;
@@ -58,9 +98,9 @@ function _makeItem(plan) {
const meta = document.createElement("div");
meta.className = "wfm-video-project-item-meta";
const blockLabel = t("videoProjectBlockCount", plan.block_count ?? 0);
const countLabel = cfg.countLabel(cfg.countOf(plan));
const updated = _formatUpdatedAt(plan.updated_at);
meta.textContent = updated ? `${blockLabel} · ${updated}` : blockLabel;
meta.textContent = updated ? `${countLabel} · ${updated}` : countLabel;
meta.title = plan.note || "";
info.append(name, meta);
@@ -83,13 +123,14 @@ function _makeItem(plan) {
async function _openPlan(plan) {
_s.activeFilename = plan.filename;
_renderList();
await openSavedVideoPlan(plan.filename);
await _MODES[_s.mode].open(plan.filename);
}
async function _deletePlan(plan) {
if (!window.confirm(t("videoProjectDeleteConfirm", plan.name || plan.filename))) return;
const cfg = _MODES[_s.mode];
if (!window.confirm(t(cfg.deleteConfirmKey, plan.name || plan.filename))) return;
try {
const res = await fetch("/api/wfm/video/plans/delete", {
const res = await fetch(cfg.deleteUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: plan.filename }),
@@ -97,26 +138,34 @@ async function _deletePlan(plan) {
const json = await res.json();
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
if (_s.activeFilename === plan.filename) _s.activeFilename = null;
showToast(t("videoProjectDeleted"), "success");
await refreshVideoProjectTab();
showToast(t(cfg.deletedKey), "success");
await refreshVideoProjectTab(_s.mode);
} catch (err) {
showToast(`${t("videoProjectDeleteFailed")}: ${err.message}`, "error");
showToast(`${t(cfg.deleteFailedKey)}: ${err.message}`, "error");
}
}
// Called whenever the Project subtab becomes active (see video-tab.js's prop-tab
// toggle) — always refetches rather than caching, so a plan saved elsewhere
// (Plan pane's Save/Save As) shows up as soon as the user switches over to look.
export async function refreshVideoProjectTab() {
// toggle) — always refetches rather than caching, so a plan/project saved
// elsewhere (Plan pane's Save/Save As, Edit's Save/Save As) shows up as soon
// as the user switches over to look. `mode` ("plan"|"edit") is video-tab.js's
// record of which center subtab is currently active; defaults to "plan" so
// direct calls (e.g. from initVideoProjectTab) keep the original behavior.
export async function refreshVideoProjectTab(mode = "plan") {
const list = document.getElementById("wfm-video-project-list");
if (!list) return;
_s.mode = mode === "edit" ? "edit" : "plan";
_s.activeFilename = null;
const cfg = _MODES[_s.mode];
const hintEl = document.getElementById("wfm-video-project-hint");
if (hintEl) hintEl.textContent = t(cfg.hintKey);
try {
const res = await fetch("/api/wfm/video/plans");
const res = await fetch(cfg.listUrl);
if (!res.ok) throw new Error(String(res.status));
_s.plans = await res.json();
_renderList();
} catch (err) {
list.innerHTML = `<span class="wfm-placeholder" style="color:var(--wfm-danger)">${escapeHtml(t("videoProjectLoadListFailed"))}: ${escapeHtml(err.message)}</span>`;
list.innerHTML = `<span class="wfm-placeholder" style="color:var(--wfm-danger)">${escapeHtml(t(cfg.loadListFailedKey))}: ${escapeHtml(err.message)}</span>`;
}
_s.loaded = true;
}
+112 -12
View File
@@ -17,6 +17,8 @@ import { applyStoredVideoVolume } from "./settings-tab.js";
import { initVideoPlanTab, loadWorkflowIntoVideoEditor } from "./video-plan-tab.js";
import { initVideoAssetTab, refreshVideoAssetTab } from "./video-asset-tab.js";
import { initVideoProjectTab, refreshVideoProjectTab } from "./video-project-tab.js";
import { initVideoEditTab, addClipFromFile } from "./video-edit-tab.js";
import { VIDEO_GROUP, ensureVideoGroup } from "./gallery-tab.js";
import {
setSourcePreview, getActivePreviewSource, updateActivePreviewSourceRef,
getActivePreviewVideoElement, getAllPreviewVideoElements,
@@ -26,14 +28,25 @@ export { loadWorkflowIntoVideoEditor };
// ============================================
// Subtab switching — two INDEPENDENT groups sharing the same .wfm-video-subtab-*
// look, kept deliberately uncoupled: the sidebar's Asset/Project pair collapses
// entirely when re-clicked (nothing needs to always be visible there — Video
// Source covers the "load something to work with" case), while the center
// Plan/Edit pair is a "always exactly one visible" 2-way tab. Querying each
// look, kept deliberately uncoupled: the sidebar's Project button collapses its
// panel entirely when re-clicked (nothing needs to always be visible there —
// Video Source covers the "load something to work with" case), while the center
// Plan/Edit/Asset trio is a "always exactly one visible" tab group. Querying each
// scoped to its own container (rather than one global querySelectorAll) is
// what keeps clicking one from affecting the other.
// ============================================
// Which of the center Plan/Edit subtabs was last active — the sidebar
// Project panel uses this to decide whether to list saved Video Plans or
// saved Video Edit projects (Asset has no saved-list concept, so selecting
// it leaves this unchanged rather than resetting to "plan").
let _centerSubtabMode = "plan";
function _isProjectPanelOpen() {
const panel = document.getElementById("wfm-video-subtab-project");
return !!panel && panel.style.display !== "none";
}
function _initCenterSubtabToggle() {
const scope = document.querySelector(".wfm-video-center-panel");
if (!scope) return;
@@ -45,13 +58,20 @@ function _initCenterSubtabToggle() {
scope.querySelectorAll(".wfm-video-subtab-panel").forEach((p) => {
p.style.display = p.dataset.videoSubtabPanel === target ? "" : "none";
});
if (target === "asset") refreshVideoAssetTab();
if (target === "plan" || target === "edit") {
_centerSubtabMode = target;
// Project panel may already be open (e.g. switching Plan<->Edit
// while it's pinned) — keep its list in sync with the new mode.
if (_isProjectPanelOpen()) refreshVideoProjectTab(_centerSubtabMode);
}
});
});
}
// Sidebar Asset/Project pair: clicking a button shows its panel and hides the
// other; clicking the already-active button collapses it (both panels hidden),
// letting the video preview reclaim the sidebar's width when neither is needed.
// Sidebar Project button: clicking it shows its panel; clicking it again while
// active collapses it, letting the video preview reclaim the sidebar's width
// when it isn't needed.
function _initSidebarSubtabToggle() {
const sidebar = document.querySelector(".wfm-video-form-panel");
if (!sidebar) return;
@@ -66,8 +86,7 @@ function _initSidebarSubtabToggle() {
if (willShow) {
btn.classList.add("active");
panel.style.display = "";
if (target === "asset") refreshVideoAssetTab();
else if (target === "project") refreshVideoProjectTab();
if (target === "project") refreshVideoProjectTab(_centerSubtabMode);
}
});
});
@@ -103,7 +122,17 @@ function _applyVideoI18n() {
const el = document.getElementById(id);
if (el) el.textContent = t(key);
};
setText("wfm-video-edit-placeholder-label", "videoEditPlaceholder");
const setTitle = (id, key) => {
const el = document.getElementById(id);
if (el) el.title = t(key);
};
setText("wfm-video-edit-export-btn", "videoEditExportBtn");
setTitle("wfm-video-edit-move-left-btn", "videoEditMoveLeft");
setTitle("wfm-video-edit-move-right-btn", "videoEditMoveRight");
setText("wfm-video-edit-duplicate-btn", "videoEditDuplicate");
setText("wfm-video-edit-delete-btn", "videoEditDelete");
setText("wfm-video-edit-clear-btn", "videoEditClearBtn");
setText("wfm-video-source-add-to-edit", "videoSourceAddToEdit");
setText("wfm-video-source-label", "videoSourceLabel");
setText("wfm-video-source-hint", "videoSourceHint");
setText("wfm-video-source-drop-label", "videoSourceDropLabel");
@@ -131,12 +160,40 @@ function _applyVideoI18n() {
function _wireVideoSourcePanel() {
_wireDropZone("wfm-video-source-drop", "wfm-video-source-file", (file) => {
if (!file.type.startsWith("video/")) return;
const isVideo = file.type.startsWith("video/");
const isImage = file.type.startsWith("image/");
if (!isVideo && !isImage) return;
const statusEl = document.getElementById("wfm-video-source-status");
setSourcePreview(URL.createObjectURL(file), { kind: "local", file });
setSourcePreview(URL.createObjectURL(file), { kind: "local", file }, isVideo ? "video" : "image");
if (statusEl) { statusEl.textContent = file.name; statusEl.style.color = ""; }
});
// "Add to Edit" — explicit, user-triggered handoff from whatever is loaded
// in the Source pane (a plain drop, an Asset selection, a Frame/GIF-tool
// source) into the Edit subtab's clip timeline. Deliberately NOT automatic
// on every Source drop — Frame/GIF is a single-video tool the user reaches
// for far more often than multi-clip editing, so auto-adding would clutter
// the Edit timeline with videos the user never meant to combine.
document.getElementById("wfm-video-source-add-to-edit")?.addEventListener("click", async () => {
const ref = getActivePreviewSource();
if (!ref) { showToast(t("videoNoSourceLoaded"), "error"); return; }
try {
let file;
if (ref.kind === "local") {
file = ref.file;
} else {
const blob = await comfyUI.getImageBlob(ref);
const isVideoExt = /\.(mp4|webm|mkv|mov)$/i.test(ref.filename || "");
file = new File([blob], ref.filename, { type: blob.type || (isVideoExt ? "video/mp4" : "image/png") });
}
addClipFromFile(file);
document.querySelector('.wfm-video-center-panel .wfm-video-subtab-btn[data-video-subtab="edit"]')?.click();
showToast(t("videoEditClipSent", file.name), "success");
} catch (err) {
showToast(t("errorWithMsg", err.message), "error");
}
});
document.getElementById("wfm-video-source-clear")?.addEventListener("click", () => {
setSourcePreview(null, null);
const statusEl = document.getElementById("wfm-video-source-status");
@@ -170,6 +227,7 @@ function _initPropTabs() {
// ============================================
let _capturedFrameBlob = null;
let _frameOutputDir = "";
function _blobToDataUrl(blob) {
return new Promise((resolve, reject) => {
@@ -180,6 +238,44 @@ function _blobToDataUrl(blob) {
});
}
// Same small output-dir lookup each of video-asset-tab.js/video-plan-tab.js/
// video-edit-tab.js keeps its own copy of — needed to build the absolute path
// Gallery's group-tagging API expects.
async function _fetchFrameOutputDir() {
if (_frameOutputDir) return;
try {
const res = await fetch("/api/wfm/settings/output-dir");
if (res.ok) {
const data = await res.json();
_frameOutputDir = (data.current || "").replace(/\\/g, "/").replace(/\/$/, "");
}
} catch { /* non-critical */ }
}
// Opt-in tagging of a saved frame into Gallery's curated __Video Assets__
// group (see the "Add to Video Assets group" checkbox) — mirrors
// video-edit-tab.js's _addOutputToVideoTemp, but targets the curated group
// directly since a manually-captured frame is a deliberate keep, not the
// every-run scratch output __Video Temp__ exists for.
async function _addFrameToVideoAssets(filename, subfolder) {
await _fetchFrameOutputDir();
if (!_frameOutputDir) return;
const parts = [_frameOutputDir];
if (subfolder) parts.push(subfolder);
parts.push(filename);
const path = parts.join("/");
try {
await ensureVideoGroup();
await fetch(`/wfm/gallery/groups/${encodeURIComponent(VIDEO_GROUP)}/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path }),
});
} catch (err) {
console.warn("[VideoTab] failed to tag frame into video group:", err);
}
}
function _initFrameTab() {
const captureBtn = document.getElementById("wfm-video-frame-capture-btn");
const saveBtn = document.getElementById("wfm-video-frame-save-btn");
@@ -226,6 +322,9 @@ function _initFrameTab() {
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
if (statusEl) { statusEl.textContent = `✓ ${json.filename}`; statusEl.style.color = "var(--wfm-success)"; }
showToast(t("videoFrameSaved", json.filename), "success");
if (document.getElementById("wfm-video-frame-add-to-assets")?.checked) {
await _addFrameToVideoAssets(json.filename, json.subfolder);
}
} catch (err) {
if (statusEl) { statusEl.textContent = `✗ ${err.message}`; statusEl.style.color = "var(--wfm-danger)"; }
showToast(t("errorWithMsg", err.message), "error");
@@ -321,6 +420,7 @@ export function initVideoTab() {
initVideoPlanTab();
initVideoAssetTab();
initVideoProjectTab();
initVideoEditTab();
_wireVideoSourcePanel();
_initPropTabs();
+32 -1
View File
@@ -31,6 +31,7 @@ const state = {
groupFilter: "", // empty = all groups
currentPage: 0,
showBatchOnly: false,
sortBy: localStorage.getItem("wfm_workflow_sort") || "date_desc",
};
@@ -449,6 +450,26 @@ function filterWorkflows() {
});
}
function sortWorkflows(list) {
const sorted = [...list];
switch (state.sortBy) {
case "date_asc":
sorted.sort((a, b) => a.mtime - b.mtime);
break;
case "name_asc":
sorted.sort((a, b) => a.filename.localeCompare(b.filename));
break;
case "name_desc":
sorted.sort((a, b) => b.filename.localeCompare(a.filename));
break;
case "date_desc":
default:
sorted.sort((a, b) => b.mtime - a.mtime);
break;
}
return sorted;
}
// ============================================
// Grid Rendering (3 view modes)
// ============================================
@@ -459,7 +480,7 @@ function renderGrid() {
grid.className = `wfm-grid wfm-view-${state.viewMode}`;
const filtered = filterWorkflows();
const filtered = sortWorkflows(filterWorkflows());
if (filtered.length === 0) {
grid.innerHTML =
@@ -1365,6 +1386,16 @@ export function initWorkflowTab() {
renderGrid();
});
// Toolbar: sort dropdown
const sortSel = document.getElementById("wfm-sort");
if (sortSel) sortSel.value = state.sortBy;
sortSel?.addEventListener("change", (e) => {
state.sortBy = e.target.value;
localStorage.setItem("wfm_workflow_sort", state.sortBy);
state.currentPage = 0;
renderGrid();
});
// Toolbar: Load in GenerateUI button
document.getElementById("wfm-list-load-btn")?.addEventListener("click", async () => {
if (!state.selectedWf) return;
+96 -20
View File
@@ -55,6 +55,12 @@
<select id="wfm-group-filter" class="wfm-select wfm-toolbar-select">
<option value="">All Groups</option>
</select>
<select id="wfm-sort" class="wfm-select wfm-toolbar-select">
<option value="date_desc">Date (Newest)</option>
<option value="date_asc">Date (Oldest)</option>
<option value="name_asc">Name A-Z</option>
<option value="name_desc">Name Z-A</option>
</select>
<button id="wfm-list-load-btn" class="wfm-btn wfm-btn-sm wfm-btn-primary" disabled title="Select a card first">Load in GenerateUI</button>
<button id="wfm-list-load-video-btn" class="wfm-btn wfm-btn-sm" disabled title="Select a card first">Load in Video</button>
<button id="wfm-list-open-comfyui-btn" class="wfm-btn wfm-btn-sm" disabled title="Select a card first">Send to Canvas</button>
@@ -2099,7 +2105,7 @@
<div class="wfm-help-page" id="wfm-help-page-video">
<h2 id="wfm-help-video-title">Video Tab</h2>
<p id="wfm-help-video-desc">A dedicated video generation tab for MiniMax H3 (Image to Video), LTX-2.5 (Text / Image / First & Last Frame to Video), and Wan2.2 (Text to Video) workflows, independent of the GenerateUI tab. The center panel's Plan / Edit subtabs handle batch generation and (future) editing; the sidebar's Asset / Project subtabs handle browsing generated videos and saved plans.</p>
<p id="wfm-help-video-desc">A dedicated video generation tab for MiniMax H3 (Image to Video), LTX-2.5 (Text / Image / First & Last Frame to Video), and Wan2.2 (Text to Video) workflows, independent of the GenerateUI tab. The center panel's Plan / Edit / Asset subtabs handle batch generation, clip editing, and browsing generated videos; the sidebar's Project subtab handles saved plans.</p>
<div class="wfm-help-card">
<ul>
<li id="wfm-help-video-1">Plan subtab: the topbar's settings row holds Aspect Ratio / Megapixels / Multiple / Duration (seconds) / Noise Seed as this plan's base settings — Duration always reflects the currently selected block (each block keeps its own value), the rest are plan-wide. A second row below adds Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance, each enabled only for workflows that actually support it (see helpVideo2). Split the timeline into blocks with the + Split / + Block / Delete buttons — each block has its own Prompt, Duration, and First/Last Image — First Image can be None, an explicit image, or "use previous block's last frame" (an explicit Last Image on the previous block always wins over extracting one from its generated video). Click Run to generate every block in order; Pause/Cancel work mid-run.</li>
@@ -2107,11 +2113,13 @@
<li id="wfm-help-video-3">Use the "Load in Video" button in the Workflow tab's detail modal to load a workflow containing a MiniMax H3 Image-to-Video node, an LTX-2.5 Text/Image/First&Last-Frame-to-Video node, or a Wan2.2 Text-to-Video node — this starts a fresh single-block plan seeded from the workflow's own current values. Unsupported workflows show an error instead.</li>
<li id="wfm-help-video-4">Generated videos (mp4) automatically appear in the Asset subtab's Video Temp group, and can also be browsed and played back in the Gallery tab. Organizing keepers into Video Assets is entirely up to you, on your own schedule.</li>
<li id="wfm-help-video-5">The center panel shows two previews: the left one shows either the video you select in the Asset subtab or whatever you drop/pick in the Video Source panel on its left (Clear resets it to empty), letting you use the Frame/GIF tools without running the loaded video workflow first; the right one always shows the most recently generated clip.</li>
<li id="wfm-help-video-6">Frame tab (right panel): play/seek the preview video to the position you want, click Capture Frame to grab that frame client-side, then Save to Output to write it as a PNG into ComfyUI's output folder.</li>
<li id="wfm-help-video-6">Frame tab (right panel): play/seek the preview video to the position you want, click Capture Frame to grab that frame client-side, then Save to Output to write it as a PNG into ComfyUI's output folder — check "Add to Video Assets group" first to also tag the saved frame into Gallery's Video Assets group.</li>
<li id="wfm-help-video-7">GIF tab (right panel): set Start/End (seconds), FPS, and an optional Max Width, then Convert to GIF — the server decodes the video and encodes an animated GIF into ComfyUI's output folder.</li>
<li id="wfm-help-video-8">Chaining: setting a block's First Image to "use previous block's last frame" continues smoothly from the previous clip — if that previous block has its own explicit Last Image, that image is used directly; otherwise the actual last frame of its generated video is extracted automatically.</li>
<li id="wfm-help-video-9">Project tab (sidebar, next to Asset): browse every saved plan file (thumbnail, block count, last updated), click one to load it into the Plan editor, or delete it with the × button; clicking the same tab again collapses the panel.</li>
<li id="wfm-help-video-9">Project tab (sidebar): browse every saved plan file (thumbnail, block count, last updated), click one to load it into the Plan editor, or delete it with the × button; clicking the same tab again collapses the panel.</li>
<li id="wfm-help-video-10">Highlight color: the Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple / Noise Seed / Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance labels turn the same highlight color as the GenerateUI Model tab (customizable in the Settings tab) when the loaded workflow actually supports that field — e.g. for a Wan2.2 workflow (no ResolutionSelector node), the Aspect Ratio / Megapixels / Multiple labels stay unhighlighted while Width / Height light up instead.</li>
<li id="wfm-help-video-11">Edit subtab: a lightweight clip timeline for trimming and concatenating video/image clips outside of generation. Add clips via the Video Source panel's "Add to Edit" button or the Asset subtab's "Send to Edit" button, then drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, and Clear to empty the timeline. The trim panel for the selected video clip shows a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges — image clips instead get a simple hold-duration field. ▶ Preview plays every ready clip back-to-back (trimmed to its in/out points) in the right-hand result preview; Export builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into the Video Temp group automatically. Exporting more than one clip currently drops audio, since ComfyUI's concatenation node requires uniform audio across all inputs.</li>
<li id="wfm-help-video-12">Asset subtab (center panel, next to Plan/Edit): browses every video/image tagged into Video Assets or Video Temp (via the group dropdown, or "All Video Assets" for both). Search filters by filename; the ▦/☰ buttons switch between a thumbnail grid and a table (filename/updated/tags). Selecting an item shows its tags/memo, loads it into the center preview, and offers "Send to Edit" (queues it as a clip in the Edit subtab) plus, for images only, "Set as First Image"/"Set as Last Image" (uploads it straight into the currently-selected Plan block, switching to the Plan subtab).</li>
</ul>
</div>
<button class="wfm-help-jump-btn wfm-btn wfm-btn-sm" data-jump-tab="video">Go to Video Tab</button>
@@ -2717,23 +2725,9 @@
<div class="wfm-video-layout">
<div class="wfm-video-form-panel">
<div class="wfm-video-subtab-nav">
<button type="button" class="wfm-video-subtab-btn" data-video-subtab="asset" id="wfm-video-subtab-btn-asset">Asset</button>
<button type="button" class="wfm-video-subtab-btn" data-video-subtab="project" id="wfm-video-subtab-btn-project">Project</button>
</div>
<!-- Asset: このPlanが生成した動画の素材管理(Galleryのグループ機能を再利用) -->
<div id="wfm-video-subtab-asset" class="wfm-video-subtab-panel" data-video-subtab-panel="asset" style="display:none;">
<div style="display:flex;gap:6px;align-items:center;">
<select id="wfm-video-asset-group" class="wfm-input" style="flex:1;"></select>
<button type="button" id="wfm-video-asset-refresh" class="wfm-btn wfm-btn-sm" title="Refresh">↻</button>
</div>
<span id="wfm-video-asset-status" class="wfm-i2i-status"></span>
<div class="wfm-video-asset-grid" id="wfm-video-asset-grid"></div>
<div class="wfm-video-asset-detail" id="wfm-video-asset-detail">
<span class="wfm-placeholder">Select a video</span>
</div>
</div>
<!-- Project: 保存済みVideo Plan一覧(クリックでPlanエディタへ読み込み) -->
<div id="wfm-video-subtab-project" class="wfm-video-subtab-panel" data-video-subtab-panel="project" style="display:none;">
<div style="display:flex;gap:6px;align-items:center;">
@@ -2753,27 +2747,71 @@
<div class="wfm-i2i-drop-zone" id="wfm-video-source-drop">
<label class="wfm-i2i-drop-label">
<span id="wfm-video-source-drop-label">Drop video or click to select</span>
<input type="file" accept="video/*" id="wfm-video-source-file" style="display:none;">
<input type="file" accept="video/*,image/*" id="wfm-video-source-file" style="display:none;">
</label>
</div>
<div style="display:flex;gap:6px;align-items:center;margin-top:6px;">
<span class="wfm-i2i-status" id="wfm-video-source-status" style="flex:1;"></span>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-source-add-to-edit">Add to Edit</button>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-source-clear">Clear</button>
</div>
</div>
<div class="wfm-video-preview-frame" id="wfm-video-source-preview-frame">
<span class="wfm-placeholder" id="wfm-video-source-preview-placeholder">Asset / Source Preview</span>
<video id="wfm-video-source-preview-video" class="wfm-video-preview-el" controls style="display:none;"></video>
<img id="wfm-video-source-preview-img" class="wfm-video-preview-el" style="display:none;">
</div>
<div class="wfm-video-preview-frame" id="wfm-video-preview-frame">
<span class="wfm-placeholder" id="wfm-video-preview-placeholder">Generated Video Preview</span>
<video id="wfm-video-preview-video" class="wfm-video-preview-el" controls style="display:none;"></video>
<img id="wfm-video-preview-img" class="wfm-video-preview-el" style="display:none;">
</div>
</div>
<div class="wfm-video-subtab-nav wfm-video-center-subtab-nav">
<button type="button" class="wfm-video-subtab-btn active" data-video-subtab="plan">Plan</button>
<button type="button" class="wfm-video-subtab-btn" data-video-subtab="edit">Edit</button>
<button type="button" class="wfm-video-subtab-btn" data-video-subtab="asset" id="wfm-video-subtab-btn-asset">Asset</button>
</div>
<!-- Asset: このPlanが生成した動画の素材管理(Galleryのグループ機能を再利用)。
左=一覧(サムネイル/テーブル切替可) / 右=選択ファイルの詳細(タグ・メモ等) -->
<div id="wfm-video-subtab-asset" class="wfm-video-subtab-panel wfm-video-asset-content" data-video-subtab-panel="asset" style="display:none;">
<div class="wfm-video-asset-layout">
<div class="wfm-video-asset-list-col">
<div class="wfm-video-asset-toolbar">
<select id="wfm-video-asset-group" class="wfm-input" style="flex:1;"></select>
<select id="wfm-video-asset-kind" class="wfm-input" style="flex:0.5;">
<option value="all">All Types</option>
<option value="video">Video Only</option>
<option value="image">Image Only</option>
</select>
<input type="text" id="wfm-video-asset-search" class="wfm-input" placeholder="Search filename..." style="flex:1;">
<button type="button" id="wfm-video-asset-view-grid" class="wfm-btn wfm-btn-sm wfm-video-asset-view-btn active" title="Thumbnail view">▦</button>
<button type="button" id="wfm-video-asset-view-table" class="wfm-btn wfm-btn-sm wfm-video-asset-view-btn" title="Table view">☰</button>
<button type="button" id="wfm-video-asset-refresh" class="wfm-btn wfm-btn-sm" title="Refresh">↻</button>
</div>
<span id="wfm-video-asset-status" class="wfm-i2i-status"></span>
<div class="wfm-video-asset-grid" id="wfm-video-asset-grid"></div>
<div class="wfm-video-asset-table-wrap" id="wfm-video-asset-table-wrap" style="display:none;">
<table class="wfm-video-asset-table">
<thead>
<tr>
<th>Filename</th>
<th>Updated</th>
<th>Tags</th>
</tr>
</thead>
<tbody id="wfm-video-asset-table-body"></tbody>
</table>
</div>
</div>
<div class="wfm-video-asset-detail-col">
<div class="wfm-video-asset-detail" id="wfm-video-asset-detail">
<span class="wfm-placeholder">Select a video</span>
</div>
</div>
</div>
</div>
<!-- Plan: バッチ動画生成のタイムライン編集・実行(中央下部、Planタブ選択時のみ表示) -->
@@ -2891,9 +2929,44 @@
</div>
</div>
<!-- Edit: 将来の動画編集機能用プレースホルダー -->
<!-- Edit: 簡易動画編集(クリップのタイムライン配置・トリム・結合書き出し)。
プレビューは上部のSource/Resultペインを再利用する(専用videoは持たない)。
タイムライン+下の共有ツールバーはPlan subtabの.wfm-video-timeline-*と同じ構造
(個別行ボタンではなく「選択中クリップに対して働く」共通ボタン方式)。 -->
<div id="wfm-video-subtab-edit" class="wfm-video-subtab-panel wfm-video-edit-panel" data-video-subtab-panel="edit" style="display:none;">
<span id="wfm-video-edit-placeholder-label" class="wfm-placeholder">Editing tools coming soon</span>
<div class="wfm-video-timeline-wrap">
<div class="wfm-video-edit-timeline-track" id="wfm-video-edit-timeline-track"></div>
<div class="wfm-video-timeline-toolbar">
<button type="button" id="wfm-video-edit-move-left-btn" class="wfm-btn wfm-btn-xs" title="Move left">◀</button>
<button type="button" id="wfm-video-edit-move-right-btn" class="wfm-btn wfm-btn-xs" title="Move right">▶</button>
<button type="button" id="wfm-video-edit-duplicate-btn" class="wfm-btn wfm-btn-xs">Duplicate</button>
<button type="button" id="wfm-video-edit-delete-btn" class="wfm-btn wfm-btn-xs">Delete</button>
<button type="button" id="wfm-video-edit-preview-btn" class="wfm-btn wfm-btn-xs">▶ Preview</button>
<span class="wfm-i2i-status wfm-video-edit-total-duration" id="wfm-video-edit-total-duration" style="margin-left:auto;"></span>
<button type="button" id="wfm-video-edit-clear-btn" class="wfm-btn wfm-btn-xs wfm-btn-danger">Clear</button>
</div>
</div>
<div class="wfm-video-edit-main-row">
<div class="wfm-video-edit-trim-panel" id="wfm-video-edit-trim-panel"></div>
<div class="wfm-video-edit-export-panel">
<div id="wfm-video-edit-project-name" class="wfm-i2i-status" style="min-height:14px;margin-bottom:4px;"></div>
<div style="display:flex;gap:6px;">
<button type="button" id="wfm-video-edit-save-btn" class="wfm-btn wfm-btn-sm" style="flex:1;">Save</button>
<button type="button" id="wfm-video-edit-saveas-btn" class="wfm-btn wfm-btn-sm" style="flex:1;">Save As</button>
<button type="button" id="wfm-video-edit-load-btn" class="wfm-btn wfm-btn-sm" style="flex:1;">Load</button>
</div>
<input type="file" id="wfm-video-edit-load-file-input" accept="application/json,.json" style="display:none;">
<button type="button" id="wfm-video-edit-export-btn" class="wfm-btn wfm-btn-primary" style="width:100%;margin-top:8px;">Export</button>
<div class="wfm-gen-progress-container" style="margin-top:8px;">
<div class="wfm-gen-progress-track">
<div id="wfm-video-edit-progress-bar" class="wfm-gen-progress-bar"></div>
</div>
<span id="wfm-video-edit-progress-text" class="wfm-gen-progress-text">Ready</span>
</div>
</div>
</div>
</div>
</div>
<!-- 右ペイン: プロパティ(Frame抽出 / GIF変換の2タブ)。中央ペインと同様に常時表示 -->
@@ -2909,6 +2982,9 @@
<div class="wfm-i2i-preview-wrap" id="wfm-video-frame-preview-wrap" style="display:none;">
<img class="wfm-i2i-preview-img" id="wfm-video-frame-preview-img">
</div>
<label class="wfm-video-checkbox-inline" style="margin-top:6px;">
<input type="checkbox" id="wfm-video-frame-add-to-assets"> Add to Video Assets group
</label>
<button type="button" id="wfm-video-frame-save-btn" class="wfm-btn wfm-btn-primary wfm-btn-sm" style="width:100%;margin-top:6px;" disabled>Save to Output</button>
<span class="wfm-i2i-status" id="wfm-video-frame-status"></span>
</div>