diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index 33a6f80..0c48cc1 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -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. diff --git a/tests/test_recognition_manager_advanced.py b/tests/test_recognition_manager_advanced.py index 4e4bfd3..7b03e81 100644 --- a/tests/test_recognition_manager_advanced.py +++ b/tests/test_recognition_manager_advanced.py @@ -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")