fix(asr): keep Parakeet recognition alive after decode errors (#803)

* fix(asr): keep Parakeet recognition alive after decode errors

sherpa-onnx create/decode exceptions were escaping the recognition
thread. Catch them like whisper.cpp, annotate the Parakeet helpers, and
cover create_stream failure.

* fix(asr): narrow Parakeet transcription exception handling

Keep create_stream/decode survival, but stop swallowing unrelated
bugs. Same RuntimeError/OSError/ValueError tuple as Whisper.
This commit is contained in:
Jatin K Malik
2026-09-07 18:10:33 -07:00
committed by GitHub
parent affbf6a9af
commit dcd8d06077
2 changed files with 45 additions and 19 deletions
@@ -1778,7 +1778,7 @@ class SpeechRecognitionManager:
logger.error(f"Error in whisper.cpp transcription: {e} ({audio_info})", exc_info=True)
return ""
def _download_parakeet_model(self):
def _download_parakeet_model(self) -> None:
"""Download the Parakeet model files with progress tracking."""
import requests
@@ -1794,8 +1794,8 @@ class SpeechRecognitionManager:
outer_callback = self._download_progress_callback
total_files = len(parakeet.MODEL_FILES)
def file_progress(index, name):
def report(fraction, speed, status):
def file_progress(index: int, name: str) -> Callable[[float, float, str], None]:
def report(fraction: float, speed: float, status: str) -> None:
if outer_callback:
outer_callback(
(index + fraction) / total_files,
@@ -1897,7 +1897,7 @@ class SpeechRecognitionManager:
logger.info("Removed the unverified model file; it will be downloaded again")
return verified
def _init_parakeet(self):
def _init_parakeet(self) -> None:
"""Initialize the Parakeet speech recognition engine."""
try:
import sherpa_onnx
@@ -1970,23 +1970,33 @@ class SpeechRecognitionManager:
def _transcribe_with_parakeet(self, audio_buffer: list[bytes]) -> str:
"""Transcribe 16-bit PCM chunks (16kHz) with Parakeet."""
import numpy as np
try:
import numpy as np
if not audio_buffer:
return ""
audio_data = np.frombuffer(b"".join(audio_buffer), dtype=np.int16)
audio_float = audio_data.astype(np.float32) / 32768.0
# Lock model access to prevent a race with reconfigure() setting self.model to None
with self._model_lock:
if self.model is None:
logger.warning("Model is None during transcription, returning empty result")
if not audio_buffer:
return ""
stream = self.model.create_stream()
stream.accept_waveform(16000, audio_float)
self.model.decode_stream(stream)
return stream.result.text.strip()
audio_data = np.frombuffer(b"".join(audio_buffer), dtype=np.int16)
audio_float = audio_data.astype(np.float32) / 32768.0
# Lock model access to prevent a race with reconfigure() setting self.model to None
with self._model_lock:
if self.model is None:
logger.warning("Model is None during transcription, returning empty result")
return ""
stream = self.model.create_stream()
stream.accept_waveform(16000, audio_float)
self.model.decode_stream(stream)
return stream.result.text.strip()
except (RuntimeError, OSError, ValueError) as e:
audio_info = (
f"audio buffer: {len(audio_buffer)} chunks"
if audio_buffer
else "empty audio buffer"
)
logger.error(f"Error in Parakeet transcription: {e} ({audio_info})", exc_info=True)
return ""
def _init_remote_api(self):
"""Initialize remote API speech recognition engine.
@@ -417,6 +417,22 @@ class TestTranscribeWhisper(unittest.TestCase):
self.assertEqual(result, "")
class TestTranscribeParakeet(unittest.TestCase):
def test_transcribe_create_stream_exception(self):
mgr = _make_manager()
mgr.model = MagicMock()
mgr.model.create_stream.side_effect = RuntimeError("decode boom")
mock_np = MagicMock()
mock_np.frombuffer.return_value = MagicMock()
mock_np.frombuffer.return_value.astype.return_value = MagicMock()
with patch.dict("sys.modules", {"numpy": mock_np}):
result = mgr._transcribe_with_parakeet([b"\x00\x00" * 512])
self.assertEqual(result, "")
mgr.model.create_stream.assert_called_once()
class TestInitVosk(unittest.TestCase):
def test_init_vosk_model_exists(self):
mgr = _make_manager(engine="vosk")