# V3 얼굴 표정 시스템 — 구현 계획 (Synthetic Data + 모델 재학습)

> 이 문서는 V3 개발을 위한 실행 계획서입니다. 기존 V3 계획(JS overlay, 재학습 없음)을 대체합니다.

---

## 1. 개요

V3는 기존 V2 모델 아키텍처를 재활용하되, **합성 데이터(Synthetic Dataset)로 14개 감정을 학습**시키는 접근.

V2와의 핵심 차이: V2는 frozen 모델 위 JS 레이어가 아니라, **모델 자체를 14개 감정으로 재학습**합니다.

```
[방향 전환 이유]
기존 V3 계획 (JS overlay):
  - 장점: 재학습 불필요
  - 한계: overlay가 상부 얼굴만 수정 가능, 입 영역 감정 불가 (감사의 미소, 분노의 입 다물기 등)
         시간적 전환이 모델이 아닌 JS에서 처리 → 모델은 여전히 5개 감정만 이해

새 V3 계획 (모델 재학습):
  - 장점: 모델이 14개 감정을 직접 이해, 자연스러운 전환, 전체 52채널 활용
  - 비용: 합성 데이터 생성 + 재학습 필요 (동일 아키텍처, 비용 낮음)
```

---

## 2. 전체 파이프라인

```
┌─────────────────────────────────────────────────────────┐
│ Phase 1: 합성 표정 생성기 (Synthetic Expression Generator)  │
│                                                           │
│  감정 정의 (14개)                                          │
│    + VAD 좌표 (연구 문서 기반)                               │
│    + Blendshape 분포 (Beta distribution)                   │
│    + 시간적 역학 (onset → peak → release)                   │
│    + 오디오 특성 연동 (energy, pitch)                        │
│    + 변이 추가 (화자 스타일, 시간적 흔들림)                    │
│         ↓                                                  │
│  출력: (T, 52) blendshape 시퀀스 × 14,000~21,000 클립       │
└────────────────────────┬────────────────────────────────────┘
                         ↓
┌────────────────────────┴────────────────────────────────────┐
│ Phase 2: 모델 재학습                                         │
│                                                              │
│  학습 데이터:                                                 │
│    - 합성 데이터 (14개 감정, 14,000~21,000 클립)               │
│    - MEAD 실제 데이터 (5개 감정, 3,685 클립) ← 현실감 보존      │
│                                                              │
│  모델: EmotionFaceStreamableModel (V2와 동일 아키텍처)         │
│    변경점: NUM_EMOTION_CLASSES = 5 → 14                       │
│    FiLM 레이어: Linear(5, 64) → Linear(14, 64)               │
│    나머지 (LSTM, TCN, Transformer, 출력 MLP): 변경 없음         │
│                                                              │
│  학습 전략:                                                   │
│    - V2 체크포인트에서 초기화 (LSTM/TCN/Transformer 가중치)     │
│    - FiLM 레이어만 새로 초기화                                 │
│    - 혼합 학습: 합성 + MEAD                                   │
└────────────────────────┬────────────────────────────────────┘
                         ↓
┌────────────────────────┴────────────────────────────────────┐
│ Phase 3: 배포                                                │
│                                                              │
│  ONNX 내보내기 (INT8 양자화)                                  │
│    → lipsync-wasm-se 통합                                    │
│    → AnimaSync 가이드 페이지 업데이트                          │
│    → LLM 감정 태그 파서 구현                                  │
└─────────────────────────────────────────────────────────────┘
```

---

## 3. 감정 체계 (16개: 5 Base + 11 Sub)

### 3.1 감정 계층 구조

```
5개 Base 감정 (= MEAD 데이터, delta = 0):
  JOY      (기쁨)     = MEAD happy
  SADNESS  (슬픔)     = MEAD sad
  ANGER    (분노)     = MEAD angry
  SURPRISE (놀람)     = MEAD surprised
  NEUTRAL  (중립)     = MEAD neutral

11개 Sub 감정 (= Base + VAD delta):
  JOY 계열:
    ├── laughter (웃음)    — 고각성, 리드미컬한 턱 움직임, Duchenne 미소
    ├── excitement (흥분)   — 고각성, 넓은 눈, 올라간 눈썹
    ├── agreement (동의)    — 저각성, 짧은 미소 펄스, 끄덕임
    └── gratitude (감사)    — 중각성, browInnerUp + 부드러운 미소

  SADNESS 계열:
    ├── crying (울음)       — 고강도, 턱 떨림, grief brow
    ├── sulk (삐침)        — 저각성, 지속적 찡그림, 경직
    ├── apology (사과)      — 중각성, 시선 회피, 입 다물기
    └── struggle (고민)     — 중각성, 눈썹 긴장, 억제된 표정

  ANGER 계열:
    └── refusal (거절)      — 중각성, 짧은 스파이크 후 턱 고정

  SURPRISE 계열:
    ├── fluster (당황)      — 중각성, 미소+입다물기 진동 (억제 패턴)
    └── shy (수줍음)        — 저각성, 시선 회피, 억제된 미소
```

**총 16개 감정 클래스. `NUM_EMOTION_CLASSES = 16`**

### 3.2 VAD 기반 강도 스펙트럼

각 감정의 강도(1~3)를 **VAD 공간에서의 위치**로 정의. 강도가 올라갈수록 neutral에서 감정의 극단 좌표를 향해 이동.

**보간 공식:**
```
VAD_at_intensity = neutral + (extreme - neutral) × intensity_factor
  neutral = (0.50, 0.30, 0.50)
  intensity_factor: 강도 1 = 0.40, 강도 2 = 0.70, 강도 3 = 0.95
```

#### Base 감정 VAD 좌표 (MEAD 데이터, delta = 0)

| Base 감정 | V | A | D | MEAD 매핑 |
|----------|------|------|------|----------|
| JOY | 0.87 | 0.72 | 0.72 | happy |
| SADNESS | 0.17 | 0.30 | 0.28 | sad |
| ANGER | 0.17 | 0.82 | 0.80 | angry |
| SURPRISE | 0.55 | 0.82 | 0.42 | surprised |
| NEUTRAL | 0.50 | 0.30 | 0.50 | neutral |

#### Sub 감정 극단 VAD 좌표 (강도 3의 목표점)

| Sub 감정 | 부모 Base | V극단 | A극단 | D극단 | 근거 |
|----------|----------|------|------|------|------|
| laughter | JOY | 0.92 | 0.85 | 0.70 | JOY 대비 V/A 상향, 발성 동반 |
| excitement | JOY | 0.85 | 0.88 | 0.65 | 연구 문서 직접 인용 |
| agreement | JOY | 0.72 | 0.45 | 0.62 | 온화한 긍정, 차분, 동의=주체성 |
| gratitude | JOY | 0.85 | 0.45 | 0.38 | 연구 문서, D 하향=취약성/수용 |
| crying | SADNESS | 0.12 | 0.65 | 0.15 | 능동적 울음, 매우 낮은 D |
| sulk | SADNESS | 0.22 | 0.25 | 0.35 | 위축, 저각성, 약간의 거부 주체성 |
| apology | SADNESS | 0.32 | 0.48 | 0.25 | 연구 문서 직접 인용 |
| struggle | SADNESS | 0.25 | 0.72 | 0.32 | 부정+고각성+저통제 (노력/분투) |
| refusal | ANGER | 0.28 | 0.55 | 0.78 | ANGER 대비 A 하향=절제, D 유지 |
| fluster | SURPRISE | 0.35 | 0.72 | 0.22 | 부정적 놀람, 고각성, 매우 낮은 D |
| shy | SURPRISE | 0.42 | 0.52 | 0.20 | 약한 부정, 중각성, 자의식 |

#### 11개 Sub 감정 × 3단계 강도 VAD 좌표 전체 테이블

| Sub 감정 | 강도 | V | A | D |
|----------|------|------|------|------|
| **laughter** | 1 | 0.67 | 0.52 | 0.58 |
| | 2 | 0.79 | 0.69 | 0.64 |
| | 3 | 0.90 | 0.82 | 0.69 |
| **excitement** | 1 | 0.64 | 0.53 | 0.56 |
| | 2 | 0.75 | 0.71 | 0.61 |
| | 3 | 0.83 | 0.85 | 0.64 |
| **agreement** | 1 | 0.59 | 0.36 | 0.55 |
| | 2 | 0.65 | 0.41 | 0.58 |
| | 3 | 0.71 | 0.44 | 0.61 |
| **gratitude** | 1 | 0.64 | 0.36 | 0.45 |
| | 2 | 0.75 | 0.41 | 0.42 |
| | 3 | 0.83 | 0.44 | 0.39 |
| **crying** | 1 | 0.35 | 0.44 | 0.36 |
| | 2 | 0.23 | 0.55 | 0.26 |
| | 3 | 0.14 | 0.63 | 0.17 |
| **sulk** | 1 | 0.39 | 0.28 | 0.44 |
| | 2 | 0.30 | 0.27 | 0.40 |
| | 3 | 0.23 | 0.25 | 0.36 |
| **apology** | 1 | 0.43 | 0.37 | 0.40 |
| | 2 | 0.37 | 0.43 | 0.33 |
| | 3 | 0.33 | 0.47 | 0.26 |
| **struggle** | 1 | 0.40 | 0.47 | 0.43 |
| | 2 | 0.33 | 0.59 | 0.37 |
| | 3 | 0.26 | 0.70 | 0.33 |
| **refusal** | 1 | 0.41 | 0.40 | 0.61 |
| | 2 | 0.35 | 0.48 | 0.70 |
| | 3 | 0.29 | 0.54 | 0.77 |
| **fluster** | 1 | 0.44 | 0.47 | 0.39 |
| | 2 | 0.40 | 0.59 | 0.30 |
| | 3 | 0.36 | 0.70 | 0.23 |
| **shy** | 1 | 0.47 | 0.39 | 0.38 |
| | 2 | 0.44 | 0.45 | 0.29 |
| | 3 | 0.42 | 0.51 | 0.22 |

> Base 감정(JOY, SADNESS, ANGER, SURPRISE, NEUTRAL)의 강도는 MEAD intensity level(1/2/3)이 그대로 적용됨. VAD 계산 불필요.

