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:
@@ -1,11 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use doctate_server::config::{Config, User, WhisperUserSettings};
|
||||
use doctate_server::transcribe;
|
||||
use doctate_server::transcribe::ffmpeg::remux_faststart;
|
||||
use doctate_server::transcribe::ollama::{generate_oneliner, OllamaError};
|
||||
use doctate_server::transcribe::recovery::scan_and_enqueue;
|
||||
use doctate_server::config::WhisperUserSettings;
|
||||
use doctate_server::transcribe::whisper::{transcribe, WhisperError};
|
||||
use serde_json::json;
|
||||
use wiremock::matchers::{body_partial_json, method, path, query_param};
|
||||
@@ -242,6 +245,75 @@ async fn recovery_enqueues_only_pending_recordings() {
|
||||
assert!(received[1].ends_with("2026-04-12T10-00-00Z.m4a"));
|
||||
}
|
||||
|
||||
// -------------------- Worker: failure marker --------------------
|
||||
|
||||
fn test_config_with_whisper(whisper_url: String) -> Arc<Config> {
|
||||
Arc::new(Config {
|
||||
server_port: 3000,
|
||||
data_path: "/tmp".into(),
|
||||
log_level: "info".into(),
|
||||
log_path: "/tmp".into(),
|
||||
log_max_days: 90,
|
||||
users: vec![User {
|
||||
slug: "dr_test".into(),
|
||||
api_key: "k".into(),
|
||||
web_password: "unused".into(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
}],
|
||||
api_keys: HashMap::new(),
|
||||
retention_audio_days: 30,
|
||||
retention_transcript_days: 30,
|
||||
retention_document_days: 0,
|
||||
whisper_url,
|
||||
whisper_timeout_seconds: 5,
|
||||
// Ollama URL is irrelevant — worker never reaches it in the failure path.
|
||||
ollama_url: "http://127.0.0.1:1".into(),
|
||||
ollama_model: "test".into(),
|
||||
ollama_keep_alive: 0,
|
||||
llm_url: String::new(),
|
||||
llm_api_key: String::new(),
|
||||
llm_model: String::new(),
|
||||
llm_temperature: 0.0,
|
||||
session_timeout_hours: 8,
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_renames_audio_to_failed_on_whisper_error() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/asr"))
|
||||
.respond_with(ResponseTemplate::new(500).set_body_string("boom"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Real case dir with a real m4a fixture so ffmpeg remux succeeds.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let case_dir = tmp.path().join("dr_test/open/aaaa");
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
let audio = case_dir.join("2026-04-13T10-30-00Z.m4a");
|
||||
std::fs::copy(fixture("sample.m4a"), &audio).unwrap();
|
||||
|
||||
let config = test_config_with_whisper(server.uri());
|
||||
let (tx, rx) = transcribe::channel();
|
||||
tx.send(transcribe::TranscribeJob {
|
||||
audio_path: audio.clone(),
|
||||
user_slug: "dr_test".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
drop(tx); // close channel so the worker loop exits after processing the job
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
transcribe::worker::run(rx, config, client).await;
|
||||
|
||||
// Audio must have been renamed so recovery skips it next time.
|
||||
assert!(!audio.exists(), "original .m4a still present");
|
||||
let failed = case_dir.join("2026-04-13T10-30-00Z.m4a.failed");
|
||||
assert!(failed.exists(), "expected {} to exist", failed.display());
|
||||
}
|
||||
|
||||
// -------------------- Ollama client --------------------
|
||||
|
||||
const MODEL: &str = "gemma3:4b";
|
||||
|
||||
Reference in New Issue
Block a user