8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
236 lines
15 KiB
Markdown
236 lines
15 KiB
Markdown
# G7 로컬 음성 WebSocket 내구성 증거 — 2026-08-07
|
|
|
|
## 판정
|
|
|
|
로컬 loopback Uvicorn 서버가 실제 제품 라우터의 `/voice/ws`를 제공하도록 구성하고,
|
|
결정적 synthetic PCM으로 짧은 WebSocket soak를 통과했다. 이 결과는 **물리 마이크,
|
|
공개 WSS, 실제 PostgreSQL 동의 원장, 외부 STT/TTS/LLM provider의 장시간 안정성 증거가 아니다.**
|
|
|
|
- 실행기: `scripts/soak-voice-websocket.py`
|
|
- 실제 통과한 경로: Uvicorn WebSocket transport → FastAPI router → `/voice/ws`
|
|
- 짧은 실행 시간: 요청 `8초`, 실제 timed soak `8.015초`
|
|
- 종료 코드: `0` (전 계약 통과)
|
|
- 50분 옵션: `--duration-seconds 3000`을 제공하지만 이번 검증에서는 실행하지 않았다.
|
|
|
|
## 실제 경로와 override 경계
|
|
|
|
실제 코드로 통과한 부분은 WebSocket accept/close, 인증·역할 분기, session bind 이후 ready frame,
|
|
ping/pong, `audio_start`/binary/`audio_end`, PCM→WAV 정규화, 발화마다 수행하는 G7 consent
|
|
preflight, transcript/reply/TTS frame 순서, 발화 byte cap, overflow 뒤 동일 연결 복구, close code다.
|
|
|
|
외부 비용과 비결정성을 제거하기 위해 아래 경계만 명시적으로 override했다.
|
|
|
|
1. `_principal_from_websocket`: 승인된 synthetic learner 또는 인증 거부 상태
|
|
2. `_bind_session`: synthetic 기존 회기 ID와 고정 voice preset
|
|
3. `multimodal_alliance_store.assert_voice_processing_allowed`: 회기별 in-memory
|
|
`granted`/`withdrawn`/미동의 gate
|
|
4. `voice_service.transcribe`: 입력이 실제 WAV 정규화를 통과했는지 검사한 뒤 고정 transcript 반환
|
|
5. `_run_turn_and_speak`: 고정 reply와 1개 synthetic binary TTS frame 반환
|
|
6. `voice_service.is_available`: provider 정상/degraded 종료 분기를 결정적으로 전환
|
|
7. `_MAX_AUDIO_BYTES`: overflow를 빠르게 검증하기 위해 운영 `10MiB`를 실행 중에만 `4KiB`로 축소
|
|
|
|
따라서 consent grant/withdraw 검증은 실제 DB의 consent HTTP API를 호출한 결과가 아니라,
|
|
제품 voice route가 참조하는 store 경계의 상태를 결정적으로 전환한 결과다. 다만 철회 전 첫 발화는
|
|
synthetic STT seam까지 1회 도달하고, 같은 socket에서 철회한 뒤의 발화와 철회 상태로 재연결한
|
|
발화는 둘 다 STT 호출 수를 늘리지 않아 처리 전 fail-closed를 확인했다.
|
|
|
|
## 짧은 soak 결과
|
|
|
|
| 항목 | 결과 |
|
|
|---|---:|
|
|
| 연결 열림 / 완료 | 83 / 83 |
|
|
| 정상 close `1000` | 81 |
|
|
| 미인증 close `1008` | 1 |
|
|
| provider degraded close `1011` | 1 |
|
|
| 성공 synthetic 발화 | 79 |
|
|
| synthetic STT / TTS frame | 79 / 79 |
|
|
| 유료 provider 호출 | 0 |
|
|
| grant 상태 동일 회기 재연결 | 통과 |
|
|
| 같은 socket 철회 후 STT 이전 차단 | 통과 |
|
|
| 철회 상태 동일 회기 재연결 차단 | 통과 |
|
|
| 미동의 회기 차단 | 통과 |
|
|
| overflow 거부 / 이후 복구 | 1 / 1 |
|
|
| 예기치 않은 오류 | 0 |
|
|
|
|
종료 코드별 계약은 인증 거부 `1008`, provider 준비 실패 `1011`, 정상 client close `1000`으로
|
|
확인했다. 모든 연결이 완료되어 러너 관점의 미종료 WebSocket은 없었다.
|
|
|
|
## bounded queue와 backpressure에 대한 정확한 범위
|
|
|
|
러너의 Uvicorn/WebSockets inbound queue는 `ws_max_queue=4`로 제한했다. 클라이언트는 수신을
|
|
기다리지 않고 1KiB frame 5개를 연속 전송했고, 제품 라우터의 발화 buffer cap은
|
|
`audio too large; please send a shorter utterance`로 이를 거부했다. overflow payload는 synthetic
|
|
STT seam에 도달하지 않았으며, 같은 socket에서 다음 정상 발화가 성공했다.
|
|
|
|
후속 완료감사에서 route-owned audio buffer는 chunk를 붙이기 전에 운영 `10MiB` 상한을 검사하도록
|
|
바꿨다. transcript event queue는 `32`, Deepgram client receive queue는 `16`, write limit은 `64KiB`로
|
|
고정했다. queue가 찼을 때 event producer의 `await put()`이 실제로 대기하고 consumer가 한 건을 꺼낸 뒤
|
|
재개되는 회귀도 통과했다.
|
|
|
|
이 증거가 말할 수 있는 범위는 다음과 같다.
|
|
|
|
- loopback transport queue를 명시적으로 bounded 설정한 상태에서 burst와 재연결이 hang 없이 완료됨
|
|
- 애플리케이션 발화 buffer가 상한 초과 chunk를 append 전에 버리고, provider 호출 없이 idle→다음 발화로 복구함
|
|
- route transcript event와 Deepgram transport queue가 유한하며 queue 포화 시 producer에 backpressure를 적용함
|
|
- 단일 Python 프로세스에서 운영 10MiB buffer high-water와 초과 1 byte 거부를 직접 실행함. 이때 RSS는
|
|
`77,541,376→88,092,672 bytes`, process peak 증분은 `20,279,296 bytes`, clear 뒤 `77,602,816 bytes`였다.
|
|
|
|
반대로 이 실행은 실제 운영 worker 수에서의 동시 connection RSS/CPU, Uvicorn 내부 queue high-water mark,
|
|
OS socket buffer, Cloudflare/proxy queue를 측정하지 않았다. 따라서 단일 프로세스·단일 buffer 수치를 전체
|
|
운영 topology의 절대 메모리 상한이나 50분 backpressure 증명으로 확대하지 않는다.
|
|
|
|
## 실행 명령
|
|
|
|
이번에 실제 실행한 짧은 모드:
|
|
|
|
```powershell
|
|
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 `
|
|
scripts\soak-voice-websocket.py
|
|
```
|
|
|
|
선택적 50분 모드(이번에는 실행하지 않음):
|
|
|
|
```powershell
|
|
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 `
|
|
scripts\soak-voice-websocket.py --duration-seconds 3000
|
|
```
|
|
|
|
CLI 종료 코드는 전 계약 통과 `0`, 계약/transport 실패 `1`, 잘못된 인자 `2`다.
|
|
|
|
## 관련 회귀
|
|
|
|
```powershell
|
|
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest `
|
|
-p no:cacheprovider `
|
|
apps/api/app/test_voice_ws.py `
|
|
apps/api/app/test_multimodal_alliance_store.py `
|
|
apps/api/app/test_multimodal_alliance.py -q
|
|
```
|
|
|
|
결과: `42 passed`, Starlette `python_multipart` 전환 관련 기존 경고 1건.
|
|
|
|
`test_voice_ws.py`에는 한 socket의 첫 PCM 발화는 STT까지 도달하지만, 두 번째 발화 직전에
|
|
consent가 철회되면 `multimodal_consent_withdrawn` 오류와 idle만 보내고 STT를 다시 호출하지 않는
|
|
회귀를 추가했다.
|
|
|
|
같은 Python 3.11로 `apps/api/app`과 `apps/api/engine_gateway` 전체 회귀도 실행했고
|
|
`805 passed`, 동일한 기존 경고 1건으로 통과했다.
|
|
|
|
## 내부 완료감사 추가 증거 — 2026-08-07
|
|
|
|
### 실제 PostgreSQL 동시 철회
|
|
|
|
`scripts/smoke-multimodal-consent-concurrency.py`를 개발 PostgreSQL `vignette-dev-db`에 대해 실행했다.
|
|
provider·마이크·로그인 없이 고유 synthetic learner/session을 만들고, 철회 transaction이 session 공통
|
|
consent lock을 잡은 상태에서 evaluator timeline write를 동시에 시작했다.
|
|
|
|
- 철회 미커밋 동안 timeline writer 대기: `true`
|
|
- 철회 커밋 뒤 writer fail-closed: `true`
|
|
- 해당 submission의 timeline row: `0`
|
|
- 이후 voice processing 차단: `true`
|
|
- 실제 lock 구간 포함 elapsed: `281ms`
|
|
|
|
따라서 이전 목록의 “실제 PostgreSQL grant/withdraw 동시성”은 내부 저장 경계에서 닫혔다. 다만 열린 public
|
|
WebSocket과 DB 철회를 함께 실행하는 공개 장시간 동시성은 50분 외부 gate에 남는다.
|
|
|
|
### text-only 대비 calibrated voice gain
|
|
|
|
기존 benchmark는 `text_only_accuracy`와 `fused_accuracy`를 fixture에 직접 적고 fusion decision만 맞췄기 때문에
|
|
추가 이득을 측정한 증거가 아니었다. 각 case에 독립 `target_value`를 추가하고 실제 출력의
|
|
`one_minus_mean_absolute_error`를 계산하도록 수정했다.
|
|
|
|
- text-only: `0.9533333333`
|
|
- calibrated multimodal: `0.9673333333`
|
|
- measured incremental gain: `+0.014`
|
|
- minimum gain: `0.01`
|
|
- synthetic case: `3`, fusion decision accuracy: `1.0`
|
|
- target을 text prediction으로 바꾸는 반례: gain `<0`, gate 실패
|
|
|
|
이는 **synthetic educational gold label에 대한 알고리즘 회귀**다. 독립 평가자가 라벨링한 held-out 음성이나
|
|
실제 수련생 pilot에서의 효과성·일반화 증거는 아니므로 외부 종료 gate에 별도로 남긴다.
|
|
|
|
### 후속 loopback cap/RSS 실행
|
|
|
|
`scripts/soak-voice-websocket.py --duration-seconds 3 --cycle-interval-ms 50` 결과는 연결 `61/61`, 정상 close
|
|
`59`, 성공 발화/STT/TTS `57/57/57`, 철회 차단 `2`, 미동의 차단 `1`, overflow 거부/복구 `1/1`,
|
|
예기치 않은 오류 `0`으로 통과했다. 이 실행에서 cap 초과 뒤 `idle` 미복귀 결함을 먼저 재현했고, 오류 뒤
|
|
`state=idle`을 보내 동일 socket의 다음 발화가 성공하도록 수정한 뒤 재통과했다.
|
|
|
|
완료감사 후 Python 3.11 focused voice/G7 회귀는 `78 passed`, API app 전체는 `782 passed`, Ruff 대상 파일은
|
|
`All checks passed`, SSOT checker는 `passed:true`였다. Starlette `python_multipart` 전환 경고 1건은 기존 경고다.
|
|
|
|
## 아직 남은 외부 증거
|
|
|
|
- 실제 Deepgram 운영 key·quota를 사용한 interim/final·word timestamp live 수신
|
|
- 기대 STT/TTS provider·model 네 값과 정확히 일치하는 authenticated public WSS `ready`
|
|
- 사용자가 실행 직전에 명시 동의한 물리 마이크 capture와 브라우저 `MediaRecorder` 장시간 운전
|
|
- 외부 STT/TTS provider의 50분 양방향 비용·rate limit·장애 복구
|
|
- 운영 TTS provider/model의 실제 배포 설정 증거
|
|
- 공개 TLS/WSS, Cloudflare idle timeout, 프록시 buffering과 네트워크 단절
|
|
- 독립 라벨 held-out/pilot에서 text-only 대비 실제 음성 추가 이득
|
|
- 실제 worker 수와 Uvicorn·OS·Cloudflare queue를 포함한 RSS/CPU/queue high-water 관측
|
|
|
|
이 항목들은 이번 로컬 synthetic soak로 DONE 처리할 수 없다.
|
|
|
|
## 공개 배포 뒤 외부 감사
|
|
|
|
2026-08-07 공개 배포 후 TLS 1.3/Cloudflare를 거쳐 `/voice/ws` handshake에 도달했고 비인증 요청은
|
|
`1008 not authenticated`로 닫혔다. 저장된 public auth state와 소유 voice session이 없어 authenticated
|
|
ready와 50분 soak는 시작하지 않았다.
|
|
|
|
후속 하드닝에서 `scripts/soak-public-voice-websocket.py`의 증거 계약을 v3로 올렸다. `--preflight-only`는
|
|
실제 auth cookie·소유 회기로 public WSS `ready`/ping만 확인하고, 필수 인자인 기대 STT/TTS provider/model
|
|
네 값과 서버 메타데이터를 정확히 비교한다. 이 모드는 마이크 장치 열거와 캡처를 전혀 수행하지 않으므로
|
|
물리 마이크 증거가 아니다. full soak는 transcript/reply/TTS binary/idle을 요구하고 기본 3000초로 실행되며,
|
|
auth cookie·소유 회기·`--confirm-physical-capture`를 모두 확인한 뒤에만 장치를 열거하고 캡처한다.
|
|
비밀값, session ID, 축어록, 응답 payload와 원음은 증거에 기록하지 않는다.
|
|
|
|
같은 후속 코드 범위에서 Deepgram streaming adapter가 interim/final, word timestamp, provider event를 처리하고,
|
|
`ready`가 실제 STT/TTS provider/model을 반환하도록 구현했다. streaming 중 동의는 1초마다 재검사하며 철회 시
|
|
추가 오디오 전송과 후속 저장을 중단한다. G7 word token은 deployment `SESSION_SECRET`을 키로 한
|
|
HMAC-SHA256 pseudonym만 저장하고 원문 word는 저장하지 않는다. 프론트는 네트워크/TTS 단절 뒤 작성 중 텍스트를
|
|
보존하고 텍스트 계속하기 또는 키보드 가능한 음성 재연결을 제공한다. 이는 모두 코드/내부 회귀 증거이며
|
|
Deepgram live, authenticated public ready 또는 물리 마이크 증거가 아니다.
|
|
|
|
## G7 voice hardening 공개 재배포 — 2026-08-07 04:31~04:38 KST
|
|
|
|
voice hardening release-only patch `3197805058e3…2bc`를 clean worktree에 적용해 공개 API와 Web을
|
|
재배포했다. API 8001은 새 clean worktree CWD에서 실행되며 local/public `/health`가 prod·DB·engine green,
|
|
`/voice/health`가 다음 실제 운영 설정을 반환했다.
|
|
|
|
| 메타데이터 | 실제값 |
|
|
|---|---|
|
|
| STT provider / model | `openai` / `gpt-4o-transcribe` |
|
|
| STT batch fallback | `true` |
|
|
| TTS provider / model | `openai` / `gpt-4o-mini-tts` |
|
|
|
|
운영 런타임에는 Deepgram 자격증명이 주입되지 않아 streaming provider가 활성화되지 않았다. 따라서 위 STT
|
|
metadata는 정상적인 OpenAI batch fallback 증거이지 Deepgram interim/final·word timestamp live 증거가 아니다.
|
|
인증 없는 공개 WSS는 error frame 뒤 `1008`로 닫혔고, 저장된 공개 인증 상태와 소유 회기가 없어 authenticated
|
|
`ready`/ping preflight는 실행하지 않았다. 이 확인 과정은 마이크 장치 열거·캡처를 전혀 수행하지 않았다.
|
|
|
|
Pages production `6d7e89d5`와 custom domain은 새 entry와 Session JS/CSS를 올바른 MIME으로 제공하고,
|
|
Session 청크에 AI 생성 음성 고지 문구가 포함된 것을 확인했다. watchdog도 새 clean worktree로 재등록해
|
|
`LastTaskResult=0`을 통과했다. 공개 코드와 운영 TTS 설정은 승격됐지만, G7 종료에는 여전히 실제 Deepgram
|
|
key·quota live, expected metadata authenticated public `ready`, 명시 동의 물리 마이크와 50분 soak가 모두 필요하다.
|
|
|
|
## 운영 TTS 권리·고지 경계
|
|
|
|
운영 TTS는 OpenAI API `gpt-4o-mini-tts`와 built-in voice만 사용한다. 2026-08-07에 확인한
|
|
[OpenAI Services Agreement](https://openai.com/policies/services-agreement/) 2.2는 API를 고객 애플리케이션에
|
|
통합해 최종 사용자에게 제공할 권리를, 4.1은 법이 허용하는 범위에서 출력 소유를 고객에게 둔다. 이는 서비스
|
|
계정이 해당 약관과 적용 정책을 준수한다는 전제의 제품 근거이며 별도 법률 의견으로 확대하지 않는다.
|
|
|
|
[OpenAI Text-to-Speech 가이드](https://developers.openai.com/api/docs/guides/text-to-speech)는 최종 사용자에게
|
|
듣는 음성이 AI 생성이며 사람 음성이 아니라는 명확한 고지를 요구한다. Session UI는 같은 문장을 회기 시작 전,
|
|
진행 중 데스크톱 마이크 상태와 모바일 컨트롤바에 지속 노출하고 접근성 `role="note"` 계약과 E2E를 고정했다.
|
|
Higgs·sample provider는 계속 dev-only이며 실존 인물/성우 reference를 사용하지 않는다. 새 공개 배포의
|
|
`/voice/health`에서 `openai / gpt-4o-mini-tts` exact match를 확인했다. authenticated WSS `ready` 확인은
|
|
공개 인증 상태와 소유 회기가 준비된 뒤 별도 외부 게이트로 수행한다.
|
|
|
|
후속 회귀는 Python 3.11로 voice service/WS/G7 store·API `73 passed`, public runner `12 passed`를 확인했다.
|
|
`--confirm-physical-capture`가 없는 full soak는 장치 열거 전 exit 3 `BLOCKED`로 남는다.
|
|
|
|
안전장치 추가 전 G533 마이크가 명시적 사용자 동의 없이 3초씩 두 번 열렸다. 약 -90 dBFS 무음이었고
|
|
원음·축어록을 보존하지 않았으며 즉시 추가 캡처를 중단했다. 이 실행은 G7 증거로 인정하지 않는다.
|
|
상세는 `evidence/g7-external-voice-gate-audit-2026-08-07.json`에 남겼다.
|