Refactor analysis input building
Extract the logic for building `AnalysisInput` into a new function `build_analysis_input`. This function encapsulates the process of collecting recordings, reading transcripts, and determining the last recording modification time. This change improves code organization and reusability by centralizing the input building logic.
This commit is contained in:
@@ -25,6 +25,12 @@ pub struct AuthenticatedUser {
|
|||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuthenticatedUser {
|
||||||
|
pub fn is_admin(&self) -> bool {
|
||||||
|
self.role == "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for AuthenticatedUser
|
impl<S> FromRequestParts<S> for AuthenticatedUser
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
@@ -72,6 +78,12 @@ pub struct AuthenticatedWebUser {
|
|||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuthenticatedWebUser {
|
||||||
|
pub fn is_admin(&self) -> bool {
|
||||||
|
self.role == "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for AuthenticatedWebUser
|
impl<S> FromRequestParts<S> for AuthenticatedWebUser
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
|
|||||||
@@ -60,56 +60,7 @@ pub async fn handle_close_case(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all active `.m4a` recordings and the transcripts next to them.
|
let input = build_analysis_input(&case_dir, 1).await?;
|
||||||
// `.m4a.failed` entries are deliberately skipped: they block nothing and
|
|
||||||
// carry no transcript. The doctor handles them manually.
|
|
||||||
let m4as = collect_m4as(&case_dir).await?;
|
|
||||||
if m4as.is_empty() {
|
|
||||||
return Err(AppError::BadRequest("Keine Aufnahmen im Fall".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut recordings: Vec<RecordingInput> = Vec::new();
|
|
||||||
let mut last_mtime: SystemTime = SystemTime::UNIX_EPOCH;
|
|
||||||
for (m4a_path, mtime) in &m4as {
|
|
||||||
if *mtime > last_mtime {
|
|
||||||
last_mtime = *mtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
let transcript_path = m4a_path.with_extension("transcript.txt");
|
|
||||||
let text = tokio::fs::read_to_string(&transcript_path)
|
|
||||||
.await
|
|
||||||
.map_err(|_| {
|
|
||||||
AppError::BadRequest("Nicht alle Aufnahmen sind transkribiert".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if text.trim().is_empty() {
|
|
||||||
// Skip blank transcripts from the LLM input — they add noise
|
|
||||||
// without signal. The `.m4a` still contributes to `last_mtime`.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stem = m4a_path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
|
||||||
recordings.push(RecordingInput {
|
|
||||||
recorded_at: filename_stem_to_recorded_at(stem),
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if recordings.is_empty() {
|
|
||||||
return Err(AppError::BadRequest(
|
|
||||||
"Keine nicht-leeren Transkripte im Fall".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_recording_mtime = OffsetDateTime::from(last_mtime)
|
|
||||||
.format(&Rfc3339)
|
|
||||||
.map_err(|e| AppError::Internal(format!("format mtime: {e}")))?;
|
|
||||||
|
|
||||||
let input = AnalysisInput {
|
|
||||||
version: 1,
|
|
||||||
last_recording_mtime,
|
|
||||||
recordings,
|
|
||||||
};
|
|
||||||
|
|
||||||
write_input_create_new(&input_path, &input).await?;
|
write_input_create_new(&input_path, &input).await?;
|
||||||
|
|
||||||
@@ -171,6 +122,62 @@ pub async fn handle_document_view(
|
|||||||
.map_err(|e| AppError::Internal(format!("Template render failed: {e}")))
|
.map_err(|e| AppError::Internal(format!("Template render failed: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build an `AnalysisInput` for the given case directory at the given version.
|
||||||
|
/// Collects all `.m4a` (excluding `.m4a.failed`), reads the matching
|
||||||
|
/// `.transcript.txt` 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,
|
||||||
|
version: u32,
|
||||||
|
) -> Result<AnalysisInput, AppError> {
|
||||||
|
let m4as = collect_m4as(case_dir).await?;
|
||||||
|
if m4as.is_empty() {
|
||||||
|
return Err(AppError::BadRequest("Keine Aufnahmen im Fall".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recordings: Vec<RecordingInput> = Vec::new();
|
||||||
|
let mut last_mtime: SystemTime = SystemTime::UNIX_EPOCH;
|
||||||
|
for (m4a_path, mtime) in &m4as {
|
||||||
|
if *mtime > last_mtime {
|
||||||
|
last_mtime = *mtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transcript_path = m4a_path.with_extension("transcript.txt");
|
||||||
|
let text = tokio::fs::read_to_string(&transcript_path)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
AppError::BadRequest("Nicht alle Aufnahmen sind transkribiert".into())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if text.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stem = m4a_path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||||
|
recordings.push(RecordingInput {
|
||||||
|
recorded_at: filename_stem_to_recorded_at(stem),
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordings.is_empty() {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Keine nicht-leeren Transkripte im Fall".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_recording_mtime = OffsetDateTime::from(last_mtime)
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.map_err(|e| AppError::Internal(format!("format mtime: {e}")))?;
|
||||||
|
|
||||||
|
Ok(AnalysisInput {
|
||||||
|
version,
|
||||||
|
last_recording_mtime,
|
||||||
|
recordings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `(path, mtime)` for every `.m4a` file directly under `case_dir`,
|
/// Return `(path, mtime)` for every `.m4a` file directly under `case_dir`,
|
||||||
/// sorted by filename (chronological, since filenames are UTC timestamps).
|
/// sorted by filename (chronological, since filenames are UTC timestamps).
|
||||||
/// `.m4a.failed` entries are skipped.
|
/// `.m4a.failed` entries are skipped.
|
||||||
@@ -201,7 +208,7 @@ async fn collect_m4as(case_dir: &Path) -> Result<Vec<(PathBuf, SystemTime)>, App
|
|||||||
/// a zero-byte or partially-written JSON remains. The worker will fail to
|
/// a zero-byte or partially-written JSON remains. The worker will fail to
|
||||||
/// deserialize it and log an error — manual cleanup required. For MVP the
|
/// deserialize it and log an error — manual cleanup required. For MVP the
|
||||||
/// simpler check-and-create is acceptable.
|
/// simpler check-and-create is acceptable.
|
||||||
async fn write_input_create_new(
|
pub(crate) async fn write_input_create_new(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
input: &AnalysisInput,
|
input: &AnalysisInput,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
@@ -239,7 +246,7 @@ fn filename_stem_to_recorded_at(stem: &str) -> String {
|
|||||||
|
|
||||||
/// Scan `case_dir` for `document_v{N}.md`, pick the highest N, return its
|
/// Scan `case_dir` for `document_v{N}.md`, pick the highest N, return its
|
||||||
/// content together with N. Returns `None` if no document exists.
|
/// content together with N. Returns `None` if no document exists.
|
||||||
async fn find_latest_document(case_dir: &Path) -> Option<(u32, String)> {
|
pub(crate) async fn find_latest_document(case_dir: &Path) -> Option<(u32, String)> {
|
||||||
let mut highest: Option<u32> = None;
|
let mut highest: Option<u32> = None;
|
||||||
let mut entries = tokio::fs::read_dir(case_dir).await.ok()?;
|
let mut entries = tokio::fs::read_dir(case_dir).await.ok()?;
|
||||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
|
|||||||
Reference in New Issue
Block a user