Spaces:
Running
Running
""" | |
Video processing utilities for combining video, audio, and subtitles. | |
""" | |
import os | |
import shutil | |
import subprocess | |
from pathlib import Path | |
import tempfile | |
from src.utils.logger import get_logger | |
from config import OUTPUT_DIR, SUBTITLE_FONT_SIZE | |
logger = get_logger(__name__) | |
def combine_video_audio_subtitles(video_path, audio_path, srt_path, output_path=None): | |
""" | |
Combine video with translated audio and subtitles. | |
Args: | |
video_path (str): Path to the video file | |
audio_path (str): Path to the translated audio file | |
srt_path (str): Path to the subtitle file | |
output_path (str, optional): Path for the output video | |
Returns: | |
Path: Path to the output video | |
Raises: | |
Exception: If combining fails | |
""" | |
try: | |
video_path = Path(video_path) | |
audio_path = Path(audio_path) | |
srt_path = Path(srt_path) | |
# Generate output path if not provided | |
if output_path is None: | |
lang_code = srt_path.stem.split('_')[-1] | |
output_path = OUTPUT_DIR / f"{video_path.stem}_translated_{lang_code}.mp4" | |
else: | |
output_path = Path(output_path) | |
logger.info(f"Combining video, audio, and subtitles") | |
# Verify that all input files exist | |
if not video_path.exists(): | |
raise FileNotFoundError(f"Video file does not exist: {video_path}") | |
if not audio_path.exists(): | |
raise FileNotFoundError(f"Audio file does not exist: {audio_path}") | |
if not srt_path.exists(): | |
raise FileNotFoundError(f"Subtitle file does not exist: {srt_path}") | |
logger.info(f"Input files verified: Video: {video_path.stat().st_size} bytes, " | |
f"Audio: {audio_path.stat().st_size} bytes, " | |
f"Subtitles: {srt_path.stat().st_size} bytes") | |
# Try different methods to combine | |
methods = [ | |
combine_method_subtitles_filter, | |
combine_method_with_temp, | |
combine_method_no_subtitles | |
] | |
success = False | |
error_messages = [] | |
for i, method in enumerate(methods): | |
try: | |
logger.info(f"Trying combination method {i+1}/{len(methods)}") | |
result = method(video_path, audio_path, srt_path, output_path) | |
if result and Path(result).exists() and Path(result).stat().st_size > 0: | |
success = True | |
output_path = result | |
logger.info(f"Combination method {i+1} succeeded") | |
break | |
else: | |
error_messages.append(f"Method {i+1} failed: Result file not valid") | |
except Exception as e: | |
error_message = f"Method {i+1} failed: {str(e)}" | |
logger.warning(error_message) | |
error_messages.append(error_message) | |
if not success: | |
error_message = f"All combination methods failed: {'; '.join(error_messages)}" | |
logger.error(error_message) | |
raise Exception(error_message) | |
logger.info(f"Successfully combined video, audio, and subtitles: {output_path}") | |
return output_path | |
except Exception as e: | |
logger.error(f"Combining failed: {str(e)}", exc_info=True) | |
raise Exception(f"Combining failed: {str(e)}") | |
def combine_method_subtitles_filter(video_path, audio_path, srt_path, output_path): | |
""" | |
Combine video, audio, and subtitles using ffmpeg with subtitle filter. | |
Args: | |
video_path (Path): Path to the video file | |
audio_path (Path): Path to the translated audio file | |
srt_path (Path): Path to the subtitle file | |
output_path (Path): Path for the output video | |
Returns: | |
Path: Path to the output video | |
""" | |
logger.info(f"Using subtitles filter method") | |
# Use ffmpeg to combine video, audio, and subtitles | |
cmd = [ | |
'ffmpeg', | |
'-i', str(video_path), # Video input | |
'-i', str(audio_path), # Audio input | |
'-vf', f"subtitles={str(srt_path)}:force_style='FontSize={SUBTITLE_FONT_SIZE}'", # Subtitle filter | |
'-map', '0:v', # Map video from first input | |
'-map', '1:a', # Map audio from second input | |
'-c:v', 'libx264', # Video codec | |
'-c:a', 'aac', # Audio codec | |
'-strict', 'experimental', | |
'-b:a', '192k', # Audio bitrate | |
'-y', # Overwrite output | |
str(output_path) | |
] | |
logger.debug(f"Running command: {' '.join(cmd)}") | |
process = subprocess.run(cmd, capture_output=True, text=True) | |
if process.returncode != 0: | |
error_message = f"FFmpeg subtitles filter method failed: {process.stderr}" | |
logger.error(error_message) | |
raise Exception(error_message) | |
return output_path | |
def combine_method_with_temp(video_path, audio_path, srt_path, output_path): | |
""" | |
Combine video, audio, and subtitles using temporary files. | |
Args: | |
video_path (Path): Path to the video file | |
audio_path (Path): Path to the translated audio file | |
srt_path (Path): Path to the subtitle file | |
output_path (Path): Path for the output video | |
Returns: | |
Path: Path to the output video | |
""" | |
logger.info(f"Using temporary file method") | |
# Create temporary directory | |
temp_dir = Path(tempfile.mkdtemp(prefix="video_combine_", dir=OUTPUT_DIR / "temp")) | |
try: | |
# Step 1: Combine video with audio | |
temp_video_audio = temp_dir / "video_with_audio.mp4" | |
cmd1 = [ | |
'ffmpeg', | |
'-i', str(video_path), | |
'-i', str(audio_path), | |
'-c:v', 'copy', | |
'-c:a', 'aac', | |
'-strict', 'experimental', | |
'-map', '0:v', | |
'-map', '1:a', | |
'-y', | |
str(temp_video_audio) | |
] | |
logger.debug(f"Running command (step 1): {' '.join(cmd1)}") | |
process1 = subprocess.run(cmd1, capture_output=True, text=True) | |
if process1.returncode != 0: | |
error_message = f"Step 1 failed: {process1.stderr}" | |
logger.error(error_message) | |
raise Exception(error_message) | |
# Step 2: Add subtitles to the combined video | |
cmd2 = [ | |
'ffmpeg', | |
'-i', str(temp_video_audio), | |
'-vf', f"subtitles={str(srt_path)}:force_style='FontSize={SUBTITLE_FONT_SIZE}'", | |
'-c:a', 'copy', | |
'-y', | |
str(output_path) | |
] | |
logger.debug(f"Running command (step 2): {' '.join(cmd2)}") | |
process2 = subprocess.run(cmd2, capture_output=True, text=True) | |
if process2.returncode != 0: | |
error_message = f"Step 2 failed: {process2.stderr}" | |
logger.error(error_message) | |
raise Exception(error_message) | |
return output_path | |
finally: | |
# Clean up temporary directory | |
try: | |
shutil.rmtree(temp_dir) | |
logger.debug(f"Cleaned up temporary directory: {temp_dir}") | |
except Exception as e: | |
logger.warning(f"Failed to clean up temp directory: {str(e)}") | |
def combine_method_no_subtitles(video_path, audio_path, srt_path, output_path): | |
""" | |
Fallback method: Combine only video and audio without subtitles. | |
Args: | |
video_path (Path): Path to the video file | |
audio_path (Path): Path to the translated audio file | |
srt_path (Path): Path to the subtitle file (unused in this method) | |
output_path (Path): Path for the output video | |
Returns: | |
Path: Path to the output video | |
""" | |
logger.info(f"Using fallback method (no subtitles)") | |
# Just combine video and audio as fallback | |
cmd = [ | |
'ffmpeg', | |
'-i', str(video_path), | |
'-i', str(audio_path), | |
'-c:v', 'copy', | |
'-c:a', 'aac', | |
'-strict', 'experimental', | |
'-map', '0:v', | |
'-map', '1:a', | |
'-y', | |
str(output_path) | |
] | |
logger.debug(f"Running command: {' '.join(cmd)}") | |
process = subprocess.run(cmd, capture_output=True, text=True) | |
if process.returncode != 0: | |
error_message = f"Fallback method failed: {process.stderr}" | |
logger.error(error_message) | |
raise Exception(error_message) | |
logger.warning("Video was combined without subtitles") | |
return output_path | |