diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d0fd4b..f4265c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ the frozen-backend fallback mirror it for their toolchains. ### Fixed +- The setup splash now waits through the backend's full startup budget instead of reporting slow Windows CUDA initialization as stuck after two minutes (#1749) - Dubbing jobs can now reuse every source-language code produced by automatic ASR detection without a 400 error on the next upload (#1737) - Incomplete Sherpa-ONNX model snapshots now self-repair before recognizer startup instead of failing on a missing ONNX file (#1733) - OmniVoice subprocess startup now allows slow packaged Windows Python runtimes to signal readiness before termination (#1711) diff --git a/frontend/src/components/BootstrapSplash.jsx b/frontend/src/components/BootstrapSplash.jsx index 5dd5d03d..8229dc6d 100644 --- a/frontend/src/components/BootstrapSplash.jsx +++ b/frontend/src/components/BootstrapSplash.jsx @@ -938,7 +938,12 @@ export function useBootstrapStage(pollMs = 1000) { // person can leave. const stallBudgetMs = (stage) => { if (stage === 'awaiting_setup') return Infinity; - return stage === 'installing_deps' ? 20 * 60 * 1000 : 120 * 1000; + if (stage === 'installing_deps') return 20 * 60 * 1000; + // Rust owns the backend launch and waits up to five minutes so slow + // torch/CUDA imports can finish. Keep the splash alive beyond that + // window; otherwise it reports a false failure at two minutes while + // the supervised backend is still healthy and making progress (#1749). + return stage === 'starting_backend' ? 6 * 60 * 1000 : 120 * 1000; }; const invoke = async () => { try { diff --git a/frontend/src/test/BootstrapSplashAwaitingSetupStall.test.jsx b/frontend/src/test/BootstrapSplashAwaitingSetupStall.test.jsx index 12912985..3a32e4c6 100644 --- a/frontend/src/test/BootstrapSplashAwaitingSetupStall.test.jsx +++ b/frontend/src/test/BootstrapSplashAwaitingSetupStall.test.jsx @@ -102,7 +102,7 @@ describe('useBootstrapStage — awaiting_setup is human-gated (#1376)', () => { expect(result.current.stage).toBe('installing_deps'); }); - it('a machine-owned stage that genuinely wedges still fails', async () => { + it('lets Rust finish its backend startup budget before declaring a wedge', async () => { // The other half of the contract. Exempting awaiting_setup must not // disarm the stall detector for stages nothing but the app can advance — // that would trade this bug for the info-less infinite spinner (#879). @@ -114,10 +114,28 @@ describe('useBootstrapStage — awaiting_setup is human-gated (#1376)', () => { await act(async () => {}); expect(result.current.stage).toBe('starting_backend'); + // The shell waits five minutes for slow torch/CUDA imports. The splash + // must not replace that live launch with its old two-minute false failure. await act(async () => { await vi.advanceTimersByTimeAsync(3 * 60 * 1000); }); + expect(result.current.stage).toBe('starting_backend'); + expect(result.current.message ?? '').not.toMatch(/stuck/i); + + // Pin both sides of the six-minute fallback boundary. + await act(async () => { + await vi.advanceTimersByTimeAsync(3 * 60 * 1000 - 1_000); + }); + + expect(result.current.stage).toBe('starting_backend'); + expect(result.current.message ?? '').not.toMatch(/stuck/i); + + // A frontend-only deadlock remains bounded if the shell never advances. + await act(async () => { + await vi.advanceTimersByTimeAsync(2_000); + }); + expect(result.current.stage).toBe('failed'); expect(result.current.message).toMatch(/stuck/i); });