File size: 5,869 Bytes
7d46a91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c9c13f
 
 
 
 
 
7d46a91
 
 
1c9c13f
 
7d46a91
 
1c9c13f
 
 
7d46a91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c9c13f
7d46a91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# 데λͺ¨ API 계약 β€” μ›Ή 리더 ↔ μ—”μ§„ (M6)

> 원칙: **μˆ¨μ€ μƒνƒœ(trust/doubt/쇠약도)λŠ” μ–΄λ–€ 응닡에도 μ‹£μ§€ μ•ŠλŠ”λ‹€** (보이지 μ•ŠλŠ” κ²Œμž„).
> μ„Έμ…˜ μ˜μ†ν™”: μ„œμ‚¬ μœ„μΉ˜ + μˆ¨μ€ μƒνƒœλŠ” SQLite(`data/sessions.db`)에 λ§€ λ³€κ²½ μ¦‰μ‹œ μ €μž₯.
> μ„œλ²„ μž¬μ‹œμž‘ 후에도 `GET /api/session/{sid}`둜 λ³΅μ›λœλ‹€. **포켓(μ±„νŒ…) 쀑간 μƒνƒœλŠ” λΉ„μ˜μ†** β€”
> 볡원 μ‹œ 포켓은 λ‹«νžŒ μƒνƒœλ‘œ μ‹œμž‘ν•˜κ³ , μ§„ν–‰ μ€‘μ΄λ˜ λŒ€ν™”μ˜ μ‹ λ’°μΆ• 증뢄은 μœ μ‹€λœλ‹€ (승인된 νŠΈλ ˆμ΄λ“œμ˜€ν”„).
> 단일 ν”„λ‘œμ„ΈμŠ€ μ „μ œ(인메λͺ¨λ¦¬ μΊμ‹œ + ADK 인메λͺ¨λ¦¬ μ„Έμ…˜)λŠ” μœ μ§€.

## 곡톡 νƒ€μž…

