commit a879dfd29efbbf8fb4ff1129abffa078d98b09a5 Author: stuttlepress <178523610+stuttlepress@users.noreply.github.com> Date: Sat Jan 3 17:55:53 2026 -0600 intitial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a508aa1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ +*.egg + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# OS +Thumbs.db +.DS_Store + +# ComfyUI specific +*.safetensors +*.ckpt +*.pth +*.pt +models/ +input/ +output/ +temp/ + +# Logs +*.log + +# Test files +test_*.py +tests/ + +TODO.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b77bf2a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..20aabc8 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Wan VACE Prep + +A ComfyUI node for smooth video transitions with Wan VACE - generates control video and masks + +## What It Does + +Takes two video clips and prepares them for smooth VACE-based transition +- Extracts context frames from both clips +- Builds VACE control video and mask +- Output video segments for final assembly +- Optionally include context frames in output segments for crossfade or other artifact-reduction processing + +## Installation +Clone this repository into your custom_nodes directory. + +```bash +cd /path/to/comfyui/custom_nodes +git clone https://github.com/stuttlepress/ComfyUI-Wan-VACE-Prep +``` + +Restart ComfyUI. + +## Usage + +The node appears as **"Wan VACE Prep"** in the `video/VACE` category. + +![Wan VACE Prep Node](assets/comfyui-wan-vace-prep.png) + +## Parameters + +### context_frames +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 +Number of frames at each transition edge to discard and regenerate. These create the actual transition blend zone. Must be a multiple of 4. + +### add_frames +Number of completely new frames to generate between the two clips, extending the transition duration. Must be a multiple of 4. + +Note: Wan likes to generate 4n+1 frames at a time. If you ask it for some other amount, it will silently round your request down to the nearest 4n+1. For this reason, parameters are restricted to multiples of 4, and the node adds +1 to the number of generated frames. + +### crossfade_mode +- **False (default)**: Clean output - start_images and end_images exclude context frames. Use this for simple concatenation. +- **True**: Cross-fade mode - includes context frames in start_images and end_images so they can be blended with the context frames included in the VACE generated clip. This can help mitigate color or brightness artifacts at the transition. + +## License + +MIT License - feel free to use, modify, and distribute. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cc2a14b --- /dev/null +++ b/__init__.py @@ -0,0 +1,7 @@ +""" +Wan VACE Prep - Custom node for preparing videos for Wan VACE generation +""" + +from .wan_vace_prep import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/assets/comfyui-wan-vace-prep.png b/assets/comfyui-wan-vace-prep.png new file mode 100644 index 0000000..6c629b8 Binary files /dev/null and b/assets/comfyui-wan-vace-prep.png differ diff --git a/wan_vace_prep.py b/wan_vace_prep.py new file mode 100644 index 0000000..68465b6 --- /dev/null +++ b/wan_vace_prep.py @@ -0,0 +1,107 @@ +import torch + +class WanVACEPrep: + """Generates VACE control video and mask for smooth transitions between two videos using context frames and frame replacement.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "video_1": ("IMAGE",), + "video_2": ("IMAGE",), + "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)." + }), + "add_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)." + }), + "crossfade_mode": ("BOOLEAN", { + "default": False, + "tooltip": "Include context frames in start_images and end_images for cross-fade or other use cases." + }), + } + } + + 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" + CATEGORY = "video/VACE" + + def vace_prep(self, video_1, video_2, context_frames, replace_frames, add_frames, crossfade_mode): + v1_len = video_1.shape[0] + v2_len = video_2.shape[0] + required_frames = context_frames + replace_frames + + if v1_len < required_frames: + raise ValueError( + f"context_frames ({context_frames}) + replace_frames ({replace_frames}) = {required_frames}, " + f"which is too large for video_1 length of {v1_len}. " + f"Reduce context_frames or replace_frames." + ) + + if v2_len < required_frames: + raise ValueError( + f"context_frames ({context_frames}) + replace_frames ({replace_frames}) = {required_frames}, " + f"which is too large for video_2 length of {v2_len}. " + f"Reduce context_frames or replace_frames." + ) + + 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] + + height = int(video_1.shape[1]) + width = int(video_1.shape[2]) + channels = video_1.shape[3] + + # Wan wants to generate 4n+1 frames. If we don't provide that, + # it will quietly round down to the nearest 4n+1. So we add 1 here. + vace_count = replace_frames * 2 + add_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 + + 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:] + + length = int(control_video.shape[0]) + + return (control_video, mask, width, height, length, start_images, end_images) + + +NODE_CLASS_MAPPINGS = { + "WanVACEPrep": WanVACEPrep +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "WanVACEPrep": "Wan VACE Prep" +}