"""Typed structures for a generated comic. The flow produces, in order: ComicBible — the fixed story + cast (Gemma call #1). `characters` are FIXED for the whole story: their `appearance` text is injected verbatim into every panel image prompt, which is what keeps the art consistent. Panel — one of 20 panels (Gemma calls #2..N): a visual `scene` (drives FLUX) and a `caption` (the reader text shown under the image). Comic — the assembled result: 10 pages, two panels each, images attached. """ from __future__ import annotations from dataclasses import dataclass, field from typing import List, Optional PAGES = 25 PANELS_PER_PAGE = 2 TOTAL_PANELS = PAGES * PANELS_PER_PAGE # 50 @dataclass class Character: """A fixed cast member. `appearance` is reused verbatim in image prompts.""" name: str appearance: str @classmethod def from_dict(cls, d: dict) -> "Character": return cls( name=str(d.get("name", "")).strip(), appearance=str(d.get("appearance", "")).strip(), ) @dataclass class PageSynopsis: page: int synopsis: str @dataclass class ComicBible: """The fixed story bible from the gatekeeper/planning call.""" approved: bool refusal_reason: str = "" title: str = "" logline: str = "" art_style: str = "" palette: str = "" characters: List[Character] = field(default_factory=list) pages: List[PageSynopsis] = field(default_factory=list) # length 10 when approved @classmethod def from_dict(cls, d: dict) -> "ComicBible": chars = [Character.from_dict(c) for c in (d.get("characters") or []) if str(c.get("name", "")).strip()] pages = [] for i, p in enumerate(d.get("pages") or [], start=1): pages.append(PageSynopsis( page=int(p.get("page", i) or i), synopsis=str(p.get("synopsis", "")).strip(), )) return cls( approved=bool(d.get("approved", False)), refusal_reason=str(d.get("refusal_reason", "")).strip(), title=str(d.get("title", "")).strip() or "Untitled", logline=str(d.get("logline", "")).strip(), art_style=str(d.get("art_style", "")).strip(), palette=str(d.get("palette", "")).strip(), characters=chars, pages=pages, ) def character(self, name: str) -> Optional[Character]: key = (name or "").strip().lower() for c in self.characters: if c.name.strip().lower() == key: return c return None @dataclass class Panel: """One rendered panel. `image` is JPEG/PNG bytes once the artist has run.""" page: int panel: int # 1 or 2 within the page scene: str # purely-visual description -> FLUX caption: str # reader text shown under the image characters: List[str] = field(default_factory=list) image_prompt: str = "" # the assembled FLUX prompt (set by imaging) image: Optional[bytes] = None # rendered bytes (set by the artist) @property def index(self) -> int: """0-based global panel index across the whole comic (0..19).""" return (self.page - 1) * PANELS_PER_PAGE + (self.panel - 1) @classmethod def from_dict(cls, d: dict, default_page: int = 1, default_panel: int = 1) -> "Panel": chars = [str(c).strip() for c in (d.get("characters") or []) if str(c).strip()] return cls( page=int(d.get("page", default_page) or default_page), panel=int(d.get("panel", default_panel) or default_panel), scene=str(d.get("scene", "")).strip(), caption=str(d.get("caption", "")).strip(), characters=chars, ) @dataclass class Comic: """The finished comic: bible + the 20 panels in reading order.""" bible: ComicBible panels: List[Panel] = field(default_factory=list) @property def title(self) -> str: return self.bible.title def page_panels(self, page: int) -> List[Panel]: """The (up to two) panels on a given 1-based page, in order.""" ps = [p for p in self.panels if p.page == page] return sorted(ps, key=lambda p: p.panel)