# avatar-E2E-inference — 로컬 세팅 가이드

새 리눅스 머신에서 이 파이프라인(OpenAI Realtime 대화 → 실시간 face+body 아바타)을
돌리기 위해 필요한 **가중치 · 라이브러리 · 절차** 전부. 이 문서의 모든 항목은
2026-07-24에 이 저장소에서 실제로 실행·확인한 결과다.

핵심 사실 하나: **가중치는 git에 없다.** `.gitignore`가 `*.pth *.pt *.npy *.npz *.pkl
*.vrm *.wav`를 전부 제외하므로, fresh clone에는 코드만 있고 모델은 없다. 대신 private
리포의 **GitHub Release**(`weights-v4-vel`)에 471MB로 올려두었고, 스크립트 한 줄로 받는다:

```bash
package/avatar-E2E-inference/fetch-weights.sh
```

세부 내역과 수동 설치는 §3 참조.

---

## 0. 시작 전 확인 — 이 문서가 되는 환경 / 안 되는 환경

**NVIDIA GPU가 달린 리눅스 머신이 필요하다.** Mac(Apple Silicon 포함)이나 CPU-only
머신에서는 코드 수정 없이 돌지 않는다. 이유는 세 겹이다:

1. `body_engine.BodyEngine`이 `device="cuda:0"` 고정이고, `body_worker.py`는 device를
   넘기지 않는다.
2. device 인자를 고쳐도 `GestureVRM/models/vq/quantizer.py:48`이 코드북 버퍼를
   `.cuda()`로 생성한다 — CPU/MPS에서 여기서 막힌다.
3. torch cu121 휠 자체가 macOS에 없다.

얼굴(V3)만은 CPU 폴백이 있지만, 바디가 없으면 파이프라인이 성립하지 않는다.

**Mac에서 보고 싶다면** — 추론은 GPU 머신에서 돌리고 브라우저만 로컬에서 연다.
SSH 포트 포워딩이면 충분하다 (§6-1).

그 외 전제:

- private 리포 `GoodGangLabs/kemix-engine` **read 권한** + `gh auth login`
- 대화 모드는 **OpenAI Realtime(`gpt-realtime`) 접근 권한이 있는 키** (§5).
  오프라인 추론·뷰어는 키 없이 된다.

### 검증 상태 (2026-07-24 기준)

| 구간 | 상태 |
|---|---|
| 오프라인 턴 추론 `infer_turn.py` | ✅ 실측 (face 1.14s + body 4.08s, cold) |
| 스트리밍 백엔드 `/api/stream/*` | ✅ 실측 (warmup 후 첫 청크 445ms) |
| 릴리스 다운로드 → 전개 → 생성 | ✅ 실측 (체크섬 3/3, 24프레임 생성) |
| 통합 뷰어 `viewer.html` | ✅ 오프라인 재생 확인 |
| **브라우저 대화 루프** `conversation.html` ↔ OpenAI | ⚠️ **미검증** — 실제 마이크·키·브라우저로 한 번도 돌린 적 없다 (README Phase C 참조). 여기서 처음 문제가 날 수 있는 구간이다 |

즉 CTO가 이 문서로 확실히 재현할 수 있는 것은 **오프라인 추론 + 뷰어**까지이고,
실시간 대화는 "코드는 완성, 첫 실사용 테스트"에 해당한다.

---

## 1. 하드웨어 / OS 요구사항

| 항목 | 검증된 값 | 비고 |
|---|---|---|
| OS | Ubuntu 22.04.5 LTS (kernel 6.8) | |
| GPU | NVIDIA RTX 4090 24GB | **CUDA 필수** — `body_engine.BodyEngine`은 `device="cuda:0"` 고정. CPU 폴백 없음 |
| VRAM | 실측 피크 **약 2.1GB** (face+body 두 프로세스 합) | 8GB급 GPU면 충분 |
| 드라이버 | 565.57.01 (CUDA 12.7 런타임) | torch cu121 휠과 호환 |
| Python | 3.10.12 (system python3 + venv) | 3.10 권장 (검증된 조합) |
| 디스크 | 코드 + 가중치 ≈ 1GB | 전체 코퍼스까지 받으면 +420MB |
| 브라우저 | Chrome/Edge (WebGL2 + 마이크 + WebRTC) | `http://127.0.0.1`은 secure context라 마이크 허용됨 |

