From fd5cd13f723c4cbf2647550f0eaf8c5f27f80f46 Mon Sep 17 00:00:00 2001 From: imonlinux <39863321+imonlinux@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:40:53 -0800 Subject: [PATCH] Update __main__.py (Major change) main() function completely refactored into helpers. _init_controllers now handles decoupled initialization. _process_audio loop logic was fixed to correctly initialize wake words on startup. mic.recorder() loop is now much more robust against OS buffering issue when the microphone is muted and wakewords were detected and processed after unmute. --- linux_voice_assistant/__main__.py | 138 ++++++++++++++++++------------ 1 file changed, 82 insertions(+), 56 deletions(-) diff --git a/linux_voice_assistant/__main__.py b/linux_voice_assistant/__main__.py index 2eed265..764e2be 100644 --- a/linux_voice_assistant/__main__.py +++ b/linux_voice_assistant/__main__.py @@ -134,7 +134,7 @@ async def main() -> None: finally: # --- 10. Cleanup --- _LOGGER.debug("Shutting down...") - state.mic_muted = True # Signal audio thread to stop + state.shutdown = True # Signal audio thread to stop audio_thread.join() if hasattr(state, "mqtt_controller") and state.mqtt_controller: @@ -404,69 +404,95 @@ def _process_audio(state: ServerState, mic, block_size: int): last_active: Optional[float] = None try: - _LOGGER.debug("Opening audio input device: %s", mic.name) - with mic.recorder(samplerate=16000, channels=1, blocksize=block_size) as mic_in: - while True: - if state.mic_muted: - break - - audio_chunk_array = mic_in.record(block_size).reshape(-1) - audio_chunk = ( - (np.clip(audio_chunk_array, -1.0, 1.0) * 32767.0) - .astype(" 0.5 for oi in oww_inputs for p in wake_word.process_streaming(oi)): activated = True + if (not wake_words) or (state.wake_words_changed): + state.wake_words_changed = False + wake_words = [ + ww for ww in state.wake_words.values() + if ww.id in state.active_wake_words + ] + has_oww = any(isinstance(ww, OpenWakeWord) for ww in wake_words) - if activated: - now = time.monotonic() - if (last_active is None) or ((now - last_active) > state.refractory_seconds): - state.loop.call_soon_threadsafe(state.satellite.wakeup, wake_word) - last_active = now + if has_oww and (oww_features is None): + _LOGGER.debug("Initializing OpenWakeWord features...") + oww_features = OpenWakeWordFeatures.from_builtin() - stopped = False - for micro_input in micro_inputs: - if state.stop_word.process_streaming(micro_input): - stopped = True - - if stopped and (state.stop_word.id in state.active_wake_words): - state.loop.call_soon_threadsafe(state.satellite.stop) + try: + micro_inputs.clear(); micro_inputs.extend(micro_features.process_streaming(audio_chunk)) - state.loop.call_soon_threadsafe(state.satellite.handle_audio, audio_chunk) + if has_oww: + assert oww_features is not None + oww_inputs.clear(); oww_inputs.extend(oww_features.process_streaming(audio_chunk)) + + for wake_word in wake_words: + activated = False + if isinstance(wake_word, MicroWakeWord): + if any(wake_word.process_streaming(mi) for mi in micro_inputs): activated = True + elif isinstance(wake_word, OpenWakeWord): + if any(p > 0.5 for oi in oww_inputs for p in wake_word.process_streaming(oi)): activated = True + + if activated: + now = time.monotonic() + if (last_active is None) or ((now - last_active) > state.refractory_seconds): + state.loop.call_soon_threadsafe(state.satellite.wakeup, wake_word) + last_active = now + + stopped = False + for micro_input in micro_inputs: + if state.stop_word.process_streaming(micro_input): + stopped = True - except Exception: - _LOGGER.exception("Unexpected error handling audio") + if stopped and (state.stop_word.id in state.active_wake_words): + state.loop.call_soon_threadsafe(state.satellite.stop) + + state.loop.call_soon_threadsafe(state.satellite.handle_audio, audio_chunk) + + except Exception: + _LOGGER.exception("Unexpected error handling audio") except Exception as e: _LOGGER.critical("A soundcard error occurred: %s", e)