Skip to content

Commit

Permalink
Add NixOS module, Nix shell and overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
JulianFP committed Apr 11, 2024
1 parent edbca85 commit 899e323
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 0 deletions.
43 changes: 43 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
description = "This is the runner for Project-W, a service that converts uploaded audio files into downloadable text transcripts using OpenAIs whisper AI model, hosted on a backend server and dedicated runners.";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
systems.url = "github:nix-systems/default";
};

outputs = inputs@{ nixpkgs, systems, ...}:
let
pythonOverlay = import ./nix/overlay.nix;
eachSystem = nixpkgs.lib.genAttrs (import systems);
pkgsFor = eachSystem (system:
import nixpkgs {
inherit system;
#overlays add all new packages and their dependencies
overlays = [pythonOverlay];
}
);
in {
packages = eachSystem (system: rec {
default = project-W-runner;
project-W-runner = pkgsFor.${system}.python3Packages.project-W-runner;
});
devShells = eachSystem (system: {
default = import ./nix/shell.nix { pkgs=pkgsFor.${system}; };
});
nixosModules.default = import ./nix/module.nix inputs;
overlays.default = pythonOverlay;
};
}
170 changes: 170 additions & 0 deletions nix/module.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
inputs: {config, lib, pkgs, ...}:
let
inherit (pkgs.stdenv.hostPlatform) system;
inherit (lib)
mdDoc
mkIf
mkOption
mkEnableOption
types
getExe
escapeShellArgs
;
cfg = config.services.project-W-runner;
cfg_str = "services.project-W-runner";
in {
options = {
services.project-W-runner = {
enable = mkEnableOption (mdDoc "Runner of Project-W");
package = mkOption {
type = types.package;
default = inputs.self.packages.${system}.project-W-runner;
description = mdDoc ''
Project-W runner python package to use.
'';
};
user = mkOption {
type = types.singleLineStr;
default = "project-W-runner";
description = mdDoc ''
User account under which the runner runs.
'';
};
group = mkOption {
type = types.singleLineStr;
default = "project-W-runner";
description = mdDoc ''
User group under which the runner runs.
'';
};
settings = {
runnerToken = mkOption {
type = types.singleLineStr;
default = "\${RUNNER_TOKEN}";
description = mdDoc ''
Token that the runner uses to authenticate with the backend. Warning: This will be public in the /nix/store! For production systems please use [envFile](${cfg_str}.envFile) combined with a secret management tool like sops-nix instead!!!
'';
};
backendURL = mkOption {
type = types.strMatching "^(http|https):\/\/(([a-zA-Z0-9\-]+\.)+[a-zA-Z0-9\-]+|localhost)(:[0-9]+)?((\/[a-zA-Z0-9\-]+)+)?$$";
example = "https://example.com";
description = mdDoc ''
URL under which the backend is hosted (including http/https, port, shouldn't end with /).
'';
};
modelCacheDir = mkOption {
type = types.singleLineStr;
default = "/var/cache/project-W-runner_whisperCache";
description = mdDoc ''
Directory used to cache whisper AI models.
'';
};
torchDevice = mkOption {
type = types.nullOr types.singleLineStr;
default = null;
example = "cuda:1";
description = mdDoc ''
The PyTorch device used by Whisper. If set to null then pytorches default device will be used.
'';
};
};
envOptions = mkOption {
type = types.listOf types.singleLineStr;
default = [ "runnerToken" ];
description = mdDoc ''
Attributes that require loading of environment variables. An !ENV will be added to the yaml config for these. Just add the name of the attribute itself, not the name of the attribute set(s) it is in.
'';
};
envFile = mkOption {
type = types.nullOr types.singleLineStr;
default = null;
example = "/run/secrets/secretFile";
description = mdDoc ''
Path to file to load secrets from. All secrets should be written as environment variables (in NAME=VALUE declerations, one per line). Per default, RUNNER_TOKEN sets the runner token. The content of the file most likely should look like this:
```
RUNNER_TOKEN=<your runners token>
```
This file should be accessable by the user [user](${cfg_str}.user) and by this user only!
'';
};
};
};

config =
let
stringsToReplace = builtins.map (x: x + ":") cfg.envOptions;
newStrings = builtins.map (x: x + " !ENV") stringsToReplace;
filteredSettings = pkgs.lib.filterAttrsRecursive (name: value: value != null) cfg.settings;
fileWithoutEnvs = (pkgs.formats.yaml { }).generate "project-W-runner-config-without-env.yaml" filteredSettings;
configFile = pkgs.writeTextDir "config.yml" (builtins.replaceStrings stringsToReplace newStrings (builtins.readFile fileWithoutEnvs));
#function that checks if we have attributes in cfg.envOptions that are not strings
invalidEnvOption = (attrSet:
let
v = builtins.attrValues attrSet;
boolFunc = (element:
if (builtins.isAttrs element) then (invalidEnvOption element)
else if (builtins.elem element cfg.envOptions && !(builtins.isString element)) then true
else false
);
iterateV = (i:
if (i >= (builtins.length v)) then false
else if (boolFunc (builtins.elemAt v i)) then true
else iterateV (i+1)
);
in
iterateV 0
);
in mkIf cfg.enable {
assertions = [
{
assertion = !(invalidEnvOption cfg.settings);
message = "The ${cfg_str}.envOptions option cannot contain attributes that are not some kind of string in ${cfg_str}.settings";
}
{
assertion = cfg.envOptions == [] || cfg.envFile != null;
message = "The ${cfg_str}.envFile option can't be null if ${cfg_str}.envOptions contains elements. Per default the runner token ${cfg_str}.settings.runnerToken has to be set in envFile.";
}
];

systemd = {
#create directories for persistent stuff
tmpfiles.settings.project-W-runner-dirs = {
"${cfg.settings.modelCacheDir}"."d" = {
mode = "700";
inherit (cfg) user group;
};
};

#setup systemd service for runner
services.project-W-runner = {
description = "Project-W runner";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
UMask = "0077";
ExecStart = escapeShellArgs [
"${getExe cfg.package}"
"--customConfigPath" "${configFile}"
];
PrivateTmp = true;
EnvironmentFile = mkIf (cfg.envFile != null) cfg.envFile;
};
};
};

#setup user/group under which systemd service is run
users.users = mkIf (cfg.user == "project-W-runner") {
project-W-runner = {
inherit (cfg) group;
isSystemUser = true;
};
};
users.groups = mkIf (cfg.group == "project-W-runner") {
project-W-runner = {};
};
};
}
9 changes: 9 additions & 0 deletions nix/overlay.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
(final: prev: {
python3 = prev.python3.override {
packageOverrides = pyfinal: pyprev: {
pyaml-env = prev.callPackage ./pkgs/pyaml-env.nix { };
project-W-runner = prev.callPackage ./pkgs/project-W-runner.nix { };
};
};
python3Packages = final.python3.pkgs;
})
45 changes: 45 additions & 0 deletions nix/pkgs/project-W-runner.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
lib,
ffmpeg,
python3Packages
}:

