SubtitleEdit/subtitleedit · error · ArgumentException

Voice is not a Qwen3TtsVoice

Error message

Voice is not a Qwen3TtsVoice

What it means

ArgumentException thrown at the top of Qwen3TtsCrispAsr.Speak when voice.EngineVoice is not a Qwen3TtsVoice. The engine contract is that the Voice passed in was produced by this engine's GetVoices (which wraps each entry in a Qwen3TtsVoice); any other engine's voice object is a programming error, not a runtime hazard.

Source

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

    public Task<string[]> GetModels() => Task.FromResult(new[] { ModelKeyVoiceDesign, ModelKeyCustomVoice, ModelKeyClone });

    public Task<TtsLanguage[]> GetLanguages(Voice voice, string? model) => Task.FromResult(Qwen3TtsCrispAsrLanguages.All);

    public Task<Voice[]> RefreshVoices(string language, CancellationToken cancellationToken) =>
        GetVoices(language);

    public async Task<TtsResult> Speak(
        string text,
        string outputFolder,
        Voice voice,
        TtsLanguage? language,
        string? region,
        string? model,
        CancellationToken cancellationToken)
    {
        if (voice.EngineVoice is not Qwen3TtsVoice qwen3Voice)
        {
            throw new ArgumentException("Voice is not a Qwen3TtsVoice");
        }

        var modelKey = ResolveModelKey(model);

        // Voice clone (Base) needs a reference WAV plus its transcript. The backend resolves the
        // bare voice name against --voice-dir and auto-loads the matching <name>.txt as ref-text;
        // without that transcript it returns "ref-text not set" (HTTP 500). Surface both gaps
        // up-front with an actionable message instead of an opaque server error.
        if (modelKey == ModelKeyClone)
        {
            if (string.IsNullOrEmpty(qwen3Voice.FilePath))
            {
                throw new InvalidOperationException(
                    "Qwen3 TTS (CrispASR) voice cloning requires a reference voice. "
                    + "Import one via the voice settings (you'll be asked for its transcript), "
                    + "then pick it in the voice combo.");
            }

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Ensure the Voice passed to Speak was obtained from this engine's own GetVoices/RefreshVoices.
  2. On engine switch, clear or re-resolve the selected voice before calling Speak.
  3. Add an IsVoiceInstalled/voice-type check in the UI layer so an incompatible voice can't be selected for this engine.
  4. In tests, construct voices with `new Voice(new Qwen3TtsVoice(...))`.

Example fix

// before
await engine.Speak(text, folder, someOtherEngineVoice, ...);

// after — resolve a voice from this engine first
var voices = await engine.GetVoices(language);
var voice = voices.Single(v => ((Qwen3TtsVoice)v.EngineVoice).Voice == desiredName);
await engine.Speak(text, folder, voice, ...);
Defensive patterns

Strategy: type-guard

Validate before calling

if (voice.EngineVoice is not Qwen3TtsVoice)
    throw new ArgumentException("Voice is not a Qwen3TtsVoice");

Type guard

static bool IsQwen3TtsVoice(Voice v) => v.EngineVoice is Qwen3TtsVoice;

Prevention

When it happens

Trigger: The voice combo was populated by a different engine (e.g. Qwen3TtsCpp or VibeVoiceCrispAsr) and that Voice was passed into this engine's Speak; a stale cached Voice survived an engine switch; a test constructed a Voice with the wrong EngineVoice subtype.

Common situations: Engine-switch race where the UI hands the previous engine's selected voice to the new engine; serialization/deserialization of a Voice that lost its subtype; unit tests using a shared voice fixture across engines.

Related errors


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