SubtitleEdit/subtitleedit · error · ArgumentException

Voice is not a VibeVoice

Error message

Voice is not a VibeVoice

What it means

ArgumentException thrown at the top of VibeVoiceCrispAsr.Speak when voice.EngineVoice is not a VibeVoice. VibeVoiceCrispAsr only does voice cloning and its voice list is populated exclusively with Voice(new VibeVoice(...)); passing any other engine's voice is a contract violation.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/VibeVoiceCrispAsr.cs:340

    public Task<string[]> GetModels() => Task.FromResult(new[] { ModelKeyQ4K, ModelKeyQ8_0, 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 VibeVoice vibeVoice)
        {
            throw new ArgumentException("Voice is not a VibeVoice");
        }

        if (string.IsNullOrEmpty(vibeVoice.FilePath))
        {
            throw new InvalidOperationException(
                "VibeVoice (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.");
        }

        var modelKey = ResolveModelKey(model);
        await EnsureServerRunningAsync(modelKey, cancellationToken);

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

        // OpenAI-compatible /v1/audio/speech payload. CrispASR's vibevoice backends look at:
        //   - `input`             — the text to synthesise

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Pass only voices obtained from VibeVoiceCrispAsr.GetVoices into its Speak.
  2. Clear/re-resolve the selected voice on engine switch.
  3. Filter the voice combo in the UI so non-VibeVoice entries can't be selected for this engine.
  4. In tests, build voices as `new Voice(new VibeVoice(...))`.

Example fix

// before
await vibeEngine.Speak(text, folder, qwen3Voice, ...);

// after — resolve a VibeVoice from this engine
var voices = await vibeEngine.GetVoices(language);
var voice = voices.Single(v => ((VibeVoice)v.EngineVoice).Voice == desiredName);
await vibeEngine.Speak(text, folder, voice, ...);
Defensive patterns

Strategy: type-guard

Validate before calling

if (voice.EngineVoice is not VibeVoice)
    throw new ArgumentException("Voice is not a VibeVoice");

Type guard

static bool IsVibeVoice(Voice v) => v.EngineVoice is VibeVoice;

Prevention

When it happens

Trigger: A voice from Qwen3TtsCpp, Qwen3TtsCrispAsr, or a cloud TTS engine was handed to this engine's Speak; engine-switch race left the previous engine's voice selected; tests share a voice fixture across engines.

Common situations: UI engine switch that didn't re-resolve the selected voice; deserialized Voice that lost its subtype; cross-engine test fixtures.

Related errors


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