diff --git a/CHANGELOG.md b/CHANGELOG.md index 96bfa20a..8ff4e0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ the frozen-backend fallback mirror it for their toolchains. - Optional FlashInfer acceleration for the default engine on CUDA (`OMNIVOICE_FLASHINFER=1`, ~2.2x measured) — needs the optional `flashinfer-python` package; missing package or kernel failure logs why and falls back to the standard path (#1565) - The bug reporter notices when you're on an outdated build and offers the latest release before filing — with a "File anyway" escape hatch — and stamps a `Build status` line into every report so up-to-date reports are tellable from stale ones (#1547) - Settings → Performance & Device gains a compute-device override (Auto / CUDA / ROCm / XPU / MPS / CPU, or `OMNIVOICE_DEVICE`) — pin the device when auto-detect picks wrong; only devices your machine actually has are offered (#1557) +- Opt-in 24-layer PocketTTS checkpoints via `OMNIVOICE_POCKETTTS_24L` — better prosody for it/de/es/pt at roughly 2x render time (still faster than real-time); the fast 6-layer model stays the default (#1613) — thanks @paoloantinori! ### Docs - The Docker Hub overview now shows the current engine-switching demo, Model Catalogue, and gallery voice workflow (#1593) @@ -33,6 +34,7 @@ the frozen-backend fallback mirror it for their toolchains. - The OmniVoice guide now covers combining style attributes with a reference clip (consistent instruct stabilizes cloning; the reference wins conflicts), inline pronunciation control (pinyin / CMU phonemes), and corrects the claim that the default engine can't do voice design — it can, from attributes (#1565) ### Fixed +- PocketTTS French works again — pocket-tts only ships a 24-layer French model and rejected the name the sidecar asked for, so every French request failed at model load; French now always loads `french_24l` (#1613) — thanks @paoloantinori! - Installing IndexTTS 2.5 no longer fails claiming an interrupted download — the weights repo ships `config.yaml` and VoiceStudio demanded a `config_v2_5.yaml` that exists in no upstream release; both names are accepted, so a hand-renamed checkout keeps working (#1611) — thanks @zuiaiyutu! - IndexTTS 2.5 no longer has long-text generation killed at 60 seconds — the sidecar now proves it is alive every 5 seconds while `infer()` runs, and its deadline rises to 900s (`OMNIVOICE_INDEXTTS_RECV_TIMEOUT_S`) (#1611) — thanks @zuiaiyutu! - The OpenAI-compatible `/v1/audio/speech` route now reuses the shared cached engine for explicit `model` ids instead of constructing a fresh engine — and its sidecar/model load, a ~28s floor per call for subprocess engines — on every request, with the same single-engine-resident discipline `/generate` applies (#1614) — thanks @paoloantinori! diff --git a/backend/engines/pockettts/main.py b/backend/engines/pockettts/main.py index 93055b5e..53549dd1 100644 --- a/backend/engines/pockettts/main.py +++ b/backend/engines/pockettts/main.py @@ -151,6 +151,40 @@ def _pocket_language(raw) -> str: ) +_TRUTHY = {"1", "true", "yes", "on"} + + +def _has_24l_config(language: str) -> bool: + """Whether the installed pocket-tts ships a 24-layer checkpoint for + ``language`` (it/de/es/pt/fr in 2.1.0; english has none).""" + try: + from pocket_tts.models.tts_model import CONFIGS_DIR # type: ignore[import-not-found] # noqa: PLC0415 + except Exception as exc: # noqa: BLE001 — absence of the package is not fatal here + # Log it, though: if a future pocket-tts moves CONFIGS_DIR, the 24L + # opt-in would otherwise go silently inert. + print(f"pockettts sidecar: 24l config probe failed: {exc!r}", file=sys.stderr) + return False + from pathlib import Path # noqa: PLC0415 + + return (Path(CONFIGS_DIR) / f"{language}_24l.yaml").is_file() + + +def _model_config_name(language: str) -> str: + """Pocket-tts config name to load: the 6-layer default, or the 24-layer + checkpoint when OMNIVOICE_POCKETTTS_24L is set and one exists for the + language. Opt-in only — defaults keep the fast model; the 24-layer variant + trades roughly 4x transformer compute for better prosody. + + French is the exception: pocket-tts 2.1.0 only ships a 24-layer French + model and load_model(language="french") raises, so French always maps to + french_24l regardless of the env var.""" + if language == "french": + return "french_24l" + if os.environ.get("OMNIVOICE_POCKETTTS_24L", "").strip().lower() not in _TRUTHY: + return language + return f"{language}_24l" if _has_24l_config(language) else language + + def _load_model(stdout, language: str): """Cold-construct the PocketTTS model for ``language`` (cached per language). Emits progress frames for the parent watchdog. Raises on failure (e.g. @@ -178,7 +212,7 @@ def _load_model(stdout, language: str): try: from pocket_tts import TTSModel # type: ignore[import-not-found] # noqa: PLC0415 - model = TTSModel.load_model(language=language) + model = TTSModel.load_model(language=_model_config_name(language)) _MODELS[language] = model finally: stop.set() diff --git a/docs/engines/pockettts.md b/docs/engines/pockettts.md index 9084074a..73f54e02 100644 --- a/docs/engines/pockettts.md +++ b/docs/engines/pockettts.md @@ -55,10 +55,15 @@ for this model. something an in-process engine cannot do. - The first use downloads the gated weights; the sidecar heartbeats progress during the download so the watchdog doesn't fire. +- **French always renders through the 24-layer checkpoint** (`french_24l`) — + pocket-tts ships no 6-layer French model — so French render speed is the + 24-layer figure (roughly half this page's headline speed, still faster + than real-time), not the 6-layer one. | Variable | Default | Meaning | | --- | --- | --- | | `OMNIVOICE_POCKETTTS_RECV_TIMEOUT_S` | `600` | Sidecar response deadline in seconds (min 30; cold loads download weights) | +| `OMNIVOICE_POCKETTTS_24L` | off | When truthy, load the 24-layer checkpoint for languages that ship one (it/de/es/pt/fr) instead of the 6-layer default. Better prosody at roughly 2x render time (still faster than real-time); no effect where no 24-layer model exists (e.g. English). Opt-in: the default stays the fast model. French always uses `french_24l` — pocket-tts ships no 6-layer French model and rejects `language="french"` | ## Known limits diff --git a/tests/test_pockettts_sidecar.py b/tests/test_pockettts_sidecar.py index 90915811..3f186495 100644 --- a/tests/test_pockettts_sidecar.py +++ b/tests/test_pockettts_sidecar.py @@ -25,6 +25,7 @@ import importlib.util import io import os import struct +import sys import threading from pathlib import Path @@ -405,3 +406,78 @@ def test_the_engine_is_registered_lazily(backend): def test_the_sidecar_script_path_resolves(backend): assert backend.sidecar_script().is_file() + + +# ── 24-layer opt-in (OMNIVOICE_POCKETTTS_24L) ──────────────────────────── + +class _NullStdout: + """Absorbs the sidecar's length-prefixed frames (bytes, not str).""" + + def write(self, data): + return len(data) + + def flush(self): + pass + + +def test_24l_opt_in_is_off_by_default(sc, monkeypatch): + monkeypatch.delenv("OMNIVOICE_POCKETTTS_24L", raising=False) + monkeypatch.setattr(sc, "_has_24l_config", lambda lang: True) + assert sc._model_config_name("italian") == "italian" + + +def test_24l_opt_in_selects_the_24_layer_config_when_available(sc, monkeypatch): + monkeypatch.setenv("OMNIVOICE_POCKETTTS_24L", "1") + monkeypatch.setattr(sc, "_has_24l_config", lambda lang: lang == "italian") + assert sc._model_config_name("italian") == "italian_24l" + # No 24-layer checkpoint for this language (e.g. english): unchanged. + assert sc._model_config_name("english") == "english" + + +def test_24l_opt_in_accepts_the_usual_truthy_spellings(sc, monkeypatch): + monkeypatch.setattr(sc, "_has_24l_config", lambda lang: True) + for val in ("true", "YES", " on ", "1"): + monkeypatch.setenv("OMNIVOICE_POCKETTTS_24L", val) + assert sc._model_config_name("german") == "german_24l", val + monkeypatch.setenv("OMNIVOICE_POCKETTTS_24L", "0") + assert sc._model_config_name("german") == "german" + + +def test_load_model_applies_the_24l_suffix_to_load_model(sc, monkeypatch): + """The load path must honour the opt-in, not just the name helper.""" + monkeypatch.setenv("OMNIVOICE_POCKETTTS_24L", "1") + monkeypatch.setattr(sc, "_has_24l_config", lambda lang: True) + + calls = {} + + class _FakeTTSModel: + @staticmethod + def load_model(language=None): + calls["language"] = language + return object() + + import types + fake_pkg = types.ModuleType("pocket_tts") + fake_pkg.TTSModel = _FakeTTSModel + monkeypatch.setitem(sys.modules, "pocket_tts", fake_pkg) + + sc._load_model(_NullStdout(), "italian") + assert calls["language"] == "italian_24l" + + +def test_french_always_maps_to_french_24l(sc, monkeypatch): + """pocket-tts 2.1.0 rejects language="french" outright (only a 24-layer + French model exists), so French must resolve to french_24l regardless of + the env var IN EITHER STATE and regardless of — indeed without calling — + the config probe. A regression that gates French on a truthy env value, + or that consults the probe, must fail here.""" + def _probe_must_not_run(lang): + raise AssertionError("french resolution must not consult _has_24l_config") + + monkeypatch.setattr(sc, "_has_24l_config", _probe_must_not_run) + # Env unset, falsy, and truthy — all identical for French. + monkeypatch.delenv("OMNIVOICE_POCKETTTS_24L", raising=False) + assert sc._model_config_name("french") == "french_24l" + for env in ("0", "off", "1", "true"): + monkeypatch.setenv("OMNIVOICE_POCKETTTS_24L", env) + assert sc._model_config_name("french") == "french_24l", env