```jsonc
// Step β€” 리딩 ν™”λ©΄ ν•œ μž₯
{
  "card": {"id": "E01-01", "title": "μ–΄λ‘ μœΌλ‘œμ˜ μ΄ˆλŒ€", "episode": "E01",
            "episode_title": "제1ν™”: μ–΄λ¦° 밀의 μ†λ‹˜"},
  "narration": [
    {"type": "prose|dialogue_center|character_dialogue|stage_direction",
     "text": "...", "speaker": "",           // speakerλŠ” character_dialogue만
     "role": "lead_in|aftermath|null",       // ν™”λ©΄ 배치 μ—­ν•  β€” lead_in(λ°œν™” 직전 짧은 λ„μž…)은
                                             //  μŠ€ν…Œμ΄μ§€ ν™”λ©΄ 상단에 μ–ΉνžŒλ‹€ (μœ λ„: 적재 μ‹œ κ·œμΉ™)
     "portrait": "/assets/인물/μΉ΄λ₯΄λ°€λΌ.png"} // ν™”μž μ΄ˆμƒ (router SPEAKER_PORTRAIT,
  ],                                          //  μ—†μœΌλ©΄ null β†’ μ΄λ¦„ν‘œλ§Œ. μœ μ €Β·λ‘œλΌλŠ” ν”„λ‘ νŠΈκ°€
                                              //  1인칭(였λ₯Έμͺ½ μ •λ ¬)으둜 처리
  "choices": [{"label": "λˆˆμ„ λœ¬λ‹€"}],       // μ—†μœΌλ©΄ []
  "can_chat": true,                          // κ°œμž… 포켓(μžμœ μ±„νŒ…) κ°€λŠ₯ νŽ˜μ΄μ§€
  "image": "/assets/ν”Œλž˜μ΄ 이미지/4.png",     // μ—°μΆœ μ»· (μ—†μœΌλ©΄ null). λ§€ν•‘: router IMAGE_MAP
  "background": "/assets/λ°°κ²½/μΉ¨μ‹€.png",      // μž₯μ†Œ λ°°κ²½ (μΉ΄λ“œ #μž₯μ†Œ/ νƒœκ·Έ β†’ router BACKGROUND_MAP,
                                             //  λ§€ν•‘ μ—†λŠ” μž₯μ†ŒλŠ” 직전 λ°°κ²½ μœ μ§€ Β· 엔딩은 null)
  "progress": {"index": 5, "total": 44},     // 읽기 진행도 (νŽ˜μ΄μ§€ λ‹¨μœ„)
                                             // νŽ˜μ΄μ§€ = μ—¬λŸ¬ μΉ΄λ“œ 병합(services/paging.py, 결정적
                                             // νŒŒν‹°μ…˜ Β· run별 κ· ν˜• λΆ„ν• , λͺ©ν‘œ chapter.page_target_chars
                                             // κΈ°λ³Έ 500자). card.title은 참고용 β€” ν΄λΌμ΄μ–ΈνŠΈλŠ” μΉ΄λ“œ
                                             // μ†Œμ œλͺ©μ„ ν‘œμ‹œν•˜μ§€ μ•ŠλŠ”λ‹€(μ—ν”Όμ†Œλ“œ 헀더·컷 μΊ‘μ…˜λ§Œ).
                                             // card.id = νŽ˜μ΄μ§€ 끝 μΉ΄λ“œ(μ„ νƒΒ·μ±„νŒ… κΈ°μ€€),
                                             // title/episode = 첫 μΉ΄λ“œ κΈ°μ€€.
                                             // μ—°μΆœ 컷은 단독 νŽ˜μ΄μ§€ + ν‘œμ‹œ 2νŽ˜μ΄μ§€(μ»·+λ³Έλ¬Έ) β€”
                                             // imageκ°€ 있으면 μ»· νŽ˜μ΄μ§€ 번호 = index - 1
                                             // (ν΄λΌμ΄μ–ΈνŠΈλŠ” 컷을 λ¨Όμ € λ Œλ”ν•œ λ’€ λ³Έλ¬Έ)
  "ending": null                             // 챕터 μ’…λ£Œ μ‹œμ—λ§Œ μ•„λž˜ Ending
}

// Ending
{"id": "ending_eternal", "label": "The Eternal β€” 동화/μ˜μ›", "description": "..."}

// PocketTurn
{"reply": "β€¦μ΄μƒν•œ 말을 ν•˜λŠ”κ΅¬λ‚˜, 둜라.", "converged": false, "reason": null, "turn_no": 1}
```

## μ—”λ“œν¬μΈνŠΈ

| λ©”μ„œλ“œ/경둜 | μš”μ²­ | 응닡 | λΉ„κ³  |
|---|---|---|---|
| `POST /api/session` | `{}` | `{"session_id", "step": Step}` | μƒˆ ν”Œλ ˆμ΄ μ‹œμž‘ (E01-01) |
| `GET /api/session/{sid}` | β€” | `{"step": Step}` | ν˜„μž¬ μœ„μΉ˜ 재쑰회 = **볡원 μ§„μž…μ ** (μƒˆλ‘œκ³ μΉ¨Β·μ„œλ²„ μž¬μ‹œμž‘). 404λ©΄ μƒˆ μ„Έμ…˜μ„ λ§Œλ“€ 것 |
| `POST /api/session/{sid}/advance` | `{}` | `{"step": Step}` | 선택지 μ—†λŠ” μΉ΄λ“œμ—μ„œ λ‹€μŒ μž₯ |
| `POST /api/session/{sid}/choose` | `{"index": 0}` | `{"picked": {"label","result"}, "step": Step}` | 선택 반영(좕은 μ„œλ²„ λ‚΄λΆ€) |
| `POST /api/session/{sid}/pocket/open` | `{}` | `{"opened": true}` | ν˜„μž¬ μΉ΄λ“œμ—μ„œ μ±„νŒ… μ‹œμž‘. can_chat=falseλ©΄ 409 |
| `POST /api/session/{sid}/pocket/turn` | `{"text": "..."}` | `PocketTurn` | converged=trueλ©΄ μžλ™ close(컀밋) μ™„λ£Œ μƒνƒœ |
| `POST /api/session/{sid}/pocket/close` | `{}` | `{"closed": true}` | μœ μ €κ°€ λ¨Όμ € λŒ€ν™”λ₯Ό 접을 λ•Œ (컀밋 포함) |
| `GET /api/meta` | β€” | `{"cover_image","title","subtitle","intro_images"}` | ν‘œμ§€Β·μΈνŠΈλ‘œ μ‹œν€€μŠ€(μž‘ν’ˆ/인물 μ†Œκ°œΒ·1μž₯ μ‹œμž‘ β€” μƒˆ μ‹œμž‘μ—λ§Œ ν‘œμ‹œ, 이어 μ½κΈ°λŠ” κ±΄λ„ˆλœ€) |
| `GET /assets/…` | β€” | 이미지 파일 | μ—°μΆœ 에셋 (λŒ€ν‘œλ‹˜ μ‚°μΆœλ¬Ό, 읽기 μ „μš©) |

