Rename to Linux Voice Assistant
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:])
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user