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:
@@ -78,7 +78,20 @@ pub async fn run(
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
error!(audio = %audio_path.display(), error = %e, "whisper call failed");
|
||||
mark_failed(&audio_path, &events_tx, &job.user_slug).await;
|
||||
if e.is_transient() {
|
||||
// Leave the recording as plain `.m4a` (no `.failed` suffix)
|
||||
// so the page-load heal (`enqueue_pending_for_user`) picks
|
||||
// it up on the next refresh. The natural retry trigger is
|
||||
// a human navigating the UI — no scheduler, no sidecar.
|
||||
info!(
|
||||
audio = %audio_path.display(),
|
||||
error = %e,
|
||||
reason = "transient",
|
||||
"recording left as .m4a, page-load heal will re-enqueue"
|
||||
);
|
||||
} else {
|
||||
mark_failed(&audio_path, &events_tx, &job.user_slug).await;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user