fix(tts): pocket-tts refuză text cu diacritice RO în loc să-l pronunțe greșit

Kyutai pocket-tts nu suportă română (vezi eval anterior). Codul rula
oricum sinteza pe orice text primit; acum _synthesize_pockettts
respinge diacriticele RO înainte de request, fără fallback silențios
pe altă voce.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:30:12 +00:00
parent a043e2f177
commit 00ac971044
3 changed files with 34 additions and 0 deletions

View File

@@ -73,6 +73,7 @@
#### TTS: engine pocket-tts + Supertonic
- **Motoare:** `supertonic` (voci predefinite M1-M5/F1-F5, server local `:7788`) și `pockettts` (Kyutai pocket-tts, voice cloning din sample WAV, server local `:7789`, config `tts.pockettts_url`).
- **Rutare (`tools/tts.py::synthesize`):** engine-ul se decide din catalogul `tts_voices.json` — fiecare voce are un `engine` asociat. Voce necatalogată → cade pe `tts.default_engine` din `config.json`. Dacă pocket-tts e indisponibil tehnic (connect error / 5xx) → fallback automat pe Supertonic pentru cererea curentă; erorile de conținut (voce inexistentă, fișier `.safetensors` lipsă) NU declanșează fallback, se raportează direct.
- **pocket-tts e engleză-only:** modelul nu suportă română. `tools/tts.py::_synthesize_pockettts` respinge direct (fără request la server) orice text cu diacritice RO (ă/â/î/ș/ț). Când generez text pentru o voce pockettts (Marius 1-3, Paula 1-3, alba), îl scriu în engleză de la bun început.
- **Catalog voci:** `tts_voices.json` — sursă live pentru autocomplete pe `/audio` și `/voice setvoice` (o voce nouă adăugată apare imediat, fără redeploy). Intrare pocket-tts: `{"engine": "pockettts", "state_path": "models/voices/<slug>.safetensors"}`. Intrare supertonic: `{"engine": "supertonic"}`.
- **`/voice engine <pockettts|Supertonic>`:** schimbă `tts.default_engine` în config.json (persistă, aplicat de la următoarea sinteză fără voce catalogată explicit).
- **`/voice setvoice <voce>`:** schimbă vocea implicită (autocomplete din `tts_voices.json`); dacă sunt deja în voice channel, swap live pe sesiunea curentă.

View File

@@ -128,6 +128,24 @@ class TestPockettsFallback:
assert result["ok"] is False
assert "400" in result["error"]
@patch("httpx.post")
def test_romanian_diacritics_rejected_without_calling_server(self, mock_post):
result = _synthesize_pockettts("Salut, cum îți este ziua?", {})
assert result["ok"] is False
assert "engleză" in result["error"]
mock_post.assert_not_called()
@patch("tools.tts._load_voice_catalog")
@patch("httpx.post")
def test_romanian_text_does_not_fall_back_to_supertonic(self, mock_post, mock_catalog):
mock_catalog.return_value = {"Marius 1": {"engine": "pockettts"}}
result = synthesize("Bună dimineața!", voice="Marius 1")
mock_post.assert_not_called()
assert result["ok"] is False
assert result["engine_used"] == "pockettts"
class TestSupertonicLangNaRetry:
@patch("tools.tts._load_voice_catalog")

View File

@@ -53,6 +53,15 @@ class _PocketTTSUnavailable(Exception):
"""pocket-tts server e nereachable sau eșuează server-side (5xx) — caller-ul trebuie să facă fallback pe Supertonic."""
# pocket-tts e monolingv engleză per deployment — diacriticele RO sunt un semnal
# sigur că textul n-a fost tradus (vezi personality/TOOLS.md).
_RO_DIACRITICS = set("ăâîșțĂÂÎȘȚşţŞŢ")
def _looks_romanian(text: str) -> bool:
return any(ch in _RO_DIACRITICS for ch in text)
def sanitize_for_supertonic(text: str) -> str:
"""Replace Unicode punctuation and strip chars that crash Supertonic's ONNX model."""
for src, dst in _TTS_PUNCT_MAP.items():
@@ -172,6 +181,12 @@ def _synthesize_pockettts(text: str, entry: dict) -> dict:
Raises:
_PocketTTSUnavailable: eșec tehnic (connect error/timeout/5xx) — caller-ul face fallback.
"""
if _looks_romanian(text):
return {
"ok": False,
"error": "pocket-tts nu suportă română — textul trebuie să fie în engleză.",
}
state_path = entry.get("state_path")
voice_url = entry.get("voice_url")