SubtitleEdit/subtitleedit · error · InvalidOperationException

MOSS-TTS (CrispASR) synthesis failed ({(int)response.StatusC

Error message

MOSS-TTS (CrispASR) synthesis failed ({(int)response.StatusCode}): {errorBody}{ServerLog}{LaunchCmdSuffix}

What it means

Thrown when the crispasr moss-tts server returns an HTTP response with a non-success status code (anything outside 2xx). The response body is read via SafeReadErrorAsync and included in the message, along with the status code (as both int and enum), voice, text, request JSON, and a snapshot of the server log.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/MossTtsCrispAsr.cs:498

                + (string.IsNullOrEmpty(serverLog) ? string.Empty : $"{Environment.NewLine}Server log:{Environment.NewLine}{serverLog}")
                + LaunchCmdSuffix(launchCommand),
                ex);
        }

        using (response)
        {
            if (!response.IsSuccessStatusCode)
            {
                var errorBody = await SafeReadErrorAsync(response, cancellationToken);
                var serverLog = SnapshotServerLog();
                var launchCommand = _serverLaunchCommand;
                var errMsg = $"MOSS-TTS (CrispASR) server error {(int)response.StatusCode} {response.StatusCode} — "
                    + $"Voice: {mossVoice}, Text: {text}, RequestJson: {body}, "
                    + $"ResponseBody: {errorBody}, ServerLog: {serverLog}"
                    + LaunchCmdSuffix(launchCommand);
                Se.LogError(errMsg);
                Se.WriteToolsLog(errMsg);
                throw new InvalidOperationException(
                    $"MOSS-TTS (CrispASR) synthesis failed ({(int)response.StatusCode}): {errorBody}"
                    + (string.IsNullOrEmpty(serverLog) ? string.Empty : $"{Environment.NewLine}Server log:{Environment.NewLine}{serverLog}")
                    + LaunchCmdSuffix(launchCommand));
            }

            await using var fileStream = File.Create(outputFileName);
            await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
            await contentStream.CopyToAsync(fileStream, cancellationToken);
        }

        return new TtsResult(outputFileName, text);
    }

    /// <summary>
    /// Builds the <c>/v1/audio/speech</c> JSON payload. Extracted so the #12757 fix (no per-request
    /// <c>voice</c> field) is unit-testable without a running crispasr server.
    /// </summary>
    /// <remarks>

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Read the errorBody in the exception message — it is the server's own JSON error and pinpoints the exact problem (e.g., 'input too long', 'voice not found').
  2. If the error mentions the voice file, re-import the reference WAV via the voice settings and reselect it.
  3. If 500/503, switch to a smaller model quant (Q4_K instead of F16) to reduce memory pressure.
  4. Verify the crispasr binary version matches what the download service pinned (check the .installed.sha256 sidecar) — an older binary may not support the payload fields SE sends.
  5. Shorten or simplify the input text if the server reports content-length or token-limit errors.

Example fix

// before
var payload = new Dictionary<string, object>
{
    ["input"] = text,
    ["response_format"] = "wav",
    ["speed"] = speed,
};

// after: guard empty input before it reaches the server
if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("Input text must not be empty.");
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate input before sending to the server
if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("Input text must not be empty.");
if (speed < 0.25 || speed > 4.0)
    throw new ArgumentOutOfRangeException(nameof(speed));

Type guard

null

Try / catch

catch (InvalidOperationException ex) when (ex.Message.Contains("synthesis failed ("))
{
    // Extract the status code from the message to decide retry vs abort
    var code = ExtractStatusCode(ex.Message);
    if (code >= 500 && code < 600) { /* server-side, retry */ }
    else { /* client-side (4xx), do not retry — fix the input */ }
}

Prevention

When it happens

Trigger: The POST /v1/audio/speech call completes (TCP + HTTP headers received) but response.IsSuccessStatusCode is false. Common codes: 400 when the input text is empty or contains unsupported characters; 422 when speed is out of range; 500/502/503 when the backend crashes mid-synthesis but the HTTP server layer catches it; 404 when the server version mismatched the endpoint.

Common situations: Sending an empty string or whitespace-only text; a voice WAV that was deleted or corrupted after server startup (the server still references the old --voice path); a crispasr version that changed the API contract (e.g., renamed fields); the server ran out of GPU memory for this particular long input and returned 500.

Related errors


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