# Korean Emotion Text Dataset — 상세 제작 계획

> 최종 업데이트: 2026-04-10
> 목적: MicroALBERT 학습용 한국어 감정 **대화** 데이터셋 제작
> 동일 텍스트를 TTS → 립싱크 모델 학습에도 재사용

---

## 1. 접근 방식

### Multi-turn 대화 기반 + "문장 먼저, VAD 나중"

**데이터 단위:** 개별 문장이 아닌 **대화 시나리오** (3-6턴).
감정은 대화 맥락 속에서 결정되므로, 모델도 맥락을 보고 판단해야 함.

**MicroALBERT Input 구조:**
```
[CLS] 이전턴1 [SEP] 이전턴2 [SEP] ... [SEP] 현재발화 [SEP]
→ 현재 발화의 감정을 판단 (max 512 tokens, 최근 5-8턴)
```

**데이터 제작 순서:**
1. 대화 시나리오를 **감정 카테고리만 보고 자연스럽게 작성** (VAD 숫자 신경 안 씀)
2. 작성 후 각 턴의 감정을 읽고 **intensity level (1-5)** 매기기 → VAD 앵커 테이블로 자동 변환
3. 3D 시각화로 분포 확인
4. 빈 영역만 **VAD-aware로 추가 작성** (전체의 ~20-30%)

**왜 이 순서인가:**
- 자연스러운 대화가 나옴 (숫자에 맞추려 하지 않음)
- 맥락에 따라 달라지는 애매한 감정이 자연 발생 ("응 괜찮아" = sulk)
- intensity level 5단계만 판단하면 돼서 라벨링이 빠름
- 한 대화에서 training example 여러 개 추출 → 효율적

**모델 변경 사항 (vs single-turn):**
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| Max tokens | 128 | **512** |
| 추론 속도 | ~15ms | **~35-40ms** |
| 모델 크기 | ~6.0MB | **~6.1MB** (+50KB position embedding) |
| Input | 문장 1개 | 최근 N턴 concat |

모델 자체는 동일. Position embedding만 128→512로 증가.

---

## 2. 데이터셋 규모

### 한국어 단독 — 목표: ~10,000 training examples (대화 + 독립문장 혼합)

| 항목 | 수량 |
|---|---|
| 감정 카테고리 | 16 |
| 강도 레벨 (per emotion) | 5 |
| **대화 시나리오 (수동)** | **~400** |
| 평균 턴 수 per 시나리오 | 4-5턴 |
| 대화 기반 training examples (시나리오당 ~4개) | **~1,600** |
| **독립 문장 (수동, context="")** | **~800** |
| 수동 seed 총계 | **~2,400** |
| LLM augment (×4) | ~9,600 |
| QC 전 총계 | ~12,000 |
| 예상 탈락률 (~20%) | -2,000 |
| **최종 clean 데이터** | **~10,000** |

### 대화 vs 독립문장 비율

| 유형 | 비율 | 용도 | context 필드 |
|---|---|---|---|
| 대화 맥락 포함 | **60-70%** | 맥락 의존 감정 학습 (같은 문장, 다른 맥락 → 다른 감정) | 이전 턴 concat |
| 독립 문장 | **30-40%** | 맥락 없이 판단하는 fallback 학습 | `""` (빈 문자열) |

독립 문장 유형: 혼잣말, 독백, 일기, SNS 게시글, 첫 발화 등 맥락이 없는 게 자연스러운 케이스.

이렇게 섞으면 모델이 두 가지 모드를 배움:
- Context 있을 때 → 활용해서 더 정확한 판단
- Context 없을 때 → 문장만 보고 판단 (fallback)

### 대화 시나리오에서 training example 추출 방식

한 대화에서 여러 example이 나옴:
```
시나리오 (5턴):
  A: "오늘 시험 봤어"
  B: "어떻게 됐어?"
  A: "떨어졌어..."
  B: "아 진짜? 괜찮아?"
  A: "응 괜찮아"

추출:
  Example 1: [턴1] [SEP] [턴2]                           → 턴2 감정: neutral
  Example 2: [턴1] [SEP] [턴2] [SEP] [턴3]               → 턴3 감정: sadness
  Example 3: [턴1] [SEP] [턴2] [SEP] [턴3] [SEP] [턴4]   → 턴4 감정: surprise
  Example 4: [턴1] [SEP] ... [SEP] [턴5]                  → 턴5 감정: sulk (강한 척)
```

