2 Commits
Author SHA1 Message Date
c965a7fbdd fix(backend): self-healing GPU pool so a reset can't strand requests (#589/#599) (#601)
`_reset_gpu_pool()` fires on a model-load timeout to recover a wedged worker —
it shut the ThreadPoolExecutor down and rebuilt a fresh one on next access. But
several request handlers (generation, dub_generate, dub_core, dub_translate,
openai_compat) did a *module-level* `from services.model_manager import
_gpu_pool`, capturing the executor object at import time. After a reset those
references pointed at the dead pool, so the next generate/dub/transcribe/
translate raised `RuntimeError: cannot schedule new futures after shutdown` —
surfacing as a 500 or "Can't reach the local backend" (#589 #599).

Make `_gpu_pool` a single long-lived `_ResilientGpuPool` wrapper (a
concurrent.futures.Executor) whose *inner* ThreadPoolExecutor is swapped:
- every submit() resolves the live pool, and a submit that races a shutdown
  rebuilds once and retries, so a stale captured reference self-heals;
- `_reset_gpu_pool()` now drops only the inner pool (fresh worker on retry)
  while preserving the wrapper identity every importer holds;
- pool sizing stays lazy, so we still probe the device after torch's lazy
  import (the reason for the original __getattr__ indirection).

Fixes the whole class — all importers share one wrapper, module-level or
function-level. Regression tests cover stale-ref-survives-reset, identity
stability, submit-after-inner-shutdown self-heal, and asyncio.run_in_executor
compatibility; updated the load-timeout test to the new reset semantics.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 04:42:16 +05:30
Palash DebnathandClaude Opus 4.8 5a723c0408 fix(model): bound first-run model load/download so it never hangs forever (#173)
Windows users reported 'create demo voice runs indefinitely, no audio, no
error'. Root cause: the first /generate triggers OmniVoice.from_pretrained()
which downloads multi-GB weights via the legacy LFS path (HF_HUB_DISABLE_XET=1),
with NO timeout anywhere. A stalled socket (proxy/firewall/AV) blocks the GPU-
pool worker forever inside get_model() -- before the try/except that would
surface an error -- and the frontend /generate fetch had no abort, so the
spinner spun forever with no toast.

- backend/main.py: set HF_HUB_ETAG_TIMEOUT=15 + HF_HUB_DOWNLOAD_TIMEOUT=30
  (per-read timeout: resets on each chunk, so slow-but-progressing downloads
  are never punished; only a dead socket trips it). Set before hf import.
- model_manager: get_model()/preload_model() now load via _load_model_with_timeout(),
  an asyncio.wait_for backstop (OMNIVOICE_MODEL_LOAD_TIMEOUT, default 1200s) that
  drops the poisoned GPU pool and raises a clear, actionable RuntimeError so a
  retry gets a fresh worker instead of queueing behind the wedged one.
- useTTS.js: AbortController backstop on /generate so the UI never spins forever
  even if the backend is unreachable; friendly timeout toast.
- tests: watchdog raises + resets pool + releases lock; env/floor parsing.

Cross-platform (no OS-specific behavior); backward-compatible with installed
models; local-first preserved.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:50:21 +05:30