인터넷은 `/api/session`(OpenAI Realtime 토큰 발급)과 브라우저-OpenAI WebRTC 연결에만
필요하다. 오프라인 추론(`infer_turn.py`, viewer)은 네트워크 없이 동작한다.

---

## 2. 파이썬 환경 + 라이브러리

venv는 **저장소 루트**(`kemix-engine/.venv`)에 만든다. 패키지 디렉터리에서 실행할 때
경로는 `../../.venv/bin/python`이다.

```bash
cd kemix-engine
python3.10 -m venv .venv
.venv/bin/pip install --upgrade pip

# torch는 반드시 cu121 인덱스에서 (nvidia-* cu12 휠이 함께 설치됨)
.venv/bin/pip install torch==2.5.1 --index-url https://download.pytorch.org/whl/cu121

# 나머지 직접 의존성 (transitive는 pip가 알아서 가져옴)
.venv/bin/pip install \
  "numpy==2.2.6" "scipy==1.15.3" \
  "librosa==0.11.0" "soundfile==0.14.0" \
  "omegaconf==2.3.1" "loguru==0.7.3" "rich==15.0.0" "einops==0.8.2" \
  "pandas==2.3.3" "tqdm==4.69.0" "requests==2.34.2"
```

이 두 명령은 **빈 venv에서 실제로 실행해 검증했다**(2026-07-24). 설치 후
`import body_engine` / `import face_v3_infer` / `infer_turn.py` E2E / `test_streaming.py`
10건 모두 통과한다.

### 왜 이 목록인가 (실제 import 추적 결과)

`body_engine` / `face_v3_infer`를 각각 import한 뒤 `sys.modules`에서 표준 라이브러리를
제거해 얻은 실제 런타임 의존성:

| 패키지 | 쓰는 곳 |
|---|---|
| `torch` 2.5.1+cu121 | flow 모델, VQ-VAE, V3 face |
| `numpy` 2.2.6 / `scipy` | 전 구간 (쿼터니언, 후처리 gaussian filter) |
| `librosa` + `soundfile` | mel, onset 검출, wav 로드 |
| `omegaconf` (+antlr4, PyYAML) | GestureVRM `shortcut_vrm.yaml` |
| `loguru` | GestureVRM/train_ours 로깅 |
| `rich` | `GestureVRM/models/utils/utils.py:5`의 `from rich import get_console`. **loguru의 transitive가 아니다** — 빼면 `models/vrm_lsm.py` import에서 `ModuleNotFoundError`로 죽는다 |
| `einops` | GestureVRM denoiser |
| `pandas` (+pytz, dateutil, six) | GestureVRM dataloader import 체인 |
| `tqdm` | 모델 로딩 경로 |
| `requests` | `server.py` — OpenAI ephemeral 토큰 발급 |
| (transitive) `numba, llvmlite, scikit-learn, pooch, soxr, audioread, joblib, msgpack, lazy_loader, decorator` | librosa 의존성 |

- **JS/npm 불필요.** three.js, @pixiv/three-vrm, jsm addons 전부 저장소에 vendoring 되어
  있고 importmap으로 직접 로드한다. 빌드 스텝 없음.
- `av`(PyAV)는 이 머신 venv에 있지만 이 파이프라인에서는 안 쓴다.
- soundfile 휠에 libsndfile이 포함되어 있다. wav 로드가 실패하면 그때만
  `sudo apt install libsndfile1`.

---

## 3. 가중치 / 데이터 파일 (git에 없음 — Release에서 받는다)

### 받는 법

```bash
package/avatar-E2E-inference/fetch-weights.sh      # 471MB, 체크섬 검증 후 제자리에 전개
```

private 리포라 `gh` 인증이 필요하다(`gh auth login`). 에셋 URL은 인증 없이는 404다 —
리포를 public으로 바꾸면 릴리스도 함께 공개되니 그때는 재검토할 것.

이미 가중치가 있으면 스크립트는 아무것도 하지 않는다. 덮어쓰려면 `FORCE=1`.