python3Packages.buildPythonPackage rec {
pname = "project_W_runner";
version = "0.0.1";
format = "setuptools";

src = ../../.;

nativeBuildInputs = with python3Packages; [
setuptools-scm
];
buildInputs = [
ffmpeg
];
propagatedBuildInputs = with python3Packages; [
aiohttp
click
jsonschema
openai-whisper
numpy
platformdirs
pyaml-env
];

nativeCheckInputs = with python3Packages; [
pytestCheckHook
pytest-cov
];
pythonImportsCheck = [ pname ];

#hardcode version so that setuptools-scm works without .git folder:
SETUPTOOLS_SCM_PRETEND_VERSION = version;

meta = {
description = "Runner for Project-W";
homepage = "https://github.com/JulianFP/project-W-runner";
license = lib.licenses.mit;
mainProgram = pname;
};
}
24 changes: 24 additions & 0 deletions nix/pkgs/pyaml-env.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
lib,
fetchPypi,
python3Packages
}:

python3Packages.buildPythonPackage rec {
pname = "pyaml-env";
version = "1.2.1";
src = fetchPypi {
inherit version;
pname = "pyaml_env";
sha256 = "sha256-bV3JjIyC33Q6EywZbnmWMFDJ/rBbCm8l8613dx09lbA=";
};
doCheck = false;
propagatedBuildInputs = with python3Packages; [
pyyaml
];
meta = {
description = "Parse YAML configuration with environment variables in Python";
homepage = "https://github.com/mkaranasou/pyaml_env";
license = lib.licenses.mit;
};
}
17 changes: 17 additions & 0 deletions nix/shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
let
dontCheckPythonPkg = drv: drv.overridePythonAttrs (old: { doCheck = false; });
myPythonPackages = ps: with ps; [
#all required dependencies + this projects package itself (required for sphinx)
(dontCheckPythonPkg project-W-runner)

#optional dependencies: tests
pytest
pytest-cov
];
in
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
buildInputs = with pkgs; [
(python3.withPackages myPythonPackages)
];
}

0 comments on commit 899e323

Please sign in to comment.