Update: Added resize modes (Fit, Stretch) and FPS output

This commit is contained in:
Verolelb
2025-12-23 12:40:23 +01:00
parent fc7d474d46
commit ee655b3376
3 changed files with 104 additions and 117 deletions
+52 -76
View File
@@ -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
View File
@@ -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