From 00ac9710444b8794d1cc7668c56b88a2f3ba016d Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Sat, 11 Jul 2026 17:30:12 +0000 Subject: [PATCH] =?UTF-8?q?fix(tts):=20pocket-tts=20refuz=C4=83=20text=20c?= =?UTF-8?q?u=20diacritice=20RO=20=C3=AEn=20loc=20s=C4=83-l=20pronun=C8=9Be?= =?UTF-8?q?=20gre=C8=99it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- personality/TOOLS.md | 1 + tests/test_tts.py | 18 ++++++++++++++++++ tools/tts.py | 15 +++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/personality/TOOLS.md b/personality/TOOLS.md index e4a3fe3..92a8683 100644 --- a/personality/TOOLS.md +++ b/personality/TOOLS.md @@ -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/.safetensors"}`. Intrare supertonic: `{"engine": "supertonic"}`. - **`/voice engine `:** schimbă `tts.default_engine` în config.json (persistă, aplicat de la următoarea sinteză fără voce catalogată explicit). - **`/voice setvoice `:** schimbă vocea implicită (autocomplete din `tts_voices.json`); dacă sunt deja în voice channel, swap live pe sesiunea curentă. diff --git a/tests/test_tts.py b/tests/test_tts.py index b1c3225..e37bbe1 100644 --- a/tests/test_tts.py +++ b/tests/test_tts.py @@ -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") diff --git a/tools/tts.py b/tools/tts.py index bdbd7a4..dd37f68 100644 --- a/tools/tts.py +++ b/tools/tts.py @@ -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")