{"record":{"id":"abfb93ded68d0170","repo":"SubtitleEdit/subtitleedit","slug":"voice-is-not-a-cosyvoice3voice","errorCode":null,"errorMessage":"Voice is not a CosyVoice3Voice","messagePattern":"Voice is not a CosyVoice3Voice","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/ui/Features/Video/TextToSpeech/Engines/CosyVoice3CrispAsr.cs","lineNumber":471,"sourceCode":"    public Task<string[]> GetModels() => Task.FromResult(new[] { ModelKeyQ4K, ModelKeyF16 });\n\n    public Task<TtsLanguage[]> GetLanguages(Voice voice, string? model) => Task.FromResult(CosyVoice3Languages.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 CosyVoice3Voice cosyVoice)\n        {\n            throw new ArgumentException(\"Voice is not a CosyVoice3Voice\");\n        }\n\n        // Either Preset OR FilePath must be set. Preset wins if both are populated (defensive\n        // — shouldn't happen with the constructors but guards against future drift).\n        // Error results, not throws: a throw from Speak escapes the generate loop's per-segment\n        // handling and aborts the entire run - reachable from cast rows and the review window,\n        // where the main window's transcript prompt never had a chance to fire.\n        var voiceArg = !string.IsNullOrEmpty(cosyVoice.Preset) ? cosyVoice.Preset : cosyVoice.FilePath;\n        if (string.IsNullOrEmpty(voiceArg))\n        {\n            var error = \"CosyVoice3 (CrispASR) requires a preset or an imported reference WAV. \"\n                + \"Pick one of the baked presets (zero_shot / fleurs-*) or import a 16 kHz mono \"\n                + \"reference WAV with an adjacent .txt transcription sidecar.\";\n            Se.WriteToolsLog(\"CosyVoice3 (CrispASR): \" + error, true);\n            return new TtsResult { Text = text, FileName = string.Empty, Error = true, ErrorMessage = error };\n        }\n\n        var isClone = string.IsNullOrEmpty(cosyVoice.Preset);","sourceCodeStart":453,"sourceCodeEnd":489,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Features/Video/TextToSpeech/Engines/CosyVoice3CrispAsr.cs#L453-L489","documentation":"ArgumentException from CosyVoice3CrispAsr.Speak: the passed Voice's EngineVoice is not a CosyVoice3Voice. Each TTS engine owns a concrete voice type; Speak pattern-matches and refuses to synthesise with another engine's voice object. Unlike the missing-config errors below, this is a hard throw (the comment notes a throw escapes the per-segment generate loop and aborts the whole run).","triggerScenarios":"Calling cosyVoice3Engine.Speak(...) with a Voice constructed from an EdgeTtsVoice, ElevenLabVoice, F5TtsVoice, or any non-CosyVoice3 EngineVoice; a cast/mapping bug in the voice-combo → engine dispatch; mixing voices across engines in a cast row.","commonSituations":"A cast row or review window passes the global selected voice (from a different engine) into the CosyVoice3 engine; deserialised voice JSON rehydrated as the wrong subtype; refactor that changed EngineVoice typing.","solutions":["Ensure the Voice passed to CosyVoice3CrispAsr.Speak was built by CosyVoice3CrispAsr.GetVoices/RefreshVoices.","Route each Voice to the engine that owns it — key voices by engine before dispatch.","Add a unit test that asserts every voice in a cast resolves to the engine that produced it."],"exampleFix":"// before — wrong engine's voice handed to CosyVoice3\nawait cosyVoice3Engine.Speak(text, out, edgeVoice, lang, region, model, ct);\n\n// after — dispatch each voice to its owning engine\nvar engine = voice.EngineVoice switch\n{\n    CosyVoice3Voice _ => cosyVoice3Engine,\n    EdgeTtsVoice _ => edgeTtsEngine,\n    _ => throw new InvalidOperationException($\"No engine for {voice.EngineVoice.GetType().Name}\"),\n};\nawait engine.Speak(text, out, voice, lang, region, model, ct);","handlingStrategy":"type-guard","validationCode":"if (voice.EngineVoice is not CosyVoice3Voice) return;\n// or, before dispatch:\nif (voice.EngineVoice is not CosyVoice3Voice cosy) {\n    Se.WriteToolsLog($\"Skipping CosyVoice3 synthesis: voice is {voice.EngineVoice?.GetType().Name}\", true);\n    return;\n}","typeGuard":"static bool IsCosyVoice3(Voice voice) => voice?.EngineVoice is CosyVoice3Voice;","tryCatchPattern":"try { await cosyVoice3Engine.Speak(text, out, voice, lang, region, model, ct); }\ncatch (ArgumentException ex) when (ex.Message == \"Voice is not a CosyVoice3Voice\")\n{\n    Se.WriteToolsLog($\"Voice/engine mismatch for CosyVoice3: {voice.EngineVoice?.GetType().Name}\", true);\n}","preventionTips":["Always route a Voice to the engine that produced it — key voices by engine at construction.","Unit-test that every cast/review voice resolves to its producing engine.","Never construct a Voice from a different engine's EngineVoice subtype."],"tags":["tts","cosyvoice3","type-mismatch","voice-dispatch","argument-exception"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}