sagnik-mukherjee commited on
Commit
42be466
verified
1 Parent(s): 4a47f39

Deploy provenance-first OpenComic-Continue research UI

Browse files
Files changed (3) hide show
  1. README.md +9 -2
  2. __pycache__/app.cpython-312.pyc +0 -0
  3. app.py +212 -34
README.md CHANGED
@@ -12,6 +12,13 @@ license: apache-2.0
12
 
13
  # OpenComic-Continue
14
 
15
- Public research interface for the copyright-aware OpenComic-Continue pipeline. The Space calls an authenticated Modal API; credentials are configured only as Space secrets.
 
 
 
 
 
16
 
17
- Only upload content you own or are permitted to transform. Placeholder panels are explicitly labelled when the image GPU renderer is unavailable.
 
 
 
12
 
13
  # OpenComic-Continue
14
 
15
+ Public research interface for the copyright-aware OpenComic-Continue pipeline. It analyzes several
16
+ context pages, exposes an editable multi-page prose brief, locks a separate page/panel storyboard,
17
+ and only then renders sequential identity-preserving edits. A bounded whole-frame checkpoint must
18
+ pass literal cast/prop/anatomy review; otherwise the rest of the sequence switches to a conservative
19
+ source-layer renderer. The Space calls an authenticated Modal API; credentials are configured only
20
+ as managed Space secrets.
21
 
22
+ Only upload content you own or are permitted to transform. Image output fails closed when the GPU
23
+ renderer or strict anatomy/identity/prop verifier is unavailable; planning placeholders are never
24
+ presented as generated art. Modal services are intentionally scale-to-zero and may cold-start.
__pycache__/app.cpython-312.pyc CHANGED
Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ
 
app.py CHANGED
@@ -10,9 +10,18 @@ from pathlib import Path
10
 
11
  import gradio as gr
12
  import httpx
13
- import spaces
14
  from PIL import Image
15
 
 
 
 
 
 
 
 
 
 
 
16
  API_URL = os.getenv("MODAL_API_URL", "http://127.0.0.1:8000").rstrip("/")
17
  API_TOKEN = os.getenv("OPENCOMIC_API_TOKEN", "")
18
 
@@ -40,12 +49,15 @@ def _upload_payload(files: list[str] | None) -> tuple[bytes, str, str]:
40
  return stream.getvalue(), "uploaded-pages.cbz", "application/vnd.comicbook+zip"
41
 
42
 
43
- def analyze(files: list[str] | None, reading_direction: str):
44
  payload, name, mime = _upload_payload(files)
45
  with httpx.Client(timeout=900) as client:
46
  response = client.post(
47
  f"{API_URL}/analyze-comic",
48
- params={"reading_direction": reading_direction},
 
 
 
49
  headers=_headers(),
50
  files={"file": (name, payload, mime)},
51
  )
@@ -58,8 +70,11 @@ def analyze(files: list[str] | None, reading_direction: str):
58
  )
59
  session = {
60
  "memory": result["memory"],
 
61
  "reference_images": result.get("reference_images", []),
62
  "semantic_analysis": result.get("semantic_analysis", {}),
 
 
63
  }
64
  return session, result["memory"], summary
65
 
@@ -69,8 +84,8 @@ def _data_url_to_image(value: str) -> Image.Image:
69
  return Image.open(io.BytesIO(payload)).convert("RGB")
70
 
71
 