### 독립 문장 예시

```json
{
  "id": "solo_joy_023",
  "context": "",
  "text": "오늘 날씨 진짜 좋다",
  "emotion": "joy",
  "intensity": 2,
  ...
}
```

### 왜 이 규모인가

- GoEmotions: 27 emotions에 58K → 감정당 ~2K. 우리는 16 emotions에 10K → 감정당 ~625. Distillation이라 학습 데이터 요구량이 낮음
- 5.5M 파라미터 모델은 overfit하기 쉬워서 오히려 적은 데이터가 맞음
- 대화 시나리오 400개 + 독립 문장 800개는 작업 시간 ~70시간으로 동일
- TTS 변환 시 ~10K 문장 × 3초 = ~8.3시간 오디오. 실용적 상한
- 부족하면 teacher pseudo-labeling으로 추후 증강 가능

### 감정별 배분 (QC 후 목표)

혼동하기 쉬운 감정에 약간 더 배분:

| 감정 | 목표 | 이유 |
|---|---|---|
| neutral | 700 | 정의가 모호해서 다양성 필요 |
| joy | 625 | 명확 |
| laughter | 625 | 명확 |
| excitement | 650 | joy와 겹침 주의 |
| agreement | 625 | 명확 |
| gratitude | 625 | 명확 |
| sadness | 650 | crying과 겹침 주의 |
| crying | 625 | 명확 |
| **sulk** | **700** | 한국 문화 특수, 어려움 |
| apology | 625 | 명확 |
| struggle | 650 | sadness와 겹침 주의 |
| anger | 650 | refusal과 겹침 주의 |
| refusal | 625 | 명확 |
| surprise | 625 | 명확 |
| fluster | 650 | surprise/shy와 겹침 주의 |
| shy | 650 | fluster와 겹침 주의 |
| **합계** | **~10,325** | |

---

## 3. 16 감정 × 5 강도 VAD 앵커

### VAD 범위 규칙

각 감정은 default VAD 좌표를 중심으로 **유효 범위** 내에서 variation.
범위를 벗어나면 다른 감정으로 넘어감.

**핵심 제약:**
- joy ↔ laughter: **Arousal 경계 A=0.55** 기준으로 분리. joy는 A ≤ 0.55, laughter는 A ≥ 0.55
- sadness ↔ crying: **Dominance 경계 D=-0.40** 기준. sadness는 D ≥ -0.40, crying은 D ≤ -0.40
- fluster ↔ shy: **Arousal 경계 A=0.45** 기준. fluster는 A ≥ 0.45, shy는 A ≤ 0.45

### 전체 VAD 앵커 테이블

> VAD 범위: [-1.0, +1.0]

#### neutral

| Level | V | A | D | 설명 | 예시 톤 |
|---|---|---|---|---|---|
| 1 | 0.00 | -0.10 | 0.00 | 무미건조 | "네" |
| 2 | 0.05 | -0.05 | 0.05 | 담담 | "알겠습니다" |
| 3 | 0.00 | 0.00 | 0.00 | 순수 중립 | "그렇군요" |
| 4 | -0.05 | 0.05 | -0.05 | 약간 무심 | "아 그래요" |
| 5 | 0.10 | 0.10 | 0.10 | 가벼운 긍정 | "네 좋아요" |

#### joy (기쁨)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.50 | 0.15 | 0.20 | 잔잔한 기쁨 |
| 2 | 0.60 | 0.25 | 0.25 | 가벼운 기쁨 |
| 3 | 0.76 | 0.48 | 0.35 | 기본 기쁨 (default) |
| 4 | 0.85 | 0.50 | 0.40 | 상당한 기쁨 |
| 5 | 0.90 | 0.55 | 0.45 | 큰 기쁨 (A ≤ 0.55, 그 위는 laughter) |

