File size: 7,947 Bytes
99f5113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import os
import tempfile
import gradio as gr
import numpy as np

# Lazy imports to make startup faster on Spaces CPU
ASR_MODEL = None
EN2HI = None
HI2EN = None
TTS_ENGINE = None
XTTS_AVAILABLE = False

# --- Utilities ---

def lazy_import_asr():
    global ASR_MODEL
    if ASR_MODEL is None:
        import whisper
        # small is ok for 15–60s clips on CPU. change to "medium" if GPU.
        ASR_MODEL = whisper.load_model("small")
    return ASR_MODEL


def lazy_import_mt():
    global EN2HI, HI2EN
    if EN2HI is None or HI2EN is None:
        from transformers import pipeline
        # MarianMT models (offline, no API). Good enough for EN<>HI.
        EN2HI = pipeline("translation", model="Helsinki-NLP/opus-mt-en-hi")
        HI2EN = pipeline("translation", model="Helsinki-NLP/opus-mt-hi-en")
    return EN2HI, HI2EN


def lazy_import_tts(prefer_xtts=True):
    """
    Try XTTS v2 (multi-lingual + cloning) if available, else fall back to a small CPU TTS.
    Fallback voices are non-cloned.
    """
    global TTS_ENGINE, XTTS_AVAILABLE
    if TTS_ENGINE is not None:
        return TTS_ENGINE, XTTS_AVAILABLE

    try:
        # Coqui TTS
        from TTS.api import TTS
        if prefer_xtts:
            # XTTS v2 supports speaker embedding + multilingual
            TTS_ENGINE = TTS("tts_models/multilingual/multi-dataset/xtts_v2")
            XTTS_AVAILABLE = True
        else:
            raise RuntimeError("skip xtts")
    except Exception:
        try:
            # Lightweight English-only fallback (CPU friendly)
            from TTS.api import TTS
            TTS_ENGINE = TTS("tts_models/en/vctk/vits")
            XTTS_AVAILABLE = False
        except Exception as e:
            raise RuntimeError(f"No TTS backend could be loaded: {e}")

    return TTS_ENGINE, XTTS_AVAILABLE


def extract_audio(video_path, out_wav):
    import moviepy.editor as mp
    clip = mp.VideoFileClip(video_path)
    audio = clip.audio
    audio.write_audiofile(out_wav, fps=16000, codec="pcm_s16le")
    clip.close()
    return out_wav


def transcribe(video_path, language_hint="auto"):
    """ASR using Whisper. Returns (text, lang_code)."""
    asr = lazy_import_asr()
    with tempfile.TemporaryDirectory() as td:
        wav = os.path.join(td, "audio.wav")
        extract_audio(video_path, wav)
        result = asr.transcribe(wav, language=None if language_hint == "auto" else language_hint)
        text = result.get("text", "").strip()
        lang = result.get("language", "unknown")
    return text, lang


def translate_text(text, target_lang):
    """Use MarianMT for EN<>HI only. If source==target, return original."""
    if not text.strip():
        return ""
    en2hi, hi2en = lazy_import_mt()
    tl = target_lang.lower()

    # naive detection: if Devanagari present, treat as Hindi
    is_devanagari = any(0x0900 <= ord(ch) <= 0x097F for ch in text)

    if tl in ["hi", "hindi"]:
        if is_devanagari:
            return text
        # assume english -> hindi
        out = en2hi(text)[0]["translation_text"]
        return out
    elif tl in ["en", "english"]:
        if not is_devanagari:
            return text
        out = hi2en(text)[0]["translation_text"]
        return out
    else:
        # unsupported target -> return original
        return text


def synthesize_tts(text, lang="en", ref_audio=None):
    """
    text -> wav_path. If XTTS available and ref_audio provided + consent, clone voice.
    """
    tts, xtts = lazy_import_tts(prefer_xtts=True)

    tmp_wav = tempfile.mktemp(suffix=".wav")

    if xtts:
        # XTTS supports many langs: 'en', 'hi', etc.
        speaker_wav = ref_audio if (ref_audio and os.path.exists(ref_audio)) else None
        tts.tts_to_file(text=text, language=lang, speaker_wav=speaker_wav, file_path=tmp_wav)
    else:
        # fallback is English-only; if lang not en, we just attempt anyway
        tts.tts_to_file(text=text, file_path=tmp_wav)

    return tmp_wav


