Spaces:
Build error
Build error
File size: 6,341 Bytes
a8e06d3 | 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 | """
speech.py — pluggable STT / TTS backends
------------------------------------------
FastRTC ships English-only built-ins out of the box:
- get_stt_model("moonshine/base") -> STT
- get_tts_model("kokoro") -> TTS
Neither supports Malayalam, so this module adds two small classes that
satisfy FastRTC's STTModel / TTSModel protocols (a `.stt(audio)` method
and `.tts(text, options)` / `.stream_tts_sync(text, options)` methods)
and can be dropped into `ReplyOnPause` exactly like the built-ins.
MalayalamSTT -> a Whisper checkpoint fine-tuned on Malayalam speech
MalayalamTTS -> Meta's MMS VITS Malayalam checkpoint
Swap either model id via env vars without touching any other code.
"""
from __future__ import annotations
import os
import re
from dataclasses import dataclass
import numpy as np
import torch
from transformers import (
AutoModelForSpeechSeq2Seq,
AutoProcessor,
VitsModel,
AutoTokenizer,
)
from fastrtc import KokoroTTSOptions, get_stt_model, get_tts_model
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
# Fine-tuned Whisper checkpoint for Malayalam ASR. Swap for whichever
# checkpoint gives you the best WER on your audience's accent/domain --
# a few public options: vrclc/Whisper-medium-Malayalam,
# kavyamanohar/whisper-small-malayalam, Jithjacob123/whisper-small-Malayalam.
MALAYALAM_STT_MODEL_ID = os.environ.get(
"MEDGUIDE_ML_STT_MODEL_ID", "vrclc/Whisper-medium-Malayalam"
)
# Meta MMS single-speaker VITS checkpoint for Malayalam TTS. For higher
# quality/expressiveness (at the cost of a heavier model + prompt
# engineering) swap to ai4bharat/indic-parler-tts, which also covers
# Malayalam plus 20 other Indic languages.
MALAYALAM_TTS_MODEL_ID = os.environ.get(
"MEDGUIDE_ML_TTS_MODEL_ID", "facebook/mms-tts-mal"
)
TARGET_SR = 16000
def _to_mono_float32(audio: tuple[int, np.ndarray]) -> tuple[int, np.ndarray]:
"""Normalize a FastRTC (sample_rate, array) pair to mono float32 in [-1, 1]."""
sr, arr = audio
arr = np.asarray(arr)
if arr.ndim == 2:
arr = arr.mean(axis=0) if arr.shape[0] < arr.shape[1] else arr.mean(axis=1)
if np.issubdtype(arr.dtype, np.integer):
arr = arr.astype(np.float32) / 32768.0
else:
arr = arr.astype(np.float32)
return sr, arr
def _resample(arr: np.ndarray, sr: int, target_sr: int = TARGET_SR) -> np.ndarray:
if sr == target_sr:
return arr
from scipy import signal
n_target = int(round(len(arr) * target_sr / sr))
return signal.resample(arr, max(n_target, 1)).astype(np.float32)
class MalayalamSTT:
"""Implements FastRTC's STTModel protocol: `.stt(audio) -> str`."""
def __init__(self, model_id: str = MALAYALAM_STT_MODEL_ID):
self.processor = AutoProcessor.from_pretrained(model_id)
self.model = AutoModelForSpeechSeq2Seq.from_pretrained(
model_id, dtype=DTYPE
).to(DEVICE)
self.model.eval()
# Force Malayalam decoding regardless of Whisper's language auto-detect.
try:
self.forced_ids = self.processor.get_decoder_prompt_ids(
language="malayalam", task="transcribe"
)
except Exception:
self.forced_ids = None
def stt(self, audio: tuple[int, np.ndarray]) -> str:
sr, arr = _to_mono_float32(audio)
arr = _resample(arr, sr, TARGET_SR)
inputs = self.processor(
arr, sampling_rate=TARGET_SR, return_tensors="pt"
).to(DEVICE)
with torch.no_grad():
ids = self.model.generate(
**inputs,
forced_decoder_ids=self.forced_ids,
max_new_tokens=200,
)
return self.processor.batch_decode(ids, skip_special_tokens=True)[0].strip()
@dataclass
class SimpleTTSOptions:
speed: float = 1.0
_SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?।])\s+")
class MalayalamTTS:
"""Implements FastRTC's TTSModel protocol: `.tts()` and
`.stream_tts_sync()`. MMS-VITS is not autoregressive/streamable at the
token level, so "streaming" here means synthesizing sentence-by-sentence
and yielding each clip as soon as it's ready -- still cuts first-audio
latency noticeably versus waiting for the whole reply."""
def __init__(self, model_id: str = MALAYALAM_TTS_MODEL_ID):
self.tokenizer = AutoTokenizer.from_pretrained(model_id)
self.model = VitsModel.from_pretrained(model_id).to(DEVICE)
self.model.eval()
self.sample_rate = self.model.config.sampling_rate
def _synth_one(self, text: str) -> tuple[int, np.ndarray]:
inputs = self.tokenizer(text, return_tensors="pt").to(DEVICE)
with torch.no_grad():
waveform = self.model(**inputs).waveform
arr = waveform.squeeze().cpu().numpy().astype(np.float32)
return self.sample_rate, arr.reshape(1, -1)
def tts(self, text: str, options: SimpleTTSOptions | None = None):
return self._synth_one(text)
def stream_tts_sync(self, text: str, options: SimpleTTSOptions | None = None):
chunks = [c.strip() for c in _SENTENCE_SPLIT_RE.split(text) if c.strip()]
if not chunks:
return
for chunk in chunks:
yield self._synth_one(chunk)
# ---------------------------------------------------------------------
# Per-language registry the UI/handler pick from
# ---------------------------------------------------------------------
_english_stt = None
_english_tts = None
_malayalam_stt = None
_malayalam_tts = None
def get_stt(language: str):
global _english_stt, _malayalam_stt
if language == "Malayalam":
if _malayalam_stt is None:
_malayalam_stt = MalayalamSTT()
return _malayalam_stt
if _english_stt is None:
_english_stt = get_stt_model(model="moonshine/base")
return _english_stt
def get_tts(language: str):
global _english_tts, _malayalam_tts
if language == "Malayalam":
if _malayalam_tts is None:
_malayalam_tts = MalayalamTTS()
return _malayalam_tts
if _english_tts is None:
_english_tts = get_tts_model(model="kokoro")
return _english_tts
ENGLISH_TTS_OPTIONS = KokoroTTSOptions(voice="af_heart", speed=1.0, lang="en-us") |