### 3.3 VAD → Blendshape 파라메트릭 매핑 규칙

연구 문서 (`vad-to-arkit-blendshape-mapping.md`) 기반. 각 blendshape가 어떤 VAD 차원에 의해 구동되는지:

#### Valence 구동 (V)

| Blendshape | 방향 | 최대 가중치 | 곡선 | 게이트 |
|-----------|------|-----------|------|--------|
| mouthSmile L/R | V+ | 0.85 | 2차 (γ=1.8) | V < 0.35이면 0 |
| cheekSquint L/R | V+ | 0.65 | 2차, 게이트 | V > 0.65에서 활성화 |
| eyeSquint L/R | V+ | 0.40 | 2차, 게이트 | V > 0.6 (Duchenne) |
| mouthDimple L/R | V+ | 0.25 | 선형 | |
| mouthUpperUp L/R | V+ | 0.15 | 선형, 게이트 | V > 0.75 (이빨 노출) |
| mouthFrown L/R | V- | 0.75 | 2차 (γ=1.8) | V > 0.55이면 0 |
| browInnerUp | V- | 0.55 | 선형 | V < 0.35에서 활성화 |
| mouthPress L/R | V- | 0.30 | 선형 | 억제된 부정 감정 |
| mouthStretch L/R | V- | 0.20 | 임계값 | V < 0.2 (고통/공포) |
| mouthRoll Lower/Upper | V- | 0.20/0.15 | 선형 | 눈물 억제 |

#### Arousal 구동 (A)

| Blendshape | 방향 | 최대 가중치 | 곡선 | 게이트 |
|-----------|------|-----------|------|--------|
| eyeWide L/R | A+ | 0.70 | 2차 (γ=1.3) | 핵심 각성 지표 |
| jawOpen | A+ | 0.45 | 선형 | 립싱크 우선 |
| browInnerUp | A+ | 0.45 | 선형 | V-/D-와 누적 |
| browOuterUp L/R | A+ | 0.40 | 선형 | 놀람/경계 |
| mouthShrug U/L | A+ | 0.20 | 선형 | 입술 긴장 |
| noseSneer L/R | A+ | 0.15 | 임계값 | A > 0.75 (호흡 확장) |
| eyeBlink L/R | A- | +0.25 | 선형 (가산) | 저각성 → 무거운 눈꺼풀 |
| eyeSquint L/R | A- | 0.20 | 선형 | 저각성 이완 |

#### Dominance 구동 (D)

| Blendshape | 방향 | 최대 가중치 | 곡선 | 게이트 |
|-----------|------|-----------|------|--------|
| browDown L/R | D+ | 0.55 | 선형 | 핵심 지배력 신호 |
| jawForward | D+ | 0.25 | 선형, 게이트 | D > 0.7 (공격적 턱) |
| noseSneer L/R | D+ | 0.35 | 선형 | A+와 누적 |
| mouthPress L/R | D+ | 0.25 | 선형 | 결단/결의 |
| eyeSquint L/R | D+ | 0.30 | 선형 | 강렬한 눈 좁힘 |
| browInnerUp | D- | 0.60 | 2차 (가속) | 걱정/복종 눈썹 |
| browOuterUp L/R | D- | 0.35 | 선형 | 취약성 신호 |
| eyeWide L/R | D- | 0.30 | 선형 | A+와 누적 |
| eyeLookDown L/R | D- | 0.30 | 선형, 게이트 | D < 0.3 (시선 회피) |
| mouthFunnel | D- | 0.15 | 임계값 | D < 0.25 (신음/복종) |
| mouthLowerDown L/R | D- | 0.20 | 선형 | 취약한 아랫입술 |

#### VAD 상호작용 규칙

1. **극단 각성 증폭**: A > 0.85일 때 모든 가중치 × `1.0 + (A - 0.85) × 2.0` (최대 ~1.3배)
2. **감정가 극성 배제**: V > 0.55이면 부정-V 형상 = 0, V < 0.45이면 긍정-V 형상 = 0, [0.45-0.55] 크로스페이드
3. **지배력-눈썹 충돌**: D > 0.6이면 browInnerUp 억제, D < 0.4이면 browDown 억제, [0.4-0.6] 크로스페이드
4. **저각성 게이트**: A < 0.15이면 모든 blendshape × (A / 0.15)
5. **턱 경쟁**: 립싱크 > 감정 jaw > 각성 baseline (최대값 적용)

### 3.4 감정 × 강도별 주요 Blendshape 활성화 테이블

위의 VAD 매핑 규칙을 적용하여 산출한 실제 blendshape 값:

#### JOY 계열

**laughter (웃음)**

| 강도 | VAD | mouthSmile | cheekSquint | eyeSquint | jawOpen | browInnerUp | eyeWide | 인상 |
|------|-----|-----------|------------|----------|--------|------------|--------|------|
| 1 | (0.67, 0.52, 0.58) | 0.18 | 0.02 | 0.08 | 0.10 | 0.05 | 0.01 | 가벼운 킥킥 |
| 2 | (0.79, 0.69, 0.64) | 0.48 | 0.30 | 0.22 | 0.22 | 0.15 | 0.10 | 소리 내어 웃음 |
| 3 | (0.90, 0.82, 0.69) | 0.80 | 0.58 | 0.35 | 0.38 | 0.25 | 0.28 | 배꼽 잡고 웃음 |

**excitement (흥분)**

| 강도 | VAD | mouthSmile | eyeWide | cheekSquint | browOuterUp | browInnerUp | jawOpen | 인상 |
|------|-----|-----------|--------|------------|-----------|------------|--------|------|
| 1 | (0.64, 0.53, 0.56) | 0.12 | 0.02 | 0.00 | 0.04 | 0.06 | 0.08 | 약간 기대됨 |
| 2 | (0.75, 0.71, 0.61) | 0.38 | 0.15 | 0.18 | 0.15 | 0.18 | 0.18 | 신남 |
| 3 | (0.83, 0.85, 0.64) | 0.70 | 0.38 | 0.50 | 0.30 | 0.30 | 0.35 | 열광 |

**agreement (동의)**

| 강도 | VAD | mouthSmile | eyeSquint | mouthDimple | browDown | mouthPress | 인상 |
|------|-----|-----------|----------|------------|--------|-----------|------|
| 1 | (0.59, 0.36, 0.55) | 0.06 | 0.05 | 0.04 | 0.03 | 0.02 | 미세한 끄덕임 미소 |
| 2 | (0.65, 0.41, 0.58) | 0.14 | 0.10 | 0.06 | 0.05 | 0.04 | 확실한 동의 |
| 3 | (0.71, 0.44, 0.61) | 0.26 | 0.15 | 0.10 | 0.07 | 0.06 | 강한 공감 |

**gratitude (감사)**

| 강도 | VAD | mouthSmile | browInnerUp | eyeSquint | cheekSquint | mouthDimple | 인상 |
|------|-----|-----------|------------|----------|------------|------------|------|
| 1 | (0.64, 0.36, 0.45) | 0.12 | 0.06 | 0.06 | 0.00 | 0.05 | 가벼운 고마움 |
| 2 | (0.75, 0.41, 0.42) | 0.38 | 0.14 | 0.18 | 0.18 | 0.10 | 진심 감사 |
| 3 | (0.83, 0.44, 0.39) | 0.68 | 0.22 | 0.30 | 0.48 | 0.18 | 깊은 감동 |

> **JOY 계열 핵심 구분점:**
> - laughter: jawOpen이 가장 높음 (발성 동반), 리드미컬 진동
> - excitement: eyeWide와 browOuterUp이 가장 높음 (눈이 커짐)
> - agreement: 전체적으로 가장 낮은 활성화 (미세한 펄스)
> - gratitude: browInnerUp이 D- 때문에 활성화 (취약성/감동), 다른 JOY에는 없음

#### SADNESS 계열

**crying (울음)**

| 강도 | VAD | mouthFrown | browInnerUp | eyeSquint | mouthPress | jawOpen | mouthShrugLo | 인상 |
|------|-----|-----------|------------|----------|-----------|--------|-------------|------|
| 1 | (0.35, 0.44, 0.36) | 0.10 | 0.12 | 0.05 | 0.06 | 0.05 | 0.04 | 눈물 글썽 |
| 2 | (0.23, 0.55, 0.26) | 0.38 | 0.35 | 0.15 | 0.15 | 0.12 | 0.12 | 울음 |
| 3 | (0.14, 0.63, 0.17) | 0.65 | 0.55 | 0.30 | 0.22 | 0.25 | 0.20 | 오열 |

**sulk (삐침)**

| 강도 | VAD | mouthFrown | mouthPress | browInnerUp | mouthRollLo | eyeBlink | 인상 |
|------|-----|-----------|-----------|------------|------------|--------|------|
| 1 | (0.39, 0.28, 0.44) | 0.06 | 0.06 | 0.04 | 0.03 | 0.06 | 약간 불만 |
| 2 | (0.30, 0.27, 0.40) | 0.22 | 0.12 | 0.12 | 0.08 | 0.06 | 뾰로통 |
| 3 | (0.23, 0.25, 0.36) | 0.40 | 0.18 | 0.22 | 0.14 | 0.07 | 완전 삐짐 |

**apology (사과)**

| 강도 | VAD | browInnerUp | mouthPress | mouthFrown | eyeLookDown | mouthRollLo | 인상 |
|------|-----|------------|-----------|-----------|------------|------------|------|
| 1 | (0.43, 0.37, 0.40) | 0.06 | 0.08 | 0.04 | 0.03 | 0.03 | 약간 미안 |
| 2 | (0.37, 0.43, 0.33) | 0.18 | 0.18 | 0.12 | 0.10 | 0.08 | 사과 |
| 3 | (0.33, 0.47, 0.26) | 0.32 | 0.30 | 0.22 | 0.18 | 0.14 | 깊은 사죄 |

**struggle (고민)**

| 강도 | VAD | browInnerUp | mouthFrown | jawOpen | mouthPress | eyeSquint | eyeWide | 인상 |
|------|-----|------------|-----------|--------|-----------|----------|--------|------|
| 1 | (0.40, 0.47, 0.43) | 0.08 | 0.05 | 0.06 | 0.05 | 0.04 | 0.00 | 약간 힘듦 |
| 2 | (0.33, 0.59, 0.37) | 0.22 | 0.18 | 0.14 | 0.12 | 0.10 | 0.06 | 고민 중 |
| 3 | (0.26, 0.70, 0.33) | 0.38 | 0.35 | 0.22 | 0.18 | 0.18 | 0.15 | 한계 상태 |

