File size: 6,308 Bytes
e0d2309 782f6e2 e0d2309 782f6e2 44cee7a 56412a3 e0d2309 56412a3 e0d2309 44cee7a e0d2309 782f6e2 44cee7a 782f6e2 44cee7a e0d2309 44cee7a e0d2309 44cee7a 782f6e2 e0d2309 56412a3 44cee7a 56412a3 44cee7a e0d2309 44cee7a 56412a3 782f6e2 56412a3 44cee7a 56412a3 782f6e2 e0d2309 44cee7a 56412a3 44cee7a 56412a3 e0d2309 44cee7a e0d2309 44cee7a e0d2309 782f6e2 44cee7a 782f6e2 44cee7a e0d2309 44cee7a e0d2309 44cee7a e0d2309 44cee7a e0d2309 44cee7a e0d2309 44cee7a e0d2309 44cee7a e0d2309 56412a3 44cee7a 56412a3 44cee7a 56412a3 44cee7a 56412a3 44cee7a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """Kannada TTS β tier 1: sush0401/IndicF5-Kannada-Bedtime-v2 (fine-tuned).
Tier 2: facebook/mms-tts-kan (VITS, 16kHz, no voice cloning).
Tier 3: gTTS (Google, always works, generic Kannada voice).
ZeroGPU pattern: GPU 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 logging
import os
import re
import tempfile
import numpy as np
import torch
logger = logging.getLogger(__name__)
_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
logger.info(f"Loading IndicF5 from {_FINETUNE_HUB}")
# Load to CPU β ZeroGPU packs these tensors.
_indic_model = AutoModel.from_pretrained(
_FINETUNE_HUB, trust_remote_code=True,
)
_use_indic = True
logger.info("IndicF5 loaded OK")
def _load_mms():
global _mms_model, _mms_tok
from transformers import VitsModel, AutoTokenizer
logger.info(f"Loading MMS-TTS from {_FALLBACK_HUB}")
_mms_tok = AutoTokenizer.from_pretrained(_FALLBACK_HUB)
# Load to CPU β ZeroGPU packs these tensors.
_mms_model = VitsModel.from_pretrained(_FALLBACK_HUB)
logger.info("MMS-TTS loaded OK")
def _get_model():
if not _use_indic and _mms_model is None:
try:
_load_indic()
except Exception as e:
logger.warning(f"IndicF5 load failed ({e}); trying MMS-TTS")
try:
_load_mms()
except Exception as e2:
logger.warning(f"MMS-TTS load failed too ({e2}); will use gTTS fallback")
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]:
model = _indic_model.to("cuda") # ZeroGPU intercepts inside @spaces.GPU
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]:
model = _mms_model.to("cuda") # ZeroGPU intercepts inside @spaces.GPU
silence = np.zeros(int(0.55 * MMS_SR), dtype=np.float32)
chunks = []
for sent in _split(kannada_text):
if not sent.strip():
continue
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_gtts(kannada_text: str) -> tuple[np.ndarray, int]:
"""Last-resort: gTTS Kannada (generic Google voice, no GPU needed)."""
import io
import librosa
from gtts import gTTS
logger.info("Using gTTS Kannada fallback")
tts = gTTS(text=kannada_text, lang="kn", slow=True)
fd, mp3_path = tempfile.mkstemp(suffix=".mp3")
os.close(fd)
try:
tts.save(mp3_path)
data, sr = librosa.load(mp3_path, sr=22050, mono=True)
finally:
try:
os.unlink(mp3_path)
except OSError:
pass
return data.astype(np.float32), sr
def narrate_kannada(ref_wav: str, ref_text: str, kannada_text: str,
mood: str = "", energy: float = 0.45) -> str:
"""Narrate Kannada text. Tries: IndicF5 β MMS-TTS β gTTS. Returns WAV path."""
text = (kannada_text or "").strip()
if not text:
raise ValueError("No Kannada text to narrate.")
use_indic = _get_model()
full = np.array([], dtype=np.float32)
sr = MMS_SR
# Tier 1: user's fine-tuned IndicF5
if use_indic:
try:
full, sr = _narrate_indic(ref_wav or "", text)
logger.info("IndicF5 narration OK")
except Exception as e:
logger.warning(f"IndicF5 narration failed ({e}); trying MMS-TTS")
# Tier 2: MMS-TTS-Kan
if not full.size and _mms_model is not None:
try:
full, sr = _narrate_mms(text)
logger.info("MMS-TTS narration OK")
except Exception as e:
logger.warning(f"MMS-TTS narration failed ({e}); trying gTTS")
# Tier 3: gTTS (always works, no GPU needed)
if not full.size:
try:
full, sr = _narrate_gtts(text)
logger.info("gTTS narration OK")
except Exception as e:
raise RuntimeError(f"All Kannada TTS tiers failed. Last error: {e}") from e
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
|