Video_editing / tts_utils.py
suraj-ml-projects's picture
Upload 5 files
4383d5e verified
Raw History Blame Contribute Delete
6 kB
"""
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