diff --git a/server/src/routes/user_web.rs b/server/src/routes/user_web.rs index 9b6cc3e..82c7585 100644 --- a/server/src/routes/user_web.rs +++ b/server/src/routes/user_web.rs @@ -322,7 +322,9 @@ pub async fn handle_case_page( let is_admin = user.is_admin(); let most_recent = recordings.last().map(|r| r.filename.as_str()); - let recorded_at_iso = most_recent.map(recorded_at_iso_of).filter(|s| !s.is_empty()); + let recorded_at_iso = most_recent + .map(recorded_at_iso_of) + .filter(|s| !s.is_empty()); let time_hms_utc = most_recent.map(extract_hhmm_utc).unwrap_or_default(); CasePageTemplate { diff --git a/server/src/transcribe/ollama.rs b/server/src/transcribe/ollama.rs index bc26434..cef2725 100644 --- a/server/src/transcribe/ollama.rs +++ b/server/src/transcribe/ollama.rs @@ -30,8 +30,15 @@ const MAX_CHARS: usize = 60; #[derive(Debug)] pub enum OllamaError { Http(reqwest::Error), - Status { status: u16, body: String }, + Status { + status: u16, + body: String, + }, Parse(String), + /// Model obeyed the silence rule: transcript had no extractable keyword, + /// so the response was empty. Not a failure — callers should treat this + /// as "no oneliner this round" and log below error level. + EmptyResponse, } impl std::fmt::Display for OllamaError { @@ -40,6 +47,10 @@ impl std::fmt::Display for OllamaError { Self::Http(e) => write!(f, "ollama http error: {e}"), Self::Status { status, body } => write!(f, "ollama returned {status}: {body}"), Self::Parse(msg) => write!(f, "ollama response parse error: {msg}"), + Self::EmptyResponse => write!( + f, + "ollama response was empty (no medical keyword in transcript)" + ), } } } @@ -132,7 +143,7 @@ pub async fn generate_oneliner( let normalized = normalize(&content); if normalized.is_empty() { - return Err(OllamaError::Parse("empty after normalization".into())); + return Err(OllamaError::EmptyResponse); } Ok(normalized) } diff --git a/server/src/transcribe/worker.rs b/server/src/transcribe/worker.rs index 05b1977..a68f941 100644 --- a/server/src/transcribe/worker.rs +++ b/server/src/transcribe/worker.rs @@ -229,6 +229,12 @@ pub(crate) async fn update_oneliner( ); } } + Err(ollama::OllamaError::EmptyResponse) => { + warn!( + case = %case_dir.display(), + "oneliner skipped — model returned empty (no medical keyword in transcript)" + ); + } Err(e) => { error!(case = %case_dir.display(), error = %e, "oneliner generation failed"); } diff --git a/server/tests/transcribe_test.rs b/server/tests/transcribe_test.rs index 1442c99..71e1d7a 100644 --- a/server/tests/transcribe_test.rs +++ b/server/tests/transcribe_test.rs @@ -500,6 +500,31 @@ async fn ollama_oneliner_sends_keep_alive_and_model() { // Mock::expect(1) + server drop verifies the matcher hit exactly once. } +#[tokio::test] +async fn ollama_oneliner_empty_response_when_model_obeys_silence_rule() { + // Model followed the silence rule for a keyword-less transcript and + // returned empty content. That's a valid outcome, not a parse error. + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/chat")) + .respond_with(ResponseTemplate::new(200).set_body_json(ok_chat_body(" \n "))) + .mount(&server) + .await; + + let client = reqwest::Client::new(); + let err = generate_oneliner( + &client, + &server.uri(), + MODEL, + 0, + "äh, Moment", + Duration::from_secs(5), + ) + .await + .expect_err("expected EmptyResponse"); + assert!(matches!(err, OllamaError::EmptyResponse), "got {err:?}"); +} + #[tokio::test] async fn ollama_oneliner_parse_error_on_missing_content() { let server = MockServer::start().await;