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..5d59a83 100644 --- a/hosts/htpc/configuration.nix +++ b/hosts/htpc/configuration.nix @@ -24,9 +24,9 @@ profiles.media-management.sabnzbd.default profiles.media-management.seerr profiles.media-management.sonarr.default - profiles.media-management.tunarr 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 +37,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/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 new file mode 100644 index 0000000..53ca0f6 --- /dev/null +++ b/modules/nixos/services/subgen/default.nix @@ -0,0 +1,146 @@ +{ + 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; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/subgen"; + }; + + modelDir = mkOption { + type = types.path; + default = "${cfg.dataDir}/models"; + }; + + 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; + }; + + 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; + }; + + 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 = 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; + 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 = cfg.dataDir; + UMask = "0077"; + + NoNewPrivileges = true; + 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..0763fc7 100644 --- a/modules/packages/default.nix +++ b/modules/packages/default.nix @@ -1,12 +1,20 @@ { 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; + # Choose only capabilities matching GPU on target host (whirlwind) + cudaCapabilities = [ "8.9" ]; + cudnnSupport = true; + }; 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..c7dbde2 --- /dev/null +++ b/modules/packages/subgen/default.nix @@ -0,0 +1,122 @@ +{ lib +, ctranslate2 +, fetchFromGitHub +, ffmpeg +, makeWrapper +, python3Packages +, stable-ts-whisperless +, stdenv +, cudaSupport ? stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isx86_64 +, cudaCapabilities ? [ "All" ] +, cudnnSupport ? true +, +}: + +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..e435a58 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.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 new file mode 100644 index 0000000..f1224af --- /dev/null +++ b/modules/profiles/media-management/subgen.nix @@ -0,0 +1,11 @@ +{ pkgs, ... }: { + services.subgen = { + enable = true; + modelPackage = pkgs.faster-whisper-medium; + transcribeDevice = "cuda"; + transcribeOrTranslate = "translate"; + listenAddress = "0.0.0.0"; + port = 9444; + openFirewall = true; + }; +}