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:
@@ -11,8 +11,8 @@ use time::format_description::well_known::Rfc3339;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use doctate_common::TranscriptState;
|
||||
use doctate_common::timestamp::{filename_stem_to_recorded_at, now_rfc3339};
|
||||
use doctate_common::{RECORDING_META_SUFFIX, TranscriptState};
|
||||
|
||||
use crate::analyze::{
|
||||
ANALYSIS_INPUT_FILE, AnalysisInput, AnalyzeJob, AnalyzeSender, DOCUMENT_FILE, RecordingInput,
|
||||
@@ -171,7 +171,7 @@ fn resolve_list_return_path(headers: &HeaderMap) -> String {
|
||||
|
||||
/// Build an `AnalysisInput` for the given case directory.
|
||||
/// Collects all `.m4a` (excluding `.m4a.failed`), reads the matching
|
||||
/// `.transcript.txt` for each, skips blank transcripts, and computes
|
||||
/// `<stem>.json` for each, skips blank transcripts, and computes
|
||||
/// `last_recording_mtime` as the max mtime over ALL `.m4a` (including blank
|
||||
/// ones — a late blank addendum still counts as activity).
|
||||
pub(crate) async fn build_analysis_input(case_dir: &Path) -> Result<AnalysisInput, AppError> {
|
||||
@@ -225,7 +225,7 @@ async fn read_recordings(m4as: &[(PathBuf, SystemTime)]) -> Result<Vec<Recording
|
||||
));
|
||||
}
|
||||
TranscriptState::Silent => continue,
|
||||
TranscriptState::Content(t) => t,
|
||||
TranscriptState::Content { text } => text,
|
||||
};
|
||||
|
||||
let stem = m4a_path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
@@ -322,12 +322,27 @@ pub(crate) async fn reset_case_artefacts(
|
||||
}
|
||||
delete_oneliner_unless_manual(case_dir, locks).await?;
|
||||
let mut entries = tokio::fs::read_dir(case_dir).await?;
|
||||
let meta_suffix = format!(".{RECORDING_META_SUFFIX}");
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let name = entry.file_name();
|
||||
let Some(s) = name.to_str() else { continue };
|
||||
if s.ends_with(".transcript.txt") {
|
||||
tokio::fs::remove_file(entry.path()).await?;
|
||||
} else if s.ends_with(".m4a.failed") {
|
||||
// Per-recording metadata sidecar: paired with `<stem>.m4a` or
|
||||
// its `.failed` twin. The pair check guards case-level JSONs
|
||||
// (preserved Manual oneliner, future siblings) from being
|
||||
// misidentified as recording artefacts.
|
||||
if let Some(stem) = s.strip_suffix(&meta_suffix) {
|
||||
let paired = tokio::fs::try_exists(case_dir.join(format!("{stem}.m4a")))
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
|| tokio::fs::try_exists(case_dir.join(format!("{stem}.m4a.failed")))
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if paired {
|
||||
tokio::fs::remove_file(entry.path()).await?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if s.ends_with(".m4a.failed") {
|
||||
let new_name = &s[..s.len() - ".failed".len()];
|
||||
tokio::fs::rename(entry.path(), case_dir.join(new_name)).await?;
|
||||
}
|
||||
@@ -601,13 +616,13 @@ pub async fn handle_delete_recording(
|
||||
.trim_end_matches(".failed")
|
||||
.trim_end_matches(".m4a");
|
||||
|
||||
// Audio and its sidecars. NotFound on any of these is fine: the
|
||||
// transcript / duration may never have been written (silence, failed
|
||||
// job, or the file got deleted twice).
|
||||
let targets: [PathBuf; 5] = [
|
||||
// Audio and its single metadata sidecar. NotFound on any of these
|
||||
// is fine: the JSON sidecar may never have been written (the
|
||||
// worker hadn't reached the atomic write yet, or the file got
|
||||
// deleted twice).
|
||||
let targets: [PathBuf; 4] = [
|
||||
case_dir.join(&form.filename),
|
||||
case_dir.join(format!("{stem}.transcript.txt")),
|
||||
case_dir.join(format!("{stem}.duration.txt")),
|
||||
case_dir.join(format!("{stem}.{RECORDING_META_SUFFIX}")),
|
||||
case_dir.join(DOCUMENT_FILE),
|
||||
case_dir.join(ANALYSIS_INPUT_FILE),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user