# 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`에 남겼다.