feat(pockettts): opt-in 24-layer checkpoints via OMNIVOICE_POCKETTTS_24L (#1613)

Adds an opt-in 24-layer PocketTTS checkpoint path, with French correctly using its required 24-layer model.
This commit is contained in:
Paolo Antinori
2026-08-20 20:42:40 +00:00
committed by GitHub
parent 43f1d46fe6
commit 3f5114923b
4 changed files with 118 additions and 1 deletions
+2
View File
@@ -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!
+35 -1
View File
@@ -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()
+5
View File
@@ -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
+76
View File
@@ -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