Spaces:
Sleeping
Sleeping
| """νμ΄μ§ λ³ν© β μ¬λ¬ λΉνΈμΉ΄λλ₯Ό ν νλ©΄(νμ΄μ§)μΌλ‘ λ¬Άλ κ²°μ μ νν°μ . | |
| μμΉ: | |
| - μμ¬ μ§νΒ·μ¨μ μνμ κΆμλ StoryEngineμ΄ μ μ§νλ€. Pagerλ engine.advance()λ₯Ό | |
| νμ΄μ§ κ²½κ³κΉμ§ λ°λ³΅ νΈμΆν΄ μ§λ¬Έμ λ³ν©ν λΏ, λΆμν¨κ³Όλ₯Ό μ§μ λ§λ€μ§ μλλ€ | |
| (μΉ΄λλΉ μ νν 1ν β μ΄μ€/λλ½ μ μ© κΈμ§) | |
| - νν°μ μ μ½ν μΈ (μ μ μΉ΄λ μμ)μ μμ ν¨μ β μ μ κ²½λ‘Β·μνμ 무κ΄ν΄ | |
| νμ΄μ§ μ΄μ(μ§νλ λΆλͺ¨)κ° μμ μ μ΄κ³ , 볡μ μμλ κ°μ νμ΄μ§κ° μ¬κ΅¬μ±λλ€ | |
| - λ³ν©μ μ ν λ§ν¬(card.next == μ μ λ€μ μΉ΄λ)λ‘ μ΄μ΄μ§ ꡬκ°(run)μμλ§. κ°μ λΆλ¦¬: | |
| Β· μ νμ§/λΆκΈ°(next_options) μΉ΄λ λ€ (κ²°μ μ§μ ) | |
| Β· solo μΉ΄λ(μ°μΆ μ»·Β·μ±ν μ΅μ»€β νΈμΆμκ° μ§μ ) μλ€ | |
| Β· μνΌμλ κ²½κ³ | |
| - λ°λ κ· ν: runλ³ μ΄ κΈμμλ‘ νμ΄μ§ μ k = round(μ΄/target)λ₯Ό λ¨Όμ μ νκ³ , | |
| λμ κΈμμκ° κ· λ± κ²½κ³(μ΄Β·i/k)μ κ°μ₯ κ°κΉμ΄ μΉ΄λ λ€μμ μ λ¨ β 그리λ κ³Όμ λ°©μ§ | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Optional | |
| from engine.repositories.content import ContentRepository | |
| from engine.schemas.beatcard import BeatCard, Choice, NarrationBlock | |
| from engine.schemas.chapter import EndingRule | |
| from engine.services.story import StepResult, StoryEngine | |
| class PageView: | |
| """ν νμ΄μ§μ λ λ μΆλ ₯ β λΌμ°ν°κ° κ·Έλλ‘ JSONμΌλ‘ λ§λ λ€.""" | |
| first: Optional[BeatCard] = None # νμ΄μ§ μ λͺ©Β·μνΌμλΒ·μ°μΆ μ»·μ κΈ°μ€ | |
| last: Optional[BeatCard] = None # μ νμ§Β·μ±ν Β·μ§ν μμΉ(current_id)μ κΈ°μ€ | |
| narration: list[NarrationBlock] = field(default_factory=list) | |
| narration_cards: list[str] = field(default_factory=list) # λΈλ‘λ³ μμ μΉ΄λ (λ³λ ¬) | |
| choices: list[Choice] = field(default_factory=list) | |
| can_chat: bool = False | |
| ending: Optional[EndingRule] = None | |
| class Pager: | |
| def __init__(self, repo: ContentRepository, solo_ids: set[str], | |
| cut_ids: set[str], target_chars: int | None = None): | |
| """solo_ids: λ¨λ νμ΄μ§ κ°μ μΉ΄λ(μ°μΆ μ»·+μ±ν μ΅μ»€) Β· cut_ids: μ°μΆ μ»·(νμ 2νμ΄μ§).""" | |
| self.repo = repo | |
| self.solo = set(solo_ids) | |
| self.cuts = set(cut_ids) | |
| self.target = target_chars or repo.chapter.page_target_chars | |
| self._build() | |
| # ββ μ μ νν°μ βββββββββββββββββββββββββββββββββββββββββ | |
| def _build(self) -> None: | |
| # 1) ꡬ쑰μ λΈλ μ΄ν¬λ‘ run(λ³ν© κ°λ₯ν μ΅λ ꡬκ°)μ λ§λ€κ³ | |
| runs: list[list[BeatCard]] = [] | |
| cur: list[BeatCard] = [] | |
| prev: Optional[BeatCard] = None | |
| for card in self.repo.all_cards(): | |
| if cur and self._structural_break(prev, card): | |
| runs.append(cur) | |
| cur = [] | |
| cur.append(card) | |
| prev = card | |
| if cur: | |
| runs.append(cur) | |
| # 2) runλ³λ‘ κ· ν λΆν | |
| parts: list[list[str]] = [] | |
| for run in runs: | |
| parts += [[c.id for c in part] for part in self._split_run(run)] | |
| self.partitions = parts | |
| self.part_of = {cid: i for i, part in enumerate(parts) for cid in part} | |
| # νμ λ²νΈ: νν°μ = 1νμ΄μ§, μ°μΆ μ»· νν°μ = 2νμ΄μ§(μ»·+λ³Έλ¬Έ) | |
| self._cuts_before = [] | |
| seen = 0 | |
| for part in parts: | |
| self._cuts_before.append(seen) | |
| if part[0] in self.cuts: | |
| seen += 1 | |
| self._total = len(parts) + seen | |
| def _structural_break(self, prev: BeatCard, card: BeatCard) -> bool: | |
| if prev.choices or prev.next_options: | |
| return True # κ²°μ μ§μ β νμ΄μ§λ μ νμ§λ‘ λ«νλ€ | |
| if prev.next != card.id: | |
| return True # λΉμ ν(λΆκΈ° λ³νΒ·μ νΒ·μνΌμλ μ°Έμ‘°) β κ²½λ‘ μμ μ μν΄ λΆλ¦¬ | |
| if prev.id in self.solo or card.id in self.solo: | |
| return True | |
| return prev.episode_id != card.episode_id | |
| def _chars(card: BeatCard) -> int: | |
| return sum(len(b.text) for b in card.narration) | |
| def _split_run(self, run: list[BeatCard]) -> list[list[BeatCard]]: | |
| """runμ k = round(μ΄ κΈμμ/target)κ°μ μ°μ ꡬκ°μΌλ‘ κ· λ± λΆν (κ²°μ μ ).""" | |
| total = sum(self._chars(c) for c in run) | |
| k = max(1, min(len(run), round(total / self.target))) | |
| if k == 1: | |
| return [run] | |
| bounds = [total * i / k for i in range(1, k)] # κ· λ± μ λ¨ λͺ©νμ | |
| parts: list[list[BeatCard]] = [] | |
| cur: list[BeatCard] = [] | |
| acc = 0 | |
| bi = 0 | |
| for idx, card in enumerate(run): | |
| cur.append(card) | |
| acc += self._chars(card) | |
| remaining_cards = len(run) - idx - 1 | |
| remaining_cuts = (k - 1) - bi | |
| if bi >= len(bounds) or remaining_cards == 0: | |
| continue | |
| next_acc = acc + self._chars(run[idx + 1]) | |
| # μ§κΈ μλ₯΄λ κ² λ€μ μΉ΄λκΉμ§ λκ³ κ°λ κ²λ³΄λ€ κ²½κ³μ κ°κΉκ±°λ, | |
| # λ¨μ μ λ¨ μλ§νΌμ μΉ΄λλ°μ μ λ¨μμΌλ©΄ μ¬κΈ°μ μ λ¨ | |
| if (abs(acc - bounds[bi]) <= abs(next_acc - bounds[bi]) | |
| or remaining_cards == remaining_cuts): | |
| parts.append(cur) | |
| cur = [] | |
| bi += 1 | |
| if cur: | |
| parts.append(cur) | |
| return parts | |
| # ββ νμ λ²νΈ (μ§νλ) ββββββββββββββββββββββββββββββββββ | |
| def display_total(self) -> int: | |
| return self._total | |
| def display_index(self, card_id: str) -> int: | |
| """μΉ΄λκ° μν νμ΄μ§μ λ³Έλ¬Έ νμ λ²νΈ. μ»· νμ΄μ§ λ²νΈλ μ΄ κ° - 1 (κΈ°μ‘΄ κ³μ½ μ μ§).""" | |
| p = self.part_of[card_id] | |
| own_cut = 1 if self.partitions[p][0] in self.cuts else 0 | |
| return p + self._cuts_before[p] + own_cut + 1 | |
| # ββ λ°νμ: νμ΄μ§ μ±μ°κΈ° (λΆμν¨κ³Όλ engine.advanceκ° μΉ΄λλΉ 1ν) ββ | |
| def complete(self, engine: StoryEngine, step: StepResult) -> PageView: | |
| """advance/choose/start μ§νμ step(νμ΄μ§ 첫 μΉ΄λ)μ νν°μ λκΉμ§ μ±μ΄λ€.""" | |
| if step.ending: | |
| return PageView(ending=step.ending) | |
| first = step.card | |
| part = self.partitions[self.part_of[first.id]] | |
| narration = list(step.narration) | |
| cards = [first.id] * len(step.narration) | |
| while engine.current_id != part[-1]: | |
| step = engine.advance() | |
| if step.ending: # νν°μ λ΄ λ§ν¬λ μ±ν° μ β λ°©μ΄μ μ²λ¦¬ | |
| return PageView(ending=step.ending) | |
| narration += step.narration | |
| cards += [step.card.id] * len(step.narration) | |
| last = step.card | |
| return PageView(first=first, last=last, narration=narration, | |
| narration_cards=cards, | |
| choices=list(last.choices), can_chat=last.can_chat) | |
| # ββ λ°νμ: 볡μ λ λ (λΆμν¨κ³Ό μμ) βββββββββββββββββββ | |
| def render_current(self, engine: StoryEngine) -> PageView: | |
| """νμ¬ μΉ΄λ(νμ νν°μ λ)κ° μν νμ΄μ§ μ 체λ₯Ό μ¬κ΅¬μ± β μ΄μ΄ μ½κΈ° νλ©΄.""" | |
| if engine.ending: | |
| return PageView(ending=engine.ending) | |
| part = self.partitions[self.part_of[engine.current_id]] | |
| cards = [self.repo.get_card(cid) for cid in part] | |
| narration, ncards = [], [] | |
| for c in cards: | |
| vis = engine.visible_narration(c) | |
| narration += vis | |
| ncards += [c.id] * len(vis) | |
| return PageView(first=cards[0], last=cards[-1], narration=narration, | |
| narration_cards=ncards, | |
| choices=list(cards[-1].choices), can_chat=cards[-1].can_chat) | |