[Bug] Unsupported languages are silently mis-spoken: three engines ignore the language they were given, and one declares "multi" while supporting 14
What happened
I rendered the same 30-second advertisement script in English, Polish and Spanish through all 16 TTS engines. Two engines refused the languages they do not support, with a clear message. Four produced confident, wrong-sounding audio instead, and the app said nothing.
On listening:
| engine | declares | asked for pl |
result |
|---|---|---|---|
| pockettts | multi |
refused | ValueError: PocketTTS does not support language 'pl'; supported: en, fr, de, pt, it, es. — correct |
| mlx-audio | multi |
refused | names the model, lists the languages, suggests an alternative — correct |
| kittentts | en |
rendered | English phonetics read over Polish text |
| audiocpp | en, zh |
rendered | wrong accent, unusable |
| indextts2 | zh en ja es ar |
rendered | wrong accent, unusable |
| confucius4-tts | multi |
rendered | Polish read with an American accent |
Duration shows the same thing without listening: the 30-second script came back as 55.2 s from kittentts and 57.2 s from audiocpp, because the phonemizer is fighting text it cannot read.
Two distinct defects
1. An engine that declares a language set does not enforce it.
KittenTTS is the clearest case (backend/services/tts_backend.py:1439):
language = kw.get("language")
if language and language.lower() not in {"en", "english", "auto"}:
logger.info(
"KittenTTS is English-only; ignoring language=%r — "
"use OmniVoice for multilingual synthesis.",
language,
)It knows, it says so in the backend log, and it synthesizes anyway. Nothing reaches the UI. audiocpp and indextts2 do not check at all.
supported_languages is already declared on every backend, so the check can be generic rather than per-engine.
2. Confucius4 declares ["multi"] for an engine with 14 languages.
backend/engines/confucius4/__init__.py:107:
def supported_languages(self) -> list[str]:
# 14 languages with the caller's language passed through at synthesize
# time; "multi" on the protocol surface.
return ["multi"]The comment names the real number. _normalize_language in its sidecar passes any two-letter code straight through to the model, so an unsupported language is answered with an accented approximation rather than an error. "multi" should be reserved for engines that really are open-ended (OmniVoice's 646 languages); an engine with a fixed list should declare that list.
Why this matters more than a wrong-sounding demo
The language picker offers every language for these engines, so a user picking Polish on KittenTTS or Confucius4 has no way to learn it will not work except by listening. On a batch job — the 50-video dubbing runs this project is used for — the failure is silent and per-segment.
Suggested fix
- Enforce
supported_languagesin one shared place before dispatch: when the set is not["multi"]and the requested language is outside it, raise the way PocketTTS and mlx-audio already do. Their messages are the model to copy — they name the engine, list what it does support, and point at an alternative. - Replace Confucius4's
["multi"]with its real 14-language list. - Have the language picker gray out (or warn on) languages the selected engine does not declare, so the refusal happens before generation rather than after.
- A test can assert that no registry engine except the genuinely open-ended ones declares
["multi"], and that a request outside a declared set raises.
Environment
Version: 0.5.2 (main @ eaf8bb9)
OS: macOS (Darwin 25.5.0), Apple M4 Pro, 24 GB unified memory
Compute device: MPS
Source: debpalash/OmniVoice-Studio