DoodleBook / indic_tts.py
Codex
Fix ZeroGPU model re-download loop causing browser freeze
782f6e2
Raw
History Blame
4.61 kB
"""Kannada TTS β€” primary: sush0401/IndicF5-Kannada-Bedtime-v2 (fine-tuned, non-gated).
Fallback: facebook/mms-tts-kan (VITS, 16kHz, no voice cloning).
ZeroGPU pattern: models loaded to CPU at module scope so ZeroGPU packs their
tensors. Inside inference functions (called from @spaces.GPU), .to("cuda") is
called and ZeroGPU transfers packed tensors to GPU β€” no re-download needed.
"""
from __future__ import annotations
import os
import re
import tempfile
import numpy as np
import torch
_FINETUNE_HUB = "sush0401/IndicF5-Kannada-Bedtime-v2"
_FALLBACK_HUB = "facebook/mms-tts-kan"
_indic_model = None # IndicF5 fine-tuned (with voice cloning)
_mms_model = None # MMS-TTS fallback (no voice cloning)
_mms_tok = None
_use_indic = False # set True after successful IndicF5 load
MMS_SR = 16_000
def _load_indic():
global _indic_model, _use_indic
from transformers import AutoModel
# Load to CPU β€” ZeroGPU packs these tensors.
_indic_model = AutoModel.from_pretrained(
_FINETUNE_HUB, trust_remote_code=True,
)
_use_indic = True
def _load_mms():
global _mms_model, _mms_tok
from transformers import VitsModel, AutoTokenizer
_mms_tok = AutoTokenizer.from_pretrained(_FALLBACK_HUB)
# Load to CPU β€” ZeroGPU packs these tensors.
_mms_model = VitsModel.from_pretrained(_FALLBACK_HUB)
def _get_model():
if not _use_indic and _mms_model is None:
try:
_load_indic()
except Exception:
_load_mms()
return _use_indic
# Pre-load to CPU at module scope so ZeroGPU packs the tensors.
try:
_get_model()
except Exception:
pass
def _split(text: str, max_chars: int = 200):
parts = re.split(r"(?<=[.!?ΰ₯€])\s+|\n+", text.strip())
out = []
for p in parts:
p = p.strip()
if not p:
continue
while len(p) > max_chars:
cut = p.rfind(" ", 0, max_chars)
cut = cut if cut > 0 else max_chars
out.append(p[:cut].strip())
p = p[cut:].strip()
out.append(p)
return out or [text.strip()]
def _narrate_indic(ref_wav: str, kannada_text: str) -> tuple[np.ndarray, int]:
# Move to GPU β€” ZeroGPU intercepts this inside @spaces.GPU.
model = _indic_model.to("cuda")
silence_sr = 24_000
silence = np.zeros(int(0.55 * silence_sr), dtype=np.float32)
chunks = []
for sent in _split(kannada_text):
kw = dict(ref_audio_path=ref_wav) if ref_wav and os.path.exists(ref_wav) else {}
audio = model(sent, **kw)
audio = np.asarray(audio, dtype=np.float32)
if audio.size and float(np.max(np.abs(audio))) > 1.0:
audio = audio / 32768.0
if audio.size:
chunks.append(audio)
chunks.append(silence)
return np.concatenate(chunks) if chunks else np.array([], dtype=np.float32), silence_sr
def _narrate_mms(kannada_text: str) -> tuple[np.ndarray, int]:
# Move to GPU β€” ZeroGPU intercepts this inside @spaces.GPU.
model = _mms_model.to("cuda")
silence = np.zeros(int(0.55 * MMS_SR), dtype=np.float32)
chunks = []
for sent in _split(kannada_text):
inputs = _mms_tok(sent, return_tensors="pt").to(model.device)
with torch.no_grad():
wav = model(**inputs).waveform
audio = wav.squeeze().cpu().float().numpy()
if audio.size:
chunks.append(audio)
chunks.append(silence)
full = np.concatenate(chunks) if chunks else np.array([], dtype=np.float32)
return full, MMS_SR
def narrate_kannada(ref_wav: str, ref_text: str, kannada_text: str,
mood: str = "", energy: float = 0.45) -> str:
"""Narrate Kannada text. Uses fine-tuned IndicF5 with voice cloning if available,
otherwise MMS-TTS-Kan (generic voice). Returns a temp WAV path."""
text = (kannada_text or "").strip()
if not text:
raise ValueError("Please provide Kannada text to narrate.")
use_indic = _get_model()
if use_indic:
try:
full, sr = _narrate_indic(ref_wav or "", text)
except Exception:
if _mms_model is None:
_load_mms()
full, sr = _narrate_mms(text)
else:
full, sr = _narrate_mms(text)
if not full.size:
raise RuntimeError("TTS produced no audio.")
peak = float(np.max(np.abs(full)))
if peak > 0:
full = full / peak * 0.92
import soundfile as sf
fd, path = tempfile.mkstemp(prefix="bedtime_kn_", suffix=".wav")
os.close(fd)
sf.write(path, full, sr)
return path