interactive-chat / docs /contract /demo_api.md
Junhoee's picture
Upload 162 files
1c9c13f verified
|
Raw
History Blame Contribute Delete
5.87 kB

데λͺ¨ API 계약 β€” μ›Ή 리더 ↔ μ—”μ§„ (M6)

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

곡톡 νƒ€μž…

// 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에 기둝 (연ꡬ 데이터)