feat: prepare Orion transcription pilot
This commit is contained in:
1 parent
ec54ddd0f8
commit
656f144267
4 files changed
+117
-50
No files matched your search
@@ -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))"
|
|
||||||
@@ -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"
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in new issue
Block a user