| 릴리스 에셋 | 크기 | 내용 |
|---|---|---|
| `kemix-body-v4-vel.tgz` | 470MB | flow(슬림) + VQ-VAE 4종 + mean_std + seed_bank + rest_table + vocab |
| `kemix-face-v3.tgz` | 14MB | V3 face 체크포인트 |
| `kemix-avatar.tgz` | 9MB | `GG_11.vrm` |

> **flow `best.pth`는 추론 전용이다.** 릴리스 번들은 `optimizer_state_dict`(410MB)를
> 빼서 620MB → 210MB로 담았다. `model_state_dict`는 원본과 동일하고 추론 경로가
> `ck.get("model_state_dict", ck)`로 읽으므로 코드 수정 없이 동작하지만, **학습 재개는
> 불가능하다.** 학습 머신에서 `FORCE=1`로 덮어쓰지 말 것.
>
> 번들 재생성은 `python pack_weights.py` (→ `dist/`에 tar 3개 + SHA256SUMS).

### 수동 설치 / 파일별 내역

아래는 릴리스가 없던 시절의 전체 목록이자, `gh` 없이 USB·rsync로 옮길 때의 기준이다.
디스크상 총 **902MB**(슬림 전). 경로는 저장소 루트 기준이며 **디렉터리 구조를 그대로
유지**해야 한다(코드가 상대 경로로 찾는다).

`WH = package/motion-blender/experiments/wave-hands`

### 3-1. 바디 모델 (kemix 속도 코덱 + flow, v4_vel)

| 파일 | 크기 | 역할 | 필수 |
|---|---|---|---|
| `WH/outputs/gen_train/weights_v4_vel/best.pth` | 592MB | flow 모델 체크포인트 (`Tag3VrmLSM`) | ✅ |
| `WH/outputs/gen_train/vqvae_kemix_v4_vel/best_spine.pth` | 71MB | VQ-VAE 코덱 (spine) | ✅ |
| `WH/outputs/gen_train/vqvae_kemix_v4_vel/best_arms.pth` | 71MB | VQ-VAE 코덱 (arms) | ✅ |
| `WH/outputs/gen_train/vqvae_kemix_v4_vel/best_legs.pth` | 71MB | VQ-VAE 코덱 (legs) | ✅ |
| `WH/outputs/gen_train/vqvae_kemix_v4_vel/best_fingers.pth` | 72MB | VQ-VAE 코덱 (fingers, 44본용) | ✅ |
| `WH/outputs/kemix_npz_v4/mean_std/vrm_{spine,arms,legs,fingers}_{mean,std}.npy` | 36KB (8개) | 정규화 통계 | ✅ |
| `WH/outputs/gen_train/seed_bank/seed_avg.npz` | 132KB | 감정별 프로토타입 시드(첫 윈도우 오프닝 포즈) | ✅ |
| `WH/outputs/kemix_rest_table.npy` | 3.8KB | basis→channel rest 테이블 (VRMA 출력용) | ✅ |
| `WH/outputs/dummy_lang/weights/vocab.pkl` | 20KB | PAD 토큰만 쓰는 더미 vocab (loader 구성용) | ✅ |

### 3-2. 얼굴 모델 (V3 face)

| 파일 | 크기 | 역할 | 필수 |
|---|---|---|---|
| `package/face/animasync-face-v3/models/v3_face/checkpoints/best_expression_v14.pt` | 15MB | V3 face (3.80M params, epoch 95, val_l1 0.0046) | ✅ |
| `package/face/animasync-face-v3/data/emotion/emotion_vad_anchors.json` | 14KB | 감정→VAD 앵커 | git에 있음 ✓ |

### 3-3. 뷰어 에셋

| 파일 | 크기 | 역할 | 필수 |
|---|---|---|---|
| `package/face/animasync-face-v3/avatar/GG_11.vrm` | 13MB | 아바타 (humanoid 본 + ARKit-52 동시 보유) | ✅ (git 제외) |
| `package/lipsync-wasm/v2/assets/idle01.vrma` | 137KB | idle 클립 | git에 있음 ✓ |
| `package/shader/pseudo-nilotoon/**` (src, npm-package/src, vendor, assets/looks) | — | NiloToon 셰이더 + three.js/three-vrm vendoring | git에 있음 ✓ |
| `WH/vendor/**` (three, vrm) | — | `@pixiv/three-vrm-animation` | git에 있음 ✓ |

