Merge branch 'main' into fix/timer-loop-suppress
This commit is contained in:
19 files changed
+525
-54
No files matched your search
@@ -8,3 +8,5 @@
|
||||
!/sounds/*
|
||||
!/wakewords/*
|
||||
!/wakewords/openWakeWord/*
|
||||
!/version.txt
|
||||
!/version_githash.txt
|
||||
+10
-5
@@ -40,19 +40,24 @@ LVA_XDG_RUNTIME_DIR="/run/user/${LVA_USER_ID}"
|
||||
### Audio output device (optional):
|
||||
# AUDIO_OUTPUT_DEVICE="default"
|
||||
|
||||
# Enable thinking sound (optional):
|
||||
### Enable thinking sound (optional):
|
||||
# ENABLE_THINKING_SOUND="1"
|
||||
|
||||
# Wake model (optional):
|
||||
### Wake word directory (optional):
|
||||
# path for custom files in docker is for example "app/wakewords/custom"
|
||||
# WAKE_WORD_DIR="app/wakewords"
|
||||
|
||||
### Wake model (optional):
|
||||
# WAKE_MODEL="okay_nabu"
|
||||
|
||||
# Stop model (optional):
|
||||
### Stop model (optional):
|
||||
# STOP_MODEL="stop"
|
||||
|
||||
# Refactory seconds (optional):
|
||||
### Refactory seconds (optional):
|
||||
# REFACTORY_SECONDS="2"
|
||||
|
||||
# Sound files (optional):
|
||||
### Sound files (optional):
|
||||
# path for custom files in docker is for example "sounds/custom/your_soundfile.flac"
|
||||
# WAKEUP_SOUND="sounds/wake_word_triggered.flac"
|
||||
# TIMER_FINISHED_SOUND="sounds/timer_finished.flac"
|
||||
# PROCESSING_SOUND="sounds/processing.wav"
|
||||
|
||||
@@ -44,6 +44,12 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create version files
|
||||
run: |
|
||||
echo "${{ env.CLEAN_BRANCH_NAME }}" > version.txt
|
||||
echo "${{ github.sha }}" > version_githash.txt
|
||||
ls -ltr
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
run: |
|
||||
echo "REGISTRY_LOWER=$(echo '${{ env.REGISTRY }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
echo "REPOSITORY_LOWER=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
|
||||
with:
|
||||
@@ -36,6 +37,13 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create version files
|
||||
run: |
|
||||
echo "nightly" > version.txt
|
||||
echo "${{ github.sha }}" > version_githash.txt
|
||||
ls -ltr
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
|
||||
|
||||
@@ -45,6 +45,13 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
- name: Create version files
|
||||
run: |
|
||||
echo "${{ steps.meta.outputs.version }}" > version.txt
|
||||
echo "${{ github.sha }}" > version_githash.txt
|
||||
ls -ltr
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
|
||||
|
||||
@@ -21,3 +21,5 @@ venv/
|
||||
|
||||
# Docker
|
||||
.env
|
||||
version.txt
|
||||
version_githash.txt
|
||||
@@ -54,6 +54,8 @@ COPY sounds/ ./sounds/
|
||||
COPY ../wakewords/ ./wakewords/
|
||||
COPY linux_voice_assistant/ ./linux_voice_assistant/
|
||||
COPY docker-entrypoint.sh ./
|
||||
COPY version.txt ./
|
||||
COPY version_githash.txt ./
|
||||
|
||||
### Run installation:
|
||||
RUN chmod +x docker-entrypoint.sh
|
||||
|
||||
@@ -23,18 +23,22 @@ Experimental Linux-Voice-Assistant for [Home Assistant](https://www.home-assista
|
||||
|
||||
### Hardware:
|
||||
|
||||
You can for example use the Raspberry Pi Zero 2W with the [Satellite1 Hat Board](https://futureproofhomes.net/products/satellite1-top-microphone-board), the [Respeaker Lite](https://wiki.seeedstudio.com/reSpeaker_usb_v3/) or the [Respeaker 2Mic_Hat](https://wiki.seeedstudio.com/ReSpeaker_2_Mics_Pi_HAT/). A list for possible compatible hardware can be found in the [PiCompose documentation](https://github.com/florian-asche/PiCompose) but basically any microphone that works with [Pipewire](https://pipewire.org/) can be used with the prebuild image.
|
||||
You can for example use the Raspberry Pi Zero 2W with the [Satellite1 Hat Board](https://futureproofhomes.net/products/satellite1-top-microphone-board), the [Respeaker Lite](https://wiki.seeedstudio.com/reSpeaker_usb_v3/) or the [Respeaker 2Mic_Hat](https://wiki.seeedstudio.com/ReSpeaker_2_Mics_Pi_HAT/).
|
||||
|
||||
A list for possible compatible hardware can be found in the [PiCompose documentation](https://github.com/florian-asche/PiCompose) but basically any microphone that works with [Pipewire](https://pipewire.org/) can be used with the prebuild image.
|
||||
|
||||
### Software:
|
||||
|
||||
#### Installation:
|
||||
|
||||
We have different installation methods available (Docker, systemd), each with its own dedicated instructions.
|
||||
For Raspberry Pi users, we provide a prebuild image that can be flashed to a SD card. See [PiCompose](https://github.com/florian-asche/PiCompose).
|
||||
|
||||
See [Linux-Voice-Assistant - Installation](docs/install.md).
|
||||
For all other users we have different installation methods available (Docker, systemd), each with its own dedicated instructions. See [Linux-Voice-Assistant - Installation](docs/install.md).
|
||||
|
||||
#### Parameter overview:
|
||||
|
||||
💡 **Note:** There is a [environment variable](docs/install_application.md#environment-variables-reference) for each parameter if you use docker or systemd based setup.
|
||||
|
||||
``` sh
|
||||
usage: __main__.py [-h] [--name NAME] [--audio-input-device AUDIO_INPUT_DEVICE] [--list-input-devices] [--audio-input-block-size AUDIO_INPUT_BLOCK_SIZE] [--audio-output-device AUDIO_OUTPUT_DEVICE] [--list-output-devices] [--wake-word-dir WAKE_WORD_DIR]
|
||||
[--wake-model WAKE_MODEL] [--stop-model STOP_MODEL] [--download-dir DOWNLOAD_DIR] [--refractory-seconds REFRACTORY_SECONDS] [--wakeup-sound WAKEUP_SOUND] [--timer-finished-sound TIMER_FINISHED_SOUND] [--processing-sound PROCESSING_SOUND]
|
||||
@@ -44,9 +48,9 @@ usage: __main__.py [-h] [--name NAME] [--audio-input-device AUDIO_INPUT_DEVICE]
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `--name` | Name of the voice assistant device (required) | Autogenerated (`lva-MAC-ADDRESS`) |
|
||||
| `--audio-input-device` | Soundcard name for input device | System default microphone |
|
||||
| `--audio-input-device` | Soundcard name for input device | Autodetected |
|
||||
| `--audio-input-block-size` | Audio input block size in samples | 1024 |
|
||||
| `--audio-output-device` | mpv name for output device | System default speaker |
|
||||
| `--audio-output-device` | mpv name for output device | Autodetected |
|
||||
| `--wake-word-dir` | Directory with wake word models (.tflite) and configs (.json) | `wakewords/` |
|
||||
| `--wake-model` | ID of active wake word model | `okay_nabu` |
|
||||
| `--stop-model` | ID of stop model | `stop` |
|
||||
|
||||
@@ -4,14 +4,16 @@ services:
|
||||
fix-permissions:
|
||||
image: "ghcr.io/ohf-voice/linux-voice-assistant:latest"
|
||||
entrypoint: []
|
||||
command: "chown -R ${LVA_USER_ID}:${LVA_USER_GROUP} /app/local /app/configuration"
|
||||
command: "chown -R ${LVA_USER_ID}:${LVA_USER_GROUP} /app/local /app/configuration /app/wakewords/custom /app/sounds/custom"
|
||||
env_file: .env
|
||||
group_add:
|
||||
- audio
|
||||
volumes:
|
||||
# Wakeword data and configuration
|
||||
- wakeword_data:/app/local
|
||||
- wakeword_custom:/app/wakewords/custom
|
||||
- configuration:/app/configuration
|
||||
- sounds_custom:/app/sounds/custom
|
||||
restart: "no"
|
||||
|
||||
# Linux Voice Assistant
|
||||
@@ -34,7 +36,9 @@ services:
|
||||
volumes:
|
||||
# Wakeword data and configuration
|
||||
- wakeword_data:/app/local
|
||||
- wakeword_custom:/app/wakewords/custom
|
||||
- configuration:/app/configuration
|
||||
- sounds_custom:/app/sounds/custom
|
||||
# Time and zone
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
@@ -43,6 +47,8 @@ services:
|
||||
- ${LVA_XDG_RUNTIME_DIR}:${LVA_XDG_RUNTIME_DIR}
|
||||
# Mount source code for development
|
||||
- ./linux_voice_assistant:/app/linux_voice_assistant
|
||||
- ./version.txt:/app/version.txt
|
||||
- ./version_githash.txt:/app/version_githash.txt
|
||||
depends_on:
|
||||
- fix-permissions
|
||||
# Health check for process
|
||||
@@ -59,4 +65,6 @@ services:
|
||||
|
||||
volumes:
|
||||
wakeword_data:
|
||||
wakeword_custom:
|
||||
configuration:
|
||||
sounds_custom:
|
||||
+7
-1
@@ -4,14 +4,16 @@ services:
|
||||
fix-permissions:
|
||||
image: "ghcr.io/ohf-voice/linux-voice-assistant:latest"
|
||||
entrypoint: []
|
||||
command: "chown -R ${LVA_USER_ID}:${LVA_USER_GROUP} /app/local /app/configuration"
|
||||
command: "chown -R ${LVA_USER_ID}:${LVA_USER_GROUP} /app/local /app/configuration /app/wakewords/custom /app/sounds/custom"
|
||||
env_file: .env
|
||||
group_add:
|
||||
- audio
|
||||
volumes:
|
||||
# Wakeword data and configuration
|
||||
- wakeword_data:/app/local
|
||||
- wakeword_custom:/app/wakewords/custom
|
||||
- configuration:/app/configuration
|
||||
- sounds_custom:/app/sounds/custom
|
||||
restart: "no"
|
||||
|
||||
# Linux Voice Assistant
|
||||
@@ -34,7 +36,9 @@ services:
|
||||
volumes:
|
||||
# Wakeword data and configuration
|
||||
- wakeword_data:/app/local
|
||||
- wakeword_custom:/app/wakewords/custom
|
||||
- configuration:/app/configuration
|
||||
- sounds_custom:/app/sounds/custom
|
||||
# Time and zone
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
@@ -57,4 +61,6 @@ services:
|
||||
|
||||
volumes:
|
||||
wakeword_data:
|
||||
wakeword_custom:
|
||||
configuration:
|
||||
sounds_custom:
|
||||
@@ -44,6 +44,10 @@ if [ "$ENABLE_THINKING_SOUND" = "1" ]; then
|
||||
EXTRA_ARGS+=( "--enable-thinking-sound" )
|
||||
fi
|
||||
|
||||
if [ -n "${WAKE_WORD_DIR}" ]; then
|
||||
EXTRA_ARGS+=( "--wake-word-dir" "$WAKE_WORD_DIR" )
|
||||
fi
|
||||
|
||||
if [ -n "${WAKE_MODEL}" ]; then
|
||||
EXTRA_ARGS+=( "--wake-model" "$WAKE_MODEL" )
|
||||
fi
|
||||
|
||||
@@ -272,9 +272,10 @@ The following variables can be configured in the `.env` or in the service file:
|
||||
| `NETWORK_INTERFACE` | Autodetected | network card for server |
|
||||
| `HOST` | Autodetected | API server IP-Address, can be 0.0.0.0 for all interfaces, but only one network card works for MAC-ADDRESS and ESP protocol |
|
||||
| `PORT` | `6053` | API server port |
|
||||
| `AUDIO_INPUT_DEVICE` | `default` | Audio input device name |
|
||||
| `AUDIO_OUTPUT_DEVICE` | `default` | Audio output device name |
|
||||
| `AUDIO_INPUT_DEVICE` | Autodetected | Audio input device name |
|
||||
| `AUDIO_OUTPUT_DEVICE` | Autodetected | Audio output device name |
|
||||
| `ENABLE_THINKING_SOUND` | false | Set to "1" to enable thinking sound |
|
||||
| `WAKE_WORD_DIR` | `app/wakewords` | Path to the wake word directory |
|
||||
| `WAKE_MODEL` | `okay_nabu` | Wake word model to use |
|
||||
| `REFACTORY_SECONDS` | `2` | Refractory period in seconds after wake word |
|
||||
| `WAKEUP_SOUND` | `sounds/wake_word_triggered.flac` | Sound file for wake word triggered |
|
||||
@@ -283,6 +284,14 @@ The following variables can be configured in the `.env` or in the service file:
|
||||
| `MUTE_SOUND` | `sounds/mute_switch_on.flac` | Sound file for mute on |
|
||||
| `UNMUTE_SOUND` | `sounds/mute_switch_off.flac` | Sound file for Configure Audio Devices
|
||||
|
||||
|
||||
### Use own soundfiles:
|
||||
|
||||
If you want to use your own sounds, you can add them to the `sounds/custom` aka `/var/lib/docker/volumes/lva_sounds_custom/_data` directory and reference them in the `.env` file.
|
||||
|
||||
|
||||
### Wake Word:
|
||||
|
||||
#### Available Wake Word Models:
|
||||
|
||||
The following wake word models are available:
|
||||
@@ -297,13 +306,13 @@ The following wake word models are available:
|
||||
- `choo_choo_homie` - Choo Choo Homie wake word
|
||||
|
||||
|
||||
### Wake Word:
|
||||
### Custom Wake Word:
|
||||
|
||||
💡 **Note:** The default value for this parameter is the `wakewords` directory in the repository.
|
||||
If you want to use your own wakewords, you can add them to the `wakewords/custom` aka `/var/lib/docker/volumes/lva_wakeword_custom/_data` directory and reference them in the `.env` file.
|
||||
|
||||
Change the default wake word with `--wake-model <id>` where `<id>` is the name of a model in the `wakewords` directory. For example, `--wake-model hey_jarvis` will load `wakewords/hey_jarvis.tflite` by default.
|
||||
Change the default wake word with `WAKE_MODEL <id>` where `<id>` is the name of a model in the `wakewords` directory. For example, `WAKE_MODEL hey_jarvis` will load `app/wakewords/hey_jarvis.tflite` by default.
|
||||
|
||||
You can include more wakeword directories by adding `--wake-word-dir <DIR>` where `<DIR>` contains either [microWakeWord][] or [openWakeWord][] config files and `.tflite` models. For example, `--wake-word-dir wakewords/openWakeWord` will include the default wake words for openWakeWord.
|
||||
You can include more wakeword directories by adding `WAKE_WORD_DIR <DIR>` where `<DIR>` contains either [microWakeWord][] or [openWakeWord][] config files and `.tflite` models. For example, `WAKE_WORD_DIR app/wakewords/openWakeWord` will include the default wake words for openWakeWord.
|
||||
|
||||
If you want to add [other wakewords][wakewords-collection], make sure to create a small JSON config file to identify it as an openWakeWord model. For example, download the [GLaDOS][glados] model to `glados.tflite` and create `glados.json` with:
|
||||
|
||||
@@ -315,7 +324,7 @@ If you want to add [other wakewords][wakewords-collection], make sure to create
|
||||
}
|
||||
```
|
||||
|
||||
Add `--wake-word-dir <DIR>` with the directory containing `glados.tflite` and `glados.json` to your command-line.
|
||||
Add `WAKE_WORD_DIR <DIR>` with the directory containing `glados.tflite` and `glados.json` to your command-line.
|
||||
|
||||
#### Additional Wakewords:
|
||||
|
||||
|
||||
+349
-23
@@ -1,12 +1,16 @@
|
||||
# Install Audioservice
|
||||
|
||||
For Linux-Voice-Assistant a Pulseaudio connection to the soundcard is required. Since PulseAudio is not installed by default on Ubuntu 22.04 we also support Pipewire. You can choose either one (A or B) of them.
|
||||
For Linux-Voice-Assistant a Pulseaudio connection to the soundcard is required. Since PulseAudio is not installed by default on Ubuntu 22.04 we also support Pipewire with pipewire-pulse. You can choose either variant A or B from this installation documentation.
|
||||
|
||||
**How does the communication work?**
|
||||
|
||||
The Linux-Voice-Assistant container connects to the PulseAudio(PipeWire-Pulse) server running on the host system. The container does not have direct access to the sound card hardware. Instead, it communicates with the PulseAudio running on the host system, which then handles the actual audio processing and output.
|
||||
|
||||
## A) Pipewire (recommended):
|
||||
|
||||
PipeWire is a multimedia server that provides low-latency audio/video handling. Install it with the following commands:
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
# Update package database
|
||||
sudo apt update
|
||||
|
||||
@@ -16,13 +20,13 @@ sudo apt install -y pipewire wireplumber pipewire-audio-client-libraries libspa-
|
||||
|
||||
Link the PipeWire configuration for ALSA applications:
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
sudo ln -s /usr/share/alsa/alsa.conf.d/50-pipewire.conf /etc/alsa/conf.d/
|
||||
```
|
||||
|
||||
Allow services to run without an active user session (optional, for headless setups):
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
sudo mkdir -p /var/lib/systemd/linger
|
||||
sudo touch /var/lib/systemd/linger/$USER
|
||||
```
|
||||
@@ -31,86 +35,408 @@ sudo touch /var/lib/systemd/linger/$USER
|
||||
|
||||
### Configure PipeWire (optional):
|
||||
|
||||
💡 **Note:** If you are not on a desktop system, which is already configured for PipeWire, you can configure it manually.
|
||||
|
||||
Create the PipeWire configuration directory:
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
sudo mkdir -p "/etc/pipewire"
|
||||
```
|
||||
|
||||
Create the file `/etc/pipewire/pipewire.conf` with the following content (minimal configuration for voice assistant use):
|
||||
|
||||
``` ini
|
||||
# Daemon config file for PipeWire
|
||||
```ini
|
||||
# Daemon config file for PipeWire version "1.2.7" #
|
||||
#
|
||||
# Copy and edit this file in /etc/pipewire for system-wide changes
|
||||
# or in ~/.config/pipewire for local changes.
|
||||
#
|
||||
# It is also possible to place a file with an updated section in
|
||||
# /etc/pipewire/pipewire.conf.d/ for system-wide changes or in
|
||||
# ~/.config/pipewire/pipewire.conf.d/ for local changes.
|
||||
#
|
||||
|
||||
context.properties = {
|
||||
link.max-buffers = 16
|
||||
mem.warn-mlock = true
|
||||
log.level = 3
|
||||
context.num-data-loops = 1
|
||||
core.daemon = true
|
||||
core.name = pipewire-0
|
||||
default.clock.rate = 16000
|
||||
## Configure properties in the system.
|
||||
#library.name.system = support/libspa-support
|
||||
#context.data-loop.library.name.system = support/libspa-support
|
||||
#support.dbus = true
|
||||
#link.max-buffers = 64
|
||||
link.max-buffers = 16 # version < 3 clients can't handle more
|
||||
mem.warn-mlock = true
|
||||
#mem.allow-mlock = true
|
||||
#mem.mlock-all = false
|
||||
#clock.power-of-two-quantum = true
|
||||
log.level = 3
|
||||
#cpu.zero.denormals = false
|
||||
|
||||
#loop.rt-prio = -1 # -1 = use module-rt prio, 0 disable rt
|
||||
#loop.class = data.rt
|
||||
#thread.affinity = [ 0 1 ] # optional array of CPUs
|
||||
context.num-data-loops = 1 # -1 = num-cpus, 0 = no data loops
|
||||
#
|
||||
#context.data-loops = [
|
||||
# { loop.rt-prio = -1
|
||||
# loop.class = [ data.rt audio.rt ]
|
||||
# #library.name.system = support/libspa-support
|
||||
# thread.name = data-loop.0
|
||||
# #thread.affinity = [ 0 1 ] # optional array of CPUs
|
||||
# }
|
||||
#]
|
||||
|
||||
core.daemon = true # listening for socket connections
|
||||
core.name = pipewire-0 # core name and socket name
|
||||
|
||||
## Properties for the DSP configuration.
|
||||
default.clock.rate = 16000
|
||||
#default.clock.allowed-rates = [ 48000 ]
|
||||
#default.clock.quantum = 1024
|
||||
#default.clock.min-quantum = 32
|
||||
#default.clock.max-quantum = 2048
|
||||
#default.clock.quantum-limit = 8192
|
||||
#default.clock.quantum-floor = 4
|
||||
#default.video.width = 640
|
||||
#default.video.height = 480
|
||||
#default.video.rate.num = 25
|
||||
#default.video.rate.denom = 1
|
||||
#
|
||||
#settings.check-quantum = false
|
||||
#settings.check-rate = false
|
||||
|
||||
# keys checked below to disable module loading
|
||||
module.x11.bell = true
|
||||
# enables autoloading of access module, when disabled an alternative
|
||||
# access module needs to be loaded.
|
||||
module.access = true
|
||||
# enables autoloading of module-jackdbus-detect
|
||||
module.jackdbus-detect = true
|
||||
}
|
||||
|
||||
context.properties.rules = [
|
||||
{ matches = [ { cpu.vm.name = !null } ]
|
||||
actions = {
|
||||
update-props = {
|
||||
# These overrides are only applied when running in a vm.
|
||||
default.clock.min-quantum = 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
context.spa-libs = {
|
||||
#<factory-name regex> = <library-name>
|
||||
#
|
||||
# Used to find spa factory names. It maps an spa factory name
|
||||
# regular expression to a library name that should contain
|
||||
# that factory.
|
||||
#
|
||||
audio.convert.* = audioconvert/libspa-audioconvert
|
||||
avb.* = avb/libspa-avb
|
||||
api.alsa.* = alsa/libspa-alsa
|
||||
#api.v4l2.* = v4l2/libspa-v4l2
|
||||
#api.libcamera.* = libcamera/libspa-libcamera
|
||||
api.bluez5.* = bluez5/libspa-bluez5
|
||||
api.vulkan.* = vulkan/libspa-vulkan
|
||||
api.jack.* = jack/libspa-jack
|
||||
support.* = support/libspa-support
|
||||
#video.convert.* = videoconvert/libspa-videoconvert
|
||||
#videotestsrc = videotestsrc/libspa-videotestsrc
|
||||
#audiotestsrc = audiotestsrc/libspa-audiotestsrc
|
||||
}
|
||||
|
||||
context.modules = [
|
||||
#{ name = <module-name>
|
||||
# ( args = { <key> = <value> ... } )
|
||||
# ( flags = [ ( ifexists ) ( nofail ) ] )
|
||||
# ( condition = [ { <key> = <value> ... } ... ] )
|
||||
#}
|
||||
#
|
||||
# Loads a module with the given parameters.
|
||||
# If ifexists is given, the module is ignored when it is not found.
|
||||
# If nofail is given, module initialization failures are ignored.
|
||||
# If condition is given, the module is loaded only when the context
|
||||
# properties all match the match rules.
|
||||
#
|
||||
|
||||
# Uses realtime scheduling to boost the audio thread priorities. This uses
|
||||
# RTKit if the user doesn't have permission to use regular realtime
|
||||
# scheduling. You can also clamp utilisation values to improve scheduling
|
||||
# on embedded and heterogeneous systems, e.g. Arm big.LITTLE devices.
|
||||
{ name = libpipewire-module-rt
|
||||
args = {
|
||||
nice.level = -11
|
||||
rt.prio = 88
|
||||
nice.level = -11
|
||||
rt.prio = 88
|
||||
#rt.time.soft = -1
|
||||
#rt.time.hard = -1
|
||||
#uclamp.min = 0
|
||||
#uclamp.max = 1024
|
||||
}
|
||||
flags = [ ifexists nofail ]
|
||||
}
|
||||
{ name = libpipewire-module-protocol-native }
|
||||
|
||||
# The native communication protocol.
|
||||
{ name = libpipewire-module-protocol-native
|
||||
args = {
|
||||
# List of server Unix sockets, and optionally permissions
|
||||
#sockets = [ { name = "pipewire-0" }, { name = "pipewire-0-manager" } ]
|
||||
}
|
||||
}
|
||||
|
||||
# The profile module. Allows application to access profiler
|
||||
# and performance data. It provides an interface that is used
|
||||
# by pw-top and pw-profiler.
|
||||
{ name = libpipewire-module-profiler }
|
||||
|
||||
# Allows applications to create metadata objects. It creates
|
||||
# a factory for Metadata objects.
|
||||
{ name = libpipewire-module-metadata }
|
||||
|
||||
# Creates a factory for making devices that run in the
|
||||
# context of the PipeWire server.
|
||||
{ name = libpipewire-module-spa-device-factory }
|
||||
|
||||
# Creates a factory for making nodes that run in the
|
||||
# context of the PipeWire server.
|
||||
{ name = libpipewire-module-spa-node-factory }
|
||||
|
||||
# Allows creating nodes that run in the context of the
|
||||
# client. Is used by all clients that want to provide
|
||||
# data to PipeWire.
|
||||
{ name = libpipewire-module-client-node }
|
||||
|
||||
# Allows creating devices that run in the context of the
|
||||
# client. Is used by the session manager.
|
||||
{ name = libpipewire-module-client-device }
|
||||
|
||||
# The portal module monitors the PID of the portal process
|
||||
# and tags connections with the same PID as portal
|
||||
# connections.
|
||||
{ name = libpipewire-module-portal
|
||||
flags = [ ifexists nofail ]
|
||||
}
|
||||
|
||||
# The access module can perform access checks and block
|
||||
# new clients.
|
||||
{ name = libpipewire-module-access
|
||||
args = {
|
||||
# Socket-specific access permissions
|
||||
#access.socket = { pipewire-0 = "default", pipewire-0-manager = "unrestricted" }
|
||||
|
||||
# Deprecated legacy mode (not socket-based),
|
||||
# for now enabled by default if access.socket is not specified
|
||||
#access.legacy = true
|
||||
}
|
||||
condition = [ { module.access = true } ]
|
||||
}
|
||||
|
||||
# Makes a factory for wrapping nodes in an adapter with a
|
||||
# converter and resampler.
|
||||
{ name = libpipewire-module-adapter }
|
||||
|
||||
# Makes a factory for creating links between ports.
|
||||
{ name = libpipewire-module-link-factory }
|
||||
|
||||
# Provides factories to make session manager objects.
|
||||
{ name = libpipewire-module-session-manager }
|
||||
|
||||
# Use libcanberra to play X11 Bell
|
||||
{ name = libpipewire-module-x11-bell
|
||||
args = {
|
||||
#sink.name = "@DEFAULT_SINK@"
|
||||
#sample.name = "bell-window-system"
|
||||
#x11.display = null
|
||||
#x11.xauthority = null
|
||||
}
|
||||
flags = [ ifexists nofail ]
|
||||
condition = [ { module.x11.bell = true } ]
|
||||
}
|
||||
{ name = libpipewire-module-jackdbus-detect
|
||||
args = {
|
||||
#jack.library = libjack.so.0
|
||||
#jack.server = null
|
||||
#jack.client-name = PipeWire
|
||||
#jack.connect = true
|
||||
#tunnel.mode = duplex # source|sink|duplex
|
||||
source.props = {
|
||||
#audio.channels = 2
|
||||
#midi.ports = 1
|
||||
#audio.position = [ FL FR ]
|
||||
# extra sink properties
|
||||
}
|
||||
sink.props = {
|
||||
#audio.channels = 2
|
||||
#midi.ports = 1
|
||||
#audio.position = [ FL FR ]
|
||||
# extra sink properties
|
||||
}
|
||||
}
|
||||
flags = [ ifexists nofail ]
|
||||
condition = [ { module.jackdbus-detect = true } ]
|
||||
}
|
||||
]
|
||||
|
||||
context.objects = [
|
||||
#{ factory = <factory-name>
|
||||
# ( args = { <key> = <value> ... } )
|
||||
# ( flags = [ ( nofail ) ] )
|
||||
# ( condition = [ { <key> = <value> ... } ... ] )
|
||||
#}
|
||||
#
|
||||
# Creates an object from a PipeWire factory with the given parameters.
|
||||
# If nofail is given, errors are ignored (and no object is created).
|
||||
# If condition is given, the object is created only when the context properties
|
||||
# all match the match rules.
|
||||
#
|
||||
#{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc "Spa:Pod:Object:Param:Props:patternType" = 1 } }
|
||||
#{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
|
||||
#{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
|
||||
#{ factory = spa-node-factory args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
|
||||
#{ factory = adapter args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc } }
|
||||
#{ factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }
|
||||
|
||||
# A default dummy driver. This handles nodes marked with the "node.always-process"
|
||||
# property when no other driver is currently active. JACK clients need this.
|
||||
{ factory = spa-node-factory
|
||||
args = {
|
||||
factory.name = support.node.driver
|
||||
node.name = Dummy-Driver
|
||||
node.group = pipewire.dummy
|
||||
node.sync-group = sync.dummy
|
||||
priority.driver = 200000
|
||||
#clock.id = monotonic # realtime | tai | monotonic-raw | boottime
|
||||
#clock.name = "clock.system.monotonic"
|
||||
}
|
||||
}
|
||||
{ factory = spa-node-factory
|
||||
args = {
|
||||
factory.name = support.node.driver
|
||||
node.name = Freewheel-Driver
|
||||
priority.driver = 190000
|
||||
node.group = pipewire.freewheel
|
||||
node.sync-group = sync.dummy
|
||||
node.freewheel = true
|
||||
#freewheel.wait = 10
|
||||
}
|
||||
}
|
||||
|
||||
# This creates a new Source node. It will have input ports
|
||||
# that you can link, to provide audio for this source.
|
||||
#{ factory = adapter
|
||||
# args = {
|
||||
# factory.name = support.null-audio-sink
|
||||
# node.name = "my-mic"
|
||||
# node.description = "Microphone"
|
||||
# media.class = "Audio/Source/Virtual"
|
||||
# audio.position = "FL,FR"
|
||||
# monitor.passthrough = true
|
||||
# }
|
||||
#}
|
||||
|
||||
# This creates a single PCM source device for the given
|
||||
# alsa device path hw:0. You can change source to sink
|
||||
# to make a sink in the same way.
|
||||
#{ factory = adapter
|
||||
# args = {
|
||||
# factory.name = api.alsa.pcm.source
|
||||
# node.name = "alsa-source"
|
||||
# node.description = "PCM Source"
|
||||
# media.class = "Audio/Source"
|
||||
# api.alsa.path = "hw:0"
|
||||
# api.alsa.period-size = 1024
|
||||
# api.alsa.headroom = 0
|
||||
# api.alsa.disable-mmap = false
|
||||
# api.alsa.disable-batch = false
|
||||
# audio.format = "S16LE"
|
||||
# audio.rate = 48000
|
||||
# audio.channels = 2
|
||||
# audio.position = "FL,FR"
|
||||
# }
|
||||
#}
|
||||
|
||||
# Use the metadata factory to create metadata and some default values.
|
||||
#{ factory = metadata
|
||||
# args = {
|
||||
# metadata.name = my-metadata
|
||||
# metadata.values = [
|
||||
# { key = default.audio.sink value = { name = somesink } }
|
||||
# { key = default.audio.source value = { name = somesource } }
|
||||
# ]
|
||||
# }
|
||||
#}
|
||||
]
|
||||
|
||||
context.exec = [
|
||||
#{ path = <program-name>
|
||||
# ( args = "<arguments>" | [ <arg1> <arg2> ... ] )
|
||||
# ( condition = [ { <key> = <value> ... } ... ] )
|
||||
#}
|
||||
#
|
||||
# Execute the given program with arguments.
|
||||
# If condition is given, the program is executed only when the context
|
||||
# properties all match the match rules.
|
||||
#
|
||||
# You can optionally start the session manager here,
|
||||
# but it is better to start it as a systemd service.
|
||||
# Run the session manager with -h for options.
|
||||
#
|
||||
#{ path = "/usr/bin/pipewire-media-session" args = ""
|
||||
# condition = [ { exec.session-manager = null } { exec.session-manager = true } ] }
|
||||
#
|
||||
# You can optionally start the pulseaudio-server here as well
|
||||
# but it is better to start it as a systemd service.
|
||||
# It can be interesting to start another daemon here that listens
|
||||
# on another address with the -a option (eg. -a tcp:4713).
|
||||
#
|
||||
#{ path = "/usr/bin/pipewire" args = [ "-c" "pipewire-pulse.conf" ]
|
||||
# condition = [ { exec.pipewire-pulse = null } { exec.pipewire-pulse = true } ] }
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## B) PulseAudio:
|
||||
|
||||
Make sure that you only run Pulseaudio and there is no Pipewire installed.
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
sudo apt remove --purge pipewire pipewire-pulse wireplumber
|
||||
sudo apt autoremove
|
||||
```
|
||||
|
||||
Install Pulseaudio
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
sudo apt install pulseaudio pulseaudio-utils dfu-util
|
||||
```
|
||||
|
||||
Enable and start Pulseaudio
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
systemctl --user enable pulseaudio
|
||||
systemctl --user start pulseaudio
|
||||
```
|
||||
|
||||
Check if Pulseaudio is running
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
pulseaudio --check
|
||||
pactl info
|
||||
```
|
||||
|
||||
|
||||
## Additional Information:
|
||||
|
||||
### Debug audio output:
|
||||
|
||||
You can debug the audio output with the following command:
|
||||
|
||||
```bash
|
||||
export LVA_XDG_RUNTIME_DIR=/run/user/${LVA_USER_ID}
|
||||
sudo aplay -L
|
||||
speaker-test -D pulse -c2 -twav
|
||||
```
|
||||
|
||||
💡 **Note:** Replace `$LVA_USER_ID` with your actual user id that you want to run the voice assistant.
|
||||
|
||||
💡 **Note:** You can replace `pulse` with `default` or `alsa_output.pci-0000_00_1f.3.analog-stereo` or any other audio device.
|
||||
|
||||
### Set audio volume:
|
||||
|
||||
If your driver or audiodevice is loaded and you can see the device with `aplay -L` then
|
||||
|
||||
@@ -22,6 +22,8 @@ from getmac import get_mac_address
|
||||
from .zeroconf import HomeAssistantZeroconf
|
||||
from .util import get_default_interface
|
||||
from .util import get_default_ipv4
|
||||
from .util import get_version
|
||||
from .util import get_esphome_version
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MODULE_DIR = Path(__file__).parent
|
||||
@@ -183,13 +185,25 @@ async def main() -> None:
|
||||
|
||||
# Resolve name
|
||||
if not args.name:
|
||||
print("No name specified, try to autogenerate name")
|
||||
device_name = f"lva-{mac_address_clean}"
|
||||
print(f"Name autogenerated: {device_name}")
|
||||
print("No friendly name specified, try to autogenerate name")
|
||||
friendly_name = f"LVA - {mac_address_clean}"
|
||||
print(f"Friendly name autogenerated: {friendly_name}")
|
||||
else:
|
||||
print("Name specified")
|
||||
print(f"Using name: {args.name}")
|
||||
device_name = args.name
|
||||
print("Friendly name specified")
|
||||
print(f"Using friendly name: {args.name}")
|
||||
friendly_name = args.name
|
||||
|
||||
mac_no_colon = mac_address.replace(":", "").lower()
|
||||
device_name = f"lva-{mac_no_colon}"
|
||||
print(f"Device name: {device_name}")
|
||||
|
||||
# Resolve version
|
||||
version = get_version()
|
||||
print(f"Version: {version}")
|
||||
|
||||
# Resolve esphome version
|
||||
esphome_version = get_esphome_version()
|
||||
print(f"ESPHome api version: {esphome_version}")
|
||||
|
||||
# Resolve download dir
|
||||
args.download_dir = Path(args.download_dir)
|
||||
@@ -288,9 +302,12 @@ async def main() -> None:
|
||||
|
||||
state = ServerState(
|
||||
name=device_name,
|
||||
friendly_name=friendly_name,
|
||||
network_interface=network_interface,
|
||||
mac_address=get_mac_address(interface=network_interface),
|
||||
ip_address=host_ip_address,
|
||||
version=version,
|
||||
esphome_version=esphome_version,
|
||||
audio_queue=Queue(),
|
||||
entities=[],
|
||||
available_wake_words=available_wake_words,
|
||||
|
||||
@@ -63,9 +63,12 @@ class Preferences:
|
||||
@dataclass
|
||||
class ServerState:
|
||||
name: str
|
||||
friendly_name: str
|
||||
mac_address: str
|
||||
ip_address: str
|
||||
network_interface: str
|
||||
version: str
|
||||
esphome_version: str
|
||||
audio_queue: "Queue[Optional[bytes]]"
|
||||
entities: "List[ESPHomeEntity]"
|
||||
available_wake_words: "Dict[str, AvailableWakeWord]"
|
||||
|
||||
@@ -291,15 +291,15 @@ class VoiceSatelliteProtocol(APIServer):
|
||||
elif isinstance(msg, VoiceAssistantTimerEventResponse):
|
||||
self.handle_timer_event(VoiceAssistantTimerEventType(msg.event_type), msg)
|
||||
elif isinstance(msg, DeviceInfoRequest):
|
||||
# Compute dynamic device name
|
||||
base_name = re.sub(r"[\s-]+", "-", self.state.name.lower()).strip("-")
|
||||
mac_no_colon = self.state.mac_address.replace(":", "").lower()
|
||||
mac_last6 = mac_no_colon[-6:]
|
||||
device_name = f"{base_name}-{mac_last6}"
|
||||
_LOGGER.debug("Device info request")
|
||||
|
||||
yield DeviceInfoResponse(
|
||||
uses_password=False,
|
||||
name=device_name,
|
||||
name=self.state.name,
|
||||
friendly_name=self.state.friendly_name,
|
||||
project_name="Open Home Foundation.Linux Voice Assistant",
|
||||
project_version=self.state.version,
|
||||
esphome_version=self.state.esphome_version,
|
||||
mac_address=self.state.mac_address,
|
||||
manufacturer="Open Home Foundation",
|
||||
model="Linux Voice Assistant",
|
||||
|
||||
@@ -4,8 +4,68 @@ import uuid
|
||||
# netifaces lib is from netifaces2
|
||||
import netifaces
|
||||
from collections.abc import Callable
|
||||
from importlib.metadata import version, PackageNotFoundError
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Cache for version to avoid repeated file reading
|
||||
_version_cache: Optional[str] = None
|
||||
_esphome_version_cache: Optional[str] = None
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""
|
||||
Read the version from version.txt file.
|
||||
|
||||
This function reads the content safely without risk of code injection,
|
||||
as it only reads raw text and performs no evaluation.
|
||||
|
||||
Returns:
|
||||
str: The version from version.txt or 'unknown' if the file
|
||||
does not exist or cannot be read.
|
||||
"""
|
||||
global _version_cache
|
||||
|
||||
if _version_cache is not None:
|
||||
return _version_cache
|
||||
|
||||
version_file = Path(__file__).parent.parent / "version.txt"
|
||||
|
||||
try:
|
||||
# Sicher lesen: nur Rohtext, keine Evaluierung
|
||||
version = version_file.read_text(encoding="utf-8").strip()
|
||||
_version_cache = version if version else "unknown"
|
||||
except (FileNotFoundError, PermissionError, OSError):
|
||||
_version_cache = "unknown"
|
||||
|
||||
return _version_cache
|
||||
|
||||
|
||||
def get_esphome_version() -> str:
|
||||
"""
|
||||
Read the version of the installed aioesphomeapi package.
|
||||
|
||||
This function uses importlib.metadata to safely retrieve the version
|
||||
of an installed Python package without executing any code from the
|
||||
package itself.
|
||||
|
||||
Returns:
|
||||
str: The version of aioesphomeapi (e.g., '42.7.0'), or 'unknown'
|
||||
if the package is not installed or the version cannot be read.
|
||||
"""
|
||||
global _esphome_version_cache
|
||||
|
||||
if _esphome_version_cache is not None:
|
||||
return _esphome_version_cache
|
||||
|
||||
try:
|
||||
_esphome_version_cache = version("aioesphomeapi")
|
||||
except PackageNotFoundError:
|
||||
_esphome_version_cache = "unknown"
|
||||
|
||||
return _esphome_version_cache
|
||||
|
||||
|
||||
def call_all(*callables: Optional[Callable[[], None]]) -> None:
|
||||
for item in filter(None, callables):
|
||||
item()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
placeholder
|
||||
@@ -0,0 +1 @@
|
||||
placeholder
|
||||
Reference in new issue
Block a user