SubtitleEdit/subtitleedit · error · ArgumentException

Voice is not an F5TtsVoice

Error message

Voice is not an F5TtsVoice

What it means

ArgumentException from F5TtsCrispAsr.Speak: the passed Voice's EngineVoice is not an F5TtsVoice. F5-TTS requires its concrete voice type because it reads f5Voice.FilePath (the reference WAV) to clone from. Any other engine's voice is rejected before the file checks run.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/F5TtsCrispAsr.cs:363

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

    public Task<TtsLanguage[]> GetLanguages(Voice voice, string? model) => Task.FromResult(Array.Empty<TtsLanguage>());

    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 F5TtsVoice f5Voice)
        {
            throw new ArgumentException("Voice is not an F5TtsVoice");
        }

        if (string.IsNullOrEmpty(f5Voice.FilePath))
        {
            throw new InvalidOperationException(
                "F5-TTS (CrispASR) requires a reference voice WAV. "
                + "Import one via the voice settings, then pick it in the voice combo. "
                + "Reference WAV should be 24 kHz mono (3-10 s of clean speech) with an "
                + "adjacent .txt file holding the spoken transcription.");
        }

        var refText = TryReadRefText(f5Voice.FilePath);
        var modelKey = ResolveModelKey(model);
        await EnsureServerRunningAsync(modelKey, f5Voice.FilePath, refText, cancellationToken);

        var outputFileName = Path.Combine(TtsOutputFolder.Resolve(outputFolder, GetSetFolder), Guid.NewGuid() + ".wav");

        var speed = Math.Clamp(Se.Settings.Video.TextToSpeech.F5TtsCrispAsrSpeed, 0.25, 4.0);

View on GitHub (pinned to 17a9f07487)

Solutions

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

Example fix

// before
await f5TtsEngine.Speak(text, out, edgeVoice, lang, region, model, ct);

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

Type guard

static bool IsF5TtsVoice(Voice voice) => voice?.EngineVoice is F5TtsVoice;

Try / catch

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

Prevention

When it happens

Trigger: Calling f5TtsEngine.Speak(...) with a Voice whose EngineVoice is not an F5TtsVoice; a dispatch bug; deserialised voice rehydrated to the wrong subtype.

Common situations: Cast row passing a global voice from another engine into F5-TTS; refactor that changed EngineVoice typing; voice JSON rehydration choosing the wrong concrete type.

Related errors


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