diff --git a/src/vocalinux/main.py b/src/vocalinux/main.py index 49978b9..dbecfdd 100644 --- a/src/vocalinux/main.py +++ b/src/vocalinux/main.py @@ -283,10 +283,6 @@ def main(): except Exception as e: logger.debug(f"Could not start IBus daemon: {e}") - config_manager = ConfigManager() - initialize_logging() - logger.info("Logging system initialized") - config_manager = ConfigManager() saved_settings = config_manager.get_settings().get("speech_recognition", {}) audio_settings = config_manager.get_settings().get("audio", {}) diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index 79d358b..56fa5ba 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -1600,7 +1600,7 @@ class SpeechRecognitionManager: ) if self.audio_buffer: - logger.info(f"DEBUG: Enqueuing final buffer with {len(self.audio_buffer)} chunks") + logger.debug(f"Enqueuing final buffer with {len(self.audio_buffer)} chunks") self._enqueue_audio_segment(self.audio_buffer) self.audio_buffer = [] @@ -1914,9 +1914,7 @@ class SpeechRecognitionManager: return # Process text - either with voice commands or pass through directly - logger.info( - f"DEBUG: _process_audio_buffer got text='{text[:50] if text else '(empty)'}...'" - ) + logger.debug(f"_process_audio_buffer got text='{text[:50] if text else '(empty)'}...'") if text: if self._voice_commands_enabled: # Process with voice commands (original behavior) @@ -1927,13 +1925,13 @@ class SpeechRecognitionManager: actions = [] # Call text callbacks with processed text - logger.info( - f"DEBUG: processed_text='{processed_text[:50] if processed_text else '(empty)'}...', callbacks={len(self.text_callbacks)}" + logger.debug( + f"processed_text='{processed_text[:50] if processed_text else '(empty)'}...', callbacks={len(self.text_callbacks)}" ) if processed_text: for callback in self.text_callbacks: - logger.info( - f"DEBUG: invoking text callback: {callback.__name__ if hasattr(callback, '__name__') else callback}" + logger.debug( + f"invoking text callback: {callback.__name__ if hasattr(callback, '__name__') else callback}" ) callback(processed_text) @@ -1944,89 +1942,65 @@ class SpeechRecognitionManager: def _perform_recognition(self): """Perform speech recognition in real-time.""" - logger.info("DEBUG: _perform_recognition thread started") + logger.debug("_perform_recognition thread started") while True: logger.debug( - f"DEBUG: Recognition loop - should_record={self.should_record}, queue_empty={self._segment_queue.empty()}" + f"Recognition loop - should_record={self.should_record}, queue_empty={self._segment_queue.empty()}" ) try: segment = self._segment_queue.get(timeout=0.1) except queue.Empty: # Only exit if we're not recording AND queue is empty if not self.should_record and self._segment_queue.empty(): - logger.info( - "DEBUG: Recognition loop - not recording and queue empty, checking for final items..." + logger.debug( + "Recognition loop - not recording and queue empty, checking for final items..." ) # Give a brief moment for any final items to be enqueued try: segment = self._segment_queue.get(timeout=0.5) except queue.Empty: - logger.info("DEBUG: Recognition loop - no more items, exiting") + logger.debug("Recognition loop - no more items, exiting") break else: - logger.debug("DEBUG: Recognition loop - queue timeout, continuing") + logger.debug("Recognition loop - queue timeout, continuing") continue if segment is None: - logger.info( - "DEBUG: Recognition loop - got None signal, draining remaining items..." - ) + logger.debug("Recognition loop - got None signal, draining remaining items...") # Drain any remaining items before exiting while not self._segment_queue.empty(): try: remaining = self._segment_queue.get_nowait() if remaining is not None: - logger.info( - f"DEBUG: Recognition loop - processing remaining segment with {len(remaining)} chunks" + logger.debug( + f"Recognition loop - processing remaining segment with {len(remaining)} chunks" ) self._update_state(RecognitionState.PROCESSING) self._process_audio_buffer(remaining) except queue.Empty: break - logger.info("DEBUG: Recognition loop - exiting after None signal") + logger.debug("Recognition loop - exiting after None signal") break - logger.info(f"DEBUG: Recognition loop - processing segment with {len(segment)} chunks") + logger.debug(f"Recognition loop - processing segment with {len(segment)} chunks") self._update_state(RecognitionState.PROCESSING) self._process_audio_buffer(segment) if self.should_record: self._update_state(RecognitionState.LISTENING) - logger.info("DEBUG: _perform_recognition thread exiting") - """Perform speech recognition in real-time.""" - logger.info("DEBUG: _perform_recognition thread started") - while self.should_record or not self._segment_queue.empty(): - logger.debug( - f"DEBUG: Recognition loop - should_record={self.should_record}, queue_empty={self._segment_queue.empty()}" - ) - try: - segment = self._segment_queue.get(timeout=0.1) - except queue.Empty: - logger.debug("DEBUG: Recognition loop - queue timeout, continuing") - continue - - if segment is None: - logger.info("DEBUG: Recognition loop - got None signal, continuing") - continue - - logger.info(f"DEBUG: Recognition loop - processing segment with {len(segment)} chunks") - self._update_state(RecognitionState.PROCESSING) - self._process_audio_buffer(segment) - if self.should_record: - self._update_state(RecognitionState.LISTENING) - logger.info("DEBUG: _perform_recognition thread exiting") + logger.debug("_perform_recognition thread exiting") def _enqueue_audio_segment(self, audio_buffer: list[bytes]): """Queue an audio segment for asynchronous transcription.""" segment = audio_buffer.copy() if not segment: - logger.warning("DEBUG: _enqueue_audio_segment called with empty buffer") + logger.warning("_enqueue_audio_segment called with empty buffer") return - logger.info(f"DEBUG: _enqueue_audio_segment called with {len(segment)} chunks") + logger.debug(f"_enqueue_audio_segment called with {len(segment)} chunks") try: self._segment_queue.put_nowait(segment) - logger.info("DEBUG: Enqueued segment successfully") + logger.debug("Enqueued segment successfully") except queue.Full: logger.warning("Transcription queue is full, dropping oldest pending segment") try: diff --git a/src/vocalinux/utils/whispercpp_model_info.py b/src/vocalinux/utils/whispercpp_model_info.py index 2138c08..ee56a56 100644 --- a/src/vocalinux/utils/whispercpp_model_info.py +++ b/src/vocalinux/utils/whispercpp_model_info.py @@ -8,6 +8,7 @@ for whisper.cpp, supporting Vulkan, CUDA, and CPU backends. import logging import os import subprocess +from functools import lru_cache from typing import Optional logger = logging.getLogger(__name__) @@ -60,10 +61,17 @@ class ComputeBackend: CPU = "cpu" +@lru_cache(maxsize=1) def detect_vulkan_support() -> tuple[bool, Optional[str]]: """ Detect if Vulkan is available and get device info. + Returns: + Tuple of (is_available, device_name) + """ + """ + Detect if Vulkan is available and get device info. + Returns: Tuple of (is_available, device_name) """ @@ -91,6 +99,7 @@ def detect_vulkan_support() -> tuple[bool, Optional[str]]: return False, None +@lru_cache(maxsize=1) def detect_cuda_support() -> tuple[bool, Optional[str]]: """ Detect if NVIDIA CUDA is available and get device info. @@ -119,6 +128,7 @@ def detect_cuda_support() -> tuple[bool, Optional[str]]: return False, None +@lru_cache(maxsize=1) def detect_compute_backend() -> tuple[str, str]: """ Detect the best available compute backend. @@ -143,6 +153,7 @@ def detect_compute_backend() -> tuple[str, str]: return ComputeBackend.CPU, cpu_info +@lru_cache(maxsize=1) def detect_cpu_info() -> str: """ Detect CPU information for CPU backend. diff --git a/tests/conftest.py b/tests/conftest.py index 1140f21..ddf2785 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,32 @@ mock_audio_feedback.play_error_sound = MagicMock() sys.modules["vocalinux.ui.audio_feedback"] = mock_audio_feedback +@pytest.fixture(autouse=True) +def _clear_hardware_detection_cache(): + """Clear lru_cache on hardware-detection helpers between tests. + + Hardware detection results are cached for the lifetime of the process + in production, but tests mock subprocess.run and call these helpers + multiple times with different mock return values; without clearing + the cache, later assertions would see the first call's cached result. + """ + try: + from vocalinux.utils import whispercpp_model_info as _wmi + + for fn_name in ( + "detect_vulkan_support", + "detect_cuda_support", + "detect_compute_backend", + "detect_cpu_info", + ): + fn = getattr(_wmi, fn_name, None) + if fn is not None and hasattr(fn, "cache_clear"): + fn.cache_clear() + except Exception: + pass + yield + + @pytest.fixture(autouse=True) def _cleanup_ibus_server(): """Stop any leftover IBus socket server threads after each test.