""" utils/audio_utils.py ──────────────────── Audio extraction, silence detection, and audio mixing helpers. Libraries: ffmpeg-python, librosa, pydub All CPU-friendly, no GPU needed. """ import os import tempfile import subprocess import numpy as np import librosa from pydub import AudioSegment from pydub.silence import detect_nonsilent def extract_audio(video_path: str) -> str: """ Pull the audio track out of a video file using ffmpeg. Returns path to a WAV file (16kHz mono — ideal for Whisper). """ out_path = os.path.join(tempfile.gettempdir(), "extracted_audio.wav") # ffmpeg command: extract audio, convert to 16kHz mono WAV cmd = [ "ffmpeg", "-y", # -y = overwrite without asking "-i", video_path, # input video "-vn", # drop video stream "-acodec", "pcm_s16le", # uncompressed WAV "-ar", "16000", # 16 kHz sample rate (Whisper prefers this) "-ac", "1", # mono channel out_path, ] subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return out_path def detect_silence_segments(audio_path: str, min_silence_ms: int = 700, silence_thresh_db: int = -40): """ Detect silence / low-energy sections in the audio. Returns list of (start_sec, end_sec) tuples for silent regions. These mark where background music or ambient sound lives — useful so we don't try to dub over music sections. Parameters: min_silence_ms – minimum duration to count as silence (ms) silence_thresh_db – dB level below which is considered silence """ audio = AudioSegment.from_wav(audio_path) # detect_nonsilent returns [(start_ms, end_ms), ...] of SPEECH speech_intervals = detect_nonsilent( audio, min_silence_len=min_silence_ms, silence_thresh=silence_thresh_db, ) # Invert to get silence intervals total_ms = len(audio) silence_segs = [] prev_end = 0 for start, end in speech_intervals: if start > prev_end + 200: # gap > 200ms counts as silence silence_segs.append((prev_end / 1000, start / 1000)) prev_end = end if prev_end < total_ms - 200: silence_segs.append((prev_end / 1000, total_ms / 1000)) return silence_segs def mix_audio_with_video(video_path: str, speech_audio_path: str, bgm_volume: float = 0.15) -> str: """ Mix Hindi speech with the original video's background audio. Strategy: - Extract original audio, lower its volume (bgm_volume) - Overlay the Hindi speech track on top - Result: faint original BGM + clear Hindi speech """ bgm_path = os.path.join(tempfile.gettempdir(), "bgm_only.wav") # Extract original audio subprocess.run([ "ffmpeg", "-y", "-i", video_path, "-vn", "-acodec", "pcm_s16le", "-ar", "44100", "-ac", "2", bgm_path, ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # Load both bgm = AudioSegment.from_wav(bgm_path) speech = AudioSegment.from_wav(speech_audio_path) # Lower BGM volume bgm = bgm + (20 * np.log10(bgm_volume)) # dB adjustment # Pad speech if shorter than BGM if len(speech) < len(bgm): speech = speech + AudioSegment.silent(duration=len(bgm) - len(speech)) # Overlay speech on BGM mixed = bgm.overlay(speech) mixed_path = os.path.join(tempfile.gettempdir(), "mixed_audio.wav") mixed.export(mixed_path, format="wav") return mixed_path