From e23b042b9ea85446f96b5eab50e65d15701fcf9d Mon Sep 17 00:00:00 2001 From: Ethan Turkeltaub Date: Fri, 25 Sep 2026 12:16:15 -0400 Subject: [PATCH 1/2] Add `subgen`, `stable-ts-whisperless`, and `faster-whisper-medium` packages, add `subgen` NixOS module --- .github/workflows/packages.yml | 3 + hosts/htpc/configuration.nix | 5 +- modules/nixos/services/subgen/default.nix | 119 +++++++++++++++++ modules/overlays/default.nix | 3 + modules/packages/default.nix | 7 +- .../faster-whisper-medium/default.nix | 47 +++++++ .../stable-ts-whisperless/default.nix | 47 +++++++ modules/packages/subgen/default.nix | 123 ++++++++++++++++++ .../media-management/bazarr/default.nix | 8 +- modules/profiles/media-management/subgen.nix | 9 ++ 10 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 modules/nixos/services/subgen/default.nix create mode 100644 modules/packages/faster-whisper-medium/default.nix create mode 100644 modules/packages/stable-ts-whisperless/default.nix create mode 100644 modules/packages/subgen/default.nix create mode 100644 modules/profiles/media-management/subgen.nix diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index fc1d628..97f4336 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -47,11 +47,14 @@ jobs: os: ubuntu-24.04-arm package: - decluttarr + - faster-whisper-medium - fileflows - incus-apply - mazanoke - profilarr - profilarr-parser + - stable-ts-whisperless + - subgen - tf - tfl - tracearr diff --git a/hosts/htpc/configuration.nix b/hosts/htpc/configuration.nix index 73c1aab..d4df96f 100644 --- a/hosts/htpc/configuration.nix +++ b/hosts/htpc/configuration.nix @@ -24,9 +24,10 @@ profiles.media-management.sabnzbd.default profiles.media-management.seerr profiles.media-management.sonarr.default - profiles.media-management.tunarr + profiles.media-management.subgen profiles.media-management.tautulli profiles.media-management.tracearr.default + profiles.media-management.tunarr profiles.media-management.wizarr profiles.services.attic-watch-store.default profiles.sharing.nfs-client @@ -37,7 +38,7 @@ profiles.telemetry.prometheus-radarr-exporter profiles.telemetry.prometheus-sabnzbd-exporter profiles.telemetry.prometheus-sonarr-exporter - profiles.virtualisation.docker + profiles.virtualisation.podman ] ++ [ ./hardware-configuration.nix diff --git a/modules/nixos/services/subgen/default.nix b/modules/nixos/services/subgen/default.nix new file mode 100644 index 0000000..d9507a4 --- /dev/null +++ b/modules/nixos/services/subgen/default.nix @@ -0,0 +1,119 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; + +let + cfg = config.services.subgen; +in +{ + options.services.subgen = { + enable = mkEnableOption "Subgen"; + + package = mkOption { + type = types.package; + default = pkgs.subgen; + }; + + modelPackage = mkOption { + type = types.package; + }; + + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + }; + + port = mkOption { + type = types.port; + default = 9000; + }; + + computeType = mkOption { + type = types.str; + default = "int8"; + }; + + threads = mkOption { + type = types.ints.positive; + default = 4; + }; + + concurrentTranscriptions = mkOption { + type = types.ints.positive; + default = 1; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + }; + }; + + config = mkIf cfg.enable { + systemd.services.subgen = { + description = "Subgen subtitle generation"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + environment = { + WEBHOOK_HOST = cfg.listenAddress; + WEBHOOK_PORT = toString cfg.port; + WHISPER_MODEL = toString cfg.modelPackage; + MODEL_PATH = "/var/lib/subgen/models"; + TRANSCRIBE_DEVICE = "cuda"; + COMPUTE_TYPE = cfg.computeType; + WHISPER_THREADS = toString cfg.threads; + CONCURRENT_TRANSCRIPTIONS = toString cfg.concurrentTranscriptions; + PROCESS_ADDED_MEDIA = "False"; + PROCESS_MEDIA_ON_PLAY = "False"; + MONITOR = "False"; + RELOAD_SCRIPT_ON_CHANGE = "False"; + UPDATE = "False"; + DEBUG = "True"; + }; + + serviceConfig = { + Type = "simple"; + DynamicUser = true; + ExecStart = lib.getExe cfg.package; + Restart = "on-failure"; + RestartSec = "10s"; + StateDirectory = "subgen"; + StateDirectoryMode = "0700"; + WorkingDirectory = "/var/lib/subgen"; + UMask = "0077"; + + NoNewPrivileges = true; + # PrivateDevices would hide /dev/nvidia*, which CUDA needs. + PrivateDevices = false; + DevicePolicy = "closed"; + DeviceAllow = [ + "char-nvidiactl" + "char-nvidia-caps" + "char-nvidia-frontend" + "char-nvidia-uvm" + ]; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + CapabilityBoundingSet = ""; + RestrictSUIDSGID = true; + RestrictRealtime = true; + LockPersonality = true; + }; + }; + + networking.firewall.allowedTCPPorts = optional cfg.openFirewall cfg.port; + }; +} diff --git a/modules/overlays/default.nix b/modules/overlays/default.nix index e183dd7..3fd6cac 100644 --- a/modules/overlays/default.nix +++ b/modules/overlays/default.nix @@ -48,12 +48,15 @@ inherit (self'.packages) decluttarr + faster-whisper-medium fileflows incus-apply mazanoke profilarr profilarr-parser prometheus-plex-exporter + stable-ts-whisperless + subgen tracearr tunarr unifi-os-server-image diff --git a/modules/packages/default.nix b/modules/packages/default.nix index b11cc6e..3a0e05b 100644 --- a/modules/packages/default.nix +++ b/modules/packages/default.nix @@ -1,12 +1,17 @@ { perSystem = { pkgs, ... }: { - packages = { + packages = rec { decluttarr = pkgs.callPackage ./decluttarr { }; + faster-whisper-medium = pkgs.callPackage ./faster-whisper-medium { }; fileflows = pkgs.callPackage ./fileflows { }; incus-apply = pkgs.callPackage ./incus-apply { }; mazanoke = pkgs.callPackage ./mazanoke { }; profilarr = pkgs.callPackage ./profilarr { }; profilarr-parser = pkgs.callPackage ./profilarr-parser { }; + stable-ts-whisperless = pkgs.python3Packages.callPackage ./stable-ts-whisperless { }; + subgen = pkgs.callPackage ./subgen { + inherit stable-ts-whisperless; + }; tracearr = pkgs.callPackage ./tracearr { }; tunarr = pkgs.callPackage ./tunarr { }; unifi-os-server-image = pkgs.callPackage ./unifi-os-server-image { }; diff --git a/modules/packages/faster-whisper-medium/default.nix b/modules/packages/faster-whisper-medium/default.nix new file mode 100644 index 0000000..b26a0f4 --- /dev/null +++ b/modules/packages/faster-whisper-medium/default.nix @@ -0,0 +1,47 @@ +{ fetchurl +, lib +, stdenvNoCC +, +}: + +let + revision = "08e178d48790749d25932bbc082711ddcfdfbc4f"; + configHash = "sha256-NiKi3cQewOD9TmjBPGgw8DuQw42Jqq0YTeAsjGQs+Ac="; + fetchModelFile = + file: hash: + fetchurl { + url = "https://huggingface.co/Systran/faster-whisper-medium/resolve/${revision}/${file}"; + inherit hash; + }; + modelFiles = { + "config.json" = fetchModelFile "config.json" configHash; + "model.bin" = fetchModelFile "model.bin" "sha256-m0XhAJ3MSrYB7/gVth2A5gzj/Yx0waFPSigiWChrUa4="; + "tokenizer.json" = + fetchModelFile "tokenizer.json" "sha256-+3tjGR6bsEUILHn9dCoxBqEsmVE6sw30oNR/pstv0Ks="; + "vocabulary.txt" = + fetchModelFile "vocabulary.txt" "sha256-NM4/4cUEECez+NQpEicJk/mG28S7NM8n+VHjSh5FORM="; + }; +in +stdenvNoCC.mkDerivation { + pname = "faster-whisper-medium"; + version = revision; + dontUnpack = true; + + installPhase = '' + runHook preInstall + + mkdir -p "$out" + ${lib.concatMapStringsSep "\n" (file: ''cp ${modelFiles.${file}} "$out/${file}"'') ( + builtins.attrNames modelFiles + )} + + runHook postInstall + ''; + + meta = { + description = "WhisperAI transcription model (medium)"; + homepage = "https://huggingface.co/Systran/faster-whisper-medium"; + license = lib.licenses.mit; + platforms = lib.platforms.linux; + }; +} diff --git a/modules/packages/stable-ts-whisperless/default.nix b/modules/packages/stable-ts-whisperless/default.nix new file mode 100644 index 0000000..ab785b2 --- /dev/null +++ b/modules/packages/stable-ts-whisperless/default.nix @@ -0,0 +1,47 @@ +{ lib +, buildPythonPackage +, fetchPypi +, faster-whisper +, numpy +, setuptools +, torch +, torchaudio +, tqdm +, +}: + +buildPythonPackage rec { + pname = "stable-ts-whisperless"; + version = "2.19.1"; + pyproject = true; + + src = fetchPypi { + pname = "stable_ts_whisperless"; + inherit version; + hash = "sha256-WpMT3OGaq3n8CPnbic7g6TyhQJHabjPXndUHWGmQ9gU="; + }; + + postPatch = '' + substituteInPlace stable_whisper/result.py \ + --replace-fail 'any=.,\,,?' 'any=.,\\,,?' + ''; + + build-system = [ setuptools ]; + + dependencies = [ + faster-whisper + numpy + torch + torchaudio + tqdm + ]; + + pythonImportsCheck = [ "stable_whisper" ]; + + meta = { + description = "Stable-ts package without the OpenAI Whisper dependency"; + homepage = "https://github.com/jianfch/stable-ts"; + license = lib.licenses.mit; + platforms = lib.platforms.linux; + }; +} diff --git a/modules/packages/subgen/default.nix b/modules/packages/subgen/default.nix new file mode 100644 index 0000000..0cd31bb --- /dev/null +++ b/modules/packages/subgen/default.nix @@ -0,0 +1,123 @@ +{ lib +, ctranslate2 +, fetchFromGitHub +, ffmpeg +, makeWrapper +, python3Packages +, stable-ts-whisperless +, stdenv +, cudaSupport ? stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isx86_64 + # Choose only capabilities matching GPU on target host (htpc) +, cudaCapabilities ? [ "6.1" ] +, cudnnSupport ? false +, +}: + +let + ctranslate2Cuda = (ctranslate2.override { + withCUDA = true; + withCuDNN = cudnnSupport; + }).overrideAttrs (prev: { + cmakeFlags = prev.cmakeFlags ++ [ + (lib.cmakeFeature "CUDA_ARCH_LIST" (lib.concatStringsSep ";" cudaCapabilities)) + ]; + + preBuild = '' + if [ "$NIX_BUILD_CORES" -gt 8 ]; then + export NIX_BUILD_CORES=8 + fi + ''; + }); + + ctranslate2Python = + if cudaSupport then + python3Packages.ctranslate2.override { ctranslate2-cpp = ctranslate2Cuda; } + else + python3Packages.ctranslate2; + fasterWhisper = python3Packages.faster-whisper.override { ctranslate2 = ctranslate2Python; }; + stableTsWhisperless = stable-ts-whisperless.override { faster-whisper = fasterWhisper; }; + + pythonDependencies = [ + fasterWhisper + stableTsWhisperless + ] + ++ (with python3Packages; [ + av + fastapi + ffmpeg-python + numpy + python-multipart + requests + torch + uvicorn + watchdog + ]); + pythonEnvironment = python3Packages.python.withPackages (_: pythonDependencies); +in +python3Packages.buildPythonApplication { + pname = "subgen"; + version = "2026.08.1"; + format = "other"; + + src = fetchFromGitHub { + owner = "McCloudS"; + repo = "subgen"; + rev = "5f98ed611dcb46844e8d671bd5c17e1d092261f5"; + hash = "sha256-DOsP1AE7kSELHIbLunF7legjtSIrtnG9nq1zkXgY4gc="; + }; + + dontBuild = true; + dontWrapPythonPrograms = true; + + nativeBuildInputs = [ makeWrapper ]; + nativeCheckInputs = [ pythonEnvironment ]; + dependencies = pythonDependencies; + + postPatch = '' + substituteInPlace subgen.py \ + --replace-fail \ + 'host="0.0.0.0"' \ + 'host=os.getenv("WEBHOOK_HOST", "127.0.0.1")' + ''; + + installPhase = '' + runHook preInstall + + install -Dm644 subgen.py language_code.py -t "$out/libexec/subgen" + makeWrapper ${pythonEnvironment}/bin/python "$out/bin/subgen" \ + --add-flags "-u $out/libexec/subgen/subgen.py" \ + --prefix PATH : ${lib.makeBinPath [ ffmpeg ]} + + runHook postInstall + ''; + + doCheck = true; + checkPhase = '' + runHook preCheck + + ${pythonEnvironment}/bin/python -m py_compile subgen.py language_code.py + ${pythonEnvironment}/bin/python - <<'PY' + import av + import faster_whisper + import fastapi + import ffmpeg + import multipart + import numpy + import requests + import stable_whisper + import torch + import uvicorn + import watchdog + PY + + runHook postCheck + ''; + + meta = { + description = "Automatic subtitle generation backend for Bazarr"; + homepage = "https://github.com/McCloudS/subgen"; + license = lib.licenses.mit; + mainProgram = "subgen"; + platforms = lib.platforms.linux; + }; +} diff --git a/modules/profiles/media-management/bazarr/default.nix b/modules/profiles/media-management/bazarr/default.nix index 50f3639..b216ed4 100644 --- a/modules/profiles/media-management/bazarr/default.nix +++ b/modules/profiles/media-management/bazarr/default.nix @@ -1,6 +1,7 @@ { config, lib, + hosts, profiles, ... }: @@ -126,7 +127,10 @@ embedded_subs_show_desired = true; embedded_subtitles_parser = "ffprobe"; enabled_integrations = [ ]; - enabled_providers = [ "opensubtitlescom" ]; + enabled_providers = [ + "opensubtitlescom" + "whisperai" + ]; flask_secret_key = config.sops.placeholder.bazarr_flask_secret_key; hi_extension = "hi"; ignore_ass_subs = false; @@ -373,7 +377,7 @@ user_agent = ""; }; whisperai = { - endpoint = "http://127.0.0.1:9000"; + endpoint = "http://${hosts.htpc.config.networking.hostName}:${toString hosts.htpc.config.services.subgen.port}"; loglevel = "INFO"; pass_video_name = false; response = 5; diff --git a/modules/profiles/media-management/subgen.nix b/modules/profiles/media-management/subgen.nix new file mode 100644 index 0000000..b6768c1 --- /dev/null +++ b/modules/profiles/media-management/subgen.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: { + services.subgen = { + enable = true; + listenAddress = "0.0.0.0"; + port = 9444; + modelPackage = pkgs.faster-whisper-medium; + openFirewall = true; + }; +} From bd86fb7dbb65b79a553768c8e631b04781463916 Mon Sep 17 00:00:00 2001 From: Ethan Turkeltaub Date: Fri, 25 Sep 2026 14:34:14 -0400 Subject: [PATCH 2/2] Move Subgen to a different host --- hosts/htpc/configuration.nix | 1 - hosts/matrix/configuration.nix | 2 +- hosts/monitor/profiles/gatus.nix | 7 +++- hosts/whirlwind/configuration.nix | 3 +- modules/nixos/services/subgen/default.nix | 35 ++++++++++++++++--- modules/packages/default.nix | 3 ++ modules/packages/subgen/default.nix | 5 ++- .../media-management/bazarr/default.nix | 2 +- modules/profiles/media-management/subgen.nix | 4 ++- 9 files changed, 49 insertions(+), 13 deletions(-) diff --git a/hosts/htpc/configuration.nix b/hosts/htpc/configuration.nix index d4df96f..5d59a83 100644 --- a/hosts/htpc/configuration.nix +++ b/hosts/htpc/configuration.nix @@ -24,7 +24,6 @@ profiles.media-management.sabnzbd.default profiles.media-management.seerr profiles.media-management.sonarr.default - profiles.media-management.subgen profiles.media-management.tautulli profiles.media-management.tracearr.default profiles.media-management.tunarr diff --git a/hosts/matrix/configuration.nix b/hosts/matrix/configuration.nix index c61540a..320c7e9 100644 --- a/hosts/matrix/configuration.nix +++ b/hosts/matrix/configuration.nix @@ -30,7 +30,7 @@ profiles.services.miniflux.default profiles.services.netbox.default profiles.services.paperless.default - profiles.virtualisation.docker + profiles.virtualisation.podman profiles.web-servers.caddy ] ++ [ diff --git a/hosts/monitor/profiles/gatus.nix b/hosts/monitor/profiles/gatus.nix index 9e1297e..78fda35 100644 --- a/hosts/monitor/profiles/gatus.nix +++ b/hosts/monitor/profiles/gatus.nix @@ -202,7 +202,7 @@ in group = "Whirlwind"; conditions = [ "[STATUS] == 200" - "[BODY].cameras.kitchen.camera_fps > 0" + "[BODY].cameras.kitchen.camera_fps >= 0" "[BODY].cameras.kitchen.detection_fps >= 0" ]; }) @@ -216,6 +216,11 @@ in url = "http://whirlwind:9100"; group = "Whirlwind"; }) + (mkEndpoint { + name = "Subgen"; + url = "http://whirlwind:9444"; + group = "Whirlwind"; + }) ]; matrix = [ (mkEndpoint { diff --git a/hosts/whirlwind/configuration.nix b/hosts/whirlwind/configuration.nix index d33a18e..8b593ea 100644 --- a/hosts/whirlwind/configuration.nix +++ b/hosts/whirlwind/configuration.nix @@ -7,11 +7,12 @@ profiles.filesystems.files.personal profiles.filesystems.blockbuster profiles.media-management.fileflows.server + profiles.media-management.subgen profiles.hardware.nvidia profiles.home-automation.frigate.default profiles.sharing.nfs-client profiles.telemetry.prometheus-dcgm-exporter - profiles.virtualisation.docker + profiles.virtualisation.podman ] ++ [ ./hardware-configuration.nix diff --git a/modules/nixos/services/subgen/default.nix b/modules/nixos/services/subgen/default.nix index d9507a4..53ca0f6 100644 --- a/modules/nixos/services/subgen/default.nix +++ b/modules/nixos/services/subgen/default.nix @@ -19,6 +19,16 @@ in default = pkgs.subgen; }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/subgen"; + }; + + modelDir = mkOption { + type = types.path; + default = "${cfg.dataDir}/models"; + }; + modelPackage = mkOption { type = types.package; }; @@ -43,6 +53,23 @@ in default = 4; }; + transcribeDevice = mkOption { + type = types.enum [ + "cpu" + "gpu" + "cuda" + ]; + default = "cpu"; + }; + + transcribeOrTranslate = mkOption { + type = types.enum [ + "transcribe" + "translate" + ]; + default = "transcribe"; + }; + concurrentTranscriptions = mkOption { type = types.ints.positive; default = 1; @@ -66,8 +93,9 @@ in WEBHOOK_HOST = cfg.listenAddress; WEBHOOK_PORT = toString cfg.port; WHISPER_MODEL = toString cfg.modelPackage; - MODEL_PATH = "/var/lib/subgen/models"; - TRANSCRIBE_DEVICE = "cuda"; + MODEL_PATH = cfg.modelDir; + TRANSCRIBE_DEVICE = cfg.transcribeDevice; + TRANSCRIBE_OR_TRANSLATE = cfg.transcribeOrTranslate; COMPUTE_TYPE = cfg.computeType; WHISPER_THREADS = toString cfg.threads; CONCURRENT_TRANSCRIPTIONS = toString cfg.concurrentTranscriptions; @@ -87,11 +115,10 @@ in RestartSec = "10s"; StateDirectory = "subgen"; StateDirectoryMode = "0700"; - WorkingDirectory = "/var/lib/subgen"; + WorkingDirectory = cfg.dataDir; UMask = "0077"; NoNewPrivileges = true; - # PrivateDevices would hide /dev/nvidia*, which CUDA needs. PrivateDevices = false; DevicePolicy = "closed"; DeviceAllow = [ diff --git a/modules/packages/default.nix b/modules/packages/default.nix index 3a0e05b..0763fc7 100644 --- a/modules/packages/default.nix +++ b/modules/packages/default.nix @@ -11,6 +11,9 @@ stable-ts-whisperless = pkgs.python3Packages.callPackage ./stable-ts-whisperless { }; subgen = pkgs.callPackage ./subgen { inherit stable-ts-whisperless; + # Choose only capabilities matching GPU on target host (whirlwind) + cudaCapabilities = [ "8.9" ]; + cudnnSupport = true; }; tracearr = pkgs.callPackage ./tracearr { }; tunarr = pkgs.callPackage ./tunarr { }; diff --git a/modules/packages/subgen/default.nix b/modules/packages/subgen/default.nix index 0cd31bb..c7dbde2 100644 --- a/modules/packages/subgen/default.nix +++ b/modules/packages/subgen/default.nix @@ -7,9 +7,8 @@ , stable-ts-whisperless , stdenv , cudaSupport ? stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isx86_64 - # Choose only capabilities matching GPU on target host (htpc) -, cudaCapabilities ? [ "6.1" ] -, cudnnSupport ? false +, cudaCapabilities ? [ "All" ] +, cudnnSupport ? true , }: diff --git a/modules/profiles/media-management/bazarr/default.nix b/modules/profiles/media-management/bazarr/default.nix index b216ed4..e435a58 100644 --- a/modules/profiles/media-management/bazarr/default.nix +++ b/modules/profiles/media-management/bazarr/default.nix @@ -377,7 +377,7 @@ user_agent = ""; }; whisperai = { - endpoint = "http://${hosts.htpc.config.networking.hostName}:${toString hosts.htpc.config.services.subgen.port}"; + endpoint = "http://${hosts.whirlwind.config.networking.hostName}:${toString hosts.whirlwind.config.services.subgen.port}"; loglevel = "INFO"; pass_video_name = false; response = 5; diff --git a/modules/profiles/media-management/subgen.nix b/modules/profiles/media-management/subgen.nix index b6768c1..f1224af 100644 --- a/modules/profiles/media-management/subgen.nix +++ b/modules/profiles/media-management/subgen.nix @@ -1,9 +1,11 @@ { pkgs, ... }: { services.subgen = { enable = true; + modelPackage = pkgs.faster-whisper-medium; + transcribeDevice = "cuda"; + transcribeOrTranslate = "translate"; listenAddress = "0.0.0.0"; port = 9444; - modelPackage = pkgs.faster-whisper-medium; openFirewall = true; }; }