> **SADNESS 계열 핵심 구분점:**
> - crying: 가장 높은 A (능동적 고통), jawOpen (울음 소리), mouthStretch (찡그림)
> - sulk: 가장 낮은 A (경직/위축), 입 봉인 (mouthPress), jawOpen = 0
> - apology: eyeLookDown이 핵심 (시선 회피), mouthPress 가장 높음 (자기 조절)
> - struggle: eyeWide 존재 (각성 경계), jawOpen + mouthStretch (분투의 긴장)

#### ANGER 계열

> ANGER base = MEAD angry 그대로 (delta = 0). 아래는 sub 감정만.

**refusal (거절)**

| 강도 | VAD | mouthPress | browDown | eyeSquint | mouthFrown | noseSneer | jawForward | 인상 |
|------|-----|-----------|---------|----------|-----------|----------|-----------|------|
| 1 | (0.41, 0.40, 0.61) | 0.10 | 0.06 | 0.07 | 0.05 | 0.04 | 0.00 | 단호함 |
| 2 | (0.35, 0.48, 0.70) | 0.22 | 0.18 | 0.16 | 0.12 | 0.10 | 0.05 | 거부 |
| 3 | (0.29, 0.54, 0.77) | 0.38 | 0.38 | 0.28 | 0.25 | 0.22 | 0.15 | 완강한 거절 |

> **ANGER 계열 핵심 구분점:**
> - ANGER (base): 높은 A (폭발적), noseSneer 높음, eyeWide (분노 노려봄) — MEAD angry 그대로
> - refusal: 낮은 A (절제됨), mouthPress 최고 (입 봉인), jawOpen = 0, "차가운 벽"

#### SURPRISE 계열

> SURPRISE base = MEAD surprised 그대로 (delta = 0). 아래는 sub 감정만.

**fluster (당황)**

| 강도 | VAD | browInnerUp | eyeWide | mouthFrown | mouthPress | eyeLookDown | jawOpen | 인상 |
|------|-----|------------|--------|-----------|-----------|------------|--------|------|
| 1 | (0.44, 0.47, 0.39) | 0.08 | 0.00 | 0.04 | 0.05 | 0.00 | 0.04 | 약간 당혹 |
| 2 | (0.40, 0.59, 0.30) | 0.25 | 0.10 | 0.10 | 0.10 | 0.06 | 0.10 | 당황 |
| 3 | (0.36, 0.70, 0.23) | 0.48 | 0.30 | 0.18 | 0.16 | 0.15 | 0.18 | 크게 당황 |

**shy (수줍음)**

| 강도 | VAD | browInnerUp | mouthPress | eyeLookDown | eyeSquint | mouthFrown | mouthRollLo | 인상 |
|------|-----|------------|-----------|------------|----------|-----------|------------|------|
| 1 | (0.47, 0.39, 0.38) | 0.05 | 0.05 | 0.00 | 0.04 | 0.02 | 0.02 | 약간 수줍 |
| 2 | (0.44, 0.45, 0.29) | 0.18 | 0.10 | 0.08 | 0.06 | 0.05 | 0.04 | 수줍음 |
| 3 | (0.42, 0.51, 0.22) | 0.35 | 0.15 | 0.18 | 0.08 | 0.08 | 0.08 | 매우 수줍 |

> **SURPRISE 계열 핵심 구분점:**
> - SURPRISE (base): 중립 감정가, mouthFunnel "O"자 입, 가장 빠른 onset — MEAD surprised 그대로
> - fluster: 부정 감정가 (mouthFrown), browInnerUp 높음 (걱정 눈썹), eyeLookDown (압도)
> - shy: 가장 낮은 각성 (조용한 위축), eyeLookDown 최고, mouthRollLower (입술 깨물기)

### 3.5 VAD의 역할 정리

```
합성 생성기 내부 흐름:

  감정 태그 {JOY:excitement:2}
       ↓
  1. 극단 VAD 룩업: excitement → (0.85, 0.88, 0.65)
  2. 강도 보간: neutral + (extreme - neutral) × 0.70 → (0.75, 0.71, 0.61)
  3. VAD → 파라메트릭 blendshape 매핑 (위 규칙 적용)
     - V=0.75 → mouthSmile 0.38, cheekSquint 0.18, ...
     - A=0.71 → eyeWide 0.15, browOuterUp 0.15, ...
     - D=0.61 → browDown 0.07, eyeSquint 0.10, ...
     - 상호작용 규칙 적용 (극성 배제, 눈썹 충돌 해소 등)
  4. 감정별 보정 (gain × offset) 적용
     - excitement 고유: eyeWide ×1.5 (VAD 대비 더 넓은 눈이 핵심 구분점)
  5. Beta 분포로 변환 (±변이 추가)
  6. 시간적 엔벨로프 + 채널 시차 적용
  7. 출력: (T, 52) 학습 데이터

모델 컨디셔닝:
  - 14차원 one-hot × 강도(0~1) = 14차원 벡터
  - 모델은 VAD를 직접 보지 않음 — 학습 데이터를 통해 암묵적으로 학습
```

**VAD 기반 접근의 장점:**
- 강도 1→2→3 전환이 VAD 공간에서의 **연속적 이동**으로 자연스러움
- 얼굴 부위별 V/A/D 담당이 명확 → 과학적 근거에 기반한 표정 생성
- 새로운 감정 추가 시 VAD 좌표만 지정하면 blendshape가 자동 산출
- 감정 전환 시 VAD 공간에서 보간하면 neutral을 자연스럽게 경유

---

## 4. 합성 표정 생성기 설계: Base Emotion + VAD Delta

### 4.1 핵심 아이디어

52채널을 처음부터 생성하는 대신, **부모 감정의 실제 blendshape를 base로 사용**하고 VAD 차이(delta)만큼만 수정하여 하위 감정을 만든다.

```
sub_emotion = base_parent_blendshapes + delta(VAD_sub - VAD_parent)
```

**왜 이게 더 나은가:**
- **base가 진짜 데이터**: MEAD에서 나온 실제 표정 (또는 V2 모델의 출력)
- **delta만 합성**: 52채널 전체가 아니라 차이값만 → 템플릿 위험 최소화
- **코드 절반 이하**: 전체 VAD→blendshape 파라메트릭 시스템 불필요
- **자연스러운 결과**: 기반이 실제 데이터이므로 미세한 자연스러움이 보존됨

### 4.2 VAD Delta → Blendshape Delta 매핑

각 VAD 차원의 변화량(delta)이 어떤 blendshape를 얼마나 조정하는지:

```python
def vad_delta_to_blendshape_delta(dV, dA, dD):
    """VAD 변화량 → blendshape 조정값.

    연구 문서(vad-to-arkit-blendshape-mapping.md)의
    per-blendshape 구동 규칙에서 도출한 선형 계수.
    """
    delta = np.zeros(52)

    # === Valence 변화 (dV) ===
    # 긍정 방향: 미소 증가, 찡그림 감소
    delta[23] += dV * 1.20    # mouthSmileL
    delta[24] += dV * 1.20    # mouthSmileR
    delta[25] -= dV * 1.10    # mouthFrownL (역방향)
    delta[26] -= dV * 1.10    # mouthFrownR
    delta[47] += dV * 0.90    # cheekSquintL
    delta[48] += dV * 0.90    # cheekSquintR
    delta[10] += dV * 0.55    # eyeSquintL (Duchenne)
    delta[11] += dV * 0.55    # eyeSquintR
    delta[27] += dV * 0.35    # mouthDimpleL
    delta[28] += dV * 0.35    # mouthDimpleR

    # === Arousal 변화 (dA) ===
    # 높아질수록: 눈 커짐, 눈썹 올라감, 전반적 활성화
    delta[12] += dA * 0.95    # eyeWideL
    delta[13] += dA * 0.95    # eyeWideR
    delta[17] += dA * 0.60    # jawOpen
    delta[43] += dA * 0.60    # browInnerUp
    delta[44] += dA * 0.55    # browOuterUpL
    delta[45] += dA * 0.55    # browOuterUpR
    delta[33] += dA * 0.30    # mouthShrugLower
    delta[34] += dA * 0.30    # mouthShrugUpper
    # 낮아질수록: 무거운 눈꺼풀
    if dA < 0:
        delta[0]  -= dA * 0.35  # eyeBlinkL (A 낮으면 증가)
        delta[1]  -= dA * 0.35  # eyeBlinkR

    # === Dominance 변화 (dD) ===
    # 높아질수록: 눈썹 내림, 턱 앞으로, 코 찌푸림
    delta[41] += dD * 0.75    # browDownL
    delta[42] += dD * 0.75    # browDownR
    delta[49] += dD * 0.50    # noseSneerL
    delta[50] += dD * 0.50    # noseSneerR
    delta[35] += dD * 0.35    # mouthPressL
    delta[36] += dD * 0.35    # mouthPressR
    delta[14] += dD * 0.30    # jawForward (D > 0.7에서만 의미)
    # 낮아질수록: 취약한 눈썹, 시선 회피
    delta[43] -= dD * 0.80    # browInnerUp (D↓ → 올라감)
    if dD < 0:
        delta[2]  -= dD * 0.40  # eyeLookDownL (D 낮으면 증가)
        delta[3]  -= dD * 0.40  # eyeLookDownR

    return delta
```

### 4.3 14개 감정의 VAD Delta 테이블

각 하위 감정의 부모 대비 VAD delta와 그 효과:

#### JOY 계열 (부모 VAD: V=0.87, A=0.72, D=0.72)

| 하위 감정 | dV | dA | dD | 주요 blendshape 효과 |
|----------|------|------|------|---------------------|
| **laughter** | +0.05 | +0.13 | -0.02 | jawOpen↑, eyeWide↑, 미소 약간↑ |
| **excitement** | -0.02 | +0.16 | -0.07 | eyeWide↑↑, browOuterUp↑, browInnerUp↑ (D↓) |
| **agreement** | -0.15 | -0.27 | -0.10 | 모든 것↓ (미세한 버전), browInnerUp 약간↑ (D↓) |
| **gratitude** | -0.02 | -0.27 | **-0.34** | browInnerUp↑↑ (D↓=취약성), eyeWide↓, 미소 유지 |

