Rename to Linux Voice Assistant

This commit is contained in:
Michael Hansen
2025-08-26 09:30:58 -05:00
parent 551cb35735
commit 4356633ed6
14 changed files with 92 additions and 33 deletions
+5 -5
View File
@@ -1,6 +1,6 @@
# Home Assistant Voice Satellite (HAV-SAT)
# Linux Voice Assistant
Experimental voice satellite for [Home Assistant][homeassistant] that uses the [ESPHome][esphome] protocol.
Experimental Linux voice assistant for [Home Assistant][homeassistant] that uses the [ESPHome][esphome] protocol.
Runs on Linux `aarch64` and `x86_64` platforms. Tested with Python 3.13 and Python 3.11.
Supports announcments, start/continue conversation, and timers.
@@ -16,14 +16,14 @@ Install system dependencies (`apt-get`):
Clone and install project:
``` sh
git clone https://github.com/OHF-Voice/hav-sat.git
cd hav-sat
git clone https://github.com/OHF-Voice/linux-voice-assistant.git
cd linux-voice-assistant
script/setup
```
## Running
Use `script/run` or `python3 -m hav_sat`
Use `script/run` or `python3 -m linux_voice_assistant`
See `--help` for more options.
@@ -6,11 +6,16 @@ import json
import logging
import threading
import time
import tempfile
import shutil
import posixpath
from collections.abc import Iterable
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from queue import Queue
from typing import Dict, List, Optional
from urllib.request import urlopen
from urllib.parse import urlparse, urlunparse
# pylint: disable=no-name-in-module
import sounddevice as sd
@@ -63,6 +68,7 @@ class AvailableWakeWord:
wake_word: str
trained_languages: List[str]
config_path: Path
url: Optional[str] = None
@dataclass
@@ -81,6 +87,15 @@ class ServerState:
media_player_entity: Optional[MediaPlayerEntity] = None
satellite: "Optional[VoiceSatelliteProtocol]" = None
_external_wake_word_dir: Optional[tempfile.TemporaryDirectory] = None
@property
def external_wake_word_dir(self) -> Path:
if self._external_wake_word_dir is None:
self._external_wake_word_dir = tempfile.TemporaryDirectory()
return Path(self._external_wake_word_dir.name)
# -----------------------------------------------------------------------------
@@ -98,7 +113,7 @@ class VoiceSatelliteProtocol(APIServer):
server=self,
key=len(state.entities),
name="Media Player",
object_id="hav_sat_media_player",
object_id="linux_voice_assistant_media_player",
music_player=state.music_player,
announce_player=state.tts_player,
)
@@ -211,6 +226,26 @@ class VoiceSatelliteProtocol(APIServer):
if isinstance(msg, ListEntitiesRequest):
yield ListEntitiesDoneResponse()
elif isinstance(msg, VoiceAssistantConfigurationRequest):
external_wake_words = {
external_ww.wake_word.id: external_ww
for external_ww in msg.external_wake_words
if external_ww.model_type == "micro_wake_word"
}
for wake_word_id, external_ww in external_wake_words.items():
if wake_word_id in self.state.available_wake_words:
# Skip if already added
continue
self.state.available_wake_words[wake_word_id] = AvailableWakeWord(
id=external_ww.wake_word.id,
wake_word=external_ww.wake_word.wake_word,
trained_languages=external_ww.wake_word.trained_languages,
config_path=self.state.external_wake_word_dir
/ f"{external_ww.wake_word.id}.json",
url=external_ww.url,
)
yield VoiceAssistantConfigurationResponse(
available_wake_words=[
VoiceAssistantWakeWord(
@@ -235,6 +270,48 @@ class VoiceSatelliteProtocol(APIServer):
if not model_info:
continue
if (not model_info.config_path.exists()) and model_info.url:
_LOGGER.debug("Downloading wake word config: %s", model_info.url)
with (
open(model_info.config_path, "wb") as config_file,
urlopen(model_info.url) as response,
):
if response.status != 200:
_LOGGER.error(
"Download failed with status: %s", response.status
)
continue
shutil.copyfileobj(response, config_file)
with open(
model_info.config_path, "r", encoding="utf-8"
) as config_file:
config_dict = json.load(config_file)
parsed_config_url = urlparse(model_info.url)
model_url = urlunparse(
parsed_config_url._replace(
path=posixpath.join(
posixpath.dirname(parsed_config_url.path),
config_dict["model"],
)
)
)
_LOGGER.debug("Downloading wake word model: %s", model_url)
model_path = model_info.config_path.parent / config_dict["model"]
with (
open(model_path, "wb") as model_file,
urlopen(model_url) as response,
):
if response.status != 200:
_LOGGER.error(
"Download failed with status: %s", response.status
)
continue
shutil.copyfileobj(response, model_file)
_LOGGER.debug("Loading wake word: %s", model_info.config_path)
self.state.wake_word = MicroWakeWord.from_config(
model_info.config_path,
+5 -5
View File
@@ -3,15 +3,15 @@ requires = ["setuptools>=62.3"]
build-backend = "setuptools.build_meta"
[project]
name = "hav-satellite"
name = "linux-voice-assistant"
version = "1.0.0"
license = {text = "Apache-2.0"}
description = "Voice satellite for Home Assistant using the ESPHome protocol"
description = "Linux voice assistant for Home Assistant using the ESPHome protocol"
readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
keywords = ["home", "assistant", "voice", "esphome"]
keywords = ["home", "assistant", "voice", "esphome", "linux"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
@@ -41,7 +41,7 @@ dev = [
]
[project.urls]
"Source Code" = "http://github.com/OHF-Voice/hav-sat"
"Source Code" = "http://github.com/OHF-Voice/linux-voice-assistant"
[tool.setuptools]
platforms = ["any"]
@@ -49,5 +49,5 @@ zip-safe = true
include-package-data = true
[tool.setuptools.packages.find]
include = ["hav_sat"]
include = ["linux_voice_assistant"]
exclude = ["tests", "tests.*"]
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
_DIR = Path(__file__).parent
_PROGRAM_DIR = _DIR.parent
_VENV_DIR = _PROGRAM_DIR / ".venv"
_MODULE_DIR = _PROGRAM_DIR / "hav_sat"
_MODULE_DIR = _PROGRAM_DIR / "linux_voice_assistant"
_FORMAT_DIRS = [_MODULE_DIR]
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
_DIR = Path(__file__).parent
_PROGRAM_DIR = _DIR.parent
_VENV_DIR = _PROGRAM_DIR / ".venv"
_MODULE_DIR = _PROGRAM_DIR / "hav_sat"
_MODULE_DIR = _PROGRAM_DIR / "linux_voice_assistant"
_LINT_DIRS = [_MODULE_DIR]
+1 -1
View File
@@ -14,4 +14,4 @@ if _VENV_DIR.exists():
else:
python_exe = "python3"
subprocess.check_call([python_exe, "-m", "hav_sat"] + sys.argv[1:])
subprocess.check_call([python_exe, "-m", "linux_voice_assistant"] + sys.argv[1:])
-18
View File
@@ -1,18 +0,0 @@
{
"type": "micro",
"wake_word": "Hey Peppa Pig",
"author": "Michael Hansen",
"website": "https://www.home-assistant.io",
"model": "hey_peppa_pig.tflite",
"trained_languages": [
"en"
],
"version": 2,
"micro": {
"probability_cutoff": 0.97,
"feature_step_size": 10,
"sliding_window_size": 5,
"tensor_arena_size": 30000,
"minimum_esphome_version": "2024.7.0"
}
}
Binary file not shown.