feat: Mark failed recordings with .m4a.failed
Add a new `failed` field to `RecordingView` and corresponding logic in the worker. When FFmpeg remuxing or Whisper transcription fails, the worker now renames the original `.m4a` file to `.m4a.failed`. This prevents the recovery scan from reprocessing these files and signals to the UI that transcription failed. The UI displays a message indicating failure and suggests renaming the file back to `.m4a` to retry. A new integration test `worker_renames_audio_to_failed_on_whisper_error` is included to verify this behavior.
This commit is contained in:
@@ -14,6 +14,9 @@ use crate::error::AppError;
|
||||
struct RecordingView {
|
||||
filename: String,
|
||||
transcript: Option<String>,
|
||||
/// True if the file has been renamed to `<ts>.m4a.failed` by the worker
|
||||
/// after a non-recoverable ffmpeg or whisper error.
|
||||
failed: bool,
|
||||
}
|
||||
|
||||
struct CaseView {
|
||||
@@ -80,7 +83,8 @@ fn validate_user_slug(user: &str) -> Result<(), AppError> {
|
||||
}
|
||||
|
||||
fn validate_filename(filename: &str) -> Result<(), AppError> {
|
||||
if !filename.ends_with(".m4a")
|
||||
let ok_suffix = filename.ends_with(".m4a") || filename.ends_with(".m4a.failed");
|
||||
if !ok_suffix
|
||||
|| filename.contains('/')
|
||||
|| filename.contains('\\')
|
||||
|| filename.contains("..")
|
||||
@@ -178,12 +182,21 @@ async fn scan_recordings(case_dir: &Path) -> Vec<RecordingView> {
|
||||
Ok(s) => s,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if !filename.ends_with(".m4a") {
|
||||
let failed = filename.ends_with(".m4a.failed");
|
||||
let is_audio = filename.ends_with(".m4a") || failed;
|
||||
if !is_audio {
|
||||
continue;
|
||||
}
|
||||
let transcript_path = case_dir.join(&filename).with_extension("transcript.txt");
|
||||
// Transcript naming uses the original `<ts>.m4a` stem in both cases
|
||||
// (failed recordings never produced one, but the lookup still works).
|
||||
let stem_path = case_dir.join(filename.trim_end_matches(".failed"));
|
||||
let transcript_path = stem_path.with_extension("transcript.txt");
|
||||
let transcript = tokio::fs::read_to_string(&transcript_path).await.ok();
|
||||
recordings.push(RecordingView { filename, transcript });
|
||||
recordings.push(RecordingView {
|
||||
filename,
|
||||
transcript,
|
||||
failed,
|
||||
});
|
||||
}
|
||||
|
||||
recordings.sort_by(|a, b| a.filename.cmp(&b.filename));
|
||||
|
||||
Reference in New Issue
Block a user