From e75b5f54bf64b8544ee99a7dc5b54b691ffb79c3 Mon Sep 17 00:00:00 2001 From: samseen Date: Sat, 24 May 2025 09:58:24 +0530 Subject: [PATCH] first commit --- README.md | 125 +++++++++++++ __init__.py | 14 ++ requirements.txt | 5 + ss_before_after_video.py | 275 ++++++++++++++++++++++++++++ ss_before_after_video_with_depth.py | 192 +++++++++++++++++++ 5 files changed, 611 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 requirements.txt create mode 100644 ss_before_after_video.py create mode 100644 ss_before_after_video_with_depth.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaf58b9 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +[![SamSeen Logo](https://img.shields.io/badge/ComfyUI SS_Before_After_Transition-by_SamSeen_Solutions-orange?style=for-the-badge)](https://github.com/MrSamSeen) +![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) + +# ComfyUI SS Before After Node + +This repository provides two powerful custom nodes for ComfyUI to create stunning before-and-after transition videos. These nodes are designed for visual comparisons, transformations, and creative effects, supporting both standard and depth map-based transitions. + +## Features + +- **SSBeforeAndAfterVideo**: Create videos with fade and wipe transitions between two images. +- **SSBeforeAndAfterVideoWithDepthMap**: Create depth-aware transitions using a depth map for more immersive effects. +- High-quality output with customizable resolution, frame rate, feathering, and looping. +- Automatic handling of output directories and filenames. +- Progress bar for frame generation. +- Robust ffmpeg integration for video encoding. + +--- + +## Node 1: SSBeforeAndAfterVideo + +### Description +Creates before-and-after transition videos using classic fade and wipe effects. Ideal for showcasing transformations, comparisons, or visual storytelling. + +### Supported Transitions +- **Fade**: Smoothly blends from the before image to the after image. +- **Wipe**: Animated reveal from left, right, top, or bottom, with optional feathered (blurred) edge. + +### Parameters +- **before_image**: The starting image (tensor). +- **after_image**: The ending image (tensor). +- **transition_type**: Choose from `fade`, `wipe_from_left`, `wipe_from_top`, `wipe_from_right`, `wipe_from_bottom`. +- **fps**: Frames per second (1-120). +- **width/height**: Output video resolution (64-4096 px). +- **output_path**: Output directory (default `/output/`). +- **filename_prefix**: Prefix for output video files. +- **transition_duration**: Duration of the transition (0.5-10.0 seconds). +- **hold_duration**: Time to hold before and after images (0.0-5.0 seconds). +- **feather**: Feathering amount for wipe transitions (0.0-1.0). +- **loop_transition**: If enabled, the video will loop back to the start. + +### How It Works +1. **Resizes** both images to the specified resolution. +2. **Generates frames** for the hold, transition, and (optionally) loop-back phases. +3. **Applies the selected transition** (fade or wipe) with feathering. +4. **Saves frames** to a temporary directory. +5. **Encodes video** using ffmpeg. +6. **Cleans up** temporary files and returns the video path and a preview frame. + +--- + +## Node 2: SSBeforeAndAfterVideoWithDepthMap + +### Description +Creates before-and-after transition videos using a depth map to control the transition. This enables advanced effects like revealing the after image from back-to-front, front-to-back, or from the middle outwards, based on scene depth. + +### Supported Transitions +- **back_to_front**: Reveals the after image starting from the farthest (background) regions. +- **front_to_back**: Reveals the after image starting from the closest (foreground) regions. +- **middle_out**: Reveals the after image from the center depth outward. + +### Parameters +- **before_image**: The starting image (tensor). +- **after_image**: The ending image (tensor). +- **depth_map**: Depth map image (tensor, same size as input images). +- **transition_type**: Choose from `back_to_front`, `front_to_back`, `middle_out`. +- **fps**: Frames per second (1-120). +- **width/height**: Output video resolution (64-4096 px). +- **output_path**: Output directory (default `/output/`). +- **filename_prefix**: Prefix for output video files. +- **transition_duration**: Duration of the transition (0.5-10.0 seconds). +- **hold_duration**: Time to hold before and after images (0.0-5.0 seconds). +- **feather**: Feathering amount for the depth mask (0.0-1.0). +- **loop_transition**: If enabled, the video will loop back to the start. +- **easing_method**: Transition easing ("none", "ease-in", "ease-out", "both"). + +### How It Works +1. **Resizes** all images to the specified resolution. +2. **Normalizes the depth map** and creates a mask based on the selected transition type and progress. +3. **Applies feathering** to the mask for smooth transitions. +4. **Blends images** according to the mask for each frame. +5. **Handles looping** and hold frames as configured. +6. **Encodes video** using ffmpeg and cleans up temporary files. + +--- + +## Installation + +1. Place this folder in your `ComfyUI/custom_nodes` directory. +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` +3. Ensure ffmpeg is installed and available in your system PATH. + +--- + +## Usage + +1. Restart ComfyUI after installing the node. +2. Add the desired node (`SSBeforeAndAfterVideo` or `SSBeforeAndAfterVideoWithDepthMap`) to your workflow. +3. Configure the parameters as needed. +4. Run the workflow to generate your before-and-after transition video. +5. The output video and a preview frame will be saved to your specified output directory. + +--- + +## Troubleshooting +- **ffmpeg not found**: Make sure ffmpeg is installed and accessible from your system PATH. +- **Missing dependencies**: Install all required Python packages listed in `requirements.txt`. +- **Output not appearing**: Check the output directory path and permissions. + +--- + +## License +MIT License + +--- + +## Credits +Developed by SamSeen. Inspired by creative visual storytelling needs in ComfyUI. + +--- + +## Contribution +Pull requests and suggestions are welcome! \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c9b4087 --- /dev/null +++ b/__init__.py @@ -0,0 +1,14 @@ +print("Initializing SS Before and After node for transition view creation...") + +from .ss_before_after_video import SSBeforeAndAfterVideo +from .ss_before_after_video_with_depth import SSBeforeAndAfterVideoWithDepthMap + +NODE_CLASS_MAPPINGS = { + "SSBeforeAndAfterVideo": SSBeforeAndAfterVideo, + "SSBeforeAndAfterVideoWithDepthMap": SSBeforeAndAfterVideoWithDepthMap +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SSBeforeAndAfterVideo": "SS Before After Video", + "SSBeforeAndAfterVideoWithDepthMap": "SS Before After Video (Depth Map)" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4ae064b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +torch +numpy +Pillow +opencv-python +imageio \ No newline at end of file diff --git a/ss_before_after_video.py b/ss_before_after_video.py new file mode 100644 index 0000000..1df043f --- /dev/null +++ b/ss_before_after_video.py @@ -0,0 +1,275 @@ +# Robust imports with fallbacks for standalone or missing dependencies +try: + import torch +except ImportError: + raise ImportError('Please install torch to use this module.') +try: + import numpy as np +except ImportError: + raise ImportError('Please install numpy to use this module.') +try: + from PIL import Image +except ImportError: + raise ImportError('Please install pillow to use this module.') +import cv2 +try: + from comfy.utils import ProgressBar +except ImportError: + raise ImportError('Please ensure comfy.utils is available in your Python path.') +try: + import folder_paths +except ImportError: + raise ImportError('Please ensure folder_paths is available in your Python path.') +try: + import imageio +except ImportError: + raise ImportError('Please install imageio to use this module.') +import os +import subprocess +import time +# Check for ffmpeg availability +def get_ffmpeg_path(): + """Get the path to ffmpeg executable""" + # Try to find ffmpeg in the system path + try: + if os.name == 'nt': # Windows + ffmpeg_command = "where ffmpeg" + else: # Unix/Linux/Mac + ffmpeg_command = "which ffmpeg" + + result = subprocess.run(ffmpeg_command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except: + pass + + # Check if ffmpeg is in the current directory + current_dir = os.path.dirname(os.path.abspath(__file__)) + if os.name == 'nt': # Windows + ffmpeg_path = os.path.join(current_dir, "ffmpeg.exe") + else: # Unix/Linux/Mac + ffmpeg_path = os.path.join(current_dir, "ffmpeg") + + if os.path.exists(ffmpeg_path): + return ffmpeg_path + + # Try to use imageio's ffmpeg + try: + import imageio + return imageio.plugins.ffmpeg.get_exe() + except ImportError: + raise ImportError('Please install imageio to use this module.') + except: + pass + + return None + +ffmpeg_path = get_ffmpeg_path() + +class SSBeforeAndAfterVideo: + def __init__(self): + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "before_image": ("IMAGE",), + "after_image": ("IMAGE",), + "transition_type": (["fade", "wipe_from_left", "wipe_from_top", "wipe_from_right", "wipe_from_bottom"], {"default": "fade"}), + "fps": ("INT", {"default": 30, "min": 1, "max": 120, "step": 1}), + "width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}), + "height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}), + "output_path": ("STRING", {"default": "/output/"}), + "filename_prefix": ("STRING", {"default": "before_after_transition"}), + "transition_duration": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 10.0, "step": 0.1}), + "hold_duration": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.1}), + "feather": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}), + "loop_transition": ("BOOLEAN", {"default": False}), + "loop_transition": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES = ("STRING", "IMAGE") + RETURN_NAMES = ("video_path", "preview_frame") + OUTPUT_NODE = True + FUNCTION = "create_transition_video" + CATEGORY = "👀 SamSeen" + DESCRIPTION = "Create stunning before and after transition videos with various wipe and fade effects. Perfect for showcasing transformations and comparisons!" + + def tensor_to_pil(self, tensor): + """Convert tensor to PIL Image""" + if tensor.dim() == 4: + tensor = tensor.squeeze(0) + + # Convert from tensor to numpy + np_image = tensor.cpu().numpy() + + # Scale to 0-255 and convert to uint8 + np_image = (np_image * 255).astype(np.uint8) + + return Image.fromarray(np_image) + + def pil_to_tensor(self, pil_image): + """Convert PIL Image to tensor""" + np_image = np.array(pil_image).astype(np.float32) / 255.0 + return torch.from_numpy(np_image).unsqueeze(0) + + def resize_images(self, before_img, after_img, width, height): + """Resize both images to the specified dimensions""" + before_pil = self.tensor_to_pil(before_img) + after_pil = self.tensor_to_pil(after_img) + + before_resized = before_pil.resize((width, height), Image.LANCZOS) + after_resized = after_pil.resize((width, height), Image.LANCZOS) + + return before_resized, after_resized + + def create_fade_transition(self, before_img, after_img, progress): + """Create a fade transition between two images""" + # Convert PIL images to numpy arrays + before_np = np.array(before_img, dtype=np.float32) + after_np = np.array(after_img, dtype=np.float32) + + # Blend the images based on progress + blended = before_np * (1 - progress) + after_np * progress + + return Image.fromarray(blended.astype(np.uint8)) + + def create_wipe_transition(self, before_img, after_img, progress, direction, feather): + """Create a wipe transition between two images with feathering (blurred edge)""" + width, height = before_img.size + mask = np.zeros((height, width), dtype=np.float32) + feather_pixels_x = max(1, int(width * feather)) + feather_pixels_y = max(1, int(height * feather)) + + if direction == "wipe_from_left": + wipe_pos = int(progress * width) + left = max(0, wipe_pos - feather_pixels_x // 2) + right = min(width, wipe_pos + feather_pixels_x // 2) + mask[:, :left] = 1.0 + mask[:, right:] = 0.0 + if right > left: + feather = np.linspace(1.0, 0.0, right - left) + mask[:, left:right] = feather + elif direction == "wipe_from_top": + wipe_pos = int(progress * height) + top = max(0, wipe_pos - feather_pixels_y // 2) + bottom = min(height, wipe_pos + feather_pixels_y // 2) + mask[:top, :] = 1.0 + mask[bottom:, :] = 0.0 + if bottom > top: + feather = np.linspace(1.0, 0.0, bottom - top) + mask[top:bottom, :] = feather[:, None] + elif direction == "wipe_from_right": + wipe_pos = int((1.0 - progress) * width) + left = max(0, wipe_pos - feather_pixels_x // 2) + right = min(width, wipe_pos + feather_pixels_x // 2) + mask[:, :left] = 0.0 + mask[:, right:] = 1.0 + if right > left: + feather = np.linspace(0.0, 1.0, right - left) + mask[:, left:right] = feather + elif direction == "wipe_from_bottom": + wipe_pos = int((1.0 - progress) * height) + top = max(0, wipe_pos - feather_pixels_y // 2) + bottom = min(height, wipe_pos + feather_pixels_y // 2) + mask[:top, :] = 0.0 + mask[bottom:, :] = 1.0 + if bottom > top: + feather = np.linspace(0.0, 1.0, bottom - top) + mask[top:bottom, :] = feather[:, None] + elif direction == "wipe_from_bottom": + wipe_pos = int(height * (1 - progress)) + top = max(0, wipe_pos - feather_pixels_y // 2) + bottom = min(height, wipe_pos + feather_pixels_y // 2) + mask[:top, :] = 0.0 + mask[bottom:, :] = 1.0 + if bottom > top: + alpha = np.linspace(1, 0, bottom - top) + mask[top:bottom, :] = alpha[:, None] + + # Blur the mask for a feathered (soft) edge + blur_size = max(1, int(max(feather_pixels_x, feather_pixels_y) // 2) * 2 + 1) + if blur_size > 1: + mask = cv2.GaussianBlur(mask, (blur_size, blur_size), 0) + mask = np.clip(mask, 0, 1) + + before_np = np.array(before_img, dtype=np.float32) + after_np = np.array(after_img, dtype=np.float32) + mask_3d = np.stack([mask] * 3, axis=2) + result = before_np * (1 - mask_3d) + after_np * mask_3d + return Image.fromarray(result.astype(np.uint8)) + + def generate_transition_frames(self, before_img, after_img, transition_type, fps, transition_duration, hold_duration, feather, loop_transition): + frames = [] + total_transition_frames = int(fps * transition_duration) + total_hold_frames = int(fps * hold_duration) + # Hold the before image + for _ in range(total_hold_frames): + frames.append(before_img.copy()) + # Transition frames + for i in range(total_transition_frames): + progress = i / max(1, total_transition_frames - 1) + if transition_type == "fade": + frame = self.create_fade_transition(before_img, after_img, progress) + elif transition_type.startswith("wipe"): + frame = self.create_wipe_transition(before_img, after_img, progress, transition_type, feather) + else: + frame = before_img.copy() + frames.append(frame) + # Hold the after image + for _ in range(total_hold_frames): + frames.append(after_img.copy()) + # Optionally loop back + if loop_transition: + for i in range(total_transition_frames): + progress = 1 - (i / max(1, total_transition_frames - 1)) + if transition_type == "fade": + frame = self.create_fade_transition(before_img, after_img, progress) + elif transition_type.startswith("wipe"): + frame = self.create_wipe_transition(before_img, after_img, progress, transition_type, feather) + else: + frame = after_img.copy() + frames.append(frame) + for _ in range(total_hold_frames): + frames.append(before_img.copy()) + return frames + + def create_transition_video(self, before_image, after_image, transition_type, fps, width, height, output_path, filename_prefix, transition_duration, hold_duration, feather, loop_transition=False): + if ffmpeg_path is None: + raise RuntimeError("ffmpeg not found. Please install ffmpeg to use video functionality.") + before_img, after_img = self.resize_images(before_image, after_image, width, height) + # Use the correct frame generation method for transitions + frames = self.generate_transition_frames( + before_img, after_img, transition_type, fps, transition_duration, hold_duration, feather, loop_transition + ) + # Prepare output directory + if output_path.startswith("/output/"): + output_dir = folder_paths.get_output_directory() + relative_path = output_path[8:] + if relative_path: + output_dir = os.path.join(output_dir, relative_path) + else: + output_dir = output_path + os.makedirs(output_dir, exist_ok=True) + # Generate unique filename + counter = 1 + while True: + filename = f"{filename_prefix}_{counter:05d}.mp4" + video_path = os.path.join(output_dir, filename) + if not os.path.exists(video_path): + break + counter += 1 + temp_dir = os.path.join(output_dir, "temp_depth_frames") + os.makedirs(temp_dir, exist_ok=True) + for idx, frame in enumerate(frames): + frame.save(os.path.join(temp_dir, f"frame_{idx:04d}.png")) + ffmpeg_cmd = [ffmpeg_path, "-y", "-framerate", str(fps), "-i", os.path.join(temp_dir, "frame_%04d.png"), "-c:v", "libx264", "-pix_fmt", "yuv420p", video_path] + subprocess.run(ffmpeg_cmd, check=True) + for f in os.listdir(temp_dir): + os.remove(os.path.join(temp_dir, f)) + os.rmdir(temp_dir) + preview_frame = frames[len(frames) // 2] + preview_tensor = torch.from_numpy(np.array(preview_frame).astype(np.float32) / 255.0).unsqueeze(0) + return (video_path, preview_tensor) diff --git a/ss_before_after_video_with_depth.py b/ss_before_after_video_with_depth.py new file mode 100644 index 0000000..695a669 --- /dev/null +++ b/ss_before_after_video_with_depth.py @@ -0,0 +1,192 @@ +# Robust imports with fallbacks for standalone or missing dependencies + +import torch +import numpy as np +from PIL import Image +import cv2 +from comfy.utils import ProgressBar +import os +import subprocess +try: + import folder_paths +except ImportError: + class FolderPaths: + def get_input_directory(self): + return "input" + def get_output_directory(self): + return "output" + def get_temp_directory(self): + return "temp" + folder_paths = FolderPaths() +# Check for ffmpeg availability +def get_ffmpeg_path(): + """Get the path to ffmpeg executable""" + # Try to find ffmpeg in the system path + try: + if os.name == 'nt': # Windows + ffmpeg_command = "where ffmpeg" + else: # Unix/Linux/Mac + ffmpeg_command = "which ffmpeg" + + result = subprocess.run(ffmpeg_command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except: + pass + + # Check if ffmpeg is in the current directory + current_dir = os.path.dirname(os.path.abspath(__file__)) + if os.name == 'nt': # Windows + ffmpeg_path = os.path.join(current_dir, "ffmpeg.exe") + else: # Unix/Linux/Mac + ffmpeg_path = os.path.join(current_dir, "ffmpeg") + + if os.path.exists(ffmpeg_path): + return ffmpeg_path + + # Try to use imageio's ffmpeg + try: + import imageio + return imageio.plugins.ffmpeg.get_exe() + except: + pass + + return None + +ffmpeg_path = get_ffmpeg_path() + +class SSBeforeAndAfterVideoWithDepthMap: + def __init__(self): + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "before_image": ("IMAGE",), + "after_image": ("IMAGE",), + "depth_map": ("IMAGE",), + "transition_type": (["back_to_front", "front_to_back", "middle_out"], {"default": "back_to_front"}), + "fps": ("INT", {"default": 30, "min": 1, "max": 120, "step": 1}), + "width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}), + "height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}), + "output_path": ("STRING", {"default": "/output/"}), + "filename_prefix": ("STRING", {"default": "before_after_depth_transition"}), + "transition_duration": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 10.0, "step": 0.1}), + "hold_duration": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.1}), + "feather": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}), + "loop_transition": ("BOOLEAN", {"default": False}), + "easing_method": (["none", "ease-in", "ease-out", "both"], {"default": "none"}), + } + } + + RETURN_TYPES = ("STRING", "IMAGE") + RETURN_NAMES = ("video_path", "preview_frame") + OUTPUT_NODE = True + FUNCTION = "create_transition_video" + CATEGORY = "👀 SamSeen" + DESCRIPTION = "Create before and after transition videos using a depth map for depth-based transitions." + + def tensor_to_pil(self, tensor): + if tensor.dim() == 4: + tensor = tensor.squeeze(0) + np_image = tensor.cpu().numpy() + np_image = (np_image * 255).astype(np.uint8) + return Image.fromarray(np_image) + + def resize_images(self, before_img, after_img, depth_img, width, height): + before_pil = self.tensor_to_pil(before_img) + after_pil = self.tensor_to_pil(after_img) + depth_pil = self.tensor_to_pil(depth_img) + before_resized = before_pil.resize((width, height), Image.LANCZOS) + after_resized = after_pil.resize((width, height), Image.LANCZOS) + depth_resized = depth_pil.resize((width, height), Image.LANCZOS) + return before_resized, after_resized, depth_resized + + def create_depth_transition(self, before_img, after_img, depth_img, progress, transition_type, feather): + before_np = np.array(before_img, dtype=np.float32) + after_np = np.array(after_img, dtype=np.float32) + depth_np = np.array(depth_img, dtype=np.float32) + if depth_np.ndim == 3: + depth_np = depth_np[..., 0] + depth_norm = (depth_np - depth_np.min()) / (depth_np.ptp() + 1e-8) + if transition_type == "back_to_front": + threshold = 1.0 - progress + mask = (depth_norm >= threshold).astype(np.float32) + elif transition_type == "front_to_back": + threshold = progress + mask = (depth_norm <= threshold).astype(np.float32) + elif transition_type == "middle_out": + center = 0.5 + spread = progress * 0.5 + mask = ((depth_norm >= center - spread) & (depth_norm <= center + spread)).astype(np.float32) + else: + mask = np.zeros_like(depth_norm, dtype=np.float32) + feather_px = max(1, int(feather * 50)) + if feather_px > 1: + mask = cv2.GaussianBlur(mask, (feather_px | 1, feather_px | 1), 0) + mask = np.clip(mask, 0, 1) + mask_3d = np.stack([mask] * 3, axis=2) + result = before_np * (1 - mask_3d) + after_np * mask_3d + return Image.fromarray(result.astype(np.uint8)) + + def generate_transition_frames(self, before_img, after_img, depth_img, transition_type, fps, transition_duration, hold_duration, feather, loop_transition=False, easing_method="none"): + frames = [] + hold_frames = int(fps * hold_duration) + transition_frames = int(fps * transition_duration) + pbar = ProgressBar(hold_frames + transition_frames + (transition_frames if loop_transition else 0) + hold_frames) + for i in range(hold_frames): + frames.append(before_img.copy()) + pbar.update(1) + for i in range(transition_frames): + progress = i / (transition_frames - 1) if transition_frames > 1 else 1.0 + frame = self.create_depth_transition(before_img, after_img, depth_img, progress, transition_type, feather) + frames.append(frame) + pbar.update(1) + if loop_transition: + for i in range(transition_frames): + progress = i / (transition_frames - 1) if transition_frames > 1 else 1.0 + frame = self.create_depth_transition(after_img, before_img, depth_img, progress, transition_type, feather) + frames.append(frame) + pbar.update(1) + for i in range(hold_frames): + frames.append(before_img.copy()) + pbar.update(1) + else: + for i in range(hold_frames): + frames.append(after_img.copy()) + pbar.update(1) + return frames + + def create_transition_video(self, before_image, after_image, depth_map, transition_type, fps, width, height, output_path, filename_prefix, transition_duration, hold_duration, feather, loop_transition=False, easing_method="none"): + before_img, after_img, depth_img = self.resize_images(before_image, after_image, depth_map, width, height) + frames = self.generate_transition_frames(before_img, after_img, depth_img, transition_type, fps, transition_duration, hold_duration, feather, loop_transition, easing_method) + if output_path.startswith("/output/"): + output_dir = folder_paths.get_output_directory() + relative_path = output_path[8:] + if relative_path: + output_dir = os.path.join(output_dir, relative_path) + else: + output_dir = output_path + os.makedirs(output_dir, exist_ok=True) + counter = 1 + while True: + filename = f"{filename_prefix}_{counter:05d}.mp4" + video_path = os.path.join(output_dir, filename) + if not os.path.exists(video_path): + break + counter += 1 + temp_dir = os.path.join(output_dir, "temp_depth_frames") + os.makedirs(temp_dir, exist_ok=True) + for idx, frame in enumerate(frames): + frame.save(os.path.join(temp_dir, f"frame_{idx:04d}.png")) + if ffmpeg_path is None: + raise RuntimeError("FFmpeg executable not found. Please ensure FFmpeg is installed and accessible.") + ffmpeg_cmd = [ffmpeg_path, "-y", "-framerate", str(fps), "-i", os.path.join(temp_dir, "frame_%04d.png"), "-c:v", "libx264", "-pix_fmt", "yuv420p", video_path] + subprocess.run(ffmpeg_cmd, check=True) + for f in os.listdir(temp_dir): + os.remove(os.path.join(temp_dir, f)) + os.rmdir(temp_dir) + preview_frame = frames[len(frames) // 2] + preview_tensor = torch.from_numpy(np.array(preview_frame).astype(np.float32) / 255.0).unsqueeze(0) + return (video_path, preview_tensor) \ No newline at end of file