Residual A — the chunked dub-stream had a PARALLEL wedge mechanism (its own
ping-loop timeout, its own _reset_pool_on_wedge, a dead-end "Try restarting
the server" message). A wedged chunk now routes through the SAME
run_transcribe_guarded bound+reset as the whole-file paths (#731/#851): the
guard resets the poisoned pool once per wedged attempt (no double-reset on
retry) and the user sees the actionable ASRTimeoutError. The reset logic is
extracted to asr_backend.reset_pool_after_wedge — one shared mechanism, so
the semantics can't drift again. run_transcribe_guarded also gains a
timeout_env param so chunk errors name OMNIVOICE_TRANSCRIBE_CHUNK_TIMEOUT_S
instead of the whole-file knob.
Residual B — the crash-isolated ASR sidecar (#393, faster-whisper-isolated)
is wired as an explicit ESCAPE HATCH, not a default:
- selectable end-to-end: Settings engine list gets an explanatory
install_hint; honest gpu_compat ("cuda","cpu" — it wraps the same
CTranslate2 engine as faster-whisper); get_active_asr_backend now hands
back a process-wide singleton for subprocess-isolated backends (a fresh
instance per request would leak atexit hooks and respawn the sidecar —
reloading its model — on every transcribe).
- on the SECOND consecutive guarded timeout-with-reset in one session
(resets aren't recovering the hang; the wedged thread keeps its VRAM),
the error the user sees + the log recommend switching to the isolated
engine in Settings → Engines. Never auto-switched (owner rule: no silent
behavior divergence); a completed transcribe resets the streak.
Tests (fail-before/pass-after verified against origin/main): wedged-chunk
SSE integration (reset count + actionable error + recommendation surfaces),
consecutive-timeout streak (fires at 2, resets on success, suppressed when
already on the isolated engine), timeout_env parametrization, shared-reset
helper, isolated backend in list_backends with hint + honest availability,
singleton caching, gpu_compat matrix entry. Docs: troubleshooting §14 gains
the chunk knob + escape-hatch guidance.
Closes the residuals tracked on #730.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A whisperx/CTranslate2 transcribe can hang hard on some Windows+CUDA setups and
never return. ASR shares the small (1-2 worker) _gpu_pool with TTS, so one stuck
worker starved every other request — the next TTS generate then surfaced as
"Can't reach the local backend" though the process was alive (#720/#721/#723).
Two parts:
- Bound the three remaining unguarded whole-file transcribe paths (dub
whole-file dub_core.py, batch.py, live-dictation capture_ws.py) with
run_transcribe_guarded, matching the dub-QC/dictation/OpenAI paths that were
already bounded by #656.
- On timeout, run_transcribe_guarded now calls executor.reset() when the pool
supports it (_ResilientGpuPool, already built for the model-load-timeout case
in #589/#599): the wedged worker is abandoned and the next submit gets a fresh
one, restoring capacity without an app restart. Best-effort — a plain
ThreadPoolExecutor (tests) just gets the bound + actionable error.
Regression tests: pool.reset() is invoked on timeout; a non-reset pool still
bounds cleanly.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Windows/CUDA user (Vietnam) hit "Can't reach the local backend" only when
dubbing/transcribing. Their log proves the backend started fine — model loaded,
preload complete, 25 models — and the log ends right after
`whisperx transcribing …tmp.wav`. The backend was alive; the *transcription*
stalled (large-v3 ASR contending with the resident TTS model for VRAM on an
8 GB-class GPU), which the UI surfaces as an unreachable backend.
Root cause (class, not instance): the chunked dub pipeline already bounds each
chunk (OMNIVOICE_TRANSCRIBE_CHUNK_TIMEOUT_S), but the *whole-file* transcribe
paths ran unbounded:
- dub QC re-transcribe (dub_export)
- dictation (capture)
- OpenAI-compat /audio/transcriptions
A slow/stuck transcribe on any of these hung the request AND held a GPU-pool
worker — indistinguishable from a dead backend.
Fix: add run_transcribe_guarded() in services/asr_backend.py — a shared
asyncio.wait_for wrapper (ASRTimeoutError, a TimeoutError subclass) with a
generous env-tunable bound (OMNIVOICE_ASR_TRANSCRIBE_TIMEOUT_S, default 300 s).
On timeout the request returns 504 with actionable guidance (backend is alive;
free VRAM / pick a smaller ASR model / use CPU; restart to clear the stuck
worker) instead of hanging forever. Wired into all three whole-file paths.
Docs: new troubleshooting §14 — "Can't reach the local backend during
transcription/dubbing" — explains it's ASR weight/VRAM pressure, not a network/
mirror problem, and corrects the misconception that a "Network → Restricted/Global
mirror" Settings toggle exists (the Network control is LAN sharing). Serves the
#602/#585/#567 "can't reach backend" cluster.
Test: backend/tests/test_asr_transcribe_timeout.py — slow fn raises ASRTimeoutError
with the actionable message, fast fn passes through, subclass-of-TimeoutError so
the openai_compat broad catch still maps to 504.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>