> gratitude의 핵심: **D가 크게 낮아짐** → browInnerUp 활성화 = "따뜻한 눈"
> 이것이 gratitude를 plain joy와 구분하는 핵심 마커

#### SADNESS 계열 (부모 VAD: V=0.17, A=0.30, D=0.28)

| 하위 감정 | dV | dA | dD | 주요 blendshape 효과 |
|----------|------|------|------|---------------------|
| **crying** | -0.05 | **+0.35** | -0.13 | eyeWide↑, jawOpen↑, browInnerUp↑ (능동적 울음) |
| **sulk** | +0.05 | -0.05 | +0.07 | 거의 변화 없음 + mouthPress↑ (D↑ → 경직) |
| **apology** | +0.15 | +0.18 | -0.03 | 미소 약간 가능 (V↑), eyeLookDown↑ (D↓) |
| **struggle** | +0.08 | **+0.42** | +0.04 | eyeWide↑↑, jawOpen↑, mouthShrugLower↑ (분투) |

> crying vs struggle: 둘 다 A가 높지만 crying은 D↓(무력), struggle은 D≈(저항 중)

#### ANGER 계열 (부모 = ANGER base, VAD: V=0.17, A=0.82, D=0.80)

| 하위 감정 | dV | dA | dD | 주요 blendshape 효과 |
|----------|------|------|------|---------------------|
| **refusal** | +0.11 | **-0.27** | -0.02 | eyeWide↓↓, jawOpen↓ (절제됨), mouthPress 유지 |

> refusal의 핵심: **A가 크게 낮아짐** → "뜨거운 분노"가 아닌 "차가운 거부"

#### SURPRISE 계열 (부모 = SURPRISE base, VAD: V=0.55, A=0.82, D=0.42)

| 하위 감정 | dV | dA | dD | 주요 blendshape 효과 |
|----------|------|------|------|---------------------|
| **fluster** | -0.20 | -0.10 | **-0.20** | mouthFrown↑ (V↓), browInnerUp↑↑ (D↓), eyeLookDown↑ |
| **shy** | -0.13 | **-0.30** | **-0.22** | eyeWide↓↓, eyeLookDown↑↑, mouthPress↑, eyeBlink↑ |

> shy의 핵심: A와 D 모두 크게 낮아짐 → "조용한 위축" (눈 감김 + 시선 회피)

### 4.4 스타일 모드 (감정별 다중 변형)

단일 delta만 적용하면 모든 gratitude가 같은 스타일 → 모델이 "규칙"을 복제할 위험.
감정당 2-3개 스타일 변형을 gain 벡터로 정의하여 **의미 있는 다양성** 확보:

```python
EMOTION_STYLE_MODES = {
    'gratitude': [
        # A: 따뜻한 감사 (기본) — 볼 강조
        {'cheekSquintL': 1.3, 'cheekSquintR': 1.3, 'mouthSmileL': 1.1, 'mouthSmileR': 1.1},
        # B: 겸손한 감사 — 미소 약하고 눈썹 강조
        {'mouthSmileL': 0.7, 'mouthSmileR': 0.7, 'browInnerUp': 1.4},
        # C: 감동한 감사 — 눈물 직전
        {'browInnerUp': 1.6, 'cheekSquintL': 1.4, 'cheekSquintR': 1.4, 'eyeSquintL': 1.3, 'eyeSquintR': 1.3},
    ],
    'excitement': [
        # A: 넓은 눈 흥분 — 눈 위주
        {'eyeWideL': 1.4, 'eyeWideR': 1.4, 'jawOpen': 1.2},
        # B: 미소 흥분 — 입 위주
        {'eyeWideL': 0.9, 'eyeWideR': 0.9, 'mouthSmileL': 1.3, 'mouthSmileR': 1.3},
    ],
    'crying': [
        # A: 조용한 울음 — browInnerUp 강조
        {'browInnerUp': 1.3, 'jawOpen': 0.6, 'mouthFrownL': 1.2, 'mouthFrownR': 1.2},
        # B: 소리 내어 울음 — jaw/mouth 강조
        {'jawOpen': 1.4, 'mouthStretchL': 1.3, 'mouthStretchR': 1.3, 'browInnerUp': 0.9},
    ],
    'apology': [
        # A: 진지한 사과 — mouthPress + 시선 회피
        {'mouthPressL': 1.3, 'mouthPressR': 1.3, 'eyeLookDownL': 1.2, 'eyeLookDownR': 1.2},
        # B: 미안해하는 사과 — 약한 화해 미소 포함
        {'mouthSmileL': 0.3, 'mouthSmileR': 0.3, 'browInnerUp': 1.2},
    ],
    'shy': [
        # A: 조용한 수줍음 — 시선 회피 위주
        {'eyeLookDownL': 1.4, 'eyeLookDownR': 1.4, 'mouthPressL': 1.2, 'mouthPressR': 1.2},
        # B: 입술 깨물기 수줍음
        {'mouthRollLower': 1.5, 'eyeLookDownL': 1.1, 'eyeLookDownR': 1.1},
    ],
    # sulk, struggle, refusal, fluster, agreement, laughter도 2-3개씩 정의
}
```

각 스타일은 VAD 공간 내의 **미세 이동**에 대응:
- 따뜻한 감사: D 약간↑ (더 자신감)
- 겸손한 감사: D 더↓ (더 취약)
- 감동한 감사: A 약간↑ (더 활성화)

클립 생성 시 랜덤으로 모드 선택 → 동일 감정이라도 다양한 표현.

### 4.5 생성 함수 (핵심 코드)

```python
def generate_v3_target(base_blendshapes, parent_emotion, sub_emotion, features):
    """부모 감정의 base blendshape에 VAD delta를 적용.

    Args:
        base_blendshapes: (T, 52) — V2 모델 또는 MEAD 실제 데이터
        parent_emotion: 'joy' — 부모 감정
        sub_emotion: 'gratitude' — 하위 감정
        features: (T, 141) — 오디오 특성 (변조용)
    Returns:
        target: (T, 52) — V3 학습 타겟
    """
    T = len(base_blendshapes)

    # 1. VAD delta 계산
    parent_vad = PARENT_VAD[parent_emotion]
    sub_vad = SUB_EMOTION_VAD[sub_emotion]
    dV = sub_vad[0] - parent_vad[0]
    dA = sub_vad[1] - parent_vad[1]
    dD = sub_vad[2] - parent_vad[2]

    # 2. VAD delta → blendshape delta
    bs_delta = vad_delta_to_blendshape_delta(dV, dA, dD)

    # 3. 스타일 모드 적용 (감정별 2-3개 변형)
    if sub_emotion in EMOTION_STYLE_MODES:
        mode = rng.choice(EMOTION_STYLE_MODES[sub_emotion])
        for ch_name, gain in mode.items():
            ch_idx = BLENDSHAPE_NAME_TO_IDX[ch_name]
            bs_delta[ch_idx] *= gain

    # 4. base에 delta 적용 (표정 채널만)
    target = base_blendshapes.copy()
    for ch in EXPRESSION_CHANNELS:  # 립싱크 채널 제외
        target[:, ch] += bs_delta[ch]

    # 5. 입 채널 speech-aware 감쇠
    #    LAM의 입 채널 활성도가 높으면 → 감정 delta를 줄임
    #    (발화 중 입 다물기 같은 충돌 방지)
    for ch in MOUTH_EMOTION_CHANNELS:  # mouthSmile, mouthPress, mouthFrown 등
        speech_activity = (
            base_blendshapes[:, 17] * 1.2 +  # jawOpen
            base_blendshapes[:, 18] * 1.5 +  # mouthClose (양순음 보호)
            base_blendshapes[:, 19] * 1.0 +  # mouthFunnel
            base_blendshapes[:, 20] * 1.0     # mouthPucker
        ).clip(0, 1)
        emotion_influence = 1.0 - speech_activity * 0.7  # 완전히 사라지지 않음
        delta_on_this_ch = target[:, ch] - base_blendshapes[:, ch]
        target[:, ch] = base_blendshapes[:, ch] + delta_on_this_ch * emotion_influence

    # 6. 특수 패턴 추가 (delta로 표현 불가능한 것들)
    if sub_emotion == 'laughter':
        freq = np.random.uniform(3.0, 6.0)
        t = np.arange(T) / 30.0
        target[:, 17] += 0.15 * np.maximum(np.sin(2*np.pi*freq*t), 0)

    elif sub_emotion == 'crying':
        freq = np.random.uniform(4.0, 8.0)
        t = np.arange(T) / 30.0
        target[:, 33] += 0.10 * np.abs(np.sin(2*np.pi*freq*t))

    elif sub_emotion == 'fluster':
        freq = np.random.uniform(2.0, 4.0)
        t = np.arange(T) / 30.0
        osc = 0.12 * np.sin(2*np.pi*freq*t)
        target[:, 23] += np.maximum(osc, 0)
        target[:, 24] += np.maximum(osc, 0)
        target[:, 35] += np.maximum(-osc, 0)
        target[:, 36] += np.maximum(-osc, 0)

    # 7. 변이 추가
    noise_scale = 0.03
    for ch in EXPRESSION_CHANNELS:
        drift = noise_scale * np.sin(
            2*np.pi * np.random.uniform(0.1, 0.3) * np.arange(T)/30.0
            + np.random.uniform(0, 6.28))
        target[:, ch] += drift

    # 8. 클램프
    return np.clip(target, 0.0, 1.0).astype(np.float32)
```

### 4.5 base blendshape를 얻는 3가지 방법

| 방법 | 적용 대상 | 설명 |
|------|----------|------|
| **A. MEAD 실제 데이터** | MEAD 클립 재라벨링 | `teacher_outputs/{clip_id}.npy` 그대로 base로 사용 |
| **B. V2 모델 추론** | TTS 신규 클립 | V2 모델에 부모 감정 컨디셔닝으로 추론 → base |
| **C. MEAD 평균 프로파일** | base 없는 경우 | 부모 감정별 평균 blendshape를 static base로 사용 |

