path_exists() { [[ -e "$1" || -L "$1" ]] } is_recording() { case "${1,,}" in *.wav | *.flac | *.mp3 | *.ogg | *.opus | *.aac | *.m4a) return 0 ;; *) return 1 ;; esac } move_no_clobber() { local source=$1 local destination=$2 if path_exists "$destination"; then return 1 fi mv -T --no-clobber -- "$source" "$destination" ! path_exists "$source" && path_exists "$destination" } fail_recording() { local source=$1 local failed_dir=$2 local message=$3 local details=${4:-} local base target error_tmp suffix=0 base=$(basename "$source") target="$failed_dir/$base" rm -f -- "${source}.transcribe-state" while path_exists "$target" || path_exists "${target}.error.txt"; do ((suffix += 1)) target="$failed_dir/$base.failed-$suffix" done if ! move_no_clobber "$source" "$target"; then printf 'Could not quarantine %s: destination collision or move failure\n' "$source" >&2 return 1 fi error_tmp=$(mktemp "$failed_dir/.transcription-error.XXXXXX") { printf '%s\n' "$message" if [[ -n "$details" && -s "$details" ]]; then printf '\n' cat -- "$details" fi } >"$error_tmp" if ! ln -- "$error_tmp" "${target}.error.txt"; then printf 'Could not publish error sidecar for %s\n' "$target" >&2 rm -f -- "$error_tmp" return 1 fi rm -f -- "$error_tmp" printf 'Moved failed recording to %s\n' "$target" >&2 } recover_completed_recording() { local source=$1 local transcript=$2 local archive=$3 local state_file="${source}.transcribe-state" if [[ ! -f "$state_file" ]]; then return 1 fi if ! path_exists "$transcript"; then rm -f -- "$state_file" return 1 fi if ! move_no_clobber "$source" "$archive"; then return 2 fi rm -f -- "$state_file" printf 'Recovered completed transcription and archived %s\n' "$archive" return 0 } process_recording() ( set -o errexit local source=$1 local inbox_dir=$2 local transcript_dir=$3 local archive_dir=$4 local failed_dir=$5 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 diarization_succeeded=false base=$(basename "$source") stem=${base%.*} transcript="$transcript_dir/$stem.md" archive="$archive_dir/$base" original_path="$inbox_dir/$base" state_file="${source}.transcribe-state" if recover_completed_recording "$source" "$transcript" "$archive"; then return 0 else recovery_status=$? fi if [[ $recovery_status -eq 2 ]]; then fail_recording "$source" "$failed_dir" "Archive collision after transcription: $archive" return 1 fi if path_exists "$transcript"; then fail_recording "$source" "$failed_dir" "Transcript already exists: $transcript" return 1 fi if path_exists "$archive"; then fail_recording "$source" "$failed_dir" "Archived recording already exists: $archive" return 1 fi recorded_at=$(date --date="@$(stat -c %Y -- "$source")" --iso-8601=seconds) transcribed_at=$(date --utc --iso-8601=seconds) work_dir=$(mktemp -d "$runtime_dir/work.XXXXXX") trap 'rm -r -- "$work_dir"' EXIT log_file="$work_dir/transcription.log" raw_output="$work_dir/transcript" 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 \ -nostdin \ -hide_banner \ -loglevel error \ -i "$source" \ -vn \ -ac 1 \ -ar 16000 \ -c:a pcm_s16le \ "$work_dir/audio.wav" >"$log_file" 2>&1; then fail_recording "$source" "$failed_dir" "FFmpeg conversion failed" "$log_file" return 1 fi if ! whisper-cli \ --model "$TRANSCRIBE_MODEL" \ --file "$work_dir/audio.wav" \ --language auto \ --output-json-full \ --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" return 1 fi if [[ -f "$TRANSCRIBE_DIARIZATION_MODEL/.complete" && -f "$TRANSCRIBE_DIARIZATION_MODEL/config.yaml" ]] \ && diarize-transcript \ --audio "$work_dir/audio.wav" \ --model "$TRANSCRIBE_DIARIZATION_MODEL" \ --transcript "${raw_output}.json" \ --output "$rendered_output" >>"$log_file" 2>&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 draft=$(mktemp "$transcript_dir/.transcript.XXXXXX") { printf '# %s\n\n' "$stem" printf -- '- Original recording: %s\n' "$original_path" printf -- '- Recording timestamp: %s\n' "$recorded_at" printf -- '- Transcription timestamp: %s\n' "$transcribed_at" 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 SHA-256: %s\n' "$TRANSCRIBE_MODEL_SHA256" if [[ $diarization_succeeded == true ]]; then printf -- '- Diarization engine: pyannote.audio %s (CPU)\n' "$TRANSCRIBE_DIARIZATION_ENGINE_VERSION" 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" } >"$draft" chmod 0644 "$draft" touch -- "$state_file" if ! ln -- "$draft" "$transcript"; then rm -f -- "$draft" "$state_file" fail_recording "$source" "$failed_dir" "Transcript appeared while transcription was running: $transcript" "$log_file" return 1 fi rm -f -- "$draft" if ! move_no_clobber "$source" "$archive"; then printf 'Could not archive %s; leaving recoverable state in processing\n' "$source" >&2 return 1 fi rm -f -- "$state_file" printf 'Transcribed %s to %s\n' "$archive" "$transcript" ) process_scope() { local recordings_dir=$1 local transcript_dir=$2 local inbox_dir="$recordings_dir/inbox" local processing_dir="$recordings_dir/processing" local archive_dir="$recordings_dir/archive" local failed_dir="$recordings_dir/failed" local source processing_target recording_status signature index scope_status=0 local -a sources signatures mkdir -p "$inbox_dir" "$processing_dir" "$archive_dir" "$failed_dir" "$transcript_dir" || return 1 while IFS= read -r -d '' source; do if is_recording "$source"; then process_recording "$source" "$inbox_dir" "$transcript_dir" "$archive_dir" "$failed_dir" recording_status=$? if [[ $recording_status -ne 0 ]]; then scope_status=1 fi fi done < <(find "$processing_dir" -maxdepth 1 -type f -print0 | sort -z) while true; do sources=() 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 sleep "${TRANSCRIBE_SETTLE_SECONDS:-300}" for index in "${!sources[@]}"; do source=${sources[$index]} if [[ ! -f "$source" ]] || [[ $(stat -c '%s:%y:%i' -- "$source") != "${signatures[$index]}" ]]; then continue fi processing_target="$processing_dir/$(basename "$source")" if ! move_no_clobber "$source" "$processing_target"; then fail_recording "$source" "$failed_dir" "Processing filename already exists: $processing_target" 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" } runtime_dir="${XDG_RUNTIME_DIR:?}/transcribe-recordings" mkdir -p "$runtime_dir" set +o errexit process_scope "$TRANSCRIBE_SYNC_ROOT/recordings" "$TRANSCRIBE_SYNC_ROOT/notes/transcripts"