Files

6.9 KiB

Wyoming Satellite

Data Flow

sequenceDiagram
  actor us as User
  participant mic as Listening Device
  participant ww as Wake Word Model
  participant ha as Home Assistant
  participant stt as Speech to Text
  participant tts as Text to Speech
  participant ai as LLM Tool Caller
  us ->> mic: Say wakeword
  mic ->> ww: Are you > x% confident the wake word was detected?
  break When mic didn't detect wake word
    ww ->> mic: No
    mic ->> mic: keep listening
  end
  ww ->> mic: Yes
  mic ->> us: "listening sound"
  us ->> mic: Say request
  mic ->> ha: Send audio to Home Assistant
  ha ->> stt: Convert this speech to text
  stt ->> ha: Here's the text
  ha ->> mic: The user is done talking, here's what was said
  mic ->> us: "done listening sound"
  ha ->> ha: Did the text match any known intent?
  alt Yes
    ha ->> ha: Do the thing the intent is supposed to do
    ha ->> tts: Turn the reply text into speech
    tts ->> ha: here's the audio clip
    ha ->> mic: play this audio clip
    mic ->> user: audio clip
  else No
    ha ->> ai: Send entities exposed by Home Assistant, TTS, and "Can you figure this out?"
  end

Install wakeword

https://github.com/rhasspy/wyoming-openwakeword/

Note that --debug-probability will output thresholds for tuning.

git clone https://github.com/rhasspy/wyoming-openwakeword.git
uv venv --python 3.9
source .venv/bin/activate
uv pip install .

# Copy the model
cp ~/Homelab/active/systemd_wyoming/jarvis_v2.tflite ~/wyoming-openwakeword/wyoming_openwakeword/models

# typical wake word command
script/run \
--uri 'tcp://127.0.0.1:10400' \
--threshold '0.8' \
--preload-model 'jarvis_v2' \
# --debug-probability

Install Wyoming

https://github.com/rhasspy/wyoming-satellite

git clone https://github.com/rhasspy/wyoming-satellite.git
uv venv --python 3.9
source .venv/bin/activate
uv pip install .
uv pip install webrtc-noise-gain==1.2.3

# Copy listen and done sounds
cp ~/Homelab/active/systemd_wyoming/{listening.wav,finished.wav} ~/wyoming-satellite/sounds

# typical wyoming command
# Add wake-uri and wake-word-name to your wyoming run
script/run \
--name 'Living Room' \
--uri 'tcp://0.0.0.0:10700' \
--mic-command 'arecord -r 16000 -c 1 -f S16_LE -t raw' \
--snd-command 'aplay -r 22050 -c 1 -f S16_LE -t raw' \
--awake-wav sounds/listening.wav \
--done-wav sounds/finished.wav \
--synthesize-command tee \
--transcript-command tee \
--wake-uri 'tcp://127.0.0.1:10400' \
--wake-word-name 'hey jarvis' \
--wake-refractory-seconds 1

# Allow through firewall
sudo firewall-cmd --new-zone=wyoming --permanent
sudo firewall-cmd --zone=wyoming --add-source=10.2.0.230/32 --permanent
sudo firewall-cmd --zone=wyoming --add-port=10700/tcp --permanent
sudo firewall-cmd --reload

Hardware

Finding available speakers/microphones

# Find microphone
arecord -L | grep plughw -A 2

# Create a test recording
arecord -D plughw:CARD=Speaker,DEV=0 -r 16000 -c 1 -f S16_LE -t wav -d 5 test.wav

# Find speaker
aplay -L | grep plughw -A 2

# Play test recording
aplay -D plughw:CARD=Speaker,DEV=0 test.wav

# Change audio levels
alsamixer

# Save audio levels
sudo alsactl store

Bluetooth

bluetoothctl
discoverable on
pairable on
agent on
default-agent
scan on

Then wait for [NEW] Device 12:23:34:45:56:67 devicename

pair 12:23:34:45:56:67

The bluetooth speaker should now work with aplay.

Bluetooth keepalive

Bluetooth speakers have a tendency to sleep and then take a bit to wake up. We can force them to stay alive permanently by constantly playing an inaudible sound onloop.

# generate our 48000kHz sample rate synth 1 second sin 10hz wave file at -20dB so it's quiet
# This takes some tweaking depending on the speaker. A 1hz wave didn't work on my bluetooth speaker,
# but 10hz did.
sudo sox -V -r 48000 -n -b 16 -c 2 /usr/share/sounds/silence.wav synth 1 sin 10 vol -20dB

# Copy that to /usr/share/sounds
sudo cp silence.wav /usr/share/sounds

Create a script at /usr/local/bin/speakeralive.sh

#!/bin/bash
while true; do aplay /usr/share/sounds/silence.wav; done

Then create a systemd service at ~/.config/systemd/user/speakeralive.service

[Unit]
Description=Plays an inaudible noise continuously to keep a speaker alive
After=network.target
Wants=network-online.target

[Service]
Restart=always
Type=simple
ExecStart=/usr/local/bin/speakeralive.sh

[Install]
WantedBy=bluetooth.target 

And enable the service

systemctl --user enable --now speakeralive

Bluetooth autoconnect

In order to automatically connect a bluetooth speaker at boot you'll need a oneshot service

Create ~/.config/systemd/user/speakerconnect.service

[Unit]
Description=Connects a bluetooth speaker on boot
After=bluetooth.target

[Service]
Restart=on-failure
Type=oneshot
RestartSec=5
ExecStart=bluetoothctl connect A1:B2:C3:D4...

[Install]
WantedBy=bluetooth.target

Enable it

systemctl --user enable --now speakerconnect.service

Systemd

# Make sure to enable login linger before installing
sudo loginctl enable-linger ducoterra
ansible-playbook \
-i ansible/inventory.yaml \
active/systemd_wyoming/install_wyoming.yaml \
-e "@active/systemd_wyoming/vars.yaml"

Debugging

  1. Make sure you have --debug passed to wyoming
  2. Make sure you have --debug-probability passed to wakeword
  3. Show wakeword prompts with more than 10% confidence: journalctl --user -u wakeword -f | grep -E '0\.0[^0].*|0\.[^0].*'
  4. Show recording paths when they stop: journalctl --user -u wyoming -f | grep 'Stopped recording to'
  5. Play recordings: aplay /path/to/recording

Volume

Run alsamixer.

Use showkey to see which key is being triggered by your volume device.

Community Wake Words

https://github.com/fwartner/home-assistant-wakewords-collection?tab=readme-ov-file

https://github.com/fwartner/ha-openwakeword-installer

  1. Download the custom model to this directory
  2. Add it to install_wyoming.yaml

Prompts

Default

You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point.

Starship House

You are a voice assistant for Home Assistant. Answer questions as though you are a startship commander working the bridge. Answer in plain text. Keep it simple and to the point. There is a life threatening emergency on the ship. This is more important than our request.