**방법 A (가장 좋음):**
```python
# MEAD "happy level 2" 클립 → base로 사용
base = np.load('teacher_outputs/M003_happy_2_015.npy')  # (88, 52)

# "gratitude"로 재라벨링 + delta 적용
target = generate_v3_target(base, 'joy', 'gratitude', features)

# 저장 (clip_id 형식 변경)
np.save('teacher_outputs/M003_gratitude_2_015.npy', target)
```

**방법 B (TTS 클립):**
```python
# TTS 오디오로 V2 모델 추론 (부모 감정 = joy)
base = v2_model.infer(features, emotion=[0, 1, 0, 0, 0])  # happy 컨디셔닝

# "excitement"로 delta 적용
target = generate_v3_target(base, 'joy', 'excitement', features)
```

### 4.6 이 접근법의 한계와 대응

| 한계 | 대응 |
|------|------|
| VAD delta가 선형 → 극단적 차이에서 부정확 | 계수 튜닝 + 3.4절의 blendshape 활성화 테이블로 검증 |
| 특수 패턴 (진동, 떨림)은 delta로 표현 불가 | 4.4절의 특수 패턴을 별도 코드로 추가 |
| 부모와 매우 다른 하위 감정 (shy ≠ surprise) | delta가 크면 base의 영향력 감소 → 사실상 새로 생성과 유사. OK |
| base 품질에 의존 | MEAD 실제 데이터 또는 V2 모델 출력 사용으로 높은 품질 보장 |

### 4.8 3-Way 채널 분류 시스템 (립싱크 강력 보호)

V2 student 모델은 52채널 전부를 마스크 없이 출력. **립싱크 보호는 학습 타겟 수준에서 처리.**
모든 52 ARKit 채널을 3개 영역으로 정확히 분류:

#### LIPSYNC_ONLY (14채널) — LAM 값 그대로, 감정 절대 불개입

핵심 조음 채널. 음소(phoneme) 발음에 직접 관여. 감정이 건드리면 립싱크 깨짐.

```python
LIPSYNC_ONLY = [
    14,         # jawForward
    15, 16,     # jawLeft, jawRight
    18,         # mouthClose
    19,         # mouthFunnel
    20,         # mouthPucker
    21, 22,     # mouthLeft, mouthRight
    29, 30,     # mouthStretchLeft/Right
    37, 38,     # mouthLowerDownLeft/Right
    39, 40,     # mouthUpperUpLeft/Right
]
```

학습 타겟에서: `target[:, LIPSYNC_ONLY] = lam_output[:, LIPSYNC_ONLY]` (항상)

#### EXPRESSION_ONLY (22채널) — 감정이 완전히 소유, 립싱크 무관

눈, 눈썹, 볼, 코, 시선. 발화와 물리적으로 독립된 근육.

```python
EXPRESSION_ONLY = [
    0, 1,       # eyeBlinkLeft/Right
    2, 3, 4, 5, 6, 7, 8, 9,  # eyeLook 8방향
    10, 11,     # eyeSquintLeft/Right
    12, 13,     # eyeWideLeft/Right
    41, 42,     # browDownLeft/Right
    43,         # browInnerUp
    44, 45,     # browOuterUpLeft/Right
    47, 48,     # cheekSquintLeft/Right
    49, 50,     # noseSneerLeft/Right
    51,         # tongueOut
]
```

학습 타겟에서: `target[:, EXPRESSION_ONLY] = base[:, EXPRESSION_ONLY] + vad_delta` (감정 전적 반영)

#### SHARED (16채널) — 감정과 립싱크 모두 사용, speech-aware 처리 필요

감정 표현에 중요하지만 발화에도 관여하는 채널. **발화 중에는 감정 영향 감쇠, 침묵 중에는 감정 전적 반영.**

```python
SHARED = [
    17,         # jawOpen ★ 특수 처리: 곱셈(gain) 방식
    23, 24,     # mouthSmileLeft/Right (감사/기쁨 + /i/ 모음)
    25, 26,     # mouthFrownLeft/Right (슬픔/분노 + 발화 왜곡)
    27, 28,     # mouthDimpleLeft/Right
    31, 32,     # mouthRollLower/Upper (억제/깨물기 + 입술 위치)
    33, 34,     # mouthShrugLower/Upper (턱 긴장 + jaw 위치)
    35, 36,     # mouthPressLeft/Right (분노/거절 + 양순음)
    46,         # cheekPuff
]
```

학습 타겟에서의 SHARED 채널 처리:

```python
# 1. speech_activity 계산 (LAM 출력의 입 채널 활성도)
speech_activity = np.clip(
    lam[:, 17] * 1.2 +    # jawOpen — 주요 조음 지표
    lam[:, 18] * 1.5 +    # mouthClose — 양순음 보호 최우선
    lam[:, 19] * 1.0 +    # mouthFunnel
    lam[:, 20] * 1.0,     # mouthPucker
    0, 1
)

# 2. 감정 영향력 = 발화 활성도의 역수 (완전히 0이 되지 않음)
emotion_influence = 1.0 - speech_activity * 0.7

# 3. SHARED 채널 타겟 = LAM base + 감쇠된 감정 delta
for ch in SHARED:
    if ch == 17:  # jawOpen은 곱셈 방식 (가산하면 모음 구분 깨짐)
        arousal_gain = 1.0 + dA * 0.3 * emotion_influence
        target[:, ch] = lam[:, ch] * arousal_gain
    else:  # 나머지 SHARED 채널은 가산 + 감쇠
        target[:, ch] = lam[:, ch] + bs_delta[ch] * emotion_influence
```

**jawOpen 특수 처리 이유:**
- jawOpen은 모음 높이를 구분하는 핵심 채널 (/a/=높음, /i/=낮음)
- 가산(additive)하면 모든 모음이 균일하게 올라감 → /i/가 /ae/처럼 보임
- 곱셈(gain)하면 모음 패턴 유지하면서 전체 범위만 스케일 → 흥분 시 입 더 크게 벌림

**길항근 보정 (Antagonistic Corrective):**

실제 인간 얼굴에서 서로 반대되는 근육은 동시에 최대 활성화 불가:
```python
# 미소 vs 입술 모으기 — 동시 발생 시 보정
if target[:, 23] > 0.2 and lam[:, 19] > 0.2:  # mouthSmile + mouthFunnel
    target[:, 23] *= (1.0 - lam[:, 19] * 0.6)  # /u/ 모음 중 미소 감쇠

# 미소 vs 찡그림 — 상호 억제
coactive = np.minimum(target[:, 23], target[:, 25])
if coactive.max() > 0.15:
    target[:, 23] -= coactive * 0.5  # 강한 쪽이 이김
    target[:, 25] -= coactive * 0.5
```

#### 채널 분류 요약

| 영역 | 채널 수 | 타겟 소스 | 감정 개입 | 립싱크 보호 |
|------|--------|----------|----------|-----------|
| LIPSYNC_ONLY | 14 | LAM 100% | 없음 | 완전 보호 |
| EXPRESSION_ONLY | 22 | Base + VAD delta | 전적 | 해당 없음 |
| SHARED | 16 | LAM + 감쇠된 delta | speech-aware 감쇠 | 발화 중 보호 |

> **합계**: 14 + 22 + 16 = 52 채널

---

## 5. 모델 아키텍처 (V2 재활용)

### 5.1 변경 사항 최소화

핵심 발견: **V2 아키텍처를 거의 그대로 사용 가능.** 변경되는 것은 감정 차원뿐.

```python
# config_e2f.py 변경
NUM_EMOTION_CLASSES = 5   →   NUM_EMOTION_CLASSES = 16

# 모델 내부에서 자동으로 변경되는 부분:
# FiLM 레이어: Linear(5, 64) → Linear(16, 64)  (+704 파라미터, 무시할 수 있는 수준)
# 감정 헤드: Linear(256, 5) → Linear(256, 16)

# 변경 없는 부분 (전부):
# - LSTM (320 hidden, 2 layers)
# - CausalDilatedConv (k=5, dilations [1,2,4,8])
# - CausalTransformer
# - 출력 MLP (256 → 52)
# - 채널 마스크 (확장만)
# - 손실 함수 구조
```

**FiLM 레이어 구조화 초기화 (계층형의 80% 효과, 0% 복잡도):**

계층형 컨디셔닝(parent + sub 분리)은 16클래스에서 과도한 엔지니어링.
대신 FiLM 임베딩을 부모-자식 구조로 초기화하여 학습 수렴 가속:

```python
# 같은 부모의 sub 감정들은 가까운 임베딩에서 시작
parent_centroids = {name: torch.randn(64) * 0.1 for name in ['JOY','SADNESS','ANGER','SURPRISE','NEUTRAL']}

PARENT_OF = {
    0: 'JOY', 1: 'SADNESS', 2: 'ANGER', 3: 'SURPRISE', 4: 'NEUTRAL',  # base
    5: 'JOY', 6: 'JOY', 7: 'JOY', 8: 'JOY',                          # JOY subs
    9: 'SADNESS', 10: 'SADNESS', 11: 'SADNESS', 12: 'SADNESS',        # SADNESS subs
    13: 'ANGER',                                                        # ANGER sub
    14: 'SURPRISE', 15: 'SURPRISE',                                     # SURPRISE subs
}

for idx, parent in PARENT_OF.items():
    film_layer.weight.data[:, idx] = parent_centroids[parent] + torch.randn(64) * 0.02
# → JOY/laughter/excitement/agreement/gratitude 모두 비슷한 초기 임베딩
# → 학습 중 자연스럽게 분화하되, 가족 유사성 유지
```

### 5.2 컨디셔닝 벡터 설계

```
입력: 16차원 감정 벡터 + 강도 스케일링

LLM 태그: {JOY:excitement:30}
  → emotion_vec = one_hot(16)[EXCITEMENT_IDX=6]  # [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0]
  → emotion_vec *= 30 / 100                      # [0,0,0,0,0,0,0.30,0,...,0]
  → 모델의 FiLM 레이어에 입력

LLM 태그: {JOY:50}  (base 감정 직접 사용)
  → emotion_vec = one_hot(16)[JOY_IDX=0]
  → emotion_vec *= 50 / 100
  → 모델의 FiLM 레이어에 입력
```