72
- def generate(
73
- session: dict | None,
74
  pages: int,
75
  creativity: float,
76
  dialogue_density: float,
@@ -78,47 +93,70 @@ def generate(
78
  character_fidelity: float,
79
  reading_direction: str,
80
  research_mode: bool,
81
- ):
82
- if not session:
83
- raise gr.Error("Analyze a comic before requesting a continuation.")
84
- memory = session.get("memory", session)
85
- reference_images = session.get("reference_images", [])
86
- request = {
87
- "memory": memory,
88
- "reference_images": reference_images,
 
 
89
  "settings": {
90
  "pages": int(pages),
91
  "creativity": creativity,
92
  "dialogue_density": dialogue_density,
93
  "style_fidelity": style_fidelity,
94
  "character_fidelity": character_fidelity,
95
- "reading_direction": reading_direction,
96
  "research_mode": research_mode,
97
  "seed": 20260823,
98
  },
99
  }
100
- with httpx.Client(timeout=900) as client:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  planned_response = client.post(
102
  f"{API_URL}/plan-continuation", headers=_headers(), json=request
103
  )
104
  if planned_response.is_error:
105
  raise gr.Error(
106
- f"Planning failed ({planned_response.status_code}): "
107
- f"{planned_response.text[:500]}"
108
  )
109
  planned = planned_response.json()
110
  render_request = {
111
  **request,
112
- "script": planned["script"],
113
  "planner_metadata": planned.get("planner", {}),
114
  }
115
- response = client.post(
116
- f"{API_URL}/render-script", headers=_headers(), json=render_request
117
- )
118
  if response.is_error:
119
- raise gr.Error(
120
- f"Rendering failed ({response.status_code}): {response.text[:500]}"
121
- )
122
  result = response.json()
123
  images = [_data_url_to_image(item) for item in result.get("page_data_urls", [])]
124
  research = {
@@ -132,8 +170,10 @@ def generate(
132
  }
133
  updated_session = {
134
  "memory": result.get("memory"),
 
135
  "reference_images": result.get("next_reference_images", reference_images),
136
  "semantic_analysis": session.get("semantic_analysis", {}),
 
137
  }
138
  return (
139
  images,
@@ -144,6 +184,102 @@ def generate(
144
  )
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def record_preference(choice: str, notes: str) -> str:
148
  if not choice:
149
  return "Choose a preference before submitting."
@@ -157,11 +293,14 @@ def record_preference(choice: str, notes: str) -> str:
157
  with gr.Blocks(title="OpenComic-Continue") as demo:
158
  gr.Markdown(
159
  "# OpenComic-Continue\n"
160
- "A story-first research prototype: understand the comic, lock a brief continuation "
161
- "story, storyboard four causal boxes, then render each box from the previous image. "
 
 
162
  "Only upload material you own or have permission to transform."
163
  )
164
  memory_state = gr.State()
 
165
  with gr.Tab("Analyze"):
166
  uploads = gr.File(
167
  label="Comic pages, PDF, CBZ, or ZIP",
@@ -169,20 +308,24 @@ with gr.Blocks(title="OpenComic-Continue") as demo:
169
  type="filepath",
170
  )
171
  direction = gr.Radio(["ltr", "rtl"], value="ltr", label="Reading direction")
 
 
 
 
 
 
172
  analyze_button = gr.Button("Analyze comic", variant="primary")
173
  analysis_summary = gr.Markdown()
174
  memory_json = gr.JSON(label="StoryMemory")
175
  analyze_button.click(
176
  analyze,
177
- [uploads, direction],
178
  [memory_state, memory_json, analysis_summary],
179
  api_name="analyze_quality",
180
  )
181
  with gr.Tab("Continue"):
182
  with gr.Row():
183
- page_count = gr.Slider(
184
- 1, 1, value=1, step=1, label="Quality pages per run", interactive=False
185
- )
186
  creativity = gr.Slider(0, 1, value=0.5, label="Creativity")
187
  dialogue = gr.Slider(
188
  0,
@@ -197,16 +340,51 @@ with gr.Blocks(title="OpenComic-Continue") as demo:
197
  ["auto", "ltr", "rtl"], value="auto", label="Reading direction"
198
  )
199
  research_mode = gr.Checkbox(label="Research mode (show routing and model metadata)")
200
- generate_button = gr.Button("Generate continuation", variant="primary")
201
  gr.Markdown(
202
- "Generation runs in two bounded backend stages: a locked story + storyboard, then "
203
- "sequential panel rendering with per-panel visual checks."
204
  )
 
 
 
 
 
 
 
205
  gallery = gr.Gallery(label="Continuation pages", columns=2, object_fit="contain")
206
  scripts = gr.JSON(label="Structured scripts")
207
  updated_memory = gr.JSON(label="Updated StoryMemory")
208
  research_json = gr.JSON(label="Research metadata")
209
- generate_button.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  generate,
211
  [
212
  memory_state,
 
10
 
11
  import gradio as gr
12
  import httpx
 
13
  from PIL import Image
14
 
15
+ try:
16
+ import spaces
17
+ except ImportError: # Local CPU development; Hugging Face provides this module in the Space.
18
+ class _LocalSpaces:
19
+ @staticmethod
20
+ def GPU(*_args, **_kwargs):
21
+ return lambda function: function
22
+
23
+ spaces = _LocalSpaces()
24
+
25
  API_URL = os.getenv("MODAL_API_URL", "http://127.0.0.1:8000").rstrip("/")
26
  API_TOKEN = os.getenv("OPENCOMIC_API_TOKEN", "")
27
 
 
49
  return stream.getvalue(), "uploaded-pages.cbz", "application/vnd.comicbook+zip"
50
 
51
 
52
+ def analyze(files: list[str] | None, reading_direction: str, continuity_notes: str):
53
  payload, name, mime = _upload_payload(files)
54
  with httpx.Client(timeout=900) as client:
55
  response = client.post(
56
  f"{API_URL}/analyze-comic",
57
+ params={
58
+ "reading_direction": reading_direction,
59
+ "continuity_notes": continuity_notes,
60
+ },
61
  headers=_headers(),
62
  files={"file": (name, payload, mime)},
63
  )
 
70
  )
71
  session = {
72
  "memory": result["memory"],
73
+ "context_images": result.get("context_images", []),
74
  "reference_images": result.get("reference_images", []),
75
  "semantic_analysis": result.get("semantic_analysis", {}),
76
+ "layout_analysis": result.get("layout_analysis", {}),
77
+ "reading_direction": reading_direction,
78
  }
79
  return session, result["memory"], summary
80
 
 
84
  return Image.open(io.BytesIO(payload)).convert("RGB")
85
 
86
 
87
+ def _continuation_request(
88
+ session: dict,
89
  pages: int,
90
  creativity: float,
91
  dialogue_density: float,
 
93
  character_fidelity: float,
94
  reading_direction: str,
95
  research_mode: bool,
96
+ ) -> dict:
97
+ resolved_direction = (
98
+ session.get("reading_direction", "ltr")
99
+ if reading_direction == "auto"
100
+ else reading_direction
101
+ )
102
+ return {
103
+ "memory": session.get("memory", session),
104
+ "context_images": session.get("context_images", []),
105
+ "reference_images": session.get("reference_images", []),
106
  "settings": {
107
  "pages": int(pages),
108
  "creativity": creativity,
109
  "dialogue_density": dialogue_density,
110
  "style_fidelity": style_fidelity,
111
  "character_fidelity": character_fidelity,
112
+ "reading_direction": resolved_direction,
113
  "research_mode": research_mode,
114
  "seed": 20260823,
115
  },
116
  }
117
+
118
+
119
+ def generate(
120
+ session: dict | None,
121
+ pages: int,
122
+ creativity: float,
123
+ dialogue_density: float,
124
+ style_fidelity: float,
125
+ character_fidelity: float,
126
+ reading_direction: str,
127
+ research_mode: bool,
128
+ ):
129
+ if not session:
130
+ raise gr.Error("Analyze a comic before requesting a continuation.")
131
+ request = _continuation_request(
132
+ session,
133
+ pages,
134
+ creativity,
135
+ dialogue_density,
136
+ style_fidelity,
137
+ character_fidelity,
138
+ reading_direction,
139
+ research_mode,
140
+ )
141
+ reference_images = request["reference_images"]
142
+ context_images = request["context_images"]
143
+ with httpx.Client(timeout=3600) as client:
144
  planned_response = client.post(
145
  f"{API_URL}/plan-continuation", headers=_headers(), json=request
146
  )
147
  if planned_response.is_error:
148
  raise gr.Error(
149
+ f"Planning failed ({planned_response.status_code}): {planned_response.text[:500]}"
 
150
  )
151
  planned = planned_response.json()
152
  render_request = {
153
  **request,
154
+ "scripts": planned["scripts"],
155
  "planner_metadata": planned.get("planner", {}),
156
  }
157
+ response = client.post(f"{API_URL}/render-script", headers=_headers(), json=render_request)
 
 
158
  if response.is_error:
159
+ raise gr.Error(f"Rendering failed ({response.status_code}): {response.text[:500]}")
 
 
160
  result = response.json()
161
  images = [_data_url_to_image(item) for item in result.get("page_data_urls", [])]
162
  research = {
 
170
  }
171
  updated_session = {
172
  "memory": result.get("memory"),
173
+ "context_images": context_images,
174
  "reference_images": result.get("next_reference_images", reference_images),
175
  "semantic_analysis": session.get("semantic_analysis", {}),
176
+ "layout_analysis": session.get("layout_analysis", {}),
177
  }
178
  return (
179
  images,
 
184
  )
185
 
186
 
187
+ def draft_story_stage(
188
+ session: dict | None,
189
+ pages: int,
190
+ creativity: float,
191
+ dialogue_density: float,
192
+ style_fidelity: float,
193
+ character_fidelity: float,
194
+ reading_direction: str,
195
+ research_mode: bool,
196
+ ):
197
+ if not session:
198
+ raise gr.Error("Analyze a comic before drafting its continuation.")
199
+ request = _continuation_request(
200
+ session,
201
+ pages,
202
+ creativity,
203
+ dialogue_density,
204
+ style_fidelity,
205
+ character_fidelity,
206
+ reading_direction,
207
+ research_mode,
208
+ )
209
+ with httpx.Client(timeout=1800) as client:
210
+ response = client.post(f"{API_URL}/draft-story", headers=_headers(), json=request)
211
+ if response.is_error:
212
+ raise gr.Error(f"Story drafting failed ({response.status_code}): {response.text[:500]}")
213
+ drafted = response.json()
214
+ state = {"request": request, "story": drafted, "scripts": None}
215
+ return (
216
+ state,
217
+ json.dumps(drafted["story_brief"], indent=2, ensure_ascii=False),
218
+ "Story drafted and editorially validated. Edit it if needed, then lock it into a storyboard.",
219
+ {"story_stage": drafted},
220
+ )
221
+
222
+
223
+ def storyboard_stage(planning_state: dict | None, approved_story: dict | str | None):
224
+ if not planning_state or not approved_story:
225
+ raise gr.Error("Draft and approve a story before storyboarding.")
226
+ if isinstance(approved_story, str):
227
+ try:
228
+ approved_story = json.loads(approved_story)
229
+ except json.JSONDecodeError as exc:
230
+ raise gr.Error(f"The edited story is not valid JSON: {exc}") from exc
231
+ request = {**planning_state["request"], "locked_story_brief": approved_story}
232
+ with httpx.Client(timeout=1800) as client:
233
+ response = client.post(
234
+ f"{API_URL}/storyboard-continuation", headers=_headers(), json=request
235
+ )
236
+ if response.is_error:
237
+ raise gr.Error(f"Storyboarding failed ({response.status_code}): {response.text[:500]}")
238
+ planned = response.json()
239
+ state = {**planning_state, "request": request, "storyboard": planned, "scripts": planned["scripts"]}
240
+ return (
241
+ state,
242
+ planned.get("planner", {}).get("causal_plan", {}),
243
+ planned["scripts"],
244
+ "Storyboard locked. Review the page and panel beats, then start visual rendering.",
245
+ {"story_stage": planning_state.get("story", {}), "storyboard_stage": planned},
246
+ )
247
+
248
+
249
+ def render_locked_stage(session: dict | None, planning_state: dict | None):
250
+ if not session or not planning_state or not planning_state.get("scripts"):
251
+ raise gr.Error("Lock a storyboard before rendering.")
252
+ request = planning_state["request"]
253
+ render_request = {
254
+ **request,
255
+ "scripts": planning_state["scripts"],
256
+ "planner_metadata": planning_state.get("storyboard", {}).get("planner", {}),
257
+ }
258
+ with httpx.Client(timeout=3600) as client:
259
+ response = client.post(f"{API_URL}/render-script", headers=_headers(), json=render_request)
260
+ if response.is_error:
261
+ raise gr.Error(f"Rendering failed ({response.status_code}): {response.text[:500]}")
262
+ result = response.json()
263
+ images = [_data_url_to_image(item) for item in result.get("page_data_urls", [])]
264
+ research = {
265
+ "story_stage": planning_state.get("story", {}),
266
+ "storyboard_stage": planning_state.get("storyboard", {}),
267
+ "renderer": result.get("renderer", {}),
268
+ "job_id": result.get("job_id"),
269
+ }
270
+ updated_session = {
271
+ "memory": result.get("memory"),
272
+ "context_images": request.get("context_images", []),
273
+ "reference_images": result.get(
274
+ "next_reference_images", request.get("reference_images", [])
275
+ ),
276
+ "semantic_analysis": session.get("semantic_analysis", {}),
277
+ "layout_analysis": session.get("layout_analysis", {}),
278
+ "reading_direction": request["settings"]["reading_direction"],
279
+ }
280
+ return images, result.get("scripts", []), updated_session, result.get("memory"), research
281
+
282
+
283
  def record_preference(choice: str, notes: str) -> str:
284
  if not choice:
285
  return "Choose a preference before submitting."
 
293
  with gr.Blocks(title="OpenComic-Continue") as demo:
294
  gr.Markdown(
295
  "# OpenComic-Continue\n"
296
+ "A multi-page story-first research prototype: understand several context pages, lock a "
297
+ "complete continuation arc, storyboard each page into natural four- or five-panel pacing, "
298
+ "then render every box from the previous "
299
+ "image plus immutable cast/prop references. "
300
  "Only upload material you own or have permission to transform."
301
  )
302
  memory_state = gr.State()
303
+ planning_state = gr.State()
304
  with gr.Tab("Analyze"):
305
  uploads = gr.File(
306
  label="Comic pages, PDF, CBZ, or ZIP",
 
308
  type="filepath",
309
  )
310
  direction = gr.Radio(["ltr", "rtl"], value="ltr", label="Reading direction")
311
+ continuity_notes = gr.Textbox(
312
+ label="Optional continuity corrections",
313
+ placeholder="Example: four physical cats; the round bottle is pink, not red",
314
+ lines=2,
315
+ max_lines=4,
316
+ )
317
  analyze_button = gr.Button("Analyze comic", variant="primary")
318
  analysis_summary = gr.Markdown()
319
  memory_json = gr.JSON(label="StoryMemory")
320
  analyze_button.click(
321
  analyze,
322
+ [uploads, direction, continuity_notes],
323
  [memory_state, memory_json, analysis_summary],
324
  api_name="analyze_quality",
325
  )
326
  with gr.Tab("Continue"):
327
  with gr.Row():
328
+ page_count = gr.Slider(2, 4, value=2, step=1, label="Continuation pages")
 
 
329
  creativity = gr.Slider(0, 1, value=0.5, label="Creativity")
330
  dialogue = gr.Slider(
331
  0,
 
340
  ["auto", "ltr", "rtl"], value="auto", label="Reading direction"
341
  )
342
  research_mode = gr.Checkbox(label="Research mode (show routing and model metadata)")
 
343
  gr.Markdown(
344
+ "1. Draft the causal prose story. 2. Edit/approve it and lock a storyboard. "
345
+ "3. Render the locked panels sequentially with strict anatomy, cast, and prop checks."
346
  )
347
+ with gr.Row():
348
+ draft_button = gr.Button("1 路 Draft story", variant="primary")
349
+ storyboard_button = gr.Button("2 路 Lock storyboard")
350
+ render_button = gr.Button("3 路 Render panels")
351
+ stage_status = gr.Markdown()
352
+ story_brief = gr.Code(label="Editable story brief", language="json", lines=24)
353
+ storyboard_json = gr.JSON(label="Locked page/panel storyboard")
354
  gallery = gr.Gallery(label="Continuation pages", columns=2, object_fit="contain")
355
  scripts = gr.JSON(label="Structured scripts")
356
  updated_memory = gr.JSON(label="Updated StoryMemory")
357
  research_json = gr.JSON(label="Research metadata")
358
+ draft_button.click(
359
+ draft_story_stage,
360
+ [
361
+ memory_state,
362
+ page_count,
363
+ creativity,
364
+ dialogue,
365
+ style,
366
+ character,
367
+ generation_direction,
368
+ research_mode,
369
+ ],
370
+ [planning_state, story_brief, stage_status, research_json],
371
+ api_name="draft_story_quality",
372
+ )
373
+ storyboard_button.click(
374
+ storyboard_stage,
375
+ [planning_state, story_brief],
376
+ [planning_state, storyboard_json, scripts, stage_status, research_json],
377
+ api_name="storyboard_quality",
378
+ )
379
+ render_button.click(
380
+ render_locked_stage,
381
+ [memory_state, planning_state],
382
+ [gallery, scripts, memory_state, updated_memory, research_json],
383
+ api_name="render_quality",
384
+ )
385
+ # Keep the original combined API contract for scripted research clients.
386
+ legacy_generate_button = gr.Button("Legacy combined generation", visible=False)
387
+ legacy_generate_button.click(
388
  generate,
389
  [
390
  memory_state,