Spaces:
Running
Running
| """๋นํธ์นด๋ YAML ์คํค๋ง (๋ด๋ฌํฐ๋ธ ๋ ์ด์ด). | |
| ๊ถ์๋ ๋นํธ์นด๋ MD(๋ํ๋ ์์ ). content/beatcards/*.yaml ์ | |
| scripts/beatcard_convert.py ์ฐ์ถ๋ฌผ์ด๋ฉฐ, ์ด ์คํค๋ง๊ฐ ๊ทธ ๊ตฌ์กฐ๋ฅผ ์ ๊ทผ๋ค. | |
| ์ถ(axis) ํ๊ธฐ: | |
| - ์นด๋ ํ๊ทธ `#์ถ/์ ๋ขฐ+` `#์ถ/์์ฌ+` โ ์นด๋ ์ง์ ์ ์ ์ฉ (choices ์๋ ์นด๋๋ง) | |
| - ์ ํ์ง ๊ฒฐ๊ณผ ๋ธ๋ก `ใ์ ๋ขฐ+ ยท โฆใ` โ ํด๋น ์ ํ ์ ์ ์ฉ | |
| """ | |
| from __future__ import annotations | |
| from typing import Literal, Optional | |
| from pydantic import BaseModel, ConfigDict, Field | |
| Axis = Literal["trust", "doubt", "neutral"] | |
| NARRATION_TYPES = ("prose", "dialogue_center", "stage_direction", "character_dialogue") | |
| # ๋ฐํ ์ง์ ์ ์ด ๊ธธ์ด ์ดํ prose๋ ๋ฆฌ๋์ธ โ ๋ ๋๊ฐ ์คํ ์ด์ง ํ๋ฉด ์๋จ์ ์น๋๋ค | |
| LEAD_IN_MAX_CHARS = 140 | |
| def is_lead_in(block_type: str, text: str, next_type: str) -> bool: | |
| """๋ฆฌ๋์ธ ์ ๋ ๊ท์น (๋จ์ผ ๊ถ์ โ ๋ณํ๊ธฐยท์ ์ฌ๊ฐ ๊ณต์ ).""" | |
| return (block_type == "prose" and len(text) <= LEAD_IN_MAX_CHARS | |
| and next_type == "character_dialogue") | |
| class NarrationBlock(BaseModel): | |
| """๊ณ ์ ์ง๋ฌธ ๋ธ๋ก. gate๊ฐ ์์ผ๋ฉด ์ ๋ขฐ ๋ ๋ฒจ์ ๋ฐ๋ผ ๋ ธ์ถ์ด ๊ฐ๋ฆฐ๋ค.""" | |
| model_config = ConfigDict(extra="forbid") | |
| type: Literal["prose", "dialogue_center", "stage_direction", "character_dialogue"] | |
| text: str | |
| speaker: str = "" | |
| anchor: bool = False # True = ๊ฐ์ ํฌ์ผ์์ LLM ๋ณ์ฃผ ๋์ ์ต์ปค ๋์ฌ | |
| gate: Literal["all", "trust_high"] = "all" # ใํ๋ฉดยท์ ์ใ / ใ์ฌ์ธตยท์ ๋ขฐ้ซ ํด๊ธใ | |
| # ํ๋ฉด ๋ฐฐ์น ์ญํ (์ฐ์ถ ๊ตฌ์กฐํ). ๋ฏธ์ง์ ์ด๋ฉด ์ ์ฌ ์ ์ ๋ ๊ท์น(derive_roles) ์ ์ฉ: | |
| # lead_in = ๋ฐํ ์ง์ ์ ์งง์ ๋์ prose (์คํ ์ด์ง ํ๋ฉด์ ์นํ) ยท aftermath = ์์ฝ | |
| role: Optional[Literal["lead_in", "aftermath"]] = None | |
| def derive_roles(narration: list[NarrationBlock]) -> None: | |
| """role ๋ฏธ์ง์ ๋ธ๋ก์ ์ ๋ ๊ท์น ์ ์ฉ (์ ์๋ฆฌ ๊ฐฑ์ โ ๋ช ์ role์ ์กด์ค).""" | |
| for i, b in enumerate(narration[:-1]): | |
| if b.role is None and is_lead_in(b.type, b.text, narration[i + 1].type): | |
| b.role = "lead_in" | |
| class Choice(BaseModel): | |
| """์ ํ์ง. axis/result/next๋ ๊ฒฐ๊ณผ ๋ธ๋ก ๋งค์นญ์ผ๋ก ์ฑ์์ง๋ฉฐ ๋ฏธํ์ ์ด๋ฉด None.""" | |
| model_config = ConfigDict(extra="forbid") | |
| label: str | |
| modifiers: list[str] = Field(default_factory=list) # ใ๋ฅ๋๋ฐ๊ฒฌใ ๋ฑ | |
| axis: Optional[Axis] = None | |
| result: str = "" # ์ ํ ์งํ ๊ณ ์ ๊ฒฐ๊ณผ ์ฐ๋ฌธ | |
| next: Optional[str] = None # ๋ถ๊ธฐ ์นด๋ (์์ผ๋ฉด card.next ๋ฐ๋ฆ) | |
| # ํ๋ฅ ๋ถ๊ธฐ ์นด๋(๋๋ฌ์ )์ ์ ํ์ ์ ํ ์ปค๋ฐ. ๋๋ฌ์ ์ ๊ฑด๋๋ฐ์ง ์๊ธฐ ์ํ ํํ: | |
| # ์) E06-09 ๋์กฐ โ E06-10์ ์ ์ ํต๊ณผํ๋ E06-10์ ๋ถ๊ธฐ๋ E06-10C๋ก ์์ฝ | |
| preset_branch: Optional[str] = None | |
| class BeatCard(BaseModel): | |
| model_config = ConfigDict(extra="forbid") | |
| id: str | |
| title: str | |
| subtitle: str = "" | |
| tags: list[str] = Field(default_factory=list) | |
| status: str = "์ด์" | |
| canon: bool = False | |
| next: Optional[str] = None # ์นด๋ ID ๋๋ ์ํผ์๋ ์ฐธ์กฐ(E07 ๋ฑ) | |
| next_options: list[str] = Field(default_factory=list) # E06-10B/C ํ ๋ถ๊ธฐ ํ๋ณด | |
| axis: Optional[Axis] = None # ์นด๋ ์ง์ ์ ์ถ ์ด๋ (#์ถ/ ํ๊ทธ, ๋จ์ผ๊ฐ์ผ ๋๋ง) | |
| frailty: bool = False # #๋ฉ์ปค๋์ฆ/์ ์ฝ๋ โ ์ง์ ์ ์ ์ฝ๋ +1 | |
| narration: list[NarrationBlock] = Field(default_factory=list) | |
| choices: list[Choice] = Field(default_factory=list) | |
| r_card_refs: list[str] = Field(default_factory=list) | |
| note: str = "" | |
| direction: str = "" | |
| image: str = "" | |
| def is_pocket(self) -> bool: | |
| """๊ฐ์ ํฌ์ผ ์ฌ๋ถ โ ์ ํ์ง ๋๋ ์ต์ปค ๋์ฌ(์์ ์ฑํ ๋ณ์ฃผ ๋์)๊ฐ ์์ผ๋ฉด ํฌ์ผ.""" | |
| return bool(self.choices) or any(b.anchor for b in self.narration) | |
| def can_chat(self) -> bool: | |
| """์์ ์ฑํ ํ์ฉ โ ์นด๋ฅด๋ฐ๋ผ๊ฐ ์ต์ปค ํ์์ธ ์นด๋๋ง (MVP: ์นด๋ฅด๋ฐ๋ผ ์ค์ฌ ํ๋ ์ด์ ). | |
| ์๋ฒ์ง/๋ง๋๋ชจ์์ ค ๋ฑ ๋ค๋ฅธ ํ์์ ํฌ์ผ์ ์ ํ์ง๋ง ์ ๊ณตํ๋ค | |
| (์บ๋ฆญํฐ ์์ด์ ํธ๊ฐ ์นด๋ฅด๋ฐ๋ผ ์ ์ฉ์ด๋ฏ๋ก, ํ ํ์ ์ฑํ ์ 2๋จ๊ณ).""" | |
| return any(b.anchor and "์นด๋ฅด๋ฐ๋ผ" in (b.speaker or "") for b in self.narration) | |
| def episode_id(self) -> str: | |
| return self.id.split("-")[0] | |
| class EpisodeMeta(BaseModel): | |
| model_config = ConfigDict(extra="forbid") | |
| id: str | |
| title: str = "" | |
| source: str = "" | |
| class EpisodeFile(BaseModel): | |
| """content/beatcards/E##.yaml ํ ํ์ผ = ์ํผ์๋ ํ๋.""" | |
| model_config = ConfigDict(extra="forbid") | |
| episode: EpisodeMeta | |
| cards: list[BeatCard] | |
| class RCardStage(BaseModel): | |
| model_config = ConfigDict(extra="forbid") | |
| stage: int | |
| title: str = "" | |
| anchor: str = "" # ๋ํ ๋์ฌ โโฆโ | |
| prose: str = "" | |
| variants: list[str] = Field(default_factory=list) | |
| class RCard(BaseModel): | |
| """R์นด๋ โ ํ์ฐจ ์๋ ์ฌ์ฌ์ฉ ์นด๋ (ํํผ์ฌ๋ค๋ฆฌ/์ฝ์ ๋ฐ์).""" | |
| model_config = ConfigDict(extra="forbid") | |
| id: str | |
| title: str = "" | |
| tags: list[str] = Field(default_factory=list) | |
| rules: list[str] = Field(default_factory=list) # ๐ ์ ๋ ๊ท์น | |
| stages: list[RCardStage] = Field(default_factory=list) | |
| raw: str = "" # ํ์๊ฐ ๊ตฌ์กฐํํ์ง ๋ชปํ ์์ฌ ๋ณธ๋ฌธ (๊ฒ์ฆ ๋ฆฌํฌํธ ๋์) | |