Aarya2004
Deploy: sync hosted Space to local app (chat, document capture, Modal backends, pages, mobile/QR)
47b2a99
Raw
History Blame Contribute Delete
3.76 kB
"""AudioModel: local speech-to-text for the spoken voice note (ADR-0009).
Wraps CohereLabs/cohere-transcribe-03-2026 (2B, #1 WER, on-device) via transformers'
canonical path β€” AutoProcessor + CohereAsrForConditionalGeneration, the approach the
model card documents (the generic `pipeline()` API errors on this model). Verified
locally: a trade note transcribes cleanly.
Heavy (torch + a 2B model), so the import + load are LAZY β€” importing this module
costs nothing; the model loads on first `.transcribe()`. Keeps the spoken note inside
the on-device Private Stack (πŸ”Œ Off the Grid). Gated repo: needs HF access + token.
"""
DEFAULT_MODEL = "CohereLabs/cohere-transcribe-03-2026"
def to_wav_16k_mono(path: str) -> str:
"""Return a path to a 16kHz mono WAV for `path`.
Browser/phone MediaRecorder emits a **webm** container (Opus), which librosa /
transformers' `load_audio` cannot decode ("appears to be a video file"). Twilio call
recordings can be `.mp3` too. We normalize anything that isn't already a `.wav` to
16kHz mono PCM WAV with ffmpeg first β€” the format the ASR model wants, and a safer
container all round. A `.wav` input is returned unchanged (no needless transcode).
Raises RuntimeError with a clear message if ffmpeg isn't installed.
"""
import os
import shutil
import subprocess
import tempfile
if path.lower().endswith(".wav"):
return path
if shutil.which("ffmpeg") is None:
raise RuntimeError(
"ffmpeg is required to transcode the voice note (browser/phone records webm, "
"which the ASR model can't read). Install it (e.g. `brew install ffmpeg`)."
)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
out = tmp.name
subprocess.run(
["ffmpeg", "-y", "-i", path, "-ar", "16000", "-ac", "1", "-f", "wav", out],
check=True,
capture_output=True,
)
if not os.path.getsize(out):
raise RuntimeError(f"ffmpeg produced an empty WAV transcoding {path!r}.")
return out
class AudioModel:
def __init__(self, model: str = DEFAULT_MODEL, language: str = "en"):
self.name = model
self._language = language
self._processor = None
self._model = None
def _load(self):
if self._model is None:
from transformers import AutoProcessor, CohereAsrForConditionalGeneration
self._processor = AutoProcessor.from_pretrained(self.name)
self._model = CohereAsrForConditionalGeneration.from_pretrained(
self.name, device_map="auto"
)
return self._processor, self._model
def transcribe(self, path: str) -> str:
import os
from transformers.audio_utils import load_audio
processor, model = self._load()
# Normalize webm/m4a/mp3 β†’ 16kHz mono WAV (load_audio can't decode webm). Clean up
# any temp file we created (but never the caller's original .wav).
wav = to_wav_16k_mono(path)
try:
audio = load_audio(wav, sampling_rate=16000)
finally:
if wav != path:
try:
os.unlink(wav)
except OSError:
pass
inputs = processor(audio, sampling_rate=16000, return_tensors="pt", language=self._language)
inputs.to(model.device, dtype=model.dtype)
outputs = model.generate(**inputs, max_new_tokens=256)
decoded = processor.decode(outputs, skip_special_tokens=True)
# decode() returns a list (one string per batch item); we transcribe one clip.
if isinstance(decoded, list):
decoded = decoded[0] if decoded else ""
return decoded