File size: 3,627 Bytes
4383d5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
"""
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