였λ₯˜: μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ„Έμ…˜ **λ˜λŠ” μ½˜ν…μΈ  κ°±μ‹ μœΌλ‘œ λ¬΄νš¨ν™”λœ μ„Έμ…˜** 404 Β·
μƒνƒœμ— μ•ˆ λ§žλŠ” 호좜(선택지 μΉ΄λ“œμ—μ„œ advance λ“±) 409 Β· `{"detail": "..."}` (FastAPI κΈ°λ³Έ 포맷).

- ν΄λΌμ΄μ–ΈνŠΈλŠ” `session_id`λ₯Ό `localStorage`(`carmilla_sid`)에 λ³΄κ΄€ν•˜κ³ , 기동 μ‹œ GET으둜 볡원을
  μ‹œλ„ν•œλ‹€. 404λ©΄ ν‚€λ₯Ό 버리고 μƒˆ μ„Έμ…˜. 볡원 λ²”μœ„λŠ” "ν˜„μž¬ νŽ˜μ΄μ§€λΆ€ν„°" β€” λ’€λ‘œκ°€κΈ° νžˆμŠ€ν† λ¦¬λŠ” μ„Έμ…˜ λ‚΄ ν•œμ •
- μ½˜ν…μΈ (λΉ„νŠΈμΉ΄λ“œ) μž¬λ³€ν™˜μœΌλ‘œ μΉ΄λ“œ ID/μˆœμ„œκ°€ λ°”λ€Œλ©΄ κΈ°μ‘΄ μ„Έμ…˜μ€ μ„œλ²„κ°€ λ¬΄νš¨ν™”ν•œλ‹€ (버전 μŠ€νƒ¬ν”„)

## 흐름

```
POST /api/session ─→ step
  β”œβ”€ choices=[] ──── POST advance ──→ step (반볡)
  β”œβ”€ choices=[...] ─ POST choose ──→ picked.result ν‘œμ‹œ β†’ step
  β”‚                  └─ (선택 μ „) POST pocket/open β†’ pocket/turn* β†’ converged β†’ μ„ νƒμ§€λ‘œ 볡귀
  └─ ending ─────── μ—”λ”© ν™”λ©΄
```

- 포켓 수렴/ν„΄ μƒν•œ(6ν„΄)Β·μ΅œμ†Œ 체λ₯˜(2ν„΄)λŠ” μ„œλ²„ μ½”λ“œκ°€ κ°•μ œ. ν΄λΌμ΄μ–ΈνŠΈλŠ” converged만 λ”°λ₯Έλ‹€
- ν„΄ λ‘œκ·ΈλŠ” μ„œλ²„κ°€ `logs/playlog.jsonl`에 기둝 (연ꡬ 데이터)