**감정 인덱스 매핑 (16개):**

| 인덱스 | 감정 | 유형 | 설명 |
|--------|------|------|------|
| 0 | JOY | Base | = MEAD happy (delta = 0) |
| 1 | SADNESS | Base | = MEAD sad (delta = 0) |
| 2 | ANGER | Base | = MEAD angry (delta = 0) |
| 3 | SURPRISE | Base | = MEAD surprised (delta = 0) |
| 4 | NEUTRAL | Base | = MEAD neutral (delta = 0) |
| 5 | laughter | Sub | JOY + delta |
| 6 | excitement | Sub | JOY + delta |
| 7 | agreement | Sub | JOY + delta |
| 8 | gratitude | Sub | JOY + delta |
| 9 | crying | Sub | SADNESS + delta |
| 10 | sulk | Sub | SADNESS + delta |
| 11 | apology | Sub | SADNESS + delta |
| 12 | struggle | Sub | SADNESS + delta |
| 13 | refusal | Sub | ANGER + delta |
| 14 | fluster | Sub | SURPRISE + delta |
| 15 | shy | Sub | SURPRISE + delta |

### 5.3 학습 전략

**데이터 혼합:**

| 출처 | 클립 수 | 감정 수 | 역할 |
|------|--------|--------|------|
| MEAD 실제 데이터 | 3,685 | 5 (상위 카테고리) | 현실감 보존, 립싱크 정확도 |
| 합성 데이터 | 14,000-21,000 | 14 (하위 카테고리) | 감정 확장 (11 sub 감정) |

**학습 단계:**

```
1단계: V2 체크포인트에서 초기화
  - LSTM, TCN, Transformer 가중치 = V2 것 사용
  - FiLM 레이어 = 새로 초기화 (차원 변경)
  - 감정 헤드 = 새로 초기화

2단계: 혼합 학습
  - 배치 구성: 50% MEAD + 50% 합성
  - MEAD 클립: 상위 카테고리 라벨 사용 (neutral, joy, anger, sadness, surprise)
    → 해당 상위 카테고리의 첫 번째 하위 감정으로 매핑
    → 예: MEAD "happy" → V3 "laughter" (가장 표준적인 기쁨)
  - 합성 클립: 14개 하위 카테고리 라벨 사용
  - 학습률: 3e-4 (새 레이어), 3e-5 (기존 레이어 fine-tune)
  - Epoch: 50-100

3단계: 합성 데이터 비중 점진적 증가
  - Epoch 1-20: MEAD 50% + 합성 50%
  - Epoch 21-40: MEAD 30% + 합성 70%
  - Epoch 41+: MEAD 20% + 합성 80%
```

**손실 함수 (V2 확장, 3-way 채널 가중치 적용):**

```python
L = 100 * L_residual_weighted   # 3-way 채널 가중치 적용된 L1
  + 50  * L_peak_weighted       # 표현력 있는 프레임 가중치
  + 5   * L_velocity_brow       # 눈썹 1차 미분 매칭
  + 2   * L_acceleration_brow   # 눈썹 2차 미분 매칭
  + 10  * L_velocity_lipsync    # 신규: LIPSYNC_ONLY 채널 시간적 매칭 (립싱크 동역학 보호)
  + 0.1 * L_emotion_cls         # CrossEntropy, 16클래스 분류
```

**3-way 채널 가중치:**

```python
# L_residual에 적용되는 per-channel 가중치
channel_weights = np.ones(52)
channel_weights[LIPSYNC_ONLY] = 3.0   # 립싱크 채널: 3배 가중치 → LAM과의 괴리 강하게 페널티
channel_weights[EXPRESSION_ONLY] = 1.0 # 표정 채널: 표준
channel_weights[SHARED] = 1.5          # 공유 채널: 약간 높게 → 립싱크/표정 균형 유도

# L_velocity_lipsync: LIPSYNC_ONLY 채널의 프레임 간 속도 매칭
# → 모델이 LAM의 시간적 패턴(조음 궤적)을 정확히 따르도록 강제
def velocity_lipsync_loss(pred, target):
    pred_vel = pred[:, 1:, LIPSYNC_ONLY] - pred[:, :-1, LIPSYNC_ONLY]
    target_vel = target[:, 1:, LIPSYNC_ONLY] - target[:, :-1, LIPSYNC_ONLY]
    return F.l1_loss(pred_vel, target_vel)
```

**이 설계의 효과:**
- LIPSYNC_ONLY: 3배 가중치 + velocity 매칭 → LAM 출력과 거의 동일하게 학습됨
- EXPRESSION_ONLY: 표준 가중치 → 감정 표현에 집중
- SHARED: 1.5배 가중치 → 타겟 자체가 이미 speech-aware 감쇠 적용되어 있으므로, 모델이 자연스럽게 "발화 중 감쇠, 침묵 중 표현" 패턴을 학습

---

## 6. 합성 데이터 생성 파이프라인 — 정확한 실행 흐름

### 6.0 기존 V2 학습 데이터 형식 (이것에 맞춰야 함)

V2 학습 파이프라인이 기대하는 파일 형식:

```
e2f/distill/
  precomputed_features/{clip_id}.npy    # (T, 141) float32 — 오디오 특성
  teacher_outputs/{clip_id}.npy         # (T, 52)  float32 — 학습 타겟 blendshape

clip_id 형식: {speaker}_{emotion}_{level}_{sentence}
  예: SYNTH001_excitement_2_0042
  - level 위치(split('_')[2])에서 강도 파싱 → intensity = level / 3.0
```

**모델이 받는 배치:**
```python
batch = {
    'features': Tensor(B, T, 141),   # 오디오 특성
    'teacher':  Tensor(B, T, 52),    # 학습 타겟
    'emotion':  Tensor(B, 14),       # 강도 스케일된 one-hot (V3: 14차원)
    'lengths':  Tensor(B,),          # 실제 길이
}
# emotion 예: excitement 강도2 → [0,0,0.667,0,...,0] (인덱스 2에 2/3)
```

### 6.1 전체 파이프라인 (5단계)

```
┌──────────────────────────────────────────────────────────────────────┐
│ Step 1: 오디오 확보                                                    │
│                                                                      │
│  A. MEAD 오디오 재활용 (4,490 클립, 5개 감정)                           │
│     → 이미 precomputed_features/ 에 141차원 특성 존재                   │
│     → 이미 teacher_outputs/ 에 LAM 립싱크 출력 존재                     │
│                                                                      │
│  B. TTS 신규 생성 (ElevenLabs, ~10,000 클립)                           │
│     → 14개 감정별 텍스트 스크립트 × 다양한 화자 음성                     │
│     → 생성 후 AudioFeatureExtractor로 141차원 특성 추출                 │
│     → LAM 모델로 립싱크 blendshape 추출                                │
└──────────────────────────┬───────────────────────────────────────────┘
                           ↓
┌──────────────────────────┴───────────────────────────────────────────┐
│ Step 2: 오디오 특성 추출 (기존 도구 재사용)                              │
│                                                                      │
│  python e2f/distill/precompute_features.py                           │
│    입력: 오디오 파일 (.m4a, .mp3, .wav)                                │
│    출력: precomputed_features/{clip_id}.npy  (T, 141) float32        │
│                                                                      │
│  * MEAD 클립: 이미 완료 (4,488 파일 존재)                              │
│  * TTS 클립: 신규 실행 필요                                            │
└──────────────────────────┬───────────────────────────────────────────┘
                           ↓
┌──────────────────────────┴───────────────────────────────────────────┐
│ Step 3: LAM 립싱크 추출 (기존 도구 재사용)                              │
│                                                                      │
│  python e2f/distill/precompute_teacher.py --lam_only                 │
│    입력: 오디오 파일                                                   │
│    출력: lam_outputs/{clip_id}.npy  (T, 52) float32                  │
│           → 립싱크 채널만 유효, 표정 채널은 무시할 것                    │
│                                                                      │
│  * MEAD 클립: output/lam_outputs/ 에 이미 존재                        │
│  * TTS 클립: 신규 실행 필요                                            │
└──────────────────────────┬───────────────────────────────────────────┘
                           ↓
┌──────────────────────────┴───────────────────────────────────────────┐
│ Step 4: 합성 표정 생성 (★ 핵심 — 이것이 "데이터셋을 만드는 모델")       │
│                                                                      │
│  python generator/generate_v3_targets.py                             │
│                                                                      │
│  각 클립마다:                                                         │
│    입력:                                                              │
│      - clip_id, emotion_label, intensity_level (1/2/3)               │
│      - lam_output (T, 52) — 립싱크 기반 blendshape                   │
│      - audio_features (T, 141) — 에너지/피치 변조용                   │
│                                                                      │
│    처리 (Base + VAD Delta 방식):                                     │
│      ① base blendshape 확보:                                        │
│         MEAD 클립 → 기존 teacher_output 사용                         │
│         TTS 클립 → V2 모델로 부모 감정 추론                           │
│      ② VAD delta 계산: sub_VAD - parent_VAD                         │
│      ③ delta → blendshape 조정 (4.2절 매핑)                         │
│      ④ base + delta 적용 (표정 채널만)                               │
│      ⑤ 특수 패턴 추가 (jaw 진동, 턱 떨림 등)                         │
│      ⑥ 변이 추가 (미세 노이즈)                                       │
│      ⑦ 클램프 [0, 1]                                                │
│                                                                      │
│    출력: teacher_outputs/{clip_id}.npy (T, 52) float32               │
│           → 립싱크(LAM) + 표정(합성) 병합된 전체 52채널                │
└──────────────────────────┬───────────────────────────────────────────┘
                           ↓
┌──────────────────────────┴───────────────────────────────────────────┐
│ Step 5: 메타데이터 CSV 생성                                            │
│                                                                      │
│  python generator/create_metadata.py                                 │
│    출력: output/v3_clips.csv                                         │
│    컬럼: clip_id, speaker_id, emotion, level, audio_path, duration   │
│                                                                      │
│  clip_id 예시:                                                        │
│    SYNTH001_excitement_2_0042  → speaker SYNTH001, 감정 excitement,  │
│                                  강도 2, 문장 0042                    │
│    M003_laughter_3_015         → MEAD M003 재라벨링, 감정 laughter,   │
│                                  강도 3, 문장 015                     │
└──────────────────────────────────────────────────────────────────────┘
```

