suraj-ml-projects/Video_editing
0
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 