#### laughter (웃음)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.65 | 0.55 | 0.20 | 가벼운 웃음 (A ≥ 0.55) |
| 2 | 0.72 | 0.60 | 0.25 | 킥킥 |
| 3 | 0.82 | 0.65 | 0.30 | 기본 웃음 (default) |
| 4 | 0.85 | 0.75 | 0.30 | 크게 웃음 |
| 5 | 0.90 | 0.90 | 0.35 | 폭소 |

#### excitement (흥분)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.40 | 0.55 | 0.30 | 살짝 들뜸 |
| 2 | 0.50 | 0.65 | 0.35 | 기대됨 |
| 3 | 0.62 | 0.75 | 0.46 | 기본 흥분 (default) |
| 4 | 0.70 | 0.85 | 0.50 | 매우 들뜸 |
| 5 | 0.75 | 0.95 | 0.55 | 극도 흥분 |

#### agreement (동의)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.20 | 0.05 | 0.20 | 형식적 동의 |
| 2 | 0.30 | 0.10 | 0.25 | 가벼운 동의 |
| 3 | 0.40 | 0.20 | 0.35 | 기본 동의 (default) |
| 4 | 0.50 | 0.30 | 0.40 | 적극 동의 |
| 5 | 0.60 | 0.40 | 0.50 | 강한 동의 |

#### gratitude (감사)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.50 | 0.05 | -0.10 | 형식적 감사 |
| 2 | 0.60 | 0.10 | 0.00 | 가벼운 감사 |
| 3 | 0.70 | 0.20 | 0.10 | 기본 감사 (default) |
| 4 | 0.80 | 0.40 | 0.15 | 깊은 감사 |
| 5 | 0.90 | 0.50 | 0.20 | 감격 수준 감사 (D ≤ 0.30 유지) |

#### sadness (슬픔)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | -0.30 | -0.10 | -0.10 | 약간 우울 |
| 2 | -0.45 | -0.15 | -0.20 | 속상함 |
| 3 | -0.63 | -0.27 | -0.33 | 기본 슬픔 (default) |
| 4 | -0.75 | -0.35 | -0.35 | 깊은 슬픔 |
| 5 | -0.85 | -0.40 | -0.40 | 절망적 슬픔 (D ≥ -0.40, 그 아래는 crying) |

#### crying (울음)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | -0.55 | -0.15 | -0.42 | 눈물 글썽 (D ≤ -0.40) |
| 2 | -0.65 | -0.10 | -0.45 | 조용히 울음 |
| 3 | -0.75 | -0.10 | -0.50 | 기본 울음 (default) |
| 4 | -0.82 | 0.05 | -0.55 | 서럽게 울음 |
| 5 | -0.90 | 0.20 | -0.65 | 오열 (A 살짝 상승 — 격한 울음) |

#### sulk (삐침)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | -0.10 | -0.10 | -0.05 | 애교 삐침 (거의 장난) |
| 2 | -0.20 | -0.15 | -0.10 | 가벼운 토라짐 |
| 3 | -0.30 | -0.20 | -0.20 | 기본 삐침 (default) |
| 4 | -0.40 | -0.15 | -0.30 | 진심 삐침 |
| 5 | -0.50 | -0.10 | -0.40 | 냉전 수준 |

#### apology (사과)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | -0.10 | -0.05 | -0.30 | 형식적 사과 |
| 2 | -0.20 | -0.10 | -0.45 | 가벼운 사과 |
| 3 | -0.30 | -0.20 | -0.60 | 기본 사과 (default) |
| 4 | -0.40 | -0.10 | -0.70 | 진심 사과 |
| 5 | -0.50 | 0.10 | -0.80 | 간절한 사과 (A 약간 상승) |

#### struggle (고민)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | -0.25 | 0.00 | -0.15 | 살짝 고민 |
| 2 | -0.35 | 0.05 | -0.25 | 갈등 |
| 3 | -0.50 | 0.10 | -0.40 | 기본 고민 (default) |
| 4 | -0.60 | 0.20 | -0.50 | 깊은 고민 |
| 5 | -0.70 | 0.35 | -0.60 | 극심한 갈등 (A 상승 — 머리 쥐어뜯는 수준) |