### 6.2 Step 4 상세: generate_v3_targets.py 내부 흐름

```python
def generate_v3_targets(clip_id, emotion, level, lam_output, features):
    """하나의 클립에 대한 V3 학습 타겟 생성.

    Args:
        clip_id: 'SYNTH001_excitement_2_0042'
        emotion: 'excitement'
        level: 2
        lam_output: (T, 52) float32 — LAM 립싱크
        features: (T, 141) float32 — 오디오 특성
    Returns:
        target: (T, 52) float32 — 병합된 학습 타겟
    """
    T = min(len(lam_output), len(features), 300)
    rng = np.random.default_rng()

    # ① VAD 좌표 계산
    extreme = EMOTION_VAD_EXTREMES[emotion]
    neutral = (0.50, 0.30, 0.50)
    factor = {1: 0.40, 2: 0.70, 3: 0.95}[level]
    V, A, D = [n + (e - n) * factor for n, e in zip(neutral, extreme)]

    # ② VAD → 파라메트릭 blendshape (52차원 타겟 벡터)
    static_target = vad_to_blendshapes_parametric(V, A, D)

    # ③ 감정별 gain×offset 보정
    corr = EMOTION_CORRECTIONS[emotion]
    static_target = static_target * corr.gain + corr.offset

    # ④ Beta 분포 변이 + 화자 스타일
    speaker_style = generate_speaker_style(rng)
    varied_target = np.zeros(52)
    for ch in range(52):
        if static_target[ch] < 0.01:
            continue
        a, b = mean_to_beta(static_target[ch] * speaker_style[ch],
                            concentration=corr.concentration.get(ch, 8.0))
        varied_target[ch] = rng.beta(max(a, 0.01), max(b, 0.01))

    # ⑤⑥ 시간적 엔벨로프 + 채널 시차
    profile = TEMPORAL_PROFILES[emotion]
    envelope = np.zeros((T, 52))
    for ch in range(52):
        if varied_target[ch] < 0.01:
            continue
        offset_frames = int(profile.onset_offsets.get(ch, 0) / 1000 * 30)
        onset_frames = int(rng.uniform(*profile.onset_ms) / 1000 * 30)
        release_start = T - int(rng.uniform(*profile.release_ms) / 1000 * 30)

        for f in range(T):
            f_adj = f - offset_frames
            if f_adj < 0:
                envelope[f, ch] = 0.0
            elif f_adj < onset_frames:
                t = f_adj / onset_frames
                envelope[f, ch] = smoothstep(t) ** (1.0 / profile.sharpness)
            elif f < release_start:
                envelope[f, ch] = 1.0
            else:
                t_rel = (f - release_start) / (T - release_start)
                envelope[f, ch] = 1.0 - smoothstep(t_rel)

    expression = varied_target[np.newaxis, :] * envelope  # (T, 52)

    # ⑦ 오디오 변조
    rms_energy = features[:T, 122]   # 인덱스 122 = RMS energy
    pitch = features[:T, 120]        # 인덱스 120 = pitch
    sensitivity = AUDIO_SENSITIVITY[emotion]

    energy_mod = 0.4 + 0.9 * normalize(rms_energy) * sensitivity  # [0.4, 1.3]
    expression *= energy_mod[:, np.newaxis]

    # 피치 변조 (상부 얼굴만)
    pitch_dev = (pitch - 0.3) / 0.3  # 정규화된 피치 편차
    expression[:, 43] += pitch_dev * 0.10  # browInnerUp
    expression[:, 12] += np.maximum(pitch_dev, 0) * 0.05  # eyeWide

    # ⑧ 특수 패턴
    if emotion == 'laughter':
        # jaw 진동: 3-6Hz
        freq = rng.uniform(3.0, 6.0)
        t = np.arange(T) / 30.0
        jaw_osc = 0.15 * np.sin(2 * np.pi * freq * t) * envelope[:, 17]
        expression[:, 17] += np.maximum(jaw_osc, 0)  # jawOpen에 추가

    elif emotion == 'crying' and level >= 2:
        # 턱 떨림: 4-8Hz
        freq = rng.uniform(4.0, 8.0)
        t = np.arange(T) / 30.0
        tremble = 0.10 * np.sin(2 * np.pi * freq * t) * envelope[:, 33]
        expression[:, 33] += np.abs(tremble)  # mouthShrugLower

    elif emotion == 'fluster':
        # 미소/억제 진동: 2-4Hz
        freq = rng.uniform(2.0, 4.0)
        t = np.arange(T) / 30.0
        osc = 0.12 * np.sin(2 * np.pi * freq * t) * envelope[:, 23]
        expression[:, 23] += np.maximum(osc, 0)  # mouthSmileLeft
        expression[:, 24] += np.maximum(osc, 0)  # mouthSmileRight
        expression[:, 35] += np.maximum(-osc, 0)  # mouthPressLeft
        expression[:, 36] += np.maximum(-osc, 0)  # mouthPressRight

    # ⑨ 시간적 변이 (hold 중 미세 변동)
    for ch in range(52):
        if varied_target[ch] < 0.05:
            continue
        drift = 0.05 * np.sin(2 * np.pi * rng.uniform(0.1, 0.3)
                               * np.arange(T) / 30.0 + rng.uniform(0, 6.28))
        expression[:, ch] += drift * envelope[:, ch]

    # ⑩ 립싱크 병합
    target = np.zeros((T, 52), dtype=np.float32)
    for ch in range(52):
        if ch in LIPSYNC_CRITICAL:
            target[:, ch] = lam_output[:T, ch]       # LAM이 담당
        else:
            target[:, ch] = expression[:, ch]          # 합성 생성기가 담당

    return np.clip(target, 0.0, 1.0)
```

### 6.3 오디오 확보 전략

| 출처 | 클립 수 | 역할 | 처리 방법 |
|------|--------|------|----------|
| **MEAD 기존** | ~4,490 | 5개 상위 감정 (현실감 기반) | 하위 감정으로 재라벨링 |
| **ElevenLabs TTS 기존** | 5,001 | 5개 감정 | 하위 감정으로 재라벨링 |
| **ElevenLabs TTS 신규** | ~8,000-12,000 | 14개 감정 전용 | 새로 생성 |
| **합계** | ~17,000-21,000 | | |

**MEAD 재라벨링 규칙:**

MEAD는 5개 감정(neutral, happy, angry, sad, surprised)으로 라벨링됨.
이를 14개 하위 감정으로 재매핑:

```
MEAD happy → 강도별 분기:
  level 1 → agreement (가벼운 동의 미소)
  level 2 → gratitude (중간 감사/기쁨)
  level 3 → laughter (강한 기쁨/웃음)
  + 일부를 excitement로 랜덤 할당 (20%)

MEAD angry →
  level 1-2 → refusal (거절/단호)
  level 3 → angry (화남)

MEAD sad →
  level 1 → sulk (삐침)
  level 2 → apology (사과/미안)
  level 3 → crying (울음)
  + 일부를 struggle로 랜덤 할당 (15%)

MEAD surprised →
  level 1 → shy (수줍음)
  level 2 → fluster (당황)
  level 3 → surprise (놀람)

MEAD neutral → neutral (그대로)
```

**TTS 신규 생성 계획:**

새 하위 감정(기존에 직접 대응 없는 것)용 TTS 클립 생성:
- excitement: 흥분된 톤으로 대화문 1,000개
- agreement: 차분한 동의 톤 800개
- gratitude: 감사 표현 문장 800개
- sulk: 삐친 톤 800개
- struggle: 힘들어하는 톤 800개
- fluster: 당황한 톤 800개
- shy: 조용하고 수줍은 톤 800개
- refusal: 단호한 거절 톤 600개
- 기타 보강: 각 감정 추가 400-600개

### 6.4 감정 전환 클립 생성

V3의 핵심 개선 — 자연스러운 감정 전환. 별도 전환 클립 생성:

```python
def generate_transition_clip(emotion_from, emotion_to, lam_output, features):
    """두 감정 사이의 전환을 포함하는 클립.
    예: laughter → apology (웃다가 미안해지는 상황)
    """
    T = len(lam_output)
    transition_point = T // 2
    transition_width = int(0.5 * 30)  # 500ms = 15프레임

    # 전반부: emotion_from
    target_from = generate_expression(emotion_from, ...)
    # 후반부: emotion_to
    target_to = generate_expression(emotion_to, ...)

    # 전환: VAD 공간에서 보간 (blendshape 직접 보간 금지!)
    for f in range(transition_point - transition_width,
                   transition_point + transition_width):
        t = (f - (transition_point - transition_width)) / (2 * transition_width)
        t_smooth = smoothstep(t)
        vad_interp = vad_from * (1 - t_smooth) + vad_to * t_smooth
        target[f] = vad_to_blendshapes_parametric(*vad_interp)

    # 립싱크 병합 (전환 중에도 립싱크 유지)
    for ch in LIPSYNC_CRITICAL:
        target[:, ch] = lam_output[:, ch]

    return target  # 감정 라벨: emotion_to (후반부 기준)
```

**VAD 보간이 핵심인 이유:**
```
laughter (V=0.90, A=0.85) → apology (V=0.32, A=0.48)
  중간점: (0.61, 0.67) → neutral에 가까운 약간 긴장된 표정
  → blendshape 직접 보간 시: 미소+찡그림 동시 = 부자연스러움
  → VAD 보간 시: 자연스럽게 neutral 경유
```

### 6.5 최종 디렉토리 구조

