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
- Re-select the voice from the Chatterbox voice list after switching engines.
- Clear non-Chatterbox voice mappings before invoking Speak.
- 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
- Filter the voice picker to Chatterbox voices when Chatterbox is active.
- Clear non-Chatterbox mappings on engine switch.
- Guard the type at the call site before resolving the model key.
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
- Voice is not a AllTalkVoice
- Voice is not an AzureVoice
- Voice is not a Qwen3TtsVoice
- Voice is not a VibeVoice
- Chatterbox TTS hit a CrispASR runtime bug during synthesis (
AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13).
Data as JSON: /api/errors/a5bb82f724e106fe.
Report an issue: GitHub.