#### anger (분노)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | -0.25 | 0.30 | 0.15 | 짜증 |
| 2 | -0.35 | 0.45 | 0.20 | 불쾌 |
| 3 | -0.51 | 0.59 | 0.25 | 기본 분노 (default) |
| 4 | -0.65 | 0.75 | 0.40 | 격분 |
| 5 | -0.80 | 0.90 | 0.55 | 폭발 |

#### refusal (거절)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | -0.15 | 0.25 | 0.30 | 완곡한 거절 |
| 2 | -0.25 | 0.35 | 0.40 | 단호한 거절 |
| 3 | -0.40 | 0.50 | 0.50 | 기본 거절 (default) |
| 4 | -0.50 | 0.60 | 0.60 | 강한 거절 |
| 5 | -0.60 | 0.70 | 0.70 | 격렬한 거부 |

#### surprise (놀람)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.20 | 0.35 | 0.00 | 살짝 놀람 |
| 2 | 0.30 | 0.50 | -0.03 | 놀람 |
| 3 | 0.40 | 0.67 | -0.07 | 기본 놀람 (default) |
| 4 | 0.45 | 0.80 | -0.10 | 크게 놀람 |
| 5 | 0.50 | 0.95 | -0.15 | 경악 |

#### fluster (당황)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.05 | 0.45 | -0.20 | 살짝 당황 (A ≥ 0.45) |
| 2 | 0.08 | 0.55 | -0.30 | 당황 |
| 3 | 0.10 | 0.70 | -0.40 | 기본 당황 (default) |
| 4 | 0.05 | 0.80 | -0.50 | 크게 당황 |
| 5 | 0.00 | 0.90 | -0.60 | 패닉 |

#### shy (수줍음)

| Level | V | A | D | 설명 |
|---|---|---|---|---|
| 1 | 0.15 | 0.05 | -0.10 | 살짝 쑥스러움 |
| 2 | 0.20 | 0.10 | -0.20 | 부끄러움 |
| 3 | 0.30 | 0.20 | -0.30 | 기본 수줍음 (default) |
| 4 | 0.35 | 0.30 | -0.40 | 많이 수줍음 |
| 5 | 0.40 | 0.45 | -0.50 | 극도로 부끄러움 (A ≤ 0.45, 그 위는 fluster) |

---

## 4. 문장 작성 가이드라인

### 4.1 말투 분포 (seed 30개당)

| 말투 | 수량 | 비율 |
|---|---|---|
| 반말 (casual) | 10 | 33% |
| 비격식 존댓말 (-요체) | 10 | 33% |
| 격식체 (-습니다체) | 5 | 17% |
| 혼잣말/독백 | 3 | 10% |
| 채팅/SNS체 | 2 | 7% |

### 4.2 맥락 분포

| 맥락 | 비율 | 예시 상황 |
|---|---|---|
| 일상대화 | 30% | 친구, 가족, 연인 |
| 직장/업무 | 20% | 동료, 상사 |
| 고객응대 | 10% | 콜센터, 매장 |
| SNS/채팅 | 15% | 카톡, 댓글 |
| 독백/일기 | 10% | 혼잣말 |
| 방송/콘텐츠 | 10% | 유튜브, 라이브 |
| 기타 | 5% | 게임, 교육 |

### 4.3 문장 길이 분포

| 유형 | 글자 수 | 비율 | 예시 |
|---|---|---|---|
| 감탄사 | 1-5 | 10% | "헐", "와", "아..." |
| 초단문 | 6-15 | 20% | "진짜 화난다" |
| 단문 | 16-30 | 30% | "오늘 하루 정말 힘들었어" |
| 중문 | 31-60 | 25% | "네가 그렇게 말해줘서 정말 감동받았어, 고마워" |
| 장문 | 61-100 | 15% | 복합 문장 |

**100자 초과 금지** — TTS 품질 저하 및 실제 대화 비현실적.

