{"record":{"id":"6df7fefb57ca2ed4","repo":"SubtitleEdit/subtitleedit","slug":"voice-is-not-a-qwen3ttsvoice","errorCode":null,"errorMessage":"Voice is not a Qwen3TtsVoice","messagePattern":"Voice is not a Qwen3TtsVoice","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/ui/Features/Video/TextToSpeech/Engines/Qwen3TtsCrispAsr.cs","lineNumber":737,"sourceCode":"    public Task<string[]> GetModels() => Task.FromResult(new[] { ModelKeyVoiceDesign, ModelKeyCustomVoice, ModelKeyClone });\n\n    public Task<TtsLanguage[]> GetLanguages(Voice voice, string? model) => Task.FromResult(Qwen3TtsCrispAsrLanguages.All);\n\n    public Task<Voice[]> RefreshVoices(string language, CancellationToken cancellationToken) =>\n        GetVoices(language);\n\n    public async Task<TtsResult> Speak(\n        string text,\n        string outputFolder,\n        Voice voice,\n        TtsLanguage? language,\n        string? region,\n        string? model,\n        CancellationToken cancellationToken)\n    {\n        if (voice.EngineVoice is not Qwen3TtsVoice qwen3Voice)\n        {\n            throw new ArgumentException(\"Voice is not a Qwen3TtsVoice\");\n        }\n\n        var modelKey = ResolveModelKey(model);\n\n        // Voice clone (Base) needs a reference WAV plus its transcript. The backend resolves the\n        // bare voice name against --voice-dir and auto-loads the matching <name>.txt as ref-text;\n        // without that transcript it returns \"ref-text not set\" (HTTP 500). Surface both gaps\n        // up-front with an actionable message instead of an opaque server error.\n        if (modelKey == ModelKeyClone)\n        {\n            if (string.IsNullOrEmpty(qwen3Voice.FilePath))\n            {\n                throw new InvalidOperationException(\n                    \"Qwen3 TTS (CrispASR) voice cloning requires a reference voice. \"\n                    + \"Import one via the voice settings (you'll be asked for its transcript), \"\n                    + \"then pick it in the voice combo.\");\n            }\n","sourceCodeStart":719,"sourceCodeEnd":755,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Features/Video/TextToSpeech/Engines/Qwen3TtsCrispAsr.cs#L719-L755","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the Voice passed to Speak was obtained from this engine's own GetVoices/RefreshVoices.","On engine switch, clear or re-resolve the selected voice before calling Speak.","Add an IsVoiceInstalled/voice-type check in the UI layer so an incompatible voice can't be selected for this engine.","In tests, construct voices with `new Voice(new Qwen3TtsVoice(...))`."],"exampleFix":"// before\nawait engine.Speak(text, folder, someOtherEngineVoice, ...);\n\n// after — resolve a voice from this engine first\nvar voices = await engine.GetVoices(language);\nvar voice = voices.Single(v => ((Qwen3TtsVoice)v.EngineVoice).Voice == desiredName);\nawait engine.Speak(text, folder, voice, ...);","handlingStrategy":"type-guard","validationCode":"if (voice.EngineVoice is not Qwen3TtsVoice)\n    throw new ArgumentException(\"Voice is not a Qwen3TtsVoice\");","typeGuard":"static bool IsQwen3TtsVoice(Voice v) => v.EngineVoice is Qwen3TtsVoice;","tryCatchPattern":null,"preventionTips":["Resolve voices from the active engine's GetVoices, never reuse across engines.","Clear the selected voice on engine switch.","Filter the voice combo by the current engine's voice subtype."],"tags":["tts","voice","type-mismatch","csharp"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}