Create video_encode_tool.py
Browse files- tools/video_encode_tool.py +143 -0
tools/video_encode_tool.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tools/video_encode_tool.py
|
| 2 |
+
#
|
| 3 |
+
# Copyright (C) August 4, 2025 Carlos Rodrigues dos Santos
|
| 4 |
+
#
|
| 5 |
+
# Version: 1.1.0
|
| 6 |
+
#
|
| 7 |
+
# This file defines the VideoEncodeTool specialist. Its purpose is to abstract away
|
| 8 |
+
# the underlying command-line tools (like FFmpeg) used for video manipulation tasks
|
| 9 |
+
# such as concatenation and creating transitions. By encapsulating this logic, the core
|
| 10 |
+
# Deformes4D engine can remain agnostic to the specific tool being used.
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import subprocess
|
| 14 |
+
import logging
|
| 15 |
+
import gradio as gr
|
| 16 |
+
from typing import List, Optional, Tuple
|
| 17 |
+
import random
|
| 18 |
+
import time
|
| 19 |
+
import shutil
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
class VideoEncodeTool:
|
| 24 |
+
"""
|
| 25 |
+
A specialist for handling video encoding and manipulation tasks.
|
| 26 |
+
Currently uses FFmpeg as the backend.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def create_transition_bridge(self, start_image_path: str, end_image_path: str,
|
| 30 |
+
duration: float, fps: int, target_resolution: Tuple[int, int],
|
| 31 |
+
workspace_dir: str, effect: Optional[str] = None) -> str:
|
| 32 |
+
"""
|
| 33 |
+
Creates a short video clip that transitions between two static images using FFmpeg's xfade filter.
|
| 34 |
+
This is useful for creating a "bridge" during a hard "cut" decided by the cinematic director.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
start_image_path (str): The file path to the starting image.
|
| 38 |
+
end_image_path (str): The file path to the ending image.
|
| 39 |
+
duration (float): The desired duration of the transition in seconds.
|
| 40 |
+
fps (int): The frames per second for the output video.
|
| 41 |
+
target_resolution (Tuple[int, int]): The (width, height) of the output video.
|
| 42 |
+
workspace_dir (str): The directory to save the output video.
|
| 43 |
+
effect (Optional[str], optional): The specific xfade effect to use. If None, a random
|
| 44 |
+
effect is chosen. Defaults to None.
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
str: The file path to the generated transition video clip.
|
| 48 |
+
"""
|
| 49 |
+
output_path = os.path.join(workspace_dir, f"bridge_{int(time.time())}.mp4")
|
| 50 |
+
width, height = target_resolution
|
| 51 |
+
|
| 52 |
+
fade_effects = [
|
| 53 |
+
"fade", "wipeleft", "wiperight", "wipeup", "wipedown", "dissolve",
|
| 54 |
+
"fadeblack", "fadewhite", "radial", "rectcrop", "circleopen",
|
| 55 |
+
"circleclose", "horzopen", "horzclose"
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
selected_effect = effect if effect and effect.strip() else random.choice(fade_effects)
|
| 59 |
+
|
| 60 |
+
# The duration of each image loop and the xfade itself should match the total desired duration.
|
| 61 |
+
transition_duration = max(0.1, duration) # Ensure duration is not zero
|
| 62 |
+
|
| 63 |
+
# Construct the FFmpeg command
|
| 64 |
+
# -v error: Suppress all console output except for errors.
|
| 65 |
+
# -loop 1: Loop the input image.
|
| 66 |
+
# -t {duration}: Set the duration for the looped image.
|
| 67 |
+
# -filter_complex: Defines a complex filtergraph.
|
| 68 |
+
# - scale,setsar: Pre-process each image to the target resolution and aspect ratio.
|
| 69 |
+
# - xfade: Apply the crossfade transition.
|
| 70 |
+
# - offset=0: Start the transition immediately.
|
| 71 |
+
# -map "[out]": Map the output of the filtergraph to the final video.
|
| 72 |
+
# -c:v libx264 -r {fps} -pix_fmt yuv420p: Standard high-compatibility video encoding settings.
|
| 73 |
+
cmd = (
|
| 74 |
+
f"ffmpeg -y -v error -loop 1 -t {transition_duration} -i \"{start_image_path}\" -loop 1 -t {transition_duration} -i \"{end_image_path}\" "
|
| 75 |
+
f"-filter_complex \"[0:v]scale={width}:{height},setsar=1[v0];[1:v]scale={width}:{height},setsar=1[v1];"
|
| 76 |
+
f"[v0][v1]xfade=transition={selected_effect}:duration={transition_duration}:offset=0[out]\" "
|
| 77 |
+
f"-map \"[out]\" -c:v libx264 -r {fps} -pix_fmt yuv420p \"{output_path}\""
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
logger.info(f"Creating FFmpeg transition bridge with effect: '{selected_effect}' | Duration: {transition_duration}s")
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
subprocess.run(cmd, shell=True, check=True, text=True)
|
| 84 |
+
except subprocess.CalledProcessError as e:
|
| 85 |
+
logger.error(f"FFmpeg bridge creation failed. Return code: {e.returncode}")
|
| 86 |
+
logger.error(f"FFmpeg command: {cmd}")
|
| 87 |
+
logger.error(f"FFmpeg stderr: {e.stderr}")
|
| 88 |
+
raise gr.Error(f"Failed to create transition video. Details: {e.stderr}")
|
| 89 |
+
|
| 90 |
+
return output_path
|
| 91 |
+
|
| 92 |
+
def concatenate_videos(self, video_paths: List[str], output_path: str, workspace_dir: str):
|
| 93 |
+
"""
|
| 94 |
+
Concatenates multiple video clips into a single file without re-encoding.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
video_paths (List[str]): A list of absolute paths to the video clips to be concatenated.
|
| 98 |
+
output_path (str): The absolute path for the final output video.
|
| 99 |
+
workspace_dir (str): The directory to use for temporary files, like the concat list.
|
| 100 |
+
"""
|
| 101 |
+
if not video_paths:
|
| 102 |
+
raise gr.Error("VideoEncodeTool: No video fragments provided for concatenation.")
|
| 103 |
+
|
| 104 |
+
if len(video_paths) == 1:
|
| 105 |
+
logger.info("Only one video clip found. Skipping concatenation and just copying the file.")
|
| 106 |
+
# If there's only one clip, a simple copy is much faster.
|
| 107 |
+
shutil.copy(video_paths[0], output_path)
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
list_file_path = os.path.join(workspace_dir, "concat_list.txt")
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
with open(list_file_path, 'w', encoding='utf-8') as f:
|
| 114 |
+
for path in video_paths:
|
| 115 |
+
f.write(f"file '{os.path.abspath(path)}'\n")
|
| 116 |
+
|
| 117 |
+
cmd_list = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', list_file_path, '-c', 'copy', output_path]
|
| 118 |
+
|
| 119 |
+
logger.info(f"Concatenating {len(video_paths)} video clips into {output_path} using FFmpeg...")
|
| 120 |
+
|
| 121 |
+
subprocess.run(cmd_list, check=True, capture_output=True, text=True)
|
| 122 |
+
|
| 123 |
+
logger.info(f"FFmpeg concatenation successful. Final video is at: {output_path}")
|
| 124 |
+
|
| 125 |
+
except subprocess.CalledProcessError as e:
|
| 126 |
+
logger.error(f"FFmpeg concatenation failed. Return code: {e.returncode}")
|
| 127 |
+
logger.error(f"FFmpeg stderr: {e.stderr}")
|
| 128 |
+
raise gr.Error(f"Failed to assemble the final video using FFmpeg. Details: {e.stderr}")
|
| 129 |
+
except Exception as e:
|
| 130 |
+
logger.error(f"An unexpected error occurred during video concatenation: {e}", exc_info=True)
|
| 131 |
+
raise gr.Error("An unexpected error occurred during the final video assembly.")
|
| 132 |
+
finally:
|
| 133 |
+
if os.path.exists(list_file_path):
|
| 134 |
+
os.remove(list_file_path)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# --- Singleton Instance ---
|
| 138 |
+
# We create a single instance of the tool to be imported by other modules.
|
| 139 |
+
video_encode_tool_singleton = VideoEncodeTool()```
|
| 140 |
+
|
| 141 |
+
O especialista `VideoEncodeTool` está agora mais poderoso. O `Deformes4D_engine` poderá chamá-lo tanto para montar os clipes gerados pelo LTX quanto para criar transições rápidas e eficientes para os "cortes", deixando a lógica de geração de latentes focada nos segmentos que precisam de movimento complexo.
|
| 142 |
+
|
| 143 |
+
Essa abstração está deixando nosso `Deformes4D_engine` cada vez mais limpo e focado em sua tarefa de orquestração de alto nível.
|