sorac ์นด๋ฅด๋ฐ๋ผ โ ์ ์ฒด ๊ตฌ์กฐ ๋ช
์ธ์
์ด ๋ฌธ์ ํ๋๋ก "๋ฌด์จ ํ์ผ์ด ์ด๋ ์๊ณ , ์ด๋ค ์ญํ ์ด๋ฉฐ, ์ด๋ป๊ฒ ๋ง๋ฌผ๋ ค ๋๋์ง"๋ฅผ ํ์
ํ ์ ์๊ฒ ์ด ๋ช
์ธ์.
๋ง์ง๋ง ๊ฐฑ์ : 2026-07-03 (MVP M0~M6 ์๋ฃ ์์ )
0. ํ ๋ฌธ์ฅ ์์ฝ
๋นํธ์นด๋(๋ํ๋์ด ์ด ์์ฌ ์๊ณ )๋ฅผ YAML๋ก ๋ณํํด ์ ์ฌํ๊ณ , ํ์ด์ฌ ์ํ๋จธ์ ์ด "์ฑ
์ฝ๊ธฐ"๋ฅผ,
google-adk LLM ํ์ดํ๋ผ์ธ์ด "์นด๋ฅด๋ฐ๋ผ์์ ๋ํ"๋ฅผ ๋ด๋นํ๋ฉฐ, ์น ๋ฆฌ๋๊ฐ ๊ทธ๊ฑธ ํ๋ฉด์ ๊ทธ๋ฆฐ๋ค.
[๋ํ๋ ๋นํธ์นด๋ MD] โโ๋ณํโโโถ [content/ YAML] โโ์ ์ฌโโโถ โโ StoryEngine (์ฝ๊ธฐ: ๊ฒฐ์ ์ ์ฝ๋)
โโ PocketService (๋ํ: ADK LLM)
โ
[web/index.html] โโโHTTPโโ [router.py]
ํต์ฌ ์ค๊ณ ์์น (CLAUDE.md):
- ์์ฌ ์งํ์ ์ฝ๋๊ฐ, ๋์ฌ ์์ฑ๋ง LLM์ด ํ๋ค โ ์คํ ๋ฆฌ๊ฐ ์ฐ์ผ๋ก ๊ฐ์ง ์๊ฒ
- ์จ์ ์ํ(์ ๋ขฐ/์์ฌ/์ ์ฝ)๋ ์ ์ ์๊ฒ ์ ๋ ์ซ์๋ก ๋
ธ์ถํ์ง ์๋๋ค
- ์ฝํ
์ธ (YAML)์ ์ฝ๋์ ๋ถ๋ฆฌ โ ์ด์ผ๊ธฐ ์์ ์ ์ฝํ
์ธ ์ชฝ์์๋ง
1. ๋ฃจํธ ํด๋ ์ง๋
| ๊ฒฝ๋ก |
๋๊ฐ ๋ง๋ ๊ฒ |
์ญํ |
CLAUDE.md |
๊ณต๋ |
๊ฐ๋ฐ ํ๋ฒ. ์ํคํ
์ฒ ๊ท์นยท๊ธ์ง์ฌํญ. Claude Code๊ฐ ๋งค ์ธ์
์ฝ์ |
engine/ |
Claude |
์์ง ๋ณธ์ฒด (ํ์ด์ฌ ํจํค์ง) โ ์๋ ยง3 |
content/ |
๋ณํ๊ธฐ ์ฐ์ถ๋ฌผ |
์์ง์ด ์ฝ๋ ์์ฌ ๋ฐ์ดํฐ (YAML) โ ์๋ ยง2 |
web/ |
Claude |
์น ๋ฆฌ๋ ํ๋ก ํธ (๋จ์ผ HTML) |
scripts/ |
Claude |
๊ฐ๋ฐ์ฉ ์คํฌ๋ฆฝํธ (๋ณํ๊ธฐ, CLI ํ๋ ์ด์ด) |
tests/ |
Claude |
์๋ ํ
์คํธ 18๊ฐ |
docs/ |
Claude |
๊ณํ์ยทAPI ๊ณ์ฝยท๋ณธ ๋ช
์ธ์ |
logs/ |
๋ฐํ์ ์์ฑ |
ํ๋ ์ด ํด ๋ก๊ทธ (์ฐ๊ตฌ ๋ฐ์ดํฐ, git ์ ์ธ) |
data/ |
๋ฐํ์ ์์ฑ |
ํ๋ ์ด ์ธ์
์์ํ SQLite (sessions.db, git ์ ์ธ) |
.env |
์ ์ |
OPENAI_API_KEY (git ์ ์ธ โ ์ ๋ ์ปค๋ฐ ๊ธ์ง) |
.venv/ |
์ค์น ์ฐ์ถ๋ฌผ |
ํ์ด์ฌ ๊ฐ์ํ๊ฒฝ (google-adk 2.3.0 ๋ฑ) |
์นด๋ฅด๋ฐ๋ผ/ |
๋ํ๋ |
์ฝํ
์ธ ์์ฒ โ ๋นํธ์นด๋ MDยท์๊ณ ยท๊ธฐํยท์ด๋ฏธ์ง |
reference/ |
๋ํ๋(ํ๋กํ ํ์
) |
๋น-ADK ๊ฐ๋
๊ฒ์ฆ ์์ง. ์ฐธ๊ณ ์ฉ, ์คํ ์ ํจ |
requirements.txt / pyproject.toml |
Claude |
์์กด์ฑ ๋ชฉ๋ก / pytest ์ค์ |
2. content/ โ ์์ง์ด ์ฝ๋ ์์ฌ ๋ฐ์ดํฐ (3๋ ์ด์ด)
์์ง์ ์ ์ผํ ์์ฌ ์
๋ ฅ. ์์ฒ์ ์นด๋ฅด๋ฐ๋ผ/0701_์นด๋ฅด๋ฐ๋ผ/๋นํธ์นด๋/์ MD ํ์ผ(๋ํ๋ ์์ )์ด๊ณ ,
์ฌ๊ธฐ ์๋ YAML์ scripts/beatcard_convert.py๊ฐ ๋ง๋ ํ์๋ฌผ์ด๋ค. ์ด์ผ๊ธฐ๋ฅผ ๊ณ ์น๋ ค๋ฉด
MD๋ฅผ ๊ณ ์น๊ณ ๋ณํ๊ธฐ๋ฅผ ๋ค์ ๋๋ฆฐ๋ค (YAML ์ง์ ์์ ๊ธ์ง).
| ํ์ผ |
๋ ์ด์ด |
๋ด์ฉ |
content/beatcards/E01.yaml ~ E06.yaml |
๋ด๋ฌํฐ๋ธ |
๋นํธ์นด๋ 61์ฅ. ์นด๋ = ํ๋ฉด ํ ์ฅ. ์ง๋ฌธ ๋ธ๋ก(prose/์ค์๋์ฌ/์ธ๋ฌผ๋์ฌ/๋ฌด๋์ง์), ์ ํ์ง(๋ผ๋ฒจยท๊ฒฐ๊ณผ์ฐ๋ฌธยท์ ๋ขฐ์ถยท๋ถ๊ธฐ), next ๋งํฌ, ํ๊ทธ |
content/beatcards/R-ํํผ์ฌ๋ค๋ฆฌ.yaml |
๋ด๋ฌํฐ๋ธ |
์ ์ ๊ฐ ์ ์ฒด๋ฅผ ์ง๊ฒฉํ์ ๋ ์นด๋ฅด๋ฐ๋ผ์ 3๋จ๊ณ ํํผ ๋์ (์ฌ์ฌ์ฉ ์นด๋) |
content/beatcards/R-์ฝ์ ๋ฐ์.yaml |
๋ด๋ฌํฐ๋ธ |
์ฑ๋ฌผยท๋ง๋ ๋ฑ ์ฝ์ ์ธ๊ธ ์ ๋ฐ์ ์์น |
content/chapters/carmilla_chapter1.yaml |
์ฑํฐ ๋ฉํ |
์์ ์นด๋(E01-01), ๋ฒ์(E01~E06), ์๋ฉ 3์ข
๊ท์น, ์บ๋
ผ ๊ท์น, R์นด๋ ๋ฑ๋ก, ์ฑํ
ํด ์ํ(6) |
content/characters.yaml |
์ ์ฒด์ฑ |
์ธ๋ฌผ ์ํธ 3๋ช
(๋ก๋ผ=ํ๋ ์ด์ด, ์นด๋ฅด๋ฐ๋ผ, ์๋ฒ์ง). ์ฑ๊ฒฉยทํ๋ฒยท์ง์๊ฒฝ๊ณ |
์นด๋ ํ ์ฅ์ ๊ตฌ์กฐ (์):
- id: E06-09
title: ๋๋ง์ ๋น๋ฐ
canon: true
next: E06-10
axis: null
frailty: false
narration:
- {type: prose, text: "..."}
- {type: dialogue_center, text: "์ฃฝ์์ ์คํ๋ ค ์ถ๋ณต์ผ ์๋ ์์ด."}
- {type: dialogue_center, text: "...์์์ด์ผ.", gate: trust_high}
- {type: character_dialogue, speaker: ์นด๋ฅด๋ฐ๋ผ, text: "...", anchor: true}
choices:
- {label: ๋๋ง์ ๋น๋ฐ์ ๋์กฐํ๋ค, axis: trust, result: "...", next: null}
- {label: ๊ทธ๋๋ ๋๊ตฐ๊ฐ์๊ฒ ์๋ ค์ผ..., axis: doubt, result: "...", next: null}
3. engine/ โ ์์ง ๋ณธ์ฒด
3-1. ์ต์์ ํ์ผ
| ํ์ผ |
์ญํ |
model.py |
LLM ์ค์ . ๋ฃจํธ .env๋ฅผ ๋ก๋ฉํ๊ณ LiteLLM์ผ๋ก ๋ชจ๋ธ ์ด๊ธฐํ (LLM_MODEL/CHARACTER_MODEL env๋ก ๊ต์ฒด ๊ฐ๋ฅ, ๊ธฐ๋ณธ openai/gpt-5.4-nano) |
agent.py |
ADK ํ์ดํ๋ผ์ธ ์กฐ๋ฆฝ: pocket_session = Sequential[๋ก๋ โ ๊ฐ๋ โ ์นด๋ฅด๋ฐ๋ผ โ ๋๋ ํฐ]. adk web์ฉ root_agent๋ ์ฌ๊ธฐ์ ๋
ธ์ถ |
app.py |
ADK App(์ด๋ฆ+๋ฃจํธ์์ด์ ํธ+ํ๋ฌ๊ทธ์ธ) + Runner(์ธ์
์๋น์ค) ์์ฑ |
router.py |
FastAPI ์๋ฒ. HTTP ์๋ํฌ์ธํธ(์ธ์
/advance/choose/pocket) + ์น ๋ฆฌ๋ยท์ด๋ฏธ์ง ์๋น. ์ฐ์ถ ์ด๋ฏธ์ง ๋งคํ(IMAGE_MAP)๋ ์ฌ๊ธฐ |
3-2. services/ โ ๋น์ฆ๋์ค ๋ก์ง (๊ฒ์ ๊ท์น์ ์ฌ์ฅ)
| ํ์ผ |
์ญํ |
story.py |
์ฝ๊ธฐ ์ํ๋จธ์ . ์นด๋ ์ ์ด(next), ์ ํ์ง ๋ฐ์, ์ฌ์ธต ๊ฒ์ดํธ ๋
ธ์ถ ํ์ , ์๋ฉ ์กฐ๊ฑด์ ํ๊ฐ(์ ์ฉ ํ์ โ eval ๊ธ์ง). ๋๋ฌ์ ๋ณด์ฅ(preset_branch) ๋ก์ง ํฌํจ |
state.py |
์จ์ ์ํ 3๊ฐ. ์ ๋ขฐ์ถ ์์(trust/doubt, ์ด๋ฒคํธ๋น ยฑ12), ์ ์ฝ๋(๋จ์กฐ ์ฆ๊ฐ๋ง ํ์ฉ), ์(๋์
1ํ ํ๋ฆฝ). ์ ๋ขฐ ๋ ๋ฒจ ํ์ฐ(ยฑ12/ยฑ36 ๊ฒฝ๊ณ โ 5๋จ๊ณ) โ ์ด ๋ ๋ฒจ์ด ์นด๋ฅด๋ฐ๋ผ์ "๊ฒฐ"๊ณผ ์ฌ์ธต ํด๊ธ์ ๊ฒฐ์ |
pocket.py |
์ฑํ
์ธ์
๊ด๋ฆฌ. ํฌ์ผ ์ด๊ธฐ(์นด๋ฅด๋ฐ๋ผ ์นด๋๋ง)/ํด ์คํ/๋ซ๊ธฐ(์ํ ์ปค๋ฐ). ์๋ ด ์ฌ๊ฒ์ฆ: LLM์ด "๋๋ด์"๊ณ ํด๋ ์ฝ๋๊ฐ ๊ฒ์ฌ โ ์ต์ 3ํด ์ฒด๋ฅ, ๋นํธ ์๋ ์ข
๋ฃ ๊ธฐ๊ฐ, 6ํด ๊ฐ์ ์๋ ด |
paging.py |
ํ์ด์ง ๋ณํฉ. ์ฌ๋ฌ ์นด๋๋ฅผ ํ ํ๋ฉด์ผ๋ก โ ๊ฒฐ์ ์ ํํฐ์
(์ ํ ๋งํฌ ๊ตฌ๊ฐ๋ง, ์ ํ์งยท์ฐ์ถ ์ปทยท์ฑํ
์ต์ปคยท์ํผ์๋ ๊ฒฝ๊ณ์์ ๋ถ๋ฆฌ, page_target_chars ์ํํธ ์บก). ๋ถ์ํจ๊ณผ๋ ์ฌ์ ํ story.py๊ฐ ์นด๋๋น 1ํ |
retrieval.py |
(2๋จ๊ณ ์คํ
) GraphRAG ๊ฒ์ ์๋ฆฌ. ์คํฌ์ผ๋ฌ ํํฐ ๊ฐ์ ์ง์ |
3-3. sub_agents/ + agents/ โ ADK ๋ํ ํ์ดํ๋ผ์ธ (์ฑํ
ํ ํด์ 4๋ช
์ด ์์๋๋ก ์ผํจ)
์ ์ ์
๋ ฅ โ โ pocket_context_loader โ โก input_guard โ โข carmilla โ โฃ director โ ์๋ต
| ์์ |
ํ์ผ |
ํ์
|
ํ๋ ์ผ |
| โ |
agents/pocket_context_loader.py |
์ฝ๋(BaseAgent) |
ํ์ฌ ์นด๋์ ์ง๋ฌธยท์ต์ปค๋์ฌยท์ ๋ขฐ ๋ ๋ฒจ๋ณ "๊ฒฐ"์ ์ธ์
์ ์ ์ฌ. ์บ๋ฆญํฐ์ฉ ๋ธ๋ฆฌํ์ ๋๋ ํฐ์ฉ ๋
ธํธ๋ฅผ ๋ถ๋ฆฌ (์คํฌ์ผ๋ฌ ๋์ ๋ฐฉ์ง) |
| โก |
agents/input_guard.py |
์ฝ๋(BaseAgent) |
์ ์ ์
๋ ฅ ๊ฒ์ฌ: ์ ์ฒด ์ง๊ฒฉ("ํกํ๊ท์ง?") โ R-ํํผ์ฌ๋ค๋ฆฌ ๋จ๊ณ ์ง์, ์ฝ์ (์ญ์๊ฐ ๋ฑ) โ R-์ฝ์ ๋ฐ์ ์ง์, ๋ฉํ ๋ฐํ(AI/๊ฒ์) โ ์ธ๊ณ๊ด ๋ณต๊ท ์ง์ |
| โข |
sub_agents/character/agent.py |
LLM |
์นด๋ฅด๋ฐ๋ผ ์๋ต ์์ฑ. instruction = instructions/character_carmilla.md + ์ธ๋ฌผ์ํธ + ์ฅ๋ฉด๋ธ๋ฆฌํ + ๊ฐ๋์ง์ |
| โฃ |
sub_agents/director/agent.py |
LLM |
์ฌํ. ์ ์ ์๊ฒ ์ ๋ณด์. ๋๊ตฌ ํธ์ถ๋ก๋ง ๊ธฐ๋ก: ์ ๋ขฐ์ถ ์ด๋/๋ชฉํ ๋นํธ ๋ฌ์ฑ/์๋ ด ์ ์ธ |
3-4. tools/state/ โ ๋๋ ํฐ๊ฐ ์ฐ๋ ๋๊ตฌ 3์ข
(1ํ์ผ 1ํจ์)
| ํ์ผ |
๊ธฐ๋ฅ |
update_trust.py |
์ด๋ฒ ํด ๋ํ๋ก ์ ๋ขฐ/์์ฌ ์ถ์ ยฑ12 ์ด๋ |
mark_beat.py |
์ฅ๋ฉด ๋ชฉํ ๋นํธ ๋ฌ์ฑ ๊ธฐ๋ก |
finish_pocket.py |
์๋ ด ์ ์ธ(escalate) โ ๋จ ์ต์ข
๊ฒฐ์ ์ pocket.py ์ฝ๋๊ฐ ์ฌ๊ฒ์ฆ |
3-5. ๋๋จธ์ง
| ๊ฒฝ๋ก |
์ญํ |
schemas/ |
์ฝํ
์ธ YAML์ Pydantic ์คํค๋ง(beatcard/chapter/character) + validate CLI (python -m engine.schemas.validate content/ โ ์คํค๋งยท๋งํฌ ๋ฌด๊ฒฐ์ฑยท๋๋ฌ์ฑ ๊ฒ์ฌ) |
repositories/content.py |
YAML โ ๋ฉ๋ชจ๋ฆฌ ์ ์ฌ. ์คํฌ์ผ๋ฌ ํํฐ(ํ์ฌ ์นด๋ ์ดํ ์กฐํ ์ฐจ๋จ). ์นด๋ ์์/์ ์ฒด ์(์งํ๋์ฉ). ์ฝํ
์ธ ๋ฒ์ ์คํฌํ(content_version) |
repositories/playlog.py |
ํด ๋ก๊ทธ jsonl ์ ์ฅ (logs/playlog.jsonl, ์ฐ๊ตฌ ๋ฐ์ดํฐ) |
repositories/play_session.py |
์ธ์
์์ํ โ ์์ฌ ์์น+์จ์ ์ํ ์ค๋
์ท์ SQLite(data/sessions.db)์ ๋งค ๋ณ๊ฒฝ ์ฆ์ ์ ์ฅ. ์๋ฒ ์ฌ์์ ํ ๋ณต์(ํฌ์ผ ์ฑํ
์ ๋น์์ยทํ๊ธฐ). ์ฝํ
์ธ ๋ฒ์ ๋ถ์ผ์น ์ธ์
์ ๋ฌดํจํ |
plugins/logging.py |
ADK ํ๋ฌ๊ทธ์ธ โ ์ฑํ
์ ์ ์ ์
๋ ฅ/์๋ต/๋๊ตฌํธ์ถ/์ํ๋ณํ๋ฅผ ์๋ ๊ธฐ๋ก |
instructions/ |
LLM ํ๋กฌํํธ ์๋ฌธ (์นด๋ฅด๋ฐ๋ผ ์ธ๊ฒฉ ๊ท์น, ๋๋ ํฐ ํ์ ์ ์ฐจ). ๋งํฌยทํ๋ ๊ท์น์ ๊ณ ์น๋ ค๋ฉด ์ฌ๊ธฐ |
callbacks/ |
(๋น ์ค์บํด๋) before_model ์คํฌ์ผ๋ฌ ์ต์ข
๋ฐฉ์ด์ ์๋ฆฌ โ 2๋จ๊ณ |
4. web/ โ ์น ๋ฆฌ๋ (ํ๋ก ํธ์๋)
web/index.html ๋จ์ผ ํ์ผ (๋ฐ๋๋ผ JS, ๋น๋ ์์). router.py๊ฐ ์๋น.
- ์ฝ๊ธฐ: ํ๋ฉด ๋จ์ ํ์ด์ง โ ์คํฌ๋กค ์์ด ๋๊น์ผ๋ก๋ง ์ฝ๋๋ค. ์๋ฒ ํ์ด์ง(์ฌ๋ฌ ์นด๋ ๋ณํฉ,
paging.py๊ฐ run๋ณ ๊ท ํ ๋ถํ โ ๋ชฉํ 500์, ๋ณํฉ ํ์ด์ง ๋ฐด๋ ์ฝ 360~760์)๋ฅผ ํ๋ก ํธ๊ฐ ๋ทฐํฌํธ ์ค์ธก(#page-measure)์ผ๋ก ํ๋ฉด๋ค๋ก ๋ถํ (buildScreens), ๊ฐ์ ์งํ๋ ๋ฒํธ๋ฅผ ๊ณต์ . ์ฐ์ธก ํด๋ฆญ/โ = ๋ค์, ์ข์ธก ํด๋ฆญ/โ = ์ด์ (์ฝ๊ธฐ ์ ์ฉ ๋๋์๋ณด๊ธฐ โ ์ ํ ๋ฒ๋ณต ๋ถ๊ฐ). ์ฐ์ถ ์ปท(์ด๋ฏธ์ง)์ ์์ฒด ๋ฒํธ๋ฅผ ๊ฐ์ง ๋
๋ฆฝ ํ์ด์ง
- ํ์ดํ: ์ํผ์๋ ์ฒซ ํ์ด์ง์ "์ Nํ" ํค๋๋ง ํ์ โ ์นด๋ ์์ ๋ชฉ์ ๋ ๋ํ์ง ์๋๋ค(์ฑ
์ ํ๋ฆ). ์ปท ์บก์
์ ์ ์ง
- ์ด์ด ์ฝ๊ธฐ: ์ธ์
ID๋ฅผ
localStorage์ ๋ณด๊ด โ ์๋ก๊ณ ์นจยท์๋ฒ ์ฌ์์ ํ์๋ ํ์ฌ ํ์ด์ง๋ถํฐ ์ฌ๊ฐ (ํ์ง ๋ฒํผ์ด "๋ฐค์ ์๋๋ค"๋ก ๋ฐ๋). ์๋ฉ์์ ์ฌ์์ํ๋ฉด ํ๊ธฐ
- ์บ๋ฆญํฐ ๋ฐํ(ํ์ด๋ธ๋ฆฌ๋): ์์ฌ ์
character_dialogue๋ ์ปดํฉํธ char-line(์ข ํ์๋ช
ยท ์ฐ ๋์ฌ)์ผ๋ก ๋ณธ๋ฌธ์ ํก์ โ ์ฑ
์ ํ๋ฆ ์ฐ์ . ์ ์ ยท๋ก๋ผ๋ "๋"๋ก ํ๊ธฐ. dialogue_center๋ ์ค์ ์ธ์ฉ ์คํ์ผ ์ ์ง
- VN ์คํ
์ด์ง: ์ธํฐ๋์
ํ์ด์ง(์ ํ์งยท์ฑํ
)์ ๋ง์ง๋ง ์ด์ ๋ฐํ๋ง ์คํ
์ด์ง(๋จ๋
ํ๋ฉด) โ ์ด์์ด ํ๋ ์(76vh)์ ์ฑ์ฐ๊ณ ํ๋จ ํจ๋์ ์ฅ์ ์ด๋ฆํยท๋์ฌ, ๋ง์ง๋ง ๊ทธ๋ฃน์ด๋ฉด ์ ํ์งยท๋ง๊ฑธ๊ธฐ ๋ฒํผ ๋ด์ฅ. ๋ฐํ ์ง์ ์ ์งง์ ๋์
prose(role=lead_in, 140์ ์ดํ ์๋ ์ ๋)๋ ์คํ
์ด์ง ์๋จ์ ์นํ๋ค โ ํ ๋ฌธ์ฅ ํํธ ํ๋ฉด ๋ฐฉ์ง. ์ฑํ
๋๋ก์ด๊ฐ ์ด๋ฆฌ๋ฉด ์ด์ ์์์ ๋ํํ๋ ๊ตฌ๋ (์ฐธ๊ณ : ํ๋์ด ์ด๋ฏธ์ง/7.png)
- ์ ํ์ง:
โบ ๋ง์ปค + ๊ฐ๋ ๊ตฌ๋ถ์ ๋ฆฌ์คํธ (๋ํํ๋ฏ). ์ ํ ๊ฒฐ๊ณผ ์ฐ๋ฌธ ํ์ ํ ์ฐ์ธก ํด๋ฆญ์ผ๋ก ์งํ
- ์ฑํ
: ์นด๋ฅด๋ฐ๋ผ ์นด๋์์ "โฆ๋ง์ ๊ฑด๋ค" โ ํ๋จ ๋๋ก์ด. ์๋ ด ์ ์ฌ์ด(3.4์ด) ํ ์๋ ๋ณต๊ท
- ์ฐ์ถ: ์๋จ ์งํ ๋ฐ(n/์ดํ์ด์ง = ์นด๋ 61 + ์ปท 5 = 66), ์ค์ ์ฅ๋ฉด ์ด๋ฏธ์ง ํ์ด์ง, ํ์ง
- ์ด๋ฏธ์ง ์๋ณธ:
์นด๋ฅด๋ฐ๋ผ/0630โฆ/์ด๋ฏธ์ง/ (router์ /assets๋ก ์๋น, ๋งคํ์ IMAGE_MAP)
5. scripts/ ยท tests/ ยท docs/
| ๊ฒฝ๋ก |
์ญํ |
scripts/beatcard_convert.py |
๋นํธ์นด๋ MD โ content/ YAML ๋ณํ๊ธฐ. ์ ํ์ง-๊ฒฐ๊ณผ ๋งค์นญ, ๋ถ๊ธฐ ํด์, ๋๋ฌ์ ์ฐํ ๋ฐฉ์ง. ์ฝํ
์ธ ๊ฐฑ์ ์ ์ฌ์คํ |
scripts/play.py |
ํฐ๋ฏธ๋ ํ๋ ์ด์ด. --policy trust/doubt/neutral --quiet๋ก ์๋ฉ 3์ข
์๋ ๊ฒ์ฆ ๊ฐ๋ฅ |
tests/ (18๊ฐ) |
test_story.py ์ฝ๊ธฐ/์๋ฉ/์คํฌ์ผ๋ฌ ยท test_pocket.py ์ฑํ
ํ์ดํ๋ผ์ธ(๊ฐ์ง LLM์ผ๋ก ๊ฒฐ์ ์ ๊ฒ์ฆ) ยท test_integration.py ์ฝ๊ธฐโ์ฑํ
โ์๋ฉ ์ ์ฒด ๊ดํต ยท fakes.py ์คํฌ๋ฆฝํธ LLM ํ
์คํธ ๋๋ธ |
docs/plans/mvp.md |
๋ง์ผ์คํค M0~M6 ๊ณํยท์ํยท๊ฒฐ์ ๋ก๊ทธ |
docs/contract/demo_api.md |
์น ๋ฆฌ๋ โ ์๋ฒ HTTP API ๊ณ์ฝ |
docs/contract/beatcard_guide.md |
๋นํธ์นด๋ ์์ฑ ๊ฐ์ด๋ (๊ถ์ฅ ๋ถ๋ยท3๋ง ํจํดยท๋ฆฌ๋์ธ ๊ท์น โ E07~ ์งํ ์ฐธ๊ณ , validate ๊ฒฝ๊ณ ์ ์ฐ๋) |
docs/ARCHITECTURE.md |
๋ณธ ๋ฌธ์ |
6. ๋ฐํ์ ํ๋ฆ (์ค์ ๋ก ์ด๋ป๊ฒ ๋๋)
์ฝ๊ธฐ ํด (LLM ์์, 0์)
[ํฐ/๋ธ๋ผ์ฐ์ ] ์ฐ์ธก ํด๋ฆญ
โ POST /api/session/{id}/advance (router.py)
โ StoryEngine.advance() (story.py)
์นด๋ next ๋ฐ๋ผ ์ด๋ ยท ์ ์ฝ๋/์ ๋ขฐ์ถ ๋ฐ์ ยท ์ฌ์ธต ๊ฒ์ดํธ ํ์
โ ์นด๋ JSON ์๋ต โ ํ๋ฉด ๋ ๋
์ ํ ํด (LLM ์์)
์ ํ์ง ํด๋ฆญ โ POST /choose {index}
โ StoryEngine.choose(): ์ ๋ขฐ์ถ ยฑ12 ยท ๋ถ๊ธฐ(next/preset) ๊ฒฐ์ ยท ๊ฒฐ๊ณผ ์ฐ๋ฌธ ๋ฐํ
์ฑํ
ํด (LLM 2ํ ํธ์ถ โ ์นด๋ฅด๋ฐ๋ผ 1 + ๋๋ ํฐ 1)
"โฆ๋ง์ ๊ฑด๋ค" โ POST /pocket/open โ PocketService.open() (์นด๋ฅด๋ฐ๋ผ ์นด๋ ๊ฒ์ฆ + ์ํ ์๋ฉ)
๋ฉ์์ง ์ ์ก โ POST /pocket/turn
โ ADK Runner: ๋ก๋ โ ๊ฐ๋ โ ์นด๋ฅด๋ฐ๋ผ(LLM) โ ๋๋ ํฐ(LLM+๋๊ตฌ)
โ PocketService๊ฐ ์๋ ด ์ฌ๊ฒ์ฆ (3ํด ๋ฏธ๋ง ๊ธฐ๊ฐ / 6ํด ๊ฐ์ ์ข
๋ฃ)
โ ์๋ ด ์: ์จ์ ์ํ๋ฅผ ๋ณธํธ์ ์ปค๋ฐํ๊ณ ์ฝ๊ธฐ๋ก ๋ณต๊ท
์๋ฉ (LLM ์์)
E06 ๋(โE07 ์ฐธ์กฐ) ๋๋ฌ โ story.py๊ฐ ending_rules ํ๊ฐ:
doubt โฅ 30 โ โ ์ฒ๋จ (์์ ์บ๋
ผ)
trust โฅ 60 & doubt<20 โ โก ์์ (๋คํฌ ๋ก๋งจ์ค)
๊ทธ ์ธ โ โข ๊ตฌ์ (์ด๋ณ)
7. ์คํ ๋ช
๋ น ๋ชจ์
.venv/bin/uvicorn engine.router:app --host 0.0.0.0 --port 8777
.venv/bin/python scripts/play.py
.venv/bin/python scripts/play.py --policy trust --quiet
.venv/bin/python scripts/beatcard_convert.py "์นด๋ฅด๋ฐ๋ผ/0701_์นด๋ฅด๋ฐ๋ผ/๋นํธ์นด๋" -o content/beatcards
.venv/bin/python -m engine.schemas.validate content/
.venv/bin/python -m pytest tests/ -q
8. "์ด๊ฑธ ๋ฐ๊พธ๊ณ ์ถ์ผ๋ฉด ์ด๋๋ฅผ ๋ณด๋" ์นํธ์ํธ
| ํ๊ณ ์ถ์ ๊ฒ |
๋ณด๋ ๊ณณ |
| ์ด์ผ๊ธฐ ๋ณธ๋ฌธยท์ ํ์ง ์์ |
๋นํธ์นด๋ MD (์นด๋ฅด๋ฐ๋ผ/0701โฆ/๋นํธ์นด๋/) โ ๋ณํ๊ธฐ ์ฌ์คํ |
| ์นด๋ฅด๋ฐ๋ผ ๋งํฌยทํ๋ ๊ท์น |
engine/instructions/character_carmilla.md |
| ๋ํ ์ข
๋ฃ(์๋ ด) ํ์ ๊ธฐ์ค |
engine/instructions/director.md + engine/services/pocket.py (MIN_TURNS ๋ฑ) |
| ์๋ฉ ์กฐ๊ฑด ์ซ์ |
content/chapters/carmilla_chapter1.yaml (ending_rules) |
| ์ ๋ขฐ์ถ ์ด๋๋ยท๋ ๋ฒจ ๊ฒฝ๊ณ |
engine/services/state.py (AXIS_DELTA, trust_level) |
| ์ฑํ
ํด ์ํ |
content/chapters/carmilla_chapter1.yaml (pocket_max_turns) |
| ํ์ด์ง๋น ๊ธ ๋ฐ๋ |
content/chapters/carmilla_chapter1.yaml์ page_target_chars (๋ฏธ์ค์ ์ 500) |
| ์ฐ์ถ ์ด๋ฏธ์ง ์ถ๊ฐยท๊ต์ฒด |
engine/router.py (IMAGE_MAP) + scripts/sync_assets.py ์ฌ์คํ (์์ฒ: ์นด๋ฅด๋ฐ๋ผ/0701_์นด๋ฅด๋ฐ๋ผ/์ด๋ฏธ์ง/) |
| ์ฅ์๋ณ ๋ฐฐ๊ฒฝ ์ถ๊ฐยท๊ต์ฒด |
engine/router.py (BACKGROUND_MAP โ ์นด๋ #์ฅ์/ ํ๊ทธ ๊ธฐ์ค) + sync_assets |
| ํ์ ์ด์ ์ถ๊ฐยท๊ต์ฒด (VN ๋ฐํ) |
engine/router.py (SPEAKER_PORTRAIT) + sync_assets |
| ์ธํธ๋ก ์ํ์ค(์ํยท์ธ๋ฌผ ์๊ฐ) ๊ต์ฒด |
engine/router.py (INTRO_IMAGES) |
| ํ๋ฉด ๋์์ธยท์ธํฐ๋์
|
web/index.html |
| ์ธ์
์ ์ฅยท๋ณต์ ์ ์ฑ
(TTL, ๋ฒ์ ๋ฌดํจํ) |
engine/repositories/play_session.py + engine/router.py (_save/_restore) |
| ์ ์ฒด์ง๊ฒฉ/์ฝ์ ํธ๋ฆฌ๊ฑฐ ๋จ์ด |
engine/agents/input_guard.py (TRIGGERS) |
| LLM ๋ชจ๋ธ ๊ต์ฒด |
.env์ LLM_MODEL=... / CHARACTER_MODEL=... |