### 4.4 이모티콘 사용

- SNS/채팅 맥락 문장 (15%) 에서만 사용
- 나머지 맥락에서는 사용 금지

감정별 대표 이모티콘:

| 감정 | 이모티콘 |
|---|---|
| joy/laughter | ㅋㅋ, ㅎㅎ, ^^ |
| sadness/crying | ㅠㅠ, ㅜㅜ, ... |
| anger | ㅡㅡ, ;; |
| sulk | ㅠ, 흥 |
| surprise | ?!, 헐 |
| shy | ㅎ, ///, ㅎㅎ; |

### 4.5 한국어 특수 감정 작성 팁

#### sulk (삐침) — 가장 한국적인 감정

핵심 특징:
- 분노와 다름: arousal이 낮음 (조용한 불만)
- 슬픔과 다름: 관계적 (특정 대상에게 향함)
- 토라짐/냉전/무시 패턴

대표 표현:
- "흥 알아서 해", "됐어 말 안 해", "나 몰라"
- "왜 나한테만 그래", "맨날 그러면서"
- 짧고 끊어지는 말투가 특징

Level 1 (애교 삐침) vs Level 5 (진심 냉전) 차이:
```
L1: "에이~ 왜 그래~" (장난, V≈-0.10)
L5: "..." (무반응, 진심 화남에 가까움, V≈-0.50)
```

#### struggle (고민)

핵심 특징:
- 슬픔보다 active (뭔가 해결하려고 시도 중)
- Arousal이 약간 양수 (고민은 에너지를 씀)

대표 표현:
- "이걸 어떡하지...", "도저히 답이 안 보여"
- "하... 모르겠다", "진짜 어렵다 이거"

---

## 5. 데이터 포맷

### 5.1 대화 시나리오 파일 (scenarios.jsonl)

대화 시나리오 원본 저장:

```json
{
  "scenario_id": "daily_friend_003",
  "setting": "친구 간 일상 대화",
  "turns": [
    {"speaker": "A", "text": "나 오늘 진짜 좋은 일 있었어!"},
    {"speaker": "B", "text": "와 진짜? 뭔데뭔데?"},
    {"speaker": "A", "text": "승진했어!!"},
    {"speaker": "B", "text": "대박 축하해!! 진짜 고생했잖아"},
    {"speaker": "A", "text": "고마워 ㅠㅠ 너 덕분이야"}
  ],
  "emotion_labels": [
    {"turn": 0, "emotion": "excitement", "intensity": 4},
    {"turn": 1, "emotion": "surprise",   "intensity": 3},
    {"turn": 2, "emotion": "excitement", "intensity": 5},
    {"turn": 3, "emotion": "joy",        "intensity": 4},
    {"turn": 4, "emotion": "gratitude",  "intensity": 4}
  ],
  "meta": {
    "style": "반말",
    "context": "일상대화",
    "source": "manual"
  }
}
```

### 5.2 Training example 파일 (train.jsonl)

시나리오에서 추출한 학습 데이터:

```json
{
  "id": "daily_friend_003_t4",
  "context": "나 오늘 진짜 좋은 일 있었어! [SEP] 와 진짜? 뭔데뭔데? [SEP] 승진했어!! [SEP] 대박 축하해!! 진짜 고생했잖아",
  "text": "고마워 ㅠㅠ 너 덕분이야",
  "emotion": "gratitude",
  "emotion_ko": "감사",
  "intensity": 4,
  "valence": 0.80,
  "arousal": 0.40,
  "dominance": 0.15,
  "meta": {
    "scenario_id": "daily_friend_003",
    "turn_index": 4,
    "style": "반말",
    "context_type": "일상대화",
    "source": "manual",
    "augmented_from": null
  }
}
```

context가 빈 경우 (첫 턴 또는 독립 발화):
```json
{
  "id": "mono_001",
  "context": "",
  "text": "오늘 날씨 진짜 좋다",
  "emotion": "joy",
  ...
}
```

### 5.3 동반 파일

