음성 재생과 운영 배포 정리

This commit is contained in:
Yun Chan 2026-06-28 12:18:20 +09:00
parent 8ed185ce6c
commit ac7db95542
1020 changed files with 46863 additions and 2175 deletions

View file

@ -268,14 +268,16 @@ OpenAI STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은
OpenAI가 아닌 provider row는 현재 live OpenAI TTS로 보내지 않고 기존 fallback을 사용한다.
dev 런타임 스키마 보강은 기존 DB의 `app.persona_voice_map` 누락도 복구해 seed materializer와
`/voice/ws` 바인딩이 같은 테이블을 사용하게 한다.
스트림 청크마다 립싱크 RMS 힌트(`estimate_chunk_rms`, PCM 디코딩 없이 바이트 에너지 근사)를 동봉한다.
프론트는 마이크 클릭 시 `AudioContext`를 먼저 resume해 재생 권한을 확보하고, TTS Blob을 Web Audio
buffer source로 재생하면서 `AnalyserNode`로 립싱크 RMS를 산출한다. 디코딩 실패 시 `<audio>` 재생으로
fallback한다.
- 키 없으면 명확히 degraded(`is_available()=False`, `VoiceUnavailable`). 라우트가 503/WS close로 변환.
`/voice/ws` WebSocket 캐스케이드(`routes/voice.py`):
```
client: audio_start → [binary chunks] → audio_end
server: ready → state(listening) → state(thinking) → transcript → reply
→ state(speaking) → tts_chunk(메타) + [binary audio] → tts_end → state(idle)
→ state(speaking) → [binary audio] → tts_end → state(idle)
```
- 인증: REST와 동일한 서버측 세션 쿠키를 WebSocket 쿠키에서 복원(`_principal_from_websocket`),
learner만 허용.
@ -361,11 +363,13 @@ GET {ENGINE_URL}/ready|/health — readiness/liveness
```
- `EngineMessage(role, content, cache)``cache`는 L0~L2 cache_control 힌트(게이트웨이가 해석).
- `GenerateRequest(ai_role, messages, tier, model, max_tokens, temperature, structured_schema, session_id, metadata)`
`tier`: `client`(Sonnet/Solar) / `feedback`(Opus) / `fast`(Haiku) 라우팅 힌트.
- `GenerateRequest(ai_role, messages, model, max_tokens, temperature, structured_schema, session_id, metadata)`.
- `session_id`를 넘기면 게이트웨이가 상주 풀을 재사용해 멀티턴 prompt caching 이점을 살린다.
- 장애는 `EngineError`로 전파 → 라우트가 503/SSE error 프레임으로 변환.
- 앱 전역 싱글톤 `engine_client`(lifespan에서 startup/shutdown).
- Node.js 교체 가능성을 위해 wire contract는 `app/contracts/engine_gateway.py`가 소유한다.
현재 FastAPI client와 Python gateway가 같은 `GenerateRequest`/`GenerateResponse`/`Stream*Event`
모델과 SSE `token|done|error` 프레임 helper를 공유한다.
### 2.11 in-memory 스토어 — `app/store.py` (degraded 폴백)
@ -416,7 +420,7 @@ cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
- **상주 풀** `/session`, `/session/{sid}/turn`, `/session/{sid}`(DELETE) — 회기 단위 프로세스.
- **stateless 어댑터**(engine_client 계약과 정합):
- `POST /v1/generate``EngineMessage[]``_split_messages()`로 (system_prompt, last_user) 분리
`--append-system-prompt`로 주입, 마지막 user를 stdin content로. structured_schema는
`--system-prompt`로 주입, 마지막 user를 stdin content로. structured_schema는
`_inject_schema()`로 system에 JSON 준수 지시 주입(claude -p는 response_format 미지원이라 차선).
- `POST /v1/stream``turn_stream()`이 assistant 텍스트 델타를 즉시 yield → SSE
`event: token` / `event: done`(provider/model/cost 메타) / `event: error` 프레이밍.
@ -453,7 +457,7 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
| `/admin` | Admin(운영 홈) | admin |
| `/admin/users` | Admin(사용자 관리) | admin |
| `/admin/access` | Admin(접근 권한) | admin |
| `/admin/tickets` | Admin(운영 티켓 처리 큐: 접수, 우선순위, 상태 변경) | admin |
| `/admin/tickets` | Admin(운영 티켓 처리 큐: 접수, 필터, 카테고리 큐, 우선순위, 수동 상태 변경 감사) | admin |
| `/settings` | Settings | 3역할 공통 |
- `RequireAuth``lib/auth`의 AuthContext로 가드하고, 권한 불일치 시 역할 홈(`roleHomePath`)으로 보낸다.
@ -523,7 +527,10 @@ DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에
- **운영 콘솔** `app.admin_health_event` — 관리자 `/admin/health` 조회 시점의 서비스별 헬스 샘플을
append-only로 남긴다. 이 값은 상시 모니터링 SLA가 아니라 운영 콘솔이 관측한 최근 샘플 이력이다.
`app.support_ticket`은 인증 사용자가 제출한 문제·불만·장애 티켓을 저장하고, 관리자는 `/admin/tickets`에서
우선순위와 처리 상태를 갱신한다. RLS는 티켓 제출자 본인 insert/select와 admin 전체 처리만 허용한다.
상태·카테고리·우선순위·담당그룹·정체·검색 필터와 카테고리 큐를 실제 DB 기준으로 처리한다.
티켓 접수와 관리자 수동 처리 변경은 `audit.audit_log`에 metadata-only로 남기며, 제목/본문 전문은
감사 로그에 복제하지 않는다. 중복 병합, 자동 수정 후보, 이슈/PR 생성은 운영자 승인 전까지 실행하지 않는다.
RLS는 티켓 제출자 본인 insert/select와 admin 전체 처리만 허용한다.
- **메모리 4계층**:
- ① WORKING `app.session_state` — 상태머신 수치 체크포인트(매 턴 UPSERT, openness/ideation CHECK 제약).
- ② EPISODIC 임베딩 `app.turn_embedding` — BGE-M3 dense `vector(1024)` + sparse, HNSW 인덱스.