[Bug] Unsupported languages are silently mis-spoken: three engines ignore the language they were given, and one declares "multi" while supporting 14

Author: martinezplCreated Sep 14, 2026Updated Sep 18, 2026
Labelsbugready-for-agent

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):

python
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:

python
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_languages in 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