Compare commits

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

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

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

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

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

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

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

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

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

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

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

Text overlays and audio settings are persisted in Edit projects.

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 17:23:13 +09:00
15 changed files with 1951 additions and 68 deletions
+2
View File
@@ -2,6 +2,8 @@
Condensed version-by-version index of DEVLOG.md (each line: version + one-line summary of what changed and why). Full rationale, code details, and "How to apply" lessons live in the excluded DEVLOG.md itself; this index exists so graphify can extract a queryable semantic node per release without feeding the full log to the local Ollama model. Regenerate with `python tools/generate_doc_index.py devlog` whenever DEVLOG.md changes.
- **v0.7.4(2026-09-29)** — Video Edit: BGM/音声トラック合成(Phase 4)— 複数クリップ結合時の音声消失も解消
- **v0.7.3(2026-09-24)** — GenerateUIタブにQwen Image 2.1(TextEncodeQwenImage21)対応を追加
- **v0.7.2(2026-09-18)** — Workflowタブにソート機能追加(Date/Name 昇順降順)
- **v0.7.1(2026-09-17)** — セキュリティ修正: Tagger VLM の Unsloth 経路でAPIキーがSSRFにより外部送信され得る不備を修正
- **v0.7.0(2026-09-17)** — プレビュー枠の高さ固定・AssetサブタブをPlan/Editと並ぶ中央下部へ移設・トリムパネルにClipchamp風スクラバー追加・Frame保存のVideo Assets登録とAsset→Plan画像連携
+58
View File
@@ -2,6 +2,64 @@
---
## v0.7.4(2026-09-29)
### Video Edit: BGM/音声トラック合成(Phase 4)— 複数クリップ結合時の音声消失も解消
[VIDEO_EDIT_TAB_PLAN.md](VIDEO_EDIT_TAB_PLAN.md)のPhase 4。着手前に既存の動画編集系ノードを調査し、ComfyUI Core 0.37.0の音声ノード群(`LoadAudio`/`TrimAudioDuration`/`AudioConcat`/`AudioMerge`/`AudioAdjustVolume`/`EmptyAudio`)と`ConcatenateVideo`の`complete_audio`入力(結合動画全体の音声を上書き)だけで、書き出しグラフ内で完結できると判明したため、計画にあった独自`mix-audio` APIは作らなかった。音声トラックの組み方はH3Studioのreel書き出し(クリップ音声を尺にconform→連結→musicを重ねる)を参考にした。
**グラフ構成**: クリップごとに`EmptyAudio(クリップ尺)`+`AudioMerge(自クリップの音声)`で尺ぴったりのセグメントを作り(`AudioMerge`はaudio2をaudio1の長さにpad/trimし、音声なしクリップ=Noneならaudio1をそのまま返す)、`AudioConcat`で連結→(元音量≠0dBなら`AudioAdjustVolume`)→BGM(`LoadAudio→TrimAudioDuration(開始位置, 総尺)→AudioAdjustVolume`)を`AudioMerge`で重ねて`complete_audio`へ渡す。単一の動画クリップで元音声そのまま・BGMなしの場合のみ従来通り再エンコードなしでパススルー。
**UI**: 書き出しパネルにオーディオ欄(元の音声を残す+音量dB、BGMファイル、BGM音量、BGM開始位置)。BGMは「BGM選択...」ボタン、オーディオ欄への音声/動画ファイルのドラッグ&ドロップ、AssetサブタブのVideoに追加した「BGMに設定(音声のみ)」(動画の音声トラックを使う。Galleryは音声ファイルを扱わないため)で指定できる。▶プレビュー中はBGMも`<audio>`で同時再生。クリアボタンでオーディオ欄も初期状態に戻る(BGMのみ設定されている状態でもクリア可能)。
**実機で判明した注意点**: 生の`Video Slice`出力と画像由来の`CreateVideo`クリップを`ConcatenateVideo`へ直接混ぜると`Video chunk N could not be encoded compatibly`(extradata/color space不一致)で失敗する——complete_audioの有無に関係なく。既存の「複数クリップ時は全クリップを`GetVideoComponents→CreateVideo`で再構成する」処理は音声対策だけでなくこのためにも必要だったので維持した。
**検証**: 動画2本(片方トリム)+静止画+BGM(-6dB)をUIから書き出し、尺6.625秒・音声同尺、区間ごとのRMSで元音声+BGM/BGMのみを確認。音声なし動画(audio=None)+BGM、mp4をBGMにした書き出しも確認。
### Video Edit: テキストオーバーレイ(Phase 3)
クリップ単位のテキスト(文字・クリップ内の開始/終了秒・サイズ(フレーム高さ%)・色・9方向アンカー・縁取り・背景)。クリップ相対時刻で保持するので並べ替えに追従し、書き出し時に絶対時刻へ変換する。
Core `TextOverlay`は上/下固定・区間指定なし・既定フォントがCJK非対応、既存のテキスト系カスタムノード(advanced-textoverlay等)も静止画向けか独立パイプラインで、かつ全フレームをIMAGEテンソル化するとメモリが厳しい(720p×720フレームで約8GB)ため、新設の`POST /api/wfm/video/edit/overlay-text`(`VideoService.overlay_text_on_video()`、`asyncio.to_thread`)でPyAVストリーム処理にした: 1フレームずつデコード→該当区間のテキストレイヤー(事前に1回だけ描画)をPillowで合成→libx264でエンコード、音声パケットは再エンコードせずリマックス。書き出しはグラフ出力を`wfm_edit_pre`中間ファイルに保存→焼き込み→中間ファイル削除(`wfm_edit_pre`接頭辞かつoutput内のみ削除対象)の2段構成。フォントはmeiryo→YuGothM→NotoSansJP→msgothic→arialの順に探索。プレビューはSource/Result枠上にCSSで近似表示(`video-preview.js`に`getPreviewPaneElements()`を追加)。
**検証**: ComfyUI再起動後、動画+画像クリップに英日テキスト2件を付けて書き出し、各時刻のフレームで表示/非表示・位置・日本語描画・背景、音声長の一致、中間ファイル削除を確認。
### Video Edit: クロップ(Phase 2)
動画クリップごとに正規化座標(0..1)のクロップ矩形を持ち(ComfyUI-LoadVideoCropと同じ方式)、Sourceプレビュー上で移動+四隅リサイズ(アスペクト比 自由/16:9/9:16/1:1/4:3/3:4固定可、外側は暗転)。「完了」で確定、「キャンセル」で編集前のクロップに戻す。Plan/Assetサブタブ・他タブへの移動、別クリップ選択時は「完了」扱いで編集モードを自動終了。書き出しはCore `VideoCrop`(0.37.0でもexperimental)を`Video Slice`の直後に挿入——`crop`入力は計画どおり二重ネスト`{"crop":{"x","y","width","height"}}`で、偶数ピクセルに丸める(H.264 4:2:0制約)。出力サイズは先頭動画クリップのクロップ後サイズで、サイズの異なる他クリップは`ImageScale(crop=center)`でフィット。解像度不一致ガードはクロップなしの動画クリップ同士だけ比較。`ImageCrop`は0.37でdeprecatedのため不採用。
**検証**: 608x352→300x200のクロップ出力が元フレームの同領域と一致(平均画素差3.1=コーデック誤差程度)。UIで1:1クロップしたクリップを先頭に書き出し、280x280・字幕がクロップ後の枠内・2本目が中央フィットされることを確認。
### Video Edit: Undo/Redo(Phase 5残り)
編集状態(クリップ順・トリム・保持時間・クロップ・テキスト・オーディオ設定)のJSONスナップショット履歴(上限100)。各編集経路に個別に仕込むのではなく、全編集が必ず通る既存の再描画3関数(`_renderTimeline`/`_refreshTextPreview`/`_syncAudioPanel`)から300msデバウンスで記録し、状態が変わったときだけpushする(選択・プレビュー・リサイズでは積まれず、連続入力やドラッグは1ステップにまとまる)。削除クリップはFile参照を持つ`_clipRegistry`から再アップロードなしで復元。↶/↷ボタンとCtrl+Z / Ctrl+Y / Ctrl+Shift+Z(入力欄内はブラウザ標準のundoを優先)。プロジェクト読込時は履歴リセット。
### その他
- **プロジェクト保存**にクロップ・テキスト・オーディオ設定を追加(旧プロジェクトは既定値で読込)。
- **BGM選択・Loadボタンが実ブラウザでダイアログを開かない不具合を修正**: hidden `<input type=file>`をボタンのclickハンドラから`input.click()`で開く方式が、ユーザー環境ではエラーも出さずに無反応だった(同タブのVideo Sourceは`<label>`で包む方式で正常動作)。両ボタンを`<label>`方式に変更。Playwrightはファイル選択を横取りするため自動テストでは再現できなかった。
- **トリム値入力直後に「+ テキスト追加」のクリックが効かない不具合を修正**(テキスト欄実装時に混入): blur時の`change`でテキスト欄全体を再構築し、押そうとしたボタンが置き換わっていた。上限値だけ更新する方式に変更。
**How to apply**: ComfyUI Coreの新しい動画/音声ノードは、`execution_success`で判断せず出力をprobe・画素比較まで実測すること(`VideoTrim`/`VideoCrop`の二重ネストのように、形式を誤ってもエラーにならず無視されるノードがある)。ファイル選択UIは`<label>`で包むネイティブ方式で作り、動作確認はユーザー実機で行うこと。
---
## v0.7.3(2026-09-24)
### GenerateUIタブにQwen Image 2.1(TextEncodeQwenImage21)対応を追加
Comfy-Orgのnightly workflowテンプレート`qwen_image2.1_t2i.json`系(`TextEncodeQwenImage21`ノードでprompt/negative_prompt/resolutionとimage_1〜image_10参照画像を一手に扱う、T2IとEdit兼用のsubgraph構成)をGenerateUIタブで使えるようにした。
**変更内容**: `comfyui-workflow.js`の`analyzeWorkflow()`に`TextEncodeQwenImage21`の分岐を追加。`TextEncodeBooguEdit`と同じ「1ノードにprompt/negative_prompt両方を持ち、ロールは固定(KSampler側の配線をたどらない)」パターンで`prompt_nodes`へ登録し、Input タブのPositive/Negative Prompt欄に反映されるようにした。あわせて`_getWidgetMapping()`に`TextEncodeQwenImage21: ["prompt", "negative_prompt", "resolution"]`を追加し、Settings欄からの書き戻しにも対応。
**調査で判明した既存アーキテクチャ**: GenerateUIのModel/Diffusion Model/Text Encoder/VAE/KSampler/EmptyLatentImage検出は元々ノードタイプ非依存の汎用ロジックなので、UNETLoader+CLIPLoader+VAELoaderの3分割ロード構成(Flux/Qwen Image系で共通)は無改修でそのまま動作した。また画像アップロードUI(Input タブのImageサブパネル)は`generate-tab.js`ではなく`comfyui-editor.js`が担当しており、`analysis.load_image_nodes`に載る全LoadImageノード(1個に限らず)に対して個別カードを描画する汎用実装だったため、Qwen Image 2.1の複数参照画像編集(image_1・image_2をそれぞれ別のLoadImageで接続したワークフロー)もコード変更なしでそのまま動作した——ユーザー提供のI2Iテストワークフローで実機確認済み。
**How to apply**: 新しい「1ノードでprompt+negative_prompt+複数画像入力」型の text-encoder ノードをGenerateUIに対応させる際は、まず`analyzeWorkflow()`のprompt_nodes検出だけ確認すればよい場合が多い——モデルロード・サンプラー・LoadImageカードは大抵ノードタイプに依存しない汎用ロジックが既にカバーしている。画像アップロードUIを探すときは`generate-tab.js`ではなく`comfyui-editor.js`を見ること(両者は別ファイル)。
関連: [[project_v071_unsloth_ssrf_fix_and_outer_gitignore]]
---
## v0.7.2(2026-09-18)
### Workflowタブにソート機能追加(Date/Name 昇順降順)
+3 -3
View File
@@ -37,7 +37,7 @@ Thumbnail / Table views, Thumbnail side panel, Badge filtering, Search, Side pan
One-click capture, Auto-save as thumbnail, Embedded workflow metadata, Auto-import
### GenerateUI Tab (v0.3.5)
6-tab layout, Save button, Input tab, Model tab, Settings tab, Always-visible Raw JSON, Bypass/Mute node handling, One-click generation, Seed control, Style selector, Create Catalog / Catalog buttons, Batch type selector, Batch tab, UI-to-API conversion, Video workflow support, Eagle integration
6-tab layout, Save button, Input tab, Model tab, Settings tab, Always-visible Raw JSON, Bypass/Mute node handling, One-click generation, Seed control, Style selector, Create Catalog / Catalog buttons, Batch type selector, Batch tab, UI-to-API conversion, Video workflow support, Eagle integration, Qwen Image 2.1 support
### Feeder subtab (v0.3.5 / v0.3.42)
ImageFeeder node control, Image library, Selection management, Presets, Continuous Run loop, Index sync, Seed, WFS_GalleryFeeder node, Node & group selector, Image grid, After Gen modes, Run / Stop controls
@@ -75,8 +75,8 @@ Model Browser, Thumbnail / Table views, Table column sort, Search & Filter, User
### Image Edit Tab (v0.3.65)
Layer-based image editor, Loading images, Open PSD button, Close button, New button, Tools, Layer panel, Duplicate layer (⧉), Layer lock, Text quality, Export, Canvas navigation, Undo / Redo
### Video Tab (v0.5.0 – v0.7.2)
Dedicated video generation UI, independent of GenerateUI, Plan subtab, Base settings row, Edit subtab, Edit project persistence, Asset subtab, Project subtab, Two center previews, First Frame / Last Frame are both optional, Semantic node lookup, not hardcoded IDs, Field highlight color, Load in Video, Frame tab, GIF tab, Playback volume
### Video Tab (v0.5.0 – v0.7.4)
Dedicated video generation UI, independent of GenerateUI, Plan subtab, Base settings row, Edit subtab, Edit: Audio / BGM, Edit: Text overlays, Edit: Crop, Edit: Undo / Redo, Edit project persistence, Asset subtab, Project subtab, Two center previews, First Frame / Last Frame are both optional, Semantic node lookup, not hardcoded IDs, Field highlight color, Load in Video, Frame tab, GIF tab, Playback volume
### AI TOOL Tab (v0.3.14)
4-pane layout, Translation pane, Chat pane, Chat pane — image generation via Tool Calling, Chat pane — generate_image extra parameters, TOOLS pane (VLM), TOOLS pane — shared Chat attachment, TOOLS pane (Wildcards), Chat pane — image-to-image (I2I), Chat pane — SVG generation, no ComfyUI workflow involved, Chat pane — Skills, Settings pane, Unsloth backend, Settings pane — Chat Image Generation, Settings pane — Chat I2I Generation, Settings pane — Generation, Model unload, Settings shared, Backend support, URL security, Lemonade's other endpoints (not integrated)
+9 -4
View File
@@ -122,6 +122,7 @@ A comprehensive workflow, asset management, and generation UI plugin for [ComfyU
- **Video workflow support** (v0.5.2) — loading a MiniMax H3 (or other all-in-one video-generation) workflow now works from GenerateUI too, not just the dedicated Video tab: the Input tab's Prompt field picks up its `prompt` widget, the Image tab picks up its LoadImage-based `first_frame` the same way any other I2I workflow does, and generation runs with a longer timeout since video generation takes much longer than a still image; the result preview shows the generated mp4 inline (`<video>`) instead of trying to render it as an image
- **LTX-2.5 / Wan2.2 support** (v0.5.4) — these follow ComfyUI's regular node-graph pattern rather than an all-in-one node, so they needed their own detection: Positive/Negative Prompt now traces through LTX-2.5's `LTXVDualCFGGuider`/`LTXVConditioning` nodes to the actual prompt text, and the Settings tab's Latent Image section recognizes Wan2.2's `EmptyHunyuanLatentVideo` and LTX-2.5's `EmptyLTXVLatentVideo` (width/height/batch size, plus an added **Length (frames)** field) instead of showing "No EmptyLatentImage node found"; the extended generation timeout now also triggers off a `SaveVideo`/`CreateVideo` node anywhere in the graph, not just an all-in-one node
- **Eagle integration** — auto-save generated images to [Eagle](https://eagle.cool/) with metadata
- **Qwen Image 2.1 support** (v0.7.3) — `TextEncodeQwenImage21` (the unified T2I/Edit encoder used by Comfy-Org's Qwen-Image-2.1 workflow templates) is now recognized: its `prompt`/`negative_prompt` widgets populate the Input tab's Positive/Negative Prompt textareas and are editable like any other prompt node; its `resolution` (a pixel-budget int, not width/height) is exposed for Raw JSON editing. No extra work was needed for the model's up to 10 reference images (`image_1`…`image_10`, used for image editing) — each one arrives as an ordinary LoadImage node, so it already gets its own card in the Input tab's Image sub-panel via the existing generic LoadImage detection
</details>
@@ -414,14 +415,18 @@ An experimental batch generator: runs the workflow currently loaded in GenerateU
</details>
<details>
<summary><h3>Video Tab (v0.5.0 – v0.7.2)</h3></summary>
<summary><h3>Video Tab (v0.5.0 – v0.7.4)</h3></summary>
- **Dedicated video generation UI, independent of GenerateUI** — a purpose-built tab for MiniMax H3 (Image to Video), LTX-2.5 (Text / Image / First & Last Frame to Video), and Wan2.2 (Text to Video) workflows (LTX-2.5/Wan2.2: v0.5.4); does not share state with the GenerateUI tab, and model selection (UNet/CLIP/VAE/Audio VAE) is fixed to the loaded workflow's own defaults rather than editable here
- **Plan subtab** (center panel, batch redesign in v0.5.5) — a **plan** is a sequence of timeline blocks, each a fully self-contained generation job with its own Prompt, Duration, and First/Last Image; split the timeline with **+ Split** / **+ Block** / **Delete**, then **Run** to generate every block in order (Pause/Cancel work mid-run) — a plan with a single block behaves exactly like the old one-shot form. First Image can be None, an explicit image, or "use previous block's last frame" (chaining — an explicit Last Image on the previous block always wins over extracting one from its generated output). Save / Save As / Clear a plan (`ws_videoplan_*.json`, with a PNG contact-sheet thumbnail that embeds the full plan data so dropping the image back on works too)
- **Base settings row** (topbar, v0.5.7) — Aspect Ratio / Megapixels / Multiple / Duration (seconds) / Noise Seed on one row — Duration always reflects the currently selected block (each block keeps its own value), the rest are plan-wide — plus a second row of model-specific fields: Width / Height (become directly editable for workflows with no Resolution Selector node, e.g. Wan2.2 and LTX-2.5's First & Last Frame to Video), Turbo Mode / Strength / Steps (MiniMax H3's turbo LoRA — Turbo Mode doubles as Wan2.2's own turbo toggle, sharing one field), and Prompt Enhance (LTX-2.5 only). Every field in both rows is enabled only when the loaded workflow actually has it — see Field highlight color below (one exception: Wan2.2's Text-to-Video variant doesn't expose Noise Seed at all, so it alone leaves that field disabled)
- **Edit subtab** (v0.7.0) — a lightweight clip timeline for trimming and concatenating video/image clips outside of generation: add clips via the Video Source panel's **Add to Edit** button or the Asset subtab's **Send to Edit** button, drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, Clear to empty the timeline. The trim panel for a selected video clip shows a Clipchamp-style scrubber — a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges (image clips get a simple hold-duration field instead); the playhead's own time badge sits a row above the in/out handle badges (v0.7.2) so it stays legible even when the playhead lands right on a trim point, and the scrubber has extra headroom/height and spacing from the clip name above it (v0.7.2). **▶ Preview** plays every ready clip back-to-back (trimmed to its in/out points) in the result preview, and **Export** builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into Video Temp automatically — exporting more than one clip currently drops audio, since ComfyUI's concatenation node requires uniform audio across all inputs
- **Edit project persistence** (v0.7.2) — **Save** / **Save As** / **Load** buttons in the Edit subtab's export panel persist the timeline itself (clip order, trim points, and which uploaded server-side file each clip refers to) as a `ws_videoeditproj_*.json` project file; Load re-fetches and re-probes each clip from its saved server reference, so the original files need to still exist
- **Asset subtab** (center panel, next to Plan/Edit — moved here from the sidebar in v0.7.0) — browses generated videos/images through three groups: **Video Assets** (a manually-curated keeper group you organize yourself, on your own schedule), **Video Temp** (every plan run's output lands here automatically regardless of whether the plan itself was saved — a **Clear Video Temp** button in the Gallery tab's detail panel empties it in one click), and **All Video Assets** (the union of both); a filename search box, a video/image type filter dropdown (All Types / Video Only / Image Only, v0.7.2), and a thumbnail-grid/table (▦/☰) view toggle sit above the list (v0.7.0). Selecting an item shows it in the center preview and lets you tag/memo it, **Send to Edit** (queues it as a clip in the Edit subtab), jump straight to the full Gallery tab, or — for images only — **Set as First Image** / **Set as Last Image** (v0.7.0, uploads it straight into the currently-selected Plan block and switches to the Plan subtab)
- **Edit subtab** (v0.7.0) — a lightweight clip timeline for trimming and concatenating video/image clips outside of generation: add clips via the Video Source panel's **Add to Edit** button or the Asset subtab's **Send to Edit** button, drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, Clear to empty the timeline. The trim panel for a selected video clip shows a Clipchamp-style scrubber — a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges (image clips get a simple hold-duration field instead); the playhead's own time badge sits a row above the in/out handle badges (v0.7.2) so it stays legible even when the playhead lands right on a trim point, and the scrubber has extra headroom/height and spacing from the clip name above it (v0.7.2). **▶ Preview** plays every ready clip back-to-back (trimmed to its in/out points) in the result preview, and **Export** builds and runs the actual ComfyUI graph (trim → crop → concatenate if more than one clip → save), tagging the result into Video Temp automatically. Multi-clip exports keep each clip's own audio (v0.7.4 — previously they were always silent). **Clear** empties the clips and also resets the Audio section to its defaults (v0.7.4)
- **Edit: Audio / BGM** (v0.7.4) — the export panel's **Audio** section sets the soundtrack: **Keep original audio** (with its own volume in dB) keeps every clip's audio aligned to its segment even across multiple clips (image clips contribute silence), and one **BGM** file is laid over the whole video with its own volume and start offset (a longer BGM is cut to the video length). Pick the BGM with **BGM...**, by dropping an audio/video file onto the Audio section, or with **Set as BGM** on a video in the Asset subtab (that video's audio track is used; videos without audio are rejected). Built entirely inside the export graph with ComfyUI core audio nodes (EmptyAudio / AudioMerge / AudioConcat / LoadAudio / TrimAudioDuration / AudioAdjustVolume) passed to ConcatenateVideo's `complete_audio` — no extra dependencies. ▶ Preview plays the BGM alongside the clips
- **Edit: Text overlays** (v0.7.4) — each clip can carry any number of text overlays (**+ Add text** under the trim panel): text, start/end seconds within the clip, size as % of frame height, color, 9-position anchor, outline, and an optional background box. They follow the clip when it's reordered, are previewed live over the Source/Result panes, and are burned in on Export as a final pass (PyAV decode → Pillow → H.264 encode, frame by frame so memory stays flat; the soundtrack is remuxed untouched). Japanese/CJK text renders with a system CJK font (Meiryo / Yu Gothic / Noto Sans JP on Windows)
- **Edit: Crop** (v0.7.4) — **Edit crop** puts a draggable/corner-resizable rectangle over the Source preview (optionally locked to 16:9 / 9:16 / 1:1 / 4:3 / 3:4); **Done** keeps it, **Cancel** restores the crop from before editing, and switching to another subtab/tab ends editing like Done. Export applies it with ComfyUI core `VideoCrop`; the output size is the first video clip's cropped size, and other clips of a different size are center-fit to it. Text overlay previews are drawn relative to the cropped frame
- **Edit: Undo / Redo** (v0.7.4) — **↶ / ↷** in the timeline toolbar (or Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z outside text fields) step through edits to clips, trims, crops, text and audio settings; a deleted clip comes back without re-uploading. History resets when a project is loaded
- **Edit project persistence** (v0.7.2) — **Save** / **Save As** / **Load** buttons in the Edit subtab's export panel persist the timeline itself (clip order, trim points, and which uploaded server-side file each clip refers to — plus crops, text overlays and audio/BGM settings since v0.7.4) as a `ws_videoeditproj_*.json` project file; Load re-fetches and re-probes each clip from its saved server reference, so the original files need to still exist
- **Asset subtab** (center panel, next to Plan/Edit — moved here from the sidebar in v0.7.0) — browses generated videos/images through three groups: **Video Assets** (a manually-curated keeper group you organize yourself, on your own schedule), **Video Temp** (every plan run's output lands here automatically regardless of whether the plan itself was saved — a **Clear Video Temp** button in the Gallery tab's detail panel empties it in one click), and **All Video Assets** (the union of both); a filename search box, a video/image type filter dropdown (All Types / Video Only / Image Only, v0.7.2), and a thumbnail-grid/table (▦/☰) view toggle sit above the list (v0.7.0). Selecting an item shows it in the center preview and lets you tag/memo it, **Send to Edit** (queues it as a clip in the Edit subtab), **Set as BGM** for videos (v0.7.4 — uses its audio track as the Edit BGM), jump straight to the full Gallery tab, or — for images only — **Set as First Image** / **Set as Last Image** (v0.7.0, uploads it straight into the currently-selected Plan block and switches to the Plan subtab)
- **Project subtab** (sidebar) — lists saved plan files while the center Plan subtab is active, or saved Edit projects while the center Edit subtab is active (v0.7.2 — thumbnail/block-or-clip-count/last-updated) — click one to load it into the matching editor, or delete it with the × button; the panel collapses back down when its own tab is clicked again, and switching Plan↔Edit while it's open refreshes the list in place
- **Two center previews** (v0.5.5) — the left preview shows either the video currently selected in the Asset subtab or whatever's dropped/picked in the **Video Source** panel to its left (**Clear** resets it to empty); the right preview always shows the most recently generated clip; both preview frames are a fixed height (v0.7.0) so switching sources never shifts the Plan/Edit/Asset panel below them. The Frame/GIF tools act on whichever preview was touched most recently
- **First Frame / Last Frame are both optional** — leaving First Image empty runs the workflow text-only (the node is simply left unconnected — a "bypass"); workflows that don't even include a LoadImage node for First Frame are supported out of the box (Wan2.2's Text-to-Video graph has neither slot at all), and a LoadImage node is only injected into the graph on the fly if an image is actually supplied
+31 -2
View File
@@ -1,5 +1,5 @@
"""Video tab API routes: last-frame capture, animated GIF conversion, and
Plan (batch timeline) file management."""
"""Video tab API routes: last-frame capture, animated GIF conversion, Edit
tab probe/text-overlay burn-in, and Plan/Edit project file management."""
import asyncio
import logging
@@ -22,6 +22,7 @@ def setup_routes(app: web.Application):
app.router.add_post("/api/wfm/video/frame/save-to-output", handle_save_frame)
app.router.add_post("/api/wfm/video/to-gif", handle_to_gif)
app.router.add_get("/api/wfm/video/edit/probe", handle_probe_video)
app.router.add_post("/api/wfm/video/edit/overlay-text", handle_overlay_text)
app.router.add_get("/api/wfm/video/plans", handle_list_plans)
app.router.add_get("/api/wfm/video/plans/content", handle_get_plan_content)
@@ -95,6 +96,34 @@ async def handle_probe_video(request: web.Request) -> web.Response:
return web.json_response({"error": str(e)}, status=500)
async def handle_overlay_text(request: web.Request) -> web.Response:
try:
body = await request.json()
filename = body.get("filename", "")
subfolder = body.get("subfolder", "")
type_ = body.get("type", "output")
overlays = body.get("overlays") or []
delete_source = bool(body.get("delete_source"))
if not filename:
return web.json_response({"error": "filename required"}, status=400)
if not isinstance(overlays, list):
return web.json_response({"error": "overlays must be a list"}, status=400)
# Decode+re-encode of every frame — run off the event loop so other
# requests aren't stalled for the duration of the burn-in.
result = await asyncio.to_thread(
_service.overlay_text_on_video, filename, subfolder, type_, overlays, "video/wfm_edit", delete_source
)
return web.json_response({"status": "ok", **result})
except FileNotFoundError as e:
return web.json_response({"error": str(e)}, status=404)
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
logger.error("Error burning text overlay into video: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def handle_list_plans(request: web.Request) -> web.Response:
try:
result = await asyncio.to_thread(_plan_service.list_plans)
+195
View File
@@ -8,6 +8,8 @@ implementation (e.g. Eagle's video2gif/video-to-frame plugins).
import base64
import logging
import os
import sys
from pathlib import Path
logger = logging.getLogger(__name__)
@@ -18,6 +20,86 @@ try:
except Exception: # pragma: no cover - Pillow is a hard ComfyUI dependency
_RESAMPLE = None
# Text overlay anchors: 9-grid "<vertical>-<horizontal>" (e.g. "bottom-center").
_ANCHOR_V = ("top", "middle", "bottom")
_ANCHOR_H = ("left", "center", "right")
# Fonts tried in order for text overlays. Pillow's built-in default font has no
# CJK glyphs (Japanese/Chinese text would render as boxes), so a system font
# with CJK coverage is preferred; load_default() is only the last resort.
_FONT_CANDIDATES = {
"win32": ["meiryo.ttc", "YuGothM.ttc", "NotoSansJP-Regular.ttf", "msgothic.ttc", "arial.ttf"],
"darwin": [
"/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
"/Library/Fonts/Arial Unicode.ttf",
],
"linux": [
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
],
}
def _load_overlay_font(size: int):
from PIL import ImageFont
platform = "win32" if sys.platform.startswith("win") else ("darwin" if sys.platform == "darwin" else "linux")
for name in _FONT_CANDIDATES[platform]:
path = name
if platform == "win32" and not os.path.isabs(name):
path = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", name)
if os.path.isfile(path):
try:
return ImageFont.truetype(path, size)
except Exception:
continue
return ImageFont.load_default(size=size)
def _render_text_layer(width: int, height: int, ov: dict):
"""Renders one overlay's text onto a transparent full-frame RGBA layer, so
it can be alpha-composited over every frame in its time range without
re-rendering the text per frame (same idea as ComfyUI core's TextOverlay)."""
from PIL import ImageColor, ImageDraw
text = str(ov.get("text") or "").replace("\\n", "\n")
layer = Image.new("RGBA", (width, height), (0, 0, 0, 0))
if not text.strip():
return layer
size = max(8, round(float(ov.get("font_size") or 6.0) / 100.0 * height))
font = _load_overlay_font(size)
stroke = max(1, round(size * 0.06)) if ov.get("outline", True) else 0
spacing = round(size * 0.2)
try:
color = ImageColor.getrgb(str(ov.get("color") or "#ffffff"))
except ValueError:
color = (255, 255, 255)
fill = (*color[:3], 255)
anchor = str(ov.get("anchor") or "bottom-center").split("-")
v = anchor[0] if anchor and anchor[0] in _ANCHOR_V else "bottom"
h = anchor[1] if len(anchor) > 1 and anchor[1] in _ANCHOR_H else "center"
draw = ImageDraw.Draw(layer)
box = draw.multiline_textbbox((0, 0), text, font=font, spacing=spacing, stroke_width=stroke, align=h)
tw, th = box[2] - box[0], box[3] - box[1]
margin = round(0.04 * min(width, height))
x = {"left": margin, "center": (width - tw) / 2, "right": width - margin - tw}[h] - box[0]
y = {"top": margin, "middle": (height - th) / 2, "bottom": height - margin - th}[v] - box[1]
if ov.get("background"):
pad = round(size * 0.3)
draw.rounded_rectangle(
(x + box[0] - pad, y + box[1] - pad, x + box[2] + pad, y + box[3] + pad),
radius=pad, fill=(0, 0, 0, 140),
)
draw.multiline_text((x, y), text, font=font, fill=fill, spacing=spacing, align=h,
stroke_width=stroke, stroke_fill=(0, 0, 0, 255))
return layer
class VideoService:
def _resolve_media_path(self, filename: str, subfolder: str, type_: str) -> Path:
@@ -157,3 +239,116 @@ class VideoService:
optimize=True,
)
return {"filename": save_name, "subfolder": out_subfolder, "frame_count": len(pil_frames)}
def overlay_text_on_video(
self,
filename: str,
subfolder: str,
type_: str,
overlays: list,
filename_prefix: str = "video/wfm_edit",
delete_source: bool = False,
) -> dict:
"""Burns timed text overlays into a video (Video Edit tab, Phase 3).
ComfyUI core has no time-ranged video text node (core TextOverlay draws
on every frame of an IMAGE batch, top/bottom only), and routing the
whole timeline through IMAGE tensors would hold every decoded frame in
memory at once — so this streams instead: decode one frame, composite
any overlay whose [start, end) covers it, encode, repeat. Audio packets
are remuxed untouched (no audio re-encode), so the soundtrack built by
the export graph (original audio / BGM) survives this pass as-is.
overlays: [{text, start, end, font_size (% of frame height), color,
anchor ("<top|middle|bottom>-<left|center|right>"), outline, background}]
with start/end in seconds from the start of the video.
"""
import av # type: ignore
import folder_paths # type: ignore
src_path = self._resolve_media_path(filename, subfolder, type_)
items = []
for ov in overlays or []:
try:
start = max(0.0, float(ov.get("start") or 0.0))
end = float(ov.get("end") or 0.0)
except (TypeError, ValueError):
continue
if end > start and str(ov.get("text") or "").strip():
items.append((start, end, ov))
if not items:
raise ValueError("No valid text overlays")
output_dir = folder_paths.get_output_directory()
full_output_folder, out_filename, counter, out_subfolder, _ = folder_paths.get_save_image_path(filename_prefix, output_dir)
save_name = f"{out_filename}_{counter:05}_.mp4"
save_path = Path(full_output_folder) / save_name
in_c = av.open(str(src_path))
out_c = None
try:
in_v = in_c.streams.video[0]
in_a = in_c.streams.audio[0] if in_c.streams.audio else None
width, height = in_v.codec_context.width, in_v.codec_context.height
rate = in_v.average_rate or in_v.guessed_rate or 24
layers = [(s, e, _render_text_layer(width, height, ov)) for s, e, ov in items]
out_c = av.open(str(save_path), mode="w")
out_v = out_c.add_stream("libx264", rate=rate)
out_v.width = width
out_v.height = height
out_v.pix_fmt = "yuv420p"
out_v.options = {"crf": "18", "preset": "medium"}
out_a = out_c.add_stream_from_template(in_a) if in_a is not None else None
streams = [in_v] + ([in_a] if in_a is not None else [])
first_pts = None
index = 0
for packet in in_c.demux(*streams):
if packet.stream.type == "audio":
if packet.dts is None:
continue
packet.stream = out_a
out_c.mux(packet)
continue
for frame in packet.decode():
if frame.pts is not None and first_pts is None:
first_pts = frame.pts
t = float((frame.pts - first_pts) * in_v.time_base) if frame.pts is not None else index / float(rate)
active = [layer for s, e, layer in layers if s <= t < e]
if active:
img = frame.to_image().convert("RGBA")
for layer in active:
img.alpha_composite(layer)
out_frame = av.VideoFrame.from_image(img.convert("RGB"))
else:
out_frame = frame
out_frame.pts = index
out_frame.time_base = out_v.codec_context.time_base
index += 1
for out_packet in out_v.encode(out_frame):
out_c.mux(out_packet)
for out_packet in out_v.encode(None):
out_c.mux(out_packet)
except BaseException:
if out_c is not None:
out_c.close()
out_c = None
save_path.unlink(missing_ok=True)
raise
finally:
if out_c is not None:
out_c.close()
in_c.close()
# The export graph's intermediate file is only deleted when the caller
# explicitly asks and it's one of this tab's own "_pre" intermediates in
# output — never an arbitrary user file.
if delete_source and type_ == "output" and Path(filename).name.startswith("wfm_edit_pre"):
try:
src_path.unlink()
except OSError as e:
logger.warning("Could not delete intermediate %s: %s", src_path, e)
return {"filename": save_name, "subfolder": out_subfolder, "frame_count": index}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-workflow-studio"
description = "A comprehensive workflow, asset management, and generation UI plugin for ComfyUI. Manage workflows, models, images, and prompts; batch generation and Image Feeder in the GenerateUI tab; library side panel with canvas drag & drop and AI tools."
version = "0.7.2"
version = "0.7.4"
license = { file = "LICENSE" }
requires-python = ">= 3.10"
dependencies = ["jinja2"]
+232
View File
@@ -602,6 +602,237 @@
flex: none;
}
/* ---- Edit: テキストオーバーレイ(Phase 3) / オーディオ(Phase 4) ---- */
.wfm-video-edit-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 12px;
font-weight: 600;
color: var(--wfm-text-secondary);
margin-bottom: 6px;
}
.wfm-video-edit-text-section {
margin-top: 16px;
padding-top: 10px;
border-top: 1px solid var(--wfm-border);
}
.wfm-video-edit-text-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.wfm-video-edit-text-row {
border: 1px solid var(--wfm-border);
border-radius: 6px;
padding: 6px 8px;
}
.wfm-video-edit-text-row-head {
display: flex;
gap: 6px;
margin-bottom: 6px;
}
.wfm-video-edit-text-row-head input {
flex: 1;
min-width: 0;
}
.wfm-video-edit-text-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
gap: 6px 8px;
align-items: end;
}
.wfm-video-edit-text-grid > label {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 11px;
color: var(--wfm-text-secondary);
margin: 0;
}
.wfm-video-edit-text-grid > label.wfm-video-checkbox-inline {
flex-direction: row;
}
.wfm-video-edit-text-grid input[type="color"] {
width: 100%;
height: 28px;
padding: 0;
border: 1px solid var(--wfm-border);
border-radius: 4px;
background: none;
}
.wfm-video-edit-audio-panel {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--wfm-border);
}
.wfm-video-edit-audio-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 2px 6px;
margin-top: 4px;
font-size: 11px;
color: var(--wfm-text-secondary);
}
.wfm-video-edit-audio-row > label {
grid-column: 1 / -1;
margin: 0;
}
.wfm-video-edit-audio-row input[type="range"] {
width: 100%;
}
.wfm-video-edit-audio-row input[type="number"] {
grid-column: 1 / -1;
}
.wfm-video-edit-audio-val {
min-width: 44px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.wfm-video-edit-bgm-file {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
}
.wfm-video-edit-audio-panel.drag-over {
outline: 2px dashed var(--wfm-primary);
outline-offset: 2px;
border-radius: 4px;
}
.wfm-video-edit-bgm-hint {
display: block;
font-size: 10px;
margin-top: 2px;
}
/* <label>で包んだファイル選択を.wfm-btnと同じ見た目にする */
.wfm-video-edit-file-btn {
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
user-select: none;
}
.wfm-video-edit-bgm-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wfm-video-edit-crop-section {
margin-top: 16px;
padding-top: 10px;
border-top: 1px solid var(--wfm-border);
}
.wfm-video-edit-crop-row {
display: flex;
align-items: end;
gap: 12px;
}
.wfm-video-edit-crop-row > label {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 11px;
color: var(--wfm-text-secondary);
margin: 0;
width: 110px;
}
#wfm-video-edit-crop-toggle.active {
background: var(--wfm-primary);
color: #fff;
}
/* Sourceプレビュー上のクロップ矩形。外側はbox-shadowで暗転、
.interactive時のみドラッグ(移動/四隅リサイズ)を受け付ける */
.wfm-video-edit-crop-layer {
position: absolute;
overflow: hidden;
pointer-events: none;
}
.wfm-video-edit-crop-rect {
position: absolute;
box-sizing: border-box;
border: 1px dashed rgba(255, 255, 255, 0.8);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
}
.wfm-video-edit-crop-layer.interactive .wfm-video-edit-crop-rect {
pointer-events: auto;
cursor: move;
border: 2px solid var(--wfm-primary);
touch-action: none;
}
.wfm-video-edit-crop-handle {
display: none;
position: absolute;
width: 12px;
height: 12px;
background: var(--wfm-primary);
border: 1px solid #fff;
border-radius: 2px;
}
.wfm-video-edit-crop-layer.interactive .wfm-video-edit-crop-handle { display: block; }
.wfm-video-edit-crop-handle[data-h="nw"] { left: -7px; top: -7px; cursor: nwse-resize; }
.wfm-video-edit-crop-handle[data-h="ne"] { right: -7px; top: -7px; cursor: nesw-resize; }
.wfm-video-edit-crop-handle[data-h="sw"] { left: -7px; bottom: -7px; cursor: nesw-resize; }
.wfm-video-edit-crop-handle[data-h="se"] { right: -7px; bottom: -7px; cursor: nwse-resize; }
/* プレビュー枠上のテキスト近似表示レイヤー(実際の焼き込みはサーバー側)。
left/top/width/heightはJSが表示中の<video>/<img>の実寸に合わせて設定する */
.wfm-video-edit-text-layer {
position: absolute;
pointer-events: none;
overflow: hidden;
}
.wfm-video-edit-text-item {
position: absolute;
white-space: pre;
line-height: 1.2;
font-family: "Meiryo", "Yu Gothic", "Noto Sans JP", sans-serif;
}
.wfm-video-edit-text-item.outline {
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000, 0 0 2px #000;
}
.wfm-video-edit-text-item.bg {
background: rgba(0, 0, 0, 0.55);
padding: 0.15em 0.3em;
border-radius: 0.3em;
}
.wfm-video-asset-detail label {
font-size: 12px;
color: var(--wfm-text-secondary);
@@ -664,6 +895,7 @@
}
.wfm-video-preview-frame {
position: relative;
flex: 1;
min-width: 0;
height: 260px;
+23
View File
@@ -1296,6 +1296,28 @@ export const comfyWorkflow = {
}
}
// TextEncodeQwenImage21 (Qwen Image 2.1 unified T2I/Edit encoder) — prompt +
// negative_prompt in one node (fixed roles, same shape as TextEncodeBooguEdit).
// Also carries a "resolution" pixel-budget int (not width/height) and up to 10
// optional images.image_N reference inputs (image_1 is the edit target when wired;
// with no images connected the node behaves as pure T2I). The reference images
// themselves need no special handling here — they arrive as ordinary LoadImage
// nodes already picked up generically by the load_image_nodes pass below.
if (ct === "TextEncodeQwenImage21") {
if (typeof inputs.prompt === "string") {
result.prompt_nodes.push({
id, type: ct, title: `${title} [positive]`, role: "positive",
text: inputs.prompt, textKey: "prompt",
});
}
if (typeof inputs.negative_prompt === "string") {
result.prompt_nodes.push({
id, type: ct, title: `${title} [negative]`, role: "negative",
text: inputs.negative_prompt, textKey: "negative_prompt",
});
}
}
// Mage-Flow text encoder — prompt + negative_prompt in one node (fixed roles,
// unlike KSampler-derived getRole() since both wires originate from this node)
if (ct === "TextEncodeMageFlowEdit") {
@@ -1651,6 +1673,7 @@ function _getWidgetMapping(nodeType) {
ImpactWildcardProcessor: ["wildcard_text"],
ImpactWildcardEncode: ["wildcard_text"],
WFS_PromptText: ["positive", "negative"],
TextEncodeQwenImage21: ["prompt", "negative_prompt", "resolution"],
};
return mappings[nodeType] || null;
}
+114 -6
View File
@@ -1361,7 +1361,7 @@ const LANGUAGES = {
videoEditTotalDuration: (time) => `Total: ${time}`,
videoEditPreviewPlay: "▶ Preview",
videoEditPreviewStop: "■ Stop",
videoEditConfirmClear: "Clear the entire edit timeline?",
videoEditConfirmClear: "Clear the entire edit timeline (clips and audio settings)?",
videoSourceAddToEdit: "Add to Edit",
videoEditDuplicate: "Duplicate",
videoEditDelete: "Delete",
@@ -1391,6 +1391,42 @@ const LANGUAGES = {
videoEditProjectSaveFailed: "Failed to save project",
videoEditProjectLoaded: "Project loaded",
videoEditProjectLoadFailed: "Failed to load project",
videoEditTextOverlays: "Text overlays",
videoEditTextAdd: "+ Add text",
videoEditTextEmpty: "No text on this clip",
videoEditTextDefault: "Text",
videoEditTextStart: "Start (s)",
videoEditTextEnd: "End (s)",
videoEditTextSize: "Size (%)",
videoEditTextColor: "Color",
videoEditTextPosition: "Position",
videoEditTextOutline: "Outline",
videoEditTextBackground: "Background",
videoEditTextAnchorLabels: ["↖ Top left", "↑ Top center", "↗ Top right", "← Middle left", "● Center", "→ Middle right", "↙ Bottom left", "↓ Bottom center", "↘ Bottom right"],
videoEditBurningText: "Burning in text...",
videoEditAudioTitle: "Audio",
videoEditKeepAudio: "Keep original audio",
videoEditOriginalVolume: "Original volume",
videoEditBgmSelect: "BGM...",
videoEditBgmRemove: "Remove BGM",
videoEditBgmNone: "No BGM",
videoEditBgmVolume: "BGM volume",
videoEditBgmOffset: "BGM start (s)",
videoEditBgmHint: "Drop an audio file here, or use \"Set as BGM\" on a video in Asset",
videoEditBgmNoAudio: "This video has no audio track",
videoEditBgmUnsupported: "Drop an audio or video file",
videoAssetSetAsBgm: "Set as BGM (audio only)",
videoAssetSetAsBgmDone: (name) => `Set the audio of "${name}" as BGM`,
videoEditCrop: "Crop",
videoEditCropEdit: "Edit crop",
videoEditCropDone: "Done",
videoEditCropCancel: "Cancel",
videoEditCropReset: "Reset",
videoEditCropAspect: "Aspect ratio",
videoEditCropFree: "Free",
videoEditCropNone: "Full frame",
videoEditUndo: "Undo (Ctrl+Z)",
videoEditRedo: "Redo (Ctrl+Y / Ctrl+Shift+Z)",
videoConfirmClearPlan: "Clear the current plan?",
videoPlanPause: "Pause",
videoPlanResume: "Resume",
@@ -1443,7 +1479,7 @@ const LANGUAGES = {
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): lists saved Video Plans while the center Plan subtab is active, or saved Edit projects while the center Edit subtab is active (thumbnail/block or clip count, last updated) — click one to load it, or delete it with the × button; clicking the same tab again collapses the panel.",
helpVideo10: "Highlight color: the Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple / Noise Seed / Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance labels turn the same highlight color as the GenerateUI Model tab (customizable in the Settings tab) when the loaded workflow actually supports that field — e.g. for a Wan2.2 workflow (no ResolutionSelector node), the Aspect Ratio / Megapixels / Multiple labels stay unhighlighted while Width / Height light up instead.",
helpVideo11: "Edit subtab: a lightweight clip timeline for trimming and concatenating video/image clips outside of generation. Add clips via the Video Source panel's \"Add to Edit\" button or the Asset subtab's \"Send to Edit\" button, then drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, and Clear to empty the timeline. The trim panel for the selected video clip shows a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges — image clips instead get a simple hold-duration field. ▶ Preview plays every ready clip back-to-back (trimmed to its in/out points) in the right-hand result preview; Export builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into the Video Temp group automatically. Exporting more than one clip currently drops audio, since ComfyUI's concatenation node requires uniform audio across all inputs. The export panel's Save/Save As/Load buttons persist the timeline itself (clip order, trim points, which uploaded file each clip refers to) as a JSON project file on the server — Load re-fetches each clip from its saved server reference and re-probes it, so the original files must still exist. These saved projects also show up in the sidebar's Project tab whenever the Edit subtab is active (see helpVideo9).",
helpVideo11: "Edit subtab: a lightweight clip timeline for trimming and concatenating video/image clips outside of generation. Add clips via the Video Source panel's \"Add to Edit\" button or the Asset subtab's \"Send to Edit\" button, then drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, and Clear to empty the timeline (this also resets the Audio section). The trim panel for the selected video clip shows a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges — image clips instead get a simple hold-duration field. ▶ Preview plays every ready clip back-to-back (trimmed to its in/out points) in the right-hand result preview; Export builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into the Video Temp group automatically. The Audio section of the export panel controls the soundtrack: \"Keep original audio\" (with its own volume in dB) keeps each clip's audio aligned to its segment even across multiple clips (image clips contribute silence), and \"BGM...\" lays one audio file over the whole video with its own volume and start offset (a longer BGM is cut to the video length). Each clip can also carry text overlays (\"+ Add text\" under the trim panel: text, start/end seconds within the clip, size as % of frame height, color, 9-position anchor, outline, background box); they are previewed live over the Source/Result panes and burned in on Export as a final pass. The BGM can be chosen with \"BGM...\", by dropping an audio/video file onto the Audio section, or with \"Set as BGM\" on a video in the Asset subtab (its audio track is used). Video clips can also be cropped: \"Edit crop\" shows a draggable/resizable rectangle over the Source preview (optionally locked to 16:9 / 9:16 / 1:1 / 4:3 / 3:4) — \"Done\" keeps it, \"Cancel\" restores the crop from before editing, and switching to another subtab/tab ends editing like Done; the export size becomes the first video clip's cropped size, and other clips of a different size are center-fit to it. ↶/↷ in the timeline toolbar (or Ctrl+Z / Ctrl+Y) undo and redo edits to clips, trims, crops, text and audio settings. The export panel's Save/Save As/Load buttons persist the timeline itself (clip order, trim points, which uploaded file each clip refers to) as a JSON project file on the server — Load re-fetches each clip from its saved server reference and re-probes it, so the original files must still exist. These saved projects also show up in the sidebar's Project tab whenever the Edit subtab is active (see helpVideo9).",
helpVideo12: "Asset subtab (center panel, next to Plan/Edit): browses every video/image tagged into Video Assets or Video Temp (via the group dropdown, or \"All Video Assets\" for both). Search filters by filename; the ▦/☰ buttons switch between a thumbnail grid and a table (filename/updated/tags). Selecting an item shows its tags/memo, loads it into the center preview, and offers \"Send to Edit\" (queues it as a clip in the Edit subtab) plus, for images only, \"Set as First Image\"/\"Set as Last Image\" (uploads it straight into the currently-selected Plan block, switching to the Plan subtab).",
},
@@ -2802,7 +2838,7 @@ const LANGUAGES = {
videoEditTotalDuration: (time) => `合計: ${time}`,
videoEditPreviewPlay: "▶ プレビュー",
videoEditPreviewStop: "■ 停止",
videoEditConfirmClear: "編集タイムラインを全てクリアしますか?",
videoEditConfirmClear: "編集タイムライン(クリップとオーディオ設定)を全てクリアしますか?",
videoSourceAddToEdit: "Editに追加",
videoEditDuplicate: "複製",
videoEditDelete: "削除",
@@ -2832,6 +2868,42 @@ const LANGUAGES = {
videoEditProjectSaveFailed: "プロジェクトの保存に失敗しました",
videoEditProjectLoaded: "プロジェクトを読み込みました",
videoEditProjectLoadFailed: "プロジェクトの読み込みに失敗しました",
videoEditTextOverlays: "テキストオーバーレイ",
videoEditTextAdd: "+ テキスト追加",
videoEditTextEmpty: "このクリップにテキストはありません",
videoEditTextDefault: "テキスト",
videoEditTextStart: "開始(秒)",
videoEditTextEnd: "終了(秒)",
videoEditTextSize: "サイズ(%)",
videoEditTextColor: "色",
videoEditTextPosition: "位置",
videoEditTextOutline: "縁取り",
videoEditTextBackground: "背景",
videoEditTextAnchorLabels: ["↖ 左上", "↑ 上中央", "↗ 右上", "← 左中央", "● 中央", "→ 右中央", "↙ 左下", "↓ 下中央", "↘ 右下"],
videoEditBurningText: "テキストを焼き込み中...",
videoEditAudioTitle: "オーディオ",
videoEditKeepAudio: "元の音声を残す",
videoEditOriginalVolume: "元音声の音量",
videoEditBgmSelect: "BGM選択...",
videoEditBgmRemove: "BGMを外す",
videoEditBgmNone: "BGMなし",
videoEditBgmVolume: "BGM音量",
videoEditBgmOffset: "BGM開始位置(秒)",
videoEditBgmHint: "音声ファイルをここへドロップ、またはAssetの動画で「BGMに設定」",
videoEditBgmNoAudio: "この動画には音声トラックがありません",
videoEditBgmUnsupported: "音声または動画ファイルをドロップしてください",
videoAssetSetAsBgm: "BGMに設定(音声のみ)",
videoAssetSetAsBgmDone: (name) => `「${name}」の音声をBGMに設定しました`,
videoEditCrop: "クロップ",
videoEditCropEdit: "クロップ編集",
videoEditCropDone: "完了",
videoEditCropCancel: "キャンセル",
videoEditCropReset: "リセット",
videoEditCropAspect: "アスペクト比",
videoEditCropFree: "自由",
videoEditCropNone: "全体",
videoEditUndo: "元に戻す (Ctrl+Z)",
videoEditRedo: "やり直し (Ctrl+Y / Ctrl+Shift+Z)",
videoConfirmClearPlan: "現在のPlanをクリアしますか?",
videoPlanPause: "一時停止",
videoPlanResume: "再開",
@@ -2884,7 +2956,7 @@ const LANGUAGES = {
helpVideo8: "チェイン機能: ブロックのFirst Imageを「前ブロックの最終フレームを使用」に設定すると、前のクリップから連続した映像になります。前ブロックに明示的なLast Imageが設定されていればそれがそのまま使われ、無ければ生成された動画から実際の最終フレームが自動抽出されます。",
helpVideo9: "Projectタブ(左サイドバー): 中央がPlanサブタブのときは保存済みPlan一覧、Editサブタブのときは保存済みEditプロジェクト一覧(サムネイル/ブロック数またはクリップ数・更新日時)を表示します。クリックで読み込み、×ボタンで削除できます。同じタブをもう一度押すとパネルを閉じます。",
helpVideo10: "ハイライト表示: 読み込んだワークフローが実際にサポートしているフィールドに応じて、Prompt/Duration/First Image/Last Image/Aspect Ratio/Megapixels/Multiple/Noise Seed/Width/Height/Turbo Mode/Strength/Steps/Prompt Enhanceのラベルが、GenerateUI ModelタブのハイライトとSettingsタブで共通設定できる色に変わります。例えばWan2.2ワークフロー(ResolutionSelectorノードが無い)ではAspect Ratio/Megapixels/Multipleはハイライトされない代わりに、Width/Heightがハイライトされます。",
helpVideo11: "Editサブタブ: 生成とは独立して動画/画像クリップをトリム・結合できる簡易タイムラインです。Video Sourceパネルの「Editに追加」ボタン、またはAssetサブタブの「Editへ送る」ボタンでクリップを追加し、ブロックをドラッグ(または◀/▶ツールバーボタン)で並べ替え、選択中クリップをDuplicate/Delete、Clearでタイムラインを空にできます。選択中の動画クリップのトリムパネルには時間目盛り・Sourceプレビューと同期するドラッグ可能な再生ヘッド・時刻バッジ付きのドラッグ可能なイン/アウトハンドルが表示されます(画像クリップは代わりにシンプルな保持時間の入力欄)。「▶ プレビュー」は準備済みの全クリップをイン/アウト点でトリムして右側の結果プレビューで連続再生し、「書き出し」は実際のComfyUIグラフ(トリム→2クリップ以上ならConcatenate→保存)を構築・実行して、結果をVideo Tempグループへ自動登録します。複数クリップの書き出しは現状音声が失われます(ComfyUIの結合ノードが全入力で音声形式の統一を要求するため)。書き出しパネルのSave/Save As/Loadボタンは、タイムラインそのもの(クリップ順序・トリム区間・各クリップが参照するアップロード済みファイル)をサーバー上にJSONプロジェクトとして保存します。Loadは保存されたサーバー参照から各クリップを再取得・再プローブするため、元のファイルが存在している必要があります。保存済みプロジェクトは、中央がEditサブタブのときサイドバーのProjectタブにも一覧表示されます(helpVideo9参照)。",
helpVideo11: "Editサブタブ: 生成とは独立して動画/画像クリップをトリム・結合できる簡易タイムラインです。Video Sourceパネルの「Editに追加」ボタン、またはAssetサブタブの「Editへ送る」ボタンでクリップを追加し、ブロックをドラッグ(または◀/▶ツールバーボタン)で並べ替え、選択中クリップをDuplicate/Delete、Clearでタイムラインを空にできます(オーディオ欄も初期状態に戻ります)。選択中の動画クリップのトリムパネルには時間目盛り・Sourceプレビューと同期するドラッグ可能な再生ヘッド・時刻バッジ付きのドラッグ可能なイン/アウトハンドルが表示されます(画像クリップは代わりにシンプルな保持時間の入力欄)。「▶ プレビュー」は準備済みの全クリップをイン/アウト点でトリムして右側の結果プレビューで連続再生し、「書き出し」は実際のComfyUIグラフ(トリム→2クリップ以上ならConcatenate→保存)を構築・実行して、結果をVideo Tempグループへ自動登録します。書き出しパネルのオーディオ欄でサウンドトラックを設定できます。「元の音声を残す」(音量dB調整付き)は複数クリップでも各クリップの音声をその区間に揃えて保持し(画像クリップ区間は無音)、「BGM選択...」で1本の音声ファイルを動画全体に重ねられます(音量・開始位置を指定可、動画より長いBGMは動画の長さでカット)。また各クリップにはテキストオーバーレイを付けられます(トリムパネル下の「+ テキスト追加」: 文字・クリップ内の開始/終了秒・サイズ(フレーム高さに対する%)・色・9方向の位置・縁取り・背景)。Source/Resultプレビュー上でリアルタイムに確認でき、書き出し時に最終工程として焼き込まれます。BGMは「BGM選択...」ボタン、オーディオ欄への音声/動画ファイルのドラッグ&ドロップ、またはAssetサブタブの動画の「BGMに設定」(その動画の音声トラックを使用)で指定できます。動画クリップはクロップもできます。「クロップ編集」でSourceプレビュー上にドラッグ/リサイズ可能な枠が表示されます(16:9 / 9:16 / 1:1 / 4:3 / 3:4 に固定も可)。「完了」で確定、「キャンセル」で編集前の状態に戻り、他のサブタブ/タブへ移動すると「完了」と同じ扱いで編集を終了します。書き出しサイズは先頭の動画クリップのクロップ後サイズになり、サイズの異なる他のクリップは中央基準で合わせられます。タイムラインツールバーの ↶/↷(または Ctrl+Z / Ctrl+Y)で、クリップ・トリム・クロップ・テキスト・オーディオ設定の編集を元に戻す/やり直しできます。書き出しパネルのSave/Save As/Loadボタンは、タイムラインそのもの(クリップ順序・トリム区間・各クリップが参照するアップロード済みファイル)をサーバー上にJSONプロジェクトとして保存します。Loadは保存されたサーバー参照から各クリップを再取得・再プローブするため、元のファイルが存在している必要があります。保存済みプロジェクトは、中央がEditサブタブのときサイドバーのProjectタブにも一覧表示されます(helpVideo9参照)。",
helpVideo12: "Assetサブタブ(中央パネル、Plan/Editの隣): グループ切替(または「All Video Assets」で両方まとめて)でVideo AssetsまたはVideo Tempに登録された動画/画像を一覧表示します。検索欄はファイル名でフィルタし、▦/☰ボタンでサムネイルグリッドとテーブル表示(ファイル名・更新日時・タグ)を切り替えられます。項目を選択するとタグ・メモが表示され中央プレビューに読み込まれ、「Editへ送る」(Editサブタブのクリップとして追加)に加え、画像の場合のみ「First Imageに設定」「Last Imageに設定」(選択中のPlanブロックへ直接アップロードし、Planサブタブへ切り替え)が使えます。",
},
@@ -4243,7 +4315,7 @@ const LANGUAGES = {
videoEditTotalDuration: (time) => `总计: ${time}`,
videoEditPreviewPlay: "▶ 预览",
videoEditPreviewStop: "■ 停止",
videoEditConfirmClear: "确定清空整个编辑时间线吗?",
videoEditConfirmClear: "确定清空整个编辑时间线(片段和音频设置)吗?",
videoSourceAddToEdit: "添加到Edit",
videoEditDuplicate: "复制",
videoEditDelete: "删除",
@@ -4273,6 +4345,42 @@ const LANGUAGES = {
videoEditProjectSaveFailed: "项目保存失败",
videoEditProjectLoaded: "项目已加载",
videoEditProjectLoadFailed: "项目加载失败",
videoEditTextOverlays: "文字叠加",
videoEditTextAdd: "+ 添加文字",
videoEditTextEmpty: "此片段没有文字",
videoEditTextDefault: "文字",
videoEditTextStart: "开始(秒)",
videoEditTextEnd: "结束(秒)",
videoEditTextSize: "大小(%)",
videoEditTextColor: "颜色",
videoEditTextPosition: "位置",
videoEditTextOutline: "描边",
videoEditTextBackground: "背景",
videoEditTextAnchorLabels: ["↖ 左上", "↑ 上中", "↗ 右上", "← 左中", "● 中央", "→ 右中", "↙ 左下", "↓ 下中", "↘ 右下"],
videoEditBurningText: "正在烧录文字...",
videoEditAudioTitle: "音频",
videoEditKeepAudio: "保留原始音频",
videoEditOriginalVolume: "原始音量",
videoEditBgmSelect: "选择BGM...",
videoEditBgmRemove: "移除BGM",
videoEditBgmNone: "无BGM",
videoEditBgmVolume: "BGM音量",
videoEditBgmOffset: "BGM起始(秒)",
videoEditBgmHint: "将音频文件拖放到此处,或在 Asset 中对视频使用“设为BGM”",
videoEditBgmNoAudio: "该视频没有音轨",
videoEditBgmUnsupported: "请拖放音频或视频文件",
videoAssetSetAsBgm: "设为BGM(仅音频)",
videoAssetSetAsBgmDone: (name) => `已将“${name}”的音频设为BGM`,
videoEditCrop: "裁切画面",
videoEditCropEdit: "编辑裁切",
videoEditCropDone: "完成",
videoEditCropCancel: "取消",
videoEditCropReset: "重置",
videoEditCropAspect: "宽高比",
videoEditCropFree: "自由",
videoEditCropNone: "完整画面",
videoEditUndo: "撤销 (Ctrl+Z)",
videoEditRedo: "重做 (Ctrl+Y / Ctrl+Shift+Z)",
videoConfirmClearPlan: "要清空当前 Plan 吗?",
videoPlanPause: "暂停",
videoPlanResume: "继续",
@@ -4325,7 +4433,7 @@ const LANGUAGES = {
helpVideo8: "链接功能:将某个区块的 First Image 设置为“使用前一个区块的最后一帧”,即可与上一个片段流畅衔接——如果前一个区块已明确设置了 Last Image,将直接使用该图片;否则会自动从其生成的视频中提取实际的最后一帧。",
helpVideo9: "Project标签页(左侧边栏):中央为 Plan 子标签时显示已保存的 Plan 列表,为 Edit 子标签时显示已保存的 Edit 项目列表(缩略图/区块数或片段数、最后更新时间),点击即可加载,或用 × 按钮删除;再次点击同一标签会收起面板。",
helpVideo10: "高亮颜色:当所加载的工作流实际支持该字段时,Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple / Noise Seed / Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance 标签会变为与 GenerateUI Model 标签页相同的高亮颜色(可在 Settings 标签页自定义)——例如 Wan2.2 工作流(没有 ResolutionSelector 节点)中,Aspect Ratio / Megapixels / Multiple 不会高亮,取而代之的是 Width / Height 会高亮。",
helpVideo11: "Edit 子标签:独立于生成流程的轻量剪辑时间线,用于裁剪和拼接视频/图片片段。通过 Video Source 面板的「Add to Edit」按钮,或 Asset 子标签的「Send to Edit」按钮添加片段,拖动区块(或使用 ◀/▶ 工具栏按钮)重新排序,对选中片段执行 Duplicate/Delete,Clear 可清空时间线。选中视频片段的裁剪面板会显示时间刻度尺、与 Source 预览同步的可拖动播放头,以及带时间标签的可拖动入/出点手柄(图片片段则只有一个简单的保持时长输入框)。「▶ Preview」会按入/出点依次裁剪并在右侧结果预览区连续播放所有就绪片段;「Export」会构建并运行实际的 ComfyUI 图(裁剪→若多于一个片段则拼接→保存),并自动把结果标记进 Video Temp 分组。目前导出多个片段会丢失音频,因为 ComfyUI 的拼接节点要求所有输入的音频格式一致。导出面板的 Save/Save As/Load 按钮会把时间线本身(片段顺序、裁剪区间、每个片段引用的已上传文件)作为 JSON 项目保存到服务器;Load 会根据保存的服务器引用重新获取并重新探测每个片段,因此原始文件必须仍然存在。已保存的项目在中央为 Edit 子标签时也会显示在侧边栏的 Project 标签中(参见 helpVideo9)。",
helpVideo11: "Edit 子标签:独立于生成流程的轻量剪辑时间线,用于裁剪和拼接视频/图片片段。通过 Video Source 面板的「Add to Edit」按钮,或 Asset 子标签的「Send to Edit」按钮添加片段,拖动区块(或使用 ◀/▶ 工具栏按钮)重新排序,对选中片段执行 Duplicate/Delete,Clear 可清空时间线(音频区域也会恢复初始状态)。选中视频片段的裁剪面板会显示时间刻度尺、与 Source 预览同步的可拖动播放头,以及带时间标签的可拖动入/出点手柄(图片片段则只有一个简单的保持时长输入框)。「▶ Preview」会按入/出点依次裁剪并在右侧结果预览区连续播放所有就绪片段;「Export」会构建并运行实际的 ComfyUI 图(裁剪→若多于一个片段则拼接→保存),并自动把结果标记进 Video Temp 分组。导出面板的音频区域用于设置音轨:“保留原始音频”(可调节音量 dB)在多个片段拼接时也会让各片段的音频与其区间对齐(图片片段区间为静音);“选择BGM...”可将一个音频文件叠加到整个视频上(可设置音量与起始位置,比视频长的 BGM 会按视频长度截断)。此外,每个片段都可以添加文字叠加(裁剪面板下方的“+ 添加文字”:文字、片段内开始/结束秒数、大小(占画面高度的%)、颜色、9 方位位置、描边、背景),可在 Source/Result 预览上实时查看,并在导出时作为最后一步烧录进视频。BGM 可通过“选择BGM...”按钮、将音频/视频文件拖放到音频区域,或在 Asset 子标签中对视频使用“设为BGM”(使用该视频的音轨)来指定。视频片段还可以裁切画面:点击“编辑裁切”会在 Source 预览上显示可拖动/缩放的裁切框(可锁定为 16:9 / 9:16 / 1:1 / 4:3 / 3:4),“完成”确认,“取消”恢复到编辑前状态,切换到其他子标签/标签页时按“完成”处理并结束编辑;导出尺寸为第一个视频片段裁切后的尺寸,尺寸不同的其他片段会以中心为基准适配。时间轴工具栏的 ↶/↷(或 Ctrl+Z / Ctrl+Y)可撤销/重做对片段、裁剪、裁切、文字和音频设置的编辑。导出面板的 Save/Save As/Load 按钮会把时间线本身(片段顺序、裁剪区间、每个片段引用的已上传文件)作为 JSON 项目保存到服务器;Load 会根据保存的服务器引用重新获取并重新探测每个片段,因此原始文件必须仍然存在。已保存的项目在中央为 Edit 子标签时也会显示在侧边栏的 Project 标签中(参见 helpVideo9)。",
helpVideo12: "Asset 子标签(中央面板,位于 Plan/Edit 旁):浏览标记到 Video Assets 或 Video Temp 分组的所有视频/图片(通过分组下拉框选择,或选择「All Video Assets」同时浏览两者)。搜索框按文件名过滤,▦/☰ 按钮可在缩略图网格和表格视图(文件名/更新时间/标签)之间切换。选中一项会显示其标签/备注并加载到中央预览,此外还提供「Send to Edit」(作为片段加入 Edit 子标签);仅图片还额外提供「Set as First Image」「Set as Last Image」(直接上传到当前选中的 Plan 区块,并切换到 Plan 子标签)。",
},
};
+19 -1
View File
@@ -12,7 +12,7 @@ import { VIDEO_GROUP, VTEMP_GROUP, ensureVideoGroup, isVideoFile } from "./galle
import { setSourcePreview } from "./video-preview.js";
import { showToast } from "./app.js";
import { t } from "./i18n.js";
import { addClipFromFile } from "./video-edit-tab.js";
import { addClipFromFile, setBgmFromFile } from "./video-edit-tab.js";
import { setBlockImageFromFile } from "./video-plan-tab.js";
// Sentinel for the "All Video Assets" option — not a real backend group, since
@@ -242,6 +242,7 @@ function _renderDetail(img) {
<textarea id="wfm-video-asset-memo" class="wfm-textarea" rows="3"></textarea>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-memo-save" style="margin-top:6px;">Save Memo</button>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-send-to-edit" style="width:100%;margin-top:12px;">${t("videoEditSendToEdit")}</button>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-set-bgm" style="width:100%;margin-top:6px;display:${isVideoFile(img) ? "block" : "none"};">${t("videoAssetSetAsBgm")}</button>
<div id="wfm-video-asset-set-plan-image-row" style="display:${isVideoFile(img) ? "none" : "flex"};gap:6px;margin-top:6px;">
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-set-first" style="flex:1;">${t("videoAssetSetAsFirst")}</button>
<button type="button" class="wfm-btn wfm-btn-sm" id="wfm-video-asset-set-last" style="flex:1;">${t("videoAssetSetAsLast")}</button>
@@ -287,6 +288,23 @@ function _renderDetail(img) {
}
});
// Gallery lists no audio files, so a video asset's own audio track is the
// Asset-side way to pick a BGM (LoadAudio decodes audio from video files).
panel.querySelector("#wfm-video-asset-set-bgm")?.addEventListener("click", async () => {
try {
const res = await fetch(`/wfm/gallery/image/serve?path=${encodeURIComponent(img.path)}`);
if (!res.ok) throw new Error(String(res.status));
const blob = await res.blob();
const file = new File([blob], img.filename, { type: blob.type || "video/mp4" });
if (await setBgmFromFile(file, img.filename)) {
document.querySelector('.wfm-video-center-panel .wfm-video-subtab-btn[data-video-subtab="edit"]')?.click();
showToast(t("videoAssetSetAsBgmDone", img.filename), "success");
}
} catch (err) {
showToast(t("errorWithMsg", err.message), "error");
}
});
const setAsPlanImage = async (which) => {
try {
const res = await fetch(`/wfm/gallery/image/serve?path=${encodeURIComponent(img.path)}`);
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -18,8 +18,8 @@
*/
const _PANES = {
source: { video: "wfm-video-source-preview-video", img: "wfm-video-source-preview-img", placeholder: "wfm-video-source-preview-placeholder" },
result: { video: "wfm-video-preview-video", img: "wfm-video-preview-img", placeholder: "wfm-video-preview-placeholder" },
source: { frame: "wfm-video-source-preview-frame", video: "wfm-video-source-preview-video", img: "wfm-video-source-preview-img", placeholder: "wfm-video-source-preview-placeholder" },
result: { frame: "wfm-video-preview-frame", video: "wfm-video-preview-video", img: "wfm-video-preview-img", placeholder: "wfm-video-preview-placeholder" },
};
const _objectUrls = { source: null, result: null };
@@ -111,3 +111,16 @@ export function getAllPreviewVideoElements() {
export function getResultPreviewVideoElement() {
return document.getElementById(_PANES.result.video);
}
// The frame/<video>/<img> elements of one pane ("source" | "result") — used by
// video-edit-tab.js to position its text-overlay preview layer over whichever
// media element is currently showing in that pane.
export function getPreviewPaneElements(pane) {
const ids = _PANES[pane];
if (!ids) return null;
return {
frame: document.getElementById(ids.frame),
video: document.getElementById(ids.video),
img: document.getElementById(ids.img),
};
}
+10
View File
@@ -132,6 +132,16 @@ function _applyVideoI18n() {
setText("wfm-video-edit-duplicate-btn", "videoEditDuplicate");
setText("wfm-video-edit-delete-btn", "videoEditDelete");
setText("wfm-video-edit-clear-btn", "videoEditClearBtn");
setTitle("wfm-video-edit-undo-btn", "videoEditUndo");
setTitle("wfm-video-edit-redo-btn", "videoEditRedo");
setText("wfm-video-edit-audio-title", "videoEditAudioTitle");
setText("wfm-video-edit-keep-audio-label", "videoEditKeepAudio");
setText("wfm-video-edit-orig-vol-label", "videoEditOriginalVolume");
setText("wfm-video-edit-bgm-btn", "videoEditBgmSelect");
setTitle("wfm-video-edit-bgm-clear", "videoEditBgmRemove");
setText("wfm-video-edit-bgm-vol-label", "videoEditBgmVolume");
setText("wfm-video-edit-bgm-offset-label", "videoEditBgmOffset");
setText("wfm-video-edit-bgm-hint", "videoEditBgmHint");
setText("wfm-video-source-add-to-edit", "videoSourceAddToEdit");
setText("wfm-video-source-label", "videoSourceLabel");
setText("wfm-video-source-hint", "videoSourceHint");
+33 -3
View File
@@ -2118,7 +2118,7 @@
<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): browse every saved plan file (thumbnail, block count, last updated), click one to load it into the Plan editor, or delete it with the × button; clicking the same tab again collapses the panel.</li>
<li id="wfm-help-video-10">Highlight color: the Prompt / Duration / First Image / Last Image / Aspect Ratio / Megapixels / Multiple / Noise Seed / Width / Height / Turbo Mode / Strength / Steps / Prompt Enhance labels turn the same highlight color as the GenerateUI Model tab (customizable in the Settings tab) when the loaded workflow actually supports that field — e.g. for a Wan2.2 workflow (no ResolutionSelector node), the Aspect Ratio / Megapixels / Multiple labels stay unhighlighted while Width / Height light up instead.</li>
<li id="wfm-help-video-11">Edit subtab: a lightweight clip timeline for trimming and concatenating video/image clips outside of generation. Add clips via the Video Source panel's "Add to Edit" button or the Asset subtab's "Send to Edit" button, then drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, and Clear to empty the timeline. The trim panel for the selected video clip shows a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges — image clips instead get a simple hold-duration field. ▶ Preview plays every ready clip back-to-back (trimmed to its in/out points) in the right-hand result preview; Export builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into the Video Temp group automatically. Exporting more than one clip currently drops audio, since ComfyUI's concatenation node requires uniform audio across all inputs.</li>
<li id="wfm-help-video-11">Edit subtab: a lightweight clip timeline for trimming and concatenating video/image clips outside of generation. Add clips via the Video Source panel's "Add to Edit" button or the Asset subtab's "Send to Edit" button, then drag blocks to reorder (or use the ◀/▶ toolbar buttons), Duplicate/Delete the selected clip, and Clear to empty the timeline (this also resets the Audio section). The trim panel for the selected video clip shows a time ruler, a draggable playhead synced to the Source preview, and draggable in/out handles with time badges — image clips instead get a simple hold-duration field. ▶ Preview plays every ready clip back-to-back (trimmed to its in/out points) in the right-hand result preview; Export builds and runs the actual ComfyUI graph (trim → concatenate if more than one clip → save), tagging the result into the Video Temp group automatically. The Audio section of the export panel controls the soundtrack: &quot;Keep original audio&quot; (with its own volume in dB) keeps each clip&#39;s audio aligned to its segment even across multiple clips (image clips contribute silence), and &quot;BGM...&quot; lays one audio file over the whole video with its own volume and start offset. Each clip can also carry text overlays (&quot;+ Add text&quot; under the trim panel), previewed live over the Source/Result panes and burned in on Export as a final pass. The BGM can be chosen with &quot;BGM...&quot;, by dropping an audio/video file onto the Audio section, or with &quot;Set as BGM&quot; on a video in the Asset subtab (its audio track is used). Video clips can also be cropped with &quot;Edit crop&quot; (a draggable rectangle over the Source preview, optionally aspect-locked; Cancel restores the previous crop); the export size becomes the first video clip&#39;s cropped size. ↶/↷ (or Ctrl+Z / Ctrl+Y) undo and redo timeline edits.</li>
<li id="wfm-help-video-12">Asset subtab (center panel, next to Plan/Edit): browses every video/image tagged into Video Assets or Video Temp (via the group dropdown, or "All Video Assets" for both). Search filters by filename; the ▦/☰ buttons switch between a thumbnail grid and a table (filename/updated/tags). Selecting an item shows its tags/memo, loads it into the center preview, and offers "Send to Edit" (queues it as a clip in the Edit subtab) plus, for images only, "Set as First Image"/"Set as Last Image" (uploads it straight into the currently-selected Plan block, switching to the Plan subtab).</li>
</ul>
</div>
@@ -2942,6 +2942,8 @@
<button type="button" id="wfm-video-edit-duplicate-btn" class="wfm-btn wfm-btn-xs">Duplicate</button>
<button type="button" id="wfm-video-edit-delete-btn" class="wfm-btn wfm-btn-xs">Delete</button>
<button type="button" id="wfm-video-edit-preview-btn" class="wfm-btn wfm-btn-xs">▶ Preview</button>
<button type="button" id="wfm-video-edit-undo-btn" class="wfm-btn wfm-btn-xs" title="Undo (Ctrl+Z)" disabled>↶</button>
<button type="button" id="wfm-video-edit-redo-btn" class="wfm-btn wfm-btn-xs" title="Redo (Ctrl+Y)" disabled>↷</button>
<span class="wfm-i2i-status wfm-video-edit-total-duration" id="wfm-video-edit-total-duration" style="margin-left:auto;"></span>
<button type="button" id="wfm-video-edit-clear-btn" class="wfm-btn wfm-btn-xs wfm-btn-danger">Clear</button>
</div>
@@ -2955,9 +2957,37 @@
<div style="display:flex;gap:6px;">
<button type="button" id="wfm-video-edit-save-btn" class="wfm-btn wfm-btn-sm" style="flex:1;">Save</button>
<button type="button" id="wfm-video-edit-saveas-btn" class="wfm-btn wfm-btn-sm" style="flex:1;">Save As</button>
<button type="button" id="wfm-video-edit-load-btn" class="wfm-btn wfm-btn-sm" style="flex:1;">Load</button>
<!-- File pickers are native <label>-wrapped inputs (same as the Video Source drop zone),
not a button calling input.click() from JS — that programmatic path can be refused
by the browser without any error (confirmed on a user's machine for the BGM picker). -->
<label id="wfm-video-edit-load-btn" class="wfm-btn wfm-btn-sm wfm-video-edit-file-btn" style="flex:1;" tabindex="0" role="button">Load<input type="file" id="wfm-video-edit-load-file-input" accept="application/json,.json" style="display:none;"></label>
</div>
<div class="wfm-video-edit-audio-panel">
<div class="wfm-video-edit-section-head"><span id="wfm-video-edit-audio-title">Audio</span></div>
<label class="wfm-video-checkbox-inline">
<input type="checkbox" id="wfm-video-edit-keep-audio" checked> <span id="wfm-video-edit-keep-audio-label">Keep original audio</span>
</label>
<div class="wfm-video-edit-audio-row">
<label for="wfm-video-edit-orig-vol" id="wfm-video-edit-orig-vol-label">Original volume</label>
<input type="range" id="wfm-video-edit-orig-vol" min="-30" max="12" step="1" value="0">
<span class="wfm-video-edit-audio-val" id="wfm-video-edit-orig-vol-val">0 dB</span>
</div>
<div class="wfm-video-edit-bgm-file">
<label class="wfm-btn wfm-btn-xs wfm-video-edit-file-btn" tabindex="0" role="button"><span id="wfm-video-edit-bgm-btn">BGM...</span><input type="file" id="wfm-video-edit-bgm-input" accept="audio/*,video/mp4,.mp3,.wav,.m4a,.aac,.flac,.ogg,.opus,.mp4" style="display:none;"></label>
<span class="wfm-i2i-status wfm-video-edit-bgm-name" id="wfm-video-edit-bgm-name"></span>
<button type="button" id="wfm-video-edit-bgm-clear" class="wfm-btn wfm-btn-xs" title="Remove BGM">✕</button>
</div>
<div class="wfm-i2i-status wfm-video-edit-bgm-hint" id="wfm-video-edit-bgm-hint">Drop an audio file here, or use &quot;Set as BGM&quot; on a video in Asset</div>
<div class="wfm-video-edit-audio-row">
<label for="wfm-video-edit-bgm-vol" id="wfm-video-edit-bgm-vol-label">BGM volume</label>
<input type="range" id="wfm-video-edit-bgm-vol" min="-30" max="12" step="1" value="-6">
<span class="wfm-video-edit-audio-val" id="wfm-video-edit-bgm-vol-val">-6 dB</span>
</div>
<div class="wfm-video-edit-audio-row">
<label for="wfm-video-edit-bgm-offset" id="wfm-video-edit-bgm-offset-label">BGM start (s)</label>
<input type="number" id="wfm-video-edit-bgm-offset" class="wfm-input" min="0" step="0.1" value="0">
</div>
</div>
<input type="file" id="wfm-video-edit-load-file-input" accept="application/json,.json" style="display:none;">
<button type="button" id="wfm-video-edit-export-btn" class="wfm-btn wfm-btn-primary" style="width:100%;margin-top:8px;">Export</button>
<div class="wfm-gen-progress-container" style="margin-top:8px;">
<div class="wfm-gen-progress-track">