move systemd prefixes to software prefixes
This commit is contained in:
BIN
active/software_wyoming/finished.wav
Normal file
BIN
active/software_wyoming/finished.wav
Normal file
Binary file not shown.
100
active/software_wyoming/install_wyoming.yaml
Normal file
100
active/software_wyoming/install_wyoming.yaml
Normal file
@@ -0,0 +1,100 @@
|
||||
- name: Install or Update the Wyoming Service
|
||||
hosts: wyoming
|
||||
vars_files:
|
||||
- secrets/vars.yaml
|
||||
tasks:
|
||||
- name: Install and upgrade wyoming/wakeword
|
||||
when: install
|
||||
block:
|
||||
# Stop running services before install/upgrade
|
||||
- name: Stop wakeword service
|
||||
ansible.builtin.systemd_service:
|
||||
state: stopped
|
||||
name: wakeword.service
|
||||
scope: user
|
||||
ignore_errors: true
|
||||
- name: Stop wyoming service
|
||||
ansible.builtin.systemd_service:
|
||||
state: stopped
|
||||
name: wyoming.service
|
||||
scope: user
|
||||
ignore_errors: true
|
||||
# Wyoming Service
|
||||
- name: Checkout/pull the wyoming repo
|
||||
ansible.builtin.git:
|
||||
repo: '{{ wyoming.repo }}'
|
||||
dest: "{{ ansible_env.HOME }}/wyoming-satellite"
|
||||
update: yes
|
||||
- name: Run the script/setup in the wyoming repo
|
||||
command: "{{ ansible_env.HOME }}/wyoming-satellite/script/setup"
|
||||
- name: Install audio enhancements in wyoming repo
|
||||
command: "{{ ansible_env.HOME }}/wyoming-satellite/.venv/bin/pip3 install 'webrtc-noise-gain==1.2.3'"
|
||||
# Wake word service
|
||||
- name: Checkout/pull the wyoming repo
|
||||
ansible.builtin.git:
|
||||
repo: '{{ wakeword.repo }}'
|
||||
dest: "{{ ansible_env.HOME }}/wyoming-openwakeword"
|
||||
update: yes
|
||||
- name: Run the script/setup in the wakeword repo
|
||||
command: "{{ ansible_env.HOME }}/wyoming-openwakeword/script/setup"
|
||||
# Custom sound bites for start/stop listening
|
||||
- name: Copy sound bites
|
||||
copy:
|
||||
src: "{{ item }}"
|
||||
dest: "{{ ansible_env.HOME }}/wyoming-satellite/sounds/"
|
||||
with_items:
|
||||
- listening.wav
|
||||
- finished.wav
|
||||
# Custom wake word models
|
||||
- name: Copy custom models
|
||||
copy:
|
||||
src: "{{ item }}"
|
||||
dest: "{{ ansible_env.HOME }}/wyoming-openwakeword/wyoming_openwakeword/models/{{ item }}"
|
||||
with_items:
|
||||
- jarvis_v2.tflite
|
||||
# Copy and start systemd services
|
||||
- name: Create ~/.config/systemd/user dir
|
||||
ansible.builtin.file:
|
||||
path: "{{ ansible_env.HOME }}/.config/systemd/user"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
- name: Copy wyoming.service
|
||||
template:
|
||||
src: wyoming.service
|
||||
dest: "{{ ansible_env.HOME }}/.config/systemd/user/wyoming.service"
|
||||
mode: '0644'
|
||||
- name: Copy wakeword.service
|
||||
template:
|
||||
src: wakeword.service
|
||||
dest: "{{ ansible_env.HOME }}/.config/systemd/user/wakeword.service"
|
||||
mode: '0644'
|
||||
- name: Reload wakeword service
|
||||
ansible.builtin.systemd_service:
|
||||
state: restarted
|
||||
name: wakeword.service
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
- name: Reload wyoming service
|
||||
ansible.builtin.systemd_service:
|
||||
state: restarted
|
||||
name: wyoming.service
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
- name: Add debug alias for wakeford
|
||||
lineinfile:
|
||||
path: "{{ ansible_env.HOME }}/.bashrc"
|
||||
line: alias debug-wakeword="journalctl --user -u wakeword -f | grep -E '0\.0[^0].*|0\.[^0].*'"
|
||||
regexp: '^alias debug-wakeword=.*$'
|
||||
state: present
|
||||
insertafter: EOF
|
||||
create: true
|
||||
- name: Add debug alias for wyoming
|
||||
lineinfile:
|
||||
path: "{{ ansible_env.HOME }}/.bashrc"
|
||||
line: alias debug-wyoming="journalctl --user -u wyoming -f | grep 'Stopped recording to'"
|
||||
regexp: '^alias debug-wyoming=.*$'
|
||||
state: present
|
||||
insertafter: EOF
|
||||
create: true
|
||||
BIN
active/software_wyoming/jarvis_v2.tflite
Normal file
BIN
active/software_wyoming/jarvis_v2.tflite
Normal file
Binary file not shown.
BIN
active/software_wyoming/listening.wav
Normal file
BIN
active/software_wyoming/listening.wav
Normal file
Binary file not shown.
18
active/software_wyoming/vars.yaml
Normal file
18
active/software_wyoming/vars.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
install: false
|
||||
|
||||
wyoming:
|
||||
repo: https://github.com/rhasspy/wyoming-satellite.git
|
||||
listen_ip: 0.0.0.0
|
||||
listen_port: 10700
|
||||
mic_auto_gain: 31
|
||||
mic_noise_suppression: 2
|
||||
wake_uri: tcp://127.0.0.1:10400
|
||||
wake_word_name: jarvis_v2
|
||||
wake_refractory_seconds: 1
|
||||
|
||||
wakeword:
|
||||
repo: https://github.com/rhasspy/wyoming-openwakeword.git
|
||||
listen_ip: 127.0.0.1
|
||||
listen_port: 10400
|
||||
threshold: 0.8
|
||||
preload_model: jarvis_v2
|
||||
18
active/software_wyoming/wakeword.service
Normal file
18
active/software_wyoming/wakeword.service
Normal file
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Wyoming Satellite Wake Word
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={{ ansible_env.HOME }}/wyoming-openwakeword/script/run \
|
||||
--uri 'tcp://{{ wakeword.listen_ip }}:{{ wakeword.listen_port }}' \
|
||||
--threshold '{{ wakeword.threshold }}' \
|
||||
--preload-model '{{ wakeword.preload_model }}' \
|
||||
--debug-probability
|
||||
WorkingDirectory={{ ansible_env.HOME }}/wyoming-openwakeword
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
279
active/software_wyoming/wyoming.md
Normal file
279
active/software_wyoming/wyoming.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Wyoming Satellite
|
||||
|
||||
- [Wyoming Satellite](#wyoming-satellite)
|
||||
- [Data Flow](#data-flow)
|
||||
- [Install wakeword](#install-wakeword)
|
||||
- [Install Wyoming](#install-wyoming)
|
||||
- [Hardware](#hardware)
|
||||
- [Bluetooth keepalive](#bluetooth-keepalive)
|
||||
- [Bluetooth autoconnect](#bluetooth-autoconnect)
|
||||
- [Systemd](#systemd)
|
||||
- [Debugging](#debugging)
|
||||
- [Volume](#volume)
|
||||
- [Community Wake Words](#community-wake-words)
|
||||
- [Prompts](#prompts)
|
||||
- [Default](#default)
|
||||
- [Starship House](#starship-house)
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
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.
|
||||
|
||||
```bash
|
||||
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>
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
bluetoothctl
|
||||
discoverable on
|
||||
pairable on
|
||||
agent on
|
||||
default-agent
|
||||
scan on
|
||||
```
|
||||
|
||||
Then wait for `[NEW] Device 12:23:34:45:56:67 devicename`
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
# 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`
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
while true; do aplay /usr/share/sounds/silence.wav; done
|
||||
```
|
||||
|
||||
Then create a systemd service at `~/.config/systemd/user/speakeralive.service`
|
||||
|
||||
```conf
|
||||
[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
|
||||
|
||||
```bash
|
||||
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`
|
||||
|
||||
```conf
|
||||
[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
|
||||
|
||||
```bash
|
||||
systemctl --user enable --now speakerconnect.service
|
||||
```
|
||||
|
||||
## Systemd
|
||||
|
||||
```bash
|
||||
# Make sure to enable login linger before installing
|
||||
sudo loginctl enable-linger ducoterra
|
||||
```
|
||||
|
||||
```bash
|
||||
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.
|
||||
30
active/software_wyoming/wyoming.service
Normal file
30
active/software_wyoming/wyoming.service
Normal file
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
Description=Wyoming Satellite
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={{ ansible_env.HOME }}/wyoming-satellite/script/run \
|
||||
--name '{{ ansible_hostname }}' \
|
||||
--uri 'tcp://{{ wyoming.listen_ip }}:{{ wyoming.listen_port }}' \
|
||||
--mic-command 'arecord -r 16000 -c 1 -f S16_LE -t raw {% if hostvars[inventory_hostname].mic_device %}-D {{hostvars[inventory_hostname].mic_device}}{% endif %}' \
|
||||
--snd-command 'aplay -r 22050 -c 1 -f S16_LE -t raw {% if hostvars[inventory_hostname].speaker_device %}-D {{hostvars[inventory_hostname].speaker_device}}{% endif %}' \
|
||||
--awake-wav sounds/listening.wav \
|
||||
--done-wav sounds/finished.wav \
|
||||
--timer-finished-wav sounds/timer_finished.wav \
|
||||
--mic-auto-gain {{ wyoming.mic_auto_gain }} \
|
||||
--mic-noise-suppression {{ wyoming.mic_noise_suppression }} \
|
||||
--mic-volume-multiplier {{ hostvars[inventory_hostname].mic_volume_multiplier }} \
|
||||
--wake-uri '{{ wyoming.wake_uri }}' \
|
||||
--wake-word-name '{{ wyoming.wake_word_name }}' \
|
||||
--wake-refractory-seconds {{ wyoming.wake_refractory_seconds }} \
|
||||
--timer-finished-wav-repeat 10 1 \
|
||||
--debug \
|
||||
--debug-recording-dir /tmp
|
||||
WorkingDirectory={{ ansible_env.HOME }}/wyoming-satellite
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Reference in New Issue
Block a user