CoolFace
Apppublic

suraj-ml-projects/Video_editing

sourceHugging Facemitupdated 4d agoView on Hugging Face
0likes
streamlit_app.py824 linesDownload Raw Back to root
1"""2๐ŸŽฌ Movie Dubber โ€” Any Language โ†’ Hindi3Single-file Streamlit app for Hugging Face Spaces (CPU, free tier)4 5ALL code is in this one file โ€” no utils/ folder needed.6This is the most reliable way to avoid import errors on HF Spaces.7 8Pipeline:9  Upload MP4 โ†’ Extract Audio โ†’ Transcribe โ†’ Translate โ†’ TTS โ†’ Merge โ†’ Download10"""11 12import os13import gc14import math15import tempfile16import subprocess17 18import streamlit as st19 20# โ”€โ”€โ”€ Page config (MUST be first Streamlit call) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€21st.set_page_config(22    page_title="๐ŸŽฌ Movie Dubber โ€” Any Language โ†’ Hindi",23    page_icon="๐ŸŽฌ",24    layout="wide",25    initial_sidebar_state="expanded",26)27 28st.markdown("""29<style>30.stProgress > div > div { background-color: #FF4B4B; }31.seg-card {32    background: #1a1a2e;33    border-left: 4px solid #FF4B4B;34    border-radius: 8px;35    padding: 10px 14px;36    margin: 6px 0;37}38.spk { background:#FF4B4B; color:#fff; border-radius:10px;39        padding:2px 9px; font-size:0.78em; font-weight:700; }40.ts  { color:#999; font-size:0.82em; }41</style>42""", unsafe_allow_html=True)43 44 45# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•46# SECTION 1 โ€” AUDIO UTILITIES47# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•48 49def extract_audio(video_path: str) -> str:50    """51    Pull audio from video using ffmpeg.52    Returns path to a 16 kHz mono WAV โ€” ideal for Whisper.53    """54    out = os.path.join(tempfile.gettempdir(), "extracted_audio.wav")55    subprocess.run([56        "ffmpeg", "-y", "-i", video_path,57        "-vn",                   # no video58        "-acodec", "pcm_s16le",  # uncompressed WAV59        "-ar", "16000",          # 16 kHz60        "-ac", "1",              # mono61        out,62    ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)63    return out64 65 66def detect_silence_segments(audio_path: str):67    """Return list of (start_sec, end_sec) for silent / music regions."""68    from pydub import AudioSegment69    from pydub.silence import detect_nonsilent70 71    audio  = AudioSegment.from_wav(audio_path)72    speech = detect_nonsilent(audio, min_silence_len=700, silence_thresh=-40)73 74    total_ms = len(audio)75    silence, prev = [], 076    for s, e in speech:77        if s > prev + 200:78            silence.append((prev / 1000, s / 1000))79        prev = e80    if prev < total_ms - 200:81        silence.append((prev / 1000, total_ms / 1000))82    return silence83 84 85# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•86# SECTION 2 โ€” TRANSCRIPTION87# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•88 89def transcribe_audio(audio_path: str, model_size: str = "base", progress_cb=None):90    """91    Transcribe audio with faster-whisper (INT8 quantised, CPU-optimised).92    Returns list of { start, end, text, speaker, lang }.93    """94    from faster_whisper import WhisperModel95 96    if progress_cb:97        progress_cb(5, f"Loading Whisper '{model_size}'โ€ฆ")98 99    model = WhisperModel(model_size, device="cpu", compute_type="int8")100 101    if progress_cb:102        progress_cb(20, "Transcribingโ€ฆ")103 104    raw_segs, info = model.transcribe(105        audio_path,106        beam_size=3,107        vad_filter=True,108        vad_parameters={"min_silence_duration_ms": 500},109        word_timestamps=True,110    )111 112    if progress_cb:113        progress_cb(50, f"Language detected: {info.language}")114 115    segments = []116    for i, seg in enumerate(raw_segs):117        text = seg.text.strip()118        if not text:119            continue120        speaker = _heuristic_speaker(segments, seg.start)121        segments.append({122            "start":   round(seg.start, 2),123            "end":     round(seg.end, 2),124            "text":    text,125            "speaker": speaker,126            "lang":    info.language,127        })128        if progress_cb and i % 5 == 0:129            progress_cb(min(90, 50 + i * 2), f"Segment {i+1} doneโ€ฆ")130 131    del model132    gc.collect()133    return segments134 135 136def _heuristic_speaker(existing, current_start):137    """Flip speaker ID when gap > 1.5 s (lightweight alternative to pyannote)."""138    if not existing:139        return "1"140    if current_start - existing[-1]["end"] > 1.5:141        return "2" if existing[-1]["speaker"] == "1" else "1"142    return existing[-1]["speaker"]143 144 145# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•146# SECTION 3 โ€” TRANSLATION (Helsinki-NLP MarianMT)147# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•148 149_DIRECT_HI = {"en-hi": "Helsinki-NLP/opus-mt-en-hi"}150_TO_EN = {151    "de": "Helsinki-NLP/opus-mt-de-en",152    "fr": "Helsinki-NLP/opus-mt-fr-en",153    "es": "Helsinki-NLP/opus-mt-es-en",154    "it": "Helsinki-NLP/opus-mt-it-en",155    "pt": "Helsinki-NLP/opus-mt-pt-en",156    "ja": "Helsinki-NLP/opus-mt-ja-en",157    "zh": "Helsinki-NLP/opus-mt-zh-en",158    "ko": "Helsinki-NLP/opus-mt-ko-en",159    "ar": "Helsinki-NLP/opus-mt-ar-en",160    "ru": "Helsinki-NLP/opus-mt-ru-en",161    "tr": "Helsinki-NLP/opus-mt-tr-en",162    "nl": "Helsinki-NLP/opus-mt-nl-en",163    "pl": "Helsinki-NLP/opus-mt-pl-en",164}165_mt_cache = {}166 167 168def _load_mt(name):169    from transformers import MarianMTModel, MarianTokenizer170    if name not in _mt_cache:171        tok = MarianTokenizer.from_pretrained(name)172        mdl = MarianMTModel.from_pretrained(name)173        mdl.eval()174        _mt_cache[name] = (tok, mdl)175    return _mt_cache[name]176 177 178def _mt(text, model_name):179    tok, mdl = _load_mt(model_name)180    inp = tok([text], return_tensors="pt", padding=True,181              truncation=True, max_length=512)182    out = mdl.generate(**inp, num_beams=2, max_length=512)183    return tok.decode(out[0], skip_special_tokens=True).strip()184 185 186def translate_to_hindi(text: str, source_lang: str = "en") -> str:187    """Translate text โ†’ Hindi. Pivots through English if no direct model."""188    if not text.strip() or source_lang == "hi":189        return text190    src = source_lang.lower()191    # Direct path192    if f"{src}-hi" in _DIRECT_HI:193        try:194            return _mt(text, _DIRECT_HI[f"{src}-hi"])195        except Exception:196            pass197    # Pivot: src โ†’ English198    if src != "en" and src in _TO_EN:199        try:200            text = _mt(text, _TO_EN[src])201        except Exception:202            pass203    # English โ†’ Hindi204    try:205        return _mt(text, _DIRECT_HI["en-hi"])206    except Exception:207        return f"[Translation failed: {text}]"208 209 210# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•211# SECTION 4 โ€” HINDI TTS (gTTS โ€” free, instant, no GPU)212# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•213 214# โ”€โ”€ Voice options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€215# gTTS uses Google's TTS via HTTP. Different TLDs give subtly different voices.216# We label them Male/Female as a UX convenience; the underlying difference is217# accent/server โ€” gTTS doesn't expose true gender control, but the variation218# is clearly audible and useful for multi-speaker dubbing.219VOICE_OPTIONS = {220    "Male 1 (Standard)":    "com",       # US/international server221    "Male 2 (Indian)":      "co.in",     # Indian English server222    "Female 1 (UK)":        "co.uk",     # UK server โ€” higher pitch223    "Female 2 (Australia)": "com.au",    # Australian server224    "Default":              "com",       # fallback225}226 227# Legacy alias kept so any existing code using SPEAKER_OPTIONS still works228SPEAKER_OPTIONS = VOICE_OPTIONS229 230 231def _text_to_wav(text: str, tld: str, out_path: str):232    """Hindi text โ†’ WAV via gTTS (MP3 intermediate)."""233    from gtts import gTTS234    from pydub import AudioSegment235 236    if not text.strip():237        AudioSegment.silent(500).export(out_path, format="wav")238        return239 240    mp3 = out_path.replace(".wav", ".mp3")241    gTTS(text=text, lang="hi", tld=tld, slow=False).save(mp3)242    AudioSegment.from_mp3(mp3).export(out_path, format="wav")243    try:244        os.remove(mp3)245    except Exception:246        pass247 248 249def _fit_to_duration(speech, target_ms):250    """Speed-fit TTS audio to match original segment duration."""251    from pydub import AudioSegment252    dur = len(speech)253    if not dur or not target_ms:254        return speech255    ratio = dur / target_ms256    if ratio > 1.3:257        mult = min(ratio, 1.8)258        speech = speech._spawn(259            speech.raw_data,260            overrides={"frame_rate": int(speech.frame_rate * mult)},261        ).set_frame_rate(speech.frame_rate)262    elif dur < target_ms:263        speech = speech + AudioSegment.silent(target_ms - dur)264    return speech265 266 267def generate_hindi_speech(268    segments,269    speaker="com",270    speaker_voice_map=None,271    speech_volume=1.2,272    progress_cb=None,273):274    """275    Generate full dubbed audio track.276 277    Supports per-speaker voices via speaker_voice_map:278        { "1": "com", "2": "co.uk", ... }279 280    If speaker_voice_map is None, all segments use `speaker` (tld string).281    Each segment โ†’ TTS with its speaker's voice โ†’ speed-fit โ†’ overlaid at282    correct timestamp on a silent base track.283    """284    from pydub import AudioSegment285 286    total_ms = int(max(s["end"] for s in segments) * 1000) + 2000287    track    = AudioSegment.silent(duration=total_ms)288    tmp, n   = tempfile.gettempdir(), len(segments)289 290    for i, seg in enumerate(segments):291        if progress_cb:292            progress_cb(int((i + 1) / n * 90), f"Synthesising {i+1}/{n}โ€ฆ")293 294        hindi = seg.get("hindi_text", "").strip()295        if not hindi:296            continue297 298        # Pick voice: per-speaker map โ†’ fallback to global speaker tld299        spk_id = str(seg.get("speaker", "1"))300        if speaker_voice_map and spk_id in speaker_voice_map:301            tld = speaker_voice_map[spk_id]302        else:303            tld = speaker304 305        start_ms  = int(seg["start"] * 1000)306        target_ms = int(seg["end"] * 1000) - start_ms307        wav = os.path.join(tmp, f"seg_{i:04d}.wav")308 309        try:310            _text_to_wav(hindi, tld, wav)311        except Exception:312            continue313 314        speech = AudioSegment.from_wav(wav)315        if speech_volume != 1.0:316            speech = speech + (20 * math.log10(max(speech_volume, 0.01)))317        speech = _fit_to_duration(speech, target_ms)318        track  = track.overlay(speech, position=start_ms)319 320        try:321            os.remove(wav)322        except Exception:323            pass324        gc.collect()325 326    out = os.path.join(tmp, "hindi_dubbed_audio.wav")327    track.export(out, format="wav")328    return out329 330 331# โ”€โ”€ Transcription download helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€332 333def build_transcription_txt(segments: list) -> str:334    """335    Format transcribed segments into a readable .txt file.336 337    Output format per segment:338        [MM:SS - MM:SS] Speaker N:339        Original text340 341    Easy to read, copy, and share.342    """343    lines = []344    for seg in segments:345        start_m, start_s = divmod(int(seg["start"]), 60)346        end_m,   end_s   = divmod(int(seg["end"]),   60)347        ts = f"[{start_m:02d}:{start_s:02d} - {end_m:02d}:{end_s:02d}]"348        lines.append(f"{ts} Speaker {seg['speaker']}:")349        lines.append(seg["text"])350        lines.append("")   # blank line between segments351    return "\n".join(lines)352 353 354# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•355# SECTION 5 โ€” VIDEO MERGE (ffmpeg, no video re-encoding)356# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•357 358def merge_audio_video(video_path, dubbed_audio_path, bgm_volume=0.15):359    """360    Mix Hindi speech with faint original BGM, then mux into video.361    Video stream is COPIED (no re-encode) โ†’ fast even on CPU.362    """363    out = os.path.join(tempfile.gettempdir(), "hindi_dubbed_output.mp4")364    bgm = str(round(bgm_volume, 3))365 366    result = subprocess.run([367        "ffmpeg", "-y",368        "-i", video_path,369        "-i", dubbed_audio_path,370        "-filter_complex",371        f"[0:a]volume={bgm}[b];[b][1:a]amix=inputs=2:duration=first[mix]",372        "-map", "0:v", "-map", "[mix]",373        "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-shortest",374        out,375    ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)376 377    if result.returncode != 0:378        # Fallback: replace audio entirely (skip BGM blend)379        fallback = os.path.join(tempfile.gettempdir(), "dubbed_fallback.mp4")380        subprocess.run([381            "ffmpeg", "-y",382            "-i", video_path, "-i", dubbed_audio_path,383            "-map", "0:v", "-map", "1:a",384            "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-shortest",385            fallback,386        ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)387        return fallback388 389    return out390 391 392# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•393# SECTION 6 โ€” SMART AUTO-ASSIGN394# Splits a Hindi dubbing text block and maps lines โ†’ segments395# Rule-based only โ€” no ML models, fully CPU-safe396# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•397 398import re399 400# Emotion keyword lists for simple rule-based detection401_EMOTION_RULES = {402    "๐Ÿ˜  Angry":  ["เค—เฅเคธเฅเคธเคพ", "เค•เฅเคฐเฅ‹เคง", "เคšเคฟเคฒเฅเคฒเคพเค“", "เคฎเคค เค•เคฐเฅ‹", "เคฌเค‚เคฆ เค•เคฐเฅ‹", "เคจเคนเฅ€เค‚", "เค•เคญเฅ€ เคจเคนเฅ€เค‚"],403    "๐Ÿ’• Soft":   ["เคชเฅเคฏเคพเคฐ", "เคถเคพเค‚เคค", "เคงเฅ€เคฐเฅ‡", "เคฎเคพเคซ", "เคฆเฅเค–", "เคฐเฅ‹", "เค†เคเคธเฅ‚"],404    "๐Ÿ˜„ Happy":  ["เค–เฅเคถเฅ€", "เคนเคพเคนเคพเคนเคพ", "เคฌเคขเคผเคฟเคฏเคพ", "เคถเคพเคฌเคพเคถ", "เคตเคพเคน", "เคฎเคœเคผเคพ", "เคœเฅ€เคค"],405    "๐Ÿ˜ Neutral": [],  # default406}407 408def _detect_emotion(text: str) -> str:409    """Return emotion label for a Hindi line โ€” pure keyword matching, no model."""410    for label, keywords in _EMOTION_RULES.items():411        if any(kw in text for kw in keywords):412            return label413    return "๐Ÿ˜ Neutral"414 415 416def _split_hindi_text(raw: str) -> list:417    """418    Split Hindi paragraph into sentences.419    Splits on: newline, Hindi danda (เฅค), ? !420    Strips whitespace and drops empty lines.421    """422    normalized = re.sub(r"[เฅค?!\n]+", "\n", raw)423    return [ln.strip() for ln in normalized.splitlines() if ln.strip()]424 425 426def _merge_short_lines(lines: list, target: int) -> list:427    """If we have MORE lines than segments, merge the shortest adjacent pairs."""428    lines = list(lines)429    while len(lines) > target:430        # Find index of shortest line that has a neighbour431        idx = min(range(len(lines) - 1), key=lambda i: len(lines[i]))432        merged = lines[idx] + " " + lines[idx + 1]433        lines = lines[:idx] + [merged] + lines[idx + 2:]434    return lines435 436 437def _split_long_lines(lines: list, target: int) -> list:438    """If we have FEWER lines than segments, split the longest line at midpoint."""439    lines = list(lines)440    while len(lines) < target:441        idx = max(range(len(lines)), key=lambda i: len(lines[i]))442        line = lines[idx]443        mid = len(line) // 2444 445        # Find a natural break near the middle (comma or space)446        split_at = -1447        for offset in range(mid):448            for pos in [mid - offset, mid + offset]:449                if 0 < pos < len(line) and line[pos] in "ุŒ, ":450                    split_at = pos451                    break452            if split_at != -1:453                break454        if split_at == -1:455            split_at = mid  # fallback: hard split at middle456 457        p1 = line[:split_at].strip()458        p2 = line[split_at:].strip().lstrip("ุŒ, ")459        if not p1 or not p2:460            break  # can't split further461 462        lines = lines[:idx] + [p1, p2] + lines[idx + 1:]463    return lines464 465 466def auto_assign_hindi_text(segments: list, hindi_text: str) -> list:467    """468    Split a block of Hindi dubbing text and assign each line469    to the matching segment, preserving speaker + timestamps.470 471    Steps:472      1. Split text into lines on newline / danda / ? / !473      2. Adjust line count to match segment count:474         - Too many lines โ†’ merge shortest adjacent pairs475         - Too few lines  โ†’ split longest lines at midpoint476      3. Assign one line per segment477      4. Detect emotion per line (keyword-based, no model)478    """479    lines = _split_hindi_text(hindi_text)480    n = len(segments)481    if not lines:482        return segments483 484    if len(lines) > n:485        lines = _merge_short_lines(lines, n)486    elif len(lines) < n:487        lines = _split_long_lines(lines, n)488 489    # Safety pad490    while len(lines) < n:491        lines.append("")492 493    return [494        {**seg, "hindi_text": lines[i] if i < len(lines) else "",495         "emotion": _detect_emotion(lines[i] if i < len(lines) else "")}496        for i, seg in enumerate(segments)497    ]498 499 500# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•501# STREAMLIT UI502# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•503 504# Session state505for k in ["audio_path", "segments", "translated_segments",506          "dubbed_audio_path", "output_video_path", "speaker_voice_map"]:507    if k not in st.session_state:508        st.session_state[k] = None509 510# โ”€โ”€ Sidebar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€511with st.sidebar:512    st.title("โš™๏ธ Settings")513 514    st.subheader("๐ŸŽ™๏ธ Transcription")515    whisper_model = st.selectbox(516        "Whisper model",517        ["tiny", "base", "small"],518        index=1,519        help="tiny=fastest | base=balanced โœ… | small=most accurate",520    )521 522    st.subheader("๐ŸŽš๏ธ Audio Mix")523    bgm_volume    = st.slider("Background music volume", 0.0, 1.0, 0.15, 0.05)524    speech_volume = st.slider("Hindi speech volume",     0.5, 2.0, 1.2,  0.1)525 526    st.subheader("โ„น๏ธ Stack")527    st.caption("faster-whisper ยท Helsinki-NLP ยท gTTS ยท ffmpeg\n100% free ยท CPU ยท Open-source")528 529# โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€530st.title("๐ŸŽฌ Movie Dubber")531st.markdown("**Upload any video โ†’ Get Hindi dubbed output โ€” 100% free**")532 533# STEP 1 โ€” Upload534st.header("Step 1 โ€” Upload Video")535uploaded = st.file_uploader(536    "MP4 / AVI / MKV / MOV (keep under ~200 MB for CPU tier)",537    type=["mp4", "avi", "mkv", "mov"],538)539 540if uploaded:541    video_path = os.path.join(tempfile.gettempdir(), uploaded.name)542    with open(video_path, "wb") as f:543        f.write(uploaded.read())544 545    st.video(video_path)546    st.success(f"โœ… {uploaded.name} ({os.path.getsize(video_path)/1e6:.1f} MB)")547 548    # STEP 2 โ€” Extract Audio549    st.header("Step 2 โ€” Extract Audio")550    if st.button("๐Ÿ”Š Extract Audio", use_container_width=True):551        with st.spinner("Running ffmpegโ€ฆ"):552            bar  = st.progress(0, "Extractingโ€ฆ")553            apth = extract_audio(video_path)554            bar.progress(70, "Detecting silenceโ€ฆ")555            sil  = detect_silence_segments(apth)556            bar.progress(100, "Done!")557            st.session_state.audio_path = apth558 559        st.audio(apth)560        st.success("โœ… Audio extracted!")561        if sil:562            with st.expander(f"๐Ÿ”‡ {len(sil)} silence segment(s)"):563                for s, e in sil[:10]:564                    st.caption(f"  {s:.1f}s โ†’ {e:.1f}s")565 566    # STEP 3 โ€” Transcribe567    if st.session_state.audio_path:568        st.header("Step 3 โ€” Transcribe Speech")569        st.info(f"Whisper **{whisper_model}** on CPU โ€” ~1โ€“3 min/min of audio. โ˜•")570 571        if st.button("๐Ÿ“ Transcribe", use_container_width=True):572            bar  = st.progress(0, "Loadingโ€ฆ")573            segs = transcribe_audio(574                st.session_state.audio_path,575                model_size=whisper_model,576                progress_cb=lambda p, t: bar.progress(p, t),577            )578            bar.progress(100, "Done!")579            st.session_state.segments = segs580            st.session_state.translated_segments = [{**s, "hindi_text": ""} for s in segs]581            st.session_state.speaker_voice_map = None  # reset on new transcription582            st.success(f"โœ… {len(segs)} segments found!")583 584        # โ”€โ”€ Download transcription as TXT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€585        if st.session_state.segments:586            txt_content = build_transcription_txt(st.session_state.segments)587            st.download_button(588                label="โฌ‡๏ธ Download Transcription (.txt)",589                data=txt_content.encode("utf-8"),590                file_name="transcription.txt",591                mime="text/plain",592                use_container_width=True,593            )594 595        # โ”€โ”€ Speaker voice assignment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€596        if st.session_state.segments:597            st.subheader("๐ŸŽ™๏ธ Assign Voices to Speakers")598            st.caption(599                "Each detected speaker can have a different Hindi voice. "600                "Changes take effect when you generate Hindi speech."601            )602 603            # Find unique speaker IDs in order of first appearance604            seen, unique_speakers = set(), []605            for seg in st.session_state.segments:606                spk = str(seg.get("speaker", "1"))607                if spk not in seen:608                    seen.add(spk)609                    unique_speakers.append(spk)610 611            voice_names = list(VOICE_OPTIONS.keys())612 613            # Default assignments: alternate Male 1 / Female 1 for first two,614            # then Default for any extras615            defaults = ["Male 1 (Standard)", "Female 1 (UK)", "Male 2 (Indian)",616                        "Female 2 (Australia)", "Default"]617 618            voice_map = {}  # spk_id โ†’ tld string619            cols = st.columns(min(len(unique_speakers), 3))620            for idx, spk_id in enumerate(unique_speakers):621                col = cols[idx % len(cols)]622                with col:623                    default_voice = defaults[idx] if idx < len(defaults) else "Default"624                    chosen = st.selectbox(625                        f"Speaker {spk_id} voice",626                        voice_names,627                        index=voice_names.index(default_voice),628                        key=f"voice_spk_{spk_id}",629                    )630                    voice_map[spk_id] = VOICE_OPTIONS[chosen]631 632            if st.button("๐Ÿ’พ Save Voice Assignments", use_container_width=True):633                st.session_state.speaker_voice_map = voice_map634                st.success(635                    "โœ… Voice assignments saved! "636                    + " | ".join(637                        f"Speaker {k} โ†’ {[n for n,v in VOICE_OPTIONS.items() if v==tld][0]}"638                        for k, tld in voice_map.items()639                    )640                )641 642    # STEP 4 โ€” Translate643    if st.session_state.segments:644        st.header("Step 4 โ€” Translate to Hindi")645 646        tab_auto, tab_machine = st.tabs([647            "โœ๏ธ Write Hindi Yourself (Smart Assign)",648            "๐Ÿค– Auto Machine-Translate",649        ])650 651        # โ”€โ”€ Tab A: Smart assign from user's own Hindi block โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€652        with tab_auto:653            st.markdown(654                "Write your full Hindi dubbing script in **one box** โ€” "655                "the app will split it and assign each line to the right speaker automatically."656            )657            st.markdown(658                "**Supported separators:** newline ยท Hindi danda `เฅค` ยท `?` ยท `!`"659            )660 661            # Show original lines for reference662            with st.expander("๐Ÿ“„ Show original transcribed lines for reference"):663                for seg in st.session_state.segments:664                    st.caption(665                        f"[{seg['start']:.1f}s โ†’ {seg['end']:.1f}s] "666                        f"Speaker {seg['speaker']}: {seg['text']}"667                    )668 669            hindi_block = st.text_area(670                "Write full Hindi dubbing text here",671                height=250,672                placeholder=(673                    "เค…เคฐเฅ‡ เคธเฅเคจเฅ‹, เคคเฅเคฎ เค•เคนเคพเค เคœเคพ เคฐเคนเฅ‡ เคนเฅ‹?\n"674                    "เคฎเฅˆเค‚ เคฌเคพเคœเคพเคฐ เคœเคพ เคฐเคนเคพ เคนเฅ‚เคเฅค\n"675                    "เค เฅ€เค• เคนเฅˆ, เคœเคฒเฅเคฆเฅ€ เค†เคจเคพเฅค"676                ),677                key="hindi_block_input",678            )679 680            n_segs  = len(st.session_state.segments)681            n_lines = len(_split_hindi_text(hindi_block)) if hindi_block.strip() else 0682 683            # Live line-count feedback684            if hindi_block.strip():685                if n_lines == n_segs:686                    st.success(f"โœ… {n_lines} lines detected โ€” perfect match with {n_segs} segments!")687                elif n_lines > n_segs:688                    st.warning(689                        f"โš ๏ธ {n_lines} lines, {n_segs} segments โ€” "690                        f"{n_lines - n_segs} extra line(s) will be merged automatically."691                    )692                else:693                    st.warning(694                        f"โš ๏ธ {n_lines} lines, {n_segs} segments โ€” "695                        f"{n_segs - n_lines} missing line(s) will be split automatically."696                    )697 698            if st.button("๐ŸŽฏ Auto Assign to Speakers", use_container_width=True,699                         disabled=not hindi_block.strip()):700                assigned = auto_assign_hindi_text(701                    st.session_state.segments, hindi_block702                )703                st.session_state.translated_segments = assigned704                st.success("โœ… Hindi text assigned to all speakers!")705 706                # Preview result707                st.markdown("#### Assignment preview")708                for seg in assigned:709                    emotion = seg.get("emotion", "")710                    st.markdown(711                        f'<div class="seg-card">'712                        f'<span class="spk">Speaker {seg["speaker"]}</span> '713                        f'<span class="ts">{seg["start"]:.1f}s โ†’ {seg["end"]:.1f}s</span> '714                        f'<span style="font-size:0.8em;color:#aaa;">{emotion}</span><br>'715                        f'{seg["hindi_text"]}'716                        f'</div>',717                        unsafe_allow_html=True,718                    )719 720        # โ”€โ”€ Tab B: Machine translation (original behaviour) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€721        with tab_machine:722            st.info(723                "Automatically translates each segment using Helsinki-NLP MarianMT. "724                "Free, offline, CPU-only."725            )726            if st.button("๐ŸŒ Auto Translate All Segments", use_container_width=True):727                segs = st.session_state.segments728                bar  = st.progress(0, "Loading translation modelโ€ฆ")729                out  = []730                for i, seg in enumerate(segs):731                    bar.progress(int((i+1)/len(segs)*100), f"Translating {i+1}/{len(segs)}โ€ฆ")732                    out.append({**seg, "hindi_text": translate_to_hindi(733                        seg["text"], source_lang=seg.get("lang", "en")734                    )})735                    gc.collect()736                st.session_state.translated_segments = out737                st.success("โœ… Translation done!")738 739    # STEP 5 โ€” Edit740    if st.session_state.translated_segments and any(741        s.get("hindi_text") for s in st.session_state.translated_segments742    ):743        st.header("Step 5 โ€” Review & Edit Translations")744        st.caption("Fix Hindi text before generating speech.")745 746        edited = []747        for i, seg in enumerate(st.session_state.translated_segments):748            c1, c2 = st.columns(2)749            with c1:750                st.markdown(751                    f'<div class="seg-card">'752                    f'<span class="spk">Speaker {seg.get("speaker","?")}</span> '753                    f'<span class="ts">{seg["start"]:.1f}sโ†’{seg["end"]:.1f}s</span><br>'754                    f'<b>Original:</b> {seg["text"]}</div>',755                    unsafe_allow_html=True,756                )757            with c2:758                new = st.text_area(759                    f"Hindi #{i+1}", value=seg.get("hindi_text",""),760                    key=f"hi_{i}", height=100, label_visibility="collapsed",761                )762            edited.append({**seg, "hindi_text": new})763 764        if st.button("๐Ÿ’พ Save Edits", use_container_width=True):765            st.session_state.translated_segments = edited766            st.success("โœ… Saved!")767 768        # STEP 6 โ€” TTS769        st.header("Step 6 โ€” Generate Hindi Speech")770        if st.button("๐ŸŽค Generate Hindi Audio", use_container_width=True):771            bar  = st.progress(0, "Startingโ€ฆ")772            dpth = generate_hindi_speech(773                st.session_state.translated_segments,774                speaker="com",                                # global fallback tld775                speaker_voice_map=st.session_state.speaker_voice_map,776                speech_volume=speech_volume,777                progress_cb=lambda p, t: bar.progress(p, t),778            )779            bar.progress(100, "Done!")780            st.session_state.dubbed_audio_path = dpth781            st.audio(dpth)782            st.success("โœ… Hindi audio ready!")783 784        # STEP 7 โ€” Merge785        if st.session_state.dubbed_audio_path:786            st.header("Step 7 โ€” Merge & Download")787            if st.button("๐ŸŽฌ Create Dubbed Video", use_container_width=True):788                with st.spinner("Mergingโ€ฆ"):789                    out_path = merge_audio_video(790                        video_path=video_path,791                        dubbed_audio_path=st.session_state.dubbed_audio_path,792                        bgm_volume=bgm_volume,793                    )794                    st.session_state.output_video_path = out_path795 796                st.video(out_path)797                st.success("๐ŸŽ‰ Done!")798                with open(out_path, "rb") as f:799                    st.download_button(800                        "โฌ‡๏ธ Download Hindi Dubbed Video",801                        data=f,802                        file_name="hindi_dubbed.mp4",803                        mime="video/mp4",804                        use_container_width=True,805                    )806 807else:808    st.markdown("""809    ---810    ### How it works811 812    | Step | What happens |813    |------|-------------|814    | 1๏ธโƒฃ Upload | Any MP4 / AVI / MKV video |815    | 2๏ธโƒฃ Extract | ffmpeg pulls the audio track |816    | 3๏ธโƒฃ Transcribe | Whisper converts speech โ†’ text with timestamps |817    | 4๏ธโƒฃ Translate | MarianMT translates โ†’ natural Hindi |818    | 5๏ธโƒฃ Edit | Fix any translation manually |819    | 6๏ธโƒฃ Speak | gTTS generates Hindi audio per segment |820    | 7๏ธโƒฃ Merge | ffmpeg mixes Hindi audio back into video |821 822    > โš ๏ธ On CPU: transcription takes ~1โ€“3 min per minute of video. Grab a chai โ˜•823    """)824