Refactor recording metadata to JSON sidecar

Replaces the `.transcript.txt` sidecar with a structured `.json` file
for recording metadata. This change consolidates transcript text,
duration, and other potential metadata into a single, extensible JSON
object.

This also refactors the `TranscriptState` enum to better represent the
on-disk state (absence of file means pending) and the in-memory
representation. The `Transcript` enum now specifically models the
terminal outcomes of the transcriber (`Silent` or `Content`).

The commit includes updates to documentation, data structures, path
handling, and various tests to align with the new metadata format.
This commit is contained in:
2026-04-27 12:48:25 +02:00
parent a510c20e75
commit 66b3b7e4c8
20 changed files with 637 additions and 335 deletions
+13 -11
View File
@@ -136,10 +136,8 @@ async fn transient_whisper_failure_is_reenqueued_and_recovers() {
".m4a.failed must not exist after transient error"
);
assert!(
!case_dir
.join("2026-04-15T20-30-00Z.transcript.txt")
.exists(),
"no transcript yet — the 503 produced nothing"
!case_dir.join("2026-04-15T20-30-00Z.json").exists(),
"no metadata sidecar yet — the 503 produced nothing"
);
// Act 2: heal re-enqueues the pending .m4a (second mock → 200).
@@ -154,22 +152,26 @@ async fn transient_whisper_failure_is_reenqueued_and_recovers() {
)
.await;
// Assert: the transcript lands. Poll because the worker runs
// asynchronously after the heal hands off the job.
let transcript = case_dir.join("2026-04-15T20-30-00Z.transcript.txt");
// Assert: the recording metadata sidecar lands. Poll because the
// worker runs asynchronously after the heal hands off the job.
let meta_path = case_dir.join("2026-04-15T20-30-00Z.json");
let start = std::time::Instant::now();
while !transcript.exists() {
while !meta_path.exists() {
if start.elapsed() > Duration::from_secs(10) {
panic!(
"transcript did not appear within 10s (whisper hits: {})",
"meta sidecar did not appear within 10s (whisper hits: {})",
whisper.received_requests().await.unwrap().len()
);
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
let body = std::fs::read_to_string(&transcript).unwrap();
assert_eq!(body, "recovered text");
let bytes = std::fs::read(&meta_path).unwrap();
let meta: doctate_common::RecordingMeta = serde_json::from_slice(&bytes).unwrap();
match meta.transcript {
doctate_common::Transcript::Content { text } => assert_eq!(text, "recovered text"),
other => panic!("expected Transcript::Content, got {other:?}"),
}
// Teardown: close the worker channel so the background task exits.
drop(tx_t);