ASTRALK commited on
Commit
00207fd
·
verified ·
1 Parent(s): be67303

Upload comic/schema.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. comic/schema.py +124 -0
comic/schema.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Typed structures for a generated comic.
2
+
3
+ The flow produces, in order:
4
+ ComicBible — the fixed story + cast (Gemma call #1). `characters` are FIXED for
5
+ the whole story: their `appearance` text is injected verbatim into
6
+ every panel image prompt, which is what keeps the art consistent.
7
+ Panel — one of 20 panels (Gemma calls #2..N): a visual `scene` (drives FLUX)
8
+ and a `caption` (the reader text shown under the image).
9
+ Comic — the assembled result: 10 pages, two panels each, images attached.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from typing import List, Optional
16
+
17
+ PAGES = 25
18
+ PANELS_PER_PAGE = 2
19
+ TOTAL_PANELS = PAGES * PANELS_PER_PAGE # 50
20
+
21
+
22
+ @dataclass
23
+ class Character:
24
+ """A fixed cast member. `appearance` is reused verbatim in image prompts."""
25
+ name: str
26
+ appearance: str
27
+
28
+ @classmethod
29
+ def from_dict(cls, d: dict) -> "Character":
30
+ return cls(
31
+ name=str(d.get("name", "")).strip(),
32
+ appearance=str(d.get("appearance", "")).strip(),
33
+ )
34
+
35
+
36
+ @dataclass
37
+ class PageSynopsis:
38
+ page: int
39
+ synopsis: str
40
+
41
+
42
+ @dataclass
43
+ class ComicBible:
44
+ """The fixed story bible from the gatekeeper/planning call."""
45
+ approved: bool
46
+ refusal_reason: str = ""
47
+ title: str = ""
48
+ logline: str = ""
49
+ art_style: str = ""
50
+ palette: str = ""
51
+ characters: List[Character] = field(default_factory=list)
52
+ pages: List[PageSynopsis] = field(default_factory=list) # length 10 when approved
53
+
54
+ @classmethod
55
+ def from_dict(cls, d: dict) -> "ComicBible":
56
+ chars = [Character.from_dict(c) for c in (d.get("characters") or [])
57
+ if str(c.get("name", "")).strip()]
58
+ pages = []
59
+ for i, p in enumerate(d.get("pages") or [], start=1):
60
+ pages.append(PageSynopsis(
61
+ page=int(p.get("page", i) or i),
62
+ synopsis=str(p.get("synopsis", "")).strip(),
63
+ ))
64
+ return cls(
65
+ approved=bool(d.get("approved", False)),
66
+ refusal_reason=str(d.get("refusal_reason", "")).strip(),
67
+ title=str(d.get("title", "")).strip() or "Untitled",
68
+ logline=str(d.get("logline", "")).strip(),
69
+ art_style=str(d.get("art_style", "")).strip(),
70
+ palette=str(d.get("palette", "")).strip(),
71
+ characters=chars,
72
+ pages=pages,
73
+ )
74
+
75
+ def character(self, name: str) -> Optional[Character]:
76
+ key = (name or "").strip().lower()
77
+ for c in self.characters:
78
+ if c.name.strip().lower() == key:
79
+ return c
80
+ return None
81
+
82
+
83
+ @dataclass
84
+ class Panel:
85
+ """One rendered panel. `image` is JPEG/PNG bytes once the artist has run."""
86
+ page: int
87
+ panel: int # 1 or 2 within the page
88
+ scene: str # purely-visual description -> FLUX
89
+ caption: str # reader text shown under the image
90
+ characters: List[str] = field(default_factory=list)
91
+ image_prompt: str = "" # the assembled FLUX prompt (set by imaging)
92
+ image: Optional[bytes] = None # rendered bytes (set by the artist)
93
+
94
+ @property
95
+ def index(self) -> int:
96
+ """0-based global panel index across the whole comic (0..19)."""
97
+ return (self.page - 1) * PANELS_PER_PAGE + (self.panel - 1)
98
+
99
+ @classmethod
100
+ def from_dict(cls, d: dict, default_page: int = 1, default_panel: int = 1) -> "Panel":
101
+ chars = [str(c).strip() for c in (d.get("characters") or []) if str(c).strip()]
102
+ return cls(
103
+ page=int(d.get("page", default_page) or default_page),
104
+ panel=int(d.get("panel", default_panel) or default_panel),
105
+ scene=str(d.get("scene", "")).strip(),
106
+ caption=str(d.get("caption", "")).strip(),
107
+ characters=chars,
108
+ )
109
+
110
+
111
+ @dataclass
112
+ class Comic:
113
+ """The finished comic: bible + the 20 panels in reading order."""
114
+ bible: ComicBible
115
+ panels: List[Panel] = field(default_factory=list)
116
+
117
+ @property
118
+ def title(self) -> str:
119
+ return self.bible.title
120
+
121
+ def page_panels(self, page: int) -> List[Panel]:
122
+ """The (up to two) panels on a given 1-based page, in order."""
123
+ ps = [p for p in self.panels if p.page == page]
124
+ return sorted(ps, key=lambda p: p.panel)