| 파일 | 내용 |
|---|---|
| `emotion_vad_anchors.json` | 16 × 5 VAD 앵커 정의 (이 문서의 Section 3) |
| `emotion_labels.json` | 16개 감정 enum (id, name, name_ko, parent) |
| `dataset_stats.json` | 자동 생성 — 감정×강도별 문장 수 통계 |

---

## 6. LLM Augmentation 전략

### 6.1 비율

| 단계 | 시나리오/example 수 | 누적 examples |
|---|---|---|
| 수동 시나리오 (600 × ~4 examples) | ~2,400 | 2,400 |
| LLM Round 1 (시나리오 변형, ×2) | ~4,800 | 7,200 |
| LLM Round 2 (말투/맥락 전환, ×2) | ~4,800 | 12,000 |
| QC 탈락 (~20%) | -2,000 | ~10,000 |

**수동:생성 비율 = 1:4**. 1:5 이상이면 한국어 품질 저하 (번역체, 부자연스러운 존대 혼용).

### 6.2 Augmentation 프롬프트

**Round 1: 대화 시나리오 변형 (같은 감정 흐름, 다른 표현)**

```
당신은 한국어 감정 대화 데이터셋을 만드는 전문가입니다.

아래 대화와 동일한 감정 흐름과 강도를 유지하면서,
다른 상황/어휘로 자연스러운 한국어 대화 2개를 생성하세요.

원본 대화:
{{turns_formatted}}

감정 흐름: {{emotion_flow}}
말투: {{style}}

규칙:
1. 각 턴의 감정과 강도를 동일하게 유지
2. 상황/소재를 바꾸되 감정 흐름은 동일
3. 원본 표현의 50% 이상 교체
4. 번역체 금지, 실제 한국어 화자의 대화처럼
5. 턴 수는 원본과 동일

출력: [{"turns": [...], "setting": "..."}, ...]
```

**Round 2: 말투/맥락 전환**

```
아래 대화의 감정 흐름을 유지하면서,
말투를 '{{target_style}}'로, 맥락을 '{{target_context}}'로 바꿔
대화 2개를 생성하세요.

원본 대화 ({{source_style}}, {{source_context}}):
{{turns_formatted}}

규칙:
1. 감정 종류와 강도 동일 유지
2. {{target_style}} 말투와 {{target_context}} 맥락 자연스럽게 반영
3. 한국어 화자가 실제로 할 법한 대화만

출력: [{"turns": [...], "style": "{{target_style}}", "context": "{{target_context}}"}, ...]
```

### 6.3 품질 검증 파이프라인

**자동 필터:**
1. 중복 제거: 기존 문장과 Jaccard 유사도 > 0.7 → reject
2. 길이 검증: seed 대비 ±50% 초과 → flag
3. 언어 검증: 비한국어 문자 (허용 이모티콘 제외) → reject
4. Perplexity 검증: 한국어 LM 기준 2σ 초과 → flag

**수동 검수:**
- Flag된 문장: 100% 검수
- 나머지: 20% 랜덤 샘플 검수
- 통과율 85% 미만 시 해당 배치 재생성

---

## 7. 3D VAD 분포 시각화

데이터셋 제작 후 VAD 분포를 3D scatter plot으로 시각화하여 검증.

### 시각화 스크립트 (Python)

