Spaces:
Running
Running
| """ | |
| ASR Models β English (Parakeet) and Arabic (wav2vec2) singletons. | |
| Strategy (ported from Raij/src/models.py, hotword biasing removed): | |
| - Lazy-loaded singletons: models load on first call, not at import time. | |
| - Warmup on load: a silent audio pass pre-JITs the computation graph so | |
| the first real request has the same latency as subsequent ones. | |
| - Batch inference: both transcribe_*_batch functions accept a list of | |
| audio file paths and run a single forward pass. | |
| - Thread lock on Parakeet: model.transcribe() is stateful (TDT decoder), | |
| so we serialize all calls behind _en_model_lock. | |
| - Warmup functions (warmup_parakeet / warmup_wav2vec2) are called | |
| periodically by the batch worker warmup loop to prevent OpenMP | |
| thread pool spin-down during idle periods. | |
| """ | |
| import os | |
| import threading | |
| from loguru import logger | |
| # Force PyTorch path, no TensorFlow | |
| os.environ.setdefault("USE_TF", "0") | |
| os.environ.setdefault("USE_TORCH", "1") | |
| # Cap OpenMP threads β adjust if running on a GPU server with more cores | |
| os.environ.setdefault("OMP_NUM_THREADS", "2") | |
| import torch | |
| torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", "2"))) | |
| torch.set_num_interop_threads(1) | |
| _audio_model_en = None | |
| _audio_model_ar = None | |
| _en_model_lock = threading.Lock() | |
| _ar_model_lock = threading.Lock() | |
| # βββββββββββββββββββββββ English ASR (Parakeet) ββββββββββββββββββββββββ | |
| def get_audio_model_en(): | |
| """ | |
| Loads nvidia/parakeet-tdt-0.6b-v2 via NeMo. | |
| Runs a 1-second silence warmup to pre-JIT internal computation graphs. | |
| Uses greedy_batch decoding strategy for best throughput. | |
| No hotword biasing β general-purpose decoding. | |
| """ | |
| global _audio_model_en | |
| if _audio_model_en is not None: | |
| return _audio_model_en | |
| import wave | |
| import tempfile | |
| import nemo.collections.asr as nemo_asr | |
| logger.info("Loading English ASR model (nvidia/parakeet-tdt-0.6b-v2)...") | |
| model = nemo_asr.models.ASRModel.from_pretrained("nvidia/parakeet-tdt-0.6b-v2") | |
| # ββ Warmup: transcribe 1s of silence to pre-JIT computation graphs ββ | |
| warmup_path = None | |
| try: | |
| with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: | |
| warmup_path = f.name | |
| with wave.open(warmup_path, "wb") as wf: | |
| wf.setnchannels(1) | |
| wf.setsampwidth(2) | |
| wf.setframerate(16000) | |
| wf.writeframes(b"\x00" * 32000) # 1s of silence at 16kHz | |
| model.freeze() | |
| with torch.no_grad(): | |
| model.transcribe([warmup_path]) | |
| model.unfreeze() | |
| logger.info("β Parakeet warmup complete") | |
| except Exception as e: | |
| logger.warning(f"β οΈ Parakeet warmup failed (non-fatal): {e}") | |
| finally: | |
| if warmup_path: | |
| try: | |
| os.unlink(warmup_path) | |
| except Exception: | |
| pass | |
| # ββ Switch to greedy_batch for speed (no hotword biasing) ββ | |
| try: | |
| from omegaconf import OmegaConf | |
| decoding_cfg = OmegaConf.structured(model.cfg.decoding) | |
| OmegaConf.update(decoding_cfg, "strategy", "greedy_batch") | |
| if hasattr(decoding_cfg, "greedy"): | |
| OmegaConf.update(decoding_cfg, "greedy.max_symbols", 5) | |
| try: | |
| model.change_decoding_strategy(decoding_cfg, verbose=False) | |
| logger.info("β Parakeet decoding strategy: greedy_batch") | |
| except Exception as strat_e: | |
| logger.warning(f"β οΈ greedy_batch strategy failed ({strat_e}), using default") | |
| except Exception as e: | |
| logger.warning(f"β οΈ Parakeet decoding strategy setup failed (non-fatal): {e}") | |
| _audio_model_en = model | |
| logger.info("β English ASR model (Parakeet) loaded and ready") | |
| return _audio_model_en | |
| def transcribe_en_batch(audio_paths: list[str]) -> list[str]: | |
| """ | |
| Batch transcription for English audio using Parakeet. | |
| NeMo's model.transcribe() natively handles batching internally β | |
| it pads to the same length and runs a single forward pass. | |
| Serialized behind _en_model_lock (Parakeet TDT decoder is stateful). | |
| Returns a list of transcription strings (one per input path). | |
| """ | |
| model = get_audio_model_en() | |
| with _en_model_lock: | |
| with torch.no_grad(): | |
| transcriptions = model.transcribe(audio_paths) | |
| if isinstance(transcriptions, tuple): | |
| transcriptions = transcriptions[0] | |
| return [ | |
| (t.text if hasattr(t, "text") else str(t)).strip().rstrip(".") | |
| for t in transcriptions | |
| ] | |
| def warmup_parakeet(): | |
| """ | |
| Keeps Parakeet's OpenMP threads alive via a 0.5s silence transcription. | |
| Must use model.transcribe() (not raw encoder) to avoid corrupting the | |
| TDT decoder cache. No-op if model is not yet loaded. | |
| """ | |
| if _audio_model_en is None: | |
| return | |
| import wave | |
| import tempfile | |
| model = _audio_model_en | |
| warmup_path = None | |
| try: | |
| with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: | |
| warmup_path = f.name | |
| with wave.open(warmup_path, "wb") as wf: | |
| wf.setnchannels(1) | |
| wf.setsampwidth(2) | |
| wf.setframerate(16000) | |
| wf.writeframes(b"\x00" * 16000) # 0.5s of silence | |
| with _en_model_lock: | |
| model.eval() | |
| with torch.no_grad(): | |
| model.transcribe([warmup_path]) | |
| except Exception as e: | |
| logger.warning(f"β οΈ Parakeet warmup error (non-fatal): {e}") | |
| finally: | |
| if warmup_path: | |
| try: | |
| os.unlink(warmup_path) | |
| except Exception: | |
| pass | |
| # βββββββββββββββββββββββ Arabic ASR (wav2vec2) ββββββββββββββββββββββββ | |
| def get_audio_model_ar(): | |
| """ | |
| Loads IbrahimAmin/egyptian-arabic-wav2vec2-xlsr-53 via HuggingFace Transformers. | |
| Runs a 0.5s dummy forward pass to warm up OpenMP threads. | |
| No hotword biasing β pure greedy argmax decoding. | |
| """ | |
| global _audio_model_ar | |
| if _audio_model_ar is not None: | |
| return _audio_model_ar | |
| import numpy as np | |
| from transformers import Wav2Vec2ForCTC, AutoProcessor | |
| model_name = "IbrahimAmin/egyptian-arabic-wav2vec2-xlsr-53" | |
| logger.info(f"Loading Arabic ASR model ({model_name})...") | |
| processor = AutoProcessor.from_pretrained(model_name) | |
| model = Wav2Vec2ForCTC.from_pretrained(model_name) | |
| model.eval() | |
| # ββ Warmup: single forward pass on dummy audio ββ | |
| try: | |
| dummy = np.zeros(8000, dtype=np.float32) | |
| warmup_inputs = processor( | |
| [dummy], sampling_rate=16000, return_tensors="pt", padding=True | |
| ) | |
| with torch.no_grad(): | |
| model(**warmup_inputs) | |
| logger.info("β Arabic ASR warmup complete") | |
| except Exception as e: | |
| logger.warning(f"β οΈ Arabic ASR warmup failed (non-fatal): {e}") | |
| _audio_model_ar = {"model": model, "processor": processor, "model_name": model_name} | |
| logger.info("β Arabic ASR model (wav2vec2) loaded and ready") | |
| return _audio_model_ar | |
| def _load_audio_file(path: str): | |
| """ | |
| Load an audio file to a 16kHz mono float32 numpy array. | |
| Tries soundfile first (fast, no subprocess), then falls back to | |
| librosa (handles more formats including webm via ffmpeg backend). | |
| """ | |
| import numpy as np | |
| try: | |
| import soundfile as sf | |
| data, sr = sf.read(path, dtype="float32", always_2d=False) | |
| if data.ndim > 1: | |
| data = data.mean(axis=1) | |
| if sr != 16000: | |
| import librosa | |
| data = librosa.resample(data, orig_sr=sr, target_sr=16000) | |
| return data.astype(np.float32) | |
| except Exception: | |
| import librosa | |
| data, _ = librosa.load(path, sr=16000, mono=True) | |
| return data.astype(np.float32) | |
| def transcribe_ar_batch(audio_paths: list[str]) -> list[str]: | |
| """ | |
| Batch transcription for Arabic audio using wav2vec2. | |
| Loads all audio concurrently, pads to same length, | |
| runs one forward pass, and decodes via greedy argmax. | |
| Returns a list of transcription strings (one per input path). | |
| """ | |
| import numpy as np | |
| from concurrent.futures import ThreadPoolExecutor | |
| ar = get_audio_model_ar() | |
| model = ar["model"] | |
| processor = ar["processor"] | |
| # Load all waveforms concurrently | |
| with ThreadPoolExecutor(max_workers=min(len(audio_paths), 8)) as executor: | |
| waveforms_raw = list(executor.map(_load_audio_file, audio_paths)) | |
| # Guard against empty waveforms from failed decodes | |
| final_texts = [""] * len(audio_paths) | |
| valid_indices: list[int] = [] | |
| valid_waveforms: list[np.ndarray] = [] | |
| for idx, wav in enumerate(waveforms_raw): | |
| arr = np.asarray(wav, dtype=np.float32).reshape(-1) | |
| if arr.size == 0: | |
| logger.warning(f"β οΈ Arabic ASR: empty waveform at index {idx}, skipping") | |
| continue | |
| valid_indices.append(idx) | |
| valid_waveforms.append(arr) | |
| if not valid_waveforms: | |
| return final_texts | |
| inputs = processor( | |
| valid_waveforms, | |
| sampling_rate=16000, | |
| return_tensors="pt", | |
| padding=True, | |
| ) | |
| with _ar_model_lock: | |
| with torch.no_grad(): | |
| outputs = model(**inputs) | |
| predicted_ids = torch.argmax(outputs.logits, dim=-1) | |
| transcriptions = processor.batch_decode(predicted_ids) | |
| for local_i, text in enumerate(transcriptions): | |
| final_texts[valid_indices[local_i]] = text.strip().rstrip(".") | |
| return final_texts | |
| def warmup_wav2vec2(): | |
| """ | |
| Lightweight raw forward pass to keep wav2vec2's OpenMP threads alive. | |
| No-op if the Arabic model is not yet loaded. | |
| """ | |
| if _audio_model_ar is None: | |
| return | |
| import numpy as np | |
| ar = _audio_model_ar | |
| dummy = np.zeros(8000, dtype=np.float32) | |
| inputs = ar["processor"]( | |
| [dummy], sampling_rate=16000, return_tensors="pt", padding=True | |
| ) | |
| with torch.no_grad(): | |
| ar["model"](**inputs) | |