feat: prepare Orion transcription pilot

This commit is contained in:
kiri committed 2026-09-06 01:04:41 +02:00
1 parent ec54ddd0f8
commit 656f144267
4 files changed
+117 -50

No files matched your search

+76 -38
View File
@@ -92,8 +92,9 @@ process_recording() (
local transcript_dir=$3 local transcript_dir=$3
local archive_dir=$4 local archive_dir=$4
local failed_dir=$5 local failed_dir=$5
local base stem transcript archive original_path recorded_at transcribed_at local base stem transcript archive original_path recorded_at transcribed_at duration
local work_dir log_file raw_output rendered_output draft state_file recovery_status local work_dir log_file raw_output rendered_output draft state_file recovery_status
local diarization_succeeded=false
base=$(basename "$source") base=$(basename "$source")
stem=${base%.*} stem=${base%.*}
@@ -129,6 +130,20 @@ process_recording() (
raw_output="$work_dir/transcript" raw_output="$work_dir/transcript"
rendered_output="$work_dir/transcript.md" rendered_output="$work_dir/transcript.md"
if ! duration=$(ffprobe \
-v error \
-show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 \
"$source" 2>"$log_file"); then
fail_recording "$source" "$failed_dir" "Media validation failed" "$log_file"
return 1
fi
if [[ ! $duration =~ ^[0-9]+([.][0-9]+)?$ || -z ${duration//[.0]/} ]]; then
printf 'Invalid media duration: %s\n' "$duration" >"$log_file"
fail_recording "$source" "$failed_dir" "Media validation failed" "$log_file"
return 1
fi
if ! ffmpeg \ if ! ffmpeg \
-nostdin \ -nostdin \
-hide_banner \ -hide_banner \
@@ -148,18 +163,24 @@ process_recording() (
--file "$work_dir/audio.wav" \ --file "$work_dir/audio.wav" \
--language auto \ --language auto \
--output-json-full \ --output-json-full \
--output-file "$raw_output" >>"$log_file" 2>&1 || [[ ! -s "${raw_output}.json" ]]; then --output-txt \
--output-file "$raw_output" >>"$log_file" 2>&1 \
|| [[ ! -s "${raw_output}.json" || ! -s "${raw_output}.txt" ]]; then
fail_recording "$source" "$failed_dir" "Whisper transcription failed" "$log_file" fail_recording "$source" "$failed_dir" "Whisper transcription failed" "$log_file"
return 1 return 1
fi fi
if ! diarize-transcript \ if [[ -f "$TRANSCRIBE_DIARIZATION_MODEL/.complete" && -f "$TRANSCRIBE_DIARIZATION_MODEL/config.yaml" ]] \
--audio "$work_dir/audio.wav" \ && diarize-transcript \
--model "$TRANSCRIBE_DIARIZATION_MODEL" \ --audio "$work_dir/audio.wav" \
--transcript "${raw_output}.json" \ --model "$TRANSCRIBE_DIARIZATION_MODEL" \
--output "$rendered_output" >>"$log_file" 2>&1 || [[ ! -s "$rendered_output" ]]; then --transcript "${raw_output}.json" \
fail_recording "$source" "$failed_dir" "Speaker diarization failed" "$log_file" --output "$rendered_output" >>"$log_file" 2>&1 \
return 1 && [[ -s "$rendered_output" ]]; then
diarization_succeeded=true
else
printf 'Speaker diarization unavailable; publishing unlabeled transcript\n' >&2
cp -- "${raw_output}.txt" "$rendered_output"
fi fi
draft=$(mktemp "$transcript_dir/.transcript.XXXXXX") draft=$(mktemp "$transcript_dir/.transcript.XXXXXX")
@@ -168,12 +189,17 @@ process_recording() (
printf -- '- Original recording: %s\n' "$original_path" printf -- '- Original recording: %s\n' "$original_path"
printf -- '- Recording timestamp: %s\n' "$recorded_at" printf -- '- Recording timestamp: %s\n' "$recorded_at"
printf -- '- Transcription timestamp: %s\n' "$transcribed_at" printf -- '- Transcription timestamp: %s\n' "$transcribed_at"
printf -- '- Engine: whisper.cpp %s (Vulkan)\n' "$TRANSCRIBE_ENGINE_VERSION" printf -- '- Duration: %s seconds\n' "$duration"
printf -- '- Engine: whisper.cpp %s (%s)\n' "$TRANSCRIBE_ENGINE_VERSION" "$TRANSCRIBE_ENGINE_ACCELERATION"
printf -- '- Model: %s\n' "$TRANSCRIBE_MODEL_NAME" printf -- '- Model: %s\n' "$TRANSCRIBE_MODEL_NAME"
printf -- '- Model SHA-256: %s\n' "$TRANSCRIBE_MODEL_SHA256" printf -- '- Model SHA-256: %s\n' "$TRANSCRIBE_MODEL_SHA256"
printf -- '- Diarization engine: pyannote.audio %s (CPU)\n' "$TRANSCRIBE_DIARIZATION_ENGINE_VERSION" if [[ $diarization_succeeded == true ]]; then
printf -- '- Diarization model: %s\n' "$TRANSCRIBE_DIARIZATION_MODEL_NAME" printf -- '- Diarization engine: pyannote.audio %s (CPU)\n' "$TRANSCRIBE_DIARIZATION_ENGINE_VERSION"
printf -- '- Diarization model revision: %s\n\n' "$TRANSCRIBE_DIARIZATION_MODEL_REVISION" printf -- '- Diarization model: %s\n' "$TRANSCRIBE_DIARIZATION_MODEL_NAME"
printf -- '- Diarization model revision: %s\n\n' "$TRANSCRIBE_DIARIZATION_MODEL_REVISION"
else
printf -- '- Diarization: unavailable; unlabeled fallback published\n\n'
fi
cat -- "$rendered_output" cat -- "$rendered_output"
} >"$draft" } >"$draft"
chmod 0644 "$draft" chmod 0644 "$draft"
@@ -203,7 +229,8 @@ process_scope() {
local processing_dir="$recordings_dir/processing" local processing_dir="$recordings_dir/processing"
local archive_dir="$recordings_dir/archive" local archive_dir="$recordings_dir/archive"
local failed_dir="$recordings_dir/failed" local failed_dir="$recordings_dir/failed"
local source processing_target recording_status scope_status=0 local source processing_target recording_status signature index scope_status=0
local -a sources signatures
if ! mkdir -p "$inbox_dir" "$processing_dir" "$archive_dir" "$failed_dir" "$transcript_dir"; then if ! mkdir -p "$inbox_dir" "$processing_dir" "$archive_dir" "$failed_dir" "$transcript_dir"; then
return 1 return 1
@@ -219,23 +246,42 @@ process_scope() {
fi fi
done < <(find "$processing_dir" -maxdepth 1 -type f -print0 | sort -z) done < <(find "$processing_dir" -maxdepth 1 -type f -print0 | sort -z)
while IFS= read -r -d '' source; do while true; do
if ! is_recording "$source"; then sources=()
continue signatures=()
while IFS= read -r -d '' source; do
if is_recording "$source" && signature=$(stat -c '%s:%y:%i' -- "$source"); then
sources+=("$source")
signatures+=("$signature")
fi
done < <(find "$inbox_dir" -maxdepth 1 -type f -print0 | sort -z)
if [[ ${#sources[@]} -eq 0 ]]; then
break
fi fi
processing_target="$processing_dir/$(basename "$source")" sleep "${TRANSCRIBE_SETTLE_SECONDS:-300}"
if ! move_no_clobber "$source" "$processing_target"; then
fail_recording "$source" "$failed_dir" "Processing filename already exists: $processing_target" for index in "${!sources[@]}"; do
scope_status=1 source=${sources[$index]}
continue if [[ ! -f "$source" ]] || [[ $(stat -c '%s:%y:%i' -- "$source") != "${signatures[$index]}" ]]; then
fi continue
process_recording "$processing_target" "$inbox_dir" "$transcript_dir" "$archive_dir" "$failed_dir" fi
recording_status=$?
if [[ $recording_status -ne 0 ]]; then processing_target="$processing_dir/$(basename "$source")"
scope_status=1 if ! move_no_clobber "$source" "$processing_target"; then
fi fail_recording "$source" "$failed_dir" "Processing filename already exists: $processing_target"
done < <(find "$inbox_dir" -maxdepth 1 -type f -print0 | sort -z) scope_status=1
continue
fi
process_recording "$processing_target" "$inbox_dir" "$transcript_dir" "$archive_dir" "$failed_dir"
recording_status=$?
if [[ $recording_status -ne 0 ]]; then
scope_status=1
fi
done
done
return "$scope_status" return "$scope_status"
} }
@@ -243,14 +289,6 @@ process_scope() {
runtime_dir="${XDG_RUNTIME_DIR:?}/transcribe-recordings" runtime_dir="${XDG_RUNTIME_DIR:?}/transcribe-recordings"
mkdir -p "$runtime_dir" mkdir -p "$runtime_dir"
if [[ ! -f "$TRANSCRIBE_DIARIZATION_MODEL/.complete" || ! -f "$TRANSCRIBE_DIARIZATION_MODEL/config.yaml" ]]; then
printf 'Speaker diarization model is not installed; run prepare-transcription-diarization-model\n' >&2
exit 1
fi
set +o errexit set +o errexit
process_scope "$TRANSCRIBE_SYNC_ROOT/recordings" "$TRANSCRIBE_SYNC_ROOT/notes/transcripts" process_scope "$TRANSCRIBE_SYNC_ROOT/recordings" "$TRANSCRIBE_SYNC_ROOT/notes/transcripts"
personal_status=$? exit $?
process_scope "$TRANSCRIBE_SYNC_ROOT/work/recordings" "$TRANSCRIBE_SYNC_ROOT/work/notes/transcripts"
work_status=$?
exit "$((personal_status != 0 || work_status != 0))"
+1 -5
View File
@@ -64,11 +64,7 @@ in
${pkgs.coreutils}/bin/mkdir -p \ ${pkgs.coreutils}/bin/mkdir -p \
"${home}/sync/notes/daily" \ "${home}/sync/notes/daily" \
"${home}/sync/notes/meetings" \ "${home}/sync/notes/meetings" \
"${home}/sync/notes/transcripts" \ "${home}/sync/notes/transcripts"
"${home}/sync/recordings/inbox" \
"${home}/sync/recordings/processing" \
"${home}/sync/recordings/archive" \
"${home}/sync/recordings/failed"
''; '';
}; };
+39 -7
View File
@@ -11,6 +11,7 @@ in
... ...
}: }:
let let
isOrion = config.facts.machine.name == "orion";
modelName = "large-v3-turbo-q5_0"; modelName = "large-v3-turbo-q5_0";
modelHash = "sha256-OUIhcJzVrR9AxG5gMcphvOiJMebgiMGIKUxtWlX/p+I="; modelHash = "sha256-OUIhcJzVrR9AxG5gMcphvOiJMebgiMGIKUxtWlX/p+I=";
modelSha256 = builtins.convertHash { modelSha256 = builtins.convertHash {
@@ -37,6 +38,8 @@ in
${builtins.readFile ./_diarize-transcript.py} ${builtins.readFile ./_diarize-transcript.py}
''; '';
}; };
whisper = if isOrion then pkgs.whisper-cpp else pkgs.whisper-cpp-vulkan;
acceleration = if isOrion then "CPU" else "Vulkan";
prepareDiarizationModel = pkgs.writeShellApplication { prepareDiarizationModel = pkgs.writeShellApplication {
name = "prepare-transcription-diarization-model"; name = "prepare-transcription-diarization-model";
runtimeInputs = with pkgs; [ runtimeInputs = with pkgs; [
@@ -72,18 +75,28 @@ in
diarizeTranscript diarizeTranscript
ffmpeg ffmpeg
findutils findutils
whisper-cpp-vulkan whisper
]; ];
text = builtins.readFile ./_transcribe-recordings.sh; text = builtins.readFile ./_transcribe-recordings.sh;
}; };
home = config.home.homeDirectory;
in in
{ {
home.packages = with pkgs; [ home.packages = [
ffmpeg pkgs.ffmpeg
prepareDiarizationModel prepareDiarizationModel
whisper-cpp-vulkan 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 = { systemd.user.services.transcribe-recordings = {
Unit.Description = "Transcribe synchronized recordings with whisper.cpp"; Unit.Description = "Transcribe synchronized recordings with whisper.cpp";
@@ -91,7 +104,8 @@ in
Type = "oneshot"; Type = "oneshot";
ExecStart = lib.getExe transcribeRecordings; ExecStart = lib.getExe transcribeRecordings;
Environment = [ Environment = [
"TRANSCRIBE_ENGINE_VERSION=${pkgs.whisper-cpp-vulkan.version}" "TRANSCRIBE_ENGINE_ACCELERATION=${acceleration}"
"TRANSCRIBE_ENGINE_VERSION=${whisper.version}"
"TRANSCRIBE_MODEL=${model}" "TRANSCRIBE_MODEL=${model}"
''"TRANSCRIBE_MODEL_NAME=${modelName} multilingual"'' ''"TRANSCRIBE_MODEL_NAME=${modelName} multilingual"''
"TRANSCRIBE_MODEL_SHA256=${modelSha256}" "TRANSCRIBE_MODEL_SHA256=${modelSha256}"
@@ -99,12 +113,30 @@ in
"TRANSCRIBE_DIARIZATION_MODEL=${diarizationModel}" "TRANSCRIBE_DIARIZATION_MODEL=${diarizationModel}"
"TRANSCRIBE_DIARIZATION_MODEL_NAME=${diarizationModelName}" "TRANSCRIBE_DIARIZATION_MODEL_NAME=${diarizationModelName}"
"TRANSCRIBE_DIARIZATION_MODEL_REVISION=${diarizationModelRevision}" "TRANSCRIBE_DIARIZATION_MODEL_REVISION=${diarizationModelRevision}"
"TRANSCRIBE_SYNC_ROOT=${config.home.homeDirectory}/sync" "TRANSCRIBE_SETTLE_SECONDS=300"
"TRANSCRIBE_SYNC_ROOT=${home}/sync"
]; ];
}
// lib.optionalAttrs isOrion {
Restart = "on-failure";
RestartSec = 60;
}; };
Install.WantedBy = lib.optionals isOrion [ "default.target" ];
}; };
systemd.user.timers.transcribe-recordings = { systemd.user.paths.transcribe-recordings = lib.mkIf isOrion {
Unit.Description = "Watch for synchronized recordings";
Path = {
PathChanged = "${home}/sync/recordings/inbox";
Unit = "transcribe-recordings.service";
};
Install.WantedBy = [ "paths.target" ];
};
systemd.user.timers.transcribe-recordings = lib.mkIf (!isOrion) {
Unit.Description = "Transcribe new recordings every two minutes"; Unit.Description = "Transcribe new recordings every two minutes";
Timer = { Timer = {
+1
View File
@@ -27,6 +27,7 @@ in
nixosModules.radicale nixosModules.radicale
nixosModules.actual nixosModules.actual
nixosModules.gitea nixosModules.gitea
nixosModules.transcription
./_hardware.nix ./_hardware.nix
./_disk.nix ./_disk.nix
]; ];