Compare commits

..
20 Commits
Author SHA1 Message Date
Omar Amin acda765964 Fix conditional check for DISABLE_PERIPHERAL_API 2026-07-27 13:37:22 +03:00
Omar Amin 3077e7230b Add optional peripheral API configuration options
Added optional configuration settings for peripheral API in .env.example.
2026-07-27 13:35:36 +03:00
dependabot[bot] ea991e026f Bump docker/login-action from 4.5.0 to 4.5.1 (#377)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/06fb636fac595d6fb4b28a5dfcb21a6f5091859c...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-26 18:46:17 +02:00
SpiliosDmk 69a4fd3098 docs: add realtime scheduling tuning for constrained hardware (#376)
The core problem in #164 (config fully overriding pipewire.conf) was
already fixed by switching to pipewire.conf.d. This adds the
remaining performance-tuning recommendation (libpipewire-module-rt)
from the issue thread that never made it into the docs, to reduce
audio dropouts on hardware like the Raspberry Pi 3B.

Closes #164
2026-07-26 16:01:33 +02:00
dependabot[bot] 126bdf1862 Bump docker/login-action from 4.4.0 to 4.5.0 (#375)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...06fb636fac595d6fb4b28a5dfcb21a6f5091859c)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:41:26 +02:00
Florian Asche 3b33730105 feat(ai): integrate Kilo agent framework and coding instructions (#366)
* feat(ai): integrate Kilo agent framework and coding instructions

Add configuration files, agent definitions, and skill sets to enable
AI-driven development workflows and standardized agent behavior.
2026-07-24 17:35:34 +02:00
dependabot[bot] 0f57ca989c Bump actions/checkout from 4 to 7 (#370)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 01:12:30 +02:00
litinoveweedleandOmar Amin ba7d834d1f fix race condition between LVA registration to the HA and peripheral registering tot the LVA (#373)
Co-authored-by: Omar Amin <65174210+omaramin-2000@users.noreply.github.com>
2026-07-22 01:10:10 +02:00
aryanhasgithubandFlorian Asche 2eb29470f9 Colored Debug Logging and move Voice Event to Info level in logger (#271)
* Added Colored Debug logging and arhument and changes voice event log to info

---------

Co-authored-by: Florian Asche <github@florian-asche.de>
2026-07-22 01:09:13 +02:00
dependabot[bot] 2378187197 Bump docker/setup-buildx-action from 3 to 4 (#371)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 19:40:21 +02:00
maximilianovermeyer d3874f6a2f Update install.md - Fixed log path (#372)
Fixed a typo in the path of the log dir.
2026-07-21 19:39:52 +02:00
litinoveweedleandFlorian Asche 201896196f fix Respeaker USB Mic Array v2 LEDs (#357)
Fix ReSpeaker USB LED control and suppress stale peripheral disconnect replay

---------

Co-authored-by: Florian Asche <github@florian-asche.de>
2026-07-21 19:16:10 +03:00
Florian Asche 8f788eeff0 chore(ci): update github actions and docker dependencies (#368)
- Update `actions/checkout` from v3 to v4 in workflows
- Enable `allow-unsafe-pr-checkout` in `docker-build-pr.yml` to support
  PR builds with updated checkout version
2026-07-20 18:48:13 +02:00
dependabot[bot] 305b787dd6 Bump docker/login-action from 3.1.0 to 4.4.0 (#360)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.1.0 to 4.4.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/e92390c5fb421da1463c202d546fed0ec5c39f20...af1e73f918a031802d376d3c8bbc3fe56130a9b0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 18:40:20 +02:00
dependabot[bot] e14844a113 Bump docker/setup-qemu-action from 3 to 4 (#361)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 18:40:11 +02:00
dependabot[bot] 9ed97cd8f4 Bump docker/metadata-action from 5.5.1 to 6.2.0 (#362)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.5.1 to 6.2.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/8e5442c4ef9f78752691e2d8f8d19755c6f78e81...dc802804100637a589fabce1cb79ff13a1411302)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 18:39:57 +02:00
dependabot[bot] e8042ee19d Bump actions/attest-build-provenance from 1 to 4 (#363)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1 to 4.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v1...v4)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 18:39:47 +02:00
dependabot[bot] 802908cb8f Bump docker/build-push-action from 5.3.0 to 7.3.0 (#364)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.3.0 to 7.3.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/2cdde995de11925a030ce8070c3d77a52ffcf1c0...53b7df96c91f9c12dcc8a07bcb9ccacbed38856a)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 18:39:37 +02:00
Florian Asche c37b482b87 docs(github): establish repository governance and automation workflows (#359)
Introduce essential GitHub repository files to standardize community
contributions and automate maintenance tasks.

- Add AI usage policy and contribution guidelines
- Implement issue and pull request templates for consistent reporting
- Configure Dependabot for automated dependency updates
- Add workflow for automated PR labeling
- Include funding information and repository metadata
2026-07-20 17:41:57 +02:00
Florian Asche 486ff4bb53 build(dev): enhance local development environment and setup workflow (#358)
Improve the developer experience by providing structured setup scripts,
VS Code workspace configurations, and detailed documentation.

- Add VS Code workspace configuration and recommended settings
- Update README.md with system requirements and VS Code setup instructions
- Refactor `script/setup` to include Python 3.11+ validation and system dependency checks
- Add option to skip PulseAudio wait in `docker-entrypoint.sh` for local development
- Update `.gitignore` to ensure consistent version file handling
2026-07-20 17:38:52 +02:00
39 changed files with 976 additions and 101 deletions

No files matched your search

+18
View File
@@ -8,6 +8,9 @@
### Enable debug mode (optional):
# ENABLE_DEBUG="1"
### Enable colored debug mode (optional):
# ENABLE_COLORED_DEBUG="1"
### List audio devices (optional):
# if enabled normal startup is disabled
# LIST_DEVICES="1"
@@ -64,6 +67,21 @@ LVA_PULSE_COOKIE="/run/user/${LVA_USER_ID}/pulse/cookie"
# Start listening immediately after wake word detection, without waiting for the wake sound to finish
# LISTEN_DURING_WAKE_SOUND="1"
### Perpheral api host (optional):
# Bind address for the peripheral WebSocket API
# PERIPHERAL_HOST="0.0.0.0"
### Perpheral api port (optional):
# Port for the peripheral WebSocket API
# PERIPHERAL_PORT="6055"
### Peripheral api volume step (optional):
# Volume change for peripheral api per button press, 0.0–1.0
# PERIPHERAL_VOLUME_STEP="%(default)s"
### Disable the peripheral WebSocket API entirely
# DISABLE_PERIPHERAL_API="1"
### Wake word directory (optional):
# path for custom files in docker is for example "app/wakewords/custom"
# WAKE_WORD_DIR="app/wakewords"
+39
View File
@@ -0,0 +1,39 @@
# Linux Voice Assistant - AI Policy
We support using AI (i.e., LLMs) as tools when contributing to Linux Voice Assistant. However, you are responsible for any contributions you submit, and we are responsible for any contributions we merge and release. We hold a high bar for all contributions to our projects.
Our maintainers dedicate their time and expertise to reviewing contributions. Submitting AI-generated content that you have not personally reviewed and understood wastes that time and will not be accepted.
## Autonomous agents
We do not allow autonomous agents to be used for contributing to our projects. We will close any pull requests or issues that we believe were created autonomously, and may mark automated comments as spam. This includes contributions that bypass the provided issue or pull request templates.
## Communication on issues, pull requests, and code reviews
We don't mind if you use AI tools to help you write. However, do not have tools post unreviewed content on your behalf. Keep responses to the minimum needed to communicate your intent. We may hide any comments that we believe are unreviewed AI output.
If you are opening a pull request, we expect you to be able to explain the proposed changes in your own words. This includes the pull request description and responses to questions. If you use AI to help generate the pull request summary, you must review it for technical accuracy.
**Do not use AI to generate answers to questions from maintainers.** You should understand and be able to explain your own work. Using AI to improve grammar or clarity is fine, but the substance of your responses must be your own.
If you wish to include context from an interaction with AI in your comments, it must be in a quote block (e.g., using `>`) and disclosed as such. It must be accompanied by your own commentary explaining the relevance and implications of the context. Do not share long snippets.
## Non-native English speakers
We understand that AI is useful when communicating as a non-native English speaker. If you are using AI to translate your comments, please ensure the translation accurately reflects your intent. Including your original text in a details block shows the effort behind your contribution, helps maintainers verify the translation if needed, and keeps the conversation readable.
## Code and documentation contributions
AI can be a helpful tool for writing code and documentation. However, due to the foundational open source nature of our projects, we require a human in the loop who understands the work produced by AI.
All contributions must be reviewed and understood by the contributor before submission. You should be able to explain every change in a pull request you submit. Pull requests that appear to be unreviewed AI output will be closed without review.
## Our use of AI
Some of our projects use AI tools to assist with code reviews, issue triaging, reporting, and other project management tasks. These tools may leave comments on pull requests or issues. As with any automated tooling, these comments are not always correct.
If an AI tool leaves a comment on your contribution, treat it as you would any other review comment. If you believe it is incorrect, say so; a brief explanation is sufficient. Maintainers always have the final say. If in doubt, ask a maintainer.
## Enforcement
Contributions that do not follow this policy will be closed. Repeated violations may result in being blocked from contributing to LVA projects.
+22
View File
@@ -0,0 +1,22 @@
# Contributing to Linux Voice Assistant
Everybody is invited and welcome to contribute to Linux Voice Assistant. Even if you are not a developer, perhaps you would like to help with the documentation? If you are a developer and have a feature that isn't supported by LVA yet, why not spend some time and help to integrate it?
The process is straight-forward.
- Fork the LVA repository.
- Write the code for your feature or fix.
- Ensure tests pass and linting succeeds (`./script/lint` and `./script/tests`).
- Create a Pull Request against the `main` branch.
## Feature suggestions
If you want to suggest a new feature for LVA, please open a discussion on GitHub.
## Issue Tracker
If you want to report an issue, please create an issue on the repository.
## AI Policy
Linux Voice Assistant is an [Open Home Foundation](https://www.openhomefoundation.org/) project and follows the [OHF AI Policy](https://github.com/music-assistant/.github/blob/main/AI_POLICY.md). Please read it before submitting AI-assisted contributions. The TLDR: AI tools are allowed, but a human is responsible and must review and understand every change. Autonomous agents and unreviewed AI output will be rejected.
+1
View File
@@ -0,0 +1 @@
custom: https://www.openhomefoundation.org
+14
View File
@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: "🐛 Report a bug"
url: https://github.com/OHF-Voice/linux-voice-assistant/issues/new?template=bug_report.md
about: Report bugs in the Linux Voice Assistant.
- name: "💬 Discord community & support"
url: https://discord.com/channels/330944238910963714/1464387232201703547
about: Chat with the community and get help from other users.
- name: "❓ Questions & how-to (Q&A)"
url: https://github.com/OHF-Voice/linux-voice-assistant/discussions
about: Ask how-to questions and search previous answers.
- name: "💡 Feature requests & ideas"
url: https://github.com/OHF-Voice/linux-voice-assistant/discussions
about: Suggest and vote on new features and enhancements.
+29
View File
@@ -0,0 +1,29 @@
# What does this implement/fix?
<!-- Quick description and explanation of changes. -->
**Related issue (if applicable):**
- Fixes #<issue number>
## Types of changes
<!--
Tick exactly one box. CI derives the label from the ticked box.
-->
- [ ] Bugfix (non-breaking change which fixes an issue) — `bugfix`
- [ ] New feature (non-breaking change which adds functionality) — `new-feature`
- [ ] Enhancement to an existing feature — `enhancement`
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — `breaking-change`
- [ ] Refactor (no behaviour change) — `refactor`
- [ ] Documentation only — `documentation`
- [ ] Maintenance / chore — `maintenance`
- [ ] CI / workflow change — `ci`
- [ ] Dependencies bump — `dependencies`
## Checklist
- [ ] The code change is tested and works locally.
- [ ] `./script/lint` passes.
- [ ] `./script/tests` passes, and tests have been added/updated under `tests/` where applicable.
+14
View File
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: weekly
groups:
dependencies:
patterns:
- "*"
+6 -6
View File
@@ -20,23 +20,23 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Set lowercase registry and repository
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
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ github.token }}
# Build Docker Images (amd64 and arm64)
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Create version files
run: |
@@ -46,7 +46,7 @@ jobs:
- name: Build and push Docker image
id: push
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
platforms: linux/amd64,linux/aarch64
@@ -54,7 +54,7 @@ jobs:
tags: ${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}:nightly
labels: "nightly"
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1.4.4
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ github.repository }}
subject-digest: ${{ steps.push.outputs.digest }}
+7 -6
View File
@@ -21,9 +21,10 @@ jobs:
steps:
- name: Checkout PR code
uses: actions/checkout@v3
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
allow-unsafe-pr-checkout: true
- name: Set variables
run: |
@@ -32,16 +33,16 @@ jobs:
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
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ github.token }}
# Build Docker Images (amd64 and arm64)
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Create version files
run: |
@@ -51,7 +52,7 @@ jobs:
- name: Build and push Docker image
id: push
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
platforms: linux/amd64,linux/aarch64
@@ -59,7 +60,7 @@ jobs:
tags: "${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}-pr:${{ env.PR_NUMBER }}"
labels: "${{ env.PR_NUMBER }}"
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1.4.4
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}-pr
subject-digest: ${{ steps.push.outputs.digest }}
+7 -7
View File
@@ -21,21 +21,21 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Log in to the Container registry
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ github.token }}
# Build Docker Images (amd64 and arm64)
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
# E.g. tag v1.2.0 produces 3 tags: '1.2.0', '1.2', and 'latest'
@@ -54,7 +54,7 @@ jobs:
- name: Build and push Docker image
id: push
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
platforms: linux/amd64,linux/aarch64
@@ -62,7 +62,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ github.repository }}
subject-digest: ${{ steps.push.outputs.digest }}
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
ref: main
- uses: rhysd/changelog-from-release/action@v3
+5 -5
View File
@@ -14,7 +14,7 @@ jobs:
lint_black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
@@ -29,7 +29,7 @@ jobs:
lint_flake8:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
@@ -44,7 +44,7 @@ jobs:
lint_isort:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
@@ -59,7 +59,7 @@ jobs:
lint_mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
@@ -75,7 +75,7 @@ jobs:
lint_pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
+87
View File
@@ -0,0 +1,87 @@
name: PR Labels
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- labeled
- unlabeled
- reopened
- ready_for_review
branches:
- main
permissions:
pull-requests: write
jobs:
apply_label:
name: Apply
runs-on: ubuntu-latest
if: github.event.pull_request.user.type != 'Bot'
steps:
- name: 🏷 Apply label from PR description checkbox
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: |
const known = [
'breaking-change', 'bugfix', 'refactor',
'new-feature', 'enhancement', 'maintenance',
'ci', 'dependencies', 'documentation',
];
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;
const current = (context.payload.pull_request.labels || [])
.map(l => l.name);
const alreadyLabelled = current.filter(n => known.includes(n));
if (alreadyLabelled.length === 1) {
core.info(
`Skipping: PR already labelled '${alreadyLabelled[0]}'.`
);
return;
}
const body = context.payload.pull_request.body || '';
const re = /^\s*-\s*\[x\][^`\n]*`([^`]+)`/gim;
const ticked = [];
for (const m of body.matchAll(re)) {
const name = m[1];
if (known.includes(name) && !ticked.includes(name)) {
ticked.push(name);
}
}
if (ticked.length === 0) {
core.setFailed(
'No "Types of changes" checkbox is ticked in the PR ' +
'description. Edit the description and tick exactly ' +
'one box so this PR can be release-noted.'
);
return;
}
if (ticked.length > 1) {
core.setFailed(
`Multiple "Types of changes" checkboxes are ticked ` +
`(${ticked.join(', ')}). Tick exactly one.`
);
return;
}
const desired = ticked[0];
for (const name of current) {
if (known.includes(name) && name !== desired) {
await github.rest.issues.removeLabel({
owner, repo, issue_number, name,
}).catch(() => {});
}
}
if (!current.includes(desired)) {
await github.rest.issues.addLabels({
owner, repo, issue_number, labels: [desired],
});
}
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Set up Python 3.12
uses: actions/setup-python@v5
+1 -1
View File
@@ -23,4 +23,4 @@ venv/
.env
dev.env
version.txt
version_githash.txt
version_githash.txt
+8
View File
@@ -0,0 +1,8 @@
node_modules
package.json
package-lock.json
pnpm-lock.yaml
bun.lock
yarn.lock
worktrees
agent-manager.json
+14
View File
@@ -0,0 +1,14 @@
# Lint Command
Runs all linting checks for the project (black, isort, flake8, mypy, pylint).
## Usage
```
/lint # Run all linting checks
/lint --auto # Auto-fix formatting issues (black, isort)
```
## Command
./script/lint
+14
View File
@@ -0,0 +1,14 @@
# Run Command
Runs the Linux Voice Assistant application.
## Usage
```
/run # Run the application
/run --help # Show CLI help
```
## Command
./script/run
+14
View File
@@ -0,0 +1,14 @@
# Setup Command
Installs project dependencies in a virtual environment.
## Usage
```
/setup # Install dependencies
/setup --dev # Install dev dependencies (linting, testing tools)
```
## Command
./script/setup
+15
View File
@@ -0,0 +1,15 @@
# Test Command
Runs the pytest unit tests for the project.
## Usage
```
/tests # Run all tests
/tests <path> # Run tests in specific path
/tests -k <pattern> # Run tests matching pattern
```
## Command
./script/tests
+70
View File
@@ -0,0 +1,70 @@
# PR Review Standards
## Review Philosophy
- Only comment when confident an issue exists
- Be concise: one sentence per comment when possible
- Focus on actionable feedback, not observations
- When reviewing text, only comment on clarity issues if the text is genuinely confusing or could lead to errors
## What to Analyze
Review all code changes for:
- Code quality and style consistency with the existing codebase
- Potential bugs or issues
- Performance implications
- Blocking IO in async code
- Security concerns
- Test coverage
- Documentation updates if needed
## Project Standards
Respect the project standards as outlined in AGENTS.md. Any deviations must be raised as `[PROBLEM]`.
Key standards for LVA:
- Python 3.11, 3.12, 3.13 supported
- Black formatting with 200 char line length
- isort import sorting with black profile
- mypy type checking with strict settings
- pylint code quality checks (many checks disabled in pyproject.toml)
## Architecture-Specific Checks
- `linux_voice_assistant/wake_word.py` - Wake word model loading is hardware-dependent, verify no assumptions about specific hardware
- `linux_voice_assistant/mpv_player.py` - Audio output requires `libmpv-dev`, check for proper error handling
- `linux_voice_assistant/webrtc.py` - Noise suppression/gain algorithms need careful review for audio quality
- `linux_voice_assistant/satellite.py` - ESPHome API protocol handling must be robust
## CI Context
Lint runs on Python 3.13, tests run on Python 3.12 with libmpv-dev.
### What CI Checks
- **Lint**: `.github/workflows/lint.yml` runs all linting checks
- **Tests**: `.github/workflows/tests.yml` runs pytest unit tests
## Skip These (Low Value)
Do not comment on:
- Style/formatting (pre-commit handles this)
- Test failures (CI catches this)
- Missing dependencies (CI catches this)
- Minor naming suggestions
- Suggestions to add comments
- Logging suggestions unless security-related
## Issue Categories
Categorize every issue as:
- `[CRITICAL]` — must fix before merging (bugs, security issues, broken functionality)
- `[PROBLEM]` — should fix (code quality, bad patterns, missing tests)
- `[SUGGESTION]` — optional improvement (style, minor refactors, nice-to-haves)
## PR Title
The PR title must be a functional description of the change. It must NOT contain conventional commit prefixes such as `feat:`, `fix:`, `refactor:`, `chore:`, etc. Labels categorize PRs, not the title. Flag as `[PROBLEM]` if the title uses such prefixes.
+44
View File
@@ -0,0 +1,44 @@
name: review-pr
description: Use when asked to review a GitHub pull request, PR link is shared, or user says /review-pr
# Review GitHub Pull Request
Review the GitHub pull request: $ARGUMENTS.
## Steps
1. Run `gh pr view` to get PR details and description.
2. Run `gh pr diff` to see all changes.
3. Compare the local commit hash with the remote PR head commit.
- Make sure to do your work in a git worktree to avoid affecting the checked out branch.
- If they don't match, run `gh pr checkout` to check out the PR locally.
- **HARD STOP**: If `gh pr checkout` fails for ANY reason, STOP immediately. Do NOT attempt workarounds. Alert about the failure and wait for instructions.
- After checkout, verify the local commit hash matches the remote one. If they still don't match, STOP.
4. Run `gh pr checks` to see CI status.
5. Apply the review standards defined in `REVIEW_STANDARDS.md` (same directory).
6. Output review feedback to the CONSOLE only. DO NOT act on GitHub (no posting comments, no approving, no requesting changes).
## Output Format
List specific comments per file/line that need attention. Do not list things that are already correct.
Each comment:
1. State the severity ([CRITICAL], [PROBLEM], [SUGGESTION])
2. State the problem (1 sentence)
3. Why it matters (1 sentence, if needed)
4. Suggested fix (snippet or specific action)
End with an overall assessment and summary:
```
Overall assessment: <approve | request changes | comment>
- [CRITICAL] <issue>
- [PROBLEM] <issue>
- [SUGGESTION] <improvement>
```
## Constraints
- DO NOT make any changes to the code.
- No need to run tests or linters, just review the code changes.
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"kilo.kilocode"
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Linux Voice Assistant",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/linux_voice_assistant/__main__.py",
"console": "integratedTerminal",
"justMyCode": true,
"envFile": "${workspaceFolder}/.env"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"python.defaultInterpreterPath": "./.venv/bin/python",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests"],
"python.testing.pytestAutoInclude": false,
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.linting.mypyEnabled": true,
"python.formatting.provider": "none",
"python.terminal.activateEnvironment": true,
"python.terminal.executeInShell": true,
"python.pythonPath": "./.venv/bin/python"
}
+62
View File
@@ -0,0 +1,62 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Setup (incl. dev dependencies)",
"type": "shell",
"command": "./script/setup --dev",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new",
"clear": true
},
"problemMatcher": []
},
{
"label": "Run Linter",
"type": "shell",
"command": "./script/lint",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Run Tests",
"type": "shell",
"command": "./script/tests",
"group": "test",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Run Application (Docker)",
"type": "shell",
"command": "docker-compose",
"args": ["-f", "docker-compose.dev.yml", "up"],
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new"
},
"problemMatcher": [],
"options": {
"envFile": "${workspaceFolder}/.env"
}
}
]
}
+129
View File
@@ -0,0 +1,129 @@
# Project Context for AI Agents
## Behaviour
- NEVER automatically reply on Github (PRs or Discussions) without explicit consent from the developer.
- Always run `./script/lint` and `./script/tests` after code changes to ensure standards are met.
## Project Overview
**Linux Voice Assistant (LVA)** - A Linux-based voice satellite software for Home Assistant using the ESPHome protocol.
### Purpose
- Provides voice control and interaction for Home Assistant via the Assist integration
- Runs on x64/ARM64 Linux hardware with PulseAudio/PipeWire audio support
- Performs local wake word detection (OpenWakeWord/MicroWakeWord) and on-device processing
### Architecture
- **Protocol**: ESPHome API via `aioesphomeapi` for Home Assistant integration
- **Wake Words**: Supports both OpenWakeWord and MicroWakeWord engines
- **Audio**: Uses `soundcard` library for input, `python-mpv` for output
- **Peripherals**: WebSocket API for LEDs, buttons, HAT boards
- **Discovery**: mDNS/ZeroConf for auto-discovery by Home Assistant
### Key Components
- `linux_voice_assistant/__main__.py` - Main entry point with CLI argument parsing
- `linux_voice_assistant/satellite.py` - VoiceSatelliteProtocol implementation (ESPHome API)
- `linux_voice_assistant/wake_word.py` - Wake word model loading and detection
- `linux_voice_assistant/webrtc.py` - WebRTC noise gain/NS processing
- `linux_voice_assistant/mpv_player.py` - MPV-based media player
- `linux_voice_assistant/entity.py` - Entity definitions (MediaPlayer, MuteSwitch, etc.)
- `linux_voice_assistant/api_server.py` - Peripheral WebSocket API server
- `linux_voice_assistant/models.py` - Preferences and ServerState dataclasses
## Development Commands
### Setup
```bash
./script/setup --dev # Install dev dependencies in virtual environment
```
### Linting
```bash
./script/lint # Run all linting checks
./script/lint_black # Black formatting check (add --auto to fix)
./script/lint_isort # isort import sorting check (add --auto to fix)
./script/lint_flake8 # flake8 style checks
./script/lint_mypy # mypy type checking
./script/lint_pylint # pylint code quality checks
```
### Testing
```bash
./script/tests # Run pytest unit tests
```
## Code Quality Standards
- **Python**: 3.11, 3.12, 3.13 supported
- **Formatting**: Black (200 char line length, py312/py312 target)
- **Import Sorting**: isort with black profile
- **Type Checking**: mypy with strict settings
- **Linting**: pylint (many checks disabled in pyproject.toml for practical reasons)
## Testing Strategy
- Unit tests in `tests/unit/` using pytest with asyncio mode
- Test helper `make_state()` in `tests/unit/conftest.py` for creating state fixtures
- Mocks used heavily for hardware dependencies (audio devices, MPV, wake words)
## CI/CD
- GitHub Actions for lint checks and unit tests
- Tests run on Python 3.12 with libmpv-dev system dependency
- Lint runs on Python 3.13
## Branching and PRs
- All PRs target `main` (primary development branch). `stable` is for production releases.
- PRs labeled `bugfix` + `backport-to-stable` are automatically backported to `stable` — use only for bugs also present in `stable`.
## Agent Commands
When making code changes, run these commands in order:
1. **Format code**: `./script/lint_black --auto` and `./script/lint_isort --auto`
2. **Run linting**: `./script/lint`
3. **Run tests**: `./script/tests`
## Code Style
### Comments
Only use comments to explain complex, multi-line blocks of code. Do not comment obvious operations. Inline comments are there to explain code parts that need explaining - keep them when editing code.
### Docstring Format
Use Sphinx-style docstrings with `:param:` syntax. For simple functions, a single-line docstring is fine. The docstring should provide clarity to the caller, not explain how it works technically.
```python
def my_function(param1: str, param2: int, param3: bool = False) -> str:
"""
Brief one-line description of the function.
:param param1: Description of what param1 is used for.
:param param2: Description of what param2 is used for.
:param param3: Description of what param3 is used for.
"""
```
Do **not** use Google-style (`Args:`) or bullet-style (`- param:`) docstrings.
### File structure
Private methods should be at the bottom of the file, public at the top.
## Verification Checklist
Before claiming completion:
- [ ] Ran `./script/lint` - all checks passed
- [ ] Ran `./script/tests` - all tests passed
- [ ] For audio-related changes: Note that hardware testing (microphone/speaker) was NOT performed
- [ ] Did NOT claim hardware behavior is verified unless actually exercised
- [ ] Changes are focused on one feature or fix per change
## Areas Requiring Extra Care
- `linux_voice_assistant/wake_word.py` - Wake word model loading is hardware-dependent
- `linux_voice_assistant/mpv_player.py` - Audio output requires `libmpv-dev`
- `linux_voice_assistant/webrtc.py` - Noise suppression/gain algorithms
- `linux_voice_assistant/satellite.py` - ESPHome API protocol handling
+31
View File
@@ -113,6 +113,7 @@ usage: __main__.py [-h] [--name NAME] [--audio-input-device AUDIO_INPUT_DEVICE]
| `--peripheral-volume-step` | Volume change per button press, 0.0–1.0 | %(default)s |
| `--disable-peripheral-api` | Disable the peripheral WebSocket API entirely | False |
| `--debug` | Print DEBUG messages to console | False |
| `--colored-debug` | Print colored DEBUG messages to console | False |
| `--output-only` | Enable output only mode | False |
💡 **Note:** There is a detailed explanation on the gain, noise suppression, and wake word sensitivity flags in the [audio options](docs/audio_options.md) file.
@@ -131,6 +132,36 @@ The documentation for the build process can be found in the [GitHub Actions Work
## Development
### System Requirements
**System packages (Linux):**
- libmpv-dev (`sudo apt install libmpv-dev`)
- PulseAudio/PipeWire (`sudo apt install pulseaudio pipewire`)
- ALSA utils (`sudo apt install alsa-utils`)
**Python:**
- Python 3.11+ must be installed on your system (`python3 --version` should show 3.11+)
- On Ubuntu/Debian: `sudo apt install python3.11 python3.11-venv python3.11-dev`
### VS Code Setup
VS Code development uses a local Python virtual environment (`.venv/`):
1. Open workspace in VS Code
2. Accept Workspace Trust (bottom-left status bar)
3. Install recommended extensions when prompted:
- `ms-python.python` - Python language support
- `ms-python.vscode-pylance` - Python language server
- `kilo.kilocode` - Kilo AI assistant
4. **Terminal** → **New Terminal** (or `` Ctrl+` ``) - Opens integrated terminal (uses .venv when available)
5. Run `./script/setup --dev` to create `.venv` and install dev dependencies
6. VS Code automatically detects `.venv/bin/python` as the Python interpreter
**Available VS Code Tasks:**
- `Ctrl+Shift+B` - Run Setup or Linter
- `Ctrl+Shift+T` - Run Tests
- `Ctrl+Shift+P` → "Tasks: Run Task" - Show all tasks (Setup, Linter, Tests, Run App)
### Code Quality Checks
The project uses the following tools to ensure code quality:
+25 -19
View File
@@ -9,6 +9,10 @@ if [ "$ENABLE_DEBUG" = "1" ]; then
EXTRA_ARGS+=( "--debug" )
fi
if [ "$ENABLE_COLORED_DEBUG" = "1" ]; then
EXTRA_ARGS+=( "--colored-debug" )
fi
if [ -n "${CLIENT_NAME}" ]; then
EXTRA_ARGS+=( "--name" "$CLIENT_NAME" )
fi
@@ -124,7 +128,7 @@ if [ -n "${PERIPHERAL_VOLUME_STEP}" ]; then
EXTRA_ARGS+=( "--peripheral-volume-step" "$PERIPHERAL_VOLUME_STEP" )
fi
if [ -n "${DISABLE_PERIPHERAL_API}" ]; then
if [ -n "$DISABLE_PERIPHERAL_API" = "1" ]; then
EXTRA_ARGS+=( "--disable-peripheral-api" "$DISABLE_PERIPHERAL_API" )
fi
@@ -145,26 +149,28 @@ fi
### Wait for PulseAudio
# Wait for PulseAudio to be available before starting the application
CP_MAX_RETRIES=30
CP_RETRY_DELAY=1
### while maybe besser?
echo "Checking PulseAudio service status..."
for i in $(seq 1 $CP_MAX_RETRIES); do
# Check if PulseAudio is running
if pactl info >/dev/null 2>&1; then
echo "✅ PulseAudio is running"
break
fi
# Skip wait if SKIP_PULSE_AUDIO_WAIT is set (for local development)
if [ "$SKIP_PULSE_AUDIO_WAIT" != "1" ]; then
CP_MAX_RETRIES=30
CP_RETRY_DELAY=1
### while maybe besser?
echo "Checking PulseAudio service status..."
for i in $(seq 1 $CP_MAX_RETRIES); do
# Check if PulseAudio is running
if pactl info >/dev/null 2>&1; then
echo "✅ PulseAudio is running"
break
fi
if [ $i -eq $CP_MAX_RETRIES ]; then
echo "❌ PulseAudio did not start after $CP_MAX_RETRIES seconds"
exit 2
fi
if [ $i -eq $CP_MAX_RETRIES ]; then
echo "❌ PulseAudio did not start after $CP_MAX_RETRIES seconds"
exit 2
fi
echo "⏳ PulseAudio not running yet, retrying in $CP_RETRY_DELAY s..."
sleep $CP_RETRY_DELAY
done
echo "⏳ PulseAudio not running yet, retrying in $CP_RETRY_DELAY s..."
sleep $CP_RETRY_DELAY
done
fi
### Start application
+1 -1
View File
@@ -77,7 +77,7 @@ The easyiest way to get started is to use the prebuilt image. This image comes w
- Download ready-to-use image from [PiCompose](https://github.com/florian-asche/PiCompose)
- Change configuration in /compose/ directory if you need to.
- You may need to reboot the board like 3 times for everything to be installed. Especially for the 2MicHat drivers. And give docker-compose some time to download the images. You can watch the logs in /var/logs/picompose.log
- You may need to reboot the board like 3 times for everything to be installed. Especially for the 2MicHat drivers. And give docker-compose some time to download the images. You can watch the logs in /var/log/picompose.log
- More information within the PiCompose project.
### Option B) Install on your own:
+3
View File
@@ -260,6 +260,9 @@ The following variables can be configured in the `.env` or in the service file:
| `LVA_PULSE_SERVER` | `/run/user/${LVA_USER_ID}/pulse/native` | Path to the PulseAudio/PipeWire socket (In some cases a `:unix`infront of the path is needed) |
| `LVA_XDG_RUNTIME_DIR` | `/run/user/${LVA_USER_ID}` | XDG runtime directory |
| `LVA_PULSE_COOKIE` | `/app/configuration/tmp_pulse_cookie` | Cookie file for PulseAudio if you use encryption. By default disabled. We use a tmp file to avoid errors if the file is not found |
| `ENABLE_DEBUG` | (optional) | Set to "1" to enable debug mode |
| `ENABLE_COLORED_DEBUG` | (optional) | Set to "1" to enable colored debug mode |
| `LIST_DEVICES` | (optional) | Set to "1" to list audio devices instead of starting |
| `PREFERENCES_FILE` | (optional) | Path to a custom preferences JSON 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 |
+20
View File
@@ -54,11 +54,31 @@ sudo mkdir -p /etc/pipewire/pipewire.conf.d
sudo vi /etc/pipewire/pipewire.conf.d/linux-voice-assistant.conf
```
💡 **Note:** On constrained hardware like a Raspberry Pi 3B, audio
processing can be interrupted by other system activity, causing
audible glitches or dropped wake-word detections. Loading the
`libpipewire-module-rt` module raises PipeWire's scheduling priority
(`rt.prio`) above normal processes, which reduces these interruptions.
The `flags = [ ifexists nofail ]` ensures this configuration is
silently skipped rather than causing a startup failure on systems
where realtime scheduling isn't available (e.g. some containers or
restricted kernels).
Add the following content:
```
context.properties = {
default.clock.rate = 16000
}
context.modules = [
{ name = libpipewire-module-rt
args = {
nice.level = -11
rt.prio = 88
}
flags = [ ifexists nofail ]
}
]
```
#### Applying the Changes
@@ -164,9 +164,9 @@ class SharedState:
# ===========================================================================
# USB LED driver
#
# The ReSpeaker Mic Array v2.0 accepts LED data via a USB vendor control
# transfer. Each LED is set as an APA102 frame: 0xFF start, then per-LED
# bytes in the order [start_frame | brightness, B, G, R].
# The ReSpeaker Mic Array v2.0 LED firmware exposes a vendor-command USB
# interface. The copied pixel_ring reference uses command 0x06 for custom LED
# frames and command 0x20 for brightness, both addressed to wIndex 0x1C.
# ===========================================================================
RGB = Tuple[int, int, int]
@@ -186,20 +186,17 @@ def _scale(color: RGB, factor: float) -> RGB:
class USBLEDRing:
"""
Controls the 12 APA102 LEDs on the ReSpeaker Mic Array v2.0 over USB.
Controls the 12 LEDs on the ReSpeaker Mic Array v2.0 over USB.
Uses a USB vendor control transfer (bmRequestType=0x40, bRequest=0,
wValue=0x0001, wIndex=0x8001) to push raw LED data to the XMOS firmware.
This is the same protocol used by Seeed's pixel_ring library.
Uses the same vendor-command protocol as Seeed's pixel_ring library.
That protocol talks to the device recipient directly and does not require
detaching the kernel audio driver.
"""
# USB control transfer parameters matching the pixel_ring reference implementation.
# _CTRL_TYPE_OUT = 0x40: bmRequestType = CTRL_OUT | CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE.
# Recipient is "device" (bits 4:0 = 0x00), so no interface needs to be claimed
# and the kernel audio driver must NOT be detached.
_CMD_SHOW = 0x06
_CMD_BRIGHTNESS = 0x20
_CTRL_TIMEOUT = 8000 # ms
_CTRL_REQUEST = 0
_CTRL_VALUE = 0x0001
_CTRL_INDEX = 0x1C
_CTRL_TYPE_OUT = 0x40 # vendor, device, host-to-device
@@ -207,6 +204,7 @@ class USBLEDRing:
self._pixels: list[RGB] = [BLACK] * LED_COUNT
self._dev = None
self._dev_lock = threading.Lock()
self._last_brightness: Optional[int] = None
if _HAS_USB:
self._find_device()
@@ -220,12 +218,8 @@ class USBLEDRing:
USB_VENDOR_ID, USB_PRODUCT_ID,
)
return
try:
if dev.is_kernel_driver_active(0):
dev.detach_kernel_driver(0)
except Exception: # pylint: disable=broad-except
pass
self._dev = dev
self._last_brightness = None
_LOGGER.info(
"ReSpeaker Mic Array v2.0 found (USB %04x:%04x, bus %d, addr %d)",
USB_VENDOR_ID, USB_PRODUCT_ID, dev.bus, dev.address,
@@ -237,24 +231,32 @@ class USBLEDRing:
def set_all(self, color: RGB) -> None:
self._pixels = [color] * LED_COUNT
def _write(self, command: int, data: list[int]) -> None:
self._dev.ctrl_transfer(
self._CTRL_TYPE_OUT,
self._CTRL_REQUEST,
command,
self._CTRL_INDEX,
data,
self._CTRL_TIMEOUT,
)
def show(self, brightness: float = 1.0) -> None:
"""
Push the current pixel buffer to the device.
Frame layout (matches pixel_ring / APA102 USB protocol):
[0xFF, 0xFF, 0xFF, 0xFF] ← start frame
[0xE0|bright5, B, G, R] × LED_COUNT ← one word per LED
[0xFF, ...] ← end frame (⌈n/2⌉ bytes)
The USB firmware expects command 0x06 with 4 bytes per LED in RGB0
order, plus command 0x20 for device brightness. Sending raw APA102
frames here causes the firmware to misinterpret the payload.
"""
bright5 = max(0, min(31, int(brightness * LED_BRIGHTNESS)))
frame = [0xFF, 0xFF, 0xFF, 0xFF] # start frame
frame: list[int] = []
for r, g, b in self._pixels:
br = max(0, min(255, int(r * brightness)))
bg = max(0, min(255, int(g * brightness)))
bb = max(0, min(255, int(b * brightness)))
frame += [0xE0 | bright5, bb, bg, br]
frame += [0xFF] * math.ceil(LED_COUNT / 2) # end frame
frame += [br, bg, bb, 0]
with self._dev_lock:
if self._dev is None:
@@ -262,17 +264,14 @@ class USBLEDRing:
_LOGGER.debug("LEDs [simulated]: %s", pixels)
return
try:
self._dev.ctrl_transfer(
self._CTRL_TYPE_OUT,
self._CTRL_REQUEST,
self._CTRL_VALUE,
self._CTRL_INDEX,
frame,
self._CTRL_TIMEOUT,
)
if self._last_brightness != bright5:
self._write(self._CMD_BRIGHTNESS, [bright5])
self._last_brightness = bright5
self._write(self._CMD_SHOW, frame)
except usb.core.USBError as exc:
_LOGGER.warning("USB write error: %s – attempting reconnect", exc)
self._dev = None
self._last_brightness = None
# Attempt to re-find the device on next show() call
threading.Thread(
target=self._reconnect, daemon=True
@@ -709,11 +708,19 @@ class LVAClient:
_LOGGER.debug("Event: %s data=%s", event, data)
if event == "snapshot":
muted = data.get("muted", False)
ha_connected = data.get("ha_connected", False)
self._state.update(
muted=data.get("muted", False),
muted=muted,
volume=data.get("volume", 1.0),
ha_connected=data.get("ha_connected", False),
ha_connected=ha_connected,
)
if muted:
self._state.update(assist_state=AssistState.MUTED)
elif ha_connected:
self._state.update(assist_state=AssistState.IDLE)
else:
self._state.update(assist_state=AssistState.NOT_READY)
elif event == "wake_word_detected":
self._state.update(assist_state=AssistState.WAKE_WORD)
@@ -731,7 +738,12 @@ class LVAClient:
self._state.update(assist_state=AssistState.IDLE)
elif event == "muted":
self._state.update(assist_state=AssistState.MUTED, muted=True)
muted = data.get("muted", True)
self._state.update(muted=muted)
if muted:
self._state.update(assist_state=AssistState.MUTED)
elif self._state.assist_state == AssistState.MUTED:
self._state.update(assist_state=AssistState.IDLE)
elif event == "pipeline_error":
_LOGGER.warning("LVA pipeline error: %s", data.get("reason", ""))
@@ -777,6 +789,12 @@ class LVAClient:
status = data.get("status", "")
if status == "connected":
self._state.update(ha_connected=True)
if self._state.assist_state == AssistState.NOT_READY:
self._state.update(
assist_state=(
AssistState.MUTED if self._state.muted else AssistState.IDLE
),
)
_LOGGER.info("Home Assistant connected")
elif status == "getting_started":
_LOGGER.info("LVA starting up, waiting for HA …")
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://app.kilo.ai/config.json",
"model": "kilo/codegemma-2b:free",
"instructions": ["AGENTS.md"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}
+46 -3
View File
@@ -234,6 +234,11 @@ async def main() -> None:
action="store_true",
help="Add this to enable debug logging",
)
parser.add_argument(
"--colored-debug",
action="store_true",
help="Add this to enable colored debug logging",
)
parser.add_argument(
"--output-only",
action="store_true",
@@ -241,6 +246,15 @@ async def main() -> None:
)
args = parser.parse_args()
if args.colored_debug:
args.debug = True
_setup_logging(args)
elif args.debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
_LOGGER.debug(args)
if args.list_input_devices:
print("Audio Input devices:")
print("=" * 13)
@@ -259,9 +273,6 @@ async def main() -> None:
print(speaker["name"] + ":", speaker["description"])
return
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
_LOGGER.debug(args)
# Resolve network interface for mac-address detection
if not args.network_interface:
print("No network interface specified, try to detect default interface")
@@ -575,6 +586,38 @@ async def main() -> None:
_LOGGER.debug("Server stopped")
# -----------------------------------------------------------------------------
def _setup_logging(args: argparse.Namespace) -> None:
COLORS = {
logging.DEBUG: "\033[36m",
logging.INFO: "\033[32m",
logging.WARNING: "\033[33m",
logging.ERROR: "\033[31m",
logging.CRITICAL: "\033[35m",
}
RESET = "\033[0m"
original_format = logging.Formatter.format
def colored_format(self, record: logging.LogRecord) -> str:
color = COLORS.get(record.levelno, RESET)
return f"{color}{original_format(self, record)}{RESET}"
logging.Formatter.format = colored_format # type: ignore
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
)
logging.basicConfig(
level=logging.DEBUG if args.debug else logging.INFO,
handlers=[handler],
)
# -----------------------------------------------------------------------------
+83 -8
View File
@@ -99,6 +99,7 @@ from __future__ import annotations
import asyncio
import json
import logging
import time
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
@@ -180,6 +181,8 @@ class PeripheralAPIServer:
"""
DEFAULT_VOLUME_STEP: float = 0.05
LATE_ENTITY_RECONNECT_DEBOUNCE_S: float = 1.5
LATE_ENTITY_RECONNECT_COOLDOWN_S: float = 60.0
def __init__(
self,
@@ -208,6 +211,10 @@ class PeripheralAPIServer:
self._current_state: Optional[LVAEvent] = None
self._current_state_data: Optional[Dict[str, Any]] = None
# Safeguarded HA reconnect trigger for late entity registrations.
self._pending_entity_reconnect_task: Optional[asyncio.Task] = None
self._last_ha_reconnect_at: float = 0.0
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
@@ -230,6 +237,10 @@ class PeripheralAPIServer:
async def stop(self) -> None:
"""Gracefully shut down the server and all client connections."""
if self._pending_entity_reconnect_task is not None:
self._pending_entity_reconnect_task.cancel()
self._pending_entity_reconnect_task = None
if self._server is not None:
self._server.close()
await self._server.wait_closed()
@@ -281,14 +292,20 @@ class PeripheralAPIServer:
# Replay the current event state so the client immediately shows the
# right animation — e.g. a timer ticking animation when reconnecting
# mid-timer, or the muted indicator when reconnecting while muted.
if self._current_state is not None:
state_payload: Dict[str, Any] = {"event": self._current_state.value}
if self._current_state_data:
state_payload["data"] = self._current_state_data
try:
await websocket.send(json.dumps(state_payload))
except Exception: # pylint: disable=broad-except
pass
current_state = self._current_state
if current_state is None:
return
if current_state == LVAEvent.DISCONNECTED and state.connected:
return
state_payload: Dict[str, Any] = {"event": current_state.value}
if self._current_state_data:
state_payload["data"] = self._current_state_data
try:
await websocket.send(json.dumps(state_payload))
except Exception: # pylint: disable=broad-except
pass
# ------------------------------------------------------------------
# Command dispatch
@@ -501,6 +518,8 @@ class PeripheralAPIServer:
if satellite is not None:
satellite.register_pending_lights()
self._schedule_ha_reconnect_for_late_entity("light", object_id)
def _register_button(self, satellite: Any) -> None:
"""Register button press event support declared by a peripheral.
@@ -528,10 +547,66 @@ class PeripheralAPIServer:
if satellite is not None:
satellite.register_pending_button()
self._schedule_ha_reconnect_for_late_entity("button", "button_press_event")
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _schedule_ha_reconnect_for_late_entity(self, entity_kind: str, entity_id: str) -> None:
"""Reconnect HA once for late peripheral entity registrations."""
state = self._state
if state is None or not state.connected or state.satellite is None:
return
if self._loop is None:
return
if self._pending_entity_reconnect_task is not None and not self._pending_entity_reconnect_task.done():
_LOGGER.debug(
"Coalescing late %s registration for '%s' into pending HA reconnect",
entity_kind,
entity_id,
)
return
async def _trigger() -> None:
try:
await asyncio.sleep(self.LATE_ENTITY_RECONNECT_DEBOUNCE_S)
state_now = self._state
if state_now is None or not state_now.connected:
return
satellite_now = state_now.satellite
if satellite_now is None:
return
now = time.monotonic()
if now - self._last_ha_reconnect_at < self.LATE_ENTITY_RECONNECT_COOLDOWN_S:
_LOGGER.info(
"Skipping HA reconnect after late %s '%s': cooldown active",
entity_kind,
entity_id,
)
return
transport = getattr(satellite_now, "_transport", None)
if transport is None:
return
self._last_ha_reconnect_at = now
_LOGGER.info(
"Late %s '%s' registered after HA handshake; forcing one HA reconnect",
entity_kind,
entity_id,
)
transport.close()
except asyncio.CancelledError:
return
self._pending_entity_reconnect_task = self._loop.create_task(_trigger())
async def _push_mute_switch(self, satellite: Any, *, muted: bool) -> None:
"""Reflect a peripheral-triggered mute change to Home Assistant."""
state = self._state
+1 -1
View File
@@ -507,7 +507,7 @@ class VoiceSatelliteProtocol(APIServer):
# ------------------------------------------------------------------
def handle_voice_event(self, event_type: VoiceAssistantEventType, data: Dict[str, str]) -> None:
_LOGGER.debug("Voice event: type=%s, data=%s", event_type.name, data)
_LOGGER.info("Voice event: type=%s, data=%s", event_type.name, data)
if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_START:
self._tts_url = data.get("url")
+45 -7
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env python3
import argparse
import subprocess
import venv
import os
import shutil
import sys
import ctypes.util
from pathlib import Path
_DIR = Path(__file__).parent
@@ -15,10 +17,47 @@ parser.add_argument("--cxxflags", type=str, help="CXXFLAGS for compilation (defa
parser.add_argument("--makeflags", type=str, help="MAKEFLAGS for compilation (default: -j1)")
args = parser.parse_args()
# Find Python 3.11+ interpreter
def find_python_311():
for candidate in ["python3.13", "python3.12", "python3.11"]:
path = shutil.which(candidate)
if path:
result = subprocess.run([path, "--version"], capture_output=True, text=True)
version_str = result.stdout.strip() or result.stderr.strip()
ver_parts = version_str.replace("Python ", "").split(".")
if len(ver_parts) >= 2 and int(ver_parts[0]) == 3 and int(ver_parts[1]) >= 11:
return path
return None
python_311_exe = find_python_311()
if not python_311_exe:
print("ERROR: Python 3.11+ is required but not found on this system.")
print(" Ubuntu/Debian: sudo apt install python3.11 python3.11-venv python3.11-dev")
print(" macOS: brew install python@3.11")
sys.exit(1)
print(f"Using Python: {python_311_exe}")
subprocess.run([python_311_exe, "--version"])
# Check system dependencies (warning only, CI installs them separately)
missing = []
try:
if not ctypes.util.find_library("mpv"):
missing.append("libmpv-dev")
if not ctypes.util.find_library("asound"):
missing.append("libasound2-dev")
except Exception:
pass # Ignore errors in environments without ctypes
if missing:
print(f"\nWARNING: Missing system libraries (required for audio): {', '.join(missing)}")
print("For Docker/local dev: sudo apt install libmpv-dev pulseaudio pipewire alsa-utils")
# Create virtual environment
builder = venv.EnvBuilder(with_pip=True)
context = builder.ensure_directories(_VENV_DIR)
builder.create(_VENV_DIR)
if _VENV_DIR.exists():
shutil.rmtree(_VENV_DIR)
subprocess.check_call([python_311_exe, "-m", "venv", str(_VENV_DIR), "--symlinks"])
# Set environment variables
env = {}
@@ -28,17 +67,16 @@ if args.makeflags:
env["MAKEFLAGS"] = args.makeflags
# Upgrade dependencies
pip = [context.env_exe, "-m", "pip"]
pip = [str(_VENV_DIR / "bin" / "python"), "-m", "pip"]
subprocess.check_call(pip + ["install", "--upgrade", "pip"])
subprocess.check_call(pip + ["install", "--upgrade", "setuptools", "wheel"])
# Install requirements
subprocess.check_call(pip + ["install", "-e", str(_PROGRAM_DIR)])
# Install dev requirements
if args.dev:
subprocess.check_call(
pip + ["install", "-e", f"{_PROGRAM_DIR}[dev]"],
env={**dict(os.environ), **env}
)
)