```python
import json
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd

# 데이터 로드
data = []
with open("korean_emotion_dataset.jsonl") as f:
    for line in f:
        data.append(json.loads(line))

df = pd.DataFrame(data)

# 3D scatter plot
fig = px.scatter_3d(
    df,
    x='valence', y='arousal', z='dominance',
    color='emotion',
    symbol='emotion',
    opacity=0.7,
    title='VAD 3D Distribution — Korean Emotion Dataset',
    labels={'valence': 'Valence (긍정↔부정)',
            'arousal': 'Arousal (흥분↔차분)',
            'dominance': 'Dominance (통제↔무력)'},
    hover_data=['text', 'intensity']
)

# 축 범위 고정
fig.update_layout(
    scene=dict(
        xaxis=dict(range=[-1, 1], title='Valence'),
        yaxis=dict(range=[-1, 1], title='Arousal'),
        zaxis=dict(range=[-1, 1], title='Dominance'),
    ),
    width=1000, height=800
)

fig.write_html("vad_distribution_3d.html")  # 브라우저에서 회전/줌 가능
print(f"Total: {len(df)} sentences, {df['emotion'].nunique()} emotions")

# 감정별 분포 통계
print("\n=== Per-emotion stats ===")
for emotion in df['emotion'].unique():
    sub = df[df['emotion'] == emotion]
    print(f"{emotion:12s}: n={len(sub):4d}  "
          f"V=[{sub['valence'].min():.2f}, {sub['valence'].max():.2f}]  "
          f"A=[{sub['arousal'].min():.2f}, {sub['arousal'].max():.2f}]  "
          f"D=[{sub['dominance'].min():.2f}, {sub['dominance'].max():.2f}]")

# Dead zone 체크: VAD 공간을 grid로 나눠서 비어있는 구간 확인
print("\n=== Coverage check (4x4x4 grid) ===")
for v_bin in np.linspace(-1, 1, 5)[:-1]:
    for a_bin in np.linspace(-1, 1, 5)[:-1]:
        for d_bin in np.linspace(-1, 1, 5)[:-1]:
            mask = ((df['valence'] >= v_bin) & (df['valence'] < v_bin + 0.5) &
                    (df['arousal'] >= a_bin) & (df['arousal'] < a_bin + 0.5) &
                    (df['dominance'] >= d_bin) & (df['dominance'] < d_bin + 0.5))
            count = mask.sum()
            if count == 0:
                print(f"  EMPTY: V=[{v_bin:.1f},{v_bin+0.5:.1f}) "
                      f"A=[{a_bin:.1f},{a_bin+0.5:.1f}) "
                      f"D=[{d_bin:.1f},{d_bin+0.5:.1f})")
```

### 시각화에서 확인할 것

1. **클러스터가 16개 보이는가?** — 감정별로 구분되는 덩어리
2. **겹치는 영역은 어디인가?** — joy/laughter, sadness/crying, fluster/shy 경계
3. **빈 영역은 어디인가?** — Dead zone = 자연스러운 감정이 없는 곳 (예: V=+0.8, A=+0.9, D=-0.9 → 기쁘고 흥분되면서 무력한? → 비현실적)
4. **분포가 구형인가 타원형인가?** — 각 감정 클러스터의 모양
5. **강도 레벨이 gradient처럼 보이는가?** — 같은 색 점들이 center에서 edge로 퍼지는 패턴

---

## 8. 실행 일정

### Week 1: 기반 작업 + 시나리오 작성 시작 (Day 1-3)

**Day 1: 기반 파일 생성 (4-5시간)**
- `emotion_labels.json` 생성
- `emotion_vad_anchors.json` 생성 (이 문서의 Section 3 기반)
- 대화 시나리오 2-3개 calibration 예시 작성
- 데이터 포맷/스크립트 세팅 (scenarios.jsonl → train.jsonl 추출 스크립트)

**Day 2-3: Batch 1 작성 (~16시간)**
- 대화 시나리오 ~200개 (명확한 감정: joy, gratitude, sadness, anger 중심)
- 독립 문장 ~400개 (혼잣말, 독백, SNS, 첫 발화)
- 감정 카테고리만 보고 자연스럽게 작성, VAD 숫자 안 봄
- 작성 후 감정 + intensity level (1-5) 라벨링
- 속도: 시나리오 ~20개/시간, 독립 문장 ~75개/시간

### Week 2: 완성 + Augmentation (Day 4-7)

**Day 4-5: Batch 2 작성 (~20시간)**
- 대화 시나리오 ~200개 (어려운 감정: sulk, apology, struggle, fluster, shy, refusal 중심)
- 독립 문장 ~400개
- 문화 특수 감정 (sulk, struggle, shy) 포함 시나리오 의도적으로 작성

