Spaces:
Running
Running
Download tts_utils.py from suraj-ml-projects/Video_editing: direct link, hf CLI and curl.
- Browser
- Download file 6 kB
-
https://huggingface.co/spaces/suraj-ml-projects/Video_editing/resolve/main/tts_utils.py
- Command line
-
hf download hf://spaces/suraj-ml-projects/Video_editing/tts_utils.py
-
curl -L -o tts_utils.py https://huggingface.co/spaces/suraj-ml-projects/Video_editing/resolve/main/tts_utils.py
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 | |