### 3-4. 복사하지 않아도 되는 것 (실측 확인)

| 경로 | 크기 | 왜 불필요한가 |
|---|---|---|
| `WH/outputs/kemix_npz_v4/{train,val,test}/` | 188MB | `OursVrmDataset`은 **PAD 토큰 하나** 때문에 생성될 뿐이다. `test/`가 비어 있으면 `0 windows` 로그만 남고 정상 동작 (검증함) |
| `WH/outputs/kemix_wave16k/` | 232MB | 위 dataset이 test 클립의 wav 존재 여부만 확인 — 클립이 0개면 아예 안 본다. 단, **스모크 테스트용 wav는 1개 필요** (§6) |
| `WH/outputs/kemix_npz_v4/{energy_bins,rest_stats}.npz` | 8KB | opt-in 라벨. 추론 경로는 `_cur_energy=None`, `_cur_rest=2` 고정 (없이 기동 검증함) |
| `weights_v4_vel/epoch_*.pth` | 각 592MB | `best.pth`만 쓴다 |
| `vqvae_kemix_v4_vel/net_30000_*.pth` | 각 71MB | `best_*.pth`만 쓴다 |
| face checkpoints의 `v18*`, `lipsync_v14`, `latest_*` | 각 15MB | `best_expression_v14.pt`만 쓴다 |

즉 **최소 배포 세트 = 902MB**, 전체 코퍼스까지 복사하면 약 1.3GB.

### 3-5. gh 없이 옮길 때 (수동 tar)

```bash
# 원본 머신에서 (경로 구조 유지 tar)
cd kemix-engine
tar czf kemix-weights.tgz \
  package/motion-blender/experiments/wave-hands/outputs/gen_train/weights_v4_vel/best.pth \
  package/motion-blender/experiments/wave-hands/outputs/gen_train/vqvae_kemix_v4_vel/best_{spine,arms,legs,fingers}.pth \
  package/motion-blender/experiments/wave-hands/outputs/kemix_npz_v4/mean_std \
  package/motion-blender/experiments/wave-hands/outputs/gen_train/seed_bank/seed_avg.npz \
  package/motion-blender/experiments/wave-hands/outputs/kemix_rest_table.npy \
  package/motion-blender/experiments/wave-hands/outputs/dummy_lang/weights/vocab.pkl \
  package/face/animasync-face-v3/models/v3_face/checkpoints/best_expression_v14.pt \
  package/face/animasync-face-v3/avatar/GG_11.vrm \
  package/motion-blender/experiments/wave-hands/outputs/kemix_wave16k/daily_001_t2_excitement.wav

# 새 머신에서
cd kemix-engine && tar xzf kemix-weights.tgz
```

---

## 4. 심링크 복구 (fresh clone에서 반드시)

`avatar.vrm` 심링크는 **git에 없다** — `.gitignore`의 `*.vrm`이 심링크 이름까지 잡아서
커밋이 안 됐다. `vendor` 심링크는 tracked라 clone 시 함께 온다.

```bash
cd package/avatar-E2E-inference
ln -sfn ../face/animasync-face-v3/avatar/GG_11.vrm avatar.vrm
ls -lL avatar.vrm vendor        # 둘 다 실제 파일/디렉터리로 풀려야 함
```

---

## 5. OpenAI 키 (`.env`)

`server.py` 옆(= 이 디렉터리)에 `.env`를 만든다. git-ignored다.

```bash
cat > package/avatar-E2E-inference/.env <<'EOF'
OPENAI_API_KEY=sk-...
# 선택 (기본값이 아래와 같음)
# REALTIME_MODEL=gpt-realtime
# REALTIME_VOICE=marin
EOF
```

- 실제 환경변수가 이미 있으면 그쪽이 우선한다(`_load_dotenv`는 `setdefault`).
- 키는 **서버에만** 있고, 브라우저는 `/api/session`이 발급한 ephemeral 토큰으로
  OpenAI에 직접 붙는다.
