comicx / comic /schema.py
ASTRALK's picture
Upload comic/schema.py with huggingface_hub
00207fd verified
Raw
History Blame Contribute Delete
4.37 kB
"""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)