""" utils/tts_utils.py ────────────────── Hindi Text-to-Speech using gTTS (Google TTS via HTTP — free, no API key). Why gTTS over Coqui? - Coqui TTS Hindi models are large (1–2GB) and slow on CPU - gTTS is instant, free, and produces natural Hindi speech - No model download, no GPU needed - Works perfectly on Hugging Face Spaces Architecture: - Each translated segment → individual Hindi MP3 → WAV - Silence gaps are inserted between segments to match original timing - All segment WAVs are concatenated into one full dub track """ import os import gc import tempfile from typing import List, Dict, Callable, Optional from gtts import gTTS from pydub import AudioSegment # ── Available speaker "voices" ─────────────────────────────────────────────── # gTTS supports TLD variants which subtly affect the accent/voice SPEAKER_OPTIONS = { "Hindi (India — Standard)": "com", "Hindi (India — Alternate)": "co.in", } def _text_to_wav(text: str, tld: str, out_path: str, speed_factor: float = 1.0) -> str: """ Convert a Hindi text string to a WAV audio file. Parameters: text – Hindi text tld – gTTS top-level domain for voice accent out_path – where to save the WAV speed_factor – NOT used by gTTS directly; we use pydub speedup instead """ if not text.strip(): # Return a short silence for empty segments silence = AudioSegment.silent(duration=500) silence.export(out_path, format="wav") return out_path # Generate TTS (outputs MP3) mp3_path = out_path.replace(".wav", ".mp3") tts = gTTS(text=text, lang="hi", tld=tld, slow=False) tts.save(mp3_path) # Convert MP3 → WAV for consistent processing audio = AudioSegment.from_mp3(mp3_path) audio.export(out_path, format="wav") # Clean up MP3 try: os.remove(mp3_path) except Exception: pass return out_path def _fit_speech_to_duration(speech: AudioSegment, target_duration_ms: int) -> AudioSegment: """ Speed up or slow down speech to (roughly) fit the original segment duration. Strategy: - If speech is too long (> 130% of target), speed it up moderately - If speech is too short, pad with trailing silence - We don't slow down speech (sounds unnatural) — just pad instead This is the lightweight alternative to full lip-sync. """ speech_dur = len(speech) if speech_dur == 0 or target_duration_ms == 0: return speech ratio = speech_dur / target_duration_ms if ratio > 1.3: # Speed up: increase frame rate (pitch changes too, but acceptable) # pydub speedup: multiply frame_rate speed_multiplier = min(ratio, 1.8) # cap at 1.8× to stay intelligible sped = speech._spawn( speech.raw_data, overrides={"frame_rate": int(speech.frame_rate * speed_multiplier)}, ).set_frame_rate(speech.frame_rate) return sped elif speech_dur < target_duration_ms: # Pad with silence at end pad = AudioSegment.silent(duration=target_duration_ms - speech_dur) return speech + pad return speech def generate_hindi_speech( segments: List[Dict], speaker: str = "com", speech_volume: float = 1.2, progress_callback: Optional[Callable] = None, ) -> str: """ Generate a full Hindi dubbed audio track from translated segments. Process: 1. For each segment, generate Hindi TTS audio 2. Speed-fit to match original segment duration 3. Insert silence between segments to preserve original timing 4. Concatenate everything into one WAV file Returns path to the final dubbed WAV file. """ if not segments: raise ValueError("No segments to synthesise") # Figure out total audio duration from last segment end time total_duration_ms = int(max(s["end"] for s in segments) * 1000) + 2000 # Start with a silent base track (we'll fill it with speech) full_track = AudioSegment.silent(duration=total_duration_ms) temp_dir = tempfile.gettempdir() n = len(segments) for i, seg in enumerate(segments): if progress_callback: progress_callback( int((i + 1) / n * 90), f"Synthesising segment {i+1}/{n}…" ) hindi_text = seg.get("hindi_text", "").strip() if not hindi_text: continue # skip segments with no translation # Target duration in milliseconds seg_start_ms = int(seg["start"] * 1000) seg_end_ms = int(seg["end"] * 1000) target_ms = seg_end_ms - seg_start_ms # Generate speech WAV for this segment wav_path = os.path.join(temp_dir, f"seg_{i:04d}.wav") try: _text_to_wav(hindi_text, tld=speaker, out_path=wav_path) except Exception as e: # If TTS fails for a segment, just leave silence there continue # Load generated speech speech = AudioSegment.from_wav(wav_path) # Optionally boost volume if speech_volume != 1.0: import math db_boost = 20 * math.log10(speech_volume) speech = speech + db_boost # Fit speech duration to original segment duration speech = _fit_speech_to_duration(speech, target_ms) # Overlay speech onto the silent base track at the right position full_track = full_track.overlay(speech, position=seg_start_ms) # Clean up temp file try: os.remove(wav_path) except Exception: pass gc.collect() # Export final dubbed track output_path = os.path.join(temp_dir, "hindi_dubbed_audio.wav") full_track.export(output_path, format="wav") return output_path