- 키가 없어도 `/api/infer_turn`, `/api/warmup`, `/api/stream/*`(오프라인 추론)은 동작한다.
  `/api/session`만 503.
- Realtime API(`gpt-realtime`) 접근 권한이 있는 계정이어야 한다.

---

## 6. 실행

```bash
cd package/avatar-E2E-inference
V=../../.venv/bin/python

# A. 오프라인 턴 추론 (OpenAI 불필요) — 세팅 검증용
$V infer_turn.py --wav <wav 경로> --emotion excitement --id demo1
#   -> outputs/turns/demo1/{face.json, body.vrma, audio.wav, meta.json}
#   감정 16종: neutral joy laughter excitement agreement gratitude sadness crying
#             sulk apology struggle anger refusal surprise fluster shy

# B. 오프라인 뷰어
$V -m http.server 8317 --bind 127.0.0.1     # -> http://127.0.0.1:8317/viewer.html

# C. 실시간 대화 허브 (v2 스트리밍)
$V server.py --port 8318                    # -> http://127.0.0.1:8318/conversation.html
#   "대화 시작" → warmup → 마이크 허용 → 말하기
```

### 6-1. Mac에서 보기 (추론은 GPU 머신, 화면만 로컬)

브라우저는 OpenAI에 WebRTC로 직접 붙고, kemix 백엔드와는 HTTP로만 통신한다. 그래서
백엔드 포트 하나만 포워딩하면 Mac에서 그대로 쓸 수 있다. (`server.py`는 `127.0.0.1`에만
바인딩하므로 GPU 머신의 LAN IP로 직접 접속하는 방법은 없다 — 터널이 정답이다.)

```bash
# ① GPU 머신에서
cd package/avatar-E2E-inference && ../../.venv/bin/python server.py --port 8318

# ② Mac에서 (터널)
ssh -N -L 8318:127.0.0.1:8318 <user>@<gpu-host>

# ③ Mac 브라우저
open http://127.0.0.1:8318/conversation.html
```

`127.0.0.1`로 접속하므로 secure context가 되어 마이크가 허용된다. 아바타 렌더링(WebGL)과
마이크는 Mac에서, face/body 추론은 GPU 머신에서 돈다. 청크 왕복이 SSH를 타므로 LAN
기준 수 ms가 더해진다.

---

wav는 아무 샘플레이트나 된다(`librosa`가 16k mono로 리샘플). 코퍼스를 안 받았다면
테스트용 wav를 하나 만들면 된다:

```bash
$V -c "
import numpy as np, soundfile as sf
sr=16000; t=np.arange(int(sr*3))/sr
sf.write('/tmp/test.wav', (0.3*np.sin(2*np.pi*180*t)*(np.sin(2*np.pi*3*t)>0)).astype('float32'), sr)"
```

---

## 7. 세팅 검증 (기대 출력)

```bash
cd package/avatar-E2E-inference

# 1) 필수 파일 존재 확인
WH=../motion-blender/experiments/wave-hands; F=../face/animasync-face-v3
for f in \
  $WH/outputs/gen_train/weights_v4_vel/best.pth \
  $WH/outputs/gen_train/vqvae_kemix_v4_vel/best_spine.pth \
  $WH/outputs/gen_train/vqvae_kemix_v4_vel/best_arms.pth \
  $WH/outputs/gen_train/vqvae_kemix_v4_vel/best_legs.pth \
  $WH/outputs/gen_train/vqvae_kemix_v4_vel/best_fingers.pth \
  $WH/outputs/kemix_npz_v4/mean_std/vrm_spine_mean.npy \
  $WH/outputs/gen_train/seed_bank/seed_avg.npz \
  $WH/outputs/kemix_rest_table.npy \
  $WH/outputs/dummy_lang/weights/vocab.pkl \
  $F/models/v3_face/checkpoints/best_expression_v14.pt \
  $F/data/emotion/emotion_vad_anchors.json \
  avatar.vrm ../lipsync-wasm/v2/assets/idle01.vrma ; do
  [ -e "$f" ] && echo "OK   $f" || echo "MISS $f"
done

# 2) 스트리밍 로직 단위 테스트 (가중치 불필요, import 체인만 검증)
../../.venv/bin/python -m unittest -v test_streaming.py     # Ran 10 tests ... OK

# 3) 실제 가중치 E2E (이게 최종 확인)
../../.venv/bin/python infer_turn.py --wav <wav> --emotion excitement --id probe
```

