| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import os |
| | import subprocess |
| | import logging |
| | import random |
| | import time |
| | import shutil |
| | from typing import List, Optional, Tuple |
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| | class VideoToolError(Exception): |
| | """Custom exception for errors originating from the VideoEncodeTool.""" |
| | pass |
| |
|
| | class VideoEncodeTool: |
| | """ |
| | A specialist for handling video encoding and manipulation tasks. |
| | Currently uses FFmpeg as the backend. |
| | """ |
| |
|
| | def create_transition_bridge(self, start_image_path: str, end_image_path: str, |
| | duration: float, fps: int, target_resolution: Tuple[int, int], |
| | workspace_dir: str, effect: Optional[str] = None) -> str: |
| | """ |
| | Creates a short video clip that transitions between two static images using FFmpeg's xfade filter. |
| | This is useful for creating a "bridge" during a hard "cut" decided by the cinematic director. |
| | |
| | Args: |
| | start_image_path (str): The file path to the starting image. |
| | end_image_path (str): The file path to the ending image. |
| | duration (float): The desired duration of the transition in seconds. |
| | fps (int): The frames per second for the output video. |
| | target_resolution (Tuple[int, int]): The (width, height) of the output video. |
| | workspace_dir (str): The directory to save the output video. |
| | effect (Optional[str], optional): The specific xfade effect to use. If None, a random |
| | effect is chosen. Defaults to None. |
| | |
| | Returns: |
| | str: The file path to the generated transition video clip. |
| | |
| | Raises: |
| | VideoToolError: If the FFmpeg command fails. |
| | """ |
| | output_path = os.path.join(workspace_dir, f"bridge_{int(time.time())}.mp4") |
| | width, height = target_resolution |
| | |
| | fade_effects = [ |
| | "fade", "wipeleft", "wiperight", "wipeup", "wipedown", "dissolve", |
| | "fadeblack", "fadewhite", "radial", "rectcrop", "circleopen", |
| | "circleclose", "horzopen", "horzclose" |
| | ] |
| | |
| | selected_effect = effect if effect and effect.strip() else random.choice(fade_effects) |
| | |
| | transition_duration = max(0.1, duration) |
| |
|
| | cmd = ( |
| | f"ffmpeg -y -v error -loop 1 -t {transition_duration} -i \"{start_image_path}\" -loop 1 -t {transition_duration} -i \"{end_image_path}\" " |
| | f"-filter_complex \"[0:v]scale={width}:{height},setsar=1[v0];[1:v]scale={width}:{height},setsar=1[v1];" |
| | f"[v0][v1]xfade=transition={selected_effect}:duration={transition_duration}:offset=0[out]\" " |
| | f"-map \"[out]\" -c:v libx264 -r {fps} -pix_fmt yuv420p \"{output_path}\"" |
| | ) |
| | |
| | logger.info(f"Creating FFmpeg transition bridge with effect: '{selected_effect}' | Duration: {transition_duration}s") |
| | |
| | try: |
| | subprocess.run(cmd, shell=True, check=True, text=True) |
| | except subprocess.CalledProcessError as e: |
| | logger.error(f"FFmpeg bridge creation failed. Return code: {e.returncode}") |
| | logger.error(f"FFmpeg command: {cmd}") |
| | logger.error(f"FFmpeg stderr: {e.stderr}") |
| | raise VideoToolError(f"Failed to create transition video. Details: {e.stderr}") |
| | |
| | return output_path |
| |
|
| | def concatenate_videos(self, video_paths: List[str], output_path: str, workspace_dir: str): |
| | """ |
| | Concatenates multiple video clips into a single file without re-encoding. |
| | |
| | Args: |
| | video_paths (List[str]): A list of absolute paths to the video clips to be concatenated. |
| | output_path (str): The absolute path for the final output video. |
| | workspace_dir (str): The directory to use for temporary files, like the concat list. |
| | |
| | Raises: |
| | VideoToolError: If no video paths are provided or if the FFmpeg command fails. |
| | """ |
| | if not video_paths: |
| | raise VideoToolError("VideoEncodeTool: No video fragments provided for concatenation.") |
| |
|
| | if len(video_paths) == 1: |
| | logger.info("Only one video clip found. Skipping concatenation and just copying the file.") |
| | shutil.copy(video_paths[0], output_path) |
| | return |
| |
|
| | list_file_path = os.path.join(workspace_dir, "concat_list.txt") |
| |
|
| | try: |
| | with open(list_file_path, 'w', encoding='utf-8') as f: |
| | for path in video_paths: |
| | f.write(f"file '{os.path.abspath(path)}'\n") |
| |
|
| | cmd_list = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', list_file_path, '-c', 'copy', output_path] |
| | |
| | logger.info(f"Concatenating {len(video_paths)} video clips into {output_path} using FFmpeg...") |
| | |
| | subprocess.run(cmd_list, check=True, capture_output=True, text=True) |
| | |
| | logger.info(f"FFmpeg concatenation successful. Final video is at: {output_path}") |
| |
|
| | except subprocess.CalledProcessError as e: |
| | logger.error(f"FFmpeg concatenation failed. Return code: {e.returncode}") |
| | logger.error(f"FFmpeg stderr: {e.stderr}") |
| | raise VideoToolError(f"Failed to assemble the final video using FFmpeg. Details: {e.stderr}") |
| | except Exception as e: |
| | logger.error(f"An unexpected error occurred during video concatenation: {e}", exc_info=True) |
| | raise VideoToolError("An unexpected error occurred during the final video assembly.") |
| | finally: |
| | if os.path.exists(list_file_path): |
| | os.remove(list_file_path) |
| |
|
| |
|
| | |
| | |
| | video_encode_tool_singleton = VideoEncodeTool() |