Merge pull request #277407 from mweinelt/wyoming-satellite

wyoming-satellite: init at 1.2.0
This commit is contained in:
Martin Weinelt 2024-03-29 15:02:50 +01:00 committed by GitHub
commit 9e29e9c255
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 413 additions and 230 deletions

View File

@ -1001,7 +1001,7 @@ Make sure to also check the many updates in the [Nixpkgs library](#sec-release-2
Satellite](https://github.com/synesthesiam/homeassistant-satellite), a
streaming audio satellite for Home Assistant voice pipelines, where you can
reuse existing mic and speaker hardware. Available as
[services.homeassistant-satellite](#opt-services.homeassistant-satellite.enable).
`services.homeassistant-satellite`.
- [Apache Guacamole](https://guacamole.apache.org/), a cross-platform,
clientless remote desktop gateway. Available as

View File

@ -126,6 +126,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
- [armagetronad](https://wiki.armagetronad.org), a mid-2000s 3D lightcycle game widely played at iD Tech Camps. You can define multiple servers using `services.armagetronad.<server>.enable`.
- [wyoming-satellite](https://github.com/rhasspy/wyoming-satellite), a voice assistant satellite for Home Assistant using the Wyoming protocol. Available as [services.wyoming.satellite]($opt-services.wyoming.satellite.enable).
- [TuxClocker](https://github.com/Lurkki14/tuxclocker), a hardware control and monitoring program. Available as [programs.tuxclocker](#opt-programs.tuxclocker.enable).
- [ALVR](https://github.com/alvr-org/alvr), a VR desktop streamer. Available as [programs.alvr](#opt-programs.alvr.enable)

View File

@ -362,9 +362,6 @@
./services/audio/spotifyd.nix
./services/audio/squeezelite.nix
./services/audio/tts.nix
./services/audio/wyoming/faster-whisper.nix
./services/audio/wyoming/openwakeword.nix
./services/audio/wyoming/piper.nix
./services/audio/ympd.nix
./services/backup/automysqlbackup.nix
./services/backup/bacula.nix
@ -587,8 +584,11 @@
./services/home-automation/evcc.nix
./services/home-automation/govee2mqtt.nix
./services/home-automation/home-assistant.nix
./services/home-automation/homeassistant-satellite.nix
./services/home-automation/matter-server.nix
./services/home-automation/wyoming/faster-whisper.nix
./services/home-automation/wyoming/openwakeword.nix
./services/home-automation/wyoming/piper.nix
./services/home-automation/wyoming/satellite.nix
./services/home-automation/zigbee2mqtt.nix
./services/home-automation/zwave-js.nix
./services/logging/SystemdJournal2Gelf.nix

View File

@ -62,6 +62,7 @@ in
(mkRemovedOptionModule [ "services" "fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed")
(mkRemovedOptionModule [ "services" "fprot" ] "The corresponding package was removed from nixpkgs.")
(mkRemovedOptionModule [ "services" "frab" ] "The frab module has been removed")
(mkRemovedOptionModule [ "services" "homeassistant-satellite"] "The `services.homeassistant-satellite` module has been replaced by `services.wyoming-satellite`.")
(mkRemovedOptionModule [ "services" "ihatemoney" ] "The ihatemoney module has been removed for lack of downstream maintainer")
(mkRemovedOptionModule [ "services" "kippo" ] "The corresponding package was removed from nixpkgs.")
(mkRemovedOptionModule [ "services" "mailpile" ] "The corresponding package was removed from nixpkgs.")

View File

@ -1,225 +0,0 @@
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.services.homeassistant-satellite;
inherit (lib)
escapeShellArg
escapeShellArgs
mkOption
mdDoc
mkEnableOption
mkIf
mkPackageOption
types
;
inherit (builtins)
toString
;
# override the package with the relevant vad dependencies
package = cfg.package.overridePythonAttrs (oldAttrs: {
propagatedBuildInputs = oldAttrs.propagatedBuildInputs
++ lib.optional (cfg.vad == "webrtcvad") cfg.package.optional-dependencies.webrtc
++ lib.optional (cfg.vad == "silero") cfg.package.optional-dependencies.silerovad
++ lib.optional (cfg.pulseaudio.enable) cfg.package.optional-dependencies.pulseaudio;
});
in
{
meta.buildDocsInSandbox = false;
options.services.homeassistant-satellite = with types; {
enable = mkEnableOption (mdDoc "Home Assistant Satellite");
package = mkPackageOption pkgs "homeassistant-satellite" { };
user = mkOption {
type = str;
example = "alice";
description = mdDoc ''
User to run homeassistant-satellite under.
'';
};
group = mkOption {
type = str;
default = "users";
description = mdDoc ''
Group to run homeassistant-satellite under.
'';
};
host = mkOption {
type = str;
example = "home-assistant.local";
description = mdDoc ''
Hostname on which your Home Assistant instance can be reached.
'';
};
port = mkOption {
type = port;
example = 8123;
description = mdDoc ''
Port on which your Home Assistance can be reached.
'';
apply = toString;
};
protocol = mkOption {
type = enum [ "http" "https" ];
default = "http";
example = "https";
description = mdDoc ''
The transport protocol used to connect to Home Assistant.
'';
};
tokenFile = mkOption {
type = path;
example = "/run/keys/hass-token";
description = mdDoc ''
Path to a file containing a long-lived access token for your Home Assistant instance.
'';
apply = escapeShellArg;
};
sounds = {
awake = mkOption {
type = nullOr str;
default = null;
description = mdDoc ''
Audio file to play when the wake word is detected.
'';
};
done = mkOption {
type = nullOr str;
default = null;
description = mdDoc ''
Audio file to play when the voice command is done.
'';
};
};
vad = mkOption {
type = enum [ "disabled" "webrtcvad" "silero" ];
default = "disabled";
example = "silero";
description = mdDoc ''
Voice activity detection model. With `disabled` sound will be transmitted continously.
'';
};
pulseaudio = {
enable = mkEnableOption "recording/playback via PulseAudio or PipeWire";
socket = mkOption {
type = nullOr str;
default = null;
example = "/run/user/1000/pulse/native";
description = mdDoc ''
Path or hostname to connect with the PulseAudio server.
'';
};
duckingVolume = mkOption {
type = nullOr float;
default = null;
example = 0.4;
description = mdDoc ''
Reduce output volume (between 0 and 1) to this percentage value while recording.
'';
};
echoCancellation = mkEnableOption "acoustic echo cancellation";
};
extraArgs = mkOption {
type = listOf str;
default = [ ];
description = mdDoc ''
Extra arguments to pass to the commandline.
'';
apply = escapeShellArgs;
};
};
config = mkIf cfg.enable {
systemd.services."homeassistant-satellite" = {
description = "Home Assistant Satellite";
after = [
"network-online.target"
];
wants = [
"network-online.target"
];
wantedBy = [
"multi-user.target"
];
path = with pkgs; [
ffmpeg-headless
] ++ lib.optionals (!cfg.pulseaudio.enable) [
alsa-utils
];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
# https://github.com/rhasspy/hassio-addons/blob/master/assist_microphone/rootfs/etc/s6-overlay/s6-rc.d/assist_microphone/run
ExecStart = ''
${package}/bin/homeassistant-satellite \
--host ${cfg.host} \
--port ${cfg.port} \
--protocol ${cfg.protocol} \
--token-file ${cfg.tokenFile} \
--vad ${cfg.vad} \
${lib.optionalString cfg.pulseaudio.enable "--pulseaudio"}${lib.optionalString (cfg.pulseaudio.socket != null) "=${cfg.pulseaudio.socket}"} \
${lib.optionalString (cfg.pulseaudio.enable && cfg.pulseaudio.duckingVolume != null) "--ducking-volume=${toString cfg.pulseaudio.duckingVolume}"} \
${lib.optionalString (cfg.pulseaudio.enable && cfg.pulseaudio.echoCancellation) "--echo-cancel"} \
${lib.optionalString (cfg.sounds.awake != null) "--awake-sound=${toString cfg.sounds.awake}"} \
${lib.optionalString (cfg.sounds.done != null) "--done-sound=${toString cfg.sounds.done}"} \
${cfg.extraArgs}
'';
CapabilityBoundingSet = "";
DeviceAllow = "";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = false; # onnxruntime/capi/onnxruntime_pybind11_state.so: cannot enable executable stack as shared object requires: Operation not permitted
PrivateDevices = true;
PrivateUsers = true;
ProtectHome = false; # Would deny access to local pulse/pipewire server
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "all"; # Error in cpuinfo: failed to parse processor information from /proc/cpuinfo
Restart = "always";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SupplementaryGroups = [
"audio"
];
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
};
};
}

View File

@ -0,0 +1,244 @@
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.services.wyoming.satellite;
inherit (lib)
elem
escapeShellArgs
getExe
literalExpression
mkOption
mkEnableOption
mkIf
mkPackageOption
optional
optionals
types
;
finalPackage = cfg.package.overridePythonAttrs (oldAttrs: {
propagatedBuildInputs = oldAttrs.propagatedBuildInputs
# for audio enhancements like auto-gain, noise suppression
++ cfg.package.optional-dependencies.webrtc
# vad is currently optional, because it is broken on aarch64-linux
++ optionals cfg.vad.enable cfg.package.optional-dependencies.silerovad;
});
in
{
meta.buildDocsInSandbox = false;
options.services.wyoming.satellite = with types; {
enable = mkEnableOption "Wyoming Satellite";
package = mkPackageOption pkgs "wyoming-satellite" { };
user = mkOption {
type = str;
example = "alice";
description = ''
User to run wyoming-satellite under.
'';
};
group = mkOption {
type = str;
default = "users";
description = ''
Group to run wyoming-satellite under.
'';
};
uri = mkOption {
type = str;
default = "tcp://0.0.0.0:10700";
description = ''
URI where wyoming-satellite will bind its socket.
'';
};
name = mkOption {
type = str;
default = config.networking.hostName;
defaultText = literalExpression ''
config.networking.hostName
'';
description = ''
Name of the satellite.
'';
};
area = mkOption {
type = nullOr str;
default = null;
example = "Kitchen";
description = ''
Area to the satellite.
'';
};
microphone = {
command = mkOption {
type = str;
default = "arecord -r 16000 -c 1 -f S16_LE -t raw";
description = ''
Program to run for audio input.
'';
};
autoGain = mkOption {
type = ints.between 0 31;
default = 5;
example = 15;
description = ''
Automatic gain control in dbFS, with 31 being the loudest value. Set to 0 to disable.
'';
};
noiseSuppression = mkOption {
type = ints.between 0 4;
default = 2;
example = 3;
description = ''
Noise suppression level with 4 being the maximum suppression,
which may cause audio distortion. Set to 0 to disable.
'';
};
};
sound = {
command = mkOption {
type = nullOr str;
default = "aplay -r 22050 -c 1 -f S16_LE -t raw";
description = ''
Program to run for sound output.
'';
};
};
sounds = {
awake = mkOption {
type = nullOr path;
default = null;
description = ''
Path to audio file in WAV format to play when wake word is detected.
'';
};
done = mkOption {
type = nullOr path;
default = null;
description = ''
Path to audio file in WAV format to play when voice command recording has ended.
'';
};
};
vad = {
enable = mkOption {
type = bool;
default = true;
description = ''
Whether to enable voice activity detection.
Enabling will result in only streaming audio, when speech gets
detected.
'';
};
};
extraArgs = mkOption {
type = listOf str;
default = [ ];
description = ''
Extra arguments to pass to the executable.
Check `wyoming-satellite --help` for possible options.
'';
};
};
config = mkIf cfg.enable {
systemd.services."wyoming-satellite" = {
description = "Wyoming Satellite";
after = [
"network-online.target"
"sound.target"
];
wants = [
"network-online.target"
"sound.target"
];
wantedBy = [
"multi-user.target"
];
path = with pkgs; [
alsa-utils
];
script = let
optionalParam = param: argument: optionals (!elem argument [ null 0 false ]) [
param argument
];
in ''
export XDG_RUNTIME_DIR=/run/user/$UID
${escapeShellArgs ([
(getExe finalPackage)
"--uri" cfg.uri
"--name" cfg.name
"--mic-command" cfg.microphone.command
]
++ optionalParam "--mic-auto-gain" cfg.microphone.autoGain
++ optionalParam "--mic-noise-suppression" cfg.microphone.noiseSuppression
++ optionalParam "--area" cfg.area
++ optionalParam "--snd-command" cfg.sound.command
++ optionalParam "--awake-wav" cfg.sounds.awake
++ optionalParam "--done-wav" cfg.sounds.done
++ optional cfg.vad.enable "--vad"
++ cfg.extraArgs)}
'';
serviceConfig = {
User = cfg.user;
Group = cfg.group;
# https://github.com/rhasspy/hassio-addons/blob/master/assist_microphone/rootfs/etc/s6-overlay/s6-rc.d/assist_microphone/run
CapabilityBoundingSet = "";
DeviceAllow = "";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = false; # onnxruntime/capi/onnxruntime_pybind11_state.so: cannot enable executable stack as shared object requires: Operation not permitted
PrivateDevices = true;
PrivateUsers = true;
ProtectHome = false; # Would deny access to local pulse/pipewire server
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "all"; # Error in cpuinfo: failed to parse processor information from /proc/cpuinfo
Restart = "always";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SupplementaryGroups = [
"audio"
];
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
};
};
}

View File

@ -0,0 +1,60 @@
{ lib
, python3Packages
, fetchFromGitHub
}:
python3Packages.buildPythonApplication rec {
pname = "wyoming-satellite";
version = "1.2.0";
pyproject = true;
src = fetchFromGitHub {
owner = "rhasspy";
repo = "wyoming-satellite";
rev = "refs/tags/v${version}";
hash = "sha256-KIWhWE9Qaxs72fJ1LRTkvk6QtpBJOFlmZv2od69O15g=";
};
nativeBuildInputs = with python3Packages; [
setuptools
pythonRelaxDepsHook
];
pythonRelaxDeps = [
"zeroconf"
];
propagatedBuildInputs = with python3Packages; [
pyring-buffer
wyoming
zeroconf
];
passthru.optional-dependencies = {
silerovad = with python3Packages; [
pysilero-vad
];
webrtc = with python3Packages; [
webrtc-noise-gain
];
};
pythonImportsCheck = [
"wyoming_satellite"
];
nativeCheckInputs = with python3Packages; [
pytest-asyncio
pytestCheckHook
];
meta = with lib; {
description = "Remote voice satellite using Wyoming protocol";
homepage = "https://github.com/rhasspy/wyoming-satellite";
changelog = "https://github.com/rhasspy/wyoming-satellite/blob/${src.rev}/CHANGELOG.md";
license = licenses.mit;
maintainers = with maintainers; [ hexa ];
mainProgram = "wyoming-satellite";
};
}

View File

@ -0,0 +1,36 @@
{ lib
, buildPythonPackage
, fetchFromGitHub
# build-system
, setuptools
}:
buildPythonPackage rec {
pname = "pyring-buffer";
version = "1.0.0";
pyproject = true;
src = fetchFromGitHub {
owner = "rhasspy";
repo = "pyring-buffer";
rev = "382290312fa2ad5d75bd42c040a43e25dad9c8a7";
hash = "sha256-bHhcBU4tjFAyZ3/GjaP/hDXz2N73mCChTNYHsZyBCSM=";
};
nativeBuildInputs = [
setuptools
];
pythonImportsCheck = [
"pyring_buffer"
];
meta = with lib; {
description = "A pure Python ring buffer for bytes";
homepage = "https://github.com/rhasspy/pyring-buffer";
changelog = "https://github.com/rhasspy/pyring-buffer/blob/${src.rev}/CHANGELOG.md";
license = licenses.asl20;
maintainers = with maintainers; [ hexa ];
};
}

View File

@ -0,0 +1,61 @@
{ lib
, buildPythonPackage
, fetchFromGitHub
, stdenv
, pythonRelaxDepsHook
# build-system
, setuptools
# dependencies
, numpy
, onnxruntime
# tests
, pytestCheckHook
}:
buildPythonPackage rec {
pname = "pysilero-vad";
version = "1.0.0";
pyproject = true;
src = fetchFromGitHub {
owner = "rhasspy";
repo = "pysilero-vad";
rev = "fc1e3f74e6282249c1fd67ab0f65832ad1ce9cc5";
hash = "sha256-5jS2xZEtvzXO/ffZzseTTUHfE528W9FvKB0AKG6T62k=";
};
nativeBuildInputs = [
setuptools
pythonRelaxDepsHook
];
pythonRelaxDeps = [
"numpy"
];
propagatedBuildInputs = [
numpy
onnxruntime
];
nativeCheckInputs = [
pytestCheckHook
];
pythonImportsCheck = [
"pysilero_vad"
];
meta = with lib; {
# what(): /build/source/include/onnxruntime/core/common/logging/logging.h:294 static const onnxruntime::logging::Logger& onnxruntime::logging::LoggingManager::DefaultLogger() Attempt to use DefaultLogger but none has been registered.
broken = stdenv.isAarch64 && stdenv.isLinux;
description = "Pre-packaged voice activity detector using silero-vad";
homepage = "https://github.com/rhasspy/pysilero-vad";
changelog = "https://github.com/rhasspy/pysilero-vad/blob/${src.rev}/CHANGELOG.md";
license = licenses.mit;
maintainers = with maintainers; [ hexa ];
};
}

View File

@ -9856,6 +9856,8 @@ self: super: with self; {
pysiaalarm = callPackage ../development/python-modules/pysiaalarm { };
pysilero-vad = callPackage ../development/python-modules/pysilero-vad { };
pysimplesoap = callPackage ../development/python-modules/pysimplesoap { };
pyskyqhub = callPackage ../development/python-modules/pyskyqhub { };
@ -11512,6 +11514,8 @@ self: super: with self; {
pyric = callPackage ../development/python-modules/pyric { };
pyring-buffer = callPackage ../development/python-modules/pyring-buffer { };
pyrisco = callPackage ../development/python-modules/pyrisco { };
pyrituals = callPackage ../development/python-modules/pyrituals { };