From 7d88c8c865ac3973646ad88aafb74180fb73eb2e Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Tue, 24 Feb 2026 12:15:33 +0100 Subject: [PATCH] Add NixOS module, overlay, and systemd service for wyoming-whisper-rs Add module.nix exposing config.services.wyoming.whisper-cpp with multi-instance support, systemd hardening, and GPU device access. Export as nixosModules.default and overlays.default from the flake. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 6 + wyoming-whisper-rs/module.nix | 343 ++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 wyoming-whisper-rs/module.nix diff --git a/flake.nix b/flake.nix index 9c99811..761d778 100644 --- a/flake.nix +++ b/flake.nix @@ -9,6 +9,12 @@ forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); in { + nixosModules.default = ./wyoming-whisper-rs/module.nix; + + overlays.default = final: prev: { + wyoming-whisper-rs = final.callPackage ./wyoming-whisper-rs/package.nix { src = self; }; + }; + packages = forAllSystems (pkgs: { default = pkgs.callPackage ./wyoming-whisper-rs/package.nix { src = self; }; }); diff --git a/wyoming-whisper-rs/module.nix b/wyoming-whisper-rs/module.nix new file mode 100644 index 0000000..ebc71cf --- /dev/null +++ b/wyoming-whisper-rs/module.nix @@ -0,0 +1,343 @@ +{ + config, + lib, + pkgs, + utils, + ... +}: + +let + cfg = config.services.wyoming.whisper-cpp; + + inherit (lib) + mapAttrsToList + mkOption + mkEnableOption + optionals + types + ; + + inherit (builtins) + toString + ; + + inherit (utils) + escapeSystemdExecArgs + ; +in + +{ + options.services.wyoming.whisper-cpp = with types; { + package = mkOption { + type = package; + description = '' + The wyoming-whisper-rs package to use. + ''; + }; + + servers = mkOption { + default = { }; + description = '' + Attribute set of wyoming-whisper-rs instances to spawn. + ''; + type = attrsOf (submodule { + options = { + enable = mkEnableOption "Wyoming whisper-cpp server"; + + model = mkOption { + type = path; + description = '' + Path to the GGML whisper model file. + ''; + example = "/var/lib/wyoming/whisper-cpp/ggml-large-v3-turbo.bin"; + }; + + uri = mkOption { + type = strMatching "^(tcp|unix)://.*$"; + example = "tcp://0.0.0.0:10300"; + description = '' + URI to bind the wyoming server to. + ''; + }; + + language = mkOption { + type = nullOr (enum [ + "auto" + "af" + "am" + "ar" + "as" + "az" + "ba" + "be" + "bg" + "bn" + "bo" + "br" + "bs" + "ca" + "cs" + "cy" + "da" + "de" + "el" + "en" + "es" + "et" + "eu" + "fa" + "fi" + "fo" + "fr" + "gl" + "gu" + "ha" + "haw" + "he" + "hi" + "hr" + "ht" + "hu" + "hy" + "id" + "is" + "it" + "ja" + "jw" + "ka" + "kk" + "km" + "kn" + "ko" + "la" + "lb" + "ln" + "lo" + "lt" + "lv" + "mg" + "mi" + "mk" + "ml" + "mn" + "mr" + "ms" + "mt" + "my" + "ne" + "nl" + "nn" + "no" + "oc" + "pa" + "pl" + "ps" + "pt" + "ro" + "ru" + "sa" + "sd" + "si" + "sk" + "sl" + "sn" + "so" + "sq" + "sr" + "su" + "sv" + "sw" + "ta" + "te" + "tg" + "th" + "tk" + "tl" + "tr" + "tt" + "uk" + "ur" + "uz" + "vi" + "yi" + "yue" + "yo" + "zh" + ]); + default = null; + example = "en"; + description = '' + The language used for speech recognition. Use `null` or `"auto"` for auto-detection. + ''; + }; + + beamSize = mkOption { + type = ints.positive; + default = 5; + description = '' + The number of beams to use in beam search. + ''; + }; + + threads = mkOption { + type = ints.unsigned; + default = 0; + description = '' + Number of threads for whisper inference. Use `0` for the whisper default. + ''; + }; + + gpuDevice = mkOption { + type = ints.unsigned; + default = 0; + description = '' + GPU device index to use. + ''; + }; + + noGpu = mkOption { + type = bool; + default = false; + description = '' + Disable GPU acceleration entirely. + ''; + }; + + maxConcurrent = mkOption { + type = ints.positive; + default = 1; + description = '' + Maximum number of concurrent transcriptions. + ''; + }; + + modelName = mkOption { + type = str; + default = "whisper"; + description = '' + Model name reported in Wyoming info responses. + ''; + }; + + extraArgs = mkOption { + type = listOf str; + default = [ ]; + description = '' + Extra arguments to pass to the server commandline. + ''; + }; + }; + }); + }; + }; + + config = + let + inherit (lib) + mapAttrs' + mkIf + nameValuePair + ; + + parseUri = uri: + let + parts = builtins.match "tcp://([^:]+):(.*)" uri; + in + if parts != null then { + host = builtins.elemAt parts 0; + port = builtins.elemAt parts 1; + } else null; + in + mkIf (cfg.servers != { }) { + systemd.services = mapAttrs' ( + server: options: + let + parsed = parseUri options.uri; + in + nameValuePair "wyoming-whisper-cpp-${server}" { + inherit (options) enable; + description = "Wyoming whisper-cpp server instance ${server}"; + wants = [ + "network-online.target" + ]; + after = [ + "network-online.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + serviceConfig = { + DynamicUser = true; + User = "wyoming-whisper-cpp"; + ExecStart = escapeSystemdExecArgs ( + [ + (lib.getExe cfg.package) + "--model" + (toString options.model) + "--host" + (if parsed != null then parsed.host else "0.0.0.0") + "--port" + (if parsed != null then parsed.port else "10300") + "--beam-size" + (toString options.beamSize) + "--threads" + (toString options.threads) + "--gpu-device" + (toString options.gpuDevice) + "--max-concurrent" + (toString options.maxConcurrent) + "--model-name" + options.modelName + ] + ++ optionals (options.language != null && options.language != "auto") [ + "--language" + options.language + ] + ++ optionals options.noGpu [ + "--no-gpu" + ] + ++ options.extraArgs + ); + CapabilityBoundingSet = ""; + DeviceAllow = + if !options.noGpu then + [ + "char-nvidia-uvm" + "char-nvidia-frontend" + "char-nvidia-caps" + "char-nvidiactl" + # AMD ROCm + "char-drm" + "/dev/kfd" + ] + else + ""; + DevicePolicy = "closed"; + LockPersonality = true; + PrivateUsers = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + ProcSubset = "all"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + } + ) cfg.servers; + }; +}