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
+39 -21
View File
@@ -318,6 +318,31 @@ mod tests {
fs::write(path, b"").await.unwrap();
}
/// Seed a `<stem>.json` next to a (presumed already-touched)
/// `<stem>.m4a`. Variant is `Transcript::Silent` because these
/// tests historically used an empty `.transcript.txt`, which
/// mapped to `TranscriptState::Silent`. Tests that need real
/// content call `seed_meta_with` with the desired variant.
async fn seed_meta(case_dir: &std::path::Path, stem: &str) {
crate::paths::write_recording_meta_sync(
case_dir,
stem,
doctate_common::Transcript::Silent,
None,
);
}
async fn seed_meta_with(case_dir: &std::path::Path, stem: &str, text: &str) {
crate::paths::write_recording_meta_sync(
case_dir,
stem,
doctate_common::Transcript::Content {
text: text.to_owned(),
},
None,
);
}
fn set_mtime(path: &std::path::Path, t: SystemTime) {
set_file_mtime(path, FileTime::from_system_time(t)).unwrap();
}
@@ -335,7 +360,7 @@ mod tests {
async fn missing_transcript_skips() {
let dir = TempDir::new().unwrap();
touch(&dir.path().join("2026-01-01T10-00-00Z.m4a")).await;
touch(&dir.path().join("2026-01-01T10-00-00Z.transcript.txt")).await;
seed_meta(dir.path(), "2026-01-01T10-00-00Z").await;
touch(&dir.path().join("2026-01-01T11-00-00Z.m4a")).await;
assert_eq!(
evaluate_case(dir.path()).await,
@@ -347,7 +372,7 @@ mod tests {
async fn all_transcribed_no_document_enqueues() {
let dir = TempDir::new().unwrap();
touch(&dir.path().join("2026-01-01T10-00-00Z.m4a")).await;
touch(&dir.path().join("2026-01-01T10-00-00Z.transcript.txt")).await;
seed_meta(dir.path(), "2026-01-01T10-00-00Z").await;
assert_eq!(evaluate_case(dir.path()).await, AutoDecision::Enqueue);
}
@@ -356,7 +381,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let m4a = dir.path().join("2026-01-01T10-00-00Z.m4a");
touch(&m4a).await;
touch(&dir.path().join("2026-01-01T10-00-00Z.transcript.txt")).await;
seed_meta(dir.path(), "2026-01-01T10-00-00Z").await;
let doc = dir.path().join(DOCUMENT_FILE);
touch(&doc).await;
@@ -375,7 +400,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let m4a = dir.path().join("2026-01-01T10-00-00Z.m4a");
touch(&m4a).await;
touch(&dir.path().join("2026-01-01T10-00-00Z.transcript.txt")).await;
seed_meta(dir.path(), "2026-01-01T10-00-00Z").await;
let doc = dir.path().join(DOCUMENT_FILE);
touch(&doc).await;
@@ -390,7 +415,7 @@ mod tests {
async fn analysis_input_present_skips() {
let dir = TempDir::new().unwrap();
touch(&dir.path().join("2026-01-01T10-00-00Z.m4a")).await;
touch(&dir.path().join("2026-01-01T10-00-00Z.transcript.txt")).await;
seed_meta(dir.path(), "2026-01-01T10-00-00Z").await;
touch(&dir.path().join(ANALYSIS_INPUT_FILE)).await;
assert_eq!(
evaluate_case(dir.path()).await,
@@ -403,7 +428,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let m4a = dir.path().join("2026-01-01T10-00-00Z.m4a");
touch(&m4a).await;
touch(&dir.path().join("2026-01-01T10-00-00Z.transcript.txt")).await;
seed_meta(dir.path(), "2026-01-01T10-00-00Z").await;
let base = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
set_mtime(&m4a, base);
@@ -431,7 +456,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let m4a = dir.path().join("2026-01-01T10-00-00Z.m4a");
touch(&m4a).await;
touch(&dir.path().join("2026-01-01T10-00-00Z.transcript.txt")).await;
seed_meta(dir.path(), "2026-01-01T10-00-00Z").await;
let base = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
set_mtime(&m4a, base);
@@ -478,12 +503,12 @@ mod tests {
fs::write(eligible.join("2026-01-01T10-00-00Z.m4a"), b"fake")
.await
.unwrap();
fs::write(
eligible.join("2026-01-01T10-00-00Z.transcript.txt"),
b"patient mit brustschmerz",
seed_meta_with(
&eligible,
"2026-01-01T10-00-00Z",
"patient mit brustschmerz",
)
.await
.unwrap();
.await;
// Not eligible: has a fresh document.
let current_id = "22222222-2222-2222-2222-222222222222";
@@ -491,12 +516,7 @@ mod tests {
fs::create_dir_all(&current).await.unwrap();
let m4a = current.join("2026-01-01T10-00-00Z.m4a");
fs::write(&m4a, b"fake").await.unwrap();
fs::write(
current.join("2026-01-01T10-00-00Z.transcript.txt"),
b"stable",
)
.await
.unwrap();
seed_meta_with(&current, "2026-01-01T10-00-00Z", "stable").await;
let doc = current.join(DOCUMENT_FILE);
fs::write(&doc, b"doc").await.unwrap();
let base = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
@@ -509,9 +529,7 @@ mod tests {
fs::write(junk.join("2026-01-01T10-00-00Z.m4a"), b"fake")
.await
.unwrap();
fs::write(junk.join("2026-01-01T10-00-00Z.transcript.txt"), b"x")
.await
.unwrap();
seed_meta_with(&junk, "2026-01-01T10-00-00Z", "x").await;
let (tx, mut rx) = analyze_channel();
let events_tx = events_channel();