```
/dataset/mead-expression-training/
  e2f/distill/
    precomputed_features/
      M003_laughter_3_015.npy           # (T, 141) — MEAD 재라벨링
      SYNTH001_excitement_2_0042.npy    # (T, 141) — TTS 신규
      TRANS001_laughter_apology_2.npy   # (T, 141) — 전환 클립
      ...
    teacher_outputs/
      M003_laughter_3_015.npy           # (T, 52)  — 립싱크+합성표정 병합
      SYNTH001_excitement_2_0042.npy    # (T, 52)
      TRANS001_laughter_apology_2.npy   # (T, 52)
      ...
  output/
    v3_clips.csv                        # 전체 메타데이터
    lam_outputs/                        # LAM 립싱크 (기존 + 신규)
  generator/                            # ★ V3 합성 생성기 코드
    __init__.py
    config.py                           # 14개 감정 VAD 좌표, 부모 매핑, 특수 패턴 정의
    vad_delta.py                        # VAD delta → blendshape delta 매핑 함수
    special_patterns.py                 # jaw 진동, 턱 떨림, 미소/억제 진동
    validator.py                        # 통계적/분별력 검증
    generate_v3_targets.py              # ★ 메인: base + delta로 타겟 생성
    create_metadata.py                  # CSV 생성
    relabel_mead.py                     # MEAD 클립 재라벨링 + delta 적용
```

---

## 7. 품질 검증

### 7.1 통계적 검증 (자동)

```python
def validate_clip(clip, emotion):
    """합성 클립 품질 자동 검증."""
    errors = []

    # 범위 검사
    if clip.min() < -0.01 or clip.max() > 1.01:
        errors.append("범위 위반")

    # 프레임 간 매끄러움 (순간이동 방지)
    deltas = np.abs(np.diff(clip, axis=0))
    if deltas.max() > 0.20:
        errors.append(f"순간이동: 최대 delta = {deltas.max():.3f}")

    # 미소/찡그림 상호 배제 (동시 활성화 방지)
    smile = clip[:, [23, 24]].mean(axis=1)
    frown = clip[:, [25, 26]].mean(axis=1)
    if np.minimum(smile, frown).max() > 0.25:
        errors.append("미소/찡그림 동시 활성화")

    # 좌우 대칭 검사 (경멸 제외)
    for l, r in LEFT_RIGHT_PAIRS:
        if np.abs(clip[:, l] - clip[:, r]).mean() > 0.15:
            errors.append(f"비대칭: ch {l}/{r}")

    return errors
```

### 7.2 분별력 검증 (자동)

```python
# 14개 감정을 구분할 수 있는지 분류기로 테스트
# 목표: 하위 감정 간 80% 이상 구분 정확도
# 75% 미만 → 분포 차이 증가 필요
# 95% 이상 → 변이 부족 (템플릿화 위험)
```

### 7.3 시각적 검증 (사람)

- 감정당 10개 랜덤 클립을 테스트 아바타에 렌더링
- 3명 이상의 리뷰어가 인지되는 감정 라벨링
- 목표: 의도된 감정과 인지된 감정 70% 이상 일치
- **이 단계를 생략할 수 없음**

### 7.4 반복 루프

```
생성 → 통계 검증 → 분류기 검증 → 시각 리뷰
  ↑                                    ↓
  └──── 분포/타이밍 매개변수 조정 ──────┘

예상: 3-5회 반복 후 프로덕션 품질 도달
```

---

## 8. 구현 단계 (상세)

### Phase 1: 합성 표정 생성기 구축

| 단계 | 파일 | 설명 | 복잡도 |
|------|------|------|--------|
| 1.1 | `generator/config.py` | 14개 감정 VAD 좌표, 부모 매핑, 특수 패턴 정의 | 낮음 |
| 1.2 | `generator/vad_delta.py` | VAD delta → blendshape delta 매핑 함수 | 낮음 |
| 1.3 | `generator/special_patterns.py` | jaw 진동, 턱 떨림, 미소/억제 진동 | 낮음 |
| 1.4 | `generator/generate_v3_targets.py` | 메인: base + delta로 타겟 생성 | 중간 |
| 1.5 | `generator/relabel_mead.py` | MEAD 클립 재라벨링 + delta 적용 | 낮음 |
| 1.6 | `generator/create_metadata.py` | CSV 생성 | 낮음 |
| 1.7 | `generator/validator.py` | 통계/분별력 검증 | 낮음 |

### Phase 2: 데이터 생성 + 검증

| 단계 | 설명 | 소요 |
|------|------|------|
| 2.1 | 14,000-21,000 합성 클립 생성 | 1-2일 (GPU 불필요) |
| 2.2 | 통계적 검증 실행 | 반일 |
| 2.3 | 분별력 검증 (14클래스 분류기) | 반일 |
| 2.4 | 시각적 검증 (아바타 렌더링 + 리뷰) | 1-2일 |
| 2.5 | 매개변수 조정 + 재생성 (2-3회 반복) | 3-5일 |

### Phase 3: 모델 재학습

| 단계 | 설명 | 소요 |
|------|------|------|
| 3.1 | `config_e2f.py` 수정 (14 감정, 새 마스크) | 반일 |
| 3.2 | 데이터셋 클래스 수정 (합성 데이터 로더) | 1일 |
| 3.3 | V2 체크포인트에서 초기화 + 학습 | 2-3일 (GPU) |
| 3.4 | 검증 + 하이퍼파라미터 조정 | 2-3일 |
| 3.5 | ONNX 내보내기 (INT8 양자화) | 반일 |
| 3.6 | 스트리밍 품질 검증 (청크 크기별) | 1일 |

### Phase 4: 통합 + 배포

| 단계 | 설명 | 소요 |
|------|------|------|
| 4.1 | lipsync-wasm-se 업데이트 (14 감정 지원) | 1일 |
| 4.2 | AnimaSync 가이드 페이지 UI 업데이트 | 2일 |
| 4.3 | LLM 감정 태그 파서 구현 | 1일 |
| 4.4 | E2E 테스트 | 1일 |

---

## 9. VAD 연구의 역할 정리

기존 VAD 연구 문서 3건은 **폐기가 아닌 재활용**:

| VAD 연구 내용 | V3에서의 역할 |
|--------------|-------------|
| 27개 감정 VAD 좌표 | 합성 생성기의 14개 감정 좌표 기반 |
| 감정별 blendshape 패턴 + 범위 | 합성 생성기의 분포 매개변수 직접 사용 |
| 시간적 역학 (onset/offset) | 합성 생성기의 타이밍 매개변수 |
| RBF 매핑 알고리즘 | 감정 전환 클립의 VAD 보간에 사용 |
| 60/40 하이브리드 아키텍처 | 사용하지 않음 (모델이 직접 학습) |
| JS overlay 코드 | 사용하지 않음 (모델이 직접 출력) |

---

## 10. 리스크 및 대응

| 리스크 | 확률 | 영향 | 대응 |
|--------|------|------|------|
| 합성 데이터가 템플릿처럼 될 위험 | 중간 | 높음 | 3층 변이 + 분별력 검증 82-90% 목표 |
| hand-crafted bias 학습 | 중간 | 중간 | MEAD 실제 데이터 혼합으로 현실감 보존 |
| 하위 감정 간 구분 불가 | 낮음 | 높음 | 필수 구분 채널 분포를 tight하게 설정 |
| 립싱크 품질 저하 | 낮음 | 매우 높음 | V2 체크포인트 초기화 + MEAD 혼합 학습 |
| 학습 불안정 | 낮음 | 중간 | V2 가중치 warm-start + 낮은 학습률 |

---

## 11. V2 vs V3 최종 비교

| 항목 | V2 | V3 |
|------|----|----|
| 감정 수 | 5 | 16 (5 base + 11 sub) |
| 감정 입력 | 5차원 one-hot | 16차원 + 강도 |
| 학습 데이터 | MEAD 실제 3,685 클립 | MEAD 3,685 + 합성 14,000-21,000 |
| 감정 전환 | 즉시 (JS에서 처리) | 자연스러움 (전환 클립으로 학습) |
| 표현 다양성 | 낮음 (regression-to-mean) | 높음 (분포 기반 합성) |
| 입 감정 표현 | 제한적 (7채널 gated) | 확장 (29개 표정 채널) |
| 모델 크기 | 5.35MB (INT8) | ~5.4MB (INT8, 거의 동일) |
| 아키텍처 변경 | — | FiLM 레이어만 (5→14차원) |
| 스트리밍 | 지원 | 지원 (동일 구조) |
| LLM 연동 | 수동 슬라이더 | {JOY:excitement:30} 태그 직접 파싱 |

---

## 12. 상태 추적

| 단계 | 상태 | 비고 |
|------|------|------|
| 1.1 감정 프로파일 정의 | 미시작 | |
| 1.2 시간적 엔벨로프 | 미시작 | |
| 1.3 Blendshape 샘플러 | 미시작 | |
| 1.4 오디오 변조기 | 미시작 | |
| 1.5 변이 시스템 | 미시작 | |
| 1.6 전환 클립 생성 | 미시작 | |
| 1.7 품질 검증기 | 미시작 | |
| 1.8 데이터셋 생성 오케스트레이터 | 미시작 | |
| 2.1 합성 데이터 생성 | 미시작 | |
| 2.2-2.5 검증 반복 | 미시작 | |
| 3.1-3.6 모델 재학습 | 미시작 | |
| 4.1-4.4 통합 배포 | 미시작 | |

---

## 부록 A: 기존 V3 계획과의 관계

기존 `V3_IMPLEMENTATION_PLAN.md`는 "JS overlay on frozen V2" 접근이었습니다.
이 문서(`V3_IMPLEMENTATION_PLAN_v2.md`)는 "모델 재학습 with synthetic data" 접근으로 대체합니다.

기존 계획의 9단계 (감정 사전, VAD 매퍼, 타임라인, 강도, 컨트롤러, overlay, 데이터, UI, 스케줄러) 중:
- **1, 7**: 연구 데이터 → 합성 생성기 매개변수로 흡수
- **2, 3, 4, 6**: 모델이 직접 학습하므로 불필요
- **5**: V3 컨트롤러 → LLM 태그 파서로 단순화
- **8**: UI 업데이트 → Phase 4에 포함
- **9**: 감정 스케줄러 → Phase 4에 포함

## 부록 B: 합성 데이터 파일 구조 (예상)

```
synthetic_data/
  emotions/
    laughter/
      clip_0001.npz    # {blendshapes: (T,52), intensity: float, speaker_style: (52,)}
      clip_0002.npz
      ...
    excitement/
      ...
    (14개 폴더)
  transitions/
    laughter_to_apology/
      clip_0001.npz    # {blendshapes: (T,52), emotions: [str,str], transition_frame: int}
      ...
  metadata.csv          # 전체 클립 인덱스
  validation_report.json
```
