sagnik-mukherjee commited on
Commit
0a6a7c8
·
verified ·
1 Parent(s): 447e240

Deploy provenance-first OpenComic-Continue research UI

Browse files
Files changed (3) hide show
  1. README.md +10 -6
  2. app.py +184 -0
  3. requirements.txt +3 -0
README.md CHANGED
@@ -1,13 +1,17 @@
1
  ---
2
- title: Codex Opencomic Dev
3
- emoji: 👁
4
  colorFrom: indigo
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.25.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
  ---
2
+ title: OpenComic Continue
3
+ emoji: 📚
4
  colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.1.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
  ---
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.
app.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import io
5
+ import json
6
+ import os
7
+ import tempfile
8
+ import zipfile
9
+ from pathlib import Path
10
+
11
+ import gradio as gr
12
+ import httpx
13
+ from PIL import Image
14
+
15
+ API_URL = os.getenv("MODAL_API_URL", "http://127.0.0.1:8000").rstrip("/")
16
+ API_TOKEN = os.getenv("OPENCOMIC_API_TOKEN", "")
17
+
18
+
19
+ def _headers() -> dict[str, str]:
20
+ return {"Authorization": f"Bearer {API_TOKEN}"} if API_TOKEN else {}
21
+
22
+
23
+ def _upload_payload(files: list[str] | None) -> tuple[bytes, str, str]:
24
+ paths = [Path(item) for item in files or []]
25
+ if not paths:
26
+ raise gr.Error("Upload at least one image, PDF, CBZ, or ZIP file.")
27
+ if len(paths) == 1:
28
+ return paths[0].read_bytes(), paths[0].name, "application/octet-stream"
29
+ stream = io.BytesIO()
30
+ with zipfile.ZipFile(stream, "w", zipfile.ZIP_DEFLATED) as archive:
31
+ for index, path in enumerate(paths):
32
+ archive.writestr(f"{index:04d}{path.suffix.lower()}", path.read_bytes())
33
+ return stream.getvalue(), "uploaded-pages.cbz", "application/vnd.comicbook+zip"
34
+
35
+
36
+ def analyze(files: list[str] | None, reading_direction: str):
37
+ payload, name, mime = _upload_payload(files)
38
+ with httpx.Client(timeout=900) as client:
39
+ response = client.post(
40
+ f"{API_URL}/analyze-comic",
41
+ params={"reading_direction": reading_direction},
42
+ headers=_headers(),
43
+ files={"file": (name, payload, mime)},
44
+ )
45
+ if response.is_error:
46
+ raise gr.Error(f"Analysis failed ({response.status_code}): {response.text[:500]}")
47
+ result = response.json()
48
+ summary = (
49
+ f"Analyzed {result['pages']} pages and {result['panels']} panels. "
50
+ "The current public deployment uses deterministic perception and planning baselines."
51
+ )
52
+ return result["memory"], result["memory"], summary
53
+
54
+
55
+ def _data_url_to_image(value: str) -> Image.Image:
56
+ payload = base64.b64decode(value.split(",", 1)[1])
57
+ return Image.open(io.BytesIO(payload)).convert("RGB")
58
+
59
+
60
+ def generate(
61
+ memory: dict | None,
62
+ pages: int,
63
+ creativity: float,
64
+ dialogue_density: float,
65
+ style_fidelity: float,
66
+ character_fidelity: float,
67
+ reading_direction: str,
68
+ research_mode: bool,
69
+ ):
70
+ if not memory:
71
+ raise gr.Error("Analyze a comic before requesting a continuation.")
72
+ request = {
73
+ "memory": memory,
74
+ "settings": {
75
+ "pages": int(pages),
76
+ "creativity": creativity,
77
+ "dialogue_density": dialogue_density,
78
+ "style_fidelity": style_fidelity,
79
+ "character_fidelity": character_fidelity,
80
+ "reading_direction": reading_direction,
81
+ "research_mode": research_mode,
82
+ "seed": 20260823,
83
+ },
84
+ }
85
+ with httpx.Client(timeout=900) as client:
86
+ response = client.post(
87
+ f"{API_URL}/continue-comic", headers=_headers(), json=request
88
+ )
89
+ if response.is_error:
90
+ raise gr.Error(f"Generation failed ({response.status_code}): {response.text[:500]}")
91
+ result = response.json()
92
+ images = [_data_url_to_image(item) for item in result.get("page_data_urls", [])]
93
+ research = {
94
+ "model_variant": [item.get("model_variant") for item in result.get("scripts", [])],
95
+ "routing": result.get("routing", []),
96
+ "job_id": result.get("job_id"),
97
+ "renderer_status": "RENDERER NOT RUN (visible placeholder panels)",
98
+ }
99
+ return images, result.get("scripts", []), result.get("memory"), research
100
+
101
+
102
+ def record_preference(choice: str, notes: str) -> str:
103
+ if not choice:
104
+ return "Choose a preference before submitting."
105
+ row = {"preference": choice, "notes": notes[:1000]}
106
+ destination = Path(tempfile.gettempdir()) / "opencomic_pairwise.jsonl"
107
+ with destination.open("a", encoding="utf-8") as handle:
108
+ handle.write(json.dumps(row, ensure_ascii=False) + "\n")
109
+ return "Anonymous preference recorded for this research session."
110
+
111
+
112
+ with gr.Blocks(title="OpenComic-Continue") as demo:
113
+ gr.Markdown(
114
+ "# OpenComic-Continue\n"
115
+ "A provenance-first research prototype for structured comic continuation. "
116
+ "Only upload material you own or have permission to transform."
117
+ )
118
+ memory_state = gr.State()
119
+ with gr.Tab("Analyze"):
120
+ uploads = gr.File(
121
+ label="Comic pages, PDF, CBZ, or ZIP",
122
+ file_count="multiple",
123
+ type="filepath",
124
+ )
125
+ direction = gr.Radio(["ltr", "rtl"], value="ltr", label="Reading direction")
126
+ analyze_button = gr.Button("Analyze comic", variant="primary")
127
+ analysis_summary = gr.Markdown()
128
+ memory_json = gr.JSON(label="StoryMemory")
129
+ analyze_button.click(
130
+ analyze,
131
+ [uploads, direction],
132
+ [memory_state, memory_json, analysis_summary],
133
+ )
134
+ with gr.Tab("Continue"):
135
+ with gr.Row():
136
+ page_count = gr.Slider(1, 10, value=1, step=1, label="Pages")
137
+ creativity = gr.Slider(0, 1, value=0.5, label="Creativity")
138
+ dialogue = gr.Slider(0, 1, value=0.5, label="Dialogue density")
139
+ with gr.Row():
140
+ style = gr.Slider(0, 1, value=0.8, label="Style fidelity")
141
+ character = gr.Slider(0, 1, value=0.9, label="Character fidelity")
142
+ generation_direction = gr.Dropdown(
143
+ ["auto", "ltr", "rtl"], value="auto", label="Reading direction"
144
+ )
145
+ research_mode = gr.Checkbox(label="Research mode (show routing and model metadata)")
146
+ generate_button = gr.Button("Generate continuation", variant="primary")
147
+ gallery = gr.Gallery(label="Continuation pages", columns=2, object_fit="contain")
148
+ scripts = gr.JSON(label="Structured scripts")
149
+ updated_memory = gr.JSON(label="Updated StoryMemory")
150
+ research_json = gr.JSON(label="Research metadata")
151
+ generate_button.click(
152
+ generate,
153
+ [
154
+ memory_state,
155
+ page_count,
156
+ creativity,
157
+ dialogue,
158
+ style,
159
+ character,
160
+ generation_direction,
161
+ research_mode,
162
+ ],
163
+ [gallery, scripts, updated_memory, research_json],
164
+ )
165
+ with gr.Tab("Pairwise evaluation"):
166
+ gr.Markdown(
167
+ "Use this form after comparing two model outputs supplied by a study administrator. "
168
+ "No identity is collected."
169
+ )
170
+ preference = gr.Radio(["A", "B", "Tie", "Both invalid"], label="Preferred output")
171
+ preference_notes = gr.Textbox(label="Optional reason", lines=3)
172
+ preference_button = gr.Button("Submit preference")
173
+ preference_status = gr.Markdown()
174
+ preference_button.click(
175
+ record_preference, [preference, preference_notes], preference_status
176
+ )
177
+ gr.Markdown(
178
+ "Generated continuations may be inaccurate or legally restricted. The current demo "
179
+ "labels unexecuted image rendering rather than presenting placeholders as model results."
180
+ )
181
+
182
+
183
+ if __name__ == "__main__":
184
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=5.9,<7
2
+ httpx>=0.27,<1
3
+ pillow>=10.4,<13