SubtitleEdit/subtitleedit · error · ArgumentException

Voice is not an ElevenLabVoice

Error message

Voice is not an ElevenLabVoice

What it means

ArgumentException from ElevenLabs.Speak: the passed Voice's EngineVoice is not an ElevenLabVoice. ElevenLabs needs its concrete voice type (carrying VoiceId used in the API call). Notably this engine tolerates a null model argument (falling back to settings then eleven_multilingual_v2), but a wrong voice type is a hard throw.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/ElevenLabs.cs:282

    {
        var ms = new MemoryStream();
        await _ttsDownloadService.DownloadElevenLabsVoiceList(ms, null, cancellationToken);
        await File.WriteAllBytesAsync(Path.Combine(GetSetElevenLabsFolder(), JsonFileName), ms.ToArray(), cancellationToken);
        return await 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 ElevenLabVoice elevenLabVoice)
        {
            throw new ArgumentException("Voice is not an ElevenLabVoice");
        }

        // Callers pass null when this engine is not the globally selected one (per-actor cast
        // rows, cast-dialog voice test) - fall back to the saved/default model instead of
        // throwing, which aborted the whole generation run at the first ElevenLabs row.
        if (string.IsNullOrEmpty(model))
        {
            model = Se.Settings.Video.TextToSpeech.ElevenLabsModel;
        }

        if (string.IsNullOrEmpty(model))
        {
            model = "eleven_multilingual_v2";
        }

        Se.WriteToolsLog($"ElevenLabs: voice={elevenLabVoice.Voice}, voiceId={elevenLabVoice.VoiceId}, model={model}, textLen={text.Length}");

        var ms = new MemoryStream();

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Pass only voices produced by ElevenLabs.GetVoices/RefreshVoices into ElevenLabs.Speak.
  2. Dispatch each Voice to its owning engine by EngineVoice type.
  3. Add a test asserting each cast voice resolves to its producing engine.

Example fix

// before
await elevenLabsEngine.Speak(text, out, edgeVoice, lang, region, null, ct);

// after
var engine = voice.EngineVoice switch
{
    ElevenLabVoice _ => elevenLabsEngine,
    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 ElevenLabVoice) {
    Se.WriteToolsLog($"Skipping ElevenLabs synthesis: voice is {voice.EngineVoice?.GetType().Name}", true);
    return;
}

Type guard

static bool IsElevenLabVoice(Voice voice) => voice?.EngineVoice is ElevenLabVoice;

Try / catch

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

Prevention

When it happens

Trigger: Calling elevenLabsEngine.Speak(...) with a Voice whose EngineVoice is any non-ElevenLabVoice; a dispatch bug; deserialised voice rehydrated to the wrong subtype; mixing voices across engines in a cast.

Common situations: Cast row passing a global voice from another engine into ElevenLabs; refactor that changed EngineVoice typing; per-actor rows where the engine assignment is wrong.

Related errors


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