feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -158,14 +158,26 @@ _PROVIDER_EVENT_TYPE_FIELDS = ("event_type", "type", "kind", "label")
|
|||
async def voice_health() -> JSONResponse:
|
||||
"""Return voice service readiness."""
|
||||
available = voice_service.is_available()
|
||||
stt_available = voice_service.stt_available()
|
||||
tts_available = voice_service.tts_available()
|
||||
tts_provider = voice_service.tts_provider()
|
||||
body = {
|
||||
"status": "ok" if available else "degraded",
|
||||
"available": available,
|
||||
"stt_available": stt_available,
|
||||
"tts_available": tts_available,
|
||||
"stt_model": voice_svc.STT_MODEL,
|
||||
"tts_model": voice_svc.TTS_MODEL,
|
||||
"tts_model": (
|
||||
voice_svc.HIGGS_TTS_MODEL
|
||||
if tts_provider == "higgs"
|
||||
else voice_svc.TTS_MODEL
|
||||
),
|
||||
"tts_provider": tts_provider,
|
||||
"reason": None if available else "OPENAI_API_KEY is not configured",
|
||||
"reason": (
|
||||
None
|
||||
if available
|
||||
else "STT 또는 TTS provider가 준비되지 않았습니다"
|
||||
),
|
||||
}
|
||||
return JSONResponse(body, status_code=200 if available else 503)
|
||||
|
||||
|
|
@ -192,12 +204,6 @@ async def voice_speech(
|
|||
access_error = await _practice_access_error(learner)
|
||||
if access_error is not None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=access_error)
|
||||
if not voice_service.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="OPENAI_API_KEY is not configured",
|
||||
)
|
||||
|
||||
sess, err = await turn_runtime.load_owned_session(
|
||||
body.session_id,
|
||||
learner,
|
||||
|
|
@ -224,6 +230,11 @@ async def voice_speech(
|
|||
persona_code=sess.persona.code,
|
||||
explicit_preset=None,
|
||||
)
|
||||
if not voice_service.tts_available(voice_preset):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="TTS provider is not configured",
|
||||
)
|
||||
try:
|
||||
chunks = [
|
||||
chunk.audio
|
||||
|
|
@ -248,11 +259,11 @@ async def voice_speech(
|
|||
)
|
||||
return Response(
|
||||
content=audio,
|
||||
media_type="audio/mpeg",
|
||||
media_type=voice_service.tts_media_type_for_voice(voice_preset),
|
||||
headers={
|
||||
"Cache-Control": "no-store",
|
||||
"X-Vignette-TTS-Model": voice_svc.TTS_MODEL,
|
||||
"X-Vignette-TTS-Provider": voice_service.tts_provider(),
|
||||
"X-Vignette-TTS-Model": voice_service.tts_model_for_voice(voice_preset),
|
||||
"X-Vignette-TTS-Provider": voice_service.tts_provider_for_voice(voice_preset),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -314,7 +325,7 @@ async def voice_ws(websocket: WebSocket) -> None:
|
|||
"session_id": session_id,
|
||||
"voice": voice_preset.openai_voice,
|
||||
"preset": voice_preset.preset,
|
||||
"tts_provider": voice_service.tts_provider(),
|
||||
"tts_provider": voice_service.tts_provider_for_voice(voice_preset),
|
||||
"state": "idle",
|
||||
**bind_meta,
|
||||
},
|
||||
|
|
@ -751,7 +762,7 @@ async def _run_turn_and_speak(
|
|||
"type": "state",
|
||||
"state": "speaking",
|
||||
"voice": context.voice_preset.openai_voice,
|
||||
"tts_provider": voice_service.tts_provider(),
|
||||
"tts_provider": voice_service.tts_provider_for_voice(context.voice_preset),
|
||||
},
|
||||
)
|
||||
try:
|
||||
|
|
@ -787,15 +798,35 @@ async def _load_voice_session(
|
|||
|
||||
|
||||
def _client_turn_text_for_speech(sess: InProcSession, turn_seq: int) -> str | None:
|
||||
"""Return the persisted client-visible reply for one completed turn."""
|
||||
for turn in reversed(sess.turns):
|
||||
if (
|
||||
turn.turn_seq == turn_seq
|
||||
and turn.speaker == "client"
|
||||
and turn.is_visible_to("client")
|
||||
):
|
||||
text = (turn.text_masked or turn.text).strip()
|
||||
return text or None
|
||||
"""완료된 상담 턴 번호에 대응하는 client-visible 응답을 반환한다.
|
||||
|
||||
degraded 인메모리 미러는 상담자/내담자 한 쌍이 같은 논리 turn_seq를 쓰지만,
|
||||
DB의 app.turns.seq는 발화마다 1씩 증가한다. DB 스냅샷에서 논리 1턴을
|
||||
그대로 seq=1로 찾으면 상담자 발화만 잡혀 TTS가 404가 되므로 두 저장 형태를
|
||||
명시적으로 구분한다.
|
||||
"""
|
||||
visible_clients = [
|
||||
turn
|
||||
for turn in sess.turns
|
||||
if turn.speaker == "client" and turn.is_visible_to("client")
|
||||
]
|
||||
counselor_sequences = {
|
||||
turn.turn_seq for turn in sess.turns if turn.speaker == "counselor"
|
||||
}
|
||||
paired_sequences = counselor_sequences.intersection(
|
||||
turn.turn_seq for turn in visible_clients
|
||||
)
|
||||
if paired_sequences or not counselor_sequences:
|
||||
match = next(
|
||||
(turn for turn in reversed(visible_clients) if turn.turn_seq == turn_seq),
|
||||
None,
|
||||
)
|
||||
else:
|
||||
index = turn_seq - 1
|
||||
match = visible_clients[index] if 0 <= index < len(visible_clients) else None
|
||||
if match is not None:
|
||||
text = (match.text_masked or match.text).strip()
|
||||
return text or None
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue