stop truncating the one language code that is longer than two letters

Probing XTTS-v2's seventeen languages through the running endpoint: sixteen
returned audio and zh-cn failed with KeyError: 'zh'. Our own call site did it.
`language=lang[:2]` truncated every code to two characters, and zh-cn is the
only XTTS code longer than that, so the single language with a regioned code was
the single language that could not be spoken.

Resolution now goes through XTTS's own declared list, which also fixes the other
direction: a client sending a locale ("en-us", "pt-br", "ZH_CN") lands on the
code XTTS actually has instead of a near miss.

tools/xtts_service.py had the identical truncation. It runs in coqui's own venv
and cannot import the server package, so the list is duplicated there — and two
tests keep them honest: one compares the declared lists, the other executes the
worker's own normaliser and asserts it answers exactly as the server's does.

Also updates the bark voice-count test, which pinned the English-only list at
ten. That assumption was the bug.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent 0e8e24e2
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.5.40" __version__ = "0.5.41"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -101,6 +101,33 @@ _FAMILY_LANGUAGES: dict[str, list[str]] = { ...@@ -101,6 +101,33 @@ _FAMILY_LANGUAGES: dict[str, list[str]] = {
} }
def xtts_language(lang: str) -> str:
"""``lang`` as XTTS-v2 names it.
XTTS keys its speaker/phonemiser tables by its own codes, and one of them is
not two letters: Chinese is ``zh-cn``. The call site used to truncate to
``lang[:2]``, which turned the only such code into ``zh`` and made XTTS
raise ``KeyError: 'zh'`` -- so the one language whose code is longer than two
characters was the one language that could not be spoken. Measured
2026-10-30: the other sixteen all worked.
Resolution is against XTTS's own declared list, so a region a caller sends
("en-us", "pt-br") lands on the code XTTS actually has ("en", "pt").
"""
want = str(lang or "en").strip().lower().replace("_", "-")
declared = _FAMILY_LANGUAGES["coqui"]
if want in declared:
return want
base = want.split("-")[0]
if base in declared:
return base
# A regioned XTTS code reached by its bare prefix -- "zh" -> "zh-cn".
for code in declared:
if code.split("-")[0] == base:
return code
return base
def family_languages(model_name: str, config: Optional[dict] = None) -> list[str]: def family_languages(model_name: str, config: Optional[dict] = None) -> list[str]:
"""Languages this model can speak, or [] when nothing is known. """Languages this model can speak, or [] when nothing is known.
...@@ -375,7 +402,7 @@ class _CoquiBackend: ...@@ -375,7 +402,7 @@ class _CoquiBackend:
# or fall back to a default. `voice` may be a wav path, a built-in # or fall back to a default. `voice` may be a wav path, a built-in
# speaker name, or (e.g. a Kokoro id like "af_sarah") neither — so only # speaker name, or (e.g. a Kokoro id like "af_sarah") neither — so only
# treat it as a clone source when it's an actual file. # treat it as a clone source when it's an actual file.
kwargs = {"text": text, "language": (lang or "en")[:2]} kwargs = {"text": text, "language": xtts_language(lang)}
speakers = list(getattr(self._tts, "speakers", None) or []) speakers = list(getattr(self._tts, "speakers", None) or [])
cfg_wav = self._cfg.get("speaker_wav") cfg_wav = self._cfg.get("speaker_wav")
if voice and os.path.isfile(voice): if voice and os.path.isfile(voice):
......
...@@ -366,3 +366,82 @@ def test_every_language_kokoro_advertises_resolves_to_something_espeak_takes(): ...@@ -366,3 +366,82 @@ def test_every_language_kokoro_advertises_resolves_to_something_espeak_takes():
assert resolved in measured_bare_ok or code in T._ESPEAK_REGIONLESS, ( assert resolved in measured_bare_ok or code in T._ESPEAK_REGIONLESS, (
f"Kokoro advertises {code!r} but it resolves to {resolved!r}, " f"Kokoro advertises {code!r} but it resolves to {resolved!r}, "
f"which was never measured against espeak") f"which was never measured against espeak")
# ── XTTS keys by its OWN codes, one of which is not two letters ──────────────
#
# Probing XTTS's seventeen on 2026-10-30: sixteen returned audio and `zh-cn`
# failed with `KeyError: 'zh'`. Our own call site did it -- `language=lang[:2]`
# truncated the one XTTS code longer than two characters, so the single language
# with a regioned code was the single language that could not be spoken.
def test_chinese_keeps_its_region():
"""The regression: zh-cn[:2] == 'zh', which XTTS has no table entry for."""
assert T.xtts_language("zh-cn") == "zh-cn"
def test_a_bare_zh_reaches_the_regioned_code():
"""Clients send 'zh'; XTTS only has 'zh-cn'."""
assert T.xtts_language("zh") == "zh-cn"
@pytest.mark.parametrize("code", ["en", "es", "fr", "de", "it", "pt", "pl",
"tr", "ru", "nl", "cs", "ar", "hu", "ko",
"ja", "hi"])
def test_the_two_letter_codes_are_unchanged(code):
assert T.xtts_language(code) == code
@pytest.mark.parametrize("sent,expect", [("en-us", "en"), ("en_GB", "en"),
("pt-br", "pt"), ("ZH_CN", "zh-cn")])
def test_a_region_a_client_sends_lands_on_the_code_xtts_has(sent, expect):
"""OpenAI-compatible clients send locales; XTTS mostly wants bare codes."""
assert T.xtts_language(sent) == expect
def test_every_language_xtts_advertises_resolves_into_its_own_table():
"""The invariant that broke: nothing may be advertised that then raises."""
for code in T.family_languages("coqui/XTTS-v2"):
assert T.xtts_language(code) in T._FAMILY_LANGUAGES["coqui"], code
def test_the_call_site_no_longer_truncates():
src = open(os.path.join(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))), 'codai', 'api', 'tts_backends.py')).read()
head = src.index("class _CoquiBackend") if "class _CoquiBackend" in src \
else src.index('family = "coqui"')
body = src[head:head + 3000]
assert '"language": xtts_language(lang)' in body
assert '(lang or "en")[:2]' not in body
def test_the_isolated_worker_agrees_with_the_server():
"""tools/xtts_service.py runs in coqui's own venv and cannot import codai,
so the list is duplicated there. Duplicated is fine; diverging is not --
the worker is what actually synthesises when XTTS is run isolated."""
import re
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'tools', 'xtts_service.py')
src = open(path).read()
m = re.search(r'_XTTS_LANGUAGES\s*=\s*\[(.*?)\]', src, re.S)
assert m, 'the worker no longer declares its language list'
worker = [x.strip().strip('"\'') for x in m.group(1).split(',') if x.strip()]
assert worker == T._FAMILY_LANGUAGES["coqui"], (
f'worker and server disagree:\n worker: {worker}\n'
f' server: {T._FAMILY_LANGUAGES["coqui"]}')
assert '(req.get("language") or "en")[:2]' not in src
assert '_xtts_language(req.get("language"))' in src
def test_the_worker_normaliser_behaves_like_the_servers():
"""Same inputs, same answers -- checked by running the worker's own copy."""
import importlib.util
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'tools', 'xtts_service.py')
src = open(path).read()
ns: dict = {}
start = src.index('_XTTS_LANGUAGES = [')
end = src.index('def _tts(', start)
exec(compile(src[start:end], '<worker>', 'exec'), ns)
for code in ("zh", "zh-cn", "en", "en-us", "pt-br", "it", "ja", "", None, "xx"):
assert ns['_xtts_language'](code) == T.xtts_language(code), code
...@@ -65,10 +65,17 @@ def test_an_id_that_carries_nothing_reports_none_not_a_guess(): ...@@ -65,10 +65,17 @@ def test_an_id_that_carries_nothing_reports_none_not_a_guess():
def test_bark_is_listed_without_loading_anything(): def test_bark_is_listed_without_loading_anything():
"""The static list was the ten English speakers. suno/bark ships a bank of
13 languages x speakers 0-9, and listing only the English ten is what made
a 13-language engine look like an English one. English stays first so a
client taking voices[0] as its default is unaffected."""
got = T.family_voices("suno/bark", None) got = T.family_voices("suno/bark", None)
assert got["voices_source"] == "static" assert got["voices_source"] == "static"
assert len(got["voices"]) == 10 assert len(got["voices"]) == 13 * 10
assert got["voices"][0]["languages"] == ["en"] assert got["voices"][0]["languages"] == ["en"]
assert all(v["languages"] == ["en"] for v in got["voices"][:10])
langs = {l for v in got["voices"] for l in v["languages"]}
assert langs == set(T._FAMILY_LANGUAGES["bark"])
def test_parler_says_it_is_described_rather_than_chosen(): def test_parler_says_it_is_described_rather_than_chosen():
......
...@@ -78,11 +78,37 @@ def _load(name: str) -> dict: ...@@ -78,11 +78,37 @@ def _load(name: str) -> dict:
"sample_rate": _state["sr"]} "sample_rate": _state["sr"]}
#: XTTS-v2's own language codes. Duplicated from codai.api.tts_backends on
#: purpose: this worker runs in its own venv with coqui's dependency set and
#: cannot import the server package. tests/test_tts_languages_are_published.py
#: asserts the two lists stay identical.
_XTTS_LANGUAGES = ["en", "es", "fr", "de", "it", "pt", "pl", "tr", "ru", "nl",
"cs", "ar", "zh-cn", "hu", "ko", "ja", "hi"]
def _xtts_language(lang) -> str:
"""``lang`` as XTTS names it -- see the note on tts_backends.xtts_language.
The bug this replaces: ``lang[:2]`` turned "zh-cn", the one XTTS code longer
than two characters, into "zh", and XTTS raised ``KeyError: 'zh'``.
"""
want = str(lang or "en").strip().lower().replace("_", "-")
if want in _XTTS_LANGUAGES:
return want
base = want.split("-")[0]
if base in _XTTS_LANGUAGES:
return base
for code in _XTTS_LANGUAGES:
if code.split("-")[0] == base:
return code
return base
def _tts(req: dict) -> dict: def _tts(req: dict) -> dict:
tts = _state["tts"] tts = _state["tts"]
if tts is None: if tts is None:
return {"ok": False, "error": "no model loaded"} return {"ok": False, "error": "no model loaded"}
kwargs = {"text": req.get("text") or "", "language": (req.get("language") or "en")[:2]} kwargs = {"text": req.get("text") or "", "language": _xtts_language(req.get("language"))}
for key in ("speaker", "speaker_wav"): for key in ("speaker", "speaker_wav"):
if req.get(key): if req.get(key):
kwargs[key] = req[key] kwargs[key] = req[key]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment