SubtitleEdit/subtitleedit · error · ArgumentException

Voice is not a ChatterboxVoice

Error message

Voice is not a ChatterboxVoice

What it means

ChatterboxTtsCpp.Speak pattern-matches voice.EngineVoice to ChatterboxVoice before resolving the model key and ensuring the server is running. Any other engine's voice triggers ArgumentException — a programmer/config error before any work is done.

Source

Thrown at src/ui/Features/Video/TextToSpeech/Engines/ChatterboxTtsCpp.cs:347

            : ChatterboxLanguages.All);

    public Task<Voice[]> RefreshVoices(string language, CancellationToken cancellationToken)
    {
        return 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 ChatterboxVoice chatterboxVoice)
        {
            throw new ArgumentException("Voice is not a ChatterboxVoice");
        }

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

        // Off the calling thread because the repair shells out to ffmpeg. Deliberately not a hard
        // failure either: the check is stricter than the backend's (which still has a partial
        // 16 kHz path), so a reference that could not be re-encoded - no ffmpeg, say - is still
        // worth sending. If the backend does reject it, the 500 handler below turns "backend
        // returned empty audio" into an explanation.
        await Task.Run(() => EnsureCloneReferenceIsUsable(chatterboxVoice.FilePath), cancellationToken);

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

        // Multilingual language selection (#13273-adjacent): a per-request field the server
        // turns into the [xx] prompt token. Only the Base model is multilingual — Turbo is an
        // English-only distillation, so no field is sent for it regardless of the pick.

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Re-select the voice from the Chatterbox voice list after switching engines.
  2. Clear non-Chatterbox voice mappings before invoking Speak.
  3. Filter the voice picker to the active engine.

Example fix

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

Strategy: type-guard

Validate before calling

if (voice.EngineVoice is not ChatterboxVoice) return Invalid($"Voice {voice.Name} is not a Chatterbox voice");

Type guard

static bool IsChatterboxVoice(Voice v) => v.EngineVoice is ChatterboxVoice;

Try / catch

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

Prevention

When it happens

Trigger: A Voice whose EngineVoice is not ChatterboxVoice is passed to ChatterboxTtsCpp.Speak.

Common situations: Stale mapping after switching to/from Chatterbox; cast dialog desync; voice picked from a different engine's list.

Related errors


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