about summary refs log tree commit diff
path: root/nixos/modules/services/home-automation/wyoming/satellite.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/home-automation/wyoming/satellite.nix')
-rw-r--r--nixos/modules/services/home-automation/wyoming/satellite.nix244
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";
+      };
+    };
+  };
+}