Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43a183a232 | ||
|
|
a2d0a4e68e | ||
|
|
b94a5a4663 | ||
|
|
2b0388f454 | ||
|
|
0eafd4f7cd | ||
|
|
8a36a74bda | ||
|
|
bf3bf0d942 | ||
|
|
acda765964 | ||
|
|
3077e7230b | ||
|
|
ea991e026f | ||
|
|
69a4fd3098 | ||
|
|
126bdf1862 | ||
|
|
3b33730105 | ||
|
|
0f57ca989c | ||
|
|
ba7d834d1f | ||
|
|
2eb29470f9 | ||
|
|
2378187197 | ||
|
|
d3874f6a2f | ||
|
|
201896196f | ||
|
|
8f788eeff0 | ||
|
|
305b787dd6 | ||
|
|
e14844a113 | ||
|
|
9ed97cd8f4 | ||
|
|
e8042ee19d | ||
|
|
802908cb8f | ||
|
|
c37b482b87 | ||
|
|
486ff4bb53 |
No files matched your search
@@ -8,6 +8,9 @@
|
|||||||
### Enable debug mode (optional):
|
### Enable debug mode (optional):
|
||||||
# ENABLE_DEBUG="1"
|
# ENABLE_DEBUG="1"
|
||||||
|
|
||||||
|
### Enable colored debug mode (optional):
|
||||||
|
# ENABLE_COLORED_DEBUG="1"
|
||||||
|
|
||||||
### List audio devices (optional):
|
### List audio devices (optional):
|
||||||
# if enabled normal startup is disabled
|
# if enabled normal startup is disabled
|
||||||
# LIST_DEVICES="1"
|
# 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
|
# Start listening immediately after wake word detection, without waiting for the wake sound to finish
|
||||||
# LISTEN_DURING_WAKE_SOUND="1"
|
# 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):
|
### Wake word directory (optional):
|
||||||
# path for custom files in docker is for example "app/wakewords/custom"
|
# path for custom files in docker is for example "app/wakewords/custom"
|
||||||
# WAKE_WORD_DIR="app/wakewords"
|
# WAKE_WORD_DIR="app/wakewords"
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
custom: https://www.openhomefoundation.org
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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:
|
||||||
|
- "*"
|
||||||
@@ -20,23 +20,23 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
- name: Set lowercase registry and repository
|
- name: Set lowercase registry and repository
|
||||||
run: |
|
run: |
|
||||||
echo "REGISTRY_LOWER=$(echo '${{ env.REGISTRY }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
echo "REGISTRY_LOWER=$(echo '${{ env.REGISTRY }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
echo "REPOSITORY_LOWER=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
echo "REPOSITORY_LOWER=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
# Build Docker Images (amd64 and arm64)
|
# Build Docker Images (amd64 and arm64)
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Create version files
|
- name: Create version files
|
||||||
run: |
|
run: |
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/aarch64
|
platforms: linux/amd64,linux/aarch64
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
tags: ${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}:nightly
|
tags: ${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}:nightly
|
||||||
labels: "nightly"
|
labels: "nightly"
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
uses: actions/attest-build-provenance@v1.4.4
|
uses: actions/attest-build-provenance@v4
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.REGISTRY }}/${{ github.repository }}
|
subject-name: ${{ env.REGISTRY }}/${{ github.repository }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout PR code
|
- name: Checkout PR code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
allow-unsafe-pr-checkout: true
|
||||||
|
|
||||||
- name: Set variables
|
- name: Set variables
|
||||||
run: |
|
run: |
|
||||||
@@ -32,16 +33,16 @@ jobs:
|
|||||||
echo "REPOSITORY_LOWER=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
echo "REPOSITORY_LOWER=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
# Build Docker Images (amd64 and arm64)
|
# Build Docker Images (amd64 and arm64)
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Create version files
|
- name: Create version files
|
||||||
run: |
|
run: |
|
||||||
@@ -51,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/aarch64
|
platforms: linux/amd64,linux/aarch64
|
||||||
@@ -59,7 +60,7 @@ jobs:
|
|||||||
tags: "${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}-pr:${{ env.PR_NUMBER }}"
|
tags: "${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}-pr:${{ env.PR_NUMBER }}"
|
||||||
labels: "${{ env.PR_NUMBER }}"
|
labels: "${{ env.PR_NUMBER }}"
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
uses: actions/attest-build-provenance@v1.4.4
|
uses: actions/attest-build-provenance@v4
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}-pr
|
subject-name: ${{ env.REGISTRY_LOWER }}/${{ env.REPOSITORY_LOWER }}-pr
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
|||||||
@@ -21,21 +21,21 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
# Build Docker Images (amd64 and arm64)
|
# Build Docker Images (amd64 and arm64)
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
- name: Extract metadata for Docker
|
- name: Extract metadata for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ github.repository }}
|
images: ${{ env.REGISTRY }}/${{ github.repository }}
|
||||||
# E.g. tag v1.2.0 produces 3 tags: '1.2.0', '1.2', and 'latest'
|
# 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
|
- name: Build and push Docker image
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/aarch64
|
platforms: linux/amd64,linux/aarch64
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
uses: actions/attest-build-provenance@v1
|
uses: actions/attest-build-provenance@v4
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.REGISTRY }}/${{ github.repository }}
|
subject-name: ${{ env.REGISTRY }}/${{ github.repository }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
- uses: rhysd/changelog-from-release/action@v3
|
- uses: rhysd/changelog-from-release/action@v3
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
lint_black:
|
lint_black:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
lint_flake8:
|
lint_flake8:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
lint_isort:
|
lint_isort:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
lint_mypy:
|
lint_mypy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
lint_pylint:
|
lint_pylint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -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],
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ venv/
|
|||||||
.env
|
.env
|
||||||
dev.env
|
dev.env
|
||||||
version.txt
|
version.txt
|
||||||
version_githash.txt
|
version_githash.txt
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
bun.lock
|
||||||
|
yarn.lock
|
||||||
|
worktrees
|
||||||
|
agent-manager.json
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"kilo.kilocode"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# 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` - ESPHome API server
|
||||||
|
- `linux_voice_assistant/peripheral_api.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
|
||||||
@@ -1,3 +1,68 @@
|
|||||||
|
<a id="v1.1.14"></a>
|
||||||
|
# [v1.1.14](https://github.com/OHF-Voice/linux-voice-assistant/releases/tag/v1.1.14) - 2026-07-28
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### Exciting New Features 🎉
|
||||||
|
* Colored Debug Logging and move Voice Event to Info level in logger by [@aryanhasgithub](https://github.com/aryanhasgithub) in [#271](https://github.com/OHF-Voice/linux-voice-assistant/pull/271)
|
||||||
|
### Other Changes
|
||||||
|
* build(dev): enhance local development environment and setup workflow by [@florian-asche](https://github.com/florian-asche) in [#358](https://github.com/OHF-Voice/linux-voice-assistant/pull/358)
|
||||||
|
* docs(github): establish repository governance and automation workflows by [@florian-asche](https://github.com/florian-asche) in [#359](https://github.com/OHF-Voice/linux-voice-assistant/pull/359)
|
||||||
|
* Bump docker/build-push-action from 5.3.0 to 7.3.0 by [@dependabot](https://github.com/dependabot)[bot] in [#364](https://github.com/OHF-Voice/linux-voice-assistant/pull/364)
|
||||||
|
* Bump actions/attest-build-provenance from 1 to 4 by [@dependabot](https://github.com/dependabot)[bot] in [#363](https://github.com/OHF-Voice/linux-voice-assistant/pull/363)
|
||||||
|
* Bump docker/metadata-action from 5.5.1 to 6.2.0 by [@dependabot](https://github.com/dependabot)[bot] in [#362](https://github.com/OHF-Voice/linux-voice-assistant/pull/362)
|
||||||
|
* Bump docker/setup-qemu-action from 3 to 4 by [@dependabot](https://github.com/dependabot)[bot] in [#361](https://github.com/OHF-Voice/linux-voice-assistant/pull/361)
|
||||||
|
* Bump docker/login-action from 3.1.0 to 4.4.0 by [@dependabot](https://github.com/dependabot)[bot] in [#360](https://github.com/OHF-Voice/linux-voice-assistant/pull/360)
|
||||||
|
* chore(ci): update github actions and docker dependencies by [@florian-asche](https://github.com/florian-asche) in [#368](https://github.com/OHF-Voice/linux-voice-assistant/pull/368)
|
||||||
|
* fix Respeaker USB Mic Array v2 LEDs by [@litinoveweedle](https://github.com/litinoveweedle) in [#357](https://github.com/OHF-Voice/linux-voice-assistant/pull/357)
|
||||||
|
* Update install.md - Fixed log path by [@maximilianovermeyer](https://github.com/maximilianovermeyer) in [#372](https://github.com/OHF-Voice/linux-voice-assistant/pull/372)
|
||||||
|
* Bump docker/setup-buildx-action from 3 to 4 by [@dependabot](https://github.com/dependabot)[bot] in [#371](https://github.com/OHF-Voice/linux-voice-assistant/pull/371)
|
||||||
|
* fix race condition between LVA registration to the HA and peripheral … by [@litinoveweedle](https://github.com/litinoveweedle) in [#373](https://github.com/OHF-Voice/linux-voice-assistant/pull/373)
|
||||||
|
* Bump actions/checkout from 4 to 7 by [@dependabot](https://github.com/dependabot)[bot] in [#370](https://github.com/OHF-Voice/linux-voice-assistant/pull/370)
|
||||||
|
* feat(ai): integrate Kilo agent framework and coding instructions by [@florian-asche](https://github.com/florian-asche) in [#366](https://github.com/OHF-Voice/linux-voice-assistant/pull/366)
|
||||||
|
* Bump docker/login-action from 4.4.0 to 4.5.0 by [@dependabot](https://github.com/dependabot)[bot] in [#375](https://github.com/OHF-Voice/linux-voice-assistant/pull/375)
|
||||||
|
* docs: add realtime scheduling tuning for constrained hardware Closes [#164](https://github.com/OHF-Voice/linux-voice-assistant/issues/164) by [@SpiliosDimakopoulos](https://github.com/SpiliosDimakopoulos) in [#376](https://github.com/OHF-Voice/linux-voice-assistant/pull/376)
|
||||||
|
* Bump docker/login-action from 4.5.0 to 4.5.1 by [@dependabot](https://github.com/dependabot)[bot] in [#377](https://github.com/OHF-Voice/linux-voice-assistant/pull/377)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@dependabot](https://github.com/dependabot)[bot] made their first contribution in [#364](https://github.com/OHF-Voice/linux-voice-assistant/pull/364)
|
||||||
|
* [@litinoveweedle](https://github.com/litinoveweedle) made their first contribution in [#357](https://github.com/OHF-Voice/linux-voice-assistant/pull/357)
|
||||||
|
* [@maximilianovermeyer](https://github.com/maximilianovermeyer) made their first contribution in [#372](https://github.com/OHF-Voice/linux-voice-assistant/pull/372)
|
||||||
|
* [@SpiliosDimakopoulos](https://github.com/SpiliosDimakopoulos) made their first contribution in [#376](https://github.com/OHF-Voice/linux-voice-assistant/pull/376)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.13...v1.1.14
|
||||||
|
|
||||||
|
[Changes][v1.1.14]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.1.13"></a>
|
||||||
|
# [v1.1.13](https://github.com/OHF-Voice/linux-voice-assistant/releases/tag/v1.1.13) - 2026-07-18
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### Exciting New Features 🎉
|
||||||
|
* (Temporary workaround) Switch between MWW/OWW models from the UI by [@omaramin-2000](https://github.com/omaramin-2000) in [#348](https://github.com/OHF-Voice/linux-voice-assistant/pull/348)
|
||||||
|
* Leds and buttons events by [@omaramin-2000](https://github.com/omaramin-2000) in [#266](https://github.com/OHF-Voice/linux-voice-assistant/pull/266)
|
||||||
|
* Add Tests For LVA by [@aryanhasgithub](https://github.com/aryanhasgithub) in [#312](https://github.com/OHF-Voice/linux-voice-assistant/pull/312)
|
||||||
|
* feat: add --listen-during-wake-sound argument by [@wangwillian0](https://github.com/wangwillian0) in [#273](https://github.com/OHF-Voice/linux-voice-assistant/pull/273)
|
||||||
|
### Other Changes
|
||||||
|
* perf(player): limit cache to 32 MiB by [@florian-asche](https://github.com/florian-asche) in [#309](https://github.com/OHF-Voice/linux-voice-assistant/pull/309)
|
||||||
|
* Allow using python versions above 3.11 by [@omaramin-2000](https://github.com/omaramin-2000) in [#345](https://github.com/OHF-Voice/linux-voice-assistant/pull/345)
|
||||||
|
* Modify USB control transfer settings in respeaker_usb_mic_array.py by [@omaramin-2000](https://github.com/omaramin-2000) in [#349](https://github.com/OHF-Voice/linux-voice-assistant/pull/349)
|
||||||
|
* Broadcast entity state to all API clients; don't desync on a second connection by [@JSRossie](https://github.com/JSRossie) in [#329](https://github.com/OHF-Voice/linux-voice-assistant/pull/329)
|
||||||
|
* Fix lint_mypy by [@omaramin-2000](https://github.com/omaramin-2000) in [#352](https://github.com/OHF-Voice/linux-voice-assistant/pull/352)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@JSRossie](https://github.com/JSRossie) made their first contribution in [#329](https://github.com/OHF-Voice/linux-voice-assistant/pull/329)
|
||||||
|
* [@wangwillian0](https://github.com/wangwillian0) made their first contribution in [#273](https://github.com/OHF-Voice/linux-voice-assistant/pull/273)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.12...v1.1.13
|
||||||
|
|
||||||
|
[Changes][v1.1.13]
|
||||||
|
|
||||||
|
|
||||||
<a id="v1.1.12"></a>
|
<a id="v1.1.12"></a>
|
||||||
# [v1.1.12](https://github.com/OHF-Voice/linux-voice-assistant/releases/tag/v1.1.12) - 2026-06-22
|
# [v1.1.12](https://github.com/OHF-Voice/linux-voice-assistant/releases/tag/v1.1.12) - 2026-06-22
|
||||||
|
|
||||||
@@ -299,6 +364,8 @@ Initial release
|
|||||||
[Changes][v1.0.0]
|
[Changes][v1.0.0]
|
||||||
|
|
||||||
|
|
||||||
|
[v1.1.14]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.13...v1.1.14
|
||||||
|
[v1.1.13]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.12...v1.1.13
|
||||||
[v1.1.12]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.11...v1.1.12
|
[v1.1.12]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.11...v1.1.12
|
||||||
[v1.1.11]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.10...v1.1.11
|
[v1.1.11]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.10...v1.1.11
|
||||||
[v1.1.10]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.9...v1.1.10
|
[v1.1.10]: https://github.com/OHF-Voice/linux-voice-assistant/compare/v1.1.9...v1.1.10
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Because it runs on a full Linux system and offers access significantly more loca
|
|||||||
- Supports multiple wake words and languages
|
- Supports multiple wake words and languages
|
||||||
- Supports multiple architectures (linux/amd64 and linux/aarch64)
|
- Supports multiple architectures (linux/amd64 and linux/aarch64)
|
||||||
- Automated builds with artifact attestation for security
|
- Automated builds with artifact attestation for security
|
||||||
- Supports announcments, start/continue conversation, and timers
|
- Supports [announcments](https://www.home-assistant.io/actions/assist_satellite.announce/), [start](https://www.home-assistant.io/actions/assist_satellite.start_conversation/)/continue conversation, and timers
|
||||||
- Tested and works with Python 3.11 and Python 3.13.
|
- Tested and works with Python 3.11 and Python 3.13.
|
||||||
- Prebuild docker image available on [GitHub Container Registry](https://github.com/OHF-Voice/linux-voice-assistant/pkgs/container/linux-voice-assistant)
|
- Prebuild docker image available on [GitHub Container Registry](https://github.com/OHF-Voice/linux-voice-assistant/pkgs/container/linux-voice-assistant)
|
||||||
- Prebuild [Raspberry Pi image](https://github.com/florian-asche/PiCompose)
|
- Prebuild [Raspberry Pi image](https://github.com/florian-asche/PiCompose)
|
||||||
@@ -74,7 +74,7 @@ For all other users, we have different installation methods available (Docker, s
|
|||||||
💡 **Note:** There is an [environment variable](docs/install_application.md#environment-variables-reference) for each parameter if you use docker or systemd based setup.
|
💡 **Note:** There is an [environment variable](docs/install_application.md#environment-variables-reference) for each parameter if you use docker or systemd based setup.
|
||||||
|
|
||||||
```sh
|
```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] [--mic-auto-gain] [--mic-noise-suppression]
|
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] [--music-output-device MUSIC_OUTPUT_DEVICE] [--list-output-devices] [--wake-word-dir WAKE_WORD_DIR] [--mic-auto-gain] [--mic-noise-suppression]
|
||||||
[--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]
|
[--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]
|
||||||
[--mute-sound MUTE_SOUND] [--unmute-sound UNMUTE_SOUND] [--preferences-file PREFERENCES_FILE] [--host HOST] [--network-interface NETWORK_INTERFACE] [--port PORT] [--enable-thinking-sound] [--listen-during-wake-sound] [--debug]
|
[--mute-sound MUTE_SOUND] [--unmute-sound UNMUTE_SOUND] [--preferences-file PREFERENCES_FILE] [--host HOST] [--network-interface NETWORK_INTERFACE] [--port PORT] [--enable-thinking-sound] [--listen-during-wake-sound] [--debug]
|
||||||
```
|
```
|
||||||
@@ -85,6 +85,7 @@ usage: __main__.py [-h] [--name NAME] [--audio-input-device AUDIO_INPUT_DEVICE]
|
|||||||
| `--audio-input-device` | Soundcard name for input device | Autodetected |
|
| `--audio-input-device` | Soundcard name for input device | Autodetected |
|
||||||
| `--audio-input-block-size` | Audio input block size in samples | 1024 |
|
| `--audio-input-block-size` | Audio input block size in samples | 1024 |
|
||||||
| `--audio-output-device` | mpv name for output device | Autodetected |
|
| `--audio-output-device` | mpv name for output device | Autodetected |
|
||||||
|
| `--music-output-device` | mpv name for the music/media output device | `--audio-output-device` |
|
||||||
| `--mic-volume` | Control microphone volume | 100 |
|
| `--mic-volume` | Control microphone volume | 100 |
|
||||||
| `--mic-auto-gain` | Add WebRTC Gain to Mic | 0 |
|
| `--mic-auto-gain` | Add WebRTC Gain to Mic | 0 |
|
||||||
| `--mic-noise-suppression` | Add WebRTC Noise Suppression to Mic | 0 |
|
| `--mic-noise-suppression` | Add WebRTC Noise Suppression to Mic | 0 |
|
||||||
@@ -113,9 +114,10 @@ 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 |
|
| `--peripheral-volume-step` | Volume change per button press, 0.0–1.0 | %(default)s |
|
||||||
| `--disable-peripheral-api` | Disable the peripheral WebSocket API entirely | False |
|
| `--disable-peripheral-api` | Disable the peripheral WebSocket API entirely | False |
|
||||||
| `--debug` | Print DEBUG messages to console | False |
|
| `--debug` | Print DEBUG messages to console | False |
|
||||||
|
| `--colored-debug` | Print colored DEBUG messages to console | False |
|
||||||
| `--output-only` | Enable output only mode | 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.
|
💡 **Note:** There are detailed explanations on the controlled entities from device page in the [configuration](docs/configuration.md) file.
|
||||||
|
|
||||||
## Build Information
|
## Build Information
|
||||||
|
|
||||||
@@ -131,6 +133,36 @@ The documentation for the build process can be found in the [GitHub Actions Work
|
|||||||
|
|
||||||
## Development
|
## 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
|
### Code Quality Checks
|
||||||
|
|
||||||
The project uses the following tools to ensure code quality:
|
The project uses the following tools to ensure code quality:
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ if [ "$ENABLE_DEBUG" = "1" ]; then
|
|||||||
EXTRA_ARGS+=( "--debug" )
|
EXTRA_ARGS+=( "--debug" )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$ENABLE_COLORED_DEBUG" = "1" ]; then
|
||||||
|
EXTRA_ARGS+=( "--colored-debug" )
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "${CLIENT_NAME}" ]; then
|
if [ -n "${CLIENT_NAME}" ]; then
|
||||||
EXTRA_ARGS+=( "--name" "$CLIENT_NAME" )
|
EXTRA_ARGS+=( "--name" "$CLIENT_NAME" )
|
||||||
fi
|
fi
|
||||||
@@ -40,6 +44,10 @@ if [ -n "${AUDIO_OUTPUT_DEVICE}" ]; then
|
|||||||
EXTRA_ARGS+=( "--audio-output-device" "$AUDIO_OUTPUT_DEVICE" )
|
EXTRA_ARGS+=( "--audio-output-device" "$AUDIO_OUTPUT_DEVICE" )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "${MUSIC_OUTPUT_DEVICE}" ]; then
|
||||||
|
EXTRA_ARGS+=( "--music-output-device" "$MUSIC_OUTPUT_DEVICE" )
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "${MIC_VOLUME}" ]; then
|
if [ -n "${MIC_VOLUME}" ]; then
|
||||||
EXTRA_ARGS+=( "--mic-volume" "$MIC_VOLUME" )
|
EXTRA_ARGS+=( "--mic-volume" "$MIC_VOLUME" )
|
||||||
fi
|
fi
|
||||||
@@ -124,8 +132,8 @@ if [ -n "${PERIPHERAL_VOLUME_STEP}" ]; then
|
|||||||
EXTRA_ARGS+=( "--peripheral-volume-step" "$PERIPHERAL_VOLUME_STEP" )
|
EXTRA_ARGS+=( "--peripheral-volume-step" "$PERIPHERAL_VOLUME_STEP" )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${DISABLE_PERIPHERAL_API}" ]; then
|
if [ "$DISABLE_PERIPHERAL_API" = "1" ]; then
|
||||||
EXTRA_ARGS+=( "--disable-peripheral-api" "$DISABLE_PERIPHERAL_API" )
|
EXTRA_ARGS+=( "--disable-peripheral-api" )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$ENABLE_OUTPUT_ONLY" = "1" ]; then
|
if [ "$ENABLE_OUTPUT_ONLY" = "1" ]; then
|
||||||
@@ -145,26 +153,28 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
### Wait for PulseAudio
|
### Wait for PulseAudio
|
||||||
# Wait for PulseAudio to be available before starting the application
|
# Skip wait if SKIP_PULSE_AUDIO_WAIT is set (for local development)
|
||||||
CP_MAX_RETRIES=30
|
if [ "$SKIP_PULSE_AUDIO_WAIT" != "1" ]; then
|
||||||
CP_RETRY_DELAY=1
|
CP_MAX_RETRIES=30
|
||||||
### while maybe besser?
|
CP_RETRY_DELAY=1
|
||||||
echo "Checking PulseAudio service status..."
|
### while maybe besser?
|
||||||
for i in $(seq 1 $CP_MAX_RETRIES); do
|
echo "Checking PulseAudio service status..."
|
||||||
# Check if PulseAudio is running
|
for i in $(seq 1 $CP_MAX_RETRIES); do
|
||||||
if pactl info >/dev/null 2>&1; then
|
# Check if PulseAudio is running
|
||||||
echo "✅ PulseAudio is running"
|
if pactl info >/dev/null 2>&1; then
|
||||||
break
|
echo "✅ PulseAudio is running"
|
||||||
fi
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
if [ $i -eq $CP_MAX_RETRIES ]; then
|
if [ $i -eq $CP_MAX_RETRIES ]; then
|
||||||
echo "❌ PulseAudio did not start after $CP_MAX_RETRIES seconds"
|
echo "❌ PulseAudio did not start after $CP_MAX_RETRIES seconds"
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "⏳ PulseAudio not running yet, retrying in $CP_RETRY_DELAY s..."
|
echo "⏳ PulseAudio not running yet, retrying in $CP_RETRY_DELAY s..."
|
||||||
sleep $CP_RETRY_DELAY
|
sleep $CP_RETRY_DELAY
|
||||||
done
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
### Start application
|
### Start application
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
# Audio Options: Gain, Noise Suppression, and Wake Word Sensitivity
|
|
||||||
|
|
||||||
## What do Gain and Noise Suppression do
|
|
||||||
|
|
||||||
### Gain
|
|
||||||
|
|
||||||
When you change gain, LVA(using the [webrtc](https://github.com/OHF-Voice/webrtc-noise-gain) library) automatically adjusts the microphone input volume to keep it at a consistent level. If you're speaking quietly it boosts the signal, if you're speaking loudly it reduces it. Useful in the following ways:-
|
|
||||||
|
|
||||||
- Microphones that are too quiet by default
|
|
||||||
- Environments where you move around relative to the mic
|
|
||||||
- Keeping wake word detection consistent regardless of speaking volume
|
|
||||||
|
|
||||||
### Noise Suppression
|
|
||||||
|
|
||||||
When you change noise suppression, LVA(using the [webrtc](https://github.com/OHF-Voice/webrtc-noise-gain) library) filters out constant background noise from the audio signal. It works by learning what "silence" sounds like in your environment and subtracting that from the audio. Useful in the following ways:
|
|
||||||
|
|
||||||
- Noisy environments
|
|
||||||
- Improving STT accuracy by sending cleaner audio to Home Assistant
|
|
||||||
- Reducing false wake word triggers from background noise
|
|
||||||
|
|
||||||
## Using Gain and Noise Suppression with LVA
|
|
||||||
|
|
||||||
LVA implements gain and Noise Suppression in two ways:
|
|
||||||
|
|
||||||
- [CLI Argument/ENV file](#cli-argumentenv-file)
|
|
||||||
- [Home Assistant Entity](#home-assistant-entity)
|
|
||||||
|
|
||||||
### CLI Argument/ENV file
|
|
||||||
|
|
||||||
#### CLI Argument
|
|
||||||
|
|
||||||
Using docker-entrypoint.sh the following flags can be used to set gain and noise suppression:
|
|
||||||
- `--mic-auto-gain`(ranges from 0-31)
|
|
||||||
- `--mic-noise-suppression`(ranges from 0-4)
|
|
||||||
|
|
||||||
#### ENV file
|
|
||||||
|
|
||||||
You can add/edit these variables in the .env file to set gain and noise suppression:
|
|
||||||
- `MIC_AUTO_GAIN="0"`
|
|
||||||
- `MIC_NOISE_SUPPRESSION="0"`
|
|
||||||
|
|
||||||
|
|
||||||
### Home Assistant Entity
|
|
||||||
|
|
||||||
LVA exposes two slider entities to change these gain and noise suppression from which you can edit the gain and noise suppression at runtime.
|
|
||||||
|
|
||||||
💡 **Note:** Setting the flag and ENV values to 0 turns them off and are not used.
|
|
||||||
|
|
||||||
💡 **Note:** Keep in mind that when the flags are set they will overwrite the previous value in the preferences file and also they will be overridden if the value is changed in Home Assistant(Also applies to the ENV file but the ENV file will always overwrite the last value on startup).
|
|
||||||
|
|
||||||
|
|
||||||
## Wake Word Sensitivity
|
|
||||||
|
|
||||||
LVA exposes three numeric controls in the Home Assistant device page for fine-grained sensitivity tuning. These let you dial in the exact probability threshold that best matches your microphone quality, room acoustics, and false-activation tolerance.
|
|
||||||
|
|
||||||
| Entity | Description | Default |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| **Wake Word 1 Sensitivity** | Probability cutoff for the primary wake word | From model manifest |
|
|
||||||
| **Wake Word 2 Sensitivity** | Probability cutoff for the secondary wake word (if active) | From model manifest |
|
|
||||||
| **Stop Word Sensitivity** | Probability cutoff for the stop word | From model manifest |
|
|
||||||
|
|
||||||
Values range from `0.0` to `1.0`:
|
|
||||||
|
|
||||||
- **Higher value** (e.g. `0.95`) → more selective, fewer false activations, may miss quieter or accented speech
|
|
||||||
- **Lower value** (e.g. `0.50`) → more responsive, but more likely to trigger on similar-sounding words
|
|
||||||
The defaults are read directly from each model's `.json` manifest file (the `probability_cutoff` field), so bundled models like `okay_nabu` (0.85), `hey_jarvis` (0.97), and custom downloaded models all start at their author-tested baseline. Changes made in the Home Assistant UI are persisted in `preferences.json` and survive restarts.
|
|
||||||
|
|
||||||
**Tuning guide:**
|
|
||||||
|
|
||||||
- If the wake word **rarely activates** (misses your voice): lower the value slightly, e.g. by `0.05` steps
|
|
||||||
- If the wake word **activates too often** (false triggers from TV, music, or similar words): raise the value slightly
|
|
||||||
- Start from the model's default and make small adjustments — a change of `0.05–0.10` is usually enough to notice a difference
|
|
||||||
- Far-field microphones with noise cancellation (e.g. ReSpeaker, Satellite1) generally work well at the default values; basic USB microphones may need a lower threshold
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Dashboard / Home Assistant Entities
|
||||||
|
|
||||||
|
Once your satellite is connected, Home Assistant exposes a set of controls on its device page (via the ESPHome integration) that let you change LVA's behavior at runtime, without restarting the container or editing config files. This document walks through each of those entities.
|
||||||
|
|
||||||
|
> **Go to [Settings > Devices & services](https://my.home-assistant.io/redirect/integrations/) and select the ESPHome integration.**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Wake Word
|
||||||
|
|
||||||
|
The device page exposes one or two wake word selects (**Wake Word 1**, **Wake Word 2**), depending on whether your assistant is configured for dual wake words. Changing the selection here takes effect immediately and is persisted to `preferences.json`, so it survives restarts.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
💡 **Note:** Models suffixed with **(OWW)** in the dropdown are [openWakeWord](https://github.com/dscripka/openWakeWord) models; everything else is a [microWakeWord](https://github.com/kahrendt/microWakeWord) model. Both engines can be mixed and matched, but keep in mind:
|
||||||
|
|
||||||
|
- **microWakeWord** models are lighter-weight and use engine that can run on lower-end devices and are located in the `wakewords` directory.
|
||||||
|
- **openWakeWord** models use higher engine with more accuracy and are located in the `wakewords/openWakeWord` directory.
|
||||||
|
|
||||||
|
See the [Wake Word section](install_application.md#wake-word) of the installation guide for the full list of bundled models and how to add custom ones.
|
||||||
|
|
||||||
|
## Mute Switch
|
||||||
|
|
||||||
|
The **Mute** switch disables wake word detection and stops the assistant from responding to any wake word or button-triggered listening, without stopping the LVA process itself. While muted, a configurable sound plays to confirm the mute/unmute transition (`--mute-sound` / `--unmute-sound`).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Thinking Sound
|
||||||
|
|
||||||
|
The **Thinking Sound** switch toggles whether a short audible chime plays while the assistant is processing your request (between the end of listening and the start of the response). This is the same setting as the `--enable-thinking-sound` CLI flag / `ENABLE_THINKING_SOUND` environment variable, but can be flipped on or off at runtime from this switch. The change is saved to `preferences.json`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Media Player
|
||||||
|
|
||||||
|
The **Media Player** entity controls playback (play/pause/stop) and volume for both:
|
||||||
|
|
||||||
|
- Music/media played through the satellite, and
|
||||||
|
- The assist pipeline itself (wake word chime, TTS responses, thinking/processing sound, etc.)
|
||||||
|
|
||||||
|
Both share a single volume control — there is no separate volume for the pipeline versus music playback. Adjusting the volume slider on this entity changes both at once, and the value is persisted to `preferences.json` so it's restored on the next startup.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Microphone Settings
|
||||||
|
|
||||||
|
### Volume
|
||||||
|
|
||||||
|
Controls the microphone volume to adjust the area distance from which the microphone can pick up your voice. The volume slider is persisted to `preferences.json` so it's restored on the next startup.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### What do Gain and Noise Suppression do
|
||||||
|
|
||||||
|
#### Gain
|
||||||
|
|
||||||
|
When you change gain, LVA(using the [webrtc](https://github.com/OHF-Voice/webrtc-noise-gain) library) automatically adjusts the microphone input volume to keep it at a consistent level. If you're speaking quietly it boosts the signal, if you're speaking loudly it reduces it. Useful in the following ways:-
|
||||||
|
|
||||||
|
- Microphones that are too quiet by default
|
||||||
|
- Environments where you move around relative to the mic
|
||||||
|
- Keeping wake word detection consistent regardless of speaking volume
|
||||||
|
|
||||||
|
#### Noise Suppression
|
||||||
|
|
||||||
|
When you change noise suppression, LVA(using the [webrtc](https://github.com/OHF-Voice/webrtc-noise-gain) library) filters out constant background noise from the audio signal. It works by learning what "silence" sounds like in your environment and subtracting that from the audio. Useful in the following ways:
|
||||||
|
|
||||||
|
- Noisy environments
|
||||||
|
- Improving STT accuracy by sending cleaner audio to Home Assistant
|
||||||
|
- Reducing false wake word triggers from background noise
|
||||||
|
|
||||||
|
### Using Gain and Noise Suppression with LVA
|
||||||
|
|
||||||
|
LVA implements gain and Noise Suppression in two ways:
|
||||||
|
|
||||||
|
- [CLI Argument/ENV file](#cli-argumentenv-file)
|
||||||
|
- [Home Assistant Entity](#home-assistant-entity)
|
||||||
|
|
||||||
|
### CLI Argument/ENV file
|
||||||
|
|
||||||
|
#### CLI Argument
|
||||||
|
|
||||||
|
Using docker-entrypoint.sh the following flags can be used to set gain and noise suppression:
|
||||||
|
- `--mic-auto-gain`(ranges from 0-31)
|
||||||
|
- `--mic-noise-suppression`(ranges from 0-4)
|
||||||
|
|
||||||
|
#### ENV file
|
||||||
|
|
||||||
|
You can add/edit these variables in the .env file to set gain and noise suppression:
|
||||||
|
- `MIC_AUTO_GAIN="0"`
|
||||||
|
- `MIC_NOISE_SUPPRESSION="0"`
|
||||||
|
|
||||||
|
|
||||||
|
### Home Assistant Entity
|
||||||
|
|
||||||
|
LVA exposes two entities to change these gain and noise suppression from which you can edit them at runtime.
|
||||||
|
|
||||||
|
- 
|
||||||
|
- 
|
||||||
|
|
||||||
|
💡 **Note:** Setting the flag and ENV values to 0 turns them off and are not used.
|
||||||
|
|
||||||
|
💡 **Note:** Keep in mind that when the flags are set they will overwrite the previous value in the preferences file and also they will be overridden if the value is changed in Home Assistant(Also applies to the ENV file but the ENV file will always overwrite the last value on startup).
|
||||||
|
|
||||||
|
|
||||||
|
### Wake Word Sensitivity
|
||||||
|
|
||||||
|
LVA exposes three numeric controls in the Home Assistant device page for fine-grained sensitivity tuning. These let you dial in the exact probability threshold that best matches your microphone quality, room acoustics, and false-activation tolerance.
|
||||||
|
|
||||||
|
| Entity | Description | Default |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| **Wake Word 1 Sensitivity**  | Probability cutoff for the primary wake word | From model manifest |
|
||||||
|
| **Wake Word 2 Sensitivity**  | Probability cutoff for the secondary wake word (if active) | From model manifest |
|
||||||
|
| **Stop Word Sensitivity**  | Probability cutoff for the stop word | From model manifest |
|
||||||
|
|
||||||
|
Values range from `0.0` to `1.0`:
|
||||||
|
|
||||||
|
- **Higher value** (e.g. `0.95`) → more selective, fewer false activations, may miss quieter or accented speech
|
||||||
|
- **Lower value** (e.g. `0.50`) → more responsive, but more likely to trigger on similar-sounding words
|
||||||
|
The defaults are read directly from each model's `.json` manifest file (the `probability_cutoff` field), so bundled models like `okay_nabu` (0.85), `hey_jarvis` (0.97), and custom downloaded models all start at their author-tested baseline. Changes made in the Home Assistant UI are persisted in `preferences.json` and survive restarts.
|
||||||
|
|
||||||
|
**Tuning guide:**
|
||||||
|
|
||||||
|
- If the wake word **rarely activates** (misses your voice): lower the value slightly, e.g. by `0.05` steps
|
||||||
|
- If the wake word **activates too often** (false triggers from TV, music, or similar words): raise the value slightly
|
||||||
|
- Start from the model's default and make small adjustments — a change of `0.05–0.10` is usually enough to notice a difference
|
||||||
|
- Far-field microphones with noise cancellation (e.g. ReSpeaker, Satellite1) generally work well at the default values; basic USB microphones may need a lower threshold
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If a dashboard control doesn't behave as expected, enable `--debug` (or `ENABLE_DEBUG=1`) and check the logs — every change made from the Home Assistant device page is logged when it's received and persisted.
|
||||||
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -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)
|
- Download ready-to-use image from [PiCompose](https://github.com/florian-asche/PiCompose)
|
||||||
- Change configuration in /compose/ directory if you need to.
|
- 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.
|
- More information within the PiCompose project.
|
||||||
|
|
||||||
### Option B) Install on your own:
|
### Option B) Install on your own:
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ Environment=PREFERENCES_FILE="/home/pi/linux-voice-assistant/preferences.json"
|
|||||||
# Environment=PORT="6053"
|
# Environment=PORT="6053"
|
||||||
# Environment=AUDIO_INPUT_DEVICE="default"
|
# Environment=AUDIO_INPUT_DEVICE="default"
|
||||||
# Environment=AUDIO_OUTPUT_DEVICE="default"
|
# Environment=AUDIO_OUTPUT_DEVICE="default"
|
||||||
|
# Environment=MUSIC_OUTPUT_DEVICE="default"
|
||||||
# Environment=MIC_VOLUME="1.0"
|
# Environment=MIC_VOLUME="1.0"
|
||||||
# Environment=MIC_AUTO_GAIN="0"
|
# Environment=MIC_AUTO_GAIN="0"
|
||||||
# Environment=MIC_NOISE_SUPPRESSION="0"
|
# Environment=MIC_NOISE_SUPPRESSION="0"
|
||||||
@@ -260,16 +261,20 @@ 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_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_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 |
|
| `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 |
|
| `PREFERENCES_FILE` | (optional) | Path to a custom preferences JSON file |
|
||||||
| `NETWORK_INTERFACE` | Autodetected | network card for server |
|
| `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 |
|
| `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 |
|
| `PORT` | `6053` | API server port |
|
||||||
| `AUDIO_INPUT_DEVICE` | Autodetected | Audio input device name |
|
| `AUDIO_INPUT_DEVICE` | Autodetected | Audio input device name |
|
||||||
| `AUDIO_OUTPUT_DEVICE` | Autodetected | Audio output device name |
|
| `AUDIO_OUTPUT_DEVICE` | Autodetected | Audio output device name |
|
||||||
| `MIC_VOLUME` | Control microphone volume | 100 |
|
| `MUSIC_OUTPUT_DEVICE` | `AUDIO_OUTPUT_DEVICE` | Music/media output device name (defaults to the voice output device) |
|
||||||
| `MIC_AUTO_GAIN` | Add WebRTC Gain to Mic | 0 |
|
| `MIC_VOLUME` | 100 | Control microphone volume |
|
||||||
| `MIC_NOISE_SUPPRESSION` | Add WebRTC Noise Suppresion to Mic | 0 |
|
| `MIC_AUTO_GAIN` | 0 | Add WebRTC Gain to Mic |
|
||||||
| `AUDIO_INPUT_CHANNELS` | Number of audio input channels | 2 |
|
| `MIC_NOISE_SUPPRESSION` | 0 | Add WebRTC Noise Suppression to Mic |
|
||||||
|
| `AUDIO_INPUT_CHANNELS` | 2 | Number of audio input channels |
|
||||||
| `ENABLE_THINKING_SOUND` | false | Set to "1" to enable thinking sound |
|
| `ENABLE_THINKING_SOUND` | false | Set to "1" to enable thinking sound |
|
||||||
| `WAKE_WORD_DIR` | `app/wakewords` | Path to the wake word directory |
|
| `WAKE_WORD_DIR` | `app/wakewords` | Path to the wake word directory |
|
||||||
| `WAKE_MODEL` | `okay_nabu` | Wake word model to use |
|
| `WAKE_MODEL` | `okay_nabu` | Wake word model to use |
|
||||||
@@ -283,7 +288,7 @@ The following variables can be configured in the `.env` or in the service file:
|
|||||||
| `PROCESSING_SOUND` | `sounds/processing.wav` | Sound file for processing state |
|
| `PROCESSING_SOUND` | `sounds/processing.wav` | Sound file for processing state |
|
||||||
| `LISTEN_DURING_WAKE_SOUND` | false | Set to "1" to start listening immediately after wake word detection, without waiting for the wake sound to finish |
|
| `LISTEN_DURING_WAKE_SOUND` | false | Set to "1" to start listening immediately after wake word detection, without waiting for the wake sound to finish |
|
||||||
| `MUTE_SOUND` | `sounds/mute_switch_on.flac` | Sound file for mute on |
|
| `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
|
| `UNMUTE_SOUND` | `sounds/mute_switch_off.flac` | Sound file for mute off |
|
||||||
| `PERIPHERAL_HOST` | 0.0.0.0 | Host for the peripheral WebSocket API |
|
| `PERIPHERAL_HOST` | 0.0.0.0 | Host for the peripheral WebSocket API |
|
||||||
| `PERIPHERAL_PORT` | 6055 | Port for the peripheral WebSocket API |
|
| `PERIPHERAL_PORT` | 6055 | Port for the peripheral WebSocket API |
|
||||||
| `PERIPHERAL_VOLUME_STEP` | %(default)s | Volume change per button press |
|
| `PERIPHERAL_VOLUME_STEP` | %(default)s | Volume change per button press |
|
||||||
@@ -311,6 +316,8 @@ If you want to use your own sounds, you can add them to the `sounds/custom` aka
|
|||||||
|
|
||||||
### Wake Word:
|
### Wake Word:
|
||||||
|
|
||||||
|
💡 **Note:** The active wake word(s) can also be changed at runtime from the Home Assistant device page, without editing `.env` or restarting LVA — see [Wake Word](configuration.md#wake-word) in the dashboard documentation.
|
||||||
|
|
||||||
#### Available Wake Word Models:
|
#### Available Wake Word Models:
|
||||||
|
|
||||||
The following wake word models are available:
|
The following wake word models are available:
|
||||||
@@ -323,7 +330,6 @@ The following wake word models are available:
|
|||||||
- `hey_morgan` - Morgan wake word
|
- `hey_morgan` - Morgan wake word
|
||||||
- `hey_luna` - Luna wake word
|
- `hey_luna` - Luna wake word
|
||||||
- `hey_home_assistant` - Home Assistant wake word
|
- `hey_home_assistant` - Home Assistant wake word
|
||||||
- `stop` - Stop wake word
|
|
||||||
- `okay_computer` - Okay Computer wake word
|
- `okay_computer` - Okay Computer wake word
|
||||||
- `choo_choo_homie` - Choo Choo Homie wake word
|
- `choo_choo_homie` - Choo Choo Homie wake word
|
||||||
|
|
||||||
@@ -334,6 +340,9 @@ The following wake word models are available:
|
|||||||
- `hey_mycroft` - Mycroft wake word
|
- `hey_mycroft` - Mycroft wake word
|
||||||
- `hey_rhasspy` - Rhasspy wake word
|
- `hey_rhasspy` - Rhasspy wake word
|
||||||
|
|
||||||
|
### Stop Word:
|
||||||
|
|
||||||
|
The `stop` wake word is used to stop the current process (response, announcement, or timer). It is available in the `wakewords` directory (using the microWakeWord engine) and can be referenced in the `.env` file with `STOP_MODEL=stop`.
|
||||||
|
|
||||||
### Custom Wake Word:
|
### Custom Wake Word:
|
||||||
|
|
||||||
|
|||||||
@@ -54,11 +54,31 @@ sudo mkdir -p /etc/pipewire/pipewire.conf.d
|
|||||||
sudo vi /etc/pipewire/pipewire.conf.d/linux-voice-assistant.conf
|
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:
|
Add the following content:
|
||||||
```
|
```
|
||||||
context.properties = {
|
context.properties = {
|
||||||
default.clock.rate = 16000
|
default.clock.rate = 16000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.modules = [
|
||||||
|
{ name = libpipewire-module-rt
|
||||||
|
args = {
|
||||||
|
nice.level = -11
|
||||||
|
rt.prio = 88
|
||||||
|
}
|
||||||
|
flags = [ ifexists nofail ]
|
||||||
|
}
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Applying the Changes
|
#### Applying the Changes
|
||||||
|
|||||||
@@ -153,7 +153,37 @@ export XDG_RUNTIME_DIR=/run/user/1000
|
|||||||
export PULSE_SERVER=unix:/run/user/1000/pulse/native
|
export PULSE_SERVER=unix:/run/user/1000/pulse/native
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
To make it persistent across reboots
|
||||||
|
```
|
||||||
|
mkdir -p ~/.config/systemd/user/
|
||||||
|
nano ~/.config/systemd/user/jabra-lva.service
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=Jabra Speak 410 LVA Controller
|
||||||
|
After=network.target pipewire.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=%h/linux-voice-assistant/examples/Jabra Speak 410
|
||||||
|
ExecStart=%h/linux-voice-assistant/venv/bin/python main.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment="VOLUME_CONTROL=pipewire"
|
||||||
|
Environment="LVA_WS_URL=ws://localhost:6055"
|
||||||
|
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
||||||
|
Environment="PULSE_SERVER=unix:/run/user/1000/pulse/native"
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
Enable and start service
|
||||||
|
```
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable jabra-lva
|
||||||
|
systemctl --user start jabra-lva
|
||||||
|
```
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|||||||
@@ -164,9 +164,9 @@ class SharedState:
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# USB LED driver
|
# USB LED driver
|
||||||
#
|
#
|
||||||
# The ReSpeaker Mic Array v2.0 accepts LED data via a USB vendor control
|
# The ReSpeaker Mic Array v2.0 LED firmware exposes a vendor-command USB
|
||||||
# transfer. Each LED is set as an APA102 frame: 0xFF start, then per-LED
|
# interface. The copied pixel_ring reference uses command 0x06 for custom LED
|
||||||
# bytes in the order [start_frame | brightness, B, G, R].
|
# frames and command 0x20 for brightness, both addressed to wIndex 0x1C.
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
RGB = Tuple[int, int, int]
|
RGB = Tuple[int, int, int]
|
||||||
@@ -186,20 +186,17 @@ def _scale(color: RGB, factor: float) -> RGB:
|
|||||||
|
|
||||||
class USBLEDRing:
|
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,
|
Uses the same vendor-command protocol as Seeed's pixel_ring library.
|
||||||
wValue=0x0001, wIndex=0x8001) to push raw LED data to the XMOS firmware.
|
That protocol talks to the device recipient directly and does not require
|
||||||
This is the same protocol used by Seeed's pixel_ring library.
|
detaching the kernel audio driver.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# USB control transfer parameters matching the pixel_ring reference implementation.
|
_CMD_SHOW = 0x06
|
||||||
# _CTRL_TYPE_OUT = 0x40: bmRequestType = CTRL_OUT | CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE.
|
_CMD_BRIGHTNESS = 0x20
|
||||||
# Recipient is "device" (bits 4:0 = 0x00), so no interface needs to be claimed
|
|
||||||
# and the kernel audio driver must NOT be detached.
|
|
||||||
_CTRL_TIMEOUT = 8000 # ms
|
_CTRL_TIMEOUT = 8000 # ms
|
||||||
_CTRL_REQUEST = 0
|
_CTRL_REQUEST = 0
|
||||||
_CTRL_VALUE = 0x0001
|
|
||||||
_CTRL_INDEX = 0x1C
|
_CTRL_INDEX = 0x1C
|
||||||
_CTRL_TYPE_OUT = 0x40 # vendor, device, host-to-device
|
_CTRL_TYPE_OUT = 0x40 # vendor, device, host-to-device
|
||||||
|
|
||||||
@@ -207,6 +204,7 @@ class USBLEDRing:
|
|||||||
self._pixels: list[RGB] = [BLACK] * LED_COUNT
|
self._pixels: list[RGB] = [BLACK] * LED_COUNT
|
||||||
self._dev = None
|
self._dev = None
|
||||||
self._dev_lock = threading.Lock()
|
self._dev_lock = threading.Lock()
|
||||||
|
self._last_brightness: Optional[int] = None
|
||||||
|
|
||||||
if _HAS_USB:
|
if _HAS_USB:
|
||||||
self._find_device()
|
self._find_device()
|
||||||
@@ -220,12 +218,8 @@ class USBLEDRing:
|
|||||||
USB_VENDOR_ID, USB_PRODUCT_ID,
|
USB_VENDOR_ID, USB_PRODUCT_ID,
|
||||||
)
|
)
|
||||||
return
|
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._dev = dev
|
||||||
|
self._last_brightness = None
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"ReSpeaker Mic Array v2.0 found (USB %04x:%04x, bus %d, addr %d)",
|
"ReSpeaker Mic Array v2.0 found (USB %04x:%04x, bus %d, addr %d)",
|
||||||
USB_VENDOR_ID, USB_PRODUCT_ID, dev.bus, dev.address,
|
USB_VENDOR_ID, USB_PRODUCT_ID, dev.bus, dev.address,
|
||||||
@@ -237,24 +231,32 @@ class USBLEDRing:
|
|||||||
def set_all(self, color: RGB) -> None:
|
def set_all(self, color: RGB) -> None:
|
||||||
self._pixels = [color] * LED_COUNT
|
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:
|
def show(self, brightness: float = 1.0) -> None:
|
||||||
"""
|
"""
|
||||||
Push the current pixel buffer to the device.
|
Push the current pixel buffer to the device.
|
||||||
|
|
||||||
Frame layout (matches pixel_ring / APA102 USB protocol):
|
The USB firmware expects command 0x06 with 4 bytes per LED in RGB0
|
||||||
[0xFF, 0xFF, 0xFF, 0xFF] ← start frame
|
order, plus command 0x20 for device brightness. Sending raw APA102
|
||||||
[0xE0|bright5, B, G, R] × LED_COUNT ← one word per LED
|
frames here causes the firmware to misinterpret the payload.
|
||||||
[0xFF, ...] ← end frame (⌈n/2⌉ bytes)
|
|
||||||
"""
|
"""
|
||||||
bright5 = max(0, min(31, int(brightness * LED_BRIGHTNESS)))
|
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:
|
for r, g, b in self._pixels:
|
||||||
br = max(0, min(255, int(r * brightness)))
|
br = max(0, min(255, int(r * brightness)))
|
||||||
bg = max(0, min(255, int(g * brightness)))
|
bg = max(0, min(255, int(g * brightness)))
|
||||||
bb = max(0, min(255, int(b * brightness)))
|
bb = max(0, min(255, int(b * brightness)))
|
||||||
frame += [0xE0 | bright5, bb, bg, br]
|
frame += [br, bg, bb, 0]
|
||||||
frame += [0xFF] * math.ceil(LED_COUNT / 2) # end frame
|
|
||||||
|
|
||||||
with self._dev_lock:
|
with self._dev_lock:
|
||||||
if self._dev is None:
|
if self._dev is None:
|
||||||
@@ -262,17 +264,14 @@ class USBLEDRing:
|
|||||||
_LOGGER.debug("LEDs [simulated]: %s", pixels)
|
_LOGGER.debug("LEDs [simulated]: %s", pixels)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self._dev.ctrl_transfer(
|
if self._last_brightness != bright5:
|
||||||
self._CTRL_TYPE_OUT,
|
self._write(self._CMD_BRIGHTNESS, [bright5])
|
||||||
self._CTRL_REQUEST,
|
self._last_brightness = bright5
|
||||||
self._CTRL_VALUE,
|
self._write(self._CMD_SHOW, frame)
|
||||||
self._CTRL_INDEX,
|
|
||||||
frame,
|
|
||||||
self._CTRL_TIMEOUT,
|
|
||||||
)
|
|
||||||
except usb.core.USBError as exc:
|
except usb.core.USBError as exc:
|
||||||
_LOGGER.warning("USB write error: %s – attempting reconnect", exc)
|
_LOGGER.warning("USB write error: %s – attempting reconnect", exc)
|
||||||
self._dev = None
|
self._dev = None
|
||||||
|
self._last_brightness = None
|
||||||
# Attempt to re-find the device on next show() call
|
# Attempt to re-find the device on next show() call
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._reconnect, daemon=True
|
target=self._reconnect, daemon=True
|
||||||
@@ -709,11 +708,19 @@ class LVAClient:
|
|||||||
_LOGGER.debug("Event: %s data=%s", event, data)
|
_LOGGER.debug("Event: %s data=%s", event, data)
|
||||||
|
|
||||||
if event == "snapshot":
|
if event == "snapshot":
|
||||||
|
muted = data.get("muted", False)
|
||||||
|
ha_connected = data.get("ha_connected", False)
|
||||||
self._state.update(
|
self._state.update(
|
||||||
muted=data.get("muted", False),
|
muted=muted,
|
||||||
volume=data.get("volume", 1.0),
|
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":
|
elif event == "wake_word_detected":
|
||||||
self._state.update(assist_state=AssistState.WAKE_WORD)
|
self._state.update(assist_state=AssistState.WAKE_WORD)
|
||||||
@@ -731,7 +738,12 @@ class LVAClient:
|
|||||||
self._state.update(assist_state=AssistState.IDLE)
|
self._state.update(assist_state=AssistState.IDLE)
|
||||||
|
|
||||||
elif event == "muted":
|
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":
|
elif event == "pipeline_error":
|
||||||
_LOGGER.warning("LVA pipeline error: %s", data.get("reason", ""))
|
_LOGGER.warning("LVA pipeline error: %s", data.get("reason", ""))
|
||||||
@@ -777,6 +789,12 @@ class LVAClient:
|
|||||||
status = data.get("status", "")
|
status = data.get("status", "")
|
||||||
if status == "connected":
|
if status == "connected":
|
||||||
self._state.update(ha_connected=True)
|
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")
|
_LOGGER.info("Home Assistant connected")
|
||||||
elif status == "getting_started":
|
elif status == "getting_started":
|
||||||
_LOGGER.info("LVA starting up, waiting for HA …")
|
_LOGGER.info("LVA starting up, waiting for HA …")
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://app.kilo.ai/config.json",
|
||||||
|
"model": "kilo/codegemma-2b:free",
|
||||||
|
"instructions": ["AGENTS.md"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -66,6 +66,10 @@ async def main() -> None:
|
|||||||
"--audio-output-device",
|
"--audio-output-device",
|
||||||
help="Name for the audio output device (see --list-output-devices)",
|
help="Name for the audio output device (see --list-output-devices)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--music-output-device",
|
||||||
|
help="mpv name for the music/media output device (defaults to --audio-output-device)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--list-output-devices",
|
"--list-output-devices",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -234,6 +238,11 @@ async def main() -> None:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Add this to enable debug logging",
|
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(
|
parser.add_argument(
|
||||||
"--output-only",
|
"--output-only",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -241,6 +250,15 @@ async def main() -> None:
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
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:
|
if args.list_input_devices:
|
||||||
print("Audio Input devices:")
|
print("Audio Input devices:")
|
||||||
print("=" * 13)
|
print("=" * 13)
|
||||||
@@ -259,9 +277,6 @@ async def main() -> None:
|
|||||||
print(speaker["name"] + ":", speaker["description"])
|
print(speaker["name"] + ":", speaker["description"])
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
|
||||||
_LOGGER.debug(args)
|
|
||||||
|
|
||||||
# Resolve network interface for mac-address detection
|
# Resolve network interface for mac-address detection
|
||||||
if not args.network_interface:
|
if not args.network_interface:
|
||||||
print("No network interface specified, try to detect default interface")
|
print("No network interface specified, try to detect default interface")
|
||||||
@@ -408,7 +423,7 @@ async def main() -> None:
|
|||||||
wake_words=wake_models,
|
wake_words=wake_models,
|
||||||
active_wake_words=active_wake_words,
|
active_wake_words=active_wake_words,
|
||||||
stop_word=stop_model,
|
stop_word=stop_model,
|
||||||
music_player=MpvMediaPlayer(device=args.audio_output_device),
|
music_player=MpvMediaPlayer(device=args.music_output_device or args.audio_output_device),
|
||||||
tts_player=MpvMediaPlayer(device=args.audio_output_device),
|
tts_player=MpvMediaPlayer(device=args.audio_output_device),
|
||||||
wakeup_sound=args.wakeup_sound,
|
wakeup_sound=args.wakeup_sound,
|
||||||
start_listening_sound=args.start_listening_sound,
|
start_listening_sound=args.start_listening_sound,
|
||||||
@@ -575,6 +590,38 @@ async def main() -> None:
|
|||||||
_LOGGER.debug("Server stopped")
|
_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],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||||
|
|
||||||
@@ -180,6 +181,8 @@ class PeripheralAPIServer:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_VOLUME_STEP: float = 0.05
|
DEFAULT_VOLUME_STEP: float = 0.05
|
||||||
|
LATE_ENTITY_RECONNECT_DEBOUNCE_S: float = 1.5
|
||||||
|
LATE_ENTITY_RECONNECT_COOLDOWN_S: float = 60.0
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -208,6 +211,10 @@ class PeripheralAPIServer:
|
|||||||
self._current_state: Optional[LVAEvent] = None
|
self._current_state: Optional[LVAEvent] = None
|
||||||
self._current_state_data: Optional[Dict[str, Any]] = 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
|
# Lifecycle
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -230,6 +237,10 @@ class PeripheralAPIServer:
|
|||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Gracefully shut down the server and all client connections."""
|
"""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:
|
if self._server is not None:
|
||||||
self._server.close()
|
self._server.close()
|
||||||
await self._server.wait_closed()
|
await self._server.wait_closed()
|
||||||
@@ -281,14 +292,20 @@ class PeripheralAPIServer:
|
|||||||
# Replay the current event state so the client immediately shows the
|
# Replay the current event state so the client immediately shows the
|
||||||
# right animation — e.g. a timer ticking animation when reconnecting
|
# right animation — e.g. a timer ticking animation when reconnecting
|
||||||
# mid-timer, or the muted indicator when reconnecting while muted.
|
# mid-timer, or the muted indicator when reconnecting while muted.
|
||||||
if self._current_state is not None:
|
current_state = self._current_state
|
||||||
state_payload: Dict[str, Any] = {"event": self._current_state.value}
|
if current_state is None:
|
||||||
if self._current_state_data:
|
return
|
||||||
state_payload["data"] = self._current_state_data
|
|
||||||
try:
|
if current_state == LVAEvent.DISCONNECTED and state.connected:
|
||||||
await websocket.send(json.dumps(state_payload))
|
return
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
pass
|
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
|
# Command dispatch
|
||||||
@@ -501,6 +518,8 @@ class PeripheralAPIServer:
|
|||||||
if satellite is not None:
|
if satellite is not None:
|
||||||
satellite.register_pending_lights()
|
satellite.register_pending_lights()
|
||||||
|
|
||||||
|
self._schedule_ha_reconnect_for_late_entity("light", object_id)
|
||||||
|
|
||||||
def _register_button(self, satellite: Any) -> None:
|
def _register_button(self, satellite: Any) -> None:
|
||||||
"""Register button press event support declared by a peripheral.
|
"""Register button press event support declared by a peripheral.
|
||||||
|
|
||||||
@@ -528,10 +547,66 @@ class PeripheralAPIServer:
|
|||||||
if satellite is not None:
|
if satellite is not None:
|
||||||
satellite.register_pending_button()
|
satellite.register_pending_button()
|
||||||
|
|
||||||
|
self._schedule_ha_reconnect_for_late_entity("button", "button_press_event")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Helpers
|
# 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:
|
async def _push_mute_switch(self, satellite: Any, *, muted: bool) -> None:
|
||||||
"""Reflect a peripheral-triggered mute change to Home Assistant."""
|
"""Reflect a peripheral-triggered mute change to Home Assistant."""
|
||||||
state = self._state
|
state = self._state
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def handle_voice_event(self, event_type: VoiceAssistantEventType, data: Dict[str, str]) -> None:
|
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:
|
if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_START:
|
||||||
self._tts_url = data.get("url")
|
self._tts_url = data.get("url")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import subprocess
|
import subprocess
|
||||||
import venv
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import ctypes.util
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_DIR = Path(__file__).parent
|
_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)")
|
parser.add_argument("--makeflags", type=str, help="MAKEFLAGS for compilation (default: -j1)")
|
||||||
args = parser.parse_args()
|
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
|
# Create virtual environment
|
||||||
builder = venv.EnvBuilder(with_pip=True)
|
if _VENV_DIR.exists():
|
||||||
context = builder.ensure_directories(_VENV_DIR)
|
shutil.rmtree(_VENV_DIR)
|
||||||
builder.create(_VENV_DIR)
|
|
||||||
|
subprocess.check_call([python_311_exe, "-m", "venv", str(_VENV_DIR), "--symlinks"])
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
env = {}
|
env = {}
|
||||||
@@ -28,17 +67,16 @@ if args.makeflags:
|
|||||||
env["MAKEFLAGS"] = args.makeflags
|
env["MAKEFLAGS"] = args.makeflags
|
||||||
|
|
||||||
# Upgrade dependencies
|
# 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", "pip"])
|
||||||
subprocess.check_call(pip + ["install", "--upgrade", "setuptools", "wheel"])
|
subprocess.check_call(pip + ["install", "--upgrade", "setuptools", "wheel"])
|
||||||
|
|
||||||
# Install requirements
|
# Install requirements
|
||||||
subprocess.check_call(pip + ["install", "-e", str(_PROGRAM_DIR)])
|
subprocess.check_call(pip + ["install", "-e", str(_PROGRAM_DIR)])
|
||||||
|
|
||||||
|
|
||||||
# Install dev requirements
|
# Install dev requirements
|
||||||
if args.dev:
|
if args.dev:
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
pip + ["install", "-e", f"{_PROGRAM_DIR}[dev]"],
|
pip + ["install", "-e", f"{_PROGRAM_DIR}[dev]"],
|
||||||
env={**dict(os.environ), **env}
|
env={**dict(os.environ), **env}
|
||||||
)
|
)
|
||||||