diff options
Diffstat (limited to 'nixos/modules/services/home-automation/wyoming/satellite.nix')
-rw-r--r-- | nixos/modules/services/home-automation/wyoming/satellite.nix | 244 |
1 files changed, 244 insertions, 0 deletions
diff --git a/nixos/modules/services/home-automation/wyoming/satellite.nix b/nixos/modules/services/home-automation/wyoming/satellite.nix new file mode 100644 index 0000000000000..531d375e703a3 --- /dev/null +++ b/nixos/modules/services/home-automation/wyoming/satellite.nix @@ -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"; + }; + }; + }; +} |