# avatar-E2E-inference

실제 대화 상황 그대로 — **OpenAI Realtime API로 웹에서 대화**하고, 그 어시스턴트 음성에
맞춰 **kemix-engine이 실시간으로 바디 모션 + 얼굴 표정을 생성**해 아바타를 구동하는 E2E
파이프라인. `viewer_mic.html`(마이크·바디only·배치)처럼 장난감이 아니라, production과
같은 입력(합성 음성)·같은 출력(face+body)으로 검증하는 것이 목적.

> 배경 논의: production 음성 소스는 사람 마이크가 아니라 **API 합성 음성**(OpenAI Realtime /
> TTS). 학습 데이터도 edge-TTS라 오히려 마이크보다 분포가 가깝다. 그래서 "실제처럼" 테스트하는
> 것이 더 정확할 뿐 아니라 `mic_server`의 `corpusify` 해킹(마이크 OOD 보정)도 필요 없어진다.

## 현재 아키텍처 (Realtime WebRTC + local inference hub)

```
브라우저
   ├─ user mic ────────────────► OpenAI Realtime (WebRTC)
   ├─ assistant remote audio ──► 1.2s delay ──► 스피커
   ├─ 같은 audio를 16k PCM, 0.533s 청크로 ──► kemix 백엔드
   │                                             ├─ V3 face (warm, causal) → 52 ARKit
   │                                             └─ body worker (warm) → 44본 quaternion
   └─ audio delay와 같은 clock으로 face+body frame을 직접 VRM에 적용
```

API 키는 백엔드에만 있고 브라우저는 ephemeral token으로 OpenAI에 연결한다. 아바타는
**어시스턴트(자기) 음성**에 맞춰 움직이며 사용자 발화 중에는 listening 상태다.

## 확정한 결정 (2026-07-23, dasol)

| 항목 | 결정 | 근거 |
|---|---|---|
| v1 타이밍 | **턴 단위** (응답 완료 후 발화 전체 1회 inference → 오디오+모션 동시 재생) | 지금 있는 배치 모델(`gen_fulltake`) 그대로. 가장 빨리 도는 E2E + 속도감 검증 겸용. 스트리밍은 v2 |
| 감정 소스 | **LLM 직접 태깅** (Realtime 프롬프트가 응답마다 16감정 중 하나 방출) | 별도 분류기·체크포인트 불필요, 대사-감정 일관 |
| 바디 모델 | kemix 속도 코덱+flow (`wave-hands/gen_fulltake_tag3.py`) | 이번에 재학습한 것. 264-dim = 44본(손가락 포함) |
| 얼굴 모델 | V3 face (`face/animasync-face-v3/models/v3_face`, `best_expression_v14.pt`) | on-machine 구동 확인. mel + emotion/VAD 입력 |

## 모델 현실 점검 (무엇이 준비됐나)

| 후보 | 상태 | E2E에서 |
|---|---|---|
| `allinone` 통합 face+body 모델 | ❌ **weight 없음** (Phase 3~5 미완, checkpoints/·weights/ 부재) | 미사용 — 단 **조건부 입력 계약의 레퍼런스**로 활용 |
| `gen_v3_face.py`의 LAM lipsync | ❌ 이 머신에 없음 (`/data/recover/...` 부재) | 미사용 |
| `face-v1/deployment` ONNX API | ❌ 모델 파일(`lipsync_student.onnx`) 부재 | 미사용 |
| **V3 face 모델** (`v3_face/*.pt`) | ✅ 로컬 구동 확인 (`.venv`에서 import OK) | **얼굴 채택** |
| **kemix 속도 바디** (`gen_fulltake_tag3`) | ✅ 이번 세션 재학습 완료 | **바디 채택** |

## 조건부 입력 계약 (allinone 참조 → OpenAI Realtime 매핑)

모델이 30fps로 소비하는 것 ← Realtime이 주는 것:

| 모델 입력 | 계산 출처 | Realtime 출처 |
|---|---|---|
| `mel` (T,80) | `mel_features(wav)` | 어시스턴트 audio (PCM) |
| `onset+amplitude` (T,2) | onset 추출 | 어시스턴트 audio |
| `emotion` (16) + `vad` (3) | emotion→`emotion_vad_anchors.json` | **LLM emotion 태그** (VAD=affective valence·arousal·dominance, 음성활동 아님) |
| `word` | 토크나이즈 | 어시스턴트 transcript (스트리밍) |

## 단계별 빌드

