Spaces:
Running on Zero
Running on Zero
| """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 | |
| class Character: | |
| """A fixed cast member. `appearance` is reused verbatim in image prompts.""" | |
| name: str | |
| appearance: str | |
| def from_dict(cls, d: dict) -> "Character": | |
| return cls( | |
| name=str(d.get("name", "")).strip(), | |
| appearance=str(d.get("appearance", "")).strip(), | |
| ) | |
| class PageSynopsis: | |
| page: int | |
| synopsis: str | |
| 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 | |
| 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 | |
| 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) | |
| 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) | |
| 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, | |
| ) | |
| class Comic: | |
| """The finished comic: bible + the 20 panels in reading order.""" | |
| bible: ComicBible | |
| panels: List[Panel] = field(default_factory=list) | |
| 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) | |