Merge remote-tracking branch 'origin/main' into fix/review-2175
# Conflicts: # docs/engines/mlx-audio.md
This commit is contained in:
@@ -41,6 +41,7 @@ the frozen-backend fallback mirror it for their toolchains.
|
||||
- Correct the Windows Rustup installation command in tooling and documentation (#2066) — thanks @Rukhaam!
|
||||
- Show local setup guidance when remote native engine installation is unavailable (#2166)
|
||||
- Show scrubbed native error tails and exit codes for failed dubbing extraction (#2167)
|
||||
- Name every language the installed Kokoro table supports when rejecting an unsupported one (#2174) — thanks @paranoyouz-collab and @raya-mansouri!
|
||||
|
||||
### CI
|
||||
|
||||
|
||||
@@ -1624,6 +1624,7 @@ class KittenTTSBackend(TTSBackend):
|
||||
# resolved unchanged by `resolve_kokoro_lang_code()` below.
|
||||
_KOKORO_ISO_BY_FULL_NAME = {
|
||||
"english": "en",
|
||||
"british english": "en-gb",
|
||||
"spanish": "es",
|
||||
"french": "fr",
|
||||
"hindi": "hi",
|
||||
@@ -1634,6 +1635,31 @@ _KOKORO_ISO_BY_FULL_NAME = {
|
||||
}
|
||||
|
||||
|
||||
def _kokoro_supported_labels(aliases: dict, lang_codes: dict) -> list[str]:
|
||||
"""Human labels for every language Kokoro accepts, read from its own tables.
|
||||
|
||||
Derived from the installed package, never from
|
||||
``_KOKORO_ISO_BY_FULL_NAME``: that map exists to translate full names
|
||||
*into* Kokoro's codes, and reusing it to describe what Kokoro supports
|
||||
understates the model when a newly supported code has no full-name alias.
|
||||
Read every installed code so new languages remain visible without changing
|
||||
the input-name map.
|
||||
"""
|
||||
labels: dict[str, str] = {}
|
||||
# Prefer the full name a caller can actually pass.
|
||||
for name, iso in _KOKORO_ISO_BY_FULL_NAME.items():
|
||||
code = aliases.get(iso, iso)
|
||||
if code in lang_codes:
|
||||
labels.setdefault(code, name.title())
|
||||
# Then whatever the installed table supports that no full name reaches. Its
|
||||
# own description is a display name for some codes ("British English") and
|
||||
# an ISO tag for others ("pt-br"); either names the language better than
|
||||
# dropping it.
|
||||
for code, described in lang_codes.items():
|
||||
labels.setdefault(code, str(described))
|
||||
return sorted(labels.values())
|
||||
|
||||
|
||||
def resolve_kokoro_lang_code(language: str) -> str:
|
||||
"""Map a full language name / ISO code to Kokoro's single-letter
|
||||
`lang_code`, against the AUTHORITATIVE table read from the installed
|
||||
@@ -1651,7 +1677,12 @@ def resolve_kokoro_lang_code(language: str) -> str:
|
||||
iso = _KOKORO_ISO_BY_FULL_NAME.get(key, key)
|
||||
code = ALIASES.get(iso, iso)
|
||||
if code not in LANG_CODES:
|
||||
supported = ", ".join(sorted(name.title() for name in _KOKORO_ISO_BY_FULL_NAME))
|
||||
# Labels from newer installed tables must remain selectable even when
|
||||
# they have no entry in our compatibility map of full names.
|
||||
code = next((candidate for candidate, label in LANG_CODES.items()
|
||||
if str(label).strip().lower() == key), code)
|
||||
if code not in LANG_CODES:
|
||||
supported = ", ".join(_kokoro_supported_labels(ALIASES, LANG_CODES))
|
||||
raise ValueError(
|
||||
f"mlx-audio's Kokoro model (mlx-community/Kokoro-82M-bf16) doesn't "
|
||||
f"support language={language!r}. Kokoro supports: {supported}. "
|
||||
|
||||
@@ -89,3 +89,8 @@ preserve filter context at chunk boundaries; rate changes start a new group.
|
||||
Profile language refusals are terminal request errors on both local and remote
|
||||
rendering, including streaming. Electron and web show localized guidance that
|
||||
Auto inherits the profile language; neither silently retries the same refusal.
|
||||
|
||||
Kokoro language errors list every language in the installed model’s table.
|
||||
“British English” and `en-gb` both select its British English voice pipeline.
|
||||
Display names from newer installed Kokoro tables are accepted too, so a language
|
||||
advertised by the error message can be selected without updating a hardcoded map.
|
||||
|
||||
@@ -419,6 +419,82 @@ def test_mlx_audio_kokoro_rejects_unsupported_language_cleanly(language):
|
||||
assert "English" in msg # names what Kokoro DOES support
|
||||
|
||||
|
||||
# The label derivation takes the tables as arguments, so it runs on every
|
||||
# platform — unlike the resolution tests above, which need the real
|
||||
# Apple-Silicon-only package. The bug it guards is invisible on the runners
|
||||
# that skip.
|
||||
|
||||
|
||||
def test_kokoro_resolves_british_english_label(monkeypatch):
|
||||
pipeline = types.ModuleType("mlx_audio.tts.models.kokoro.pipeline")
|
||||
pipeline.ALIASES = {"en-gb": "b"}
|
||||
pipeline.LANG_CODES = {"b": "British English"}
|
||||
monkeypatch.setitem(sys.modules, "mlx_audio.tts.models.kokoro.pipeline", pipeline)
|
||||
assert tts_backend.resolve_kokoro_lang_code("British English") == "b"
|
||||
|
||||
|
||||
def test_kokoro_advertised_labels_round_trip_through_installed_table(monkeypatch):
|
||||
pipeline = types.ModuleType("mlx_audio.tts.models.kokoro.pipeline")
|
||||
pipeline.ALIASES = {"en": "a", "en-gb": "b", "xx": "x"}
|
||||
pipeline.LANG_CODES = {"a": "American English", "b": "British English", "x": "Newly Added Language"}
|
||||
monkeypatch.setitem(sys.modules, "mlx_audio.tts.models.kokoro.pipeline", pipeline)
|
||||
labels = tts_backend._kokoro_supported_labels(pipeline.ALIASES, pipeline.LANG_CODES)
|
||||
assert {tts_backend.resolve_kokoro_lang_code(label) for label in labels} == {"a", "b", "x"}
|
||||
|
||||
|
||||
def test_kokoro_supported_labels_name_a_code_only_an_alias_reaches():
|
||||
"""A code reached only through an alias is still named.
|
||||
|
||||
British English is `en-gb` -> "b". The supported list must include it
|
||||
alongside the other installed language codes.
|
||||
"""
|
||||
aliases = {"en": "a", "en-gb": "b", "es": "e"}
|
||||
lang_codes = {"a": "American English", "b": "British English", "e": "es"}
|
||||
labels = tts_backend._kokoro_supported_labels(aliases, lang_codes)
|
||||
assert "British English" in labels
|
||||
assert "English" in labels # the full name a caller can pass
|
||||
assert "Spanish" in labels
|
||||
|
||||
|
||||
def test_kokoro_supported_labels_track_the_installed_table():
|
||||
"""A language a later mlx-audio adds appears without editing our map.
|
||||
|
||||
That is the whole point of reading the vendored table rather than a
|
||||
hardcoded one.
|
||||
"""
|
||||
aliases = {"en": "a", "xx": "x"}
|
||||
lang_codes = {"a": "American English", "x": "Newly Added Language"}
|
||||
assert "Newly Added Language" in tts_backend._kokoro_supported_labels(aliases, lang_codes)
|
||||
|
||||
|
||||
def test_kokoro_supported_labels_prefer_the_passable_full_name():
|
||||
"""The label is the name a caller can actually pass.
|
||||
|
||||
`LANG_CODES` describes Spanish as the ISO tag "es", but "Spanish" is what
|
||||
the frontend sends, so that is the more useful label to print.
|
||||
"""
|
||||
labels = tts_backend._kokoro_supported_labels({"es": "e"}, {"e": "es"})
|
||||
assert labels == ["Spanish"]
|
||||
|
||||
|
||||
def test_kokoro_supported_labels_skip_codes_the_installed_table_lacks():
|
||||
"""An install exposing one language is not described as supporting eight.
|
||||
|
||||
Our map knows eight; the installed table is what decides.
|
||||
"""
|
||||
labels = tts_backend._kokoro_supported_labels({"it": "i"}, {"i": "it"})
|
||||
assert labels == ["Italian"]
|
||||
|
||||
|
||||
def test_mlx_audio_kokoro_error_names_british_english():
|
||||
"""The real message names British English, which it previously omitted."""
|
||||
pytest.importorskip("mlx_audio", reason="mlx-audio is Apple-Silicon-only")
|
||||
with pytest.raises(ValueError) as ei:
|
||||
tts_backend.resolve_kokoro_lang_code("Persian")
|
||||
# Reachable as "en-gb" and previously missing from the message.
|
||||
assert "British English" in str(ei.value)
|
||||
|
||||
|
||||
def test_mlx_audio_generate_rejects_unsupported_kokoro_language_before_calling_model():
|
||||
pytest.importorskip("mlx_audio", reason="mlx-audio is Apple-Silicon-only")
|
||||
backend = tts_backend.MLXAudioBackend()
|
||||
|
||||
Reference in New Issue
Block a user