- [x] **A. 오프라인 턴 단위 추론 코어** — `(wav, emotion) → face(52) + body(vrma)`.
      OpenAI 없이 코퍼스 wav로 검증(두 모델 결합 + 속도감 확인).
      - [x] `face_v3_infer.py` — V3 face: wav+emotion → 52 blendshapes JSON
      - [x] `infer_turn.py` — face+body 오케스트레이터 (body는 `gen_fulltake_tag3` 재사용).
            `outputs/turns/<id>/{face.json,body.vrma,audio.wav,meta.json}` 산출
- [x] **B. 통합 뷰어** — `viewer.html`: VRM 하나(`GG_11`, humanoid 본 + ARKit-52 둘 다 보유)에
      body(vrma)+face(blendshape)+audio를 **오디오 클럭에 동기** 재생. turn은
      `outputs/turns/index.json`에서 발견. `avatar.vrm`·`vendor`는 심링크.
- [x] **C. OpenAI Realtime 브리지 (턴 단위)** — 코드 완성, 백엔드 검증됨. 브라우저 대화 루프는
      **키 + 실제 브라우저/마이크**로 테스트 필요.
      - 토폴로지(이 환경 라이브러리 제약): **브라우저가 WebRTC로 OpenAI 직결**, 백엔드는
        (1) ephemeral 토큰 발급 (`/api/session`, 서버키는 서버에만) + (2) 추론 (`/api/infer_turn`)만.
      - `server.py` — 허브 (stdlib http.server + requests). `/api/infer_turn` **검증 완료**.
      - `conversation.html` — mic→OpenAI, `set_emotion` tool로 감정 태그, `response.done`에
        어시스턴트 음성 캡처→16k wav→`/api/infer_turn`→턴 재생. import/문법 로드 OK, 대화 루프 미검증.
      - **warm 상주 (검증됨)**: `body_engine.py`(코덱+flow 상주형, `gen_window`=v2 스트리밍
        프리미티브) + `body_worker.py`(**별도 프로세스** — V3 face와 GVRM body가 top-level
        `models`/`scripts` 패키지를 공유해 한 인터프리터에서 충돌하므로 분리). `infer_turn`은
        face=in-process warm, body=worker 파이프. **warm 턴 추론 ~0.5s** (기존 서브프로세스 ~5.6s).
      - Realtime API 확인값: 토큰 `POST /v1/realtime/client_secrets`, SDP `POST /v1/realtime/calls`,
        데이터채널 `oai-events`, 이벤트 `response.output_audio_transcript.*` / `response.done`.
- [x] **v2. 실시간 스트리밍**
      - `BodyStream`: 4.27초 rolling audio window, **16 pose frame = 4 latent frame =
        0.533초** 단위 생성. 처음에는 과거 쪽(왼쪽)을 silence-pad한다.
      - rolling seed는 이전 window의 tail이 아니라 시간축에 맞는
        `previous_g[:, 4:4+PRE]`를 사용한다. 기존 batch의 28-latent hop과 realtime의
        4-latent hop을 혼동하면 오디오와 motion 위치가 어긋난다.
      - `body_worker.py`: `stream_start/chunk/end/cancel` 상태 프로토콜.
      - `FaceStream`: causal V3를 warm 재사용하고 body와 동일한 새 프레임 수를 반환.
      - `server.py`: `/api/warmup`, `/api/stream/*`; 첫 librosa/CUDA 호출까지 warmup에서 처리.
      - `conversation.html`: OpenAI remote audio를 캡처하면서 1.2초 delay 재생하고, 그 사이
        local GPU에서 생성한 body/face를 같은 audio clock에 맞춰 적용.
      - 실제 GPU 측정: warmup 후 첫 청크 **445ms**, body `16×44×4`, face `16×52`.
        0.2초 tail은 6프레임으로 flush됨.

## 실행

```bash
# A: 한 발화 추론 (face + body 동시, OpenAI 불필요)
.venv/bin/python infer_turn.py --wav <16k mono wav> --emotion excitement --id demo1

# B: 통합 뷰어 (오프라인 검증)
.venv/bin/python -m http.server 8317 --bind 127.0.0.1     # -> /viewer.html

# C/v2: 실시간 대화 허브 — 키는 옆의 .env(OPENAI_API_KEY, git-ignored)에서 자동 로드
.venv/bin/python server.py --port 8318
#   -> http://127.0.0.1:8318/conversation.html
#      ("대화 시작" → model warmup → 마이크 허용 → 말하기)
```

`.env`(server.py 옆, git-ignored)에 `OPENAI_API_KEY=sk-...`. 실제 환경변수가 있으면 그게 우선.
`/api/infer_turn`은 키 없이도 동작(오프라인). `/api/session`(대화)만 키 필요 — 토큰 발급 검증 완료.

검증 명령:

```bash
cd package/avatar-E2E-inference
../../.venv/bin/python -m unittest -v test_streaming.py
```
