diff --git a/devices/homie/modules/home-assistant/assist.nix b/devices/homie/modules/home-assistant/assist.nix index 8ac48480..950ff648 100644 --- a/devices/homie/modules/home-assistant/assist.nix +++ b/devices/homie/modules/home-assistant/assist.nix @@ -16,21 +16,6 @@ "wyoming" ]; - customComponents = builtins.attrValues { - # Switched to HASS Ollama integration - # inherit (self.legacyPackages.${pkgs.system}.hass-components) home-llm; - # Home-llm prompt: - /* - You are 'Homie', a helpful AI Assistant that controls the devices in a house. Complete the following task as instructed. - - The current time and date is {{ (as_timestamp(now()) | timestamp_custom("%I:%M %p on %A %B %d, %Y", "EST")) }}. - - Services: {{ formatted_tools }} - Devices: - {{ formatted_devices }} - */ - }; - config = { assist_pipeline = {}; conversation = {}; @@ -48,12 +33,13 @@ speaker = 0; }; - openwakeword-docker = { + openwakeword = { enable = true; - uri = "127.0.0.1:10400"; + uri = "tcp://127.0.0.1:10400"; + + threshold = 0.8; customModelsDirectories = ["${wakewords-src}/en/yo_homie"]; - preloadModels = ["yo_homie"]; }; }; diff --git a/nixosModules/wyoming-plus/default.nix b/nixosModules/wyoming-plus/default.nix index 08562ef5..47388af8 100644 --- a/nixosModules/wyoming-plus/default.nix +++ b/nixosModules/wyoming-plus/default.nix @@ -7,157 +7,69 @@ inherit (lib) attrNames - escapeShellArgs - flatten + concatMapStringsSep + concatStringsSep filterAttrs - hasAttr listToAttrs map - mkEnableOption mkForce mkIf - mkOption + mkOverride nameValuePair - optionals - splitString - types + optionalString ; - flatMap = f: list: flatten (map f list); - - cfg = config.services.wyoming.openwakeword-docker; + cfg = config.services.wyoming; in { - options.services.wyoming.openwakeword-docker = { - enable = mkEnableOption "Wyoming openWakeWord server"; + config = let + forkedPkg = import ./pkgs/wyoming-openwakeword.nix pkgs; - image = mkOption { - type = types.package; - default = pkgs.dockerTools.pullImage { - imageName = "docker.io/rhasspy/wyoming-openwakeword"; - imageDigest = "sha256:88df83cfdaa5a0dd068f79662d06b81479ec7b59a4bea59751ff5d6f68bad24a"; - sha256 = "1c2yhrhhj1wpd5bcc3zaz1gv8mw8dw5m76cjf42nhf2sgwp2hsjl"; - finalImageName = "docker.io/rhasspy/wyoming-openwakeword"; - finalImageTag = "latest"; - }; - description = '' - The image that docker will use. - ''; - }; - - uri = mkOption { - type = types.str; - default = "0.0.0.0:10400"; - example = "192.0.2.1:5000"; - description = '' - URI to bind the wyoming server to. - ''; - }; - - customModelsDirectories = mkOption { - type = types.listOf types.path; - default = []; - description = '' - Paths to directories with custom wake word models (*.tflite model files). - ''; - }; - - preloadModels = mkOption { - type = with types; listOf str; - default = [ - "ok_nabu" - ]; - example = [ - # wyoming_openwakeword/models/*.tflite - "alexa" - "hey_jarvis" - "hey_mycroft" - "hey_rhasspy" - "ok_nabu" - ]; - description = '' - List of wake word models to preload after startup. - ''; - }; - - threshold = mkOption { - type = types.float; - default = 0.5; - description = '' - Activation threshold (0-1), where higher means fewer activations. - - See trigger level for the relationship between activations and - wake word detections. - ''; - apply = toString; - }; - - triggerLevel = mkOption { - type = types.int; - default = 1; - description = '' - Number of activations before a detection is registered. - - A higher trigger level means fewer detections. - ''; - apply = toString; - }; - - extraArgs = mkOption { - type = with types; listOf str; - default = []; - description = '' - Extra arguments to pass to the server commandline. - ''; - apply = escapeShellArgs; - }; - }; - - config = { - assertions = [ - { - assertion = - (cfg.enable - && hasAttr "khepri" config - && hasAttr "rwDataDir" config.khepri) - || !cfg.enable; - message = '' - The module `docker` from this same flake is needed to use - this openwakeword implementation. - ''; - } - ]; - - systemd.services = let - unitNames = attrNames ( - filterAttrs (_: v: v.device == "cpu") config.services.wyoming.faster-whisper.servers - ); - in - listToAttrs (map (x: + whisperUnitNames = attrNames ( + filterAttrs (_: v: v.device == "cpu") cfg.faster-whisper.servers + ); + in { + systemd.services = + # https://github.com/felschr/nixos-config/blob/6a0f0bf76e3ae80c1e180ba6f6c7fd3b8e91d2d3/services/home-assistant/wyoming.nix#L29 + mkIf (cfg.faster-whisper.servers != {}) + (listToAttrs (map (x: nameValuePair "wyoming-faster-whisper-${x}" { serviceConfig.ProcSubset = mkForce "all"; }) - unitNames); + whisperUnitNames)) + # + # openWakeWord + // mkIf (cfg.openwakeword.enable) { + wyoming-openwakeword.serviceConfig = { + MemoryDenyWriteExecute = mkForce (cfg.openwakeword.package != forkedPkg); - khepri = mkIf cfg.enable { - compositions."openwakeword" = { - networks.default = {}; + # changes according to https://github.com/rhasspy/wyoming-openwakeword/pull/27 + ExecStart = mkForce (concatStringsSep " " [ + "${cfg.openwakeword.package}/bin/wyoming-openwakeword" - services."openwakeword" = { - image = cfg.image; - restart = "always"; - networks = ["default"]; + "--uri ${cfg.openwakeword.uri}" + "--threshold ${cfg.openwakeword.threshold}" - volumes = map (dir: "${toString dir}:${toString dir}") cfg.customModelsDirectories; - cmd = - (flatMap (model: ["--preload-model" model]) cfg.preloadModels) - ++ (flatMap (dir: ["--custom-model-dir" (toString dir)]) cfg.customModelsDirectories) - ++ ["--threshold" cfg.threshold] - ++ ["--trigger-level" cfg.triggerLevel] - ++ optionals (cfg.extraArgs != "") (splitString " " cfg.extraArgs); + (concatMapStringsSep " " + (dir: "--custom-model-dir ${toString dir}") + cfg.openwakeword.customModelsDirectories) - ports = ["${cfg.uri}:10400"]; + # removed option https://github.com/rhasspy/wyoming-openwakeword/pull/27#issuecomment-2211822998 + (optionalString + (cfg.openwakeword.package != forkedPkg) + (concatMapStringsSep " " (model: "--preload-model ${model}") cfg.openwakeword.preloadModels)) + + # removed option since preloading was removed + (optionalString + (cfg.openwakeword.package != forkedPkg) + "--trigger-level ${cfg.openwakeword.triggerLevel}") + + "${cfg.openwakeword.extraArgs}" + ]); }; }; + + services.wyoming.openwakeword = mkIf (cfg.openwakeword.enable) { + package = mkOverride 900 forkedPkg; }; }; } diff --git a/nixosModules/wyoming-plus/pkgs/openwakeword.nix b/nixosModules/wyoming-plus/pkgs/openwakeword.nix new file mode 100644 index 00000000..0a78be48 --- /dev/null +++ b/nixosModules/wyoming-plus/pkgs/openwakeword.nix @@ -0,0 +1,43 @@ +{ + fetchFromGitHub, + fetchpatch, + python3Packages, + speexdsp-ns, + ... +}: +python3Packages.buildPythonApplication rec { + pname = "openwakeword"; + version = "0.6.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "dscripka"; + repo = "openWakeWord"; + rev = "v${version}"; + hash = "sha256-QsXV9REAHdP0Y0fVZuU+Gt9+gcPMB60bc3DOMDYuaDM="; + }; + + # Patch upstream to enable use of full tensorflow dep + pythonRemoveDeps = ["tflite-runtime"]; + patches = [ + (fetchpatch { + url = "https://github.com/dscripka/openWakeWord/pull/178/commits/99cd87e8898348255e864540e43bab17ce0576d6.patch"; + hash = "sha256-xveMBZTcYfT8LKiiStqYjjOdUOM/v4taQzSewo97Bfc="; + }) + ]; + + nativeBuildInputs = with python3Packages; [ + setuptools + ]; + + propagatedBuildInputs = + (with python3Packages; [ + onnxruntime + tensorflow-bin + tqdm + scipy + scikit-learn + requests + ]) + ++ [speexdsp-ns]; +} diff --git a/nixosModules/wyoming-plus/pkgs/speexdsp-ns.nix b/nixosModules/wyoming-plus/pkgs/speexdsp-ns.nix new file mode 100644 index 00000000..93e687d4 --- /dev/null +++ b/nixosModules/wyoming-plus/pkgs/speexdsp-ns.nix @@ -0,0 +1,21 @@ +{ + fetchFromGitHub, + python3Packages, + speexdsp, + swig, + ... +}: +python3Packages.buildPythonApplication { + pname = "speexdsp-ns"; + version = "0.1.2"; + + src = fetchFromGitHub { + owner = "TeaPoly"; + repo = "speexdsp-ns-python"; + rev = "8af784a230e23f4eeaa4a58111774ad0864b1f0b"; + hash = "sha256-9IGhHZBlDYfGygB+fAdEDp7qeIEOWBsiLZAUFTVBxG0="; + }; + + nativeBuildInputs = [swig]; + propagatedBuildInputs = [speexdsp]; +} diff --git a/nixosModules/wyoming-plus/pkgs/wyoming-openwakeword.nix b/nixosModules/wyoming-plus/pkgs/wyoming-openwakeword.nix new file mode 100644 index 00000000..0afb3f73 --- /dev/null +++ b/nixosModules/wyoming-plus/pkgs/wyoming-openwakeword.nix @@ -0,0 +1,32 @@ +/* +This package uses a `wyoming-openwakeword` fork that makes use of +the upstream `openwakeword` instead of a fork: https://github.com/rhasspy/wyoming-openwakeword/pull/27 + +It also enforces the python version to 3.11, because tensorflow +cannot be used with 3.12 yet. +*/ +pkgs: let + pyPkgs = pkgs.python311Packages; + + speexdsp-ns = pkgs.callPackage ./speexdsp-ns.nix { + python3Packages = pyPkgs; + }; + + openwakeword = pkgs.callPackage ./openwakeword.nix { + inherit speexdsp-ns; + python3Packages = pyPkgs; + }; +in + (pkgs.wyoming-openwakeword.override { + python3Packages = pyPkgs; + }) + .overrideAttrs (o: { + src = pkgs.fetchFromGitHub { + owner = "rhasspy"; + repo = "wyoming-openwakeword"; + rev = "synesthesiam-20240627-openwakeword"; + hash = "sha256-69oR2LHiUfx8j39nWp7XhG5xTvmOoPCLjSlH1CFvavo="; + }; + + propagatedBuildInputs = [openwakeword pyPkgs.wyoming]; + })