Spaces:
Running on Zero
Running on Zero
Restore original English demo UI exactly
Browse filesRestore the original app.py, English presentation helpers and console.js. Preserve the original purple/lime palette, fonts, sidebar and canvas layout. Backend remains the pinned GroundingPI/GroundingPI model on FanLR ZeroGPU. Local 51 tests pass. Live backend target detection completed with 29 targets, 281 output tokens and 13.47 seconds.
- README.md +10 -16
- app.py +68 -68
- assets/console.js +9 -9
- docs/ui_case_pool_22_lock.json +3 -3
- grounding_demo/console.py +68 -23
- tests/test_console.py +8 -8
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: GroundingPI
|
| 3 |
emoji: 🎯
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: green
|
|
@@ -12,15 +12,13 @@ models:
|
|
| 12 |
- GroundingPI/GroundingPI
|
| 13 |
---
|
| 14 |
|
| 15 |
-
# GroundingPI ·
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
## 本地预览
|
| 24 |
|
| 25 |
```bash
|
| 26 |
python3 -m venv .venv
|
|
@@ -28,17 +26,13 @@ python3 -m venv .venv
|
|
| 28 |
GROUNDING_BACKEND=http DEMO_PORT=7862 .venv/bin/python space_app.py
|
| 29 |
```
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
## ZeroGPU 部署
|
| 34 |
-
|
| 35 |
-
上传本目录内容到已有的 Gradio / ZeroGPU Space。本次目标为 `FanLR/GroundingPI-4B`,使用该账号已有的算力。模型仓库公开,不需要新建访问令牌。
|
| 36 |
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
|
| 41 |
-
##
|
| 42 |
|
| 43 |
```bash
|
| 44 |
.venv/bin/python -m unittest discover -s tests -v
|
|
@@ -46,4 +40,4 @@ Static Space 无法在服务器加载模型。没有 ZeroGPU 权限的新账号
|
|
| 46 |
.venv/bin/python scripts/build_bundle.py /absolute/path/studio.zip
|
| 47 |
```
|
| 48 |
|
| 49 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
title: GroundingPI Perception Studio
|
| 3 |
emoji: 🎯
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: green
|
|
|
|
| 12 |
- GroundingPI/GroundingPI
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# GroundingPI · Perception Studio
|
| 16 |
|
| 17 |
+
The original English demo, with its purple and lime palette, fonts, layout, nine task routes and 22 image examples. Supports image upload, canvas zoom, target focus, original-pixel detail, downloads and structured outputs. Video remains disabled.
|
| 18 |
|
| 19 |
+
The model is `GroundingPI/GroundingPI`, pinned at revision `127b0e1c54c68f24bf769a80067521b815b695dd`. Native processors, chat templates and GAM coordinates are preserved. Chinese OCR labels use the bundled Noto Sans CJK font.
|
| 20 |
|
| 21 |
+
## Local preview
|
|
|
|
|
|
|
| 22 |
|
| 23 |
```bash
|
| 24 |
python3 -m venv .venv
|
|
|
|
| 26 |
GROUNDING_BACKEND=http DEMO_PORT=7862 .venv/bin/python space_app.py
|
| 27 |
```
|
| 28 |
|
| 29 |
+
The preview uses `http://127.0.0.1:8000/v1` by default. Without a model service, it reports inference offline and allows browsing examples. It does not substitute stored predictions.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
## ZeroGPU
|
| 32 |
|
| 33 |
+
Deploy this directory to the existing `FanLR/GroundingPI-4B` Gradio Space. The Space and GPU backend belong to FanLR; model weights come from the public GroundingPI account repository. `space_app.py` replaces the old Space model variables with this pinned release. Do not configure both a local model path and a remote model repository.
|
| 34 |
|
| 35 |
+
## Verification
|
| 36 |
|
| 37 |
```bash
|
| 38 |
.venv/bin/python -m unittest discover -s tests -v
|
|
|
|
| 40 |
.venv/bin/python scripts/build_bundle.py /absolute/path/studio.zip
|
| 41 |
```
|
| 42 |
|
| 43 |
+
Local tests and CPU package validation are recorded separately from live GPU inference. Images, fonts and model weights retain their source licenses; see `assets/SOURCES.md` and the model repository.
|
app.py
CHANGED
|
@@ -17,15 +17,15 @@ from grounding_demo.protocol import parse_gam, parse_reference_boxes
|
|
| 17 |
from grounding_demo.render import draw_primitives
|
| 18 |
|
| 19 |
ROUTE_META = {
|
| 20 |
-
"grounding": ("
|
| 21 |
-
"dense": ("
|
| 22 |
-
"referring": ("
|
| 23 |
-
"dense_point": ("
|
| 24 |
-
"referring_point_in_mask": ("
|
| 25 |
-
"dense_point_in_mask": ("
|
| 26 |
-
"visual_prompt": ("
|
| 27 |
-
"ocr": ("
|
| 28 |
-
"gui": ("
|
| 29 |
}
|
| 30 |
CASES = load_cases()
|
| 31 |
BY_ID = {row["id"]: row for row in CASES}
|
|
@@ -62,10 +62,10 @@ def query_update(canonical, route):
|
|
| 62 |
|
| 63 |
def case_meta(row):
|
| 64 |
return (
|
| 65 |
-
"<div class='source-meta'><span>
|
| 66 |
-
f"<b>{html.escape(row['source'])}</b><span>
|
| 67 |
f"<code>{row.get('image_sha256', '')[:16]}</code>"
|
| 68 |
-
"<small>
|
| 69 |
)
|
| 70 |
|
| 71 |
|
|
@@ -90,7 +90,7 @@ def reset_image(path, case_id, route, canonical, meta):
|
|
| 90 |
targets_html([]),
|
| 91 |
"[]",
|
| 92 |
"",
|
| 93 |
-
status_html("
|
| 94 |
task_help_html(route),
|
| 95 |
)
|
| 96 |
|
|
@@ -124,7 +124,7 @@ def choose_gallery(route, evt: gr.SelectData):
|
|
| 124 |
|
| 125 |
def upload_image(path, route, canonical):
|
| 126 |
if not path:
|
| 127 |
-
raise gr.Error("
|
| 128 |
with Image.open(path) as image:
|
| 129 |
image.verify()
|
| 130 |
return reset_image(
|
|
@@ -132,8 +132,8 @@ def upload_image(path, route, canonical):
|
|
| 132 |
None,
|
| 133 |
route,
|
| 134 |
canonical,
|
| 135 |
-
"<div class='source-meta'><span>
|
| 136 |
-
"<small>
|
| 137 |
)
|
| 138 |
|
| 139 |
|
|
@@ -153,7 +153,7 @@ def edit_query(text, route, path):
|
|
| 153 |
targets_html([]),
|
| 154 |
"[]",
|
| 155 |
"",
|
| 156 |
-
status_html("
|
| 157 |
)
|
| 158 |
|
| 159 |
|
|
@@ -178,9 +178,9 @@ def result_bundle(path, canonical, route, raw, elapsed=None, usage=None):
|
|
| 178 |
if not parsed.valid:
|
| 179 |
state = "Protocol error"
|
| 180 |
message = (
|
| 181 |
-
f"
|
| 182 |
if parsed.valid
|
| 183 |
-
else "
|
| 184 |
)
|
| 185 |
return dict(
|
| 186 |
image=rendered,
|
|
@@ -203,15 +203,15 @@ def display_bundle(bundle):
|
|
| 203 |
def inference_error(exc):
|
| 204 |
timed_out = "timeout" in type(exc).__name__.lower()
|
| 205 |
return (
|
| 206 |
-
"
|
| 207 |
if timed_out else
|
| 208 |
-
"
|
| 209 |
)
|
| 210 |
|
| 211 |
|
| 212 |
def run_inference(path, query_text, route):
|
| 213 |
if not path:
|
| 214 |
-
raise gr.Error("
|
| 215 |
canonical = compile_query(query_text, route)
|
| 216 |
try:
|
| 217 |
raw, elapsed, usage = GroundingClient().infer(path, canonical)
|
|
@@ -229,21 +229,21 @@ def run_inference(path, query_text, route):
|
|
| 229 |
return display_bundle(bundle)
|
| 230 |
|
| 231 |
|
| 232 |
-
RUN_LABEL = "
|
| 233 |
|
| 234 |
|
| 235 |
def run_inference_stream(path, query_text, route):
|
| 236 |
"""Expose real request state without an opaque progress mask or synthetic steps."""
|
| 237 |
if not path:
|
| 238 |
-
raise gr.Error("
|
| 239 |
yield (
|
| 240 |
path,
|
| 241 |
-
metrics_html(ROUTE_META[route][0], "
|
| 242 |
targets_html([], pending=True),
|
| 243 |
"[]",
|
| 244 |
"",
|
| 245 |
-
status_html("
|
| 246 |
-
gr.update(value="
|
| 247 |
)
|
| 248 |
try:
|
| 249 |
result = run_inference(path, query_text, route)
|
|
@@ -272,9 +272,9 @@ def service_status():
|
|
| 272 |
ok, detail = GroundingClient().health()
|
| 273 |
return (
|
| 274 |
f"<div class='api-status {'online' if ok else 'offline'}' role='status' aria-live='polite'>"
|
| 275 |
-
f"<i aria-hidden='true'></i><b>{'
|
| 276 |
-
f"<small>{'
|
| 277 |
-
f"<details class='connection-detail'><summary>
|
| 278 |
)
|
| 279 |
|
| 280 |
|
|
@@ -303,7 +303,7 @@ with gr.Blocks(
|
|
| 303 |
css=CSS,
|
| 304 |
js=JS,
|
| 305 |
head=HOST_HEAD,
|
| 306 |
-
title="GroundingPI ·
|
| 307 |
theme=gr.themes.Base(font=["Manrope", "sans-serif"], font_mono=["ui-monospace", "monospace"]),
|
| 308 |
analytics_enabled=False,
|
| 309 |
) as demo:
|
|
@@ -315,52 +315,52 @@ with gr.Blocks(
|
|
| 315 |
gr.HTML(
|
| 316 |
"""<div class="control-brand">
|
| 317 |
<h2>Grounding<span>PI</span></h2>
|
| 318 |
-
<p>
|
| 319 |
</div>"""
|
| 320 |
)
|
| 321 |
with gr.Column(elem_id="control-settings"):
|
| 322 |
with gr.Row(elem_id="control-fields"):
|
| 323 |
gr.HTML(
|
| 324 |
-
"""<div class="media-field"><span>
|
| 325 |
-
<span class="media-option is-active"
|
| 326 |
<button class="media-option" type="button" disabled
|
| 327 |
-
title="
|
| 328 |
elem_id="media-type",
|
| 329 |
)
|
| 330 |
route = gr.Dropdown(
|
| 331 |
[(name, key) for key, (name, _) in ROUTE_META.items()],
|
| 332 |
value=first_route,
|
| 333 |
-
label="
|
| 334 |
filterable=False,
|
| 335 |
elem_id="route-select",
|
| 336 |
)
|
| 337 |
case = gr.Dropdown(
|
| 338 |
route_choices(first_route),
|
| 339 |
value=first_case["id"],
|
| 340 |
-
label="
|
| 341 |
filterable=False,
|
| 342 |
visible=False,
|
| 343 |
elem_id="case-select",
|
| 344 |
)
|
| 345 |
upload = gr.UploadButton(
|
| 346 |
-
"
|
| 347 |
)
|
| 348 |
-
with gr.Accordion("
|
| 349 |
gr.HTML(
|
| 350 |
-
"<div class='runtime-options'><span>
|
| 351 |
-
+ html.escape(os.getenv("GROUNDING_MODEL", "
|
| 352 |
-
+ "</b><span>
|
| 353 |
+ html.escape(os.getenv("GROUNDING_MAX_TOKENS", "1024"))
|
| 354 |
-
+ "</b><small>
|
| 355 |
)
|
| 356 |
run_btn = gr.Button(RUN_LABEL, variant="primary", elem_id="run-button")
|
| 357 |
with gr.Column(elem_id="service-panel"):
|
| 358 |
-
health = gr.HTML("<div class='api-status'>
|
| 359 |
-
health_btn = gr.Button("
|
| 360 |
with gr.Column(scale=7, min_width=400, elem_id="visual-stage"):
|
| 361 |
task_help = gr.HTML(task_help_html(first_route), elem_id="task-guide")
|
| 362 |
with gr.Row(elem_id="stage-toolbar"):
|
| 363 |
-
status = gr.HTML(status_html("
|
| 364 |
with gr.Column(elem_id="canvas-viewport"):
|
| 365 |
stage = gr.Image(
|
| 366 |
value=input_view(str(ROOT / first_case["image"]), first_case["prompt"], first_route),
|
|
@@ -374,18 +374,18 @@ with gr.Blocks(
|
|
| 374 |
show_fullscreen_button=False,
|
| 375 |
)
|
| 376 |
gr.HTML(
|
| 377 |
-
"""<div class="canvas-tools" aria-label="
|
| 378 |
-
<span class="image-dimensions">
|
| 379 |
<div class="zoom-tools">
|
| 380 |
-
<button type="button" data-canvas="out" aria-label="
|
| 381 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 10h10"/></svg></button>
|
| 382 |
-
<output class="zoom-value" aria-label="
|
| 383 |
-
<button type="button" data-canvas="in" aria-label="
|
| 384 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 10h10M10 5v10"/></svg></button>
|
| 385 |
<span class="tool-divider"></span>
|
| 386 |
-
<button type="button" data-canvas="fit" aria-label="
|
| 387 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M7 3H3v4m10-4h4v4M3 13v4h4m10-4v4h-4"/></svg></button>
|
| 388 |
-
<a class="canvas-download" download="grounding.png" aria-label="
|
| 389 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 3v10m-4-4 4 4 4-4M4 14v3h12v-3"/></svg></a>
|
| 390 |
</div>
|
| 391 |
</div>""",
|
|
@@ -394,21 +394,21 @@ with gr.Blocks(
|
|
| 394 |
with gr.Row(elem_id="query-row"):
|
| 395 |
query = gr.Textbox(
|
| 396 |
value=query_from_prompt(first_case["prompt"], first_route),
|
| 397 |
-
label="
|
| 398 |
show_label=False,
|
| 399 |
lines=2,
|
| 400 |
max_lines=4,
|
| 401 |
placeholder=TASK_HELP[first_route][3],
|
| 402 |
elem_id="query-dock",
|
| 403 |
)
|
| 404 |
-
gr.HTML("<p class='query-hint'>Enter
|
| 405 |
mobile_run = gr.Button(RUN_LABEL, variant="primary", elem_id="mobile-run-button")
|
| 406 |
with gr.Column(elem_id="targets-panel"):
|
| 407 |
targets = gr.HTML(targets_html([]), elem_id="targets-content")
|
| 408 |
with gr.Row(elem_id="review-row", equal_height=True):
|
| 409 |
with gr.Column(scale=4, min_width=320, elem_id="sandbox-panel"):
|
| 410 |
gr.HTML(
|
| 411 |
-
"<div class='film-heading'><h2>
|
| 412 |
)
|
| 413 |
gallery = gr.Gallery(
|
| 414 |
value=gallery_items(first_route),
|
|
@@ -422,36 +422,36 @@ with gr.Blocks(
|
|
| 422 |
interactive=False,
|
| 423 |
elem_id="case-gallery",
|
| 424 |
)
|
| 425 |
-
with gr.Accordion("
|
| 426 |
meta = gr.HTML(case_meta(first_case))
|
| 427 |
with gr.Column(scale=6, min_width=420, elem_id="review-summary"):
|
| 428 |
with gr.Row(elem_id="result-columns", equal_height=True):
|
| 429 |
with gr.Column(scale=4, min_width=200, elem_id="metrics-panel"):
|
| 430 |
-
gr.HTML("<h2 class='inspector-subheading'>
|
| 431 |
metrics = gr.HTML(metrics_html(ROUTE_META[first_route][0]))
|
| 432 |
with gr.Column(scale=6, min_width=240, elem_id="sidebar"):
|
| 433 |
gr.HTML(
|
| 434 |
-
"<div class='inspector-heading'><h2>
|
| 435 |
)
|
| 436 |
gr.HTML(
|
| 437 |
-
"<div class='evidence-detail' hidden><div class='detail-heading'><span>
|
| 438 |
elem_id="detail-panel",
|
| 439 |
)
|
| 440 |
-
with gr.Accordion("
|
| 441 |
prompt = gr.Textbox(
|
| 442 |
-
value=first_case["prompt"], label="
|
| 443 |
)
|
| 444 |
gr.HTML(
|
| 445 |
-
"<div class='console-note'>
|
| 446 |
)
|
| 447 |
-
with gr.Accordion("
|
| 448 |
normalized = gr.Code(
|
| 449 |
-
value="[]", language="json", label="
|
| 450 |
)
|
| 451 |
-
with gr.Accordion("
|
| 452 |
-
raw = gr.Textbox(label="
|
| 453 |
gr.HTML(
|
| 454 |
-
"<footer class='studio-footer'><span>GroundingPI ·
|
| 455 |
)
|
| 456 |
|
| 457 |
reset_outputs = [
|
|
|
|
| 17 |
from grounding_demo.render import draw_primitives
|
| 18 |
|
| 19 |
ROUTE_META = {
|
| 20 |
+
"grounding": ("Grounding", "Open-vocabulary object localization"),
|
| 21 |
+
"dense": ("Dense", "Dense instance localization"),
|
| 22 |
+
"referring": ("Referring", "Language-guided referring"),
|
| 23 |
+
"dense_point": ("Dense Point", "Dense point prediction"),
|
| 24 |
+
"referring_point_in_mask": ("Referring Point in Mask", "Point inside a referred target"),
|
| 25 |
+
"dense_point_in_mask": ("Dense Point in Mask", "Point inside multiple targets"),
|
| 26 |
+
"visual_prompt": ("Visual Prompt", "Exemplar-guided localization"),
|
| 27 |
+
"ocr": ("OCR", "Text recognition and localization"),
|
| 28 |
+
"gui": ("GUI", "Interface element grounding"),
|
| 29 |
}
|
| 30 |
CASES = load_cases()
|
| 31 |
BY_ID = {row["id"]: row for row in CASES}
|
|
|
|
| 62 |
|
| 63 |
def case_meta(row):
|
| 64 |
return (
|
| 65 |
+
"<div class='source-meta'><span>TRAINING SOURCE</span>"
|
| 66 |
+
f"<b>{html.escape(row['source'])}</b><span>IMAGE SHA256</span>"
|
| 67 |
f"<code>{row.get('image_sha256', '')[:16]}</code>"
|
| 68 |
+
"<small>Curated research examples for qualitative exploration, not a held-out benchmark. Inference receives only the image and task prompt.</small></div>"
|
| 69 |
)
|
| 70 |
|
| 71 |
|
|
|
|
| 90 |
targets_html([]),
|
| 91 |
"[]",
|
| 92 |
"",
|
| 93 |
+
status_html("Image ready · Run grounding to extract visual evidence"),
|
| 94 |
task_help_html(route),
|
| 95 |
)
|
| 96 |
|
|
|
|
| 124 |
|
| 125 |
def upload_image(path, route, canonical):
|
| 126 |
if not path:
|
| 127 |
+
raise gr.Error("Select an image to continue.")
|
| 128 |
with Image.open(path) as image:
|
| 129 |
image.verify()
|
| 130 |
return reset_image(
|
|
|
|
| 132 |
None,
|
| 133 |
route,
|
| 134 |
canonical,
|
| 135 |
+
"<div class='source-meta'><span>YOUR IMAGE</span><b>Uploaded image</b>"
|
| 136 |
+
"<small>Select a task and describe the visual evidence to extract.</small></div>",
|
| 137 |
)
|
| 138 |
|
| 139 |
|
|
|
|
| 153 |
targets_html([]),
|
| 154 |
"[]",
|
| 155 |
"",
|
| 156 |
+
status_html("Prompt updated · Run grounding to refresh the overlay"),
|
| 157 |
)
|
| 158 |
|
| 159 |
|
|
|
|
| 178 |
if not parsed.valid:
|
| 179 |
state = "Protocol error"
|
| 180 |
message = (
|
| 181 |
+
f"Complete · {len(valid_items)} visual targets"
|
| 182 |
if parsed.valid
|
| 183 |
+
else "Output validation failed · " + "; ".join(parsed.errors)
|
| 184 |
)
|
| 185 |
return dict(
|
| 186 |
image=rendered,
|
|
|
|
| 203 |
def inference_error(exc):
|
| 204 |
timed_out = "timeout" in type(exc).__name__.lower()
|
| 205 |
return (
|
| 206 |
+
"Request timed out. Check the connection and retry; your image and prompt are preserved."
|
| 207 |
if timed_out else
|
| 208 |
+
"Inference unavailable. Check the connection and retry; your image and prompt are preserved."
|
| 209 |
)
|
| 210 |
|
| 211 |
|
| 212 |
def run_inference(path, query_text, route):
|
| 213 |
if not path:
|
| 214 |
+
raise gr.Error("Choose an example or upload an image first.")
|
| 215 |
canonical = compile_query(query_text, route)
|
| 216 |
try:
|
| 217 |
raw, elapsed, usage = GroundingClient().infer(path, canonical)
|
|
|
|
| 229 |
return display_bundle(bundle)
|
| 230 |
|
| 231 |
|
| 232 |
+
RUN_LABEL = "Run Grounding"
|
| 233 |
|
| 234 |
|
| 235 |
def run_inference_stream(path, query_text, route):
|
| 236 |
"""Expose real request state without an opaque progress mask or synthetic steps."""
|
| 237 |
if not path:
|
| 238 |
+
raise gr.Error("Choose an example or upload an image first.")
|
| 239 |
yield (
|
| 240 |
path,
|
| 241 |
+
metrics_html(ROUTE_META[route][0], "Awaiting inference"),
|
| 242 |
targets_html([], pending=True),
|
| 243 |
"[]",
|
| 244 |
"",
|
| 245 |
+
status_html("Awaiting inference", "busy"),
|
| 246 |
+
gr.update(value="Grounding in progress…", interactive=False),
|
| 247 |
)
|
| 248 |
try:
|
| 249 |
result = run_inference(path, query_text, route)
|
|
|
|
| 272 |
ok, detail = GroundingClient().health()
|
| 273 |
return (
|
| 274 |
f"<div class='api-status {'online' if ok else 'offline'}' role='status' aria-live='polite'>"
|
| 275 |
+
f"<i aria-hidden='true'></i><b>{'Inference ready' if ok else 'Inference offline'}</b>"
|
| 276 |
+
f"<small>{'Model loaded. Ready to process your image.' if ok else 'Explore examples while the inference service is unavailable.'}</small></div>"
|
| 277 |
+
f"<details class='connection-detail'><summary>Runtime details</summary><code>{html.escape(detail)}</code></details>"
|
| 278 |
)
|
| 279 |
|
| 280 |
|
|
|
|
| 303 |
css=CSS,
|
| 304 |
js=JS,
|
| 305 |
head=HOST_HEAD,
|
| 306 |
+
title="GroundingPI · Perception Studio",
|
| 307 |
theme=gr.themes.Base(font=["Manrope", "sans-serif"], font_mono=["ui-monospace", "monospace"]),
|
| 308 |
analytics_enabled=False,
|
| 309 |
) as demo:
|
|
|
|
| 315 |
gr.HTML(
|
| 316 |
"""<div class="control-brand">
|
| 317 |
<h2>Grounding<span>PI</span></h2>
|
| 318 |
+
<p>Visual primitives for physical intelligence.</p>
|
| 319 |
</div>"""
|
| 320 |
)
|
| 321 |
with gr.Column(elem_id="control-settings"):
|
| 322 |
with gr.Row(elem_id="control-fields"):
|
| 323 |
gr.HTML(
|
| 324 |
+
"""<div class="media-field"><span>Media Type</span><div>
|
| 325 |
+
<span class="media-option is-active">Image</span>
|
| 326 |
<button class="media-option" type="button" disabled
|
| 327 |
+
title="Video is not available">Video</button></div></div>""",
|
| 328 |
elem_id="media-type",
|
| 329 |
)
|
| 330 |
route = gr.Dropdown(
|
| 331 |
[(name, key) for key, (name, _) in ROUTE_META.items()],
|
| 332 |
value=first_route,
|
| 333 |
+
label="Task Type",
|
| 334 |
filterable=False,
|
| 335 |
elem_id="route-select",
|
| 336 |
)
|
| 337 |
case = gr.Dropdown(
|
| 338 |
route_choices(first_route),
|
| 339 |
value=first_case["id"],
|
| 340 |
+
label="Selected example",
|
| 341 |
filterable=False,
|
| 342 |
visible=False,
|
| 343 |
elem_id="case-select",
|
| 344 |
)
|
| 345 |
upload = gr.UploadButton(
|
| 346 |
+
"Upload Image", file_types=["image"], type="filepath", elem_id="upload-button"
|
| 347 |
)
|
| 348 |
+
with gr.Accordion("Advanced Parameters", open=False, elem_id="advanced-options"):
|
| 349 |
gr.HTML(
|
| 350 |
+
"<div class='runtime-options'><span>Model</span><b>"
|
| 351 |
+
+ html.escape(os.getenv("GROUNDING_MODEL", "grounding-pi-vlm"))
|
| 352 |
+
+ "</b><span>Max output tokens</span><b>"
|
| 353 |
+ html.escape(os.getenv("GROUNDING_MAX_TOKENS", "1024"))
|
| 354 |
+
+ "</b><small>Read-only runtime configuration. Only the current image and task prompt enter inference.</small></div>"
|
| 355 |
)
|
| 356 |
run_btn = gr.Button(RUN_LABEL, variant="primary", elem_id="run-button")
|
| 357 |
with gr.Column(elem_id="service-panel"):
|
| 358 |
+
health = gr.HTML("<div class='api-status'>Checking inference service…</div>")
|
| 359 |
+
health_btn = gr.Button("Refresh connection", size="sm", elem_id="health-button")
|
| 360 |
with gr.Column(scale=7, min_width=400, elem_id="visual-stage"):
|
| 361 |
task_help = gr.HTML(task_help_html(first_route), elem_id="task-guide")
|
| 362 |
with gr.Row(elem_id="stage-toolbar"):
|
| 363 |
+
status = gr.HTML(status_html("Image ready"), elem_id="stage-status")
|
| 364 |
with gr.Column(elem_id="canvas-viewport"):
|
| 365 |
stage = gr.Image(
|
| 366 |
value=input_view(str(ROOT / first_case["image"]), first_case["prompt"], first_route),
|
|
|
|
| 374 |
show_fullscreen_button=False,
|
| 375 |
)
|
| 376 |
gr.HTML(
|
| 377 |
+
"""<div class="canvas-tools" aria-label="Canvas tools">
|
| 378 |
+
<span class="image-dimensions">Image canvas</span>
|
| 379 |
<div class="zoom-tools">
|
| 380 |
+
<button type="button" data-canvas="out" aria-label="Zoom out" title="Zoom out">
|
| 381 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 10h10"/></svg></button>
|
| 382 |
+
<output class="zoom-value" aria-label="Canvas zoom">100%</output>
|
| 383 |
+
<button type="button" data-canvas="in" aria-label="Zoom in" title="Zoom in">
|
| 384 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 10h10M10 5v10"/></svg></button>
|
| 385 |
<span class="tool-divider"></span>
|
| 386 |
+
<button type="button" data-canvas="fit" aria-label="Fit to canvas" title="Fit to canvas">
|
| 387 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M7 3H3v4m10-4h4v4M3 13v4h4m10-4v4h-4"/></svg></button>
|
| 388 |
+
<a class="canvas-download" download="grounding.png" aria-label="Download image" title="Download image">
|
| 389 |
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 3v10m-4-4 4 4 4-4M4 14v3h12v-3"/></svg></a>
|
| 390 |
</div>
|
| 391 |
</div>""",
|
|
|
|
| 394 |
with gr.Row(elem_id="query-row"):
|
| 395 |
query = gr.Textbox(
|
| 396 |
value=query_from_prompt(first_case["prompt"], first_route),
|
| 397 |
+
label="What would you like to locate?",
|
| 398 |
show_label=False,
|
| 399 |
lines=2,
|
| 400 |
max_lines=4,
|
| 401 |
placeholder=TASK_HELP[first_route][3],
|
| 402 |
elem_id="query-dock",
|
| 403 |
)
|
| 404 |
+
gr.HTML("<p class='query-hint'>Enter to run · Shift + Enter for a new line</p>")
|
| 405 |
mobile_run = gr.Button(RUN_LABEL, variant="primary", elem_id="mobile-run-button")
|
| 406 |
with gr.Column(elem_id="targets-panel"):
|
| 407 |
targets = gr.HTML(targets_html([]), elem_id="targets-content")
|
| 408 |
with gr.Row(elem_id="review-row", equal_height=True):
|
| 409 |
with gr.Column(scale=4, min_width=320, elem_id="sandbox-panel"):
|
| 410 |
gr.HTML(
|
| 411 |
+
"<div class='film-heading'><h2>Explore Examples</h2><small>Curated visual scenarios</small></div>"
|
| 412 |
)
|
| 413 |
gallery = gr.Gallery(
|
| 414 |
value=gallery_items(first_route),
|
|
|
|
| 422 |
interactive=False,
|
| 423 |
elem_id="case-gallery",
|
| 424 |
)
|
| 425 |
+
with gr.Accordion("Data provenance", open=False, elem_id="source-details"):
|
| 426 |
meta = gr.HTML(case_meta(first_case))
|
| 427 |
with gr.Column(scale=6, min_width=420, elem_id="review-summary"):
|
| 428 |
with gr.Row(elem_id="result-columns", equal_height=True):
|
| 429 |
with gr.Column(scale=4, min_width=200, elem_id="metrics-panel"):
|
| 430 |
+
gr.HTML("<h2 class='inspector-subheading'>Inference Metrics</h2>")
|
| 431 |
metrics = gr.HTML(metrics_html(ROUTE_META[first_route][0]))
|
| 432 |
with gr.Column(scale=6, min_width=240, elem_id="sidebar"):
|
| 433 |
gr.HTML(
|
| 434 |
+
"<div class='inspector-heading'><h2>Target Detail</h2><span class='live-mark'>Select a category below the canvas to magnify its original pixels.</span></div>"
|
| 435 |
)
|
| 436 |
gr.HTML(
|
| 437 |
+
"<div class='evidence-detail' hidden><div class='detail-heading'><span>Detail View</span><small>Original pixels · magnified</small></div><div class='detail-image'></div><div class='detail-coordinates'></div></div>",
|
| 438 |
elem_id="detail-panel",
|
| 439 |
)
|
| 440 |
+
with gr.Accordion("Prompt Console", open=False, elem_id="prompt-console"):
|
| 441 |
prompt = gr.Textbox(
|
| 442 |
+
value=first_case["prompt"], label="COMPILED PROMPT", lines=3, interactive=False
|
| 443 |
)
|
| 444 |
gr.HTML(
|
| 445 |
+
"<div class='console-note'>Transparent inputs: only this image and compiled prompt are sent to the model. No annotations, training answers, or expected counts are included.</div>"
|
| 446 |
)
|
| 447 |
+
with gr.Accordion("Structured Output", open=False, elem_id="output-console"):
|
| 448 |
normalized = gr.Code(
|
| 449 |
+
value="[]", language="json", label="PARSED COORDINATES · 0–999", interactive=False
|
| 450 |
)
|
| 451 |
+
with gr.Accordion("Native Token Sequence", open=False):
|
| 452 |
+
raw = gr.Textbox(label="RAW GAM SPECIAL TOKENS", lines=4, interactive=False)
|
| 453 |
gr.HTML(
|
| 454 |
+
"<footer class='studio-footer'><span>GroundingPI · Towards Physical Intelligence</span><span>Research demonstration <i>Qualitative examples, not benchmark results.</i></span></footer>"
|
| 455 |
)
|
| 456 |
|
| 457 |
reset_outputs = [
|
assets/console.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
| 43 |
preview.style.backgroundImage = `url(${JSON.stringify(source?.src || image.currentSrc || image.src)})`;
|
| 44 |
preview.style.backgroundSize = `${iw*scale}px ${ih*scale}px`;
|
| 45 |
preview.style.backgroundPosition = `${preview.clientWidth/2-cx*scale}px ${preview.clientHeight/2-cy*scale}px`;
|
| 46 |
-
$('.detail-coordinates').textContent = `
|
| 47 |
}
|
| 48 |
function canvasTransform() {
|
| 49 |
const stage = $('#stage-image'); if (!stage) return;
|
|
@@ -104,21 +104,21 @@
|
|
| 104 |
const caption = document.createElement('div'); caption.className = 'focus-caption';
|
| 105 |
const label = document.createElement('span');
|
| 106 |
label.setAttribute('role', 'status'); label.setAttribute('aria-live', 'polite');
|
| 107 |
-
const evidence = '
|
| 108 |
label.textContent = `${evidence} · ${selected.dataset.label} · ${instanceIndex + 1} / ${coords.length}`;
|
| 109 |
label.title = label.textContent;
|
| 110 |
caption.append(label);
|
| 111 |
-
for (const [action, name, path] of [['prev', '
|
| 112 |
const button = document.createElement('button'); button.type = 'button'; button.dataset.instance = action;
|
| 113 |
button.setAttribute('aria-label', name); button.title = name; button.disabled = coords.length < 2;
|
| 114 |
const icon = svg('svg', {viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true'});
|
| 115 |
icon.append(svg('path', {d: path, stroke: 'currentColor', 'stroke-width': 1.7}));
|
| 116 |
button.append(icon); caption.append(button);
|
| 117 |
}
|
| 118 |
-
const close = document.createElement('button'); close.type = 'button'; close.className = 'focus-close'; close.setAttribute('aria-label', '
|
| 119 |
const icon = svg('svg', { viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true' });
|
| 120 |
icon.append(svg('path', { d: 'm6 6 8 8m0-8-8 8', stroke: 'currentColor', 'stroke-width': 1.7, 'stroke-linecap': 'round' }));
|
| 121 |
-
close.title = '
|
| 122 |
close.append(icon); caption.append(close); stage.append(layer); $('#canvas-viewport').append(caption);
|
| 123 |
if (activeAction) caption.querySelector(`[data-instance=${activeAction}]`)?.focus({preventScroll: true});
|
| 124 |
showDetail(coords);
|
|
@@ -135,7 +135,7 @@
|
|
| 135 |
for (const button of document.querySelectorAll('#run-button,#mobile-run-button')) {
|
| 136 |
button.disabled = value;
|
| 137 |
button.classList.toggle('is-waiting', value);
|
| 138 |
-
if (value) button.setAttribute('aria-label', '
|
| 139 |
else button.removeAttribute('aria-label');
|
| 140 |
}
|
| 141 |
for (const id of ['controls', 'sandbox-panel', 'query-dock', 'upload-button', 'canvas-chrome']) {
|
|
@@ -169,10 +169,10 @@
|
|
| 169 |
if (!ready || waiting) return;
|
| 170 |
previousRunId = $('#stage-status .stage-status')?.dataset.runId || '';
|
| 171 |
clientPending = true;
|
| 172 |
-
clientStatus('
|
| 173 |
watchdog = setTimeout(() => {
|
| 174 |
if (!waiting) return;
|
| 175 |
-
clientStatus('
|
| 176 |
setWaiting(false);
|
| 177 |
}, 330000);
|
| 178 |
};
|
|
@@ -201,7 +201,7 @@
|
|
| 201 |
clearSelection(); resetCanvas(); lastSrc = src;
|
| 202 |
}
|
| 203 |
const dimensions = $('.image-dimensions');
|
| 204 |
-
if (dimensions && image?.naturalWidth) dimensions.textContent = `${image.naturalWidth} × ${image.naturalHeight} px${zoom>1 ? ' /
|
| 205 |
drawSelection();
|
| 206 |
}
|
| 207 |
function onClick(event) {
|
|
|
|
| 43 |
preview.style.backgroundImage = `url(${JSON.stringify(source?.src || image.currentSrc || image.src)})`;
|
| 44 |
preview.style.backgroundSize = `${iw*scale}px ${ih*scale}px`;
|
| 45 |
preview.style.backgroundPosition = `${preview.clientWidth/2-cx*scale}px ${preview.clientHeight/2-cy*scale}px`;
|
| 46 |
+
$('.detail-coordinates').textContent = `Instance ${instanceIndex + 1} / ${coords.length} · [${p.join(', ')}]`;
|
| 47 |
}
|
| 48 |
function canvasTransform() {
|
| 49 |
const stage = $('#stage-image'); if (!stage) return;
|
|
|
|
| 104 |
const caption = document.createElement('div'); caption.className = 'focus-caption';
|
| 105 |
const label = document.createElement('span');
|
| 106 |
label.setAttribute('role', 'status'); label.setAttribute('aria-live', 'polite');
|
| 107 |
+
const evidence = 'Prediction';
|
| 108 |
label.textContent = `${evidence} · ${selected.dataset.label} · ${instanceIndex + 1} / ${coords.length}`;
|
| 109 |
label.title = label.textContent;
|
| 110 |
caption.append(label);
|
| 111 |
+
for (const [action, name, path] of [['prev', 'Previous instance', 'm12 5-5 5 5 5'], ['next', 'Next instance', 'm8 5 5 5-5 5']]) {
|
| 112 |
const button = document.createElement('button'); button.type = 'button'; button.dataset.instance = action;
|
| 113 |
button.setAttribute('aria-label', name); button.title = name; button.disabled = coords.length < 2;
|
| 114 |
const icon = svg('svg', {viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true'});
|
| 115 |
icon.append(svg('path', {d: path, stroke: 'currentColor', 'stroke-width': 1.7}));
|
| 116 |
button.append(icon); caption.append(button);
|
| 117 |
}
|
| 118 |
+
const close = document.createElement('button'); close.type = 'button'; close.className = 'focus-close'; close.setAttribute('aria-label', 'Close target focus');
|
| 119 |
const icon = svg('svg', { viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true' });
|
| 120 |
icon.append(svg('path', { d: 'm6 6 8 8m0-8-8 8', stroke: 'currentColor', 'stroke-width': 1.7, 'stroke-linecap': 'round' }));
|
| 121 |
+
close.title = 'Close focus and return to targets';
|
| 122 |
close.append(icon); caption.append(close); stage.append(layer); $('#canvas-viewport').append(caption);
|
| 123 |
if (activeAction) caption.querySelector(`[data-instance=${activeAction}]`)?.focus({preventScroll: true});
|
| 124 |
showDetail(coords);
|
|
|
|
| 135 |
for (const button of document.querySelectorAll('#run-button,#mobile-run-button')) {
|
| 136 |
button.disabled = value;
|
| 137 |
button.classList.toggle('is-waiting', value);
|
| 138 |
+
if (value) button.setAttribute('aria-label', 'Grounding in progress');
|
| 139 |
else button.removeAttribute('aria-label');
|
| 140 |
}
|
| 141 |
for (const id of ['controls', 'sandbox-panel', 'query-dock', 'upload-button', 'canvas-chrome']) {
|
|
|
|
| 169 |
if (!ready || waiting) return;
|
| 170 |
previousRunId = $('#stage-status .stage-status')?.dataset.runId || '';
|
| 171 |
clientPending = true;
|
| 172 |
+
clientStatus('Request submitted · Awaiting inference', 'busy'); setWaiting(true);
|
| 173 |
watchdog = setTimeout(() => {
|
| 174 |
if (!waiting) return;
|
| 175 |
+
clientStatus('No result received. Check the connection and refresh. The server request has not been cancelled.', 'error');
|
| 176 |
setWaiting(false);
|
| 177 |
}, 330000);
|
| 178 |
};
|
|
|
|
| 201 |
clearSelection(); resetCanvas(); lastSrc = src;
|
| 202 |
}
|
| 203 |
const dimensions = $('.image-dimensions');
|
| 204 |
+
if (dimensions && image?.naturalWidth) dimensions.textContent = `${image.naturalWidth} × ${image.naturalHeight} px${zoom>1 ? ' / drag to pan' : ''}`;
|
| 205 |
drawSelection();
|
| 206 |
}
|
| 207 |
function onClick(event) {
|
docs/ui_case_pool_22_lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
{
|
| 2 |
"files": {
|
| 3 |
-
"app.py": "
|
| 4 |
"assets/Manrope-OFL.txt": "e01b637272e0cbdfb240184dd98ea5cc671556d9894dae2668d92ab2c906787c",
|
| 5 |
"assets/Manrope-variable.ttf": "d0639be45d0af36e798172419d7bd173c4bd4f29e2b76cbb69db1d11bf8b0a40",
|
| 6 |
"assets/NotoSansCJK-OFL.txt": "6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2",
|
| 7 |
"assets/NotoSansCJKsc-Regular.otf": "2c76254f6fc379fddfce0a7e84fb5385bb135d3e399294f6eeb6680d0365b74b",
|
| 8 |
"assets/SOURCES.md": "0299a8a04951a07c896b28168496209ff900e60f6ae0c98bfdc86a7cc47c1712",
|
| 9 |
-
"assets/console.js": "
|
| 10 |
"assets/frosted.css": "393bf5f1c04a49a6e9b17e855e573646489b4add85bc7973eb6bb277686b5c80",
|
| 11 |
"assets/native.css": "24b80e61a37ca92f62a871f30114bf2839290abf9c899242033b1aeef3998c38",
|
| 12 |
"assets/original.css": "7a244c7f65e40797850dcde7d6b6505b1b5de60aa24b129f44b33ae52291fd2e",
|
|
@@ -36,7 +36,7 @@
|
|
| 36 |
"cases/visual_prompt/21_visual_prompt_round5_candidates_gam_visual_dense200_142.jpg": "34101e5a696ca089b3d6026fbafda21ba8a144bb2b4d4f36fd309273ed48e286",
|
| 37 |
"cases/visual_prompt/22_visual_prompt_round5_candidates_gam_visual_dense200_187.jpg": "3daa8e2776a2dd0ab17abebd669d95728f64bfbd063986f14becfb3f1966463a",
|
| 38 |
"grounding_demo/catalog.py": "75e543fed7f0ca018fb41c4b528efc2901aa6a0f05232959297d6d40798f676f",
|
| 39 |
-
"grounding_demo/console.py": "
|
| 40 |
"grounding_demo/protocol.py": "cf043c9f026a1dcd63d55c5393f0d0921cd15eae4a0cc9a42e2834fab23486ec",
|
| 41 |
"grounding_demo/render.py": "3b3fe5901edc41586bf5f1a1c263265d47c0fbe4d2ee4c30697c1bd0f78d484b"
|
| 42 |
},
|
|
|
|
| 1 |
{
|
| 2 |
"files": {
|
| 3 |
+
"app.py": "79282810a5f15138950ffb9d2ff108067f7f73cf52c797a1931920e1fbff40b8",
|
| 4 |
"assets/Manrope-OFL.txt": "e01b637272e0cbdfb240184dd98ea5cc671556d9894dae2668d92ab2c906787c",
|
| 5 |
"assets/Manrope-variable.ttf": "d0639be45d0af36e798172419d7bd173c4bd4f29e2b76cbb69db1d11bf8b0a40",
|
| 6 |
"assets/NotoSansCJK-OFL.txt": "6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2",
|
| 7 |
"assets/NotoSansCJKsc-Regular.otf": "2c76254f6fc379fddfce0a7e84fb5385bb135d3e399294f6eeb6680d0365b74b",
|
| 8 |
"assets/SOURCES.md": "0299a8a04951a07c896b28168496209ff900e60f6ae0c98bfdc86a7cc47c1712",
|
| 9 |
+
"assets/console.js": "3af36a3aeafb72e85018b1c663f624b92877bad0152ee9e264a1c440d689f60e",
|
| 10 |
"assets/frosted.css": "393bf5f1c04a49a6e9b17e855e573646489b4add85bc7973eb6bb277686b5c80",
|
| 11 |
"assets/native.css": "24b80e61a37ca92f62a871f30114bf2839290abf9c899242033b1aeef3998c38",
|
| 12 |
"assets/original.css": "7a244c7f65e40797850dcde7d6b6505b1b5de60aa24b129f44b33ae52291fd2e",
|
|
|
|
| 36 |
"cases/visual_prompt/21_visual_prompt_round5_candidates_gam_visual_dense200_142.jpg": "34101e5a696ca089b3d6026fbafda21ba8a144bb2b4d4f36fd309273ed48e286",
|
| 37 |
"cases/visual_prompt/22_visual_prompt_round5_candidates_gam_visual_dense200_187.jpg": "3daa8e2776a2dd0ab17abebd669d95728f64bfbd063986f14becfb3f1966463a",
|
| 38 |
"grounding_demo/catalog.py": "75e543fed7f0ca018fb41c4b528efc2901aa6a0f05232959297d6d40798f676f",
|
| 39 |
+
"grounding_demo/console.py": "be37f29305a0282b4f7810a83d43f5f6196e66320f5f2faadaf9cfce57d4ccd0",
|
| 40 |
"grounding_demo/protocol.py": "cf043c9f026a1dcd63d55c5393f0d0921cd15eae4a0cc9a42e2834fab23486ec",
|
| 41 |
"grounding_demo/render.py": "3b3fe5901edc41586bf5f1a1c263265d47c0fbe4d2ee4c30697c1bd0f78d484b"
|
| 42 |
},
|
grounding_demo/console.py
CHANGED
|
@@ -23,15 +23,60 @@ CATEGORY_ROUTES = {"grounding", "dense", "layout", "dense_point_in_mask"}
|
|
| 23 |
|
| 24 |
# Presentation only: never alter the released model prompt or expected answer.
|
| 25 |
TASK_HELP = {
|
| 26 |
-
"grounding": [
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
"
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
|
|
@@ -66,12 +111,12 @@ def compile_query(query: str, route: str) -> str:
|
|
| 66 |
|
| 67 |
def metrics_html(route, state="Ready", count=0, elapsed=None, usage=None):
|
| 68 |
rows = [
|
| 69 |
-
("
|
| 70 |
-
("
|
| 71 |
-
("
|
| 72 |
-
("
|
| 73 |
-
("
|
| 74 |
-
("
|
| 75 |
]
|
| 76 |
return (
|
| 77 |
"<dl class='metrics-list'>"
|
|
@@ -82,11 +127,11 @@ def metrics_html(route, state="Ready", count=0, elapsed=None, usage=None):
|
|
| 82 |
|
| 83 |
def targets_html(items, pending=False, image_source=""):
|
| 84 |
if not items:
|
| 85 |
-
title = "
|
| 86 |
hint = (
|
| 87 |
-
"
|
| 88 |
if pending
|
| 89 |
-
else "
|
| 90 |
)
|
| 91 |
return (
|
| 92 |
"<div class='empty-targets'><svg viewBox='0 0 40 40' width='40' height='40' "
|
|
@@ -98,18 +143,18 @@ def targets_html(items, pending=False, image_source=""):
|
|
| 98 |
counts = Counter(item["label"] for item in items)
|
| 99 |
chips = "".join(
|
| 100 |
"<button type='button' class='target-chip' aria-pressed='false' "
|
| 101 |
-
f"aria-label='{html.escape(f'
|
| 102 |
f"data-label='{html.escape(label, quote=True)}' "
|
| 103 |
f"data-coordinates='{html.escape(json.dumps([item['coordinates'] for item in items if item['label'] == label]), quote=True)}'>"
|
| 104 |
f"<i aria-hidden='true' style='--category-color:rgb{PALETTE[idx % len(PALETTE)]}'></i><span>{html.escape(label)}</span><b>{count}</b></button>"
|
| 105 |
for idx, (label, count) in enumerate(counts.items())
|
| 106 |
)
|
| 107 |
-
caption = "
|
| 108 |
return (
|
| 109 |
(f"<img class='focus-source' hidden alt='' src='{html.escape(image_source, quote=True)}'>" if image_source else "")
|
| 110 |
-
+ f"<div class='targets-summary'><b>{len(items)}</b>
|
| 111 |
f"<div class='target-chips'>{chips}</div>"
|
| 112 |
-
"<div class='focus-help'><span>
|
| 113 |
-
"<button type='button' class='clear-focus' hidden>
|
| 114 |
f"<div class='targets-caption'>{caption}</div>"
|
| 115 |
)
|
|
|
|
| 23 |
|
| 24 |
# Presentation only: never alter the released model prompt or expected answer.
|
| 25 |
TASK_HELP = {
|
| 26 |
+
"grounding": [
|
| 27 |
+
"Open-Vocabulary Grounding",
|
| 28 |
+
"Turn category names into visual primitives: what is present, and where it is.",
|
| 29 |
+
"Separate categories with commas. Select a result to inspect its original pixels.",
|
| 30 |
+
"car, person"
|
| 31 |
+
],
|
| 32 |
+
"dense": [
|
| 33 |
+
"Grounding in Dense Scenes",
|
| 34 |
+
"Explore object-level perception in crowded scenes, including small and overlapping instances.",
|
| 35 |
+
"Enter object categories. Inspect overlapping predictions one instance at a time.",
|
| 36 |
+
"person, chair"
|
| 37 |
+
],
|
| 38 |
+
"referring": [
|
| 39 |
+
"Language, Grounded in Context",
|
| 40 |
+
"Link descriptions of appearance, position, and relationships to a specific visual target.",
|
| 41 |
+
"Describe appearance, position, or relationships in a complete phrase.",
|
| 42 |
+
"The person standing beside the table"
|
| 43 |
+
],
|
| 44 |
+
"dense_point": [
|
| 45 |
+
"Dense Object Pointing",
|
| 46 |
+
"Represent multiple objects as localized points: a compact form of visual grounding.",
|
| 47 |
+
"Describe the targets to locate. Points indicate positions, not object boundaries.",
|
| 48 |
+
"All visible people"
|
| 49 |
+
],
|
| 50 |
+
"referring_point_in_mask": [
|
| 51 |
+
"Point Inside a Referred Target",
|
| 52 |
+
"Use a description to predict a point inside the target object.",
|
| 53 |
+
"Describe the target. The output is an image-relative point inside the object.",
|
| 54 |
+
"The person wearing a flotation device"
|
| 55 |
+
],
|
| 56 |
+
"visual_prompt": [
|
| 57 |
+
"Grounding by Visual Example",
|
| 58 |
+
"Use spatial exemplars to identify related instances through a shared grounding interface.",
|
| 59 |
+
"Green INPUT boxes come from your prompt, not model predictions. Coordinates use the 0–999 range.",
|
| 60 |
+
"Given reference boxes <|box_start|><100><100><200><200><|box_end|> ..."
|
| 61 |
+
],
|
| 62 |
+
"ocr": [
|
| 63 |
+
"Reading with Spatial Context",
|
| 64 |
+
"Bind recognized text to its image region, connecting what is read with where it appears.",
|
| 65 |
+
"Use an OCR instruction. Select a transcription to inspect the original pixels.",
|
| 66 |
+
"Read and locate the text in the image"
|
| 67 |
+
],
|
| 68 |
+
"dense_point_in_mask": [
|
| 69 |
+
"Point Inside Multiple Targets",
|
| 70 |
+
"Predict an interior point for each matching instance in a crowded scene.",
|
| 71 |
+
"Separate categories with commas. Each prediction marks a point inside an instance.",
|
| 72 |
+
"van, bus, truck, car, pedestrian"
|
| 73 |
+
],
|
| 74 |
+
"gui": [
|
| 75 |
+
"Grounding Intent in Interfaces",
|
| 76 |
+
"Map an interaction instruction to a visual target within an interface.",
|
| 77 |
+
"Describe the element to select. The demo predicts a point; it does not execute clicks.",
|
| 78 |
+
"Click the search button"
|
| 79 |
+
]
|
| 80 |
}
|
| 81 |
|
| 82 |
|
|
|
|
| 111 |
|
| 112 |
def metrics_html(route, state="Ready", count=0, elapsed=None, usage=None):
|
| 113 |
rows = [
|
| 114 |
+
("Status", state),
|
| 115 |
+
("Task", route),
|
| 116 |
+
("Model", "GroundingPI"),
|
| 117 |
+
("Detections", str(count)),
|
| 118 |
+
("Latency", f"{elapsed:.2f} s" if elapsed is not None else "—"),
|
| 119 |
+
("Output tokens", str((usage or {}).get("completion_tokens", "—"))),
|
| 120 |
]
|
| 121 |
return (
|
| 122 |
"<dl class='metrics-list'>"
|
|
|
|
| 127 |
|
| 128 |
def targets_html(items, pending=False, image_source=""):
|
| 129 |
if not items:
|
| 130 |
+
title = "Awaiting visual evidence" if pending else "Explore a visual target"
|
| 131 |
hint = (
|
| 132 |
+
"Your image stays visible while the model processes the request."
|
| 133 |
if pending
|
| 134 |
+
else "Choose an example, run grounding, then select a category to inspect."
|
| 135 |
)
|
| 136 |
return (
|
| 137 |
"<div class='empty-targets'><svg viewBox='0 0 40 40' width='40' height='40' "
|
|
|
|
| 143 |
counts = Counter(item["label"] for item in items)
|
| 144 |
chips = "".join(
|
| 145 |
"<button type='button' class='target-chip' aria-pressed='false' "
|
| 146 |
+
f"aria-label='{html.escape(f'Inspect {label}, {count} target' + ('s' if count != 1 else ''), quote=True)}' "
|
| 147 |
f"data-label='{html.escape(label, quote=True)}' "
|
| 148 |
f"data-coordinates='{html.escape(json.dumps([item['coordinates'] for item in items if item['label'] == label]), quote=True)}'>"
|
| 149 |
f"<i aria-hidden='true' style='--category-color:rgb{PALETTE[idx % len(PALETTE)]}'></i><span>{html.escape(label)}</span><b>{count}</b></button>"
|
| 150 |
for idx, (label, count) in enumerate(counts.items())
|
| 151 |
)
|
| 152 |
+
caption = "Live model predictions · select a target or category to inspect"
|
| 153 |
return (
|
| 154 |
(f"<img class='focus-source' hidden alt='' src='{html.escape(image_source, quote=True)}'>" if image_source else "")
|
| 155 |
+
+ f"<div class='targets-summary'><b>{len(items)}</b> {'target' if len(items) == 1 else 'targets'} <span>{len(counts)} {'category' if len(counts) == 1 else 'categories'}</span></div>"
|
| 156 |
f"<div class='target-chips'>{chips}</div>"
|
| 157 |
+
"<div class='focus-help'><span>Inspect individual instances</span>"
|
| 158 |
+
"<button type='button' class='clear-focus' hidden>Show all</button></div>"
|
| 159 |
f"<div class='targets-caption'>{caption}</div>"
|
| 160 |
)
|
tests/test_console.py
CHANGED
|
@@ -44,12 +44,12 @@ class ConsoleTest(unittest.TestCase):
|
|
| 44 |
self.assertEqual(len(choices["choices"]), len(app.BY_ROUTE[route]))
|
| 45 |
self.assertEqual(len(gallery["value"]), len(app.BY_ROUTE[route]))
|
| 46 |
self.assertEqual(loaded[8], "[]")
|
| 47 |
-
self.assertIn("
|
| 48 |
|
| 49 |
def test_case_switch_resets_prediction(self):
|
| 50 |
output = app.load_case(app.CASES[1]["id"])
|
| 51 |
self.assertEqual(output[8], "[]")
|
| 52 |
-
self.assertIn("
|
| 53 |
|
| 54 |
def test_upload_cannot_reveal_unrelated_gt(self):
|
| 55 |
row = app.CASES[0]
|
|
@@ -70,7 +70,7 @@ class ConsoleTest(unittest.TestCase):
|
|
| 70 |
str(app.ROOT / row["image"]), row["prompt"], row["route"], valid + invalid
|
| 71 |
)
|
| 72 |
self.assertEqual(result["normalized"], "[]")
|
| 73 |
-
self.assertIn("
|
| 74 |
|
| 75 |
def test_actual_request_uses_latest_query_without_reference(self):
|
| 76 |
row = app.CASES[0]
|
|
@@ -81,7 +81,7 @@ class ConsoleTest(unittest.TestCase):
|
|
| 81 |
infer.assert_called_once_with(path, compile_query("car", row["route"]))
|
| 82 |
self.assertIsInstance(output[0], app.Image.Image)
|
| 83 |
self.assertEqual(len(output), 6)
|
| 84 |
-
self.assertIn("1
|
| 85 |
|
| 86 |
def test_labels_are_html_escaped(self):
|
| 87 |
self.assertIn(
|
|
@@ -97,7 +97,7 @@ class ConsoleTest(unittest.TestCase):
|
|
| 97 |
])
|
| 98 |
self.assertIn(label, result["normalized"])
|
| 99 |
self.assertIn(label, result["targets"])
|
| 100 |
-
self.assertIn("
|
| 101 |
|
| 102 |
def test_focus_buttons_carry_exact_safe_geometry(self):
|
| 103 |
class Buttons(HTMLParser):
|
|
@@ -138,7 +138,7 @@ class ConsoleTest(unittest.TestCase):
|
|
| 138 |
self.assertIn("data-phase='busy'", pending[-2])
|
| 139 |
done = next(stream)
|
| 140 |
self.assertTrue(done[-1]["interactive"])
|
| 141 |
-
self.assertIn("1
|
| 142 |
infer.assert_called_once_with(path, compile_query("car", row["route"]))
|
| 143 |
|
| 144 |
def test_stream_error_releases_action_without_stale_prediction(self):
|
|
@@ -178,14 +178,14 @@ class ConsoleTest(unittest.TestCase):
|
|
| 178 |
row = app.CASES[0]
|
| 179 |
with patch.object(app.GroundingClient, "infer", side_effect=ConnectionError("test detail")):
|
| 180 |
result = app.run_inference(str(app.ROOT / row["image"]), "car", "grounding")
|
| 181 |
-
self.assertIn("
|
| 182 |
self.assertNotIn("test detail", result[-1])
|
| 183 |
self.assertIn("test detail", result[-2])
|
| 184 |
|
| 185 |
def test_connection_status_explicit_and_escaped(self):
|
| 186 |
with patch.object(app.GroundingClient, "health", return_value=(False, "<unsafe>")):
|
| 187 |
output = app.service_status()
|
| 188 |
-
self.assertIn("
|
| 189 |
self.assertIn("<unsafe>", output)
|
| 190 |
|
| 191 |
def test_both_run_buttons_share_real_stream(self):
|
|
|
|
| 44 |
self.assertEqual(len(choices["choices"]), len(app.BY_ROUTE[route]))
|
| 45 |
self.assertEqual(len(gallery["value"]), len(app.BY_ROUTE[route]))
|
| 46 |
self.assertEqual(loaded[8], "[]")
|
| 47 |
+
self.assertIn("Image ready", loaded[10])
|
| 48 |
|
| 49 |
def test_case_switch_resets_prediction(self):
|
| 50 |
output = app.load_case(app.CASES[1]["id"])
|
| 51 |
self.assertEqual(output[8], "[]")
|
| 52 |
+
self.assertIn("Image ready", output[10])
|
| 53 |
|
| 54 |
def test_upload_cannot_reveal_unrelated_gt(self):
|
| 55 |
row = app.CASES[0]
|
|
|
|
| 70 |
str(app.ROOT / row["image"]), row["prompt"], row["route"], valid + invalid
|
| 71 |
)
|
| 72 |
self.assertEqual(result["normalized"], "[]")
|
| 73 |
+
self.assertIn("Protocol error", result["metrics"])
|
| 74 |
|
| 75 |
def test_actual_request_uses_latest_query_without_reference(self):
|
| 76 |
row = app.CASES[0]
|
|
|
|
| 81 |
infer.assert_called_once_with(path, compile_query("car", row["route"]))
|
| 82 |
self.assertIsInstance(output[0], app.Image.Image)
|
| 83 |
self.assertEqual(len(output), 6)
|
| 84 |
+
self.assertIn("1 visual targets", output[-1])
|
| 85 |
|
| 86 |
def test_labels_are_html_escaped(self):
|
| 87 |
self.assertIn(
|
|
|
|
| 97 |
])
|
| 98 |
self.assertIn(label, result["normalized"])
|
| 99 |
self.assertIn(label, result["targets"])
|
| 100 |
+
self.assertIn("Complete", result["status"])
|
| 101 |
|
| 102 |
def test_focus_buttons_carry_exact_safe_geometry(self):
|
| 103 |
class Buttons(HTMLParser):
|
|
|
|
| 138 |
self.assertIn("data-phase='busy'", pending[-2])
|
| 139 |
done = next(stream)
|
| 140 |
self.assertTrue(done[-1]["interactive"])
|
| 141 |
+
self.assertIn("1 visual targets", done[-2])
|
| 142 |
infer.assert_called_once_with(path, compile_query("car", row["route"]))
|
| 143 |
|
| 144 |
def test_stream_error_releases_action_without_stale_prediction(self):
|
|
|
|
| 178 |
row = app.CASES[0]
|
| 179 |
with patch.object(app.GroundingClient, "infer", side_effect=ConnectionError("test detail")):
|
| 180 |
result = app.run_inference(str(app.ROOT / row["image"]), "car", "grounding")
|
| 181 |
+
self.assertIn("Check the connection and retry", result[-1])
|
| 182 |
self.assertNotIn("test detail", result[-1])
|
| 183 |
self.assertIn("test detail", result[-2])
|
| 184 |
|
| 185 |
def test_connection_status_explicit_and_escaped(self):
|
| 186 |
with patch.object(app.GroundingClient, "health", return_value=(False, "<unsafe>")):
|
| 187 |
output = app.service_status()
|
| 188 |
+
self.assertIn("Inference offline", output)
|
| 189 |
self.assertIn("<unsafe>", output)
|
| 190 |
|
| 191 |
def test_both_run_buttons_share_real_stream(self):
|