SubtitleEdit/subtitleedit · error · ArgumentException

Voice is not an AzureVoice

Error message

Voice is not an AzureVoice

What it means

AzureSpeech.Speak pattern-matches voice.EngineVoice to AzureVoice; any other engine's voice triggers ArgumentException. Note this method immediately afterwards falls back region/model from settings when the caller passed null (per-actor cast rows), so the type check is the first guard.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/AzureSpeech.cs:140

        // ElevenLabs-shaped JSON that Map() cannot parse, permanently emptying the voice combo.
        var ms = new MemoryStream();
        await _ttsDownloadService.DownloadAzureVoiceList(ms, null, cancellationToken);
        await File.WriteAllBytesAsync(Path.Combine(GetSetAzureFolder(), 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 AzureVoice azureVoice)
        {
            throw new ArgumentException("Voice is not an AzureVoice");
        }

        // Callers pass null region/model when this engine is not the globally selected one
        // (per-actor cast rows, cast-dialog voice test) - fall back to the saved settings
        // instead of dereferencing null into the request URL, which produced a nonsense host
        // (https://.tts.speech...) and aborted the whole generation run.
        if (string.IsNullOrWhiteSpace(region))
        {
            region = Se.Settings.Video.TextToSpeech.AzureRegion;
        }

        if (string.IsNullOrWhiteSpace(region))
        {
            var error = "Azure region is not set - enter it in the Azure engine settings.";
            Se.WriteToolsLog("AzureSpeech: " + error, true);
            return new TtsResult { Text = text, FileName = string.Empty, Error = true, ErrorMessage = error };
        }

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Re-select the voice from the Azure voice list after switching to AzureSpeech.
  2. Validate per-actor voice mappings reference Azure voices before batch generation.
  3. Filter the voice picker to the active engine.

Example fix

// before
await _azure.Speak(text, out, voice, lang, region, model, ct);
// after
if (voice.EngineVoice is not AzureVoice)
    throw new InvalidOperationException($"Voice {voice.Name} is not an Azure voice.");
await _azure.Speak(text, out, voice, lang, region, model, ct);
Defensive patterns

Strategy: type-guard

Validate before calling

if (voice.EngineVoice is not AzureVoice) return Invalid($"Voice {voice.Name} is not an Azure voice");

Type guard

static bool IsAzureVoice(Voice v) => v.EngineVoice is AzureVoice;

Try / catch

try { await azure.Speak(...); }
catch (ArgumentException ex) when (ex.Message.Contains("AzureVoice"))
{ /* re-select the voice from the Azure list */ }

Prevention

When it happens

Trigger: A Voice whose EngineVoice is not AzureVoice is passed to AzureSpeech.Speak.

Common situations: Same as 187: stale mapping after engine switch; per-actor cast row still pointing at a non-Azure voice; voice list/engine desync in the cast dialog.

Related errors


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