ASTRALK commited on
Commit
044e70f
Β·
verified Β·
1 Parent(s): 08dce3d

Upload comic/engine.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. comic/engine.py +143 -0
comic/engine.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """generate_comic β€” the full idea -> finished comic pipeline.
2
+
3
+ It is a GENERATOR that yields progress events as it goes, so the Gradio app can show
4
+ live status and stream panels onto the page as they render (rather than freezing for
5
+ the whole ~minute of generation). Stages:
6
+
7
+ 1. WRITER bible call: safety gate + story bible. If refused -> a 'refused' event, done.
8
+ 2. WRITER panel calls: 5 batches of 2 pages each, each fed a recap of prior panels
9
+ for continuity. Yields a 'panels' event per batch.
10
+ 3. ARTIST renders all 20 panels, one at a time, yielding an 'image' event each.
11
+ 4. 'done' event with the finished Comic.
12
+
13
+ The pipeline is backend-agnostic (mock or modal) via make_backends(). Errors in a
14
+ single panel render are caught so one bad image never sinks the whole comic.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import Iterator, Optional
21
+
22
+ from .backends import make_backends, WriterBackend, ArtistBackend
23
+ from .schema import Comic, ComicBible, Panel, TOTAL_PANELS
24
+ from . import writer as W
25
+ from .imaging import build_image_prompt, panel_seed
26
+
27
+ # Panels rendered per GPU pass. 4 fits klein-9B at 832x576 on an H100 with headroom;
28
+ # the serve endpoint falls back to serial if a batch ever OOMs.
29
+ RENDER_BATCH = 4
30
+
31
+
32
+ @dataclass
33
+ class GenerateEvent:
34
+ """One step of progress. `kind` drives the UI; payload fields are kind-specific."""
35
+ kind: str # status|bible|refused|panels|image|done|error
36
+ message: str = ""
37
+ comic: Optional[Comic] = None # carried from 'bible' onward (mutated in place)
38
+ panel: Optional[Panel] = None # for 'image' events
39
+ progress: float = 0.0 # 0..1 coarse progress for a progress bar
40
+
41
+
42
+ def _retry_json(fn, parse, attempts: int = 2):
43
+ """Call fn() -> reply, parse it; retry once on a parse/JSON failure."""
44
+ last = None
45
+ for _ in range(attempts):
46
+ reply = fn()
47
+ try:
48
+ return parse(reply)
49
+ except Exception as e: # noqa: BLE001 - surface after retries
50
+ last = e
51
+ raise last if last else RuntimeError("generation failed")
52
+
53
+
54
+ def generate_comic(
55
+ idea: str,
56
+ writer: WriterBackend | None = None,
57
+ artist: ArtistBackend | None = None,
58
+ backend: str | None = None,
59
+ ) -> Iterator[GenerateEvent]:
60
+ """Yield GenerateEvents from raw idea to a fully rendered Comic."""
61
+ if writer is None or artist is None:
62
+ w, a = make_backends(backend)
63
+ writer = writer or w
64
+ artist = artist or a
65
+
66
+ idea = (idea or "").strip()
67
+ if not idea:
68
+ yield GenerateEvent("error", "Please describe the comic you want.")
69
+ return
70
+
71
+ # ── 1. bible + safety gate ────────────────────────────────────────────────
72
+ yield GenerateEvent("status", "Reading your idea and planning the story…", progress=0.02)
73
+ try:
74
+ bible: ComicBible = _retry_json(
75
+ lambda: writer.chat(W.build_bible_messages(idea)),
76
+ W.parse_bible,
77
+ )
78
+ except Exception as e: # noqa: BLE001
79
+ yield GenerateEvent("error", f"Couldn't plan the comic ({type(e).__name__}). Try again.")
80
+ return
81
+
82
+ if not bible.approved:
83
+ reason = bible.refusal_reason or "That request can't be turned into a comic."
84
+ yield GenerateEvent("refused", reason)
85
+ return
86
+
87
+ comic = Comic(bible=bible)
88
+ yield GenerateEvent("bible", f"β€œ{bible.title}” β€” {bible.logline}", comic=comic, progress=0.1)
89
+
90
+ # ── 2. panel script, batched, with running recap for continuity ───────────
91
+ written: list[Panel] = []
92
+ page_batches = W.batches()
93
+ n_batches = len(page_batches)
94
+ for bi, pages in enumerate(page_batches):
95
+ yield GenerateEvent(
96
+ "status",
97
+ f"Writing pages {pages[0]}–{pages[-1]} of {len(comic.bible.pages)}…",
98
+ comic=comic,
99
+ progress=0.1 + 0.3 * (bi / n_batches),
100
+ )
101
+ recap = W.recap_from_panels(written)
102
+ try:
103
+ panels = _retry_json(
104
+ lambda: writer.chat(W.build_panel_messages(bible, pages, recap)),
105
+ lambda r: W.parse_panels(r, pages),
106
+ )
107
+ except Exception as e: # noqa: BLE001
108
+ yield GenerateEvent("error", f"Story writing failed on pages {pages} ({type(e).__name__}).")
109
+ return
110
+ # Assemble each panel's image prompt now (deterministic, no model call).
111
+ for p in panels:
112
+ p.image_prompt = build_image_prompt(p, bible)
113
+ written.extend(panels)
114
+ comic.panels = sorted(written, key=lambda x: x.index)
115
+ yield GenerateEvent("panels", f"Pages {pages[0]}–{pages[-1]} scripted.",
116
+ comic=comic, progress=0.1 + 0.3 * ((bi + 1) / n_batches))
117
+
118
+ # ── 3. render every panel (batched through the GPU for throughput) ─────────
119
+ ordered = sorted(comic.panels, key=lambda x: x.index)
120
+ total = len(ordered) or TOTAL_PANELS
121
+ done = 0
122
+ for start in range(0, len(ordered), RENDER_BATCH):
123
+ chunk = ordered[start:start + RENDER_BATCH]
124
+ yield GenerateEvent(
125
+ "status",
126
+ f"Illustrating panels {start + 1}–{start + len(chunk)} of {total}…",
127
+ comic=comic, progress=0.4 + 0.6 * (start / total),
128
+ )
129
+ prompts = [p.image_prompt for p in chunk]
130
+ seeds = [panel_seed(bible, p) for p in chunk]
131
+ try:
132
+ images = artist.render_batch(prompts, seeds)
133
+ except Exception as e: # noqa: BLE001 - a batch failure must not sink the comic
134
+ images = [None] * len(chunk)
135
+ yield GenerateEvent("status", f"A render batch hiccupped ({type(e).__name__}); continuing…",
136
+ comic=comic)
137
+ for panel, img in zip(chunk, images):
138
+ panel.image = img
139
+ done += 1
140
+ yield GenerateEvent("image", f"Panel {done} ready.", comic=comic, panel=panel,
141
+ progress=0.4 + 0.6 * (done / total))
142
+
143
+ yield GenerateEvent("done", f"β€œ{bible.title}” is ready.", comic=comic, progress=1.0)