SubtitleEdit/subtitleedit · error · ArgumentException

Voice is not a CosyVoice3Voice

Error message

Voice is not a CosyVoice3Voice

What it means

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).

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/CosyVoice3CrispAsr.cs:471

    public Task<string[]> GetModels() => Task.FromResult(new[] { ModelKeyQ4K, ModelKeyF16 });

    public Task<TtsLanguage[]> GetLanguages(Voice voice, string? model) => Task.FromResult(CosyVoice3Languages.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 CosyVoice3Voice cosyVoice)
        {
            throw new ArgumentException("Voice is not a CosyVoice3Voice");
        }

        // Either Preset OR FilePath must be set. Preset wins if both are populated (defensive
        // — shouldn't happen with the constructors but guards against future drift).
        // Error results, not throws: a throw from Speak escapes the generate loop's per-segment
        // handling and aborts the entire run - reachable from cast rows and the review window,
        // where the main window's transcript prompt never had a chance to fire.
        var voiceArg = !string.IsNullOrEmpty(cosyVoice.Preset) ? cosyVoice.Preset : cosyVoice.FilePath;
        if (string.IsNullOrEmpty(voiceArg))
        {
            var error = "CosyVoice3 (CrispASR) requires a preset or an imported reference WAV. "
                + "Pick one of the baked presets (zero_shot / fleurs-*) or import a 16 kHz mono "
                + "reference WAV with an adjacent .txt transcription sidecar.";
            Se.WriteToolsLog("CosyVoice3 (CrispASR): " + error, true);
            return new TtsResult { Text = text, FileName = string.Empty, Error = true, ErrorMessage = error };
        }

        var isClone = string.IsNullOrEmpty(cosyVoice.Preset);

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Ensure the Voice passed to CosyVoice3CrispAsr.Speak was built by CosyVoice3CrispAsr.GetVoices/RefreshVoices.
  2. Route each Voice to the engine that owns it — key voices by engine before dispatch.
  3. Add a unit test that asserts every voice in a cast resolves to the engine that produced it.

Example fix

// before — wrong engine's voice handed to CosyVoice3
await cosyVoice3Engine.Speak(text, out, edgeVoice, lang, region, model, ct);

// after — dispatch each voice to its owning engine
var engine = voice.EngineVoice switch
{
    CosyVoice3Voice _ => cosyVoice3Engine,
    EdgeTtsVoice _ => edgeTtsEngine,
    _ => throw new InvalidOperationException($"No engine for {voice.EngineVoice.GetType().Name}"),
};
await engine.Speak(text, out, voice, lang, region, model, ct);
Defensive patterns

Strategy: type-guard

Validate before calling

if (voice.EngineVoice is not CosyVoice3Voice) return;
// or, before dispatch:
if (voice.EngineVoice is not CosyVoice3Voice cosy) {
    Se.WriteToolsLog($"Skipping CosyVoice3 synthesis: voice is {voice.EngineVoice?.GetType().Name}", true);
    return;
}

Type guard

static bool IsCosyVoice3(Voice voice) => voice?.EngineVoice is CosyVoice3Voice;

Try / catch

try { await cosyVoice3Engine.Speak(text, out, voice, lang, region, model, ct); }
catch (ArgumentException ex) when (ex.Message == "Voice is not a CosyVoice3Voice")
{
    Se.WriteToolsLog($"Voice/engine mismatch for CosyVoice3: {voice.EngineVoice?.GetType().Name}", true);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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