텍스트 응답 음성 재생 연결
This commit is contained in:
parent
64e06a1185
commit
d80e33da5e
9 changed files with 524 additions and 16 deletions
|
|
@ -12,7 +12,8 @@ from fastapi import HTTPException
|
|||
from .deps import Principal, Role
|
||||
from .persona_repository import PersonaVoiceMap
|
||||
from .routes import voice as voice_routes
|
||||
from .services.voice import VoicePreset
|
||||
from .services.voice import TTSChunk, VoicePreset
|
||||
from .store import TurnRecord
|
||||
|
||||
|
||||
SESSION_ID = "voice-ws-contract-session"
|
||||
|
|
@ -130,6 +131,98 @@ class VoiceWebSocketContractTest(unittest.IsolatedAsyncioTestCase):
|
|||
],
|
||||
)
|
||||
|
||||
def test_text_tts_uses_only_the_persisted_client_visible_reply(self) -> None:
|
||||
session = SimpleNamespace(
|
||||
turns=[
|
||||
TurnRecord(
|
||||
turn_seq=2,
|
||||
speaker="counselor",
|
||||
stage="초기",
|
||||
text="raw learner text",
|
||||
text_masked="masked learner text",
|
||||
),
|
||||
TurnRecord(
|
||||
turn_seq=2,
|
||||
speaker="client",
|
||||
stage="초기",
|
||||
text="raw client reply",
|
||||
text_masked="마스킹된 내담자 응답",
|
||||
),
|
||||
TurnRecord(
|
||||
turn_seq=3,
|
||||
speaker="client",
|
||||
stage="초기",
|
||||
text="hidden evaluator reply",
|
||||
text_masked="hidden evaluator reply",
|
||||
visible_to=("evaluator",),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
voice_routes._client_turn_text_for_speech(session, 2),
|
||||
"마스킹된 내담자 응답",
|
||||
)
|
||||
self.assertIsNone(voice_routes._client_turn_text_for_speech(session, 3))
|
||||
self.assertIsNone(voice_routes._client_turn_text_for_speech(session, 99))
|
||||
|
||||
async def test_text_turn_speech_returns_openai_audio_for_owned_persisted_turn(self) -> None:
|
||||
session = SimpleNamespace(
|
||||
persona=SimpleNamespace(code="P1"),
|
||||
turns=[
|
||||
TurnRecord(
|
||||
turn_seq=4,
|
||||
speaker="client",
|
||||
stage="초기",
|
||||
text="내담자 응답",
|
||||
text_masked="내담자 응답",
|
||||
)
|
||||
],
|
||||
)
|
||||
synthesized: list[tuple[str, VoicePreset]] = []
|
||||
|
||||
async def synthesize(text: str, voice: VoicePreset):
|
||||
synthesized.append((text, voice))
|
||||
yield TTSChunk(audio=b"mp3-a")
|
||||
yield TTSChunk(audio=b"mp3-b")
|
||||
|
||||
with patch.object(
|
||||
voice_routes,
|
||||
"_practice_access_error",
|
||||
AsyncMock(return_value=None),
|
||||
), patch.object(
|
||||
voice_routes.turn_runtime,
|
||||
"load_owned_session",
|
||||
AsyncMock(return_value=(session, None)),
|
||||
), patch.object(
|
||||
voice_routes,
|
||||
"_resolve_session_voice",
|
||||
AsyncMock(return_value=VOICE_PRESET),
|
||||
), patch.object(
|
||||
voice_routes.voice_service,
|
||||
"is_available",
|
||||
return_value=True,
|
||||
), patch.object(
|
||||
voice_routes.voice_service,
|
||||
"tts_provider",
|
||||
return_value="openai",
|
||||
), patch.object(
|
||||
voice_routes.voice_service,
|
||||
"synthesize_stream",
|
||||
new=synthesize,
|
||||
):
|
||||
response = await voice_routes.voice_speech(
|
||||
voice_routes.VoiceSpeechRequest(session_id=SESSION_ID, turn_seq=4),
|
||||
_principal(),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.media_type, "audio/mpeg")
|
||||
self.assertEqual(response.body, b"mp3-amp3-b")
|
||||
self.assertEqual(response.headers["cache-control"], "no-store")
|
||||
self.assertEqual(response.headers["x-vignette-tts-provider"], "openai")
|
||||
self.assertEqual(synthesized, [("내담자 응답", VOICE_PRESET)])
|
||||
|
||||
async def test_audio_start_binary_chunks_audio_end_ping_close_contract(self) -> None:
|
||||
websocket = FakeWebSocket(
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue