SynLayers commited on
Commit
49874ec
·
verified ·
1 Parent(s): 0d5468b

Upload demo/app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. demo/app.py +293 -0
demo/app.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ try:
8
+ import spaces
9
+ except ImportError:
10
+ class _SpacesCompat:
11
+ @staticmethod
12
+ def GPU(*decorator_args, **decorator_kwargs):
13
+ if decorator_args and callable(decorator_args[0]) and len(decorator_args) == 1 and not decorator_kwargs:
14
+ return decorator_args[0]
15
+
16
+ def decorator(fn):
17
+ return fn
18
+
19
+ return decorator
20
+
21
+ spaces = _SpacesCompat()
22
+
23
+ import gradio as gr
24
+ import torch
25
+
26
+ CURRENT_FILE = Path(__file__).resolve()
27
+ PROJECT_ROOT = CURRENT_FILE.parents[1]
28
+ for candidate in (CURRENT_FILE.parent, CURRENT_FILE.parents[1]):
29
+ if (candidate / "infer").exists() and (candidate / "models").exists():
30
+ PROJECT_ROOT = candidate
31
+ break
32
+ if str(PROJECT_ROOT) not in sys.path:
33
+ sys.path.insert(0, str(PROJECT_ROOT))
34
+
35
+ from demo.real_world_pipeline import ( # noqa: E402
36
+ DEFAULT_BBOX_MODEL,
37
+ DEFAULT_REAL_CONFIG_PATH,
38
+ DEFAULT_RUN_NAME,
39
+ DEFAULT_WORK_DIR,
40
+ run_real_world_pipeline,
41
+ )
42
+
43
+ DEFAULT_EXAMPLE_DIR = Path(
44
+ os.environ.get(
45
+ "SYNLAYERS_EXAMPLE_DIR",
46
+ "/project/llmsvgen/share/data/kmw_layered_dataset/real_world_inference/layers_real_test_1024",
47
+ )
48
+ )
49
+
50
+
51
+ def read_int_env(name: str, default: int) -> int:
52
+ raw = os.environ.get(name)
53
+ if raw is None:
54
+ return default
55
+ try:
56
+ return int(raw)
57
+ except ValueError:
58
+ return default
59
+
60
+
61
+ ZERO_GPU_SIZE = (os.environ.get("SYNLAYERS_ZERO_GPU_SIZE", "large").strip() or "large").lower()
62
+ ZERO_GPU_DURATION = max(60, read_int_env("SYNLAYERS_ZERO_GPU_DURATION", 900))
63
+
64
+
65
+ def list_example_images(limit: int = 6) -> list[list[str]]:
66
+ if not DEFAULT_EXAMPLE_DIR.exists():
67
+ return []
68
+
69
+ candidates = []
70
+ for ext in ("*.png", "*.jpg", "*.jpeg", "*.webp"):
71
+ candidates.extend(DEFAULT_EXAMPLE_DIR.glob(ext))
72
+ candidates = sorted(candidates)[:limit]
73
+ return [[str(path)] for path in candidates]
74
+
75
+
76
+ def build_gallery(result: dict) -> list[tuple[str, str]]:
77
+ gallery: list[tuple[str, str]] = []
78
+ if result.get("whole_image_rgba"):
79
+ gallery.append((result["whole_image_rgba"], "Whole RGBA"))
80
+ if result.get("background_rgba"):
81
+ gallery.append((result["background_rgba"], "Background RGBA"))
82
+ for idx, path in enumerate(result.get("layer_images", [])):
83
+ gallery.append((path, f"Layer {idx}"))
84
+ return gallery
85
+
86
+
87
+ def get_gpu_name() -> str:
88
+ if not torch.cuda.is_available():
89
+ return "None"
90
+ try:
91
+ return torch.cuda.get_device_name(torch.cuda.current_device())
92
+ except Exception as exc: # pragma: no cover - defensive runtime reporting
93
+ return f"Unavailable ({exc})"
94
+
95
+
96
+ def is_zero_gpu_space() -> bool:
97
+ accelerator = os.environ.get("ACCELERATOR", "").lower()
98
+ return (
99
+ os.environ.get("ZEROGPU_V2", "").lower() == "true"
100
+ or os.environ.get("ZERO_GPU_PATCH_TORCH_DEVICE") == "1"
101
+ or accelerator == "zerogpu"
102
+ or accelerator.startswith("zero")
103
+ )
104
+
105
+
106
+ def get_runtime_status_markdown() -> str:
107
+ accelerator = os.environ.get("ACCELERATOR", "unknown")
108
+ space_id = os.environ.get("SPACE_ID", "local")
109
+ bbox_repo = os.environ.get("SYNLAYERS_BBOX_MODEL_REPO") or os.environ.get("SYNLAYERS_BBOX_MODEL", "(unset)")
110
+ stage2_repo = os.environ.get("SYNLAYERS_STAGE2_MODEL_REPO") or os.environ.get("SYNLAYERS_MODEL_REPO", "(unset)")
111
+ zero_gpu_enabled = is_zero_gpu_space()
112
+
113
+ lines = ["## Runtime Status", f"- `SPACE_ID`: `{space_id}`", f"- `ACCELERATOR`: `{accelerator}`"]
114
+
115
+ if zero_gpu_enabled:
116
+ lines.extend(
117
+ [
118
+ f"- `ZeroGPU mode`: `True`",
119
+ f"- `Requested GPU size`: `{ZERO_GPU_SIZE}`",
120
+ f"- `Requested max duration`: `{ZERO_GPU_DURATION}` seconds",
121
+ f"- `Stage 1 bbox repo/path`: `{bbox_repo}`",
122
+ f"- `Stage 2 repo`: `{stage2_repo}`",
123
+ f"- `CUDA probe outside @spaces.GPU`: `{torch.cuda.is_available()}`",
124
+ "",
125
+ "This Space is configured for Hugging Face ZeroGPU.",
126
+ "A shared H200 GPU is requested on demand when you click `Run Full Pipeline`.",
127
+ "Queueing and quota are managed by Hugging Face ZeroGPU, not by an in-app GPU selector.",
128
+ ]
129
+ )
130
+ else:
131
+ cuda_available = torch.cuda.is_available()
132
+ lines.extend(
133
+ [
134
+ f"- `CUDA available`: `{cuda_available}`",
135
+ f"- `GPU device`: `{get_gpu_name()}`",
136
+ f"- `Stage 1 bbox repo/path`: `{bbox_repo}`",
137
+ f"- `Stage 2 repo`: `{stage2_repo}`",
138
+ "",
139
+ ]
140
+ )
141
+
142
+ if accelerator == "none" or not cuda_available:
143
+ lines.extend(
144
+ [
145
+ "This Space is not currently running with a usable CUDA GPU.",
146
+ "The GPU type must be chosen by the Space owner in Hugging Face `Settings -> Hardware`.",
147
+ "Visitors cannot switch GPUs from inside the Gradio app.",
148
+ ]
149
+ )
150
+ else:
151
+ lines.append("The CUDA runtime is available and the full SynLayers pipeline can run here.")
152
+
153
+ return "\n".join(lines)
154
+
155
+
156
+ @spaces.GPU(duration=ZERO_GPU_DURATION, size=ZERO_GPU_SIZE)
157
+ def run_demo_inference(
158
+ image_path: str,
159
+ sample_name: str,
160
+ max_new_tokens: int,
161
+ seed_value: float,
162
+ ) -> dict:
163
+ seed = int(seed_value) if seed_value >= 0 else None
164
+ return run_real_world_pipeline(
165
+ image_path=image_path,
166
+ sample_name=sample_name or None,
167
+ work_dir=DEFAULT_WORK_DIR,
168
+ bbox_model=DEFAULT_BBOX_MODEL,
169
+ config_path=DEFAULT_REAL_CONFIG_PATH,
170
+ max_new_tokens=int(max_new_tokens),
171
+ seed=seed,
172
+ run_name=DEFAULT_RUN_NAME,
173
+ )
174
+
175
+
176
+ def run_demo(
177
+ image_path: str,
178
+ sample_name: str,
179
+ max_new_tokens: int,
180
+ seed_value: float,
181
+ ):
182
+ if not image_path:
183
+ raise gr.Error("Please upload an input image first.")
184
+
185
+ try:
186
+ result = run_demo_inference(
187
+ image_path=image_path,
188
+ sample_name=sample_name,
189
+ max_new_tokens=max_new_tokens,
190
+ seed_value=seed_value,
191
+ )
192
+ except Exception as exc:
193
+ raise gr.Error(str(exc)) from exc
194
+
195
+ return (
196
+ result["bbox_visualization"],
197
+ result["merged_image"],
198
+ result["bbox_record"].get("whole_caption", ""),
199
+ result["bbox_record"],
200
+ result["metadata"],
201
+ build_gallery(result),
202
+ result["archive_path"],
203
+ result["case_dir"],
204
+ )
205
+
206
+
207
+ with gr.Blocks(title="SynLayers Real-World Demo") as demo:
208
+ gr.Markdown(
209
+ """
210
+ # SynLayers Real-World Decomposition
211
+
212
+ Upload a single image and run the full pipeline in one step:
213
+ 1. VLM for whole-caption + bounding-box detection
214
+ 2. SynLayers real-image layer decomposition
215
+
216
+ This Space can run either on a dedicated GPU Space or on Hugging Face ZeroGPU.
217
+ The first request may take time while model assets are loaded from Hugging Face.
218
+
219
+ In ZeroGPU mode, a shared GPU is requested only while inference is running.
220
+ """
221
+ )
222
+ runtime_status = gr.Markdown(get_runtime_status_markdown())
223
+ refresh_status_button = gr.Button("Refresh Runtime Status")
224
+
225
+ with gr.Row():
226
+ with gr.Column(scale=1):
227
+ image_input = gr.Image(type="filepath", label="Input Image")
228
+ sample_name_input = gr.Textbox(
229
+ label="Optional Sample Name",
230
+ placeholder="Leave empty to use the uploaded filename",
231
+ )
232
+ max_new_tokens_input = gr.Slider(
233
+ minimum=128,
234
+ maximum=2048,
235
+ value=1024,
236
+ step=64,
237
+ label="VLM Max New Tokens",
238
+ )
239
+ seed_input = gr.Number(
240
+ value=42,
241
+ precision=0,
242
+ label="Seed (-1 keeps config default)",
243
+ )
244
+ run_button = gr.Button("Run Full Pipeline", variant="primary")
245
+
246
+ with gr.Column(scale=1):
247
+ bbox_vis_output = gr.Image(type="filepath", label="Detected Bounding Boxes")
248
+ merged_output = gr.Image(type="filepath", label="Merged Decomposition")
249
+
250
+ caption_output = gr.Textbox(label="Whole Caption", lines=6)
251
+ with gr.Row():
252
+ bbox_json_output = gr.JSON(label="BBox JSON")
253
+ meta_json_output = gr.JSON(label="Inference Metadata")
254
+ layer_gallery = gr.Gallery(label="Predicted Layers", columns=4, height="auto")
255
+ with gr.Row():
256
+ archive_output = gr.File(label="Download Result Bundle")
257
+ case_dir_output = gr.Textbox(label="Saved Case Directory")
258
+
259
+ examples = list_example_images()
260
+ if examples:
261
+ gr.Examples(examples=examples, inputs=[image_input], label="Example Images")
262
+
263
+ refresh_status_button.click(
264
+ fn=get_runtime_status_markdown,
265
+ outputs=runtime_status,
266
+ )
267
+
268
+ run_button.click(
269
+ fn=run_demo,
270
+ inputs=[
271
+ image_input,
272
+ sample_name_input,
273
+ max_new_tokens_input,
274
+ seed_input,
275
+ ],
276
+ outputs=[
277
+ bbox_vis_output,
278
+ merged_output,
279
+ caption_output,
280
+ bbox_json_output,
281
+ meta_json_output,
282
+ layer_gallery,
283
+ archive_output,
284
+ case_dir_output,
285
+ ],
286
+ )
287
+
288
+
289
+ if __name__ == "__main__":
290
+ demo.queue().launch(
291
+ server_name="0.0.0.0",
292
+ server_port=int(os.environ.get("PORT", "7860")),
293
+ )