""" utils/video_utils.py ──────────────────── Merge dubbed Hindi audio back into the original video. Strategy: 1. Use ffmpeg to mix dubbed speech + original BGM (at reduced volume) 2. Replace the video's audio track with the mixed result 3. Copy video codec (no re-encoding) → fast even on CPU This avoids MoviePy for the final merge (MoviePy re-encodes, very slow on CPU). We use raw ffmpeg subprocess calls instead — much faster. """ import os import tempfile import subprocess def merge_audio_video( video_path: str, dubbed_audio_path: str, original_audio_path: str, bgm_volume: float = 0.15, ) -> str: """ Create final dubbed video: - Video stream: copied from original (no re-encode) - Audio stream: Hindi speech mixed with faint original BGM Parameters: video_path – path to original MP4 dubbed_audio_path – path to Hindi dubbed WAV original_audio_path – path to extracted original audio WAV bgm_volume – 0.0–1.0, volume of background music Returns path to output MP4. """ output_path = os.path.join(tempfile.gettempdir(), "hindi_dubbed_output.mp4") # ffmpeg filter graph: # [0:a] = original audio (background music) # [1:a] = Hindi dubbed speech # # Step 1: lower original audio volume → bgm # Step 2: mix bgm + dubbed speech together # Step 3: mux with video stream (copy, no re-encode) bgm_vol_str = str(round(bgm_volume, 3)) cmd = [ "ffmpeg", "-y", "-i", video_path, # input 0: original video (video + audio) "-i", dubbed_audio_path, # input 1: Hindi dubbed audio "-filter_complex", ( # Lower original audio volume for BGM f"[0:a]volume={bgm_vol_str}[bgm];" # Mix BGM with Hindi speech "[bgm][1:a]amix=inputs=2:duration=first:dropout_transition=3[mixed]" ), "-map", "0:v", # video stream from original "-map", "[mixed]", # audio stream = mixed result "-c:v", "copy", # copy video codec — NO re-encoding (fast!) "-c:a", "aac", # encode mixed audio as AAC "-b:a", "192k", # audio bitrate "-shortest", # trim to shortest stream output_path, ] result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if result.returncode != 0: # Try a simpler fallback: just replace audio entirely (no BGM mix) output_path = _simple_replace_audio(video_path, dubbed_audio_path) return output_path def _simple_replace_audio(video_path: str, audio_path: str) -> str: """ Fallback: simply replace video audio with dubbed audio. Used when the complex filter fails. """ output_path = os.path.join(tempfile.gettempdir(), "hindi_dubbed_output_simple.mp4") cmd = [ "ffmpeg", "-y", "-i", video_path, "-i", audio_path, "-map", "0:v", # take video from original "-map", "1:a", # take audio from dubbed file "-c:v", "copy", # copy video stream (fast) "-c:a", "aac", "-b:a", "192k", "-shortest", output_path, ] subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return output_path def get_video_duration(video_path: str) -> float: """Return video duration in seconds using ffprobe.""" cmd = [ "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", video_path, ] result = subprocess.run(cmd, capture_output=True, text=True) try: return float(result.stdout.strip()) except Exception: return 0.0