fix(voice): instrucțiune de limbă inline + fallback Supertonic pe română
Markerul opac [tts-lang:en] depindea de regula din VOICE_MODE.md, aflată la ~30k caractere distanță în system prompt — modelul o rata pe ~1 din 5 turnuri. Răspunsul în română era apoi respins integral de pocket-tts (English-only) și turnul rămânea fără audio. - src/router.py: markerul poartă instrucțiunea inline (un singur token, acoperit în continuare de strip-ul anti-jailbreak); 5/5 EN la repro - src/voice/tts_stream.py: dacă modelul tot scapă română pe o voce pockettts, blocul cade pe Supertonic (M2, ro) în loc de tăcere; coada TTS transportă (clause, voice, lang) per clauză - tools/tts.py: looks_romanian public (folosit de tts_stream) - personality/VOICE_MODE.md: regula gated pe prefixul [tts-lang:en - tests: 4 teste noi fallback + expectații marker actualizate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
69
tests/test_tts_stream_fallback.py
Normal file
69
tests/test_tts_stream_fallback.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Fallback Supertonic pentru voice mode când modelul răspunde în română
|
||||
pe o voce pocket-tts (English-only).
|
||||
|
||||
Fără fallback, fiecare clauză ar fi respinsă de pocket-tts (diacritice RO)
|
||||
și turnul ar fi tăcere totală — bug observat 2026-07-11 („cât este ora" →
|
||||
răspuns RO scris, zero audio).
|
||||
"""
|
||||
import queue
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.voice.tts_stream import TTSQueue
|
||||
from tools.tts import DEFAULT_VOICE
|
||||
|
||||
|
||||
def _drain_items(q: queue.Queue) -> list:
|
||||
items = []
|
||||
while True:
|
||||
try:
|
||||
items.append(q.get_nowait())
|
||||
except queue.Empty:
|
||||
return items
|
||||
|
||||
|
||||
class TestPushTextLangRouting:
|
||||
@patch("src.voice.tts_stream.engine_for_voice", return_value="pockettts")
|
||||
def test_romanian_text_on_pockettts_falls_back_to_supertonic(self, _eng):
|
||||
ttsq = TTSQueue(voice_id="Marius 4")
|
||||
ttsq.push_text("Ora acum e nouă și cinci minute dimineața.")
|
||||
items = _drain_items(ttsq._text_queue)
|
||||
assert items, "clauzele trebuie să ajungă în coadă, nu să fie respinse"
|
||||
for clause, voice, lang in items:
|
||||
assert voice == DEFAULT_VOICE
|
||||
assert lang == "ro"
|
||||
|
||||
@patch("src.voice.tts_stream.engine_for_voice", return_value="pockettts")
|
||||
def test_english_text_on_pockettts_keeps_cloned_voice(self, _eng):
|
||||
ttsq = TTSQueue(voice_id="Marius 4")
|
||||
ttsq.push_text("It's five past nine in the morning, Bucharest time.")
|
||||
items = _drain_items(ttsq._text_queue)
|
||||
assert items
|
||||
for clause, voice, lang in items:
|
||||
assert voice == "Marius 4"
|
||||
assert lang == "en"
|
||||
|
||||
@patch("src.voice.tts_stream.engine_for_voice", return_value="supertonic")
|
||||
def test_supertonic_voice_unaffected(self, _eng):
|
||||
ttsq = TTSQueue(voice_id="M2")
|
||||
ttsq.push_text("Ora acum e nouă și cinci minute dimineața.")
|
||||
items = _drain_items(ttsq._text_queue)
|
||||
assert items
|
||||
for clause, voice, lang in items:
|
||||
assert voice == "M2"
|
||||
assert lang == "ro"
|
||||
|
||||
|
||||
class TestWorkerConsumesTuples:
|
||||
@patch("src.voice.tts_stream.synthesize", return_value={"ok": False, "error": "x"})
|
||||
def test_worker_unpacks_clause_voice_lang(self, mock_synth):
|
||||
ttsq = TTSQueue(voice_id="Marius 4")
|
||||
ttsq._text_queue.put(("Hello there.", "Marius 4", "en"))
|
||||
ttsq.start()
|
||||
try:
|
||||
import time
|
||||
deadline = time.monotonic() + 2.0
|
||||
while not mock_synth.called and time.monotonic() < deadline:
|
||||
time.sleep(0.02)
|
||||
finally:
|
||||
ttsq.stop()
|
||||
mock_synth.assert_called_once_with("Hello there.", voice="Marius 4", lang="en")
|
||||
Reference in New Issue
Block a user