Update: Added resize modes (Fit, Stretch) and FPS output
This commit is contained in:
@@ -1,92 +1,68 @@
|
||||
\# 🎬 ComfyUI Video Workbench
|
||||
# 🎬 ComfyUI Auto-Edit Workbench
|
||||
|
||||
A powerful Custom Node for **ComfyUI** that acts as a **mini video editing studio**.
|
||||
It automatically assembles (concatenates) multiple video clips from a folder into a single sequence, handling resolution resizing, aspect ratios, and frame rates intelligently.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
A powerful Custom Node for ComfyUI that acts as a \*\*mini video editing workbench\*\*.
|
||||
* **Batch Assembly**: Loads all video files from a specific folder automatically.
|
||||
* **3 Resize Modes**:
|
||||
* **Crop (Fill Screen)**: Fills the screen, cropping edges if necessary (perfect for mixing vertical/horizontal footage).
|
||||
* **Fit (Black Bars)**: Adds black borders (letterboxing) so the entire video is visible.
|
||||
* **Stretch**: Distorts the video to fit the target resolution perfectly.
|
||||
* **Smart Strategies**:
|
||||
* **Resolution**: Auto-detect from the *First Video*, *Smallest*, *Largest*, or use a *Custom* size.
|
||||
* **FPS**: Auto-detect from *First Video*, *Lowest*, *Highest*, or *Custom*.
|
||||
* **Audio Support**: Concatenates audio tracks seamlessly.
|
||||
* **Safety Limits**: `limit_duration_sec` parameter to trim the output (great for testing without Out-Of-Memory errors).
|
||||
|
||||
It automatically assembles (concatenates) multiple video clips from a folder into a single sequence, handling resolution resizing (crop/fill) and FPS matching intelligently.
|
||||
## 📦 Installation
|
||||
|
||||
### Method 1: ComfyUI Manager (Recommended)
|
||||
1. Install [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager).
|
||||
2. Click **"Install via Git URL"**.
|
||||
3. Paste this repository URL: `https://github.com/Verolelb/ComfyUI-Video-Workbench`.
|
||||
|
||||
### Method 2: Manual Installation
|
||||
1. Navigate to your `ComfyUI/custom_nodes/` directory.
|
||||
2. Clone this repository:
|
||||
```bash
|
||||
git clone https://github.com/Verolelb/ComfyUI-Video-Workbench.git
|
||||
```
|
||||
3. Open a terminal in this folder and install the requirement:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
*(This node relies on `moviepy` and `numpy`).*
|
||||
|
||||
\## ✨ Features
|
||||
## 🛠️ How to use
|
||||
|
||||
### 1. Inputs
|
||||
|
||||
* **`directory_path`**: Absolute path to your video folder (e.g., `C:/Users/You/Desktop/MyClips`).
|
||||
* **`sort_strategy`**: Order of clips (Alphabetical, Date, Random).
|
||||
* **`resize_mode`**:
|
||||
* Use **Crop** for immersive, full-screen montages (e.g., Shorts/Reels).
|
||||
* Use **Fit** to preserve all details with black bars.
|
||||
* **`resolution_strategy`**: Keep it on **"First Video"** to let the first clip dictate the format of the whole timeline.
|
||||
* **`fps_strategy`**: Keep on **"First Video"** or force a **Custom** value (e.g., 24, 30, 60).
|
||||
|
||||
\* \*\*Batch Assembly\*\*: Loads all video files from a specified folder.
|
||||
### 2. Outputs
|
||||
|
||||
\* \*\*Smart Resizing\*\*: Automatically handles mixed aspect ratios (Vertical TikToks + Horizontal YouTube).
|
||||
1. **`IMAGE`**: The assembled video stream.
|
||||
2. **`AUDIO`**: The combined audio track.
|
||||
3. **`FLOAT` (fps)**: The final frame rate (useful to connect to the "frame_rate" input of saving nodes).
|
||||
|
||||
\* \*Center Crop / Aspect Fill logic included.\*
|
||||
### 3. Recommended Workflow for Saving
|
||||
|
||||
\* \*\*Intelligent Strategies\*\*:
|
||||
To save the video **with audio**, it is highly recommended to use the **Video Combine** node from the [ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite).
|
||||
|
||||
\* \*\*Resolution\*\*: Can adapt to the \*First Video\*, the \*Smallest\*, the \*Largest\*, or a \*Custom\* size.
|
||||
* Connect `AutoEditWorkbench` **IMAGE** -> `Video Combine` **images**.
|
||||
* Connect `AutoEditWorkbench` **AUDIO** -> `Video Combine` **audio**.
|
||||
* Connect `AutoEditWorkbench` **FPS** -> `Video Combine` **frame_rate** (Convert widget to input).
|
||||
* Set format to **`video/h264-mp4`**.
|
||||
|
||||
\* \*\*FPS\*\*: Can adapt to the \*First Video\*, \*Lowest\*, \*Highest\*, or \*Custom\*.
|
||||
|
||||
\* \*\*Audio Support\*\*: Concatenates audio tracks perfectly.
|
||||
|
||||
\* \*\*Safety Limits\*\*: Duration limit parameter to prevent Out-Of-Memory errors during testing.
|
||||
|
||||
|
||||
|
||||
\## 📦 Installation
|
||||
|
||||
|
||||
|
||||
\### Method 1: ComfyUI Manager (Recommended)
|
||||
|
||||
1\. Install \[ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager).
|
||||
|
||||
2\. Search for "Video Workbench" (once indexed) or "Install via Git URL".
|
||||
|
||||
3\. Paste this repository URL.
|
||||
|
||||
|
||||
|
||||
\### Method 2: Manual
|
||||
|
||||
1\. Go to your `ComfyUI/custom\_nodes/` directory.
|
||||
|
||||
2\. Clone this repo:
|
||||
|
||||
```bash
|
||||
|
||||
git clone https://github.com/Verolelb/ComfyUI-Video-Workbench.git
|
||||
|
||||
```
|
||||
|
||||
3\. Install dependencies:
|
||||
|
||||
```bash
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
\## 🛠️ How to use
|
||||
|
||||
|
||||
|
||||
1\. \*\*Directory Path\*\*: Paste the absolute path to your folder containing video clips (e.g., `C:/Users/You/Videos/Vacation`).
|
||||
|
||||
2\. \*\*Sort Strategy\*\*: Choose how to order the clips (Alphabetical, Date, Random).
|
||||
|
||||
3\. \*\*Resolution \& FPS Strategy\*\*:
|
||||
|
||||
\* Leave on \*\*"First Video"\*\* to let the first clip dictate the settings for the whole timeline.
|
||||
|
||||
\* Use \*\*"Custom"\*\* to force specific dimensions (e.g., 1080x1920).
|
||||
|
||||
|
||||
|
||||
\## ❤️ Credits
|
||||
|
||||
|
||||
|
||||
Created by \*\*Verolelb\*\*.
|
||||
|
||||
Powered by \[MoviePy](https://zulko.github.io/moviepy/).
|
||||
## ❤️ Credits
|
||||
|
||||
Created by **Verolelb**.
|
||||
Powered by [MoviePy](https://zulko.github.io/moviepy/).
|
||||
+51
-40
@@ -5,7 +5,7 @@ import random
|
||||
|
||||
# Try to import moviepy
|
||||
try:
|
||||
from moviepy.editor import VideoFileClip, concatenate_videoclips
|
||||
from moviepy.editor import VideoFileClip, concatenate_videoclips, ColorClip, CompositeVideoClip
|
||||
except ImportError:
|
||||
print("❌ Error: 'moviepy' is not installed. Please run: pip install moviepy")
|
||||
|
||||
@@ -20,6 +20,9 @@ class AutoEditWorkbench:
|
||||
"directory_path": ("STRING", {"default": "./input/my_footage", "multiline": False}),
|
||||
"sort_strategy": (["alphabetical_asc", "alphabetical_desc", "date_oldest", "date_newest", "random"],),
|
||||
|
||||
# --- RESIZE MODES ---
|
||||
"resize_mode": (["Crop (Fill Screen)", "Fit (Black Bars)", "Stretch"], {"default": "Crop (Fill Screen)"}),
|
||||
|
||||
# --- RESOLUTION SETTINGS ---
|
||||
"resolution_strategy": (["First Video", "Smallest (Min Area)", "Largest (Max Area)", "Custom"], {"default": "First Video"}),
|
||||
"custom_width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
|
||||
@@ -34,14 +37,12 @@ class AutoEditWorkbench:
|
||||
},
|
||||
}
|
||||
|
||||
# AJOUT DE LA SORTIE FPS (FLOAT) ICI
|
||||
RETURN_TYPES = ("IMAGE", "AUDIO", "FLOAT")
|
||||
RETURN_NAMES = ("images", "audio", "fps")
|
||||
|
||||
FUNCTION = "process_workbench"
|
||||
CATEGORY = "VideoTools/Editing"
|
||||
|
||||
def process_workbench(self, directory_path, sort_strategy,
|
||||
def process_workbench(self, directory_path, sort_strategy, resize_mode,
|
||||
resolution_strategy, custom_width, custom_height,
|
||||
fps_strategy, custom_fps,
|
||||
limit_duration_sec):
|
||||
@@ -64,27 +65,23 @@ class AutoEditWorkbench:
|
||||
elif sort_strategy == "date_newest": files.sort(key=os.path.getmtime, reverse=True)
|
||||
elif sort_strategy == "random": random.shuffle(files)
|
||||
|
||||
# --- 2. Strategy Analysis (Resolution & FPS) ---
|
||||
# --- 2. Strategy Analysis ---
|
||||
target_w, target_h = custom_width, custom_height
|
||||
target_fps = custom_fps
|
||||
|
||||
# Helper to read metadata quickly
|
||||
def get_meta(p):
|
||||
try:
|
||||
with VideoFileClip(p) as c: return {'w': c.w, 'h': c.h, 'fps': c.fps, 'area': c.w*c.h}
|
||||
except: return None
|
||||
|
||||
# Check if we need to scan all files
|
||||
need_scan = (resolution_strategy not in ["Custom", "First Video"]) or (fps_strategy not in ["Custom", "First Video"])
|
||||
|
||||
# Optimization: Apply "First Video" immediately without scanning everything if possible
|
||||
if resolution_strategy == "First Video" or fps_strategy == "First Video":
|
||||
m = get_meta(files[0])
|
||||
if m:
|
||||
if resolution_strategy == "First Video": target_w, target_h = m['w'], m['h']
|
||||
if fps_strategy == "First Video": target_fps = m['fps']
|
||||
|
||||
# Full scan if needed (Min/Max strategies)
|
||||
if need_scan:
|
||||
stats = [m for f in files if (m := get_meta(f))]
|
||||
if stats:
|
||||
@@ -100,30 +97,55 @@ class AutoEditWorkbench:
|
||||
elif fps_strategy == "Highest FPS":
|
||||
target_fps = max(stats, key=lambda x: x['fps'])['fps']
|
||||
|
||||
print(f"🎯 [AutoEdit] Target: {target_w}x{target_h} @ {target_fps:.2f} fps")
|
||||
print(f"🎯 [AutoEdit] Target: {target_w}x{target_h} @ {target_fps:.2f} fps | Mode: {resize_mode}")
|
||||
|
||||
# --- 3. Clip Processing ---
|
||||
clips = []
|
||||
for file in files:
|
||||
try:
|
||||
clip = VideoFileClip(file)
|
||||
# Aspect Fill / Center Crop Logic
|
||||
ratio_target = target_w / target_h
|
||||
ratio_clip = clip.w / clip.h
|
||||
|
||||
if ratio_clip > ratio_target:
|
||||
# Clip is wider -> Resize Height, Crop Width
|
||||
clip = clip.resize(height=target_h)
|
||||
clip = clip.crop(x1=clip.w/2 - target_w/2, width=target_w)
|
||||
else:
|
||||
# Clip is taller -> Resize Width, Crop Height
|
||||
clip = clip.resize(width=target_w)
|
||||
clip = clip.crop(y1=clip.h/2 - target_h/2, height=target_h)
|
||||
|
||||
# Finalize
|
||||
clip = clip.resize((target_w, target_h))
|
||||
# --- RESIZE LOGIC ---
|
||||
if resize_mode == "Stretch":
|
||||
# Ignore aspect ratio, just squish/stretch
|
||||
clip = clip.resize((target_w, target_h))
|
||||
|
||||
elif resize_mode == "Fit (Black Bars)":
|
||||
# Resize to fit inside box, keep aspect ratio
|
||||
# Check which dimension is the limiting factor
|
||||
ratio_target = target_w / target_h
|
||||
ratio_clip = clip.w / clip.h
|
||||
|
||||
if ratio_clip > ratio_target:
|
||||
# Video is "wider" than target -> Fit Width
|
||||
clip = clip.resize(width=target_w)
|
||||
else:
|
||||
# Video is "taller" than target -> Fit Height
|
||||
clip = clip.resize(height=target_h)
|
||||
|
||||
# Create black background and composite
|
||||
background = ColorClip(size=(target_w, target_h), color=(0,0,0), duration=clip.duration)
|
||||
clip = CompositeVideoClip([background, clip.set_position("center")])
|
||||
|
||||
else: # Default: Crop (Fill Screen)
|
||||
ratio_target = target_w / target_h
|
||||
ratio_clip = clip.w / clip.h
|
||||
|
||||
if ratio_clip > ratio_target:
|
||||
# Clip is wider -> Resize Height, Crop Width
|
||||
clip = clip.resize(height=target_h)
|
||||
clip = clip.crop(x1=clip.w/2 - target_w/2, width=target_w)
|
||||
else:
|
||||
# Clip is taller -> Resize Width, Crop Height
|
||||
clip = clip.resize(width=target_w)
|
||||
clip = clip.crop(y1=clip.h/2 - target_h/2, height=target_h)
|
||||
|
||||
clip = clip.resize((target_w, target_h))
|
||||
|
||||
# Finalize FPS
|
||||
clip = clip.set_fps(target_fps)
|
||||
clips.append(clip)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: Skipped {file}. Reason: {e}")
|
||||
|
||||
@@ -133,56 +155,45 @@ class AutoEditWorkbench:
|
||||
# --- 4. Concatenation ---
|
||||
final_clip = concatenate_videoclips(clips, method="compose")
|
||||
|
||||
# Duration Limit
|
||||
if limit_duration_sec > 0 and final_clip.duration > limit_duration_sec:
|
||||
final_clip = final_clip.subclip(0, limit_duration_sec)
|
||||
|
||||
# --- 5. Export Video (Tensor) ---
|
||||
# --- 5. Export Video ---
|
||||
print("⏳ [AutoEdit] Converting video frames to Tensor...")
|
||||
frames = [frame for frame in final_clip.iter_frames()]
|
||||
video_np = np.stack(frames)
|
||||
video_tensor = torch.from_numpy(video_np).float() / 255.0
|
||||
|
||||
# --- 6. Export Audio (Tensor) ---
|
||||
# --- 6. Export Audio ---
|
||||
audio_dict = None
|
||||
if final_clip.audio is not None:
|
||||
sr = 44100
|
||||
print(f"🔊 [AutoEdit] Processing Audio (Duration: {final_clip.duration}s)...")
|
||||
|
||||
try:
|
||||
# Sync duration to prevent errors
|
||||
final_clip.audio = final_clip.audio.set_duration(final_clip.duration)
|
||||
|
||||
# Iterate by 1-second chunks to avoid MemoryError and Numpy compatibility issues
|
||||
audio_chunks = []
|
||||
# Chunk processing for stability
|
||||
for chunk in final_clip.audio.iter_chunks(fps=sr, chunk_duration=1.0):
|
||||
audio_chunks.append(chunk)
|
||||
|
||||
if len(audio_chunks) > 0:
|
||||
audio_np = np.vstack(audio_chunks)
|
||||
|
||||
# Ensure correct shape for ComfyUI
|
||||
if len(audio_np.shape) == 1: # Mono
|
||||
if len(audio_np.shape) == 1:
|
||||
audio_np = audio_np.reshape((-1, 1))
|
||||
|
||||
# (Samples, Channels) -> (1, Channels, Samples)
|
||||
audio_tensor = torch.from_numpy(audio_np.T).float().unsqueeze(0)
|
||||
audio_dict = {"waveform": audio_tensor, "sample_rate": sr}
|
||||
print("✅ [AutoEdit] Audio ready.")
|
||||
else:
|
||||
print("⚠️ [AutoEdit] Audio track was empty.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ [AutoEdit] Audio Error: {e}")
|
||||
|
||||
# Cleanup resources
|
||||
final_clip.close()
|
||||
for c in clips: c.close()
|
||||
|
||||
# RETOUR AVEC LE FPS EN PLUS
|
||||
return (video_tensor, audio_dict, float(target_fps))
|
||||
|
||||
# Mappings for ComfyUI
|
||||
# Mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AutoEditWorkbench": AutoEditWorkbench
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user