feat: 운영 안정성과 세션 음성 경험 개선

This commit is contained in:
Yun Chan 2026-07-31 00:13:08 +09:00
parent facc4ad2d9
commit c788343467
95 changed files with 8431 additions and 1785 deletions

View file

@ -12,9 +12,12 @@ from .services.voice import (
TTS_ENDPOINT,
TTS_MODEL,
TTS_MODEL_FALLBACK,
HIGGS_TTS_ENDPOINT,
HIGGS_TTS_MODEL,
VoicePreset,
VoiceService,
assess_end_of_turn,
build_higgs_prompt,
build_tts_payload,
resolve_voice,
resolve_voice_from_map,
@ -96,6 +99,16 @@ class VoicePresetResolutionTest(unittest.TestCase):
class TTSPayloadTest(unittest.TestCase):
def test_higgs_prompt_keeps_first_word_before_emotion_tags(self) -> None:
voice = VoicePreset(preset="soft-young-fem", openai_voice="coral", rate=0.96)
prompt = build_higgs_prompt("그냥 학교 가도 아무 의미 없는 것 같아요.", voice)
self.assertTrue(prompt.startswith("그냥 "))
self.assertIn("<|emotion:helplessness|>", prompt)
self.assertIn("<|prosody:speed_slow|>", prompt)
self.assertNotIn("<|emotion:helplessness|> ", prompt)
def test_payload_contains_openai_tts_fields_and_clamps_high_speed(self) -> None:
voice = VoicePreset(
preset="soft-young-fem",
@ -219,6 +232,48 @@ class VoiceServiceStreamTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(payload["instructions"], "Keep the tone grounded.")
self.assertEqual([chunk.audio for chunk in chunks], [b"\x80\x80", b"\xff\x00"])
async def test_higgs_tts_uses_local_synthetic_voice_server_in_dev(self) -> None:
client = _CaptureTTSClient([b"RIFF", b"synthetic-wav"])
service = VoiceService(
api_key="",
environment="dev",
tts_provider="higgs",
higgs_base_url="http://127.0.0.1:9881",
)
service._higgs_client = client # type: ignore[assignment]
voice = VoicePreset(preset="soft-young-fem", openai_voice="coral")
chunks = [
chunk
async for chunk in service.synthesize_stream(
"엄마한테 말하지 않는 거죠?",
voice,
)
]
self.assertFalse(service.stt_available())
self.assertTrue(service.tts_available(voice))
self.assertEqual(service.tts_provider_for_voice(voice), "higgs")
self.assertEqual(service.tts_model_for_voice(voice), HIGGS_TTS_MODEL)
self.assertEqual(service.tts_media_type_for_voice(voice), "audio/wav")
self.assertEqual(len(client.calls), 1)
method, endpoint, payload = client.calls[0]
self.assertEqual((method, endpoint), ("POST", HIGGS_TTS_ENDPOINT))
self.assertIn("<|emotion:fear|>", str(payload["text"]))
self.assertEqual(payload["preset"], "soft-young-fem")
self.assertEqual(b"".join(chunk.audio for chunk in chunks), b"RIFFsynthetic-wav")
async def test_higgs_tts_is_fail_closed_outside_dev(self) -> None:
service = VoiceService(
api_key="",
environment="prod",
tts_provider="higgs",
)
voice = VoicePreset(preset="soft-young-fem", openai_voice="coral")
self.assertFalse(service.tts_available(voice))
self.assertEqual(service.tts_provider(), "disabled-non-dev")
async def test_dev_p1_sample_tts_streams_local_mp3_without_openai_key(self) -> None:
with TemporaryDirectory() as tmp:
sample_dir = Path(tmp)