SubtitleEdit/subtitleedit · error · InvalidOperationException

Qwen3 TTS synthesis failed ({(int)response.StatusCode}): {er

Error message

Qwen3 TTS synthesis failed ({(int)response.StatusCode}): {errorBody}

What it means

Thrown when the qwen3-tts-server returns a non-success HTTP status from /v1/synthesize or /v1/synthesize_with_voice. The server's response body is read via SafeReadErrorAsync and embedded in the message; the detailed voice/text/body context is logged separately via Se.LogError before the throw.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/Qwen3TtsCpp.cs:276

            var msg = $"Qwen3 TTS request failed: {ex.Message}"
                + (serverOutput.Length == 0 ? string.Empty : $"{Environment.NewLine}Server output:{Environment.NewLine}{serverOutput}");
            Se.LogError(ex, msg);
            Se.WriteToolsLog(msg);
            throw new InvalidOperationException(msg, ex);
        }

        using (response)
        {
            if (!response.IsSuccessStatusCode)
            {
                var errorBody = await SafeReadErrorAsync(response, cancellationToken);
                var serverOutput = SnapshotServerStderr();
                var errMsg = $"Qwen3 TTS server error {(int)response.StatusCode} {response.StatusCode} - "
                    + $"Voice: {qwen3Voice}, Text: {text}, Body: {errorBody}"
                    + (serverOutput.Length == 0 ? string.Empty : $"{Environment.NewLine}Server output:{Environment.NewLine}{serverOutput}");
                Se.LogError(errMsg);
                Se.WriteToolsLog(errMsg);
                throw new InvalidOperationException(
                    $"Qwen3 TTS synthesis failed ({(int)response.StatusCode}): {errorBody}");
            }

            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);
    }

    private static async Task<HttpResponseMessage> SynthesizeAsync(string text, string instruction, CancellationToken ct)
    {
        object payload = string.IsNullOrEmpty(instruction)
            ? new { text }
            : new { text, instruction };
        var body = JsonSerializer.Serialize(payload);
        using var content = new StringContent(body, Encoding.UTF8, "application/json");

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Inspect the embedded errorBody — the server states exactly which field/value it rejected.
  2. Match the endpoint to the model: only the VoiceDesign model accepts instruction; Base models use /v1/synthesize_with_voice with the reference audio.
  3. Normalize the reference WAV to 24 kHz mono 16-bit PCM with ffmpeg, then retry.
  4. If 500 persists with no actionable body, capture stderr (SnapshotServerStderr) and check for a model-load or Vulkan assertion in the ToolsLog.

Example fix

// before — instruction sent unconditionally to a server that may be a Base model
payload = new { text, instruction };

// after — only include instruction for the instruction-tuned model
var payload = IsVoiceDesignModel(model) && !string.IsNullOrWhiteSpace(instruction)
    ? new { text, instruction }
    : new { text };
Defensive patterns

Strategy: try-catch

Validate before calling

// Only send instruction to the instruction-tuned (VoiceDesign) model.
var sendInstruction = IsVoiceDesignModel(model) && !string.IsNullOrWhiteSpace(instruction);
// Normalize the reference WAV to 24 kHz mono before sending.
if (!string.IsNullOrEmpty(qwen3Voice.FilePath))
    EnsureWavFormat(qwen3Voice.FilePath, sampleRate: 24000, channels: 1);

Try / catch

try { response = await SynthesizeAsync(inputText, instruction, ct); }
catch (InvalidOperationException ex) when (ex.Message.Contains("synthesis failed"))
{
    // Inspect the embedded status code + errorBody, then degrade gracefully.
    Se.LogError(ex);
    throw;
}

Prevention

When it happens

Trigger: HTTP 400 for malformed text or an unsupported instruction field; HTTP 500 when the server's model failed to initialise or the reference WAV is the wrong format; HTTP 422 when instruction is sent to a non-instruction-tuned (0.6B/1.7B Base) model; HTTP 404 if the endpoint path drifts between server builds.

Common situations: Sending an instruction to a Base model (the code already omits it for non-VoiceDesign models, but a stale build may not); reference WAV is not 24 kHz mono so the server rejects it; text contains code points the model's tokenizer can't handle; a server version mismatch renamed the endpoint.

Related errors


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