Behavioral & logging changes
Improved the wake word processing loop to make the following clearly visible in DEBUG logs:
The current set of active wake word IDs (MWW/OWW).
When the Stop wake word:
is activated by the model,
is accepted (i.e. meets refractory timing + is currently allowed), and
is ignored because it is no longer in active_wake_words.
Ensured the special stop model:
Is only treated as “active” when state.stop_word.is_active is True.
Is flipped back to inactive immediately after satellite.stop() is called, so:
The first “Stop” that passes all checks cancels playback and alarm tones.
Subsequent detections in the same moment appear as “TRIGGERED but IGNORED” (expected and now clearly logged).
Why this matters
When the LVA is playing audio (TTS or timer alarm), you can now:
See exactly when the Stop model fires.
Confirm that playback is being canceled at the right time.
Understand why you might see multiple “Stop wake word ACTIVATED” messages even though the alarm already stopped (model still “hearing” the tail, but logically ignored).
This matches the behavior seen in your logs:
At lower volumes / with AEC, stop is reliably accepted and playback stops immediately.
At high volumes without AEC, the model sees far less “clean” speech, so activations are rare or absent.
(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.
__init__ was completely rewritten to be decoupled. It now listens for mic_muted/mic_unmuted events to manage its own state instead of reading from ServerState.
__init__ was completely rewritten to be decoupled. It now receives loop, event_bus, config, and preferences directly and no longer has any reference to ServerState.
ServerState is now a pure data container, no longer holding controller instances. The satellite reference was re-added, as it's required by the audio thread to call satellite.wakeup(). Fixed a TypeError by re-ordering non-default fields.
(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.
Replaced sounddevice with soundcard, removed the audio Queue, added the new download directory to the search path, and added the device listing arguments
Replaced sounddevice with soundcard, removed the audio Queue, added the new download directory to the search path, and added the device listing arguments
Refactor: Sync with Upstream Architectural Changes (Nov 2025)
Type: Refactor / Feat
Description
This PR ports several major architectural changes and new features from the upstream OHF-Voice/linux-voice-assistant repository. Because our codebase has been significantly refactored (config file, state machine), these changes were ported manually to integrate them cleanly with our new architecture.
This brings our fork up-to-date with the upstream's core logic and dependencies, ensuring we benefit from future bug fixes in those externalized libraries.
Key Changes
Audio Library Replaced: Replaced the sounddevice library with soundcard.
New Audio Capture Loop: The entire audio processing pipeline in __main__.py has been rewritten. It no longer uses a Queue and a sounddevice callback. Instead, the process_audio thread now runs a blocking mic.recorder() loop from the soundcard library, which is the new architecture adopted by the upstream project.
External Wake Word Libraries: All local wake word code (microwakeword.py, openwakeword.py, wakeword.py) and the lib/ directory have been deleted. They are replaced by new pip dependencies: pymicro-wakeword and pyopen-wakeword.
New Feature: External Wake Words: Ported the feature to allow Home Assistant to provide custom, external wake words. The LVA will now download models sent from HA to a new local/ directory.
New Feature: List Devices: Ported the --list-input-devices and --list-output-devices CLI helpers from upstream.
Dependency Updates: > * Bumped aioesphomeapi to 42.7.0 and updated api_server.py to use AuthenticationRequest (from ConnectRequest). > * Constrained zeroconf to <1. > * Updated dev dependencies (black, flake8, etc.)
Cleanup: Updated .gitignore to include the new /local/ download directory and mypy.ini for the new libraries.
Refactor: Centralize Configuration into config.json
Type: Refactor Issue: Closes #XYZ (if you have one)
Description
This pull request completes a major refactoring goal by moving all (20+) command-line arguments into a single, centralized config.json file.
The application is now launched with a single -c or --config flag pointing to this new file, dramatically simplifying startup scripts and configuration management.
Changes
NEW: linux_voice_assistant/config.py > * Adds new dataclasses (AppConfig, AudioConfig, LedConfig, etc.) to define the application's configuration structure. > * Includes a load_config_from_json() helper to parse the JSON file and provide default values for any omitted sections.
MODIFIED: linux_voice_assistant/__main__.py > * Removed all argparse.add_argument() calls, except for the new --config flag. > * The main() function now calls load_config_from_json() at startup. > * All internal logic now references the config object (e.g., config.app.name) instead of the old args object.