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): | |
| try: | |
| video_path = Path(video_path) | |
| audio_path = Path(audio_path) | |
| srt_path = Path(srt_path) | |
| 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") | |
| # FIXED FFMPEG COMMAND: | |
| cmd = [ | |
| 'ffmpeg', | |
| '-y', # Overwrite output | |
| '-i', str(video_path), # Original video | |
| '-i', str(audio_path), # Translated audio | |
| # Video processing (re-encode instead of copy to allow subtitles) | |
| '-vf', f"subtitles={str(srt_path)}:force_style='FontSize={SUBTITLE_FONT_SIZE}'", | |
| '-c:v', 'libx264', # Re-encode video | |
| '-preset', 'fast', # Faster encoding | |
| '-crf', '23', # Good quality | |
| # Audio processing | |
| '-c:a', 'aac', | |
| '-b:a', '192k', | |
| # Stream selection | |
| '-map', '0:v:0', # Video from first input | |
| '-map', '1:a:0', # Audio from second input | |
| # Metadata | |
| '-metadata:s:a:0', 'language=' + lang_code[:3], # Set audio language | |
| str(output_path) | |
| ] | |
| logger.debug(f"Running command: {' '.join(cmd)}") | |
| process = subprocess.run(cmd, capture_output=True, text=True) | |
| if process.returncode != 0: | |
| error_msg = f"FFmpeg failed: {process.stderr}" | |
| if "Filtergraph" in error_msg and "streamcopy" in error_msg: | |
| error_msg += "\nFIX: Cannot use both video filters and stream copy. Must re-encode video." | |
| raise Exception(error_msg) | |
| return output_path | |
| except Exception as e: | |
| logger.error(f"Combining failed: {str(e)}") | |
| raise Exception(f"Failed to combine video/audio/subtitles: {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") | |
| 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 | |