Spaces:
Sleeping
Sleeping
File size: 9,124 Bytes
de7fd77 15fbd84 de7fd77 15fbd84 de7fd77 15fbd84 de7fd77 | 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 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | """
HausaVoiceAIPipeline
====================
Full pipeline: Audio β Whisper ASR β NLLB translation β LLM NLU β NLLB β MMS-TTS β Audio
Models used:
- ASR : openai/whisper-large-v3 (Hausa language support)
- MT : facebook/nllb-200-distilled-600M (hau_Latn β eng_Latn)
- TTS : facebook/mms-tts-hau (Hausa VITS synthesis)
"""
import os
import time
import logging
import numpy as np
import torch
from typing import Optional, Tuple
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ββ Language codes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
HAUSA_NLLB = "hau_Latn"
ENGLISH_NLLB = "eng_Latn"
FRENCH_NLLB = "fra_Latn"
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
logger.info(f"Pipeline running on: {DEVICE}")
class HausaVoiceAIPipeline:
"""
Lazy-loading pipeline. Each component is loaded on first use to keep
the Space startup time reasonable on CPU.
"""
# Model selection is CPU-FIRST. whisper-large-v3 is ~6GB in fp32 and takes
# roughly a minute per utterance on the 2-vCPU free tier β it makes the
# Space look broken. Default to small on CPU, large-v3 when a GPU is
# present, and let an env var override either way.
_GPU = torch.cuda.is_available()
ASR_FINAL_MODEL = os.getenv(
"ASR_FINAL_MODEL",
"openai/whisper-large-v3" if _GPU else "openai/whisper-small")
ASR_PARTIAL_MODEL = os.getenv(
"ASR_PARTIAL_MODEL",
"openai/whisper-small" if _GPU else "openai/whisper-base")
NLLB_MODEL = os.getenv("NLLB_MODEL", "facebook/nllb-200-distilled-600M")
TTS_MODEL = os.getenv("TTS_MODEL", "facebook/mms-tts-hau")
def describe_models(self) -> str:
short = lambda m: m.split("/")[-1]
return (f"{short(self.ASR_FINAL_MODEL)} Β· {short(self.NLLB_MODEL)} Β· "
f"{short(self.TTS_MODEL)} Β· {'GPU' if self._GPU else 'CPU'}")
def __init__(self, pivot_language: str = "english"):
self.pivot = pivot_language # dialogue logic runs in English
self._asr = None
self._asr_fast = None
self._nllb_model = None
self._nllb_tokenizer = None
self._tts_model = None
self._tts_tokenizer = None
self.sample_rate = 16_000 # Whisper input
self.tts_sample_rate = 16_000 # MMS output
# ββ Lazy loaders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _load_asr(self):
if self._asr is not None:
return
logger.info(f"Loading {self.ASR_FINAL_MODEL} β¦")
from transformers import pipeline as hf_pipeline
self._asr = hf_pipeline(
"automatic-speech-recognition",
model=self.ASR_FINAL_MODEL,
generate_kwargs={"language": "hausa", "task": "transcribe"},
device=0 if DEVICE == "cuda" else -1,
chunk_length_s=30,
)
def _load_asr_fast(self):
"""Small model for live partial transcripts."""
if self._asr_fast is not None:
return
if self.ASR_PARTIAL_MODEL == self.ASR_FINAL_MODEL:
self._load_asr()
self._asr_fast = self._asr
return
logger.info(f"Loading {self.ASR_PARTIAL_MODEL} (partials) β¦")
from transformers import pipeline as hf_pipeline
self._asr_fast = hf_pipeline(
"automatic-speech-recognition",
model=self.ASR_PARTIAL_MODEL,
generate_kwargs={"language": "hausa", "task": "transcribe"},
device=0 if DEVICE == "cuda" else -1,
chunk_length_s=30,
)
def _load_nllb(self):
if self._nllb_model is not None:
return
logger.info("Loading NLLB-200-distilled-600M β¦")
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
model_id = self.NLLB_MODEL
self._nllb_tokenizer = AutoTokenizer.from_pretrained(model_id)
self._nllb_model = AutoModelForSeq2SeqLM.from_pretrained(model_id).to(DEVICE)
def _load_tts(self):
if self._tts_model is not None:
return
logger.info("Loading MMS-TTS Hausa β¦")
from transformers import VitsModel, AutoTokenizer
model_id = self.TTS_MODEL
self._tts_tokenizer = AutoTokenizer.from_pretrained(model_id)
self._tts_model = VitsModel.from_pretrained(model_id).to(DEVICE)
self.tts_sample_rate = self._tts_model.config.sampling_rate
# ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def transcribe(self, audio_array: np.ndarray, sample_rate: int = 16_000) -> str:
"""
Whisper ASR: raw audio β Hausa text.
audio_array : float32 numpy array, mono
"""
self._load_asr()
t0 = time.perf_counter()
# Resample if needed (scipy-based; avoids pulling in librosa/numba)
if sample_rate != 16_000:
from streaming_asr import _resample
audio_array = _resample(audio_array, sample_rate, 16_000)
result = self._asr({"array": audio_array.astype(np.float32), "sampling_rate": 16_000})
text = result["text"].strip()
logger.info(f"ASR ({time.perf_counter()-t0:.2f}s): {text}")
return text
def transcribe_partial(self, audio_array: np.ndarray,
sample_rate: int = 16_000) -> str:
"""Fast, lower-accuracy decode for live on-screen partials."""
self._load_asr_fast()
if sample_rate != 16_000:
from streaming_asr import _resample
audio_array = _resample(audio_array, sample_rate, 16_000)
result = self._asr_fast({"array": audio_array.astype(np.float32),
"sampling_rate": 16_000})
return result["text"].strip()
def make_streaming_session(self, emit_partials: bool = True,
config=None, vad_backend: str = "auto"):
"""
Build a StreamingASR wired to this pipeline's two Whisper models.
Each caller/session needs its own instance (it holds audio state).
"""
from streaming_asr import StreamingASR
return StreamingASR(
transcribe_fn=self.transcribe,
partial_transcribe_fn=self.transcribe_partial,
config=config,
vad_backend=vad_backend,
emit_partials=emit_partials,
)
def translate(self, text: str, src_lang: str, tgt_lang: str,
max_new_tokens: int = 256) -> str:
"""NLLB translation."""
self._load_nllb()
t0 = time.perf_counter()
self._nllb_tokenizer.src_lang = src_lang
inputs = self._nllb_tokenizer(text, return_tensors="pt", truncation=True,
max_length=512).to(DEVICE)
forced_id = self._nllb_tokenizer.convert_tokens_to_ids(tgt_lang)
with torch.no_grad():
tokens = self._nllb_model.generate(
**inputs,
forced_bos_token_id=forced_id,
max_new_tokens=max_new_tokens,
)
translated = self._nllb_tokenizer.decode(tokens[0], skip_special_tokens=True)
logger.info(f"NLLB {src_lang}β{tgt_lang} ({time.perf_counter()-t0:.2f}s): {translated}")
return translated
def synthesize(self, text: str) -> Tuple[int, np.ndarray]:
"""
MMS-TTS: Hausa text β (sample_rate, audio_array int16).
"""
self._load_tts()
t0 = time.perf_counter()
inputs = self._tts_tokenizer(text, return_tensors="pt").to(DEVICE)
with torch.no_grad():
output = self._tts_model(**inputs).waveform
audio = output.squeeze().cpu().numpy()
# Normalise β int16
audio = (audio / np.abs(audio).max() * 32767).astype(np.int16)
logger.info(f"TTS ({time.perf_counter()-t0:.2f}s): {len(audio)/self.tts_sample_rate:.1f}s audio")
return self.tts_sample_rate, audio
# ββ Full round-trip helper βββββββββββββββββββββββββββββββββββββββββββββββ
def hausa_to_english(self, hausa_text: str) -> str:
return self.translate(hausa_text, HAUSA_NLLB, ENGLISH_NLLB)
def english_to_hausa(self, english_text: str) -> str:
return self.translate(english_text, ENGLISH_NLLB, HAUSA_NLLB)
def audio_to_hausa_text(self, audio_array: np.ndarray,
sample_rate: int = 16_000) -> str:
return self.transcribe(audio_array, sample_rate)
def hausa_text_to_audio(self, hausa_text: str) -> Tuple[int, np.ndarray]:
return self.synthesize(hausa_text)
|