Add batch-aware nodes
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
# Wan VACE Prep
|
||||
|
||||
ComfyUI nodes for preparing videos for Wan VACE generation - handles transitions and extensions with automatic control video generation.
|
||||
ComfyUI nodes for Wan VACE workflows. Control video and mask creation for transitions and extensions, plus workflow utilites.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**What it does:** Automates the tedious parts of VACE workflows by generating control videos and masks for smooth video transitions and extensions.
|
||||
|
||||
**Install via ComfyUI Manager:** Search for "Wan VACE Prep" → Install → Restart
|
||||
**Install via ComfyUI Manager:** Search for "Wan VACE Prep"
|
||||
|
||||
**Or clone this repository:**
|
||||
```bash
|
||||
@@ -45,6 +42,60 @@ For smoothly joining two video clips together. Builds VACE controls for the tran
|
||||
|
||||
---
|
||||
|
||||
### Wan VACE Prep Batch
|
||||
Batch-aware version of Wan VACE Prep for processing multiple video pairs. Handles first/last iteration edge cases.
|
||||
|
||||

|
||||
|
||||
**Parameters:**
|
||||
|Parameter|Default|Description|
|
||||
|-|-|-|
|
||||
|video_1| |First video in the pair (IMAGE type)|
|
||||
|video_2| |Second video in the pair (IMAGE type)|
|
||||
|is_first|false|Set true for first iteration (index=0) - includes full beginning of video_1 in start_images|
|
||||
|is_last|false|Set true for last iteration - includes full ending of video_2 in end_images|
|
||||
|context_frames|8|Reference frames from each video edge that VACE uses for interpolation. These frames guide the model and are preserved in the output. Must be a multiple of 4.|
|
||||
|replace_frames|8|Number of frames at each transition edge to discard and regenerate. These create the actual transition blend zone. Must be a multiple of 4.|
|
||||
|new_frames|0|Number of completely new frames to generate between the two clips, extending the transition duration. Must be 0 or a multiple of 4.|
|
||||
|debug|false|Log diagnostic information to the console|
|
||||
|
||||
**Outputs:**
|
||||
|Output|Description|
|
||||
|-|-|
|
||||
|control_video|VACE control video input (context frames + placeholder for generation)|
|
||||
|control_mask|VACE control mask input (masks generation region)|
|
||||
|width, height, length|Control video dimensions|
|
||||
|start_images|Video segment from video_1 to preserve (excludes transition region)|
|
||||
|end_images|Video segment from video_2 to preserve (only populated on last iteration)|
|
||||
|
||||
---
|
||||
|
||||
### Wan VACE Batch Context
|
||||
Establishes iteration context for batch video processing workflows. Manages file paths, iteration tracking, and provides first/last flags for proper handling of video sequence boundaries.
|
||||
|
||||

|
||||
|
||||
**Parameters:**
|
||||
|Parameter|Default|Description|
|
||||
|-|-|-|
|
||||
|input_list| |List of video filenames to process (STRING, force input)|
|
||||
|input_dir| |Directory containing input videos|
|
||||
|project_name|.|Project name - workflow files created under ComfyUI/output/project_name. Use period (.) for no project name.|
|
||||
|index|0|Current iteration index (0-based). Valid range: 0 to (number of videos - 2)|
|
||||
|debug|false|Log iteration details to the console|
|
||||
|
||||
**Outputs:**
|
||||
|Output|Description|
|
||||
|-|-|
|
||||
|work_dir|Working directory path for intermediate files|
|
||||
|workfile_prefix|Filename prefix for this iteration's work files|
|
||||
|is_first|True if this is the first iteration (index=0)|
|
||||
|is_last|True if this is the last iteration|
|
||||
|video_1_filename|Full path to first video in current pair|
|
||||
|video_2_filename|Full path to second video in current pair|
|
||||
|
||||
--
|
||||
|
||||
### Wan VACE Extend
|
||||
For smoothly extending a video. Context frames from before the chosen extension point are used to build a control video for VACE conditioning.
|
||||
|
||||
@@ -76,11 +127,12 @@ For smoothly extending a video. Context frames from before the chosen extension
|
||||
---
|
||||
|
||||
### Load Videos From Folder (Simple)
|
||||
Load all videos from a folder. A simplified answer to KJNodes' LoadVideosFromFolder, which depends on VideoHelperSuite and can have problems in some environments.
|
||||
Load all videos from a folder for batch processing. A simplified, dependency-free alternative to KJNodes' LoadVideosFromFolder (which depends on VideoHelperSuite and can have problems in some environments).
|
||||
|
||||
- Formats: webm, mp4, mkv, gif, mov
|
||||
- All videos must have identical resolution
|
||||
- No external dependencies
|
||||
- **Formats:** webm, mp4, mkv, gif, mov
|
||||
- All videos must have identical resolution
|
||||
- No external dependencies
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
+5
-3
@@ -1,11 +1,13 @@
|
||||
"""
|
||||
Wan VACE Prep - Custom node for preparing videos for Wan VACE generation
|
||||
Wan VACE Prep - Custom nodes for preparing videos for Wan VACE generation
|
||||
"""
|
||||
from .wan_vace_prep import NODE_CLASS_MAPPINGS as PREP_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as PREP_DISPLAY
|
||||
from .wan_vace_prep_batch import NODE_CLASS_MAPPINGS as PREP_BATCH_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as PREP_BATCH_DISPLAY
|
||||
from .wan_vace_extend import NODE_CLASS_MAPPINGS as EXTEND_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as EXTEND_DISPLAY
|
||||
from .load_videos_simple import NODE_CLASS_MAPPINGS as LOAD_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as LOAD_DISPLAY
|
||||
from .wan_vace_batch_context import NODE_CLASS_MAPPINGS as CONTEXT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CONTEXT_DISPLAY
|
||||
|
||||
NODE_CLASS_MAPPINGS = {**PREP_MAPPINGS, **EXTEND_MAPPINGS, **LOAD_MAPPINGS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**PREP_DISPLAY, **EXTEND_DISPLAY, **LOAD_DISPLAY}
|
||||
NODE_CLASS_MAPPINGS = {**PREP_MAPPINGS, **EXTEND_MAPPINGS, **LOAD_MAPPINGS, **PREP_BATCH_MAPPINGS, **CONTEXT_MAPPINGS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**PREP_DISPLAY, **EXTEND_DISPLAY, **LOAD_DISPLAY, **PREP_BATCH_DISPLAY, **CONTEXT_DISPLAY}
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
+16
-7
@@ -10,7 +10,11 @@ class LoadVideosFromFolderSimple:
|
||||
return {
|
||||
"required": {
|
||||
"folder_path": ("STRING", {"default": ""}),
|
||||
},
|
||||
"debug": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Log some details to the console"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ["IMAGE"]
|
||||
@@ -25,7 +29,7 @@ class LoadVideosFromFolderSimple:
|
||||
- All videos must have identical resolution
|
||||
"""
|
||||
|
||||
def load_videos(self, folder_path):
|
||||
def load_videos(self, folder_path, debug):
|
||||
folder_path = folder_path.strip().strip('"').strip("'")
|
||||
|
||||
if not os.path.isdir(folder_path):
|
||||
@@ -39,12 +43,14 @@ class LoadVideosFromFolderSimple:
|
||||
f"Supported formats: {', '.join(self.VIDEO_EXTENSIONS)}"
|
||||
)
|
||||
|
||||
print(f"Loading {len(video_files)} videos from {folder_path}")
|
||||
if debug:
|
||||
print(f"Loading {len(video_files)} videos from {folder_path}")
|
||||
all_frames = []
|
||||
expected_shape = None
|
||||
|
||||
for idx, video_path in enumerate(video_files):
|
||||
print(f"[{idx+1}/{len(video_files)}]: {os.path.basename(video_path)}", end=" ... ")
|
||||
if debug:
|
||||
print(f"[{idx+1}/{len(video_files)}]: {os.path.basename(video_path)}", end=" ... ")
|
||||
|
||||
frames = self._load_video_frames(video_path)
|
||||
|
||||
@@ -60,13 +66,16 @@ class LoadVideosFromFolderSimple:
|
||||
)
|
||||
|
||||
all_frames.append(frames)
|
||||
print(f"{frames.shape[0]} frames")
|
||||
if debug:
|
||||
print(f"{frames.shape[0]} frames")
|
||||
|
||||
# Concatenate all frames
|
||||
print(f"\nConcatenating {len(video_files)} videos...")
|
||||
if debug:
|
||||
print(f"\nConcatenating {len(video_files)} videos...")
|
||||
output = torch.cat(all_frames, dim=0)
|
||||
|
||||
print(f"Done!\n")
|
||||
if debug:
|
||||
print(f"Done\n")
|
||||
return (output,)
|
||||
|
||||
def _get_video_files(self, folder_path):
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-wan-vace-prep"
|
||||
description = "ComfyUI nodes for smooth video transitions and extensions using Wan VACE."
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
license = { text = "MIT" }
|
||||
|
||||
dependencies = []
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
class WanVACEBatchContext:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"input_list": ("STRING", {"forceInput": True}),
|
||||
"input_dir": ("STRING", {
|
||||
"default": "",
|
||||
"tooltip": "Directory containing input videos"
|
||||
}),
|
||||
"project_name": ("STRING", {
|
||||
"default": ".",
|
||||
"tooltip": "Project name - workflow files will be created under ComfyUI/output/project_name. Use period (.) for no project name."
|
||||
}),
|
||||
"index": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"tooltip": "Current iteration index (0 based)"
|
||||
}),
|
||||
"debug": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Log some details to the console"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "BOOLEAN", "BOOLEAN")
|
||||
RETURN_NAMES = ("work_dir", "workfile_prefix", "video_1_filename", "video_2_filename", "is_first", "is_last")
|
||||
FUNCTION = "setup_context"
|
||||
CATEGORY = "video/VACE"
|
||||
DESCRIPTION = "Establishes iteration context for batch video processing."
|
||||
INPUT_IS_LIST = True
|
||||
|
||||
def setup_context(self, **kwargs):
|
||||
input_dir = kwargs.get('input_dir', [""])[0]
|
||||
input_list = kwargs.get('input_list', [])
|
||||
project_name = kwargs.get('project_name', [""])[0]
|
||||
index = kwargs.get('index', [0])[0]
|
||||
debug = kwargs.get('debug', [False])[0]
|
||||
|
||||
# Validate input list
|
||||
list_length = len(input_list)
|
||||
|
||||
if list_length < 2:
|
||||
raise ValueError(
|
||||
f"Need at least 2 videos to create transitions, found {list_length}"
|
||||
)
|
||||
|
||||
# Validate index bounds
|
||||
max_index = list_length - 2
|
||||
if index < 0 or index > max_index:
|
||||
raise ValueError(
|
||||
f"Index {index} out of range (valid: 0-{max_index} for {list_length} videos)"
|
||||
)
|
||||
|
||||
# Construct paths
|
||||
work_dir = f"{project_name}/vace-work"
|
||||
padded_index = f"{index:03d}"
|
||||
workfile_prefix = f"{work_dir}/index{padded_index}"
|
||||
|
||||
# Set iteration flags
|
||||
is_first = (index == 0)
|
||||
is_last = (index == max_index)
|
||||
|
||||
# Extract filenames
|
||||
video_1_filename = f"{input_dir}/{input_list[index]}"
|
||||
video_2_filename = f"{input_dir}/{input_list[index + 1]}"
|
||||
|
||||
if debug:
|
||||
print(f"\n=== VACE Batch Context ===")
|
||||
print(f"Index: {index} (videos {index+1}-{index+2} of {list_length})")
|
||||
print(f"{'[FIRST]' if is_first else ''} {'[LAST]' if is_last else ''}")
|
||||
print(f"Input directory: {input_dir}")
|
||||
print(f"Video 1: {input_list[index]}")
|
||||
print(f"Video 2: {input_list[index + 1]}")
|
||||
print(f"Work prefix: {workfile_prefix}")
|
||||
print(f"=========================\n")
|
||||
|
||||
return (work_dir, workfile_prefix, video_1_filename, video_2_filename, is_first, is_last)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"WanVACEBatchContext": WanVACEBatchContext
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"WanVACEBatchContext": "Wan VACE Batch Context"
|
||||
}
|
||||
+4
-15
@@ -28,10 +28,6 @@ class WanVACEPrep:
|
||||
"step": 4,
|
||||
"tooltip": "Number of new transition frames to generate, in addition to the replace_frames (multiple of 4)."
|
||||
}),
|
||||
"crossfade_mode": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Include context frames in start_images and end_images for cross-fade or other use cases."
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +37,7 @@ class WanVACEPrep:
|
||||
CATEGORY = "video/VACE"
|
||||
DESCRIPTION = "Generates VACE control video and mask for smooth transitions between two videos using context frames and frame replacement."
|
||||
|
||||
def vace_prep(self, video_1, video_2, context_frames, replace_frames, add_frames, crossfade_mode):
|
||||
def vace_prep(self, video_1, video_2, context_frames, replace_frames, add_frames):
|
||||
height = int(video_1.shape[1])
|
||||
width = int(video_1.shape[2])
|
||||
|
||||
@@ -95,15 +91,8 @@ class WanVACEPrep:
|
||||
mask = torch.zeros((total_frames, height, width), dtype=torch.float32, device=video_1.device)
|
||||
mask[context_frames:context_frames + vace_count] = 1.0
|
||||
|
||||
if crossfade_mode:
|
||||
if replace_frames > 0:
|
||||
start_images = video_1[:-replace_frames]
|
||||
else:
|
||||
start_images = video_1
|
||||
end_images = video_2[replace_frames:]
|
||||
else:
|
||||
start_images = video_1[:-(context_frames + replace_frames)]
|
||||
end_images = video_2[context_frames + replace_frames:]
|
||||
start_images = video_1[:-(context_frames + replace_frames)]
|
||||
end_images = video_2[context_frames + replace_frames:]
|
||||
|
||||
length = int(control_video.shape[0])
|
||||
|
||||
@@ -116,4 +105,4 @@ NODE_CLASS_MAPPINGS = {
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"WanVACEPrep": "Wan VACE Prep"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import torch
|
||||
|
||||
class WanVACEPrepBatch:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"video_1": ("IMAGE",),
|
||||
"video_2": ("IMAGE",),
|
||||
"is_first": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "True for first iteration (index=0) - includes full beginning of video_1 in start_images."
|
||||
}),
|
||||
"is_last": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "True for last iteration - includes full ending of video_2 in end_images."
|
||||
}),
|
||||
"context_frames": ("INT", {
|
||||
"default": 8,
|
||||
"min": 4,
|
||||
"max": 120,
|
||||
"step": 4,
|
||||
"tooltip": "Reference frames from each video edge for VACE interpolation (multiple of 4)."
|
||||
}),
|
||||
"replace_frames": ("INT", {
|
||||
"default": 8,
|
||||
"min": 0,
|
||||
"max": 120,
|
||||
"step": 4,
|
||||
"tooltip": "Number of frames to regenerate at each transition edge (multiple of 4)."
|
||||
}),
|
||||
"new_frames": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 240,
|
||||
"step": 4,
|
||||
"tooltip": "Number of new transition frames to generate, in addition to the replace_frames (multiple of 4)."
|
||||
}),
|
||||
"debug": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Log details to the console"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT", "INT", "IMAGE", "IMAGE")
|
||||
RETURN_NAMES = ("control_video", "control_mask", "width", "height", "length", "start_images", "end_images")
|
||||
FUNCTION = "vace_prep_batch"
|
||||
CATEGORY = "video/VACE"
|
||||
DESCRIPTION = "Batch-aware VACE prep that handles first/last iteration edge cases for multi-video processing."
|
||||
|
||||
def vace_prep_batch(self, video_1, video_2, context_frames, replace_frames, new_frames, is_first, is_last, debug=False):
|
||||
height = int(video_1.shape[1])
|
||||
width = int(video_1.shape[2])
|
||||
|
||||
if debug:
|
||||
print(f"\n=== Wan VACE Prep Batch ===")
|
||||
print(f"Video 1: {video_1.shape[0]} frames @ {width}x{height}")
|
||||
print(f"Video 2: {video_2.shape[0]} frames @ {width}x{height}")
|
||||
print(f"Flags: {'FIRST ' if is_first else ''}{'LAST' if is_last else 'MIDDLE' if not is_first else ''}")
|
||||
print(f"Parameters: context={context_frames}, replace={replace_frames}, new={new_frames}")
|
||||
|
||||
if video_2.shape[1] != height or video_2.shape[2] != width:
|
||||
raise ValueError(
|
||||
f"Video dimensions must match. "
|
||||
f"video_1 is {width}x{height}, video_2 is {int(video_2.shape[2])}x{int(video_2.shape[1])}"
|
||||
)
|
||||
|
||||
if width % 16 != 0 or height % 16 != 0:
|
||||
raise ValueError(
|
||||
f"Video dimensions must be divisible by 16. "
|
||||
f"Current dimensions: {width}x{height}"
|
||||
)
|
||||
|
||||
v1_len = video_1.shape[0]
|
||||
v2_len = video_2.shape[0]
|
||||
required_frames = context_frames + replace_frames
|
||||
|
||||
# Validation for video_1
|
||||
if is_first:
|
||||
if v1_len < required_frames:
|
||||
raise ValueError(
|
||||
f"First iteration: video_1 needs at least {required_frames} frames "
|
||||
f"(context_frames + replace_frames), but has {v1_len}"
|
||||
)
|
||||
else:
|
||||
if v1_len < required_frames * 2:
|
||||
raise ValueError(
|
||||
f"Middle/last iteration: video_1 needs at least {required_frames * 2} frames, "
|
||||
f"but has {v1_len}"
|
||||
)
|
||||
|
||||
# Validation for video_2
|
||||
if v2_len < required_frames:
|
||||
raise ValueError(
|
||||
f"video_2 needs at least {required_frames} frames "
|
||||
f"(context_frames + replace_frames), but has {v2_len}"
|
||||
)
|
||||
|
||||
# Extract context frames for control video/mask
|
||||
if replace_frames > 0:
|
||||
v1_context = video_1[-(context_frames + replace_frames):-replace_frames]
|
||||
v2_context = video_2[replace_frames:context_frames + replace_frames]
|
||||
else:
|
||||
v1_context = video_1[-context_frames:]
|
||||
v2_context = video_2[:context_frames]
|
||||
|
||||
channels = video_1.shape[3]
|
||||
|
||||
# Generate VACE frames placeholder (4n+1 frames)
|
||||
vace_count = (replace_frames * 2) + new_frames + 1
|
||||
vace_frames = torch.full((vace_count, height, width, channels), 0.5, dtype=video_1.dtype, device=video_1.device)
|
||||
|
||||
control_video = torch.cat([v1_context, vace_frames, v2_context], dim=0)
|
||||
|
||||
total_frames = (context_frames * 2) + vace_count
|
||||
mask = torch.zeros((total_frames, height, width), dtype=torch.float32, device=video_1.device)
|
||||
mask[context_frames:context_frames + vace_count] = 1.0
|
||||
|
||||
# Extract start_images (frames to save from video_1)
|
||||
if is_first:
|
||||
# Keep everything except last (context + replace)
|
||||
start_images = video_1[:-required_frames]
|
||||
else:
|
||||
# Skip first (context + replace), keep rest except last (context + replace)
|
||||
start_images = video_1[required_frames:-required_frames]
|
||||
|
||||
# Extract end_images (frames to save from video_2, only on last iteration)
|
||||
if is_last:
|
||||
end_images = video_2[required_frames:]
|
||||
else:
|
||||
# Return empty batch for non-last iterations
|
||||
end_images = torch.empty((0, height, width, channels), dtype=video_2.dtype, device=video_2.device)
|
||||
|
||||
length = int(control_video.shape[0])
|
||||
|
||||
if debug:
|
||||
print(f"\nOutputs:")
|
||||
print(f" control_video: {control_video.shape}")
|
||||
print(f" control_mask: {mask.shape}")
|
||||
print(f" start_images: {start_images.shape[0]} frames")
|
||||
print(f" end_images: {end_images.shape[0]} frames")
|
||||
print(f" VACE output: {total_frames} frames ({context_frames * 2} context + {vace_count} generated")
|
||||
print(f"===========================\n")
|
||||
|
||||
return (control_video, mask, width, height, length, start_images, end_images)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"WanVACEPrepBatch": WanVACEPrepBatch
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"WanVACEPrepBatch": "Wan VACE Prep Batch"
|
||||
}
|
||||
Reference in New Issue
Block a user