fix(tts): gate auto reply by ENABLE_TTS; allow on-demand and manual playback

- Pipeline: skip _send_tts_audio only for non-manual when ENABLE_TTS=false;
  remove enable_tts early return from handle_tts_request_on_demand.
- Tencent TTS: PrimaryLanguage/chunking follow user language preference only.
- Expo: let manual tts_audio bypass late-segment playback gate after interrupt.
- Docs: clarify ENABLE_TTS vs tts_request in api/.env.example and TTSProvider port.
- Tests: add manual bypass cases; adjust pipeline language tests for en+Chinese text.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-11 17:15:02 +08:00
parent ccdc4e4277
commit 93be60f74c
7 changed files with 101 additions and 17 deletions

View File

@@ -64,6 +64,29 @@ async def test_tencent_tts_zh_uses_primary_language_1_and_zh_voice() -> None:
assert seen["voice_type"] == 501004
@pytest.mark.asyncio
async def test_tencent_tts_en_user_language_uses_primary_en_even_if_text_is_chinese() -> None:
"""主语言与用户偏好一致:即使用户语言为 en 且正文为中文,也向 Tencent 提交 PrimaryLanguage=2。"""
provider = TencentTTSProvider(
secret_id="id",
secret_key="key",
voice_type=501004,
voice_type_en=501004,
)
seen: dict = {}
def fake_sync(text: str, voice_type: int, primary_language: int) -> bytes:
seen["primary_language"] = primary_language
seen["voice_type"] = voice_type
return b"OK"
with patch.object(provider, "_synthesize_sync", side_effect=fake_sync):
out = await provider.synthesize("这是中文回复。", language="en")
assert out == b"OK"
assert seen["primary_language"] == PRIMARY_LANGUAGE_EN
@pytest.mark.asyncio
async def test_tencent_tts_en_uses_primary_language_2_and_en_voice() -> None:
provider = TencentTTSProvider(

View File

@@ -0,0 +1,65 @@
"""ENABLE_TTS=false 时仍可走喇叭按需合成;自动回复路径则被关闭。"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from app.features.conversation.ws import pipeline as pl
@pytest.mark.asyncio
async def test_send_tts_manual_bypasses_enable_tts_false(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pl.settings, "enable_tts", False)
fake_tts = MagicMock()
fake_tts.synthesize = AsyncMock(return_value=b"\xff\xd3-mp3stub")
monkeypatch.setattr(pl, "get_tts_provider", lambda: fake_tts)
storage = MagicMock()
storage.upload.return_value = "https://example/public.wav"
storage.get_url.return_value = "https://example/signed.wav"
monkeypatch.setattr(pl, "get_object_storage", lambda: storage)
send_mock = AsyncMock()
monkeypatch.setattr(pl.manager, "send_message", send_mock)
monkeypatch.setattr(pl, "_tts_epoch_value", lambda _cid: 0)
cid = "c0000000-0000-4000-8000-000000000001"
out = await pl._send_tts_audio(
cid,
"hi",
chunk_index=0,
chunk_total=1,
assistant_message_id="m1",
tts_epoch_start=0,
manual=True,
language="en",
)
assert out == "https://example/public.wav"
fake_tts.synthesize.assert_awaited_once()
send_mock.assert_awaited_once()
@pytest.mark.asyncio
async def test_send_tts_auto_blocked_when_enable_tts_false(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pl.settings, "enable_tts", False)
fake_tts = MagicMock()
fake_tts.synthesize = AsyncMock(return_value=b"audio")
monkeypatch.setattr(pl, "get_tts_provider", lambda: fake_tts)
cid = "c0000000-0000-4000-8000-000000000002"
out = await pl._send_tts_audio(
cid,
"hi",
chunk_index=0,
chunk_total=1,
assistant_message_id="m1",
tts_epoch_start=0,
manual=False,
language="en",
)
assert out is None
fake_tts.synthesize.assert_not_called()