SubtitleEdit/subtitleedit · error · TimeoutException

crispasr (qwen3-tts) did not report healthy within {hasLocal

Error message

crispasr (qwen3-tts) did not report healthy within {hasLocalTalker && hasLocalCodec ? 5 : 30} minutes. Last output: {lastOutput}{LaunchCmdSuffix}

What it means

TimeoutException thrown when crispasr stays alive but never answers /health within the deadline. The deadline is conditional: 5 minutes when both talker and codec GGUFs are already staged locally, 30 minutes when --auto-download must fetch them (~2 GB talker + ~986 MB codec) on first run. The server is stopped and the last log plus launch command are appended.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/Qwen3TtsCrispAsr.cs:1039

                    _serverProcess = null;
                    _serverPort = 0;
                    _serverModelKey = null;
                    _serverLaunchCommand = null;
                    throw new InvalidOperationException(
                        $"crispasr (qwen3-tts) exited during startup (code {exitCode}). Output: {tail}"
                        + LaunchCmdSuffix(exitedLaunchCommand));
                }
                if (await ProbeHealthAsync(port, TimeSpan.FromSeconds(2), ct))
                {
                    return;
                }
                await Task.Delay(TimeSpan.FromSeconds(1), ct);
            }

            var lastOutput = SnapshotServerLog();
            var timeoutLaunchCommand = _serverLaunchCommand;
            StopServerInternal();
            throw new TimeoutException(
                $"crispasr (qwen3-tts) did not report healthy within {(hasLocalTalker && hasLocalCodec ? 5 : 30)} minutes. Last output: {lastOutput}"
                + LaunchCmdSuffix(timeoutLaunchCommand));
        }
        finally
        {
            ServerLock.Release();
        }
    }

    private static string SnapshotServerLog()
    {
        lock (_serverLog)
        {
            var s = _serverLog.ToString().TrimEnd();
            return s.Length > 2000 ? s[^2000..] : s;
        }
    }

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Pre-stage the talker and codec GGUFs locally so the 5-minute (not 30-minute) deadline applies and no network download is needed.
  2. Check Last output — a server printing download progress is just slow; a silent log suggests a route/port mismatch.
  3. Confirm the probe route in ProbeHealthAsync matches this crispasr build's health endpoint.
  4. If on a slow link, run the first synthesis once outside the app (or pre-download the cache) so subsequent starts hit the short deadline.
Defensive patterns

Strategy: retry

Validate before calling

// Pre-stage models locally so the short (5 min) deadline applies.
var hasLocalTalker = IsValidLocalModelFile(GetTalkerPath(modelKey), GetTalkerFileName(modelKey));
var hasLocalCodec = IsValidLocalModelFile(GetCodecPath(), CodecFileName);
if (!hasLocalTalker || !hasLocalCodec)
    await PreDownloadCrispAsrModelsAsync(modelKey);

Try / catch

try { await EnsureServerRunningAsync(modelKey, ct); }
catch (TimeoutException ex)
{ Se.LogError(ex); /* offer retry after pre-staging models */ throw; }

Prevention

When it happens

Trigger: First-run download over a slow/unreliable link exceeds 30 min; the server is alive but /health was renamed so the probe always fails; GPU init hangs; the model is on a slow network mount and mmap-loads for minutes; the auto-download is blocked by a proxy and the server waits indefinitely.

Common situations: Offline or metered-network first run; a newer crispasr build that changed the health route; a zombie server holding the port; very large model on integrated graphics.

Related errors


AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13). Data as JSON: /api/errors/c5008eddfd9fa485. Report an issue: GitHub.