**Day 6: 분포 확인 + Gap filling (~4시간)**
- scenarios.jsonl → train.jsonl 추출
- intensity level → VAD 앵커 자동 변환
- 3D 시각화로 분포 확인
- 빈 영역 파악 → 추가 시나리오 작성 (이때만 VAD-aware)

**Day 7: LLM Augmentation (~8시간)**
- Round 1 (시나리오 변형) + Round 2 (말투/맥락 전환) 실행
- 자동 QC 필터 적용
- 총 ~12,000 training examples 생성

### Week 3: QC + 마무리 (Day 8-10)

**Day 8-9: 검수 (~12시간)**
- Flag된 시나리오 수동 검수
- 랜덤 샘플 20% 검수
- 부족 감정×강도 조합 추가 시나리오 작성 + augment
- 최종 balance 확인 (감정×강도별 ≥100 examples)

**Day 10: 시각화 + 마무리 (~4시간)**
- 최종 3D VAD 분포 시각화
- Dead zone 확인 및 보완
- 최종 데이터셋 export (train.jsonl + scenarios.jsonl)
- `dataset_stats.json` 생성
- Edge-TTS 오디오 생성 시작

### 총 예상 소요: ~70시간 (10 working days)

---

## 9. 혼동 쌍 경계 문장

아래 감정 쌍은 VAD 공간에서 가까워서 **경계 문장을 추가로 작성**해야 합니다.
각 쌍마다 50개씩 경계 사례를 추가 (seed에 포함):

| 혼동 쌍 | VAD 거리 | 구분 기준 |
|---|---|---|
| joy ↔ laughter | 0.19 | Arousal 경계 A=0.55 |
| sadness ↔ crying | 0.27 | Dominance 경계 D=-0.40 |
| fluster ↔ shy | 0.58 | Arousal 경계 A=0.45 |
| anger ↔ refusal | 0.30 | Dominance (anger < refusal) |
| sadness ↔ struggle | 0.22 | Arousal (struggle > sadness) |
| sulk ↔ sadness | 0.35 | 관계적 vs 내면적 |
| excitement ↔ laughter | 0.25 | Valence (laughter > excitement) |
| apology ↔ sadness | 0.40 | Dominance (apology ≪ sadness) |

---

## 10. Inference 시 사용 방법

```javascript
class EmotionDetector {
    #history = [];
    
    detect(currentText) {
        this.#history.push(currentText);
        
        // 최근 N턴을 512 토큰 안에 맞게 concat
        const turns = this.#history.slice(-6);
        const input = turns.join(" [SEP] ");
        const tokenized = this.tokenizer.encode(input, { maxLength: 512 });
        
        const { cls_probs, vad } = this.model.run(tokenized);
        return { cls_probs, vad };  // → 19-dim concat for lipsync FiLM
    }
    
    resetConversation() {
        this.#history = [];  // 새 대화 시작 시
    }
}
```

- 모델 자체는 stateless — 매번 concat된 텍스트를 통째로 입력
- History 관리는 JS에서 array로
- 새 대화 시작 시 `resetConversation()`
- 512 토큰 안에 최근 5-8턴 들어감 (한국어 기준)

---

## 11. 체크리스트

- [ ] `emotion_labels.json` 생성
- [ ] `emotion_vad_anchors.json` 생성
- [ ] 대화 시나리오 Batch 1 (~300 시나리오, 명확한 감정 조합)
- [ ] 대화 시나리오 Batch 2 (~300 시나리오, 어려운 감정 조합)
- [ ] 각 턴에 감정 + intensity level 라벨링
- [ ] scenarios.jsonl → train.jsonl 추출 스크립트
- [ ] 3D 시각화 + 분포 확인 + gap filling
- [ ] LLM Augmentation Round 1 (시나리오 변형)
- [ ] LLM Augmentation Round 2 (말투/맥락 전환)
- [ ] 자동 QC 필터 실행
- [ ] 수동 검수 (flag + 20% 랜덤)
- [ ] Balance 확인 (감정×강도별 ≥100 approved)
- [ ] 최종 3D VAD 시각화 + dead zone 확인
- [ ] 최종 데이터셋 export
- [ ] Edge-TTS로 오디오 생성 시작
