Spaces:
Running on Zero
Running on Zero
Deploy provenance-first OpenComic-Continue research UI
Browse files- README.md +9 -2
- __pycache__/app.cpython-312.pyc +0 -0
- 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
Only upload content you own or are permitted to transform.
|
|
|
|
|
|
|
|
|
| 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={
|
|
|
|
|
|
|
|
|
|
| 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
|
| 73 |
-
session: dict
|
| 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 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
"
|
|
|
|
|
|
|
| 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":
|
| 96 |
"research_mode": research_mode,
|
| 97 |
"seed": 20260823,
|
| 98 |
},
|
| 99 |
}
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 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
|
| 161 |
-
"
|
|
|
|
|
|
|
| 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 |
-
"
|
| 203 |
-
"
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|