Han-Solo's picture
harden speech transcription against unreadable audio and empty transcripts
54f1187
Raw
History Blame Contribute Delete
3.28 kB
"""Local speech-to-text support for short player utterances."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import wave
MODEL_ID = "nvidia/nemotron-speech-streaming-en-0.6b"
MAX_RECORDING_SECONDS = 10
MAX_RECORDING_DRIFT_SECONDS = 0.75
MIN_RECORDING_SECONDS = 0.15
_ASR_MODEL: Any | None = None
class SpeechTranscriptionError(RuntimeError):
"""Raised when local speech transcription cannot produce usable text."""
def _load_asr_model() -> Any:
global _ASR_MODEL
if _ASR_MODEL is None:
try:
import nemo.collections.asr as nemo_asr
except Exception as exc:
raise SpeechTranscriptionError(
"Speech recognition is not installed. Install NVIDIA NeMo ASR locally."
) from exc
try:
_ASR_MODEL = nemo_asr.models.ASRModel.from_pretrained(model_name=MODEL_ID)
except Exception as exc:
raise SpeechTranscriptionError(
f"Could not load local speech model {MODEL_ID}."
) from exc
return _ASR_MODEL
def _duration_seconds(audio_path: Path) -> float | None:
try:
with wave.open(str(audio_path), "rb") as fh:
rate = fh.getframerate()
if rate <= 0:
return None
return fh.getnframes() / float(rate)
except (wave.Error, EOFError):
return None
def _validate_audio_path(audio_path: str | Path) -> Path:
path = Path(audio_path)
if not path.exists() or not path.is_file():
raise SpeechTranscriptionError("No audio file was captured.")
duration = _duration_seconds(path)
if duration is None:
raise SpeechTranscriptionError("No speech detected in the recording.")
if duration < MIN_RECORDING_SECONDS:
raise SpeechTranscriptionError("No speech detected in the recording.")
if duration > MAX_RECORDING_SECONDS + MAX_RECORDING_DRIFT_SECONDS:
raise SpeechTranscriptionError(
"Recording is too long. Try again with a shorter line."
)
return path
def _first_transcript(result: Any) -> str:
if result is None:
return ""
if isinstance(result, str):
return result
if hasattr(result, "text"):
return str(result.text)
if isinstance(result, dict):
for key in ("text", "transcript"):
if key in result:
return str(result[key])
if isinstance(result, (list, tuple)):
if not result:
return ""
return _first_transcript(result[0])
return str(result)
def _normalize_transcript(result: Any) -> str:
transcript = " ".join(_first_transcript(result).split())
if not transcript:
raise SpeechTranscriptionError("No speech detected in the recording.")
return transcript
def transcribe_audio(audio_path: str | Path, *, model: Any | None = None) -> str:
path = _validate_audio_path(audio_path)
asr_model = model if model is not None else _load_asr_model()
try:
result = asr_model.transcribe([str(path)], batch_size=1)
except SpeechTranscriptionError:
raise
except Exception as exc:
raise SpeechTranscriptionError("Speech transcription failed.") from exc
return _normalize_transcript(result)