release: v0.5.5 — Videoタブ全面刷新(Plan/Asset/Editバッチ生成 + Projectサブタブ + プレビュー2ペイン化) + Video Assets/Video Temp運用整理 + Galleryメタデータ消失バグ根本修正
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
426f27c05a
commit
b09cd2ef90
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
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.
|
||||
|
||||
- **2026-08-30: Videoタブに Plan/Asset/Edit のバッチ動画生成機能を追加(作業ブランチ `feature/video-plan-batch-tab`、未リリース)**
|
||||
- **v0.5.5** — Videoタブ全面刷新: Plan/Asset/Editバッチ生成 + Projectサブタブ + プレビュー2ペイン化 + Video Assets/Video Temp運用整理 + Galleryメタデータ消失バグ根本修正
|
||||
- **v0.5.4** — 機能追加: Video/GenerateUIタブのLTX-2.5・Wan2.2対応拡大 + AI Assistant設定のUnsloth対応・独立モーダル化・モデルアンロード機能
|
||||
- **v0.5.3** — バグ修正: Labタブのワークフロー切替時に前の内容が残留 + Galleryタブの読み込み高速化・root自動展開 + READMEスクリーンショット刷新
|
||||
- **v0.5.2** — 機能追加: GenerateUI/Labタブの動画ワークフロー対応 + VideoタブにFrame抽出/GIF変換機能 + データ管理監査とZIPフルバックアップ + Send to LI node統一
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-30: Videoタブに Plan/Asset/Edit のバッチ動画生成機能を追加(作業ブランチ `feature/video-plan-batch-tab`、未リリース)
|
||||
## v0.5.5
|
||||
|
||||
「Videoタブにバッチ動画生成機能を追加したい。既存の生成UIタブのLab機能を動画生成用に整えたイメージ」との要望を起点に着手。従来のVideoタブ(単発生成フォーム)をPlan/Asset/Editの3タブ構成に再設計し、複数回のユーザー実機確認とレイアウト調整、および調査の過程で発見した3件の既存バグ修正を1ブランチでまとめた。main未マージのチェックポイントとして記録する。
|
||||
### Videoタブ全面刷新: Plan/Asset/Editバッチ生成 + Projectサブタブ + プレビュー2ペイン化 + Video Assets/Video Temp運用整理 + Galleryメタデータ消失バグ根本修正
|
||||
|
||||
「Videoタブにバッチ動画生成機能を追加したい。既存の生成UIタブのLab機能を動画生成用に整えたイメージ」との要望を起点に着手。従来のVideoタブ(単発生成フォーム)をPlan/Asset/Editの3タブ構成に再設計し、複数セッションにわたるユーザー実機確認・レイアウト調整・追加要望への対応、および調査の過程で発見した重大バグの根本修正までを1リリースにまとめた。
|
||||
|
||||
**1. Planタブ: タイムラインブロックによるバッチ動画生成**: 「基本設定→タイムラインをブロック分割→各ブロックにFirst/Last画像とプロンプトを設定→実行で順次バッチ生成」という要望を実装。LabタブがCheckpoint/LoRA/VAE/Prompt/Ksamplerそれぞれ独立したキーフレーム配列を持つ設計なのに対し、Video Planは「各ブロックが完結した1本の生成ジョブ」という単位でデータを持つ点が本質的に異なるため、Lab方式(`_buildWorkflowForIteration`によるキーフレーム解決)ではなく新規のデータモデル(`{id, color, duration, prompt, first_image_mode, first_image_filename, last_image_filename}`の配列)を設計した。First ImageはNone/画像を指定/前ブロックの最終フレームを使用の3択(`first_image_mode`)を持ち、chain_previous選択時は前ブロックに明示的なLast Imageがあればそれを直接使い(フレーム抽出不要)、無ければ前ブロックの生成動画から実際の最終フレームを`extractLastFrameBlob()`(Labのchain image機能で実績のある関数を再利用)でクライアント側抽出してアップロードする、という優先順位をユーザーと合意の上で実装した。Run/Pause/Cancelと進捗バーはLabタブの`_runLabBatch`と同じPromiseベースの一時停止機構を踏襲。ワークフロー操作(モデルファミリー自動判定・ノードへの値注入)はVideoタブの既存ロジックから`video-workflow.js`として抽出し、Plan/旧単発フォームの双方から呼べる純粋関数群にした。
|
||||
|
||||
@@ -20,11 +22,27 @@
|
||||
|
||||
**7. バグ修正: チェイン画像が無関係な既存ファイルにすり替わる**: 「2ブロックのPlanで2つ目のFirst Imageを前ブロックの最終フレームに設定したが、生成された2つ目の動画は無関係な画像が使われている」との報告。調査の結果、`convertUiToApi()`(UI形式→API形式変換の共通処理)が持つ「COMBO項目の値がキャッシュ済み`/object_info`の選択肢に無い場合、無警告で選択肢の先頭に差し替える」というフォールバック機構(チェックポイント名の互換性対応が主目的で元々存在)が原因と判明。LoadImageノードの「image」もCOMBO型のため対象になり、バッチ実行中にアップロードした抽出フレーム画像のファイル名はその時点でキャッシュ済みの`/object_info`にまだ存在せず、無関係な既存ファイル(選択肢の先頭)に差し替えられていた。LabタブのchainImage機能がこの問題に遭遇しないのは、Labが既にAPI形式のワークフローを直接操作し`convertUiToApi()`を経由しないため。`comfyui-workflow.js`に`invalidateObjectInfoCache()`を新設し、チェイン用フレーム・ユーザーの手動画像アップロードの直後に呼んで次回変換時に最新の`/object_info`を再取得させるよう修正した。
|
||||
|
||||
**8. バグ修正: `__VideoAssets__`グループが再起動後に一覧・詳細パネルから消える**: 「再起動してギャラリーを確認したがグループに`__VideoAssets__`、`__vtemp__`が表示されなくなった」との報告。`gallery_metadata.json`を実データで直接確認したところ、`__vtemp__`はレジストリ・10件のメンバーシップとも正常だった一方、`__VideoAssets__`はグループレジストリ配列からも全画像のメンバーシップからも完全に消えていた(`delete_group()`はレジストリと全画像の`groups`配列を同時に消す実装で、症状と完全に一致)。原因は`__vtemp__`/`VideoPlan:<name>`と異なり、`__VideoAssets__`はバッチ実行側から`ensure_group`を呼ぶコードが一切無い設計だったこと — 3番で予約グループ保護(🔒表示・削除ボタン無効化)を追加する**前**の時点でManage Groupsから削除操作が可能だったため、その時点で削除され、以後`add_to_group`は個別画像への書き込みは成功してもレジストリを再登録しないため二度と一覧に出てこなくなっていた。`gallery-tab.js`に`ensureFeederGroup()`と同型の`ensureVideoGroup()`を新設し、VideoタブのAssetタブ初期化のたびに`__VideoAssets__`を確実に再登録するよう修正。削除時に失われた過去のメンバーシップ自体(開発中のテスト動画)はバックアップが無く復元不可能である旨をユーザーに報告した。
|
||||
**8. バグ修正: `__VideoAssets__`グループが再起動後に一覧・詳細パネルから消える**: 「再起動してギャラリーを確認したがグループに`__VideoAssets__`、`__vtemp__`が表示されなくなった」との報告。`gallery_metadata.json`を実データで直接確認したところ、`__vtemp__`はレジストリ・10件のメンバーシップとも正常だった一方、`__VideoAssets__`はグループレジストリ配列からも全画像のメンバーシップからも完全に消えていた(`delete_group()`はレジストリと全画像の`groups`配列を同時に消す実装で、症状と完全に一致)。原因は`__vtemp__`/`VideoPlan:<name>`と異なり、`__VideoAssets__`はバッチ実行側から`ensure_group`を呼ぶコードが一切無い設計だったこと — 3番で予約グループ保護(🔒表示・削除ボタン無効化)を追加する**前**の時点でManage Groupsから削除操作が可能だったため、その時点で削除され、以後`add_to_group`は個別画像への書き込みは成功してもレジストリを再登録しないため二度と一覧に出てこなくなっていた。`gallery-tab.js`に`ensureFeederGroup()`と同型の`ensureVideoGroup()`を新設し、VideoタブのAssetタブ初期化のたびに`__VideoAssets__`を確実に再登録するよう修正。削除時に失われた過去のメンバーシップ自体(開発中のテスト動画)はバックアップが無く復元不可能である旨をユーザーに報告した(このensure漏れが後に13番の伏線になる — `__VideoAssets__`側だけ直し`__vtemp__`側のensureが漏れていた)。
|
||||
|
||||
**検証**: 全JS/Pythonファイルを`node --check`/`python -m py_compile`で構文確認、`templates/index.html`はPythonの`html.parser`でタグ対応チェックとJinja2実レンダリングで検証。ジャンクションバグの調査・検証は、ユーザーが起動中のComfyUI_5(ポート8189で稼働)に対し`curl`でAPIを直接叩いて実施 — `os.path.realpath()`で実際にジャンクションであることを確認し、`groups/{name}/images`(生成成功)と`list_images`(グループフィルタで空)の食い違いを再現、修正版デプロイ後の再検証はユーザー自身の実機確認で完了。8番の`__VideoAssets__`消失バグは、実行中サーバーの`gallery_metadata.json`を直接読み込んで原因を特定した(読み取り中にサーバー側の`_save_to_disk()`と競合しJSONDecodeErrorになることがあったため、パース成功するまで数回リトライして実データを確認)。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更・新規ファイルを都度両フォルダへ同期。各段階(レイアウト・グループ機能・チェインバグ・グループ消失バグ)ともユーザー自身の実機確認・スクリーンショット提供を受けて次の修正に進む反復的な進め方で実施した。
|
||||
**9. Assetタブに保存済みPlan一覧を表示する「Project」サブタブを追加**: 「AssetタブにVideo Planを表示したい」との要望を起点に着手。何を指すか確認したところ「右ペインにプロジェクトタブを追加し、保存済み一覧、各動画のplan名表示としたい」との回答。調査の結果、バックエンドには`GET /api/wfm/video/plans`一覧APIと`/wfm_data/video_plan/{name}.png`サムネイル配信が2番のPlan永続化実装時から既に存在していたが、フロントエンドからは一度も呼ばれておらず丸ごと未使用のまま埋まっていたことが判明した。新規`video-project-tab.js`で保存済みPlan一覧(サムネイル・名前・ブロック数・更新日時)表示、クリックでのPlanエディタ読み込み(`video-plan-tab.js`に`openSavedVideoPlan()`をexport)、削除機能を実装し、Assetカードには生成元のPlan名をバッジ表示する機能(`img.groups`から`VideoPlan:<name>`グループを逆引き)も合わせて追加した。当初の指示通り右ペイン(Frame/GIFタブの隣)に実装したが、直後のフィードバックで「左ペインに変更したい」との指示を受け、左サイドバー(Assetタブの隣)へ丸ごと再配置した。この移動に伴い、左サイドバーのトグルロジックを「Assetボタン単体のshow/hide」から「クリックした方を開き他方を閉じる、同じボタンの再クリックで両方とも折りたたむ」という排他的トグルに拡張した。
|
||||
|
||||
**How to apply**: 「◯◯機能では動くのに△△機能では動かない」という報告があった場合、両者の実行経路が本当に同じか(今回はLabがAPI形式を直接操作するのに対しVideoはUI形式から`convertUiToApi()`を経由する、という違い)を疑うと真因に早く到達できる。ComfyUIフロントエンドの`convertUiToApi()`のようなCOMBO値検証は「無警告でのフォールバック」を行う設計になっている場合があるため、実行時に動的アップロードした値をCOMBO項目に書き込む機能(画像チェイン等)を追加する際は、そのCOMBOの選択肢キャッシュが最新かどうかを必ず確認する必要がある。StabilityMatrixのような複数ComfyUIインスタンスでモデル/出力フォルダを共有するツールはジャンクション/シンボリックリンクを使うため、`Path.resolve()`をパスキーの一部に使うコードとファイルシステムスキャンで得たパスとの間に「解決する/しない」の不一致が生まれやすい — 同じ画像を指すパス文字列の保存/検索が全経路で一貫した規約(このプロジェクトでは「常にresolve()後の文字列で統一」)に従っているか、新しい書き込み経路を追加するたびに確認する価値がある。複数のタブ切替グループが同じCSSクラス/data属性を共有する実装は、後から片方だけ物理的に別の場所へ移動すると意図せず連動してしまうため、独立させたいグループは早い段階でクラス名やquerySelectorAllのスコープを分けておくとよい。予約グループ(`__Feeder__`のような「削除されると誰も自動で作り直さない」特別なグループ)を新設する際は、保護(削除禁止)の実装と同時に「常に存在することを保証する`ensure`呼び出し」もセットで用意しないと、保護を追加する前の空白期間や手違いで一度消えた場合に二度と復活しない — この2つは別々の対策であり、片方だけでは不十分。実行中サーバーが継続的に書き込んでいるJSONファイルを外部から直接読んで調査する際は、書き込みと衝突して不完全な内容を読んでしまうことがあるため、パースが安定して成功するまでリトライするのが安全。
|
||||
**10. 中央下部のワークフロー名表示行を1行に統合**: 「Open Workflowボタン行を上段の現在読み込んでいるワークフロー名行と1行にしたい」との要望。`wfm-video-wf-name`(現在ロード中のワークフロー名)と`wfm-video-plan-workflow-row`(Planに記録されたワークフロー名+Open Workflowボタン)が縦に2行表示されていたのを、`.wfm-video-wf-row`という横並びflexコンテナでまとめ1行に統合した。
|
||||
|
||||
**11. ワークフロー設定可能項目ハイライト表示の復活**: 「以前の単発生成フォーム時の設定項目のハイライト表示を行いたい」との要望を受けて調査したところ、v0.5.4で一度実装されていた機能(`wfm-model-label-active`クラス、GenerateUI Modelタブと共通の設定色)が、4番のPlan/Asset/Edit再設計で単発フォームのコードごと失われ、ヘルプ文言だけが取り残されていたことが判明した。Planタブのブロックエディタ向けに`_updateFieldHighlights()`を再実装:Prompt/Duration/First Image/Last Image/Aspect Ratio/Megapixels/Multipleの各ラベルにIDを付与し、読み込んだワークフローが実際にそのフィールドをサポートしている場合のみハイライトする(Wan2.2のようにResolutionSelectorが無いワークフローではAspect Ratio/Megapixels/Multipleは非ハイライトのまま)。ワークフローロード時・Open Workflowボタン使用時・ブロック切り替え時(`_renderBlockEditor()`はブロック選択のたびにDOMを作り直すため都度再適用が必要)のいずれでも正しく再適用されるようにし、ハイライト色を注入するCSSセレクタ(`settings-tab.js`の`applyModelTabActiveColor()`)も旧レイアウトの`.wfm-video-form-panel`から新レイアウトの`.wfm-video-plan-content`に更新した(4番のレイアウト変更で参照先の無いセレクタになっていた)。この過程で、ヘルプ本文(`helpVideo8`)がHTML側とi18n.js側で別内容になっている既存の不整合(HTML側は旧ハイライト説明、i18n.js側はチェイン機能の説明で後者が実際に表示される)を発見し、ハイライト説明を新規`helpVideo10`として追加し直した。
|
||||
|
||||
**12. Video Assets/Video Tempグループの命名統一・自動登録の運用整理**: 「__vtempグループが消えてしまっている」という報告と、ユーザー自身による修正方針の提示(表示名をグループ名で統一、VideoPlan:自動登録を廃止し常にVideo Tempへ、VtCボタン名を「Clear __Video Temp__」に変更)を受けて実施。3番/8番で導入した`__VideoAssets__`/`__vtemp__`を`__Video Assets__`/`__Video Temp__`に改名し、Assetタブの表示名もグループ名からそのまま導出する形に統一(「Temporary (unsaved runs)」という独自表示名は廃止)。3番の「Plan保存済みなら`VideoPlan:<name>`サブグループへ」という自動振り分けは廃止し、Plan保存の有無に関わらず常に`__Video Temp__`へ登録するよう変更 — `__Video Assets__`への整理は完全にユーザー任意のタイミングに委ねる設計に転換した。対応してAssetタブの「VideoPlan:」動的取得・フィルタ表示ロジックと、9番で追加したAssetカードへのPlan名バッジ表示も、対応するグループ自体が無くなったため削除した。バックエンド予約グループリスト(`py/routes/gallery_routes.py`の`_RESERVED_GROUPS`)も新グループ名に追従させている。既存の`VideoPlan:`グループ自体はユーザー側で削除する前提で進めた。
|
||||
|
||||
**13. 重大バグ修正: `gallery_metadata.json`の非アトミック書き込みによるデータ消失(根本原因)**: 「再起動するとグループに登録した内容が消えます」との報告を受けた調査。まず8番の`ensureVideoGroup()`が実は`__VideoAssets__`(現`__Video Assets__`)しかensureしておらず`__vtemp__`(現`__Video Temp__`)はensureしていなかったという見落としを修正したが、実データ(`user/default/Workflow-Studio/gallery_metadata.json`、画像9,470件)を直接調べたところ、より深刻な問題が見つかった。`GalleryMetadataStore._save_to_disk()`が`open(path, "w")`による直接上書きの非アトミック書き込みで、バックグラウンドインデックス処理(`_bg_index_folder`、未キャッシュ画像10枚ごとに保存)が大量画像フォルダに対して極めて高頻度にこのファイルへ書き込み続けていた(実測でファイルサイズが0.5秒間隔で数百KB単位で変動し、外部プロセスから読むとタイミングによってJSONパースが失敗する状態だった)。この書き込みの最中にComfyUIプロセスが終了すると、ファイルが不完全なJSONのまま残り、次回起動時の`_load()`がパースに失敗して警告ログを出すだけで中身を空の`{"images": {}, "groups": []}`にリセットしてしまう — 以降1回でも保存が走ると、それまでの全グループ登録・タグ・メモが跡形もなく上書きされて消える、という構造だった。実際に確認したところ、グループの登録リスト自体(`__Feeder__`/`__Video Assets__`/`__Video Temp__`等)は残っていたが、動画がどのグループに属しているかという個々のメンバーシップ情報は失われていた。`_save_to_disk()`を一時ファイル書き込み+`Path.replace()`によるアトミックな置き換えに変更し、`_load()`がJSON破損を検出した場合は単に空データにするのではなく`.corrupt`ファイルとしてバックアップしてから起動するよう変更した(今後同様の事態が起きても手動復旧の余地を残す)。単体テスト(アトミック書き込み・破損時バックアップ・復旧後の再保存)で動作確認。今回失われた登録内容自体はバックアップが無く復元不可能である旨をユーザーに報告した。
|
||||
|
||||
**14. 中央プレビューの2ペイン化**: 手描きの構成図での要望「動画ソース・ファイルドロップエリアを左端に移動、プレビューを2つにして左はアセットの選択動画/ドロップされた動画のプレビュー兼用、右は生成された動画のプレビュー用」を実装。従来は単一のプレビュー(Video Sourceドロップの表示専用で、Assetタブの選択動画やPlan実行結果を自動表示する仕組み自体がそもそも存在しなかった)だったのを、3カラム構成(左: Video Sourceフォーム、中央: Asset選択/Sourceドロップ兼用プレビュー、右: 生成動画専用プレビュー、いずれも新規実装)に再構成した。`video-tab.js`・`video-asset-tab.js`(Asset選択)・`video-plan-tab.js`(バッチ実行結果)の3ファイルがプレビュー状態を共有する必要があったため、新規`video-preview.js`という共有モジュールに切り出した(video-tab.jsが既に他の2ファイルをimportしているため、逆方向のimportを追加すると循環参照になることを回避するため)。Frame/GIFツールは「直近にアクティブになった方のプレビュー」を対象に動作する設計にした。Asset選択時はGalleryの絶対パスをComfyUIのfilename/subfolder/type形式に変換する手間を避けるため、動画をBlobとしてfetchし「ローカルファイル」として扱うことで、GIF変換の既存アップロードロジックをそのまま流用できるようにした。Assetサイドバーの詳細パネルにあった動画プレビュー(`<video>`要素)は、中央プレビューに統合されたため削除した。
|
||||
|
||||
**15. ヘルプの整合性チェック**: リリース前の最終確認としてVideo Tabヘルプ全11項目(Title/Desc/1〜10)をHTML・i18n.js(3言語)・app.jsのhelpIdMapで突き合わせ、11番で発見した`helpVideo8`の不整合以外にも、`helpVideoDesc`(旧「Plan / Asset / Edit」の一括り記述が9番以降のサイドバー分離構造と食い違っていた)と`helpVideo1`/`helpVideo2`/`helpVideo3`(HTML側が旧単発フォーム時代の文言のまま放置され、実際に表示されるi18n.js側の内容と長年食い違っていた)を発見・修正した。README.mdのVideo Tabセクションも同様に、4番のPlan/Asset/Edit再設計自体が一切反映されないまま単発フォーム時代の説明で放置されていたことが判明し、9〜14番までの内容を反映して全面的に書き直した。
|
||||
|
||||
**16. `pyproject.toml`のlicense記述をComfy Registry仕様準拠の形式に修正**: リリース準備の一環で、`license = "MIT"`という単純文字列形式が、公式仕様(`license = {file = "LICENSE"}`または`{text = "..."}`のいずれか)に該当しない非推奨形式であり、`comfy env`実行時にcomfy-cliが警告を出していたことを確認した([[project_registry_flagged_versions]]で追跡しているv0.5.0〜v0.5.3の原因不明のFlagged/Pending状態の調査依頼を受けて着手)。合わせて、参照先の`LICENSE`ファイル自体がリポジトリに存在しなかった(README.mdの「MIT License」という一文のみ)ため新規作成し、`license = { file = "LICENSE" }`に修正した。調査の結果、Comfy Registryのセキュリティスキャンは主にAI+静的解析(Ruff)によるPythonコード内の危険なパターン検出(`eval`/`exec`/`subprocess`/`pickle.loads`/カスタムpip wheel等)が対象で、`pyproject.toml`のメタデータ自体(license記述含む)がスキャン対象になるという情報は見つからなかった — つまり今回の修正はComfy Registryの公式仕様に沿った改善ではあるが、Flagged/Banned問題の直接的な解決策である確証はない。一方で、他の開発者(GitHub issue経由)でも同様の原因不明なFlaggedステータスが複数報告されており、pyproject.tomlの記述とは無関係にRegistry側の自動スキャンが誤検知(false positive)を起こしている可能性が高いことが分かった。具体例として、外部バイナリをsubprocessで呼び出す正当なコード(GraphicsMagick連携)が誤検知された報告があり、本プロジェクトにも同種のパターン(`gmic_routes.py`の`subprocess.Popen`+Windows固有の`CREATE_NEW_CONSOLE`フラグ、`wildcard_service.py`の`["cmd", "/c", "mklink", ...]`)が存在するため、これらが誤検知の一因になっている可能性はあるが確証はない(いずれも正当な機能で、変更は行っていない)。
|
||||
|
||||
**検証**: 全JS/Pythonファイルを`node --check`/`python -m py_compile`で構文確認、`templates/index.html`はPythonの`html.parser`でタグ対応チェックとJinja2実レンダリングで検証。ジャンクションバグの調査・検証は、ユーザーが起動中のComfyUI_5(ポート8189で稼働)に対し`curl`でAPIを直接叩いて実施 — `os.path.realpath()`で実際にジャンクションであることを確認し、`groups/{name}/images`(生成成功)と`list_images`(グループフィルタで空)の食い違いを再現、修正版デプロイ後の再検証はユーザー自身の実機確認で完了。8番の`__VideoAssets__`消失バグは、実行中サーバーの`gallery_metadata.json`を直接読み込んで原因を特定した(読み取り中にサーバー側の`_save_to_disk()`と競合しJSONDecodeErrorになることがあったため、パース成功するまで数回リトライして実データを確認)。13番の根本原因調査では、同じファイルへの読み取りを連続実行してもファイルサイズと成否が毎回変わることを確認し、実データが継続的に高頻度書き込みされている実態そのものを証拠として特定に用いた。13番の修正自体は`tempfile`を使った独立テストスクリプトで正常保存・再ロード後のデータ保持・破損ファイル検出時のバックアップ・復旧後の再保存を単体検証(ComfyUI起動不要)。開発元gitリポジトリと実行時`custom_nodes`フォルダは別実体のため([[project_dev_deploy_sync]]参照)、変更・新規ファイルを都度両フォルダへ同期しCRLF/LF差異を除いた`diff`で完全一致を確認、Python変更(13番・12番の`gallery_routes.py`予約グループリスト)を含む回では実行時側の`__pycache__`もクリアした。各段階(レイアウト・Projectサブタブ追加・ハイライト復活・グループ運用整理・データ消失バグ・プレビュー2ペイン化・ヘルプ整合性)ともユーザー自身の実機確認・スクリーンショットや手描き構成図の提供を受けて次の修正に進む反復的な進め方で実施した。16番のRegistry仕様調査はWeb検索でComfy公式ドキュメント(`docs.comfy.org/registry/specifications`)とGitHub issue(Comfy-Org/ComfyUI-Manager #2927, #2997)を確認し、`pyproject.toml`の`license = { file = "LICENSE" }`変更後は`toml`パッケージでパース可能なことを確認した。
|
||||
|
||||
**How to apply**: 「◯◯機能では動くのに△△機能では動かない」という報告があった場合、両者の実行経路が本当に同じか(今回はLabがAPI形式を直接操作するのに対しVideoはUI形式から`convertUiToApi()`を経由する、という違い)を疑うと真因に早く到達できる。ComfyUIフロントエンドの`convertUiToApi()`のようなCOMBO値検証は「無警告でのフォールバック」を行う設計になっている場合があるため、実行時に動的アップロードした値をCOMBO項目に書き込む機能(画像チェイン等)を追加する際は、そのCOMBOの選択肢キャッシュが最新かどうかを必ず確認する必要がある。StabilityMatrixのような複数ComfyUIインスタンスでモデル/出力フォルダを共有するツールはジャンクション/シンボリックリンクを使うため、`Path.resolve()`をパスキーの一部に使うコードとファイルシステムスキャンで得たパスとの間に「解決する/しない」の不一致が生まれやすい — 同じ画像を指すパス文字列の保存/検索が全経路で一貫した規約(このプロジェクトでは「常にresolve()後の文字列で統一」)に従っているか、新しい書き込み経路を追加するたびに確認する価値がある。複数のタブ切替グループが同じCSSクラス/data属性を共有する実装は、後から片方だけ物理的に別の場所へ移動すると意図せず連動してしまうため、独立させたいグループは早い段階でクラス名やquerySelectorAllのスコープを分けておくとよい。予約グループ(`__Feeder__`のような「削除されると誰も自動で作り直さない」特別なグループ)を新設する際は、保護(削除禁止)の実装と同時に「常に存在することを保証する`ensure`呼び出し」もセットで用意しないと、保護を追加する前の空白期間や手違いで一度消えた場合に二度と復活しない — この2つは別々の対策であり、片方だけでは不十分。実行中サーバーが継続的に書き込んでいるJSONファイルを外部から直接読んで調査する際は、書き込みと衝突して不完全な内容を読んでしまうことがあるため、パースが安定して成功するまでリトライするのが安全。JSON等の設定/メタデータファイルへの永続化処理を新規実装・レビューする際は、単純な`open(path, "w")`による直接上書きになっていないか必ず確認する — 書き込み頻度が低いうちは問題が表面化しなくても、対象データ量が増えて書き込み回数が増えるほど「書き込み中のプロセス終了でファイルが壊れる」リスクが顕在化しやすく、かつ壊れたファイルのロード失敗を「空データにフォールバック」で処理していると、その後の1回の保存で過去の全データが消えるという最悪の組み合わせになる(一時ファイル+`Path.replace()`のアトミック置き換えと、破損検出時のバックアップ退避はセットで対策する)。UI再設計でコードごと削除した機能(今回のハイライト表示)は、対応するヘルプ文言だけが取り残されて「存在しない機能の説明」として残り続けることがあるため、大きめのUI再設計を行った際はヘルプ文言側の棚卸しも忘れずに行う。ユーザー向けドキュメント(README等)は実装が大きく変わった直後に更新を忘れると、次に触るまで長期間ズレたまま放置されやすい — 大規模なUI再設計コミットを作る際は、その場でREADMEの該当セクションも一緒に更新するのが安全(今回はPlan/Asset/Edit再設計そのものが数バージョン分READMEに未反映のまま残っていた)。
|
||||
|
||||
関連: [[project_v050_video_tab]], [[project_dev_deploy_sync]]
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 ketle-man
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+3
-3
@@ -26,7 +26,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Features (tab -> feature names)
|
||||
|
||||
@@ -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, New button, Tools, Layer panel, Layer lock, Text quality, Export, Canvas navigation, Undo / Redo
|
||||
|
||||
### Video Tab (v0.5.0 – v0.5.4)
|
||||
Dedicated video generation UI, independent of GenerateUI, 3-pane layout, First Frame / Last Frame are both optional, Semantic node lookup, not hardcoded IDs, Field highlight color, Load in Video, Preview frame + Video Source panel, Frame tab, GIF tab, Playback volume
|
||||
### Video Tab (v0.5.0 – v0.5.5)
|
||||
Dedicated video generation UI, independent of GenerateUI, Plan / Edit subtabs, 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
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -22,7 +22,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
|
||||
- Built-in AI tools (translation and more)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -404,18 +404,19 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Video Tab (v0.5.0 – v0.5.4)</h3></summary>
|
||||
<summary><h3>Video Tab (v0.5.0 – v0.5.5)</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
|
||||
- **3-pane layout** — left: form (First Frame / Last Frame / Prompt / Aspect Ratio / Megapixels / Multiple / Duration / Noise Seed / Generate); center: preview frame + Video Source panel, plus an Editing pane reserved for future video-editing tools; right: **Frame** / **GIF** property tabs (v0.5.2, see below)
|
||||
- **First Frame / Last Frame are both optional** — leaving First Frame 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
|
||||
- **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); Aspect Ratio / Megapixels / Multiple are set once as the plan's base settings (disabled for families with no Resolution Selector node, e.g. Wan2.2). 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
|
||||
- **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
|
||||
- **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** (v0.5.4) — Prompt / Duration / First Frame / Last Frame / Aspect Ratio / Megapixels / Multiple 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 since those controls are disabled
|
||||
- **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); unsupported workflows show an error toast instead of loading
|
||||
- **Preview frame + Video Source panel** (v0.5.2, replaces the old Result panel) — the center pane's preview shows either a just-generated video or one loaded independently of generation; a **Video Source** drop zone next to it accepts any local video file (drag-and-drop or click to pick), with a **Clear** button to reset back to empty — the Frame/GIF tools below work on whatever is currently shown here, generated or not; generated results are still saved to Gallery metadata (embedded workflow JSON) automatically, so the Gallery tab's Workflow/Prompt panels work on generated videos the same way they do for images
|
||||
- **Frame tab** (v0.5.2) — captures the preview video'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)
|
||||
- **GIF tab** (v0.5.2) — converts the preview video 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** (v0.5.4) — 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
|
||||
- **Field highlight color** — Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple 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 since those controls are disabled
|
||||
- **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)
|
||||
- **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
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@ async def create_group(request: web.Request) -> web.Response:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
_RESERVED_GROUPS = {"__Feeder__", "__VideoAssets__", "__vtemp__"}
|
||||
_RESERVED_GROUPS = {"__Feeder__", "__Video Assets__", "__Video Temp__"}
|
||||
|
||||
|
||||
async def rename_group(request: web.Request) -> web.Response:
|
||||
|
||||
@@ -53,13 +53,35 @@ class GalleryMetadataStore:
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("GalleryMetadataStore: load error: %s", e)
|
||||
# 破損ファイルをリネームしてバックアップしておく。ここでリネームせずに
|
||||
# 空の self._data のまま次の save() が走ると、破損直前までの全メタデータ
|
||||
# (タグ/メモ/グループ登録)が跡形もなく上書きされ、二度と復元できなくなる
|
||||
# (実際に発生した不具合: _save_to_disk が非アトミック書き込みだったため、
|
||||
# 大量画像フォルダのBGインデックス処理中の頻繁な保存とComfyUIの終了タイミング
|
||||
# が重なるとファイルが不完全な状態のまま残ることがあった — _save_to_disk側は
|
||||
# os.replace()によるアトミック書き込みに修正済みだが、過去に壊れたファイルや
|
||||
# 他要因による破損に備えてここでも保護しておく)。
|
||||
try:
|
||||
backup_path = self.file_path.with_name(self.file_path.name + ".corrupt")
|
||||
self.file_path.replace(backup_path)
|
||||
logger.warning("GalleryMetadataStore: corrupt file backed up to %s", backup_path)
|
||||
except Exception as backup_err:
|
||||
logger.warning("GalleryMetadataStore: failed to back up corrupt file: %s", backup_err)
|
||||
|
||||
def _save_to_disk(self):
|
||||
# 呼び出し元(save/create_group等)が既にself._lockを保持している前提の内部メソッド
|
||||
# 呼び出し元(save/create_group等)が既にself._lockを保持している前提の内部メソッド。
|
||||
# 一時ファイルに書き出してから置き換える(os.replace相当のPath.replace)ことで書き込みを
|
||||
# アトミックにする。素朴な open(path, "w") での直接上書きだと、書き込み完了前に他プロセス
|
||||
# が読むと不完全なJSONを掴む(torn read)うえ、書き込み中にComfyUIプロセスが終了すると
|
||||
# ファイルが壊れたまま残る — 大量画像フォルダのBGインデックス処理(10枚ごとに保存)で
|
||||
# 書き込み頻度が高い環境ほど発生しやすく、次回起動時のロード失敗→空データでの
|
||||
# 全上書きという形でユーザーの登録済みグループ/タグ/メモが消失する原因になっていた。
|
||||
try:
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.file_path, "w", encoding="utf-8") as f:
|
||||
tmp_path = self.file_path.with_name(self.file_path.name + ".tmp")
|
||||
with open(tmp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self._data, f, ensure_ascii=False, indent=2)
|
||||
tmp_path.replace(self.file_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("GalleryMetadataStore: save error: %s", e)
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
[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.5.4"
|
||||
license = "MIT"
|
||||
version = "0.5.5"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
|
||||
@@ -131,6 +131,17 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 現在ロード中のワークフロー名 + Planに記録されたワークフローのOpen Workflow行を
|
||||
同じ1行にまとめる(以前は縦に2行だった)。 */
|
||||
.wfm-video-wf-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wfm-video-wf-row .wfm-lab-plan-workflow-row { margin-top: 0; }
|
||||
|
||||
.wfm-video-plan-basesettings {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 160px);
|
||||
@@ -288,13 +299,6 @@
|
||||
color: var(--wfm-text-secondary);
|
||||
}
|
||||
|
||||
.wfm-video-asset-preview {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
border-radius: 4px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.wfm-video-asset-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
@@ -341,8 +345,9 @@
|
||||
border-right: 1px solid var(--wfm-border);
|
||||
}
|
||||
|
||||
/* 生成結果 or ソース選択エリアで読み込んだ動画を横並びで表示する行。
|
||||
プレビュー枠が主役、ソースパネルは選択UIのみの固定幅。 */
|
||||
/* 3カラム構成: 左=Video Source選択UI(固定幅) / 中央=Asset選択・Sourceドロップ兼用
|
||||
プレビュー / 右=生成結果専用プレビュー。.wfm-video-preview-frameは共通クラスなので
|
||||
両プレビュー枠とも自動的にflex:1で均等幅になる。 */
|
||||
.wfm-video-preview-row {
|
||||
flex: none;
|
||||
display: flex;
|
||||
@@ -360,6 +365,7 @@
|
||||
border: 1px dashed var(--wfm-border);
|
||||
border-radius: 6px;
|
||||
background: var(--wfm-bg-hover, #111);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wfm-video-preview-el {
|
||||
@@ -435,3 +441,70 @@
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ── 左ペイン Project サブタブ: 保存済みPlan一覧 ── */
|
||||
.wfm-video-project-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wfm-video-project-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wfm-video-project-item:hover { border-color: var(--wfm-primary); }
|
||||
.wfm-video-project-item.active { outline: 2px solid var(--wfm-primary); outline-offset: -2px; }
|
||||
|
||||
.wfm-video-project-item-thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: none;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
background: var(--wfm-bg-hover, #111);
|
||||
}
|
||||
|
||||
.wfm-video-project-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wfm-video-project-item-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wfm-video-project-item-meta {
|
||||
font-size: 10px;
|
||||
color: var(--wfm-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wfm-video-project-item-delete {
|
||||
flex: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--wfm-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.wfm-video-project-item-delete:hover { color: var(--wfm-danger); }
|
||||
|
||||
@@ -736,6 +736,7 @@ function applyI18nToHtml() {
|
||||
"wfm-help-video-3": "helpVideo3", "wfm-help-video-4": "helpVideo4",
|
||||
"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",
|
||||
};
|
||||
for (const [id, key] of Object.entries(helpIdMap)) {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
+22
-13
@@ -46,11 +46,11 @@ const API = {
|
||||
};
|
||||
|
||||
export const FEEDER_GROUP = "__Feeder__";
|
||||
export const VIDEO_GROUP = "__VideoAssets__";
|
||||
// Video Planを名前を付けて保存していない状態で生成した動画の一時置き場。
|
||||
// __VideoAssets__(ユーザーが手動でキュレーションする素材グループ)とは異なり、
|
||||
// 未保存Planの生成物置き場として自動的に追加される作業用グループ。
|
||||
export const VTEMP_GROUP = "__vtemp__";
|
||||
export const VIDEO_GROUP = "__Video Assets__";
|
||||
// Video Planを実行するたびに生成された動画が自動的に追加される作業用グループ
|
||||
// (Plan保存の有無に関わらず常にここへ入る)。__Video Assets__(ユーザーが手動で
|
||||
// キュレーションする素材グループ)への整理はユーザー任意のタイミングで行う。
|
||||
export const VTEMP_GROUP = "__Video Temp__";
|
||||
|
||||
const _RESERVED_GROUPS = [FEEDER_GROUP, VIDEO_GROUP, VTEMP_GROUP];
|
||||
function _isReservedGroup(name) {
|
||||
@@ -1151,11 +1151,15 @@ export async function ensureFeederGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
/** __VideoAssets__グループが存在しない場合に作成する。add_to_groupは対象グループが
|
||||
* レジストリに登録されているかを確認せず画像側にグループ名を書き込むだけのため、
|
||||
* 一度レジストリから消える(例: 予約保護が効く前の版で削除された)と、以後の手動追加は
|
||||
/** __Video Assets__ と __Video Temp__ の両方が存在しない場合に作成する。add_to_groupは
|
||||
* 対象グループがレジストリに登録されているかを確認せず画像側にグループ名を書き込むだけの
|
||||
* ため、一度レジストリから消える(例: 予約保護が効く前の版で削除された)と、以後の手動追加は
|
||||
* 画像には記録されてもグループ一覧・フィルタには一切出てこなくなる。VideoタブのAsset
|
||||
* タブ初期化時に呼び、レジストリを確実に存在させる。 */
|
||||
* タブ初期化時に呼び、レジストリを確実に存在させる。
|
||||
* (__Video Temp__側もここでensureする — 以前はensureVideoGroupがVIDEO_GROUPしか
|
||||
* 保証しておらず、__Video Temp__のレジストリが何らかの理由で消えると誰も復活させられない
|
||||
* バグがあった。バッチRun開始時のensure(video-plan-tab.jsの_ensureVideoAssetGroups)は
|
||||
* Runするまで呼ばれないため、それだけでは不十分。) */
|
||||
export async function ensureVideoGroup() {
|
||||
try {
|
||||
await fetch(API.groupEnsure, {
|
||||
@@ -1163,6 +1167,11 @@ export async function ensureVideoGroup() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: VIDEO_GROUP }),
|
||||
});
|
||||
await fetch(API.groupEnsure, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: VTEMP_GROUP }),
|
||||
});
|
||||
await loadGroups();
|
||||
} catch (e) {
|
||||
console.warn("[Gallery] ensureVideoGroup error:", e);
|
||||
@@ -1183,7 +1192,7 @@ async function clearFeederGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
/** __vtemp__グループ内の全画像を除外する(詳細パネルのVtCボタン) */
|
||||
/** __Video Temp__グループ内の全画像を除外する(詳細パネルの「Clear __Video Temp__」ボタン) */
|
||||
async function clearVtempGroup() {
|
||||
try {
|
||||
await fetch(API.groupClear(VTEMP_GROUP), { method: "POST" });
|
||||
@@ -1284,7 +1293,7 @@ function renderDetailGroup(img) {
|
||||
<button class="wfm-btn wfm-btn-sm wfm-btn-danger" id="wfm-gallery-grp-delete-btn"
|
||||
${allGroups.length === 0 || _isReservedGroup(allGroups[0]) ? "disabled" : ""} title="${t("modelsDelete")}">×</button>
|
||||
</div>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-gallery-vtemp-clear-btn" style="width:100%;margin-top:6px;" title="Clear the __vtemp__ group (auto-populated by unsaved Video Plan runs)">VtC — Clear __vtemp__</button>
|
||||
<button class="wfm-btn wfm-btn-sm" id="wfm-gallery-vtemp-clear-btn" style="width:100%;margin-top:6px;" title="${t("vtempClearBtnTitle")}">${t("vtempClearBtnLabel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1363,7 +1372,7 @@ function renderDetailGroup(img) {
|
||||
}
|
||||
});
|
||||
|
||||
// 管理セレクト変更時: 予約グループ(__Feeder__/__VideoAssets__/__vtemp__)は rename/delete を無効化
|
||||
// 管理セレクト変更時: 予約グループ(__Feeder__/__Video Assets__/__Video Temp__)は rename/delete を無効化
|
||||
el.querySelector("#wfm-gallery-grp-manage-sel")?.addEventListener("change", (e) => {
|
||||
const isReserved = _isReservedGroup(e.target.value);
|
||||
const renameBtn = el.querySelector("#wfm-gallery-grp-rename-btn");
|
||||
@@ -1372,7 +1381,7 @@ function renderDetailGroup(img) {
|
||||
if (deleteBtn) deleteBtn.disabled = isReserved;
|
||||
});
|
||||
|
||||
// VtC ボタン: __vtemp__ グループをクリア(未保存Video Planの生成物置き場)
|
||||
// Clear __Video Temp__ ボタン: __Video Temp__ グループをクリア(Video Plan実行時に自動登録される生成物置き場)
|
||||
el.querySelector("#wfm-gallery-vtemp-clear-btn")?.addEventListener("click", clearVtempGroup);
|
||||
|
||||
// グループ名変更
|
||||
|
||||
+54
-15
@@ -186,7 +186,9 @@ const LANGUAGES = {
|
||||
feederGalNoNode: "No WFS_GalleryFeeder node selected",
|
||||
feederGalEmptyGroup: "No images in the selected group",
|
||||
feederGroupCleared: "Feeder group cleared",
|
||||
vtempGroupCleared: "__vtemp__ group cleared",
|
||||
vtempGroupCleared: "__Video Temp__ group cleared",
|
||||
vtempClearBtnLabel: "Clear __Video Temp__",
|
||||
vtempClearBtnTitle: "Clear the __Video Temp__ group (video generations land here automatically; organize keepers into __Video Assets__ manually anytime)",
|
||||
addedToFeeder: "Added to Feeder",
|
||||
removedFromFeeder: "Removed from Feeder",
|
||||
appliedToWorkflow: "Applied to workflow",
|
||||
@@ -1346,7 +1348,8 @@ const LANGUAGES = {
|
||||
videoSourceLabel: "Video Source",
|
||||
videoSourceHint: "Load any video here to use Frame/GIF tools without generating one.",
|
||||
videoSourceDropLabel: "Drop video or click to select",
|
||||
videoNoVideo: "No video",
|
||||
videoSourcePreviewPlaceholder: "Select an asset or drop a video",
|
||||
videoResultPreviewPlaceholder: "No video generated yet",
|
||||
videoPropFrame: "Frame",
|
||||
videoPropGif: "GIF",
|
||||
videoFrameHint: "Play/seek the preview video to the position you want, then capture.",
|
||||
@@ -1363,16 +1366,26 @@ const LANGUAGES = {
|
||||
videoGifDoneCount: (n) => `Done (${n} frame${n === 1 ? "" : "s"})`,
|
||||
videoGifSaved: (name) => `GIF saved: ${name}`,
|
||||
videoNoSourceLoaded: "Load a video first (generate one, or drop a file into Video Source)",
|
||||
videoPropProject: "Project",
|
||||
videoProjectHint: "Saved plans",
|
||||
videoProjectEmpty: "No saved plans yet",
|
||||
videoProjectBlockCount: (n) => `${n} block${n === 1 ? "" : "s"}`,
|
||||
videoProjectDeleteConfirm: (name) => `Delete plan "${name}"?`,
|
||||
videoProjectDeleted: "Plan deleted",
|
||||
videoProjectDeleteFailed: "Failed to delete plan",
|
||||
videoProjectLoadListFailed: "Failed to load plan 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. Its Plan / Asset / Edit subtabs handle batch generation, browsing generated assets, and (future) editing, respectively.",
|
||||
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.",
|
||||
helpVideo1: "Plan subtab: set Aspect Ratio / Megapixels / Multiple as this plan's base settings, then split the timeline into blocks with the + Split / + Block / Delete buttons. Each block has its own Prompt, Duration (seconds), 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), the workflow's own resolution is used as-is and the Aspect Ratio / Megapixels / Multiple fields are disabled.",
|
||||
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 (grouped per plan), and can also be browsed and played back in the Gallery tab.",
|
||||
helpVideo5: "The Video Source panel next to the preview lets you drop or pick any local video (not just a generated one) to use the Frame/GIF tools below without running the loaded video workflow first; Clear resets it back to empty.",
|
||||
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.",
|
||||
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.",
|
||||
helpVideo10: "Highlight color: the Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple 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 since those fields are disabled.",
|
||||
},
|
||||
|
||||
ja: {
|
||||
@@ -1555,7 +1568,9 @@ const LANGUAGES = {
|
||||
feederGalNoNode: "WFS_GalleryFeederノードが選択されていません",
|
||||
feederGalEmptyGroup: "選択グループに画像がありません",
|
||||
feederGroupCleared: "Feederグループをクリアしました",
|
||||
vtempGroupCleared: "__vtemp__グループをクリアしました",
|
||||
vtempGroupCleared: "__Video Temp__グループをクリアしました",
|
||||
vtempClearBtnLabel: "Clear __Video Temp__",
|
||||
vtempClearBtnTitle: "__Video Temp__グループをクリアします(動画生成物は自動的にここへ入ります。__Video Assets__への整理は任意のタイミングで手動で行ってください)",
|
||||
addedToFeeder: "Feederに追加しました",
|
||||
removedFromFeeder: "Feederから除外しました",
|
||||
appliedToWorkflow: "ワークフローに適用しました",
|
||||
@@ -2713,7 +2728,8 @@ const LANGUAGES = {
|
||||
videoSourceLabel: "動画ソース",
|
||||
videoSourceHint: "生成せずにFrame/GIF機能を使いたい場合はここに任意の動画を読み込めます。",
|
||||
videoSourceDropLabel: "動画をドロップまたはクリックして選択",
|
||||
videoNoVideo: "動画なし",
|
||||
videoSourcePreviewPlaceholder: "アセットを選択するか動画をドロップしてください",
|
||||
videoResultPreviewPlaceholder: "生成された動画はまだありません",
|
||||
videoPropFrame: "Frame",
|
||||
videoPropGif: "GIF",
|
||||
videoFrameHint: "プレビュー動画を再生・シークして止めたい位置で「Capture Frame」を押してください。",
|
||||
@@ -2730,16 +2746,26 @@ const LANGUAGES = {
|
||||
videoGifDoneCount: (n) => `完了(${n}フレーム)`,
|
||||
videoGifSaved: (name) => `GIFを保存しました: ${name}`,
|
||||
videoNoSourceLoaded: "先に動画を用意してください(生成するか、動画ソースにファイルをドロップ)",
|
||||
videoPropProject: "Project",
|
||||
videoProjectHint: "保存済みのPlan",
|
||||
videoProjectEmpty: "保存済みのPlanはまだありません",
|
||||
videoProjectBlockCount: (n) => `${n}ブロック`,
|
||||
videoProjectDeleteConfirm: (name) => `Plan「${name}」を削除しますか?`,
|
||||
videoProjectDeleted: "Planを削除しました",
|
||||
videoProjectDeleteFailed: "Planの削除に失敗しました",
|
||||
videoProjectLoadListFailed: "Plan一覧の読み込みに失敗しました",
|
||||
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/Asset/Editの3サブタブで、それぞれバッチ生成・生成物の閲覧・(将来の)編集を扱います。",
|
||||
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の一覧を扱います。",
|
||||
helpVideo1: "Planサブタブ: Aspect Ratio/Megapixels/MultipleをこのPlanの基本設定として指定し、「+ 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など)はワークフロー自体の解像度がそのまま使われ、Aspect Ratio/Megapixels/Multiple欄は無効化されます。",
|
||||
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サブタブ(Planごとにグループ分け)に自動的に反映されるほか、Galleryタブでも閲覧・再生できます。",
|
||||
helpVideo5: "プレビューの横にある「Video Source」パネルでは、生成した動画に限らず任意のローカル動画をドロップ/選択して、下のFrame/GIF機能を読み込んだワークフローを実行せずに使えます。Clearで選択をリセットできます。",
|
||||
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フォルダに保存します。",
|
||||
helpVideo7: "GIFタブ(右パネル): Start/End(秒)・FPS・Max Width(任意)を指定して「Convert to GIF」を押すと、サーバー側で動画をデコードしアニメーションGIFとしてComfyUIのOutputフォルダに保存します。",
|
||||
helpVideo8: "チェイン機能: ブロックのFirst Imageを「前ブロックの最終フレームを使用」に設定すると、前のクリップから連続した映像になります。前ブロックに明示的なLast Imageが設定されていればそれがそのまま使われ、無ければ生成された動画から実際の最終フレームが自動抽出されます。",
|
||||
helpVideo9: "Projectタブ(左サイドバー、Assetの隣): 保存済みの全Planファイル(サムネイル・ブロック数・更新日時)を一覧表示し、クリックでPlanエディタへ読み込み、×ボタンで削除できます。同じタブをもう一度押すとパネルを閉じます。",
|
||||
helpVideo10: "ハイライト表示: 読み込んだワークフローが実際にサポートしているフィールドに応じて、Prompt/Duration/First Image/Last Image/Aspect Ratio/Megapixels/Multipleのラベルが、GenerateUI ModelタブのハイライトとSettingsタブで共通設定できる色に変わります。例えばWan2.2ワークフロー(ResolutionSelectorノードが無い)ではAspect Ratio/Megapixels/Multipleが無効化されるため、それらのラベルはハイライトされません。",
|
||||
},
|
||||
|
||||
zh: {
|
||||
@@ -2922,7 +2948,9 @@ const LANGUAGES = {
|
||||
feederGalNoNode: "未选择WFS_GalleryFeeder节点",
|
||||
feederGalEmptyGroup: "所选组中没有图片",
|
||||
feederGroupCleared: "已清空Feeder组",
|
||||
vtempGroupCleared: "已清空__vtemp__组",
|
||||
vtempGroupCleared: "已清空__Video Temp__组",
|
||||
vtempClearBtnLabel: "Clear __Video Temp__",
|
||||
vtempClearBtnTitle: "清空__Video Temp__组(视频生成结果会自动进入此组,整理到__Video Assets__由用户自行选择时机手动完成)",
|
||||
addedToFeeder: "已添加到Feeder",
|
||||
removedFromFeeder: "已从Feeder移除",
|
||||
appliedToWorkflow: "已应用到工作流",
|
||||
@@ -4081,7 +4109,8 @@ const LANGUAGES = {
|
||||
videoSourceLabel: "视频来源",
|
||||
videoSourceHint: "无需生成,也可在此加载任意视频以使用Frame/GIF工具。",
|
||||
videoSourceDropLabel: "拖放视频或点击选择",
|
||||
videoNoVideo: "暂无视频",
|
||||
videoSourcePreviewPlaceholder: "选择素材或拖放视频",
|
||||
videoResultPreviewPlaceholder: "尚未生成视频",
|
||||
videoPropFrame: "Frame",
|
||||
videoPropGif: "GIF",
|
||||
videoFrameHint: "播放/拖动预览视频到想要的位置,然后点击截取。",
|
||||
@@ -4098,16 +4127,26 @@ const LANGUAGES = {
|
||||
videoGifDoneCount: (n) => `完成(${n} 帧)`,
|
||||
videoGifSaved: (name) => `已保存GIF: ${name}`,
|
||||
videoNoSourceLoaded: "请先准备视频(生成一个,或将文件拖入视频来源)",
|
||||
videoPropProject: "Project",
|
||||
videoProjectHint: "已保存的 Plan",
|
||||
videoProjectEmpty: "暂无已保存的 Plan",
|
||||
videoProjectBlockCount: (n) => `${n} 个区块`,
|
||||
videoProjectDeleteConfirm: (name) => `确定删除 Plan「${name}」吗?`,
|
||||
videoProjectDeleted: "Plan 已删除",
|
||||
videoProjectDeleteFailed: "Plan 删除失败",
|
||||
videoProjectLoadListFailed: "Plan 列表加载失败",
|
||||
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/Asset/Edit 三个子标签分别用于批量生成、浏览已生成素材,以及(未来的)编辑功能。",
|
||||
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。",
|
||||
helpVideo1: "Plan 子标签:将 Aspect Ratio/Megapixels/Multiple 设置为该 Plan 的基础设置,然后用「+ 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)会直接使用该工作流自身的分辨率,Aspect Ratio/Megapixels/Multiple 字段将被禁用。",
|
||||
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 子标签(按 Plan 分组),也可以在 Gallery 标签页中浏览和播放。",
|
||||
helpVideo5: "预览旁边的「Video Source」面板可以拖放或选择任意本地视频(不仅限于生成的视频),无需先运行已加载的视频工作流即可使用下方的Frame/GIF工具。点击Clear可重置选择。",
|
||||
helpVideo4: "生成的视频(mp4)会自动出现在 Asset 子标签的 Video Temp 组中,也可以在 Gallery 标签页中浏览和播放。是否整理到 Video Assets,由您自行决定时机。",
|
||||
helpVideo5: "中间面板有两个预览区:左侧显示您在 Asset 子标签中选择的视频,或拖放/选择到左边「Video Source」面板中的任意本地视频(点击 Clear 可重置),无需先运行已加载的视频工作流即可使用 Frame/GIF 工具;右侧始终显示最近一次生成的片段。",
|
||||
helpVideo6: "Frame标签页(右侧面板):播放/拖动预览视频到想要的位置,点击「Capture Frame」在客户端截取该帧,再点击「Save to Output」将其保存为PNG到ComfyUI的输出文件夹。",
|
||||
helpVideo7: "GIF标签页(右侧面板):设置Start/End(秒)、FPS和可选的Max Width,点击「Convert to GIF」后,服务器会解码视频并将动画GIF保存到ComfyUI的输出文件夹。",
|
||||
helpVideo8: "链接功能:将某个区块的 First Image 设置为“使用前一个区块的最后一帧”,即可与上一个片段流畅衔接——如果前一个区块已明确设置了 Last Image,将直接使用该图片;否则会自动从其生成的视频中提取实际的最后一帧。",
|
||||
helpVideo9: "Project标签页(左侧边栏,与Asset并列):浏览所有已保存的 Plan 文件(缩略图、区块数、最后更新时间),点击即可加载到 Plan 编辑器,或用 × 按钮删除;再次点击同一标签会收起面板。",
|
||||
helpVideo10: "高亮颜色:当所加载的工作流实际支持该字段时,Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple 标签会变为与 GenerateUI Model 标签页相同的高亮颜色(可在 Settings 标签页自定义)——例如 Wan2.2 工作流(没有 ResolutionSelector 节点)中,Aspect Ratio / Megapixels / Multiple 字段被禁用,因此这些标签不会高亮。",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ export function applyModelTabActiveColor(color) {
|
||||
el.id = MODEL_ACTIVE_COLOR_STYLE_ID;
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.textContent = `.wfm-form-group label.wfm-model-label-active,\n.wfm-settings-summary.wfm-model-label-active,\n#wfm-gen-lora-col-title.wfm-model-label-active,\n.wfm-lab-model-view-btn.wfm-model-label-active,\n.wfm-video-form-panel label.wfm-model-label-active { color: ${val}; }`;
|
||||
el.textContent = `.wfm-form-group label.wfm-model-label-active,\n.wfm-settings-summary.wfm-model-label-active,\n#wfm-gen-lora-col-title.wfm-model-label-active,\n.wfm-lab-model-view-btn.wfm-model-label-active,\n.wfm-video-plan-content label.wfm-model-label-active { color: ${val}; }`;
|
||||
}
|
||||
|
||||
export function applyTheme(themeId) {
|
||||
|
||||
@@ -9,16 +9,21 @@
|
||||
*/
|
||||
|
||||
import { VIDEO_GROUP, VTEMP_GROUP, ensureVideoGroup } from "./gallery-tab.js";
|
||||
import { setSourcePreview } from "./video-preview.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
|
||||
// fetches VIDEO_GROUP and VTEMP_GROUP separately and merges them (see _loadImages).
|
||||
const ALL_GROUPS_VALUE = "__video_asset_all__";
|
||||
|
||||
// Display names are now identical to the underlying group names (minus the
|
||||
// "__...__" wrapping) — no more per-plan "VideoPlan:<name>" sub-groups, so the
|
||||
// dropdown is always exactly these three fixed entries.
|
||||
const _GROUPS = [ALL_GROUPS_VALUE, VIDEO_GROUP, VTEMP_GROUP];
|
||||
|
||||
const _s = {
|
||||
outputDir: "",
|
||||
group: ALL_GROUPS_VALUE,
|
||||
groups: [ALL_GROUPS_VALUE, VIDEO_GROUP, VTEMP_GROUP], // + any "VideoPlan:<name>" sub-groups
|
||||
images: [],
|
||||
selectedPath: null,
|
||||
loaded: false, // becomes true once the Asset subtab has been shown at least once
|
||||
@@ -35,32 +40,17 @@ async function _fetchOutputDir() {
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
async function _loadGroups() {
|
||||
try {
|
||||
const res = await fetch("/wfm/gallery/groups");
|
||||
if (!res.ok) throw new Error(res.status);
|
||||
const data = await res.json();
|
||||
const names = (data.groups || []).map((g) => g.name);
|
||||
const planGroups = names.filter((n) => n.startsWith("VideoPlan:"));
|
||||
_s.groups = [ALL_GROUPS_VALUE, VIDEO_GROUP, VTEMP_GROUP, ...planGroups];
|
||||
if (!_s.groups.includes(_s.group)) _s.group = ALL_GROUPS_VALUE;
|
||||
} catch (err) {
|
||||
console.warn("[VideoAsset] loadGroups failed:", err);
|
||||
}
|
||||
_renderGroupSelect();
|
||||
}
|
||||
|
||||
function _groupLabel(g) {
|
||||
if (g === ALL_GROUPS_VALUE) return "All Video Assets";
|
||||
if (g === VIDEO_GROUP) return "Video Assets";
|
||||
if (g === VTEMP_GROUP) return "Temporary (unsaved runs)";
|
||||
return g.replace(/^VideoPlan:/, "Plan: ");
|
||||
if (g === VTEMP_GROUP) return "Video Temp";
|
||||
return g;
|
||||
}
|
||||
|
||||
function _renderGroupSelect() {
|
||||
const sel = document.getElementById("wfm-video-asset-group");
|
||||
if (!sel) return;
|
||||
sel.innerHTML = _s.groups
|
||||
sel.innerHTML = _GROUPS
|
||||
.map((g) => `<option value="${g}"${g === _s.group ? " selected" : ""}>${_groupLabel(g)}</option>`)
|
||||
.join("");
|
||||
}
|
||||
@@ -86,9 +76,11 @@ async function _loadImages() {
|
||||
try {
|
||||
let images;
|
||||
if (_s.group === ALL_GROUPS_VALUE) {
|
||||
// "All Video Assets" = the union of the curated __VideoAssets__
|
||||
// group and __vtemp__ (auto-populated by not-yet-saved plan runs) —
|
||||
// not a single backend group filter, so fetch both and merge.
|
||||
// "All Video Assets" = the union of the curated __Video Assets__
|
||||
// group and __Video Temp__ (auto-populated by every plan run,
|
||||
// organized into __Video Assets__ by the user on their own
|
||||
// schedule) — not a single backend group filter, so fetch both
|
||||
// and merge.
|
||||
const [curated, temp] = await Promise.all([
|
||||
_fetchGroupImages(VIDEO_GROUP),
|
||||
_fetchGroupImages(VTEMP_GROUP),
|
||||
@@ -147,6 +139,26 @@ function _selectImage(img) {
|
||||
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
|
||||
// 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.
|
||||
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 });
|
||||
} catch (err) {
|
||||
console.warn("[VideoAsset] failed to load preview:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function _renderDetail(img) {
|
||||
@@ -156,7 +168,6 @@ function _renderDetail(img) {
|
||||
// markup skeleton is static, and those two values are assigned afterward
|
||||
// via textContent/.value, never interpolated into the HTML string itself.
|
||||
panel.innerHTML = `
|
||||
<video src="/wfm/gallery/image/serve?path=${encodeURIComponent(img.path)}" controls class="wfm-video-asset-preview"></video>
|
||||
<div class="wfm-video-asset-name" id="wfm-video-asset-name"></div>
|
||||
<label>Tags</label>
|
||||
<div class="wfm-video-asset-tags" id="wfm-video-asset-tags"></div>
|
||||
@@ -258,7 +269,7 @@ async function _saveMeta(path, data) {
|
||||
// generated videos show up as soon as the user switches over to look.
|
||||
export async function refreshVideoAssetTab() {
|
||||
if (!_s.loaded) {
|
||||
await _loadGroups();
|
||||
_renderGroupSelect();
|
||||
_s.loaded = true;
|
||||
}
|
||||
await _loadImages();
|
||||
@@ -274,10 +285,12 @@ export function initVideoAssetTab() {
|
||||
});
|
||||
document.getElementById("wfm-video-asset-refresh")?.addEventListener("click", () => _loadImages());
|
||||
|
||||
// __VideoAssets__ is never auto-created by a batch run (see video-plan-tab.js —
|
||||
// only VideoPlan:<name>/__vtemp__ are ensured there), so a fresh install or a
|
||||
// group deleted via Gallery's Manage Groups would otherwise never come back on
|
||||
// Neither reserved group is ever auto-created by a batch run itself (see
|
||||
// video-plan-tab.js's _ensureVideoAssetGroups, which only ensures
|
||||
// __Video Temp__ right before a run starts), so a fresh install or a group
|
||||
// deleted via Gallery's Manage Groups would otherwise never come back on
|
||||
// its own — add_to_group() happily records membership on individual images
|
||||
// without ever re-registering the group itself.
|
||||
// without ever re-registering the group itself. ensureVideoGroup() ensures
|
||||
// both __Video Assets__ and __Video Temp__ every time the Asset subtab inits.
|
||||
ensureVideoGroup();
|
||||
}
|
||||
|
||||
+76
-26
@@ -24,6 +24,7 @@ import { readAllPNGTextChunks } from "./metadata-tab.js";
|
||||
import { isVideoFilename, extractLastFrameBlob } from "./video-utils.js";
|
||||
import { locateVideoModelNodes, readTemplateDefaults, applyBlockToWorkflow } from "./video-workflow.js";
|
||||
import { VTEMP_GROUP } from "./gallery-tab.js";
|
||||
import { setResultPreview } from "./video-preview.js";
|
||||
|
||||
const VIDEO_BLOCK_COLORS = ["#4caf7d", "#3b4b8a", "#8e5cd9", "#e0c341", "#4ac0d9", "#9ad14b", "#e08a3c", "#d9527a"];
|
||||
const VIDEO_PLAN_PREFIX = "ws_videoplan_";
|
||||
@@ -140,6 +141,42 @@ function _initTimelineControls() {
|
||||
// Block editor (First/Last image, Prompt, Duration, color)
|
||||
// ============================================
|
||||
|
||||
// Mirrors the "field matches the loaded workflow" highlight GenerateUI's Model tab
|
||||
// uses (wfm-model-label-active, color customizable in Settings) — carried over from
|
||||
// the pre-Plan/Asset/Edit single-shot form, where it lived directly in video-tab.js.
|
||||
// First/Last Image/Prompt/Duration labels get re-created every block switch
|
||||
// (_renderBlockEditor rebuilds the whole editor's innerHTML), so this must be
|
||||
// re-applied there too, not just once on workflow load.
|
||||
const _FIELD_LABEL_IDS = [
|
||||
"wfm-video-block-prompt-label",
|
||||
"wfm-video-block-duration-label",
|
||||
"wfm-video-block-first-label",
|
||||
"wfm-video-block-last-label",
|
||||
"wfm-video-plan-aspect-ratio-label",
|
||||
"wfm-video-plan-megapixels-label",
|
||||
"wfm-video-plan-multiple-label",
|
||||
];
|
||||
|
||||
function _setFieldActive(id, active) {
|
||||
document.getElementById(id)?.classList.toggle("wfm-model-label-active", !!active);
|
||||
}
|
||||
|
||||
function _updateFieldHighlights() {
|
||||
const nodes = _templateWorkflow ? locateVideoModelNodes(_templateWorkflow) : null;
|
||||
if (!nodes) {
|
||||
_FIELD_LABEL_IDS.forEach((id) => _setFieldActive(id, false));
|
||||
return;
|
||||
}
|
||||
const hasResolution = !!nodes.resolutionNode;
|
||||
_setFieldActive("wfm-video-block-prompt-label", true);
|
||||
_setFieldActive("wfm-video-block-duration-label", true);
|
||||
_setFieldActive("wfm-video-block-first-label", nodes.firstFrameSlot !== -1);
|
||||
_setFieldActive("wfm-video-block-last-label", nodes.lastFrameSlot !== -1);
|
||||
_setFieldActive("wfm-video-plan-aspect-ratio-label", hasResolution);
|
||||
_setFieldActive("wfm-video-plan-megapixels-label", hasResolution);
|
||||
_setFieldActive("wfm-video-plan-multiple-label", hasResolution);
|
||||
}
|
||||
|
||||
function _frameFieldHtml(which, filename) {
|
||||
const previewStyle = filename ? "" : "display:none;";
|
||||
const previewSrc = filename ? `${comfyUI.baseUrl}/view?filename=${encodeURIComponent(filename)}&type=input` : "";
|
||||
@@ -163,7 +200,7 @@ function _frameFieldHtml(which, filename) {
|
||||
function _blockEditorHtml(block, isFirstBlock) {
|
||||
return `
|
||||
<div class="wfm-video-block-col wfm-video-block-first-col">
|
||||
<label>First Image</label>
|
||||
<label id="wfm-video-block-first-label">First Image</label>
|
||||
<select id="wfm-video-block-first-mode" class="wfm-input">
|
||||
<option value="none" ${block.first_image_mode === "none" ? "selected" : ""}>None</option>
|
||||
<option value="explicit" ${block.first_image_mode === "explicit" ? "selected" : ""}>Specify image</option>
|
||||
@@ -175,12 +212,12 @@ function _blockEditorHtml(block, isFirstBlock) {
|
||||
</div>
|
||||
|
||||
<div class="wfm-video-block-col wfm-video-block-last-col">
|
||||
<label>Last Image (optional)</label>
|
||||
<label id="wfm-video-block-last-label">Last Image (optional)</label>
|
||||
${_frameFieldHtml("last", block.last_image_filename)}
|
||||
</div>
|
||||
|
||||
<div class="wfm-video-block-col wfm-video-block-prompt-col">
|
||||
<label>Prompt</label>
|
||||
<label id="wfm-video-block-prompt-label">Prompt</label>
|
||||
<textarea id="wfm-video-block-prompt" class="wfm-textarea" rows="8">${escapeHtml(block.prompt || "")}</textarea>
|
||||
</div>
|
||||
|
||||
@@ -189,7 +226,7 @@ function _blockEditorHtml(block, isFirstBlock) {
|
||||
<span class="wfm-video-color-swatch" style="background:${escapeHtml(block.color)}"></span>
|
||||
<span>color</span>
|
||||
</div>
|
||||
<label>Duration (seconds)</label>
|
||||
<label id="wfm-video-block-duration-label">Duration (seconds)</label>
|
||||
<input type="number" id="wfm-video-block-duration" class="wfm-input" step="0.5" min="0.5" value="${block.duration}">
|
||||
</div>
|
||||
`;
|
||||
@@ -271,6 +308,8 @@ function _renderBlockEditor() {
|
||||
e.target.value = block.duration;
|
||||
_renderTimeline();
|
||||
});
|
||||
|
||||
_updateFieldHighlights();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -396,6 +435,7 @@ async function _loadPlanWorkflow() {
|
||||
_templateFilename = filename;
|
||||
const nameEl = document.getElementById("wfm-video-wf-name");
|
||||
if (nameEl) nameEl.textContent = filename;
|
||||
_updateFieldHighlights();
|
||||
showToast(t("videoWorkflowLoaded", filename), "success");
|
||||
} catch (err) {
|
||||
showToast(`${t("videoWorkflowLoadFailed")}: ${err.message}`, "error");
|
||||
@@ -416,31 +456,24 @@ async function _fetchOutputDir() {
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
function _currentPlanName() {
|
||||
return _plan.planFilename ? _stripVideoPlanPrefix(_plan.planFilename.replace(/\.json$/i, "")) : "";
|
||||
}
|
||||
|
||||
// __VideoAssets__ is a manually-curated group (users add things to it via
|
||||
// Gallery themselves) — a batch run never writes to it directly. What a run's
|
||||
// own output lands in instead: the plan's own VideoPlan:<name> group once the
|
||||
// plan has been saved, or __vtemp__ (a reserved, auto-managed scratch group,
|
||||
// cleared via the "VtC" button in Gallery's detail pane) while it hasn't.
|
||||
function _targetGroupName() {
|
||||
const planName = _currentPlanName();
|
||||
return planName ? `VideoPlan:${planName}` : VTEMP_GROUP;
|
||||
}
|
||||
|
||||
// __Video Assets__ is a manually-curated group (users add things to it via
|
||||
// Gallery themselves) — a batch run never writes to it directly. Every run's
|
||||
// own output lands in __Video Temp__ instead (a reserved, auto-managed scratch
|
||||
// group, cleared via the "Clear __Video Temp__" button in Gallery's detail
|
||||
// pane), regardless of whether the plan itself has been saved — organizing a
|
||||
// run's output into __Video Assets__ is left entirely to the user, on their
|
||||
// own schedule (previously this auto-filed into a per-plan "VideoPlan:<name>"
|
||||
// group instead, which was removed since users found manual curation simpler).
|
||||
async function _ensureVideoAssetGroups() {
|
||||
const group = _targetGroupName();
|
||||
try {
|
||||
const res = await fetch("/wfm/gallery/groups/ensure", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: group }),
|
||||
body: JSON.stringify({ name: VTEMP_GROUP }),
|
||||
});
|
||||
if (!res.ok) console.warn(`[VideoPlan] ensure group "${group}" failed: HTTP ${res.status}`);
|
||||
if (!res.ok) console.warn(`[VideoPlan] ensure group "${VTEMP_GROUP}" failed: HTTP ${res.status}`);
|
||||
} catch (err) {
|
||||
console.warn(`[VideoPlan] ensure group "${group}" error:`, err);
|
||||
console.warn(`[VideoPlan] ensure group "${VTEMP_GROUP}" error:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +483,6 @@ async function _addOutputsToVideoGroups(images) {
|
||||
console.warn("[VideoPlan] output dir unknown — skipping group tagging for", images);
|
||||
return;
|
||||
}
|
||||
const group = _targetGroupName();
|
||||
for (const img of images) {
|
||||
if (img.type !== "output") continue;
|
||||
const parts = [_outputDir];
|
||||
@@ -458,12 +490,12 @@ async function _addOutputsToVideoGroups(images) {
|
||||
parts.push(img.filename);
|
||||
const path = parts.join("/");
|
||||
try {
|
||||
const res = await fetch(`/wfm/gallery/groups/${encodeURIComponent(group)}/add`, {
|
||||
const res = await fetch(`/wfm/gallery/groups/${encodeURIComponent(VTEMP_GROUP)}/add`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path }),
|
||||
});
|
||||
if (!res.ok) console.warn(`[VideoPlan] add "${path}" to "${group}" failed: HTTP ${res.status}`);
|
||||
if (!res.ok) console.warn(`[VideoPlan] add "${path}" to "${VTEMP_GROUP}" failed: HTTP ${res.status}`);
|
||||
} catch (err) {
|
||||
console.warn(`[VideoPlan] add "${path}" to "${group}" error:`, err);
|
||||
console.warn(`[VideoPlan] add "${path}" to "${VTEMP_GROUP}" error:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,6 +623,15 @@ async function _runVideoPlanBatch() {
|
||||
await _addOutputsToVideoGroups(outputMedia);
|
||||
await _saveGeneratedVideoMeta(outputMedia, apiWorkflow);
|
||||
|
||||
// Show this block's clip in the center panel's result pane as soon as
|
||||
// it finishes, independent of whether chain_previous below needs it too.
|
||||
const resultVid = outputMedia.find((m) => isVideoFilename(m.filename));
|
||||
if (resultVid) {
|
||||
setResultPreview(_resultMediaUrl(resultVid), {
|
||||
kind: "output", filename: resultVid.filename, subfolder: resultVid.subfolder || "", type: resultVid.type || "output",
|
||||
});
|
||||
}
|
||||
|
||||
// Only needed as a fallback for the NEXT block's "chain_previous" —
|
||||
// skip the extraction entirely when this block already has its own
|
||||
// explicit last_image_filename (that value is used directly instead).
|
||||
@@ -842,6 +883,15 @@ async function _loadPlanFromServer(filename) {
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point for the Project panel's saved-plan list (video-project-tab.js) —
|
||||
// loads a plan by filename the same way drag&drop does, then jumps to the Plan
|
||||
// subtab so the freshly-loaded editor is actually visible (mirrors
|
||||
// loadWorkflowIntoVideoEditor above).
|
||||
export async function openSavedVideoPlan(filename) {
|
||||
await _loadPlanFromServer(filename);
|
||||
document.querySelector('.wfm-video-subtab-btn[data-video-subtab="plan"]')?.click();
|
||||
}
|
||||
|
||||
function _clearPlan() {
|
||||
if (!window.confirm(t("videoConfirmClearPlan"))) return;
|
||||
_plan = _emptyPlanState();
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Video Tab - shared state for the two center-panel preview panes:
|
||||
* "source" (#wfm-video-source-preview-video — an Asset selection or a
|
||||
* locally-dropped file) and "result" (#wfm-video-preview-video — the latest
|
||||
* generated clip). video-asset-tab.js (Asset selection), video-plan-tab.js
|
||||
* (batch run results) and video-tab.js (the Video Source drop zone, Frame/GIF
|
||||
* tools) all need to read or write these. Pulling this into its own module
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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" },
|
||||
};
|
||||
|
||||
const _objectUrls = { source: null, result: null };
|
||||
|
||||
// Whichever pane most recently received a video 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".
|
||||
let _activePane = null; // "source" | "result" | null
|
||||
let _activeSource = null;
|
||||
|
||||
function _setPreview(pane, url, source) {
|
||||
const ids = _PANES[pane];
|
||||
const video = document.getElementById(ids.video);
|
||||
const placeholder = document.getElementById(ids.placeholder);
|
||||
if (!video || !placeholder) return;
|
||||
|
||||
if (_objectUrls[pane]) {
|
||||
URL.revokeObjectURL(_objectUrls[pane]);
|
||||
_objectUrls[pane] = null;
|
||||
}
|
||||
if (url && url.startsWith("blob:")) _objectUrls[pane] = url;
|
||||
|
||||
if (url) {
|
||||
video.src = url;
|
||||
video.style.display = "block";
|
||||
placeholder.style.display = "none";
|
||||
_activePane = pane;
|
||||
_activeSource = source;
|
||||
} else {
|
||||
video.pause();
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
video.style.display = "none";
|
||||
placeholder.style.display = "";
|
||||
if (_activePane === pane) { _activePane = null; _activeSource = null; }
|
||||
}
|
||||
}
|
||||
|
||||
// Sets (or clears, with url=null) the Asset-selection/Video-Source-drop pane.
|
||||
export function setSourcePreview(url, source) { _setPreview("source", url, source); }
|
||||
|
||||
// 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); }
|
||||
|
||||
// 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.
|
||||
export function getActivePreviewSource() { return _activeSource; }
|
||||
|
||||
// The GIF tab uploads a still-local source on first use and needs to remember
|
||||
// the resulting server-side reference so a repeat conversion doesn't re-upload —
|
||||
// this rewrites the active source in place without touching either <video> element.
|
||||
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.
|
||||
export function getActivePreviewVideoElement() {
|
||||
if (!_activePane) return null;
|
||||
return document.getElementById(_PANES[_activePane].video);
|
||||
}
|
||||
|
||||
// Both panes' <video> elements share the same persistent volume setting.
|
||||
export function getAllPreviewVideoElements() {
|
||||
return Object.values(_PANES).map((ids) => document.getElementById(ids.video)).filter(Boolean);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { showToast } from "./app.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { escapeHtml } from "./util.js";
|
||||
import { openSavedVideoPlan } from "./video-plan-tab.js";
|
||||
|
||||
const _s = {
|
||||
plans: [],
|
||||
activeFilename: null,
|
||||
loaded: false, // becomes true once the Project subtab has been shown at least once
|
||||
};
|
||||
|
||||
function _formatUpdatedAt(iso) {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
function _renderList() {
|
||||
const list = document.getElementById("wfm-video-project-list");
|
||||
if (!list) return;
|
||||
if (_s.plans.length === 0) {
|
||||
list.innerHTML = `<span class="wfm-placeholder">${t("videoProjectEmpty")}</span>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = "";
|
||||
for (const plan of _s.plans) list.appendChild(_makeItem(plan));
|
||||
}
|
||||
|
||||
function _makeItem(plan) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "wfm-video-project-item" + (plan.filename === _s.activeFilename ? " active" : "");
|
||||
item.dataset.filename = plan.filename;
|
||||
|
||||
const thumb = document.createElement("img");
|
||||
thumb.className = "wfm-video-project-item-thumb";
|
||||
thumb.loading = "lazy";
|
||||
if (plan.thumbnail) thumb.src = plan.thumbnail;
|
||||
thumb.onerror = () => { thumb.style.visibility = "hidden"; };
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "wfm-video-project-item-info";
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "wfm-video-project-item-name";
|
||||
name.textContent = plan.name || plan.filename;
|
||||
name.title = plan.filename;
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "wfm-video-project-item-meta";
|
||||
const blockLabel = t("videoProjectBlockCount", plan.block_count ?? 0);
|
||||
const updated = _formatUpdatedAt(plan.updated_at);
|
||||
meta.textContent = updated ? `${blockLabel} · ${updated}` : blockLabel;
|
||||
meta.title = plan.note || "";
|
||||
|
||||
info.append(name, meta);
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.type = "button";
|
||||
deleteBtn.className = "wfm-video-project-item-delete";
|
||||
deleteBtn.textContent = "×";
|
||||
deleteBtn.title = t("delete");
|
||||
deleteBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
_deletePlan(plan);
|
||||
});
|
||||
|
||||
item.append(thumb, info, deleteBtn);
|
||||
item.addEventListener("click", () => _openPlan(plan));
|
||||
return item;
|
||||
}
|
||||
|
||||
async function _openPlan(plan) {
|
||||
_s.activeFilename = plan.filename;
|
||||
_renderList();
|
||||
await openSavedVideoPlan(plan.filename);
|
||||
}
|
||||
|
||||
async function _deletePlan(plan) {
|
||||
if (!window.confirm(t("videoProjectDeleteConfirm", plan.name || plan.filename))) return;
|
||||
try {
|
||||
const res = await fetch("/api/wfm/video/plans/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename: plan.filename }),
|
||||
});
|
||||
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();
|
||||
} catch (err) {
|
||||
showToast(`${t("videoProjectDeleteFailed")}: ${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() {
|
||||
const list = document.getElementById("wfm-video-project-list");
|
||||
if (!list) return;
|
||||
try {
|
||||
const res = await fetch("/api/wfm/video/plans");
|
||||
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>`;
|
||||
}
|
||||
_s.loaded = true;
|
||||
}
|
||||
|
||||
export function initVideoProjectTab() {
|
||||
document.getElementById("wfm-video-project-refresh")?.addEventListener("click", () => refreshVideoProjectTab());
|
||||
}
|
||||
+46
-71
@@ -16,25 +16,22 @@ import { comfyUI } from "./comfyui-client.js";
|
||||
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 {
|
||||
setSourcePreview, getActivePreviewSource, updateActivePreviewSourceRef,
|
||||
getActivePreviewVideoElement, getAllPreviewVideoElements,
|
||||
} from "./video-preview.js";
|
||||
|
||||
export { loadWorkflowIntoVideoEditor };
|
||||
|
||||
const state = {
|
||||
// Whatever's currently shown in the center preview frame — a just-generated
|
||||
// result, or a video dropped/picked in the Video Source panel. Used by the
|
||||
// Frame/GIF property tabs so they work on any video, not just generated ones.
|
||||
// { kind: "output"|"input", filename, subfolder, type } for anything already on
|
||||
// the server, or { kind: "local", file } for a picked file not yet uploaded.
|
||||
previewSource: null,
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Subtab switching — two INDEPENDENT groups sharing the same .wfm-video-subtab-*
|
||||
// look, kept deliberately uncoupled: the sidebar's lone Asset button just
|
||||
// shows/hides its own panel, while the center Plan/Edit pair is its own
|
||||
// exclusive 2-way tab. Querying each scoped to its own container (rather than
|
||||
// one global querySelectorAll) is what keeps clicking one from affecting the
|
||||
// other.
|
||||
// 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
|
||||
// scoped to its own container (rather than one global querySelectorAll) is
|
||||
// what keeps clicking one from affecting the other.
|
||||
// ============================================
|
||||
|
||||
function _initCenterSubtabToggle() {
|
||||
@@ -52,19 +49,27 @@ function _initCenterSubtabToggle() {
|
||||
});
|
||||
}
|
||||
|
||||
// The sidebar currently holds only "Asset" (Plan/Edit moved to the center
|
||||
// panel — see above), so it behaves as a simple show/hide toggle rather than
|
||||
// an N-way exclusive tab: there's nothing else in its group to switch to.
|
||||
function _initAssetToggle() {
|
||||
// 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.
|
||||
function _initSidebarSubtabToggle() {
|
||||
const sidebar = document.querySelector(".wfm-video-form-panel");
|
||||
const btn = sidebar?.querySelector(".wfm-video-subtab-btn");
|
||||
const panel = document.getElementById("wfm-video-subtab-asset");
|
||||
if (!btn || !panel) return;
|
||||
btn.addEventListener("click", () => {
|
||||
const willShow = panel.style.display === "none";
|
||||
panel.style.display = willShow ? "" : "none";
|
||||
btn.classList.toggle("active", willShow);
|
||||
if (willShow) refreshVideoAssetTab();
|
||||
if (!sidebar) return;
|
||||
const buttons = sidebar.querySelectorAll(".wfm-video-subtab-btn");
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const target = btn.dataset.videoSubtab;
|
||||
const panel = document.getElementById(`wfm-video-subtab-${target}`);
|
||||
const willShow = !!panel && panel.style.display === "none";
|
||||
buttons.forEach((b) => b.classList.remove("active"));
|
||||
sidebar.querySelectorAll(".wfm-video-subtab-panel").forEach((p) => { p.style.display = "none"; });
|
||||
if (willShow) {
|
||||
btn.classList.add("active");
|
||||
panel.style.display = "";
|
||||
if (target === "asset") refreshVideoAssetTab();
|
||||
else if (target === "project") refreshVideoProjectTab();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,41 +94,6 @@ function _wireDropZone(dropZoneId, fileInputId, onFile) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 結果表示
|
||||
// ============================================
|
||||
|
||||
// Sets (or clears) the center preview frame's video and records where it came from —
|
||||
// `source` is what the Frame/GIF property tabs act on (see state.previewSource above).
|
||||
// Revokes the previous blob: URL (if any) so picking a series of local files doesn't
|
||||
// leak memory.
|
||||
let _previewObjectUrl = null;
|
||||
|
||||
function _setPreviewMedia(url, source) {
|
||||
state.previewSource = source;
|
||||
const video = document.getElementById("wfm-video-preview-video");
|
||||
const placeholder = document.getElementById("wfm-video-preview-placeholder");
|
||||
if (!video || !placeholder) return;
|
||||
|
||||
if (_previewObjectUrl) {
|
||||
URL.revokeObjectURL(_previewObjectUrl);
|
||||
_previewObjectUrl = null;
|
||||
}
|
||||
if (url && url.startsWith("blob:")) _previewObjectUrl = url;
|
||||
|
||||
if (url) {
|
||||
video.src = url;
|
||||
video.style.display = "block";
|
||||
placeholder.style.display = "none";
|
||||
} else {
|
||||
video.pause();
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
video.style.display = "none";
|
||||
placeholder.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// i18nの適用(app.jsのapplyI18nToHtml()と同じ「個別要素にt()を適用」方式)
|
||||
// ============================================
|
||||
@@ -138,9 +108,12 @@ function _applyVideoI18n() {
|
||||
setText("wfm-video-source-hint", "videoSourceHint");
|
||||
setText("wfm-video-source-drop-label", "videoSourceDropLabel");
|
||||
setText("wfm-video-source-clear", "clear");
|
||||
setText("wfm-video-preview-placeholder", "videoNoVideo");
|
||||
setText("wfm-video-source-preview-placeholder", "videoSourcePreviewPlaceholder");
|
||||
setText("wfm-video-preview-placeholder", "videoResultPreviewPlaceholder");
|
||||
setText("wfm-video-prop-tab-frame", "videoPropFrame");
|
||||
setText("wfm-video-prop-tab-gif", "videoPropGif");
|
||||
setText("wfm-video-subtab-btn-project", "videoPropProject");
|
||||
setText("wfm-video-project-hint", "videoProjectHint");
|
||||
setText("wfm-video-frame-hint", "videoFrameHint");
|
||||
setText("wfm-video-frame-capture-btn", "videoCaptureFrame");
|
||||
setText("wfm-video-frame-save-btn", "videoSaveToOutput");
|
||||
@@ -160,12 +133,12 @@ function _wireVideoSourcePanel() {
|
||||
_wireDropZone("wfm-video-source-drop", "wfm-video-source-file", (file) => {
|
||||
if (!file.type.startsWith("video/")) return;
|
||||
const statusEl = document.getElementById("wfm-video-source-status");
|
||||
_setPreviewMedia(URL.createObjectURL(file), { kind: "local", file });
|
||||
setSourcePreview(URL.createObjectURL(file), { kind: "local", file });
|
||||
if (statusEl) { statusEl.textContent = file.name; statusEl.style.color = ""; }
|
||||
});
|
||||
|
||||
document.getElementById("wfm-video-source-clear")?.addEventListener("click", () => {
|
||||
_setPreviewMedia(null, null);
|
||||
setSourcePreview(null, null);
|
||||
const statusEl = document.getElementById("wfm-video-source-status");
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
const fileInput = document.getElementById("wfm-video-source-file");
|
||||
@@ -215,7 +188,7 @@ function _initFrameTab() {
|
||||
const statusEl = document.getElementById("wfm-video-frame-status");
|
||||
|
||||
captureBtn?.addEventListener("click", () => {
|
||||
const video = document.getElementById("wfm-video-preview-video");
|
||||
const video = getActivePreviewVideoElement();
|
||||
if (!video || video.style.display === "none" || !video.videoWidth) {
|
||||
showToast(t("videoNoSourceLoaded"), "error");
|
||||
return;
|
||||
@@ -277,7 +250,7 @@ function _initGifTab() {
|
||||
const statusEl = document.getElementById("wfm-video-gif-status");
|
||||
|
||||
convertBtn?.addEventListener("click", async () => {
|
||||
if (!state.previewSource) {
|
||||
if (!getActivePreviewSource()) {
|
||||
showToast(t("videoNoSourceLoaded"), "error");
|
||||
return;
|
||||
}
|
||||
@@ -287,12 +260,12 @@ function _initGifTab() {
|
||||
if (progressText) progressText.textContent = "Uploading...";
|
||||
|
||||
try {
|
||||
let ref = state.previewSource;
|
||||
let ref = getActivePreviewSource();
|
||||
if (ref.kind === "local") {
|
||||
const result = await comfyUI.uploadImage(ref.file, ref.file.name);
|
||||
ref = { kind: "input", filename: result.name, subfolder: result.subfolder || "", type: "input" };
|
||||
// Cache the uploaded reference so a repeat conversion doesn't re-upload.
|
||||
state.previewSource = ref;
|
||||
updateActivePreviewSourceRef(ref);
|
||||
}
|
||||
|
||||
const start = Number(document.getElementById("wfm-video-gif-start")?.value) || 0;
|
||||
@@ -344,9 +317,10 @@ function _initGifTab() {
|
||||
export function initVideoTab() {
|
||||
_applyVideoI18n();
|
||||
_initCenterSubtabToggle();
|
||||
_initAssetToggle();
|
||||
_initSidebarSubtabToggle();
|
||||
initVideoPlanTab();
|
||||
initVideoAssetTab();
|
||||
initVideoProjectTab();
|
||||
|
||||
_wireVideoSourcePanel();
|
||||
_initPropTabs();
|
||||
@@ -354,6 +328,7 @@ export function initVideoTab() {
|
||||
_initGifTab();
|
||||
|
||||
// 保存済みの音量を適用し、以降ユーザーがネイティブコントロールで変更した音量も
|
||||
// 自動保存する(要素自体は永続的なので初期化時に一度呼べば十分)。
|
||||
applyStoredVideoVolume(document.getElementById("wfm-video-preview-video"));
|
||||
// 自動保存する(要素自体は永続的なので初期化時に一度呼べば十分)。両方のプレビュー
|
||||
// ペイン(Asset/Source用・生成結果用)の<video>要素に適用する。
|
||||
getAllPreviewVideoElements().forEach((video) => applyStoredVideoVolume(video));
|
||||
}
|
||||
|
||||
+37
-19
@@ -2090,17 +2090,19 @@
|
||||
|
||||
<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.</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 subtabs handle batch generation and (future) editing; the sidebar's Asset / Project subtabs handle browsing generated videos and saved plans.</p>
|
||||
<div class="wfm-help-card">
|
||||
<ul>
|
||||
<li id="wfm-help-video-1">Set first_frame (optional) / last_frame (optional) images, prompt, aspect_ratio / megapixels / multiple (resolution), duration (seconds), and noise_seed, then click Generate. Leaving first_frame empty runs the workflow text-only (bypassed), and workflows with no LoadImage node at all are also supported.</li>
|
||||
<li id="wfm-help-video-2">Model selection (UNet / CLIP / VAE / Audio VAE) is fixed to the loaded workflow's defaults and cannot be changed from this tab. For workflows with no ResolutionSelector node (e.g. Wan2.2), resolution also stays fixed to the loaded workflow's defaults and the Aspect Ratio / Megapixels / Multiple fields are disabled.</li>
|
||||
<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. Unsupported workflows show an error instead.</li>
|
||||
<li id="wfm-help-video-4">Generated videos (mp4) can also be browsed and played back in the Gallery tab.</li>
|
||||
<li id="wfm-help-video-5">The Video Source panel next to the preview lets you drop or pick any local video (not just a generated one) to use the Frame/GIF tools below without running the loaded video workflow first; Clear resets it back to empty.</li>
|
||||
<li id="wfm-help-video-1">Plan subtab: set Aspect Ratio / Megapixels / Multiple as this plan's base settings, then split the timeline into blocks with the + Split / + Block / Delete buttons. Each block has its own Prompt, Duration (seconds), 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>
|
||||
<li id="wfm-help-video-2">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), the workflow's own resolution is used as-is and the Aspect Ratio / Megapixels / Multiple fields are disabled.</li>
|
||||
<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-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">Highlight color: the Prompt / Duration / First Frame / Last Frame / Aspect Ratio / Megapixels / Multiple 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 since those fields are disabled.</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-10">Highlight color: the Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple 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 since those fields are disabled.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="wfm-help-jump-btn wfm-btn wfm-btn-sm" data-jump-tab="video">Go to Video Tab</button>
|
||||
@@ -2691,7 +2693,8 @@
|
||||
<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">Asset</button>
|
||||
<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のグループ機能を再利用) -->
|
||||
@@ -2706,15 +2709,20 @@
|
||||
<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;">
|
||||
<p class="wfm-video-prop-hint" id="wfm-video-project-hint" style="flex:1;margin:0;">Saved plans</p>
|
||||
<button type="button" id="wfm-video-project-refresh" class="wfm-btn wfm-btn-sm" title="Refresh">↻</button>
|
||||
</div>
|
||||
<div class="wfm-video-project-list" id="wfm-video-project-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 中央ペイン: プレビュー(常時表示) + Plan/Editタブ(バッチ生成編集・将来の動画編集機能)。
|
||||
左のAssetサブタブ切替とは独立して常に表示される -->
|
||||
<div class="wfm-video-center-panel">
|
||||
<div class="wfm-video-preview-row">
|
||||
<div class="wfm-video-preview-frame" id="wfm-video-preview-frame">
|
||||
<span class="wfm-placeholder" id="wfm-video-preview-placeholder">No video</span>
|
||||
<video id="wfm-video-preview-video" class="wfm-video-preview-el" controls style="display:none;"></video>
|
||||
</div>
|
||||
<div class="wfm-video-source-panel">
|
||||
<div class="wfm-video-pane-header" id="wfm-video-source-label">Video Source</div>
|
||||
<p class="wfm-video-prop-hint" id="wfm-video-source-hint">Load any video here to use Frame/GIF tools without generating one.</p>
|
||||
@@ -2729,6 +2737,14 @@
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wfm-video-subtab-nav wfm-video-center-subtab-nav">
|
||||
@@ -2739,22 +2755,24 @@
|
||||
<!-- Plan: バッチ動画生成のタイムライン編集・実行(中央下部、Planタブ選択時のみ表示) -->
|
||||
<div id="wfm-video-plan-content" class="wfm-video-subtab-panel wfm-video-plan-content" data-video-subtab-panel="plan">
|
||||
<div class="wfm-video-plan-topbar">
|
||||
<span id="wfm-video-wf-name" class="wfm-video-wf-name">No workflow loaded</span>
|
||||
<div class="wfm-lab-plan-workflow-row" id="wfm-video-plan-workflow-row" style="display:none;">
|
||||
<span id="wfm-video-plan-workflow-name" class="wfm-lab-plan-workflow-name"></span>
|
||||
<button type="button" id="wfm-video-plan-workflow-load-btn" class="wfm-btn wfm-btn-xs" title="Load this plan's recorded workflow">Open Workflow</button>
|
||||
<div class="wfm-video-wf-row">
|
||||
<span id="wfm-video-wf-name" class="wfm-video-wf-name">No workflow loaded</span>
|
||||
<div class="wfm-lab-plan-workflow-row" id="wfm-video-plan-workflow-row" style="display:none;">
|
||||
<span id="wfm-video-plan-workflow-name" class="wfm-lab-plan-workflow-name"></span>
|
||||
<button type="button" id="wfm-video-plan-workflow-load-btn" class="wfm-btn wfm-btn-xs" title="Load this plan's recorded workflow">Open Workflow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-video-plan-basesettings">
|
||||
<div>
|
||||
<label for="wfm-video-plan-aspect-ratio">Aspect Ratio</label>
|
||||
<label for="wfm-video-plan-aspect-ratio" id="wfm-video-plan-aspect-ratio-label">Aspect Ratio</label>
|
||||
<select id="wfm-video-plan-aspect-ratio" class="wfm-input"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wfm-video-plan-megapixels">Megapixels</label>
|
||||
<label for="wfm-video-plan-megapixels" id="wfm-video-plan-megapixels-label">Megapixels</label>
|
||||
<input type="number" id="wfm-video-plan-megapixels" class="wfm-input" step="0.01" min="0.01" value="0.2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wfm-video-plan-multiple">Multiple</label>
|
||||
<label for="wfm-video-plan-multiple" id="wfm-video-plan-multiple-label">Multiple</label>
|
||||
<input type="number" id="wfm-video-plan-multiple" class="wfm-input" step="8" min="8" value="32">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user