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 <noreply@anthropic.com>
This commit is contained in:
Harald Hoyer 2026-02-24 12:15:33 +01:00
parent f346829cc0
commit 7d88c8c865
2 changed files with 349 additions and 0 deletions

View file

@ -9,6 +9,12 @@
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
in 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: { packages = forAllSystems (pkgs: {
default = pkgs.callPackage ./wyoming-whisper-rs/package.nix { src = self; }; default = pkgs.callPackage ./wyoming-whisper-rs/package.nix { src = self; };
}); });

View file

@ -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;
};
}