feat: Add recovery scan for pending recordings

This commit introduces a new module `transcribe::recovery` and a
function `scan_and_enqueue`.
This function is called at startup to find audio files that have not yet
been transcribed
and re-enqueues them for processing. This ensures that recordings are
not lost if the
server restarts before transcription is complete.

A new integration test `recovery_enqueues_only_pending_recordings` has
been added to
verify the functionality.
This commit is contained in:
2026-04-13 16:47:38 +02:00
parent bcae4e5466
commit 13375b8297
4 changed files with 122 additions and 0 deletions
+39
View File
@@ -1,7 +1,9 @@
use std::path::PathBuf;
use std::time::Duration;
use doctate_server::transcribe;
use doctate_server::transcribe::ffmpeg::remux_faststart;
use doctate_server::transcribe::recovery::scan_and_enqueue;
use doctate_server::transcribe::whisper::{transcribe, WhisperError};
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
@@ -116,3 +118,40 @@ async fn whisper_client_times_out() {
assert!(matches!(err, WhisperError::Http(_)), "expected Http error, got {err:?}");
}
#[tokio::test]
async fn recovery_enqueues_only_pending_recordings() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
// User 1, open case with two .m4a — one pending, one already transcribed.
let case_a = root.join("dr_a/open/aaaa");
std::fs::create_dir_all(&case_a).unwrap();
std::fs::write(case_a.join("2026-04-10T10-00-00Z.m4a"), b"x").unwrap();
std::fs::write(case_a.join("2026-04-11T10-00-00Z.m4a"), b"x").unwrap();
std::fs::write(case_a.join("2026-04-11T10-00-00Z.transcript.txt"), b"done").unwrap();
// User 2, open case with one pending .m4a.
let case_b = root.join("dr_b/open/bbbb");
std::fs::create_dir_all(&case_b).unwrap();
std::fs::write(case_b.join("2026-04-12T10-00-00Z.m4a"), b"x").unwrap();
// done/ cases must be ignored (not re-transcribed).
let case_c = root.join("dr_a/done/cccc");
std::fs::create_dir_all(&case_c).unwrap();
std::fs::write(case_c.join("2026-04-09T10-00-00Z.m4a"), b"x").unwrap();
let (tx, mut rx) = transcribe::channel();
scan_and_enqueue(root, &tx).await;
drop(tx); // close channel so the loop below terminates
let mut received = Vec::new();
while let Some(job) = rx.recv().await {
received.push(job.audio_path);
}
assert_eq!(received.len(), 2, "got: {received:?}");
// Oldest first:
assert!(received[0].ends_with("2026-04-10T10-00-00Z.m4a"));
assert!(received[1].ends_with("2026-04-12T10-00-00Z.m4a"));
}