3번의 정상 출력(RTX 4090, cold start 기준 — `real 7.1s`):

```
OursVrmDataset [test]: 57 windows (speaker_id=2)      ← 코퍼스 없으면 "0 windows" (정상)
[ckpt] .../best_expression_v14.pt  epoch=95  val_l1=0.0046  params=3.80M
{"id": "probe", "face_frames": 221, "timing": {"face_s": 1.14, "body_s": 4.08, "total_s": 5.21}}
```

`torch.load ... weights_only=False` FutureWarning 2건은 정상이다(무시).

warm 상태 성능은 README 기준: 턴 추론 ~0.5s, 스트리밍 첫 청크 445ms
(`/api/warmup` 이후, 24프레임/0.8초 청크).

---

## 8. 트러블슈팅

| 증상 | 원인 / 해결 |
|---|---|
| `No module named 'models.vrm_lsm'` (또는 face 쪽 import 실패) | V3 face와 GestureVRM이 **둘 다 top-level `models`/`scripts` 패키지**를 쓴다. 한 인터프리터에서 같이 import하면 먼저 이긴 쪽만 산다. 그래서 body는 `body_worker.py` **별도 프로세스**다. 직접 스크립트를 짤 때 둘을 한 프로세스에서 import하지 말 것 |
| `CUDA out of memory` / `Torch not compiled with CUDA` | body는 `cuda:0` 고정이라 CPU 폴백이 없다. torch를 cu121 인덱스에서 설치했는지 확인 (`python -c "import torch;print(torch.cuda.is_available())"`) |
| viewer에 새 turn이 안 뜸 | `outputs/turns/index.json`은 **`server.py` 기동 시에만** 재생성된다. `infer_turn.py` 단독 실행은 index를 갱신하지 않는다 → `server.py`를 한 번 띄우면 반영됨 |
| 브라우저에서 마이크 거부 | `127.0.0.1`/`localhost`로 접속해야 secure context다. LAN IP로 붙으면 HTTPS 없이는 마이크가 막힌다 |
| `/api/session` 503 | `.env`의 `OPENAI_API_KEY` 누락. 오프라인 추론은 이 상태로도 된다 |
| 아바타가 안 뜸 (`/avatar.vrm` 404) | §4 심링크 미복구 |
| `shortcut_vrm.yaml`의 `/home/ubuntu/...` 경로 | 무해하다. `body_engine.py`가 `beat_data_path`와 `modality_encoder.params.data_path`를 로컬 `dummy_lang/`으로, vqvae/mean_std 경로도 전부 런타임에 덮어쓴다 |
| `.venv/bin/python`이 없다고 나옴 | venv는 저장소 루트에 있다. 패키지 디렉터리에서는 `../../.venv/bin/python` (README 실행 예시의 상대경로는 루트 기준) |
| `fetch-weights.sh`가 404 / `release not found` | private 리포다. `gh auth login`으로 인증하고, 해당 계정이 `GoodGangLabs/kemix-engine` read 권한을 가졌는지 확인 |
| 학습을 재개하려는데 optimizer state가 없다 | 릴리스 번들의 `best.pth`는 추론 전용(슬림)이다. optimizer state가 든 원본은 학습 머신의 `weights_v4_vel/{best,epoch_*}.pth`에만 있다 |

---

## 9. 한 장 요약

```
1) git clone                      → 코드 전부 + 뷰어 vendor 에셋 (가중치는 없음)
2) gh auth login                  → private 리포 릴리스 접근용
3) python3.10 -m venv .venv       → torch(cu121) + 10개 패키지
4) ./fetch-weights.sh             → 릴리스에서 471MB 받아 제자리에 전개
5) ln -s .../GG_11.vrm avatar.vrm → git에 없는 유일한 심링크
6) .env에 OPENAI_API_KEY          → 대화 모드만 필요
7) infer_turn.py로 스모크 테스트   → face 1.1s + body 4.1s면 정상
8) server.py --port 8318          → conversation.html
```