def merge_audio(video_path, dubbed_wav, out_mp4):
    import moviepy.editor as mp
    video = mp.VideoFileClip(video_path)
    audio = mp.AudioFileClip(dubbed_wav).set_duration(video.duration)
    final = video.set_audio(audio)
    # write with AAC audio so browsers play fine
    final.write_videofile(out_mp4, audio_codec="aac", codec="libx264")
    video.close()
    audio.close()
    final.close()
    return out_mp4


# --- Gradio UI callbacks ---

def full_dub(video, target_lang, language_hint, use_cloning, consent, ref_voice=None):
    """
    End-to-end pipeline: ASR -> (optional) Translate -> TTS -> Merge
    Returns: transcribed_text, translated_text, dubbed_video
    """
    if not consent:
        raise gr.Error("Please confirm consent for dubbing/cloning.")

    if video is None:
        raise gr.Error("Please upload a video (15–60 seconds recommended).")

    # 1) ASR
    text, detected = transcribe(video, language_hint=language_hint)

    # 2) Translate if needed
    translated = translate_text(text, target_lang)

    # 3) TTS
    # choose TTS language code
    lang_code = "en" if target_lang.lower().startswith("en") else "hi"

    # If no translation occurred (unsupported), speak original text
    tts_text = translated if translated.strip() else text

    # Only pass ref_voice if user ticked cloning
    ref_path = None
    if use_cloning and ref_voice is not None:
        # Coqui XTTS expects a file path
        ref_path = ref_voice

    out_wav = synthesize_tts(tts_text, lang=lang_code, ref_audio=ref_path)

    # 4) Merge
    out_mp4 = tempfile.mktemp(suffix=".mp4")
    merge_audio(video, out_wav, out_mp4)

    # Small watermark text for transparency
    mark = f"Dubbed ({'cloned' if use_cloning else 'synthetic'}) | Lang: {target_lang.upper()}"

    return text, translated, out_mp4, mark


# --- Build UI ---
with gr.Blocks(theme=gr.themes.Soft(), css=".small {font-size: 12px}") as demo:
    gr.Markdown("""
    # 🎬 Safe Video Dubbing (EN ⇄ HI) β€” CPU Friendly
    **Hinglish UI**: Neeche steps follow karo. 15–60s video pe best results.

    - ASR: Whisper-small (offline)
    - Translation: MarianMT (offline)
    - TTS: Tries **XTTS v2** (voice cloning) β†’ agar available nahin hua toh English-only fallback TTS use karega.
    - Output: MP4 with replaced audio

    **Note (Safety)**: Sirf wahi awaaz clone/ dub karein jiske liye aapke paas clear consent ho. App ek chhota watermark lagata hai transparency ke liye.
    """)

    with gr.Row():
        video_in = gr.Video(label="Input Video (mp4/mov)")
        ref_voice = gr.Audio(label="(Optional) Reference Voice for Cloning β€” same person ki 5–10s WAV/MP3", type="filepath")

    with gr.Row():
        target_lang = gr.Radio(["en", "hi"], value="hi", label="Target Language (Output)")
        language_hint = gr.Dropdown(["auto", "en", "hi"], value="auto", label="ASR Language Hint")

    with gr.Row():
        use_cloning = gr.Checkbox(False, label="Enable Voice Cloning (XTTS v2)")
        consent = gr.Checkbox(False, label="I confirm consent for dubbing / cloning (required)")

    with gr.Row():
        btn = gr.Button("▢️ Run Dubbing")

    with gr.Row():
        transcribed = gr.Textbox(label="Transcribed Text")
        translated = gr.Textbox(label="Translated Text")

    dubbed = gr.Video(label="Dubbed Output (MP4)")
    watermark = gr.Markdown(visible=True)

    btn.click(fn=full_dub, inputs=[video_in, target_lang, language_hint, use_cloning, consent, ref_voice],
              outputs=[transcribed, translated, dubbed, watermark])

    gr.Markdown("""
    ---
    **Tips (Hinglish):**
    - Agar Space CPU pe slow hai, 15–30 sec clips try karo.
    - XTTS cloning ke liye GPU recommended. HF Space hardware settings me GPU select kar sakte ho.
    - Hindi ke liye XTTS best; fallback TTS English-only ho sakta hai.
    - Clean audio = better results. Background noise kam rakho.
    """)

if __name__ == "__main__":
    demo.launch()