fix: separate transient from permanent transcribe failures
Before, every Whisper error (5xx, timeout, Minerva down, corrupt audio,
4xx) renamed `<ts>.m4a` to `<ts>.m4a.failed` uniformly, turning transient
outages into permanent sackgassen that only a manual admin reset could
undo. Cases whose every recording was `.m4a.failed` stuck without a
persisted oneliner state; the UI masked them via a fallback in
`compute_oneliner_display`.
The core insight: a recoverable error is not an error. Transient Whisper
failures now leave the audio as plain `.m4a` so the existing page-load
heal (`enqueue_pending_for_user`) re-enqueues it on the next refresh —
which is exactly what happens when Minerva comes back. No new sidecar,
no retry count, no scheduler.
Changes:
- `WhisperError::is_transient` classifies `Http`, 5xx, 408, 429 as
transient; `Io` and other 4xx as permanent.
- Transcribe worker: transient → info log + continue (audio stays .m4a);
permanent → `mark_failed` as before.
- `has_any_transcript` counts `.m4a.failed` as terminal, so
`update_oneliner` runs for failed-only cases and settles
`OnelinerState::Empty` (analogous to the silent-only fix in 4531f85).
- New verdrängender `fehler`-Badge (#c00) in case list and detail when
at least one `.m4a.failed` exists; recording-level message shortened
to plain "Transkription fehlgeschlagen".
Tests:
- New integration: transient 503 leaves `.m4a` intact, heal recovers.
- New integration: `.m4a.failed`-only case settles to
`OnelinerState::Empty` without calling Ollama.
- New unit: `is_transient` table test across relevant status codes.
- New unit: `has_any_transcript` returns true for `.m4a.failed`-only
case and false for pending-only case.
- Existing worker test retargeted from 500 to 400 and renamed; added
companion `worker_leaves_m4a_intact_on_transient_whisper_error`.
This commit is contained in:
@@ -71,6 +71,12 @@ struct UserCaseView {
|
||||
recordings_count: usize,
|
||||
analyzing: bool,
|
||||
has_document: bool,
|
||||
/// True iff at least one recording is `.m4a.failed` — a *permanent*
|
||||
/// transcribe failure (ffmpeg remux or Whisper 4xx). Transient Whisper
|
||||
/// errors never set this flag because they leave the file as plain
|
||||
/// `.m4a` for the page-load heal to retry. Drives the verdrängende
|
||||
/// `fehler`-Badge over `offen`/`ausgewertet`.
|
||||
has_failed_recording: bool,
|
||||
/// True iff the inline "Analysieren"-button should be enabled.
|
||||
/// Requires: not currently analyzing, all non-failed recordings
|
||||
/// transcribed, and an LLM is configured.
|
||||
@@ -213,6 +219,10 @@ struct CasePageTemplate {
|
||||
llm_missing: bool,
|
||||
analyzing: bool,
|
||||
has_document: bool,
|
||||
/// True iff at least one `.m4a.failed` recording exists. Drives the
|
||||
/// verdrängende `fehler`-Badge in the case header; see the same
|
||||
/// field on `UserCaseView` for semantics.
|
||||
has_failed_recording: bool,
|
||||
is_admin: bool,
|
||||
/// True when the case carries a `.closed` marker. Toggles the
|
||||
/// header action button between close and reopen, and appends
|
||||
@@ -491,6 +501,7 @@ pub async fn handle_case_page(
|
||||
.iter()
|
||||
.filter(|r| r.transcript.has_file())
|
||||
.count();
|
||||
let has_failed_recording = recordings.iter().any(|r| r.failed);
|
||||
let case_id_short = case_id_str.chars().take(8).collect();
|
||||
let is_admin = user.is_admin();
|
||||
|
||||
@@ -515,6 +526,7 @@ pub async fn handle_case_page(
|
||||
llm_missing: flags.llm_missing,
|
||||
analyzing: flags.analyzing,
|
||||
has_document: flags.has_document,
|
||||
has_failed_recording,
|
||||
is_admin,
|
||||
is_closed,
|
||||
}
|
||||
@@ -732,6 +744,7 @@ async fn compute_case_view(
|
||||
.unwrap_or_default();
|
||||
let recordings_count = recordings.len();
|
||||
let non_failed_count = recordings.iter().filter(|r| !r.failed).count();
|
||||
let has_failed_recording = recordings.iter().any(|r| r.failed);
|
||||
let all_transcribed = non_failed_count > 0
|
||||
&& recordings
|
||||
.iter()
|
||||
@@ -757,6 +770,7 @@ async fn compute_case_view(
|
||||
recordings_count,
|
||||
analyzing,
|
||||
has_document,
|
||||
has_failed_recording,
|
||||
can_analyze,
|
||||
is_closed,
|
||||
days_until_purge,
|
||||
|
||||
Reference in New Issue
Block a user