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:
@@ -27,13 +27,15 @@ pub mod users;
|
||||
// Re-exports for ergonomic `use common::*;` in most test files.
|
||||
pub use artefacts::{ANALYSIS_INPUT_FILE, CLOSE_MARKER, DOCUMENT_FILE, ONELINER_FILENAME};
|
||||
pub use config::{TestConfig, unique_tmpdir};
|
||||
pub use doctate_common::Transcript;
|
||||
pub use http::{
|
||||
body_bytes, body_json, body_string, csrf_form_post, form_post, get_with_api_key,
|
||||
get_with_api_key_if_none_match, get_with_cookie, header_opt, header_str, multipart_upload_body,
|
||||
};
|
||||
pub use seed::{
|
||||
past_rfc3339, seed_case, seed_oneliner_ready, seed_oneliner_state, seed_recording,
|
||||
seed_recording_with_age, seed_recording_with_sidecars, write_closed_marker,
|
||||
seed_recording_meta, seed_recording_with_age, seed_recording_with_sidecars,
|
||||
write_closed_marker,
|
||||
};
|
||||
pub use session::{extract_session_cookie, login, login_request, login_with_csrf};
|
||||
pub use users::{
|
||||
|
||||
+54
-11
@@ -11,8 +11,8 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use doctate_common::ONELINER_FILENAME;
|
||||
use doctate_common::oneliners::OnelinerState;
|
||||
use doctate_common::{ONELINER_FILENAME, RECORDING_META_SUFFIX, RecordingMeta, Transcript};
|
||||
use doctate_server::paths::CLOSE_MARKER;
|
||||
use filetime::FileTime;
|
||||
|
||||
@@ -26,28 +26,71 @@ pub fn seed_case(data_path: &Path, slug: &str, case_id: &str) -> PathBuf {
|
||||
}
|
||||
|
||||
/// Write `<stem>.m4a` with placeholder bytes, plus optionally a
|
||||
/// `<stem>.transcript.txt`. `stem` must be the full RFC3339-like
|
||||
/// filename stem (e.g. `2026-04-15T10-00-00Z`); pass the same prefix
|
||||
/// you want to read back later. Returns the audio filename (no path)
|
||||
/// so the caller can feed it back to the delete endpoint.
|
||||
/// `<stem>.json` sidecar that mirrors the transcribe outcome.
|
||||
///
|
||||
/// `transcript`:
|
||||
/// - `None` → no sidecar written (recording stays in `Pending` state).
|
||||
/// - `Some(text)` with whitespace-only `text` → sidecar with
|
||||
/// `Transcript::Silent` (matches the old `transcript.txt = ""` rule).
|
||||
/// - `Some(text)` non-empty → sidecar with `Transcript::Content`.
|
||||
///
|
||||
/// Returns the audio filename (no path) so the caller can feed it back
|
||||
/// to the delete endpoint.
|
||||
pub fn seed_recording(case_dir: &Path, stem: &str, transcript: Option<&str>) -> String {
|
||||
let filename = format!("{stem}.m4a");
|
||||
std::fs::write(case_dir.join(&filename), b"audio-bytes").unwrap();
|
||||
if let Some(text) = transcript {
|
||||
std::fs::write(case_dir.join(format!("{stem}.transcript.txt")), text).unwrap();
|
||||
seed_recording_meta(case_dir, stem, transcript_for(text), None);
|
||||
}
|
||||
filename
|
||||
}
|
||||
|
||||
/// Like [`seed_recording`] but additionally writes the `.duration.txt`
|
||||
/// sidecar — used by the delete-recording tests that assert every
|
||||
/// sidecar is cleaned up.
|
||||
/// Like [`seed_recording`] but the sidecar carries a real duration
|
||||
/// (`duration_seconds: Some(42)`). Used by tests that assert the
|
||||
/// duration is rendered or cleaned up.
|
||||
pub fn seed_recording_with_sidecars(case_dir: &Path, stem: &str, transcript: &str) -> String {
|
||||
let filename = seed_recording(case_dir, stem, Some(transcript));
|
||||
std::fs::write(case_dir.join(format!("{stem}.duration.txt")), "42").unwrap();
|
||||
let filename = format!("{stem}.m4a");
|
||||
std::fs::write(case_dir.join(&filename), b"audio-bytes").unwrap();
|
||||
seed_recording_meta(case_dir, stem, transcript_for(transcript), Some(42));
|
||||
filename
|
||||
}
|
||||
|
||||
/// Write a fully-specified `<stem>.json` next to an already-seeded
|
||||
/// `<stem>.m4a`. Use when a test needs precise control over the
|
||||
/// transcript variant or duration; the higher-level helpers above
|
||||
/// cover the common cases.
|
||||
pub fn seed_recording_meta(
|
||||
case_dir: &Path,
|
||||
stem: &str,
|
||||
transcript: Transcript,
|
||||
duration_seconds: Option<u32>,
|
||||
) {
|
||||
let meta = RecordingMeta {
|
||||
transcript,
|
||||
duration_seconds,
|
||||
};
|
||||
let bytes = serde_json::to_vec(&meta).unwrap();
|
||||
std::fs::write(
|
||||
case_dir.join(format!("{stem}.{RECORDING_META_SUFFIX}")),
|
||||
bytes,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Map a free-text transcript to the `Transcript` variant the worker
|
||||
/// would have produced for it: whitespace-only → `Silent`, otherwise
|
||||
/// `Content`. Mirrors the old `transcript.txt = "" ⇒ Silent` rule so
|
||||
/// existing tests keep their meaning.
|
||||
fn transcript_for(text: &str) -> Transcript {
|
||||
if text.trim().is_empty() {
|
||||
Transcript::Silent
|
||||
} else {
|
||||
Transcript::Content {
|
||||
text: text.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write an m4a with an artificial `mtime` in the past (relative to
|
||||
/// now). Used by the retention-sweep tests to simulate "recorded N days
|
||||
/// ago" without a clock abstraction.
|
||||
|
||||
Reference in New Issue
Block a user