A ready-to-deploy wrapper around linux-voice-assistant that integrates with the Ctrlable Snapcast TTS Streamer.
Run as many satellites as you have USB sound cards on a single machine. Each instance is an independent systemd service managed through a single ctrlable-va command.
linux-voice-assistant acts as an ESPHome device — Home Assistant connects to it, runs the Assist pipeline, and sends the TTS URL when a response is ready. With AUDIO_OUTPUT=snapcast (the default), this wrapper intercepts that URL and sends a HomeassistantActionRequest back to HA over the same ESPHome API connection — no token, no separate HTTP call — which triggers ctrlable_snapcast_tts.announce.
Wake word → satellite → HA Assist pipeline → TTS URL
→ HomeassistantActionRequest → ctrlable_snapcast_tts.announce
→ Snapcast → all clients in the group play the response
- Linux x64 / ARM64, Python 3.11+
- PulseAudio or PipeWire
- Ctrlable Snapcast TTS Streamer add-on (Snapcast output only)
git clone https://github.com/Ctrlable/ctrlable-linux-voice-assistant
cd ctrlable-linux-voice-assistant
sudo bash install.shinstall.sh creates a shared virtualenv at /opt/ctrlable-linux-va, installs the upstream linux-voice-assistant package, and registers the systemd template ctrlable-voice-assistant@.service. A ctrlable-va shortcut is added to /usr/local/bin.
Each satellite is one .env file + one systemd instance. No separate directories, no duplicate installs.
# First satellite — auto-assigns port 6053
sudo ctrlable-va add living-room \
--input hw:CARD=USB0,DEV=0 \
--output hw:CARD=USB0,DEV=0
# Second satellite — auto-assigns port 6054
sudo ctrlable-va add kitchen \
--input hw:CARD=USB1,DEV=0 \
--output hw:CARD=USB1,DEV=0
# Third satellite — local mpv playback instead of Snapcast
sudo ctrlable-va add office \
--input hw:CARD=USB2,DEV=0 \
--output hw:CARD=USB2,DEV=0 \
--localEach command creates satellites/<name>.env, enables, and starts the service immediately.
sudo ctrlable-va list # show all instances + status + port
sudo ctrlable-va logs living-room # follow logs
sudo ctrlable-va edit kitchen # edit config, then restart
sudo ctrlable-va disable bedroom # stop + disable
sudo ctrlable-va devices # list available audio devicesINSTANCE STATUS PORT SATELLITE ID
-------- ------ ---- ------------
kitchen active 6054 kitchen [snapcast]
living-room active 6053 living-room [snapcast]
office active 6055 office [local]
Each instance listens on its own port. Add each as a separate ESPHome integration:
Settings → Devices & Services → Add Integration → ESPHome
| Instance | Host | Port |
|---|---|---|
| living-room | <machine-ip> |
6053 |
| kitchen | <machine-ip> |
6054 |
| office | <machine-ip> |
6055 |
Wake word engine and model are configured per-pipeline in the Home Assistant UI:
Settings → Voice Assistants → your pipeline → Wake word
Available options depend on which wake word packages are installed. See the linux-voice-assistant docs for installing on-device models (OpenWakeWord, MicroWakeWord).
To pass wake word flags to a specific instance, use a systemd drop-in:
sudo systemctl edit ctrlable-voice-assistant@living-room[Service]
ExecStart=
ExecStart=/opt/ctrlable-linux-va/venv/bin/python /opt/ctrlable-linux-va/run.py \
--port ${PORT} \
--audio-input-device ${AUDIO_INPUT_DEVICE} \
--audio-output-device ${AUDIO_OUTPUT_DEVICE} \
--wake-word-engine on-device \
--wake-word okay-nabuEach satellites/<name>.env supports:
| Variable | Default | Description |
|---|---|---|
AUDIO_OUTPUT |
snapcast |
snapcast or local |
SNAPCAST_SATELLITE_ID |
(instance name) | Snapcast satellite ID; defaults to the systemd instance name so you usually don't need to set this |
PORT |
auto (6053+) | ESPHome API port — must be unique per instance |
AUDIO_INPUT_DEVICE |
default |
Microphone device |
AUDIO_OUTPUT_DEVICE |
default |
Speaker device (used when AUDIO_OUTPUT=local) |
USB sound cards change their hw: index when replugged or on reboot. Create persistent udev symlinks so your configs survive reboots:
# Find vendor/product ID and serial of your USB card
udevadm info -a -n /dev/snd/controlC1 | grep -E 'idVendor|idProduct|serial'Create /etc/udev/rules.d/85-usb-sound.rules:
SUBSYSTEM=="sound", ATTRS{idVendor}=="0d8c", ATTRS{serial}=="living_room_mic", SYMLINK+="snd/living-room"
SUBSYSTEM=="sound", ATTRS{idVendor}=="0d8c", ATTRS{serial}=="kitchen_mic", SYMLINK+="snd/kitchen"
Then reference the stable name in your .env:
AUDIO_INPUT_DEVICE=hw:living-room
If your USB cards don't expose a unique serial, use the physical USB port path instead (KERNELS=="1-1.2" etc.).
SNAPCAST_SATELLITE_ID defaults to the systemd instance name. If your instances are named to match your ESPHome Atom Echo device names (e.g. ctrlable-living-room), no extra config is needed:
sudo ctrlable-va add ctrlable-living-room --input hw:CARD=USB0,DEV=0 ...This project wraps OHF-Voice/linux-voice-assistant. Microphone capture, wake word detection, STT, and the ESPHome API protocol are all provided by that package.