diff --git a/modules/nixos/wyoming.nix b/modules/nixos/wyoming.nix new file mode 100644 index 00000000..de52274f --- /dev/null +++ b/modules/nixos/wyoming.nix @@ -0,0 +1,72 @@ +let + serverModule = { + config, + lib, + ... + }: let + inherit (lib.options) mkOption; + inherit (lib.modules) mkIf mkOptionDefault; + inherit (lib.strings) match toInt; + inherit (lib.lists) elemAt; + in { + options = with lib.types; { + bind = mkOption { + type = str; + readOnly = true; + }; + port = mkOption { + type = port; + readOnly = true; + }; + }; + config = let + matched = match "^tcp://(.*):([0-9]+)$" config.uri; + bind = elemAt matched 0; + port = toInt (elemAt matched 1); + in { + bind = mkIf (matched != null) (mkOptionDefault bind); + port = mkIf (matched != null) (mkOptionDefault port); + }; + }; + nonServerModule = service: { + config, + lib, + ... + } @ args: let + cfg = config.services.wyoming.${service}; + module = serverModule (args + // { + name = service; + config = cfg; + }); + in { + options.services.wyoming.${service} = module.options; + config.services.wyoming.${service} = module.config; + }; +in + { + config, + lib, + ... + }: let + inherit (lib.options) mkOption; + inherit (lib.attrsets) genAttrs; + serviceNames = ["piper" "faster-whisper"]; + nonServerNames = ["openwakeword" "satellite"]; + nonServerServices = map nonServerModule nonServerNames; + in { + imports = nonServerServices; + + options.services.wyoming = let + mkServiceOptions = service: + with lib.types; { + servers = mkOption { + type = attrsOf (submodule [serverModule]); + }; + }; + serverServices = genAttrs serviceNames mkServiceOptions; + in + serverServices + // { + }; + } diff --git a/modules/system/exports/wyoming.nix b/modules/system/exports/wyoming.nix new file mode 100644 index 00000000..cf91106f --- /dev/null +++ b/modules/system/exports/wyoming.nix @@ -0,0 +1,60 @@ +{ + lib, + gensokyo-zone, + ... +}: let + inherit (gensokyo-zone.lib) mkAlmostOptionDefault; + inherit (lib.modules) mkIf; + wyomingService = {config, ...}: { + nixos = { + serviceAttrPath = ["services" "wyoming" config.name]; + assertions = [ + (nixosConfig: let + service = nixosConfig.services.wyoming.${config.name}; + cfg = service.servers.${config.id} or service; + in { + assertion = (! cfg ? enable) || (config.enable == cfg.enable); + message = "enable mismatch"; + }) + (mkIf config.enable (nixosConfig: let + service = nixosConfig.services.wyoming.${config.name}; + cfg = service.servers.${config.id} or service; + in { + assertion = ! cfg.enable or false || config.ports.default.port == cfg.port or null; + message = "port mismatch"; + })) + ]; + }; + defaults.port.listen = mkAlmostOptionDefault "lan"; + ports = { + default = { + transport = "tcp"; + }; + }; + }; +in { + config.exports.services = { + faster-whisper = {config, ...}: { + imports = [wyomingService]; + displayName = mkAlmostOptionDefault "Wyoming Whisper"; + id = mkAlmostOptionDefault "whisper"; + ports.default.port = mkAlmostOptionDefault 10300; + }; + piper = {config, ...}: { + imports = [wyomingService]; + displayName = mkAlmostOptionDefault "Wyoming Piper"; + id = mkAlmostOptionDefault "piper"; + ports.default.port = mkAlmostOptionDefault 10200; + }; + openwakeword = {config, ...}: { + imports = [wyomingService]; + displayName = mkAlmostOptionDefault "Wyoming openWakeWord"; + ports.default.port = mkAlmostOptionDefault 10400; + }; + satellite = {config, ...}: { + imports = [wyomingService]; + displayName = mkAlmostOptionDefault "Wyoming Satellite"; + ports.default.port = mkAlmostOptionDefault 10700; + }; + }; +} diff --git a/nixos/wyoming/openwakeword.nix b/nixos/wyoming/openwakeword.nix new file mode 100644 index 00000000..1acf1dd8 --- /dev/null +++ b/nixos/wyoming/openwakeword.nix @@ -0,0 +1,22 @@ +{ + config, + lib, + ... +}: let + inherit (lib.modules) mkIf mkDefault; + cfg = config.services.wyoming.openwakeword; +in { + imports = [./wyoming.nix]; + services.wyoming.openwakeword = { + enable = mkDefault true; + uri = mkDefault "tcp://0.0.0.0:10400"; + # models: https://github.com/dscripka/openWakeWord?tab=readme-ov-file#pre-trained-models + preloadModels = mkDefault [ + "ok_nabu" + "hey_rhasspy" + ]; + }; + + # allow access to LAN satellites + networking.firewall.interfaces.local.allowedTCPPorts = mkIf cfg.enable [cfg.port]; +} diff --git a/nixos/wyoming/piper.nix b/nixos/wyoming/piper.nix new file mode 100644 index 00000000..45db76ae --- /dev/null +++ b/nixos/wyoming/piper.nix @@ -0,0 +1,18 @@ +{ + config, + lib, + ... +}: let + inherit (lib.modules) mkDefault; +in { + imports = [./wyoming.nix]; + services.wyoming.piper = { + # voices: https://rhasspy.github.io/piper-samples/ + servers.piper = { + enable = mkDefault true; + uri = mkDefault "tcp://0.0.0.0:10200"; + voice = mkDefault "en_GB-semaine-medium"; + speaker = mkDefault 0; + }; + }; +} diff --git a/nixos/wyoming/whisper.nix b/nixos/wyoming/whisper.nix new file mode 100644 index 00000000..826e452f --- /dev/null +++ b/nixos/wyoming/whisper.nix @@ -0,0 +1,40 @@ +{ + config, + lib, + ... +}: let + inherit (lib.modules) mkIf mkForce mkDefault; + cfg = config.services.wyoming.faster-whisper; + inherit (cfg.servers) whisper; + useRocm = false; # broken... +in { + imports = [./wyoming.nix]; + services.wyoming.faster-whisper = { + # models: https://github.com/rhasspy/wyoming-faster-whisper/releases/tag/v2.0.0 + servers.whisper = { + enable = mkDefault true; + language = mkDefault "en"; + model = let + #distil = "distil"; + #distil = "distil-whisper/distil-whisper"; + distil = "Systran/faster-distil-whisper"; + #size = "small.en"; + size = "medium.en"; + #size = "large-v3"; + in + mkDefault "${distil}-${size}"; + uri = mkDefault "tcp://0.0.0.0:10300"; + device = mkIf useRocm "cuda"; + }; + }; + systemd.services.wyoming-faster-whisper-whisper = mkIf whisper.enable { + serviceConfig = mkIf (whisper.device != "cpu" && useRocm) { + DeviceAllow = [ + "char-drm" + "char-kfd" + ]; + SupplementaryGroups = ["render"]; + PrivateDevices = mkForce false; + }; + }; +} diff --git a/nixos/wyoming/wyoming.nix b/nixos/wyoming/wyoming.nix new file mode 100644 index 00000000..8e3bb75c --- /dev/null +++ b/nixos/wyoming/wyoming.nix @@ -0,0 +1,22 @@ +{ + config, + lib, + ... +}: let + inherit (lib.modules) mkIf mkMerge; + inherit (lib.attrsets) mapAttrsToList; + cfgs = config.services.wyoming; +in { + config = { + networking.firewall.interfaces.lan.allowedTCPPorts = let + mkServerPort = _: server: mkIf (server.enable && server ? port) server.port; + mkServicePorts = name: cfg: + mapAttrsToList mkServerPort + cfg.servers + or { + ${name} = cfg; + }; + in + mkMerge (mapAttrsToList mkServicePorts cfgs); + }; +} diff --git a/overlays/llm.nix b/overlays/llm.nix index a392bd6c..1797f637 100644 --- a/overlays/llm.nix +++ b/overlays/llm.nix @@ -5,7 +5,7 @@ in { postPatch = '' substituteInPlace api/types.go \ - --replace 'UseMMap: nil,' 'UseMMap: &[]bool{true}[0],' + --replace-fail 'UseMMap: nil,' 'UseMMap: &[]bool{true}[0],' '' + old.postPatch or ""; doCheck = false; @@ -39,4 +39,16 @@ in { inherit src patches; }; }); + + wyoming-openwakeword = let + inherit (prev) wyoming-openwakeword; + drv = prev.wyoming-openwakeword.override { + python3Packages = final.python311Packages; + }; + isPython312 = lib.versionAtLeast final.python3Packages.python.version "3.12"; + isBroken = wyoming-openwakeword.version == "1.10.0" && isPython312; + in + if isBroken + then drv + else lib.warnIf isPython312 "wyoming-openwakeword override outdated" wyoming-openwakeword; } diff --git a/systems/mediabox/default.nix b/systems/mediabox/default.nix index cf11ae90..ca684085 100644 --- a/systems/mediabox/default.nix +++ b/systems/mediabox/default.nix @@ -20,6 +20,9 @@ _: { enable = true; ports.proxied.enable = true; }; + piper.enable = true; + faster-whisper.enable = true; + openwakeword.enable = true; cloudflared.enable = true; plex.enable = true; invidious.enable = true; diff --git a/systems/mediabox/nixos.nix b/systems/mediabox/nixos.nix index e79e4e29..ef96e808 100644 --- a/systems/mediabox/nixos.nix +++ b/systems/mediabox/nixos.nix @@ -1,5 +1,6 @@ { config, + gensokyo-zone, meta, lib, pkgs, @@ -16,6 +17,20 @@ "/mnt/Movies".hostPath = kyuuto.libraryDir + "/movies"; "/mnt/Music".hostPath = kyuuto.libraryDir + "/music/assorted"; }; + useZLUDA = false; + useRocm = useZLUDA; + rocmPackages = + if useZLUDA + then pkgs.rocmPackages_5 + else pkgs.rocmPackages; + zluda = let + rustChannel = gensokyo-zone.inputs.systemd2mqtt.inputs.rust.legacyPackages.x86_64-linux.releases."1.79.0"; + in + pkgs.zluda.override { + inherit rocmPackages; + inherit (rustChannel) rustPlatform; + inherit (rustChannel.buildChannel) rustc; + }; in { imports = let inherit (meta) nixos; @@ -38,6 +53,11 @@ in { nixos.mediatomb nixos.invidious + # accelerated + nixos.wyoming.whisper + nixos.wyoming.piper + nixos.wyoming.openwakeword + # yarr harr fiddle dee dee >w< nixos.radarr nixos.sonarr @@ -75,10 +95,22 @@ in { in [libraryDir]; }; + nixpkgs.config = mkIf useZLUDA { + cudaSupport = true; + }; hardware.graphics = { enable = true; - extraPackages = with pkgs; [mesa.drivers]; + extraPackages = with pkgs; + mkMerge [ + [mesa.drivers] + (mkIf useZLUDA [zluda]) + (mkIf useRocm [rocmPackages.clr.icd rocmPackages.clr]) + ]; }; + environment.systemPackages = with pkgs; [ + radeontop + (mkIf useRocm rocmPackages.rocminfo) + ]; fileSystems = let bind = {