Files
lux/modules/capabilities/transcription.nix
T
2026-09-06 04:03:54 +02:00

133 lines
4.5 KiB
Nix

{ config, ... }:
{
flake.modules.homeManager.transcription =
{
config,
lib,
pkgs,
...
}:
let
modelName = "large-v3-turbo-q5_0";
modelHash = "sha256-OUIhcJzVrR9AxG5gMcphvOiJMebgiMGIKUxtWlX/p+I=";
modelSha256 = builtins.convertHash {
hash = modelHash;
toHashFormat = "base16";
};
diarizationModelName = "pyannote/speaker-diarization-community-1";
diarizationModelRevision = "3533c8cf8e369892e6b79ff1bf80f7b0286a54ee";
diarizationModel = "${config.xdg.dataHome}/transcribe-recordings/speaker-diarization-community-1";
model = pkgs.fetchurl {
name = "ggml-${modelName}.bin";
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/98aa99a0a9db05ae2342309f5096248665f7cba3/ggml-${modelName}.bin";
hash = modelHash;
};
diarizationPython = pkgs.python3.withPackages (pythonPackages: [
pythonPackages.pyannote-audio
]);
diarizeTranscript = pkgs.writeTextFile {
name = "diarize-transcript";
destination = "/bin/diarize-transcript";
executable = true;
text = ''
#!${diarizationPython}/bin/python3
${builtins.readFile ./_diarize-transcript.py}
'';
};
whisper = pkgs.whisper-cpp;
prepareDiarizationModel = pkgs.writeShellApplication {
name = "prepare-transcription-diarization-model";
runtimeInputs = with pkgs; [
coreutils
python3Packages.huggingface-hub
];
text = ''
model_dir=${lib.escapeShellArg diarizationModel}
if [[ -f "$model_dir/.complete" ]]; then
printf 'Speaker diarization model is already installed at %s\n' "$model_dir"
exit 0
fi
printf 'Accept the model terms at https://huggingface.co/${diarizationModelName}\n'
if ! hf auth whoami >/dev/null 2>&1; then
hf auth login
fi
mkdir -p "$model_dir"
hf download \
${lib.escapeShellArg diarizationModelName} \
--revision ${lib.escapeShellArg diarizationModelRevision} \
--local-dir "$model_dir"
touch "$model_dir/.complete"
printf 'Installed speaker diarization model at %s\n' "$model_dir"
'';
};
transcribeRecordings = pkgs.writeShellApplication {
name = "transcribe-recordings";
runtimeInputs = with pkgs; [
coreutils
diarizeTranscript
ffmpeg
findutils
whisper
];
text = builtins.readFile ./_transcribe-recordings.sh;
};
home = config.home.homeDirectory;
in
{
home.packages = [
pkgs.ffmpeg
prepareDiarizationModel
whisper
];
home.activation.createTranscriptionLayout = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
${pkgs.coreutils}/bin/mkdir -p \
"${home}/sync/notes/transcripts" \
"${home}/sync/recordings/inbox" \
"${home}/sync/recordings/processing" \
"${home}/sync/recordings/archive" \
"${home}/sync/recordings/failed"
'';
systemd.user.services.transcribe-recordings = {
Unit.Description = "Transcribe synchronized recordings with whisper.cpp";
Service = {
Type = "oneshot";
ExecStart = lib.getExe transcribeRecordings;
Environment = [
"TRANSCRIBE_ENGINE_ACCELERATION=CPU"
"TRANSCRIBE_ENGINE_VERSION=${whisper.version}"
"TRANSCRIBE_MODEL=${model}"
''"TRANSCRIBE_MODEL_NAME=${modelName} multilingual"''
"TRANSCRIBE_MODEL_SHA256=${modelSha256}"
"TRANSCRIBE_DIARIZATION_ENGINE_VERSION=${pkgs.python3Packages.pyannote-audio.version}"
"TRANSCRIBE_DIARIZATION_MODEL=${diarizationModel}"
"TRANSCRIBE_DIARIZATION_MODEL_NAME=${diarizationModelName}"
"TRANSCRIBE_DIARIZATION_MODEL_REVISION=${diarizationModelRevision}"
"TRANSCRIBE_SYNC_ROOT=${home}/sync"
];
Restart = "on-failure";
RestartSec = 60;
};
Install.WantedBy = [ "default.target" ];
};
systemd.user.paths.transcribe-recordings = {
Unit.Description = "Watch for synchronized recordings";
Path.PathChanged = "${home}/sync/recordings/inbox";
Install.WantedBy = [ "paths.target" ];
};
};
flake.modules.nixos.transcription = {
home-manager.sharedModules = [ config.flake.modules.homeManager.transcription ];
};
}