FanLR commited on
Commit
efa9c6f
·
verified ·
1 Parent(s): 8e0d79f

Restore native Gradio UI and use GroundingPI account model

Browse files

Use the native light Gradio theme with Chinese UI and disabled Video. Preserve 9 task routes, 22 image cases, canvas controls and CJK annotation fonts. Pin GroundingPI/GroundingPI at 127b0e1c54c68f24bf769a80067521b815b695dd. Update native architecture contract and Transformers 5.7 compatibility. Local: 51 tests, release fingerprint and CPU model/processor import passed; GPU verification follows deployment.

.env.example CHANGED
@@ -1,7 +1,7 @@
1
  # Hugging Face Space -> Settings -> Variables
2
  GROUNDING_BACKEND=zerogpu
3
- GROUNDING_MODEL_REPO=XPENG-AI/GroundingPI-4B
4
- GROUNDING_MODEL_REVISION=REPLACE_WITH_40_CHARACTER_MODEL_COMMIT_SHA
5
  GROUNDING_MAX_TOKENS=1024
6
  GROUNDING_GPU_SECONDS=120
7
 
 
1
  # Hugging Face Space -> Settings -> Variables
2
  GROUNDING_BACKEND=zerogpu
3
+ GROUNDING_MODEL_REPO=GroundingPI/GroundingPI
4
+ GROUNDING_MODEL_REVISION=127b0e1c54c68f24bf769a80067521b815b695dd
5
  GROUNDING_MAX_TOKENS=1024
6
  GROUNDING_GPU_SECONDS=120
7
 
README.md CHANGED
@@ -1,128 +1,49 @@
1
  ---
2
- title: GroundingPI
3
  emoji: 🎯
4
- colorFrom: purple
5
  colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  python_version: 3.12.12
9
  app_file: space_app.py
10
  pinned: false
11
- license: cc-by-nc-sa-4.0
12
  models:
13
- - XPENG-AI/GroundingPI-4B
14
  ---
15
 
16
- # GroundingPI · Precise Visual Grounding
17
 
18
- **From language to spatial evidence.**
19
 
20
- An interactive research workspace for locating objects, reading text and exploring
21
- interaction targets. Choose a task, provide an image and a prompt, then inspect
22
- the model's predictions directly on the image.
23
 
24
- Model: [XPENG-AI/GroundingPI-4B](https://huggingface.co/XPENG-AI/GroundingPI-4B).
25
- This private preview is hosted at `FanLR/GroundingPI-4B`. Two real HF ZeroGPU grounding requests passed on 2026-09-16; full task coverage
26
- remains pending. Licensing and asset permissions are pending:
27
- see [license status](LICENSE_PENDING.md).
28
 
29
- ## Explore visual evidence
30
-
31
- | Task | Interaction |
32
- | --- | --- |
33
- | Grounding | Enter object categories separated by commas |
34
- | Dense | Localize instances in crowded or small-object scenes |
35
- | Referring | Describe a particular object or person |
36
- | Dense Point | Locate multiple targets as image-relative points |
37
- | Referring Point in Mask | Predict a point inside a target described by a referring expression |
38
- | Dense Point in Mask | Predict an interior point for each matching instance |
39
- | Visual Prompt | Supply an input region as a visual condition |
40
- | OCR | Read text with its associated location |
41
- | GUI | Point to an interface target; no UI action is executed |
42
-
43
- 1. Select a task and a curated example, or upload your own image.
44
- 2. Write the request in the prompt field and select **Run Grounding**.
45
- 3. Select a category to focus individual instances. Use next/previous, zoom and
46
- fit-to-canvas; press Escape to leave focus.
47
- 4. Inspect the compiled prompt, normalized coordinates and raw GAM tokens.
48
-
49
- Predictions are drawn directly on the canvas. There is no original/model/reference
50
- view switch. Large regions use unfilled outlines; labels avoid obscuring small
51
- targets. The renderer does not delete, merge or relocate predictions for appearance.
52
- Visual Prompt's marked region is an **input condition**, not a model prediction.
53
-
54
- The gallery contains 22 original images from the selected candidate pool, with
55
- their corresponding task prompts. Original image formats and dimensions are preserved.
56
- Examples are qualitative scenarios selected from training pools, not a held-out
57
- benchmark. An operational demo is not evidence of a reported accuracy or speedup.
58
-
59
- ## Run locally
60
-
61
- Use a separate Python 3.12 environment:
62
 
63
  ```bash
64
  python3 -m venv .venv
65
  .venv/bin/python -m pip install -r requirements.txt
66
- .venv/bin/python -m pip check
67
- .venv/bin/python -m unittest discover -s tests -v
68
- .venv/bin/python scripts/verify_ui.py
69
- bash scripts/start.sh
70
  ```
71
 
72
- Without a model source, the page is an explicitly disconnected UI preview.
73
- For a compatible NVIDIA GPU, set `GROUNDING_MODEL_PATH=/absolute/model/directory`
74
- before startup. Do not also set `GROUNDING_MODEL_REPO`. Local execution does not
75
- automatically allocate Hugging Face GPU resources.
76
 
77
- The model is custom Oxygen-VL-v3 with a Kimi-K3 vision component, not a standard
78
- Qwen3-VL package. Keep the checkpoint's tokenizer, processor, template and custom
79
- code. Native generation uses BF16, SDPA and greedy decoding; vLLM/SGLang are not
80
- required for this Space runtime.
81
 
82
- ## Deploy a private Hugging Face Space
83
 
84
- Follow [the deployment guide](docs/HF_SPACE_DEPLOYMENT.md). Upload the contents of
85
- this directory to a **Gradio Space**, not the parent release workspace and not a ZIP.
86
 
87
- Configure `GROUNDING_BACKEND=zerogpu`, `GROUNDING_MODEL_REPO` and
88
- `GROUNDING_MODEL_REVISION` with the reviewed full 40-character model commit SHA.
89
- Store a scoped read-only `HF_TOKEN` as a Space **Secret** for private weights.
90
- Do not put credentials in code or Variables.
91
 
92
- The model is initialized once at process startup. ZeroGPU leases GPU compute for
93
- inference; it is not an instruction to reload model weights on every request.
94
- See [runtime design](docs/ZEROGPU_DEPLOYMENT.md).
95
 
96
- ## Verification boundary
97
-
98
- The existing PPU implementation has completed real generation and UI checks.
99
- This package includes automated regressions for prompts, parsing, stale-result
100
- clearing, error recovery and overlay geometry. **Two private HF ZeroGPU grounding smoke checks passed on 2026-09-16.**
101
- See [the smoke-check receipt](docs/HF_PRIVATE_SMOKE.md). Full route coverage and
102
- benchmark accuracy remain unverified. See [verification](docs/VERIFICATION.md).
103
-
104
- The default queue is single-concurrency with at most eight waiting requests.
105
- Uploads are limited to 30 MiB / 50 megapixels, and input plus output to 8,192 tokens.
106
- The default output budget is 1,024 tokens. Truncation is reported rather than
107
- presented as a complete prediction. Failed calls never fall back to annotations.
108
-
109
- ## Project map
110
-
111
- ```text
112
- space_app.py Startup and queue
113
- app.py + assets/ Canvas, controls and presentation
114
- grounding_demo/ Native inference, prompt compiler, GAM parser and overlays
115
- cases/ + data/ Qualitative examples and provenance metadata
116
- scripts/ Launch, package checks and regression helpers
117
- tests/ Automated behavior and integrity checks
118
- docs/ Deployment, runtime and verification notes
119
  ```
120
 
121
- ## Responsible release
122
-
123
- Do not upload confidential or personal images without permission. The demo is not
124
- a safety-critical controller, and its coordinates are not calibrated robot/world
125
- coordinates. Source data licenses, example redistribution, XPENG branding and final
126
- model/code licenses require owner review. Font and asset notices are in
127
- [assets/SOURCES.md](assets/SOURCES.md). Paper citation and approved benchmark results
128
- will be added after confirmation; they are not invented in this release candidate.
 
1
  ---
2
+ title: GroundingPI 视觉感知工作台
3
  emoji: 🎯
4
+ colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  python_version: 3.12.12
9
  app_file: space_app.py
10
  pinned: false
 
11
  models:
12
+ - GroundingPI/GroundingPI
13
  ---
14
 
15
+ # GroundingPI · 视觉感知工作台
16
 
17
+ 中文视觉感知 demo。支持目标检测、密集检测、描述定位、三类点定位、视觉提示、OCR 与 GUI 定位,共 9 类任务、22 张原始案例。
18
 
19
+ 模型固定为公开的 `GroundingPI/GroundingPI`,修订版本为 `127b0e1c54c68f24bf769a80067521b815b695dd`。保留模型原生处理器、聊天模板和 GAM 输出协议,中文 OCR 标签使用随项目打包的 Noto Sans CJK。
 
 
20
 
21
+ 页面支持图像上传、案例切换、画布缩放、结果聚焦、逐实例查看、原图局部放大、图片下载、结构化坐标与原始输出。视频入口保留为不可选状态。案例提示词和坐标协议与原 demo 一致。
 
 
 
22
 
23
+ ## 本地预览
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  ```bash
26
  python3 -m venv .venv
27
  .venv/bin/python -m pip install -r requirements.txt
28
+ GROUNDING_BACKEND=http DEMO_PORT=7862 .venv/bin/python space_app.py
 
 
 
29
  ```
30
 
31
+ 预览默认 HTTP 地址为 `http://127.0.0.1:8000/v1`。未启动模型服务时,页面明确显示“推理未连接”,可以浏览图像与案例,不会展示伪造预测。已有推理服务可通过 `GROUNDING_API_BASE` 和 `GROUNDING_MODEL` 配置。
 
 
 
32
 
33
+ ## ZeroGPU 部署
 
 
 
34
 
35
+ 上传本目录内容到已有的 Gradio / ZeroGPU Space。本次目标为 `FanLR/GroundingPI-4B`,使用该账号已有的算力。模型仓库公开,不需要新建访问令牌。
36
 
37
+ `space_app.py` 固定加载上述模型版本,并替换原 Space 的旧模型变量。不要���时设置本地模型目录与远程模型仓库。
 
38
 
39
+ Static Space 无法在服务器加载模型。没有 ZeroGPU 权限的新账号可以发布前端,但完整 Gradio 应用需要计算后端。本次直接复用原 FanLR Space,避免跨账号访问私有服务的问题。
 
 
 
40
 
41
+ ## 验证
 
 
42
 
43
+ ```bash
44
+ .venv/bin/python -m unittest discover -s tests -v
45
+ .venv/bin/python scripts/verify_ui.py
46
+ .venv/bin/python scripts/build_bundle.py /absolute/path/studio.zip
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  ```
48
 
49
+ 本地验证与线上实际 GPU 推理分开记录。尚未完成的线上检查不会标注为通过。图片、模型及字体分别遵循其来源许可;见 `assets/SOURCES.md` 与模型仓库许可。
 
 
 
 
 
 
 
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": ("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,10 +62,10 @@ def query_update(canonical, route):
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,7 +90,7 @@ def reset_image(path, case_id, route, canonical, meta):
90
  targets_html([]),
91
  "[]",
92
  "",
93
- status_html("Image ready · Run grounding to extract visual evidence"),
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("Select an image to continue.")
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>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,7 +153,7 @@ def edit_query(text, route, path):
153
  targets_html([]),
154
  "[]",
155
  "",
156
- status_html("Prompt updated · Run grounding to refresh the overlay"),
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"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,15 +203,15 @@ def display_bundle(bundle):
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,21 +229,21 @@ def run_inference(path, query_text, route):
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,28 +272,16 @@ 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>{'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
 
281
- CSS = (ROOT / "assets" / "paper.css").read_text(encoding="utf-8")
282
- CSS += "\n" + (ROOT / "assets" / "original.css").read_text(encoding="utf-8")
283
- CSS += "\n" + (ROOT / "assets" / "studio.css").read_text(encoding="utf-8")
284
- CSS = CSS.replace(
285
- "__MANROPE_DATA__",
286
- base64.b64encode((ROOT / "assets" / "Manrope-variable.ttf").read_bytes()).decode(),
287
- )
288
- # Gradio 5 scopes css= rules inside .contain, including media-query selectors.
289
- # Host sizing must live in head; workbench styling remains scoped.
290
- HOST_HEAD = """<style id="grounding-host-layout">
291
- gradio-app:has(#console) .gradio-container { width:100%!important; max-width:1440px!important; min-width:0!important; box-sizing:border-box; overflow:clip!important; padding:28px 32px 32px!important; margin:0 auto!important; }
292
- gradio-app:has(#console) .gradio-container>main { width:100%!important; max-width:none!important; min-width:0!important; padding:0!important; }
293
- gradio-app:has(#console) .gradio-container .contain { max-width:none!important; }
294
- gradio-app:has(#console) footer:not(.studio-footer) { display:none!important; }
295
- @media(max-width:760px) { gradio-app:has(#console) .gradio-container { padding:16px 14px!important; } }
296
- @media(max-width:430px) { gradio-app:has(#console) .gradio-container { padding:12px 10px!important; } }
297
  </style>"""
298
  JS = (ROOT / "assets" / "console.js").read_text(encoding="utf-8")
299
  first_route = "grounding"
@@ -303,89 +291,85 @@ with gr.Blocks(
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:
310
  image_state = gr.State(str(ROOT / first_case["image"]))
311
  case_state = gr.State(first_case["id"])
 
312
  with gr.Column(elem_id="console"):
313
  with gr.Row(elem_id="workspace-top", equal_height=True):
314
  with gr.Column(scale=3, min_width=280, elem_id="controls"):
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),
367
  type="filepath",
368
  interactive=False,
369
  show_label=False,
370
- height=400,
371
  elem_id="stage-image",
372
  show_download_button=False,
373
  show_share_button=False,
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,27 +378,27 @@ 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="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),
415
  columns=3,
416
  rows=1,
417
- height=170,
418
  show_label=False,
419
  allow_preview=False,
420
  object_fit="cover",
@@ -422,36 +406,36 @@ with gr.Blocks(
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 = [
 
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": ("文字识别 OCR", "识别文字并定位文本区域"),
28
+ "gui": ("界面定位 GUI", "将操作意图映射到界面元素"),
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>样例来源</span>"
66
+ f"<b>{html.escape(row['source'])}</b><span>图像校验值</span>"
67
  f"<code>{row.get('image_sha256', '')[:16]}</code>"
68
+ "<small>精选案例用于体验不同任务。模型仅接收当前图像与任务提示词。</small></div>"
69
  )
70
 
71
 
 
90
  targets_html([]),
91
  "[]",
92
  "",
93
+ status_html("图像已就绪 · 点击开始解析"),
94
  task_help_html(route),
95
  )
96
 
 
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
  None,
133
  route,
134
  canonical,
135
+ "<div class='source-meta'><span>你的图像</span><b>已上传图像</b>"
136
+ "<small>选择任务,描述你希望识别或定位的内容。</small></div>",
137
  )
138
 
139
 
 
153
  targets_html([]),
154
  "[]",
155
  "",
156
+ status_html("提示词已更新 · 再次解析以刷新结果"),
157
  )
158
 
159
 
 
178
  if not parsed.valid:
179
  state = "Protocol error"
180
  message = (
181
+ f"解析完成 · {len(valid_items)} 个视觉目标"
182
  if parsed.valid
183
+ else "输出校验未通过 · " + "; ".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
+ "请求超时,请检查连接后重试。图像与提示词已保留。"
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
  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("等待推理", "busy"),
246
+ gr.update(value="正在解析…", 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>{'模型已就绪' if ok else '推理未连接'}</b>"
276
+ f"<small>{'随时开始,探索图像中的目标。' if ok else '可以浏览案例;连接推理服务后即可解析。'}</small></div>"
277
+ f"<details class='connection-detail'><summary>运行详情</summary><code>{html.escape(detail)}</code></details>"
278
  )
279
 
280
 
281
+ CSS = (ROOT / "assets" / "native.css").read_text(encoding="utf-8")
282
+ HOST_HEAD = """<style>
283
+ gradio-app:has(#console) .gradio-container {max-width:1280px!important;margin:0 auto!important;padding:24px!important;}
284
+ @media(max-width:760px){gradio-app:has(#console) .gradio-container{padding:14px!important;}}
 
 
 
 
 
 
 
 
 
 
 
 
285
  </style>"""
286
  JS = (ROOT / "assets" / "console.js").read_text(encoding="utf-8")
287
  first_route = "grounding"
 
291
  css=CSS,
292
  js=JS,
293
  head=HOST_HEAD,
294
+ title="GroundingPI · 视觉感知工作台",
295
+ theme=gr.themes.Default(font=["Arial", "PingFang SC", "Microsoft YaHei", "sans-serif"], font_mono=["ui-monospace", "monospace"]),
296
  analytics_enabled=False,
297
  ) as demo:
298
  image_state = gr.State(str(ROOT / first_case["image"]))
299
  case_state = gr.State(first_case["id"])
300
+ gr.Markdown("# GroundingPI\n视觉定位、文字识别与交互目标探索 · 9 类任务 / 22 张案例")
301
  with gr.Column(elem_id="console"):
302
  with gr.Row(elem_id="workspace-top", equal_height=True):
303
  with gr.Column(scale=3, min_width=280, elem_id="controls"):
304
+ gr.Markdown("### 输入")
 
 
 
 
 
305
  with gr.Column(elem_id="control-settings"):
306
  with gr.Row(elem_id="control-fields"):
307
  gr.HTML(
308
+ """<div class="media-field"><span>输入类型</span><div>
309
+ <span class="media-option is-active" >图像</span>
310
  <button class="media-option" type="button" disabled
311
+ title="暂未开放视频解析">视频</button></div></div>""",
312
  elem_id="media-type",
313
  )
314
  route = gr.Dropdown(
315
  [(name, key) for key, (name, _) in ROUTE_META.items()],
316
  value=first_route,
317
+ label="选择任务",
318
  filterable=False,
319
  elem_id="route-select",
320
  )
321
  case = gr.Dropdown(
322
  route_choices(first_route),
323
  value=first_case["id"],
324
+ label="已选案例",
325
  filterable=False,
326
  visible=False,
327
  elem_id="case-select",
328
  )
329
  upload = gr.UploadButton(
330
+ "上传图像", file_types=["image"], type="filepath", elem_id="upload-button"
331
  )
332
+ with gr.Accordion("运行配置", open=False, elem_id="advanced-options"):
333
  gr.HTML(
334
+ "<div class='runtime-options'><span>模型</span><b>"
335
+ + html.escape(os.getenv("GROUNDING_MODEL", "GroundingPI"))
336
+ + "</b><span>最大输出长度</span><b>"
337
  + html.escape(os.getenv("GROUNDING_MAX_TOKENS", "1024"))
338
+ + "</b><small>当前模型的运行配置。</small></div>"
339
  )
340
  run_btn = gr.Button(RUN_LABEL, variant="primary", elem_id="run-button")
341
  with gr.Column(elem_id="service-panel"):
342
+ health = gr.HTML("<div class='api-status'>正在检查推理连接…</div>")
343
+ health_btn = gr.Button("刷新连接", size="sm", elem_id="health-button")
344
  with gr.Column(scale=7, min_width=400, elem_id="visual-stage"):
345
  task_help = gr.HTML(task_help_html(first_route), elem_id="task-guide")
346
  with gr.Row(elem_id="stage-toolbar"):
347
+ status = gr.HTML(status_html("图像已就绪"), elem_id="stage-status")
348
  with gr.Column(elem_id="canvas-viewport"):
349
  stage = gr.Image(
350
  value=input_view(str(ROOT / first_case["image"]), first_case["prompt"], first_route),
351
  type="filepath",
352
  interactive=False,
353
  show_label=False,
354
+ height=440,
355
  elem_id="stage-image",
356
  show_download_button=False,
357
  show_share_button=False,
358
  show_fullscreen_button=False,
359
  )
360
  gr.HTML(
361
+ """<div class="canvas-tools" aria-label="画布工具">
362
+ <span class="image-dimensions">图像画布</span>
363
  <div class="zoom-tools">
364
+ <button type="button" data-canvas="out" aria-label="缩小" title="缩小">
365
  <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 10h10"/></svg></button>
366
+ <output class="zoom-value" aria-label="画布缩放">100%</output>
367
+ <button type="button" data-canvas="in" aria-label="放大" title="放大">
368
  <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 10h10M10 5v10"/></svg></button>
369
  <span class="tool-divider"></span>
370
+ <button type="button" data-canvas="fit" aria-label="适应画布" title="适应画布">
371
  <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M7 3H3v4m10-4h4v4M3 13v4h4m10-4v4h-4"/></svg></button>
372
+ <a class="canvas-download" download="grounding.png" aria-label="下载图像" title="下载图像">
373
  <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 3v10m-4-4 4 4 4-4M4 14v3h12v-3"/></svg></a>
374
  </div>
375
  </div>""",
 
378
  with gr.Row(elem_id="query-row"):
379
  query = gr.Textbox(
380
  value=query_from_prompt(first_case["prompt"], first_route),
381
+ label="描述你的目标",
382
+ show_label=True,
383
  lines=2,
384
  max_lines=4,
385
  placeholder=TASK_HELP[first_route][3],
386
  elem_id="query-dock",
387
  )
388
+ gr.HTML("<p class='query-hint'>Enter 开始解析 · Shift + Enter 换行</p>")
389
  mobile_run = gr.Button(RUN_LABEL, variant="primary", elem_id="mobile-run-button")
390
  with gr.Column(elem_id="targets-panel"):
391
  targets = gr.HTML(targets_html([]), elem_id="targets-content")
392
  with gr.Row(elem_id="review-row", equal_height=True):
393
  with gr.Column(scale=4, min_width=320, elem_id="sandbox-panel"):
394
  gr.HTML(
395
+ "<div class='film-heading'><h2>示例图像</h2></div>"
396
  )
397
  gallery = gr.Gallery(
398
  value=gallery_items(first_route),
399
  columns=3,
400
  rows=1,
401
+ height=190,
402
  show_label=False,
403
  allow_preview=False,
404
  object_fit="cover",
 
406
  interactive=False,
407
  elem_id="case-gallery",
408
  )
409
+ with gr.Accordion("案例信息", open=False, elem_id="source-details"):
410
  meta = gr.HTML(case_meta(first_case))
411
  with gr.Column(scale=6, min_width=420, elem_id="review-summary"):
412
  with gr.Row(elem_id="result-columns", equal_height=True):
413
  with gr.Column(scale=4, min_width=200, elem_id="metrics-panel"):
414
+ gr.Markdown("### 解析概览")
415
  metrics = gr.HTML(metrics_html(ROUTE_META[first_route][0]))
416
  with gr.Column(scale=6, min_width=240, elem_id="sidebar"):
417
  gr.HTML(
418
+ "<div class='inspector-heading'><h2>目标细节</h2><span class='live-mark'>选择结果即可放大原始图像。</span></div>"
419
  )
420
  gr.HTML(
421
+ "<div class='evidence-detail' hidden><div class='detail-heading'><span>局部细节</span><small>原始像素 · 局部放大</small></div><div class='detail-image'></div><div class='detail-coordinates'></div></div>",
422
  elem_id="detail-panel",
423
  )
424
+ with gr.Accordion("模型提示词", open=False, elem_id="prompt-console"):
425
  prompt = gr.Textbox(
426
+ value=first_case["prompt"], label="实际发送的提示词", lines=3, interactive=False
427
  )
428
  gr.HTML(
429
+ "<div class='console-note'>只发送当前图像与提示词,模型独立生成预测结果。</div>"
430
  )
431
+ with gr.Accordion("结构化结果", open=False, elem_id="output-console"):
432
  normalized = gr.Code(
433
+ value="[]", language="json", label="目标与坐标 · 0–999", interactive=False
434
  )
435
+ with gr.Accordion("模型原始输出", open=False):
436
+ raw = gr.Textbox(label="原始 GAM 序列", lines=4, interactive=False)
437
  gr.HTML(
438
+ "<footer class='studio-footer'><span>GroundingPI · 感知走向行动</span><span>探索目标、文字与交互位置 <i>图像 → 视觉基元</i></span></footer>"
439
  )
440
 
441
  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 = `Instance ${instanceIndex + 1} / ${coords.length} · [${p.join(', ')}]`;
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 = '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,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', '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,10 +169,10 @@
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,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 ? ' / drag to pan' : ''}`;
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 = `实例 ${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 = '预测目标';
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', '上一个实例', 'm12 5-5 5 5 5'], ['next', '下一个实例', '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', '关闭目标聚焦');
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
  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
  if (!ready || waiting) return;
170
  previousRunId = $('#stage-status .stage-status')?.dataset.runId || '';
171
  clientPending = true;
172
+ clientStatus('请求已提交 · 等待推理', 'busy'); setWaiting(true);
173
  watchdog = setTimeout(() => {
174
  if (!waiting) return;
175
+ clientStatus('尚未收到结果,请检查连接并刷新。服务器请求仍可能在处理。', '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 ? ' / 拖动平移' : ''}`;
205
  drawSelection();
206
  }
207
  function onClick(event) {
assets/native.css ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Native Gradio layout. Extra styles only support canvas interactions. */
2
+ #console{gap:20px!important}#workspace-top{gap:24px!important;align-items:flex-start!important}
3
+ #controls{flex:0 0 300px!important;min-width:0!important;gap:16px!important}
4
+ #control-fields{flex-direction:column!important}#control-fields>.form{width:100%!important}
5
+ .media-field{display:flex;align-items:center;justify-content:space-between;gap:14px;font-size:13px}
6
+ .media-field>div{display:flex;gap:6px}.media-option{display:inline-flex;align-items:center;justify-content:center;min-width:54px;padding:7px 11px;border-radius:6px;font-size:13px;font-family:inherit;border:1px solid #d1d5db}
7
+ .media-option.is-active{border-color:#2563eb;background:#eff6ff;color:#1d4ed8}.media-option:disabled{background:#f3f4f6;color:#9ca3af;cursor:not-allowed}
8
+ #visual-stage{min-width:0!important;gap:12px!important}.task-guide strong{font-size:19px}.task-guide p{font-size:14px;line-height:1.6;margin:8px 0}.task-guide small{font-size:12px;color:#64748b}
9
+ #stage-toolbar{min-height:24px;align-items:center;gap:0!important}.stage-status{display:flex;align-items:center;gap:8px;font-size:13px;line-height:1.6;overflow-wrap:anywhere}
10
+ .stage-status i,.api-status i{width:7px;height:7px;border-radius:50%;background:#16a34a;flex-shrink:0}.stage-status.error i,.api-status.offline i{background:#dc2626}.stage-status.busy i{background:#2563eb;animation:pulse 1s ease-in-out infinite}.wait-clock{margin-left:auto;font-size:12px;font-variant-numeric:tabular-nums}
11
+ #stage-toolbar.client-pending #stage-status{display:none!important}.client-notice{flex:1}
12
+ #canvas-viewport{position:relative;gap:8px!important;overflow:hidden!important;padding:0!important;border:0!important}
13
+ #stage-image{position:relative!important;transform-origin:center;transition:transform 160ms ease;overflow:visible!important;touch-action:pan-y}
14
+ #stage-image img{object-fit:contain!important}#stage-image .icon-button-wrapper{display:none!important}
15
+ #canvas-viewport.is-zoomed #stage-image{touch-action:none;cursor:grab}#canvas-viewport.is-panning #stage-image{cursor:grabbing;transition:none!important}
16
+ .canvas-tools{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;color:#64748b;padding:3px 0}
17
+ .zoom-tools{display:flex;align-items:center;gap:3px;border:1px solid #d1d5db;border-radius:6px;padding:3px;background:white}
18
+ .zoom-tools button,.canvas-download{width:31px;height:31px;border:0;border-radius:4px;background:white;display:grid;place-items:center;color:#475569;cursor:pointer}
19
+ .zoom-tools button:hover,.canvas-download:hover{background:#f1f5f9}.zoom-tools button:disabled{opacity:.35;cursor:default}.zoom-tools svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
20
+ .zoom-value{min-width:42px;text-align:center;font-size:12px}.tool-divider{width:1px;height:18px;background:#e2e8f0;margin:0 4px}
21
+ #query-row{gap:6px!important;flex-direction:column!important}#query-dock{width:100%;flex:0 0 auto!important}.query-hint{color:#64748b;font-size:11px;margin:0}
22
+ #targets-panel{gap:8px!important;min-width:0!important}.empty-targets{display:flex;flex-direction:column;gap:5px;color:#64748b;font-size:12px}.empty-targets svg{display:none}.empty-targets b{font-size:13px;color:#475569}
23
+ .targets-summary{display:flex;gap:8px;align-items:baseline;font-size:13px;color:#475569}.targets-summary>span{margin-left:auto;font-size:12px;color:#64748b}.targets-summary b{font-size:18px}
24
+ .target-chips{display:flex;flex-wrap:wrap;gap:7px;max-height:220px;overflow:auto;padding:3px}
25
+ .target-chip{display:flex;align-items:center;gap:7px;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;background:white;color:#334155;font:13px inherit;cursor:pointer}
26
+ .target-chip i{width:7px;height:7px;border-radius:2px;background:var(--category-color);flex-shrink:0}.target-chip span{overflow-wrap:anywhere}.target-chip b{font-size:11px;color:#64748b}.target-chip:hover{background:#f8fafc}.target-chip[aria-pressed=true]{border-color:#2563eb;background:#eff6ff}.target-chip:focus-visible{outline:2px solid #2563eb;outline-offset:2px}
27
+ .focus-help{display:flex;align-items:center;justify-content:space-between;gap:10px;color:#64748b;font-size:12px}.clear-focus{background:none;border:0;color:#2563eb;font:inherit;cursor:pointer;text-decoration:underline}.clear-focus[hidden],.evidence-detail[hidden]{display:none!important}.targets-caption{display:none}
28
+ .focus-layer,.focus-pixels{position:absolute;pointer-events:none}.focus-pixels{z-index:4}.focus-layer{z-index:5;overflow:visible}.focus-dimmer{fill:#0f172a;opacity:.22}.focus-outline{fill:none;stroke-width:2.5;vector-effect:non-scaling-stroke}.focus-halo{fill:none;stroke:white;stroke-width:5;vector-effect:non-scaling-stroke}
29
+ .focus-caption{display:flex;align-items:center;gap:7px;position:relative;z-index:6;padding:7px 9px;border:1px solid #dbeafe;border-radius:6px;background:#eff6ff;color:#1e40af;font-size:12px}
30
+ .focus-caption span{flex:1;overflow-wrap:anywhere}.focus-caption button{width:30px;height:30px;border:0;border-radius:4px;background:#dbeafe;color:#1e40af;display:grid;place-items:center;cursor:pointer}
31
+ #review-row{gap:24px!important}#review-summary{min-width:0!important}#result-columns{gap:20px!important}#metrics-panel,#sidebar{min-width:0!important;gap:10px!important}
32
+ .film-heading h2,.inspector-heading h2{font-size:17px;margin:0 0 8px}.film-heading small,.live-mark{font-size:12px;color:#64748b}
33
+ .metrics-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px;margin:0;font-size:13px}.metrics-list>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.metrics-list dt{color:#64748b}.metrics-list dd{margin:0;color:#334155;overflow-wrap:anywhere;text-align:right}
34
+ #sidebar:not(:has(.evidence-detail:not([hidden]))){display:none!important}.detail-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;margin-bottom:8px}.detail-heading small{color:#64748b;font-size:11px}.detail-image{height:190px;background-color:#f1f5f9;background-repeat:no-repeat;border-radius:6px}.detail-coordinates{font-size:11px;color:#64748b;margin-top:6px;overflow-wrap:anywhere}
35
+ .api-status{display:flex;align-items:center;gap:7px;flex-wrap:wrap;font-size:13px}.api-status small{flex-basis:100%;font-size:12px;color:#64748b;line-height:1.6}.connection-detail{font-size:12px;color:#64748b}.connection-detail code{display:block;font-size:11px;overflow-wrap:anywhere}
36
+ .runtime-options,.source-meta{display:grid;gap:8px;font-size:12px;color:#64748b}.source-meta b,.runtime-options b{color:#334155}.console-note{font-size:12px;color:#64748b}
37
+ .studio-footer{display:flex;justify-content:space-between;gap:15px;padding-top:12px;font-size:11px;color:#64748b}.studio-footer i{font-style:normal;margin-left:12px;color:#94a3b8}
38
+ #mobile-run-button{display:none!important}#console[aria-busy=true] #canvas-viewport{cursor:progress}
39
+ @keyframes pulse{50%{opacity:.3}}
40
+ @media(max-width:760px){#workspace-top,#review-row,#result-columns{flex-direction:column!important}#controls{width:100%;flex:0 0 auto!important}#visual-stage{width:100%}#controls>h3{margin:0}#stage-image{height:330px!important}#run-button{display:none!important}#mobile-run-button{display:flex!important;margin-top:8px!important;width:100%!important}#review-summary,#sandbox-panel{width:100%!important;flex:0 0 auto!important}.studio-footer{flex-direction:column;gap:5px}.metrics-list{font-size:12px}.canvas-tools{font-size:10px}.image-dimensions{white-space:nowrap}#service-panel{gap:6px!important}.target-chip{font-size:12px}.task-guide strong{font-size:18px}}
41
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
constraints-app.txt CHANGED
@@ -20,7 +20,7 @@ h11==0.16.0
20
  hf-xet==1.6.0
21
  httpcore==1.0.9
22
  httpx==0.28.1
23
- huggingface_hub==1.4.1
24
  idna==3.19
25
  Jinja2==3.1.6
26
  markdown-it-py==4.2.0
@@ -57,7 +57,7 @@ sympy==1.14.0
57
  tokenizers==0.22.2
58
  tomlkit==0.13.3
59
  tqdm==4.70.1
60
- transformers==5.3.0
61
  typer==0.27.2
62
  typer-slim==0.24.0
63
  typing-inspection==0.4.4
 
20
  hf-xet==1.6.0
21
  httpcore==1.0.9
22
  httpx==0.28.1
23
+ huggingface_hub==1.5.0
24
  idna==3.19
25
  Jinja2==3.1.6
26
  markdown-it-py==4.2.0
 
57
  tokenizers==0.22.2
58
  tomlkit==0.13.3
59
  tqdm==4.70.1
60
+ transformers==5.7.0
61
  typer==0.27.2
62
  typer-slim==0.24.0
63
  typing-inspection==0.4.4
docs/HF_SPACE_DEPLOYMENT.md CHANGED
@@ -1,67 +1,13 @@
1
- # Hugging Face deployment
2
 
3
- This guide describes a candidate deployment, not an already-created HF service.
4
- The model repository belongs to XPENG-AI; this private Space is hosted under
5
- FanLR. Both repository names and private deployment have been confirmed.
6
 
7
- ## Two repositories
 
 
 
 
 
 
8
 
9
- - Model repository: contents of the sibling `model/` directory; proposed ID
10
- `XPENG-AI/GroundingPI-4B`.
11
- - Gradio Space: contents of this `demo/` directory; proposed ID
12
- `FanLR/GroundingPI-4B`.
13
-
14
- The parent release workspace includes internal review material and must not be
15
- uploaded. Uploading files privately is still a transfer to an external platform.
16
-
17
- ## Private preview and hardware
18
-
19
- Create both repositories as **Private from the beginning**. Protected Spaces hide
20
- source code but expose the running application, so Protected is not a confidential
21
- preview mode. Organization access depends on its roles and grants; verify the
22
- actual reader set rather than assuming Private means only the uploader.
23
- [Visibility](https://huggingface.co/docs/hub/spaces-overview#space-visibility),
24
- [organization access](https://huggingface.co/docs/hub/organizations-security).
25
-
26
- Choose Gradio and confirm available hardware before upload. According to current
27
- official documentation, organization ZeroGPU hosting requires a Team/Enterprise
28
- plan. Personal free-tier eligibility does not establish organization eligibility.
29
- Do not enable paid hardware or change subscriptions without approval.
30
- [ZeroGPU eligibility](https://huggingface.co/docs/hub/spaces-zerogpu).
31
-
32
- ## Configuration
33
-
34
- | Location | Name | Value |
35
- | --- | --- | --- |
36
- | Variable | GROUNDING_BACKEND | zerogpu |
37
- | Variable | GROUNDING_MODEL_REPO | Reviewed model repository ID |
38
- | Variable | GROUNDING_MODEL_REVISION | Full 40-character commit SHA |
39
- | Variable | GROUNDING_MAX_TOKENS | 1024 |
40
- | Variable | GROUNDING_GPU_SECONDS | 120; tune only after measurement |
41
- | Secret | HF_TOKEN | Read access limited to the private model |
42
-
43
- Do not set internal deployment variables or a local model path in HF.
44
- The repository snapshot is downloaded using its pinned revision and then loaded
45
- locally, because the custom processor opens local configuration files.
46
- Never place a token in README, source, Variables, a URL or a screenshot.
47
- [Secrets](https://huggingface.co/docs/hub/spaces-overview#managing-secrets-and-environment-variables).
48
-
49
- Keep the README SDK configuration: `app_file: space_app.py`,
50
- Gradio 5.49.1 and Python 3.12.12. Use the supplied standard NVIDIA Torch dependency
51
- pins, not a vendor PPU environment. The runtime imports spaces before loading
52
- the model; GPU work alone runs through `@spaces.GPU`.
53
-
54
- ## Acceptance before wider access
55
-
56
- 1. Validate build dependencies, complete model loading and memory use.
57
- 2. Run every task family at least once; inspect actual pixels, raw tokens and EOS.
58
- 3. Record unsuccessful calls as well as successes. Curated training examples are
59
- not an independent accuracy benchmark.
60
- 4. Verify first request, repeated requests, GPU lease release/reallocation, quota
61
- exhaustion, timeout recovery and session isolation.
62
- 5. Confirm owner access and unauthorized-user denial for both repositories and
63
- the running application. A logged-out access check is part of acceptance.
64
- 6. Pin and record model and Space revisions. Do not automatically make either public.
65
-
66
- No HF login, upload, repository creation, hardware purchase or publication is
67
- performed by this document. Recheck platform policies when deploying.
 
1
+ # 部署到已有 FanLR ZeroGPU Space
2
 
3
+ 目标:`FanLR/GroundingPI-4B`。SDK 为 Gradio 5.49.1,硬件保持已有 ZeroGPU。Space 可见性保持私有。
 
 
4
 
5
+ 1. 先运行全部单元测试、案例完整性校验及打包校验。
6
+ 2. 上传更新文件,保持项目相对路径。
7
+ 3. 模型变量:`GROUNDING_MODEL_REPO=GroundingPI/GroundingPI`,`GROUNDING_MODEL_REVISION=127b0e1c54c68f24bf769a80067521b815b695dd`,`GROUNDING_MODEL=GroundingPI`。
8
+ 4. `GROUNDING_BACKEND=zerogpu`,输出预算 1024 tokens,GPU 调用上限 120 秒。
9
+ 5. 等待构建和模型初始化完成,确认“模型已就绪”。
10
+ 6. 使用无个人信息的图片测试 9 条任务路径。中文 OCR 使用合成图片;验证中文标签、结构化文本与边框。
11
+ 7. 验证上传、切换任务/案例后清空旧预测、缩放、聚焦、实例切换、下载与错误恢复。
12
 
13
+ 新账号 GroundingPI 目前的创建页禁用了 Gradio/Docker,因此不在该账号新建计算型 Space。此次使用它的公开模型,复用 FanLR 已有 GPU 服务。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/VERIFICATION.md CHANGED
@@ -1,44 +1,7 @@
1
- # Verification and evidence boundaries
2
 
3
- ## Existing service evidence — 2026-09-15
4
 
5
- The native implementation was installed in a new isolated application environment
6
- on a single PPU. The full model loaded and generated real outputs. A 27-example
7
- Gradio regression completed uploads, generation with EOS and GAM parsing without
8
- transport/format failures. This is not 100% semantic accuracy.
9
 
10
- The original serial PPU request window had median 5.069 s and P95 14.532 s
11
- (nearest rank, 27 requests). These are historical observations for that hardware
12
- and workload, not HF latency guarantees or paper benchmark measurements.
13
-
14
- The paper-r7 UI regression included 39 passing tests and browser verification of
15
- direct prediction overlays, category focus, instance navigation, zoom, stale-state
16
- clearing and responsive layout. Example live calls returned 27 boxes and 79 points;
17
- counts are output observations, not verified recall.
18
-
19
- ## This release candidate
20
-
21
- The presentation and inference implementation are copied from the paper-r7 release.
22
- README and deployment materials have been rewritten for external publication.
23
- The UI fingerprint and example contracts remain locked. Model weights, tokenizer
24
- and image processing remain byte-identical to the source. The cleaned custom
25
- code has canonical Oxygen class names and no unused Mage/StreamMind branches.
26
- Strict loading has no missing, unexpected or mismatched keys. Before/after
27
- Transformers regression matched all generated tokens for all 27 examples, with
28
- identical preprocessing tensors. This is equivalence, not semantic accuracy.
29
-
30
- The parent release workspace contains the technical inspection report. Reported
31
- tests must not be read as license approval, independent benchmark evaluation, or
32
- approval to disclose model/data assets.
33
-
34
- ## Still pending
35
-
36
- - Standard NVIDIA GPU and final HF ZeroGPU execution.
37
- - GPU lease capture, allocation/release, quota and long-idle cold starts.
38
- - Private organization access verification and external-upload authorization.
39
- - Independent accuracy evaluation and any paper speedup claims.
40
- - Final legal, model-card, example-image and branding review.
41
-
42
- The code does not run diffusion/parallel decoding just because a paper illustration
43
- discusses it. Publish only results verified for the exact released checkpoint,
44
- decoding method, hardware and evaluation protocol.
 
1
+ # 验证范围
2
 
3
+ 自动测试覆盖案例提示词、GAM 解析、预测状态更新、HTML 安全转义、图像上传、错误恢复、中文与长标签绘制、画布几何、打包文件和模型请求边界。
4
 
5
+ 模型包预检与 CPU 处理器测试验证 `GroundingPI/GroundingPI` 的固定版本、模型代码导入、原生聊天模板、图像张量与网格,不等同于实际 GPU 推理。
 
 
 
6
 
7
+ 线上 GPU 检查必须记录实际输出、完成状态、耗时和截图。9 条任务路径使用无个人信息图片;中文 OCR 使用合成文本。未执行或失败的检查应记录其具体状态,不标注为通过。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/ZEROGPU_DEPLOYMENT.md CHANGED
@@ -1,47 +1,9 @@
1
- # Runtime and model contract
2
 
3
- ## Initialization versus requests
4
 
5
- `space_app.py` installs the spaces CUDA hooks and initializes the model once at
6
- process startup. In a ZeroGPU Space, the decorated generation function leases GPU
7
- compute and returns CPU token IDs. It does not call from_pretrained per request.
8
- The same code can run on a normal compatible GPU, where the decorator is effect-free.
9
- [Official model-loading guidance](https://huggingface.co/docs/hub/spaces-zerogpu#model-loading).
10
 
11
- The default implementation is native Transformers, not a persistent vLLM or SGLang
12
- server. An explicitly configured HTTP backend exists for approved deployments, but
13
- no automatic backend fallback is used.
14
 
15
- ## Frozen behavior
16
-
17
- - `oxygen_vl_v3` / `OxygenVLv3ForConditionalGeneration`; vision model type
18
- `oxygen_k3_vision`.
19
- - Download a reviewed, pinned snapshot; load the checkpoint's processor from a local
20
- directory with `trust_remote_code=True`.
21
- - Apply the original chat template to the current image and request.
22
- - Preserve native image processing, image token/grid alignment and GAM vocabulary.
23
- - BF16, SDPA, greedy generation and `use_cache=True`; native EOS from generation config.
24
- - Preserve `skip_special_tokens=False` and `spaces_between_special_tokens=False`.
25
- - Reject missing pixels/grid, context overflow, empty answers and missing EOS.
26
- - Do not send annotations, reference counts or previous results to the model.
27
- - Do not claim token-identical output or equal latency across hardware/backends.
28
-
29
- The model package contains optional StreamMind source for provenance; this demo does
30
- not enable that branch or require mamba-ssm. Do not copy a training environment into
31
- the Space or install vendor-specific Torch wheels.
32
-
33
- ## Limits and failures
34
-
35
- Default 1,024 new tokens; supported UI/runtime range 1–2,048. Input plus requested
36
- output must not exceed 8,192 tokens. GPU duration is a maximum lease, not a latency
37
- promise. Queue waits and initialization must be distinguished from generation.
38
-
39
- Incomplete output is a failure, not a correct empty result. On exceptions, the UI
40
- clears stale predictions, restores its controls and exposes a recoverable error.
41
- The example annotations are never used as fallback predictions.
42
-
43
- ## Deployment boundary
44
-
45
- PPU service verification does not validate NVIDIA CUDA, ZeroGPU capture/leases,
46
- platform quotas or an organization's permissions. These are final-platform checks.
47
- See [HF deployment](HF_SPACE_DEPLOYMENT.md) and [verification](VERIFICATION.md).
 
1
+ # ZeroGPU 运行设计
2
 
3
+ `spaces` 在 Torch/Transformers 之前导入。模型在进程启动时加载并放到 CUDA,由 ZeroGPU 捕获。只在 `@spaces.GPU` 函数内执行生成与设备转换;模型不随每个请求重新加载。
4
 
5
+ 固定仓库 `GroundingPI/GroundingPI` 与 40 位修订 `127b0e1c54c68f24bf769a80067521b815b695dd`,使用自定义 GroundingPI 配置、MoonViT 视觉编码器和原生处理器。模型合同检查验证完整 GAM 词汇、图像 token、EOS 与权重分片。
 
 
 
 
6
 
7
+ Transformers 5.7.0,Torch 2.10.0,BF16,SDPA,贪心生成。保留特殊坐标 tokens;没有 EOS 的截断结果不展示为成功。初始化或推理失败会明确显示离线/错误状态,不回退到样例答案。
 
 
8
 
9
+ 公开模型不需要访问秘密。GPU 配额与排队由既有 FanLR Space 管理;页面改版不改变 Space 可见性和硬件。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/ui_case_pool_22_lock.json CHANGED
@@ -1,13 +1,14 @@
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/original.css": "7a244c7f65e40797850dcde7d6b6505b1b5de60aa24b129f44b33ae52291fd2e",
12
  "assets/paper.css": "61fc60bf8c379a0894fbfe06574964ec2ed256791a23860fa4dbb872eb2869f9",
13
  "assets/studio.css": "17a4d588ae8e1099d54043c10e3bc83c9182c33bcf1dda1914053040d4b63fc0",
@@ -35,7 +36,7 @@
35
  "cases/visual_prompt/21_visual_prompt_round5_candidates_gam_visual_dense200_142.jpg": "34101e5a696ca089b3d6026fbafda21ba8a144bb2b4d4f36fd309273ed48e286",
36
  "cases/visual_prompt/22_visual_prompt_round5_candidates_gam_visual_dense200_187.jpg": "3daa8e2776a2dd0ab17abebd669d95728f64bfbd063986f14becfb3f1966463a",
37
  "grounding_demo/catalog.py": "75e543fed7f0ca018fb41c4b528efc2901aa6a0f05232959297d6d40798f676f",
38
- "grounding_demo/console.py": "5754564abe58099c9406de639663d9d1416b96bc65d7a89b24f402ec41978fb8",
39
  "grounding_demo/protocol.py": "cf043c9f026a1dcd63d55c5393f0d0921cd15eae4a0cc9a42e2834fab23486ec",
40
  "grounding_demo/render.py": "3b3fe5901edc41586bf5f1a1c263265d47c0fbe4d2ee4c30697c1bd0f78d484b"
41
  },
 
1
  {
2
  "files": {
3
+ "app.py": "11ba24d8264b7a5b5bb9c3aafbb45df3d7ef36adaf556938a74e731e59885dc5",
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": "822e670f29a584ba201dc69e76cdbc6a2b836bee99b8dd1bc806aabdf3c7f5ec",
10
  "assets/frosted.css": "393bf5f1c04a49a6e9b17e855e573646489b4add85bc7973eb6bb277686b5c80",
11
+ "assets/native.css": "24b80e61a37ca92f62a871f30114bf2839290abf9c899242033b1aeef3998c38",
12
  "assets/original.css": "7a244c7f65e40797850dcde7d6b6505b1b5de60aa24b129f44b33ae52291fd2e",
13
  "assets/paper.css": "61fc60bf8c379a0894fbfe06574964ec2ed256791a23860fa4dbb872eb2869f9",
14
  "assets/studio.css": "17a4d588ae8e1099d54043c10e3bc83c9182c33bcf1dda1914053040d4b63fc0",
 
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": "3097928c7c6a77d5d4d379cbc298222e23d2751e5269e5efad39a71e8050c311",
40
  "grounding_demo/protocol.py": "cf043c9f026a1dcd63d55c5393f0d0921cd15eae4a0cc9a42e2834fab23486ec",
41
  "grounding_demo/render.py": "3b3fe5901edc41586bf5f1a1c263265d47c0fbe4d2ee4c30697c1bd0f78d484b"
42
  },
grounding_demo/console.py CHANGED
@@ -23,60 +23,15 @@ 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
- "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,12 +66,12 @@ def compile_query(query: str, route: str) -> str:
111
 
112
  def metrics_html(route, state="Ready", count=0, elapsed=None, usage=None):
113
  rows = [
114
- ("Status", state),
115
- ("Task", route),
116
- ("Model", "grounding-pi-vlm"),
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,11 +82,11 @@ def metrics_html(route, state="Ready", count=0, elapsed=None, usage=None):
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,18 +98,18 @@ def targets_html(items, pending=False, image_source=""):
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
  )
 
23
 
24
  # Presentation only: never alter the released model prompt or expected answer.
25
  TASK_HELP = {
26
+ "grounding": ["语言所指,清晰可见", "从一句描述中发现目标,以精确边框呈现它的位置。", "用逗号分隔类别;选择结果可查看原始像素。", "汽车,行人"],
27
+ "dense": ["密集场景,逐一看清", "探索拥挤场景里的小目标、遮挡物和多个实例。", "输入目标类别,再逐个查看检测结果。", "行人,椅子"],
28
+ "referring": ["把描述,落在图像上", "用外观、位置或关系,锁定你想找的那个目标。", "描述目标的特征与位置,例如颜色、衣着或相邻物体。", "站在桌子旁边的人"],
29
+ "dense_point": ["多个目标,一点即明", "以轻量的坐标点表达多个对象的位置。", "描述需要定位的目标,点表示位置而非物体边界。", "所有可见的行人"],
30
+ "referring_point_in_mask": ["在目标内部,找到落点", "根据文字描述,在目标对象内部预测一个位置。", "描述目标,结果将以图像内的坐标点呈现。", "穿着救生衣的人"],
31
+ "dense_point_in_mask": ["复杂场景里的每个落点", "为同类目标逐一预测位于对象内部的位置。", "用逗号分隔类别,每个点对应一个目标实例。", "货车,公交车,汽车,行人"],
32
+ "visual_prompt": ["给出参照,发现相似", "以参考框为线索,在图像中寻找相似对象。", "绿色 INPUT 框来自你的输入;坐标范围为 0–999。", "给出参考框,再描述需要寻找的相似目标"],
33
+ "ocr": ["读懂文字,也找到它", "将识别的文字与所在区域连接,完整保留中文与标点。", "输入文字识别指令;选择一条结果即可检查原始图像。", "OCR task detect all the text in box format."],
34
+ "gui": ["让交互意图,找到位置", "把你的操作描述映射为界面上的目标位置。", "描述要选择的元素;此功能仅定位,不执行点击。", "点击搜索按钮"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
 
 
66
 
67
  def metrics_html(route, state="Ready", count=0, elapsed=None, usage=None):
68
  rows = [
69
+ ("状态", {"Ready": "就绪", "Completed": "已完成", "Protocol error": "协议错误", "API error": "连接异常", "等待推理": "等待推理"}.get(state, state)),
70
+ ("任务", route),
71
+ ("模型", "GroundingPI"),
72
+ ("目标数", str(count)),
73
+ ("耗时", f"{elapsed:.2f} s" if elapsed is not None else "—"),
74
+ ("输出 tokens", str((usage or {}).get("completion_tokens", "—"))),
75
  ]
76
  return (
77
  "<dl class='metrics-list'>"
 
82
 
83
  def targets_html(items, pending=False, image_source=""):
84
  if not items:
85
+ title = "正在寻找视觉线索" if pending else "让图像给出答案"
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
  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'查看 {label},共 {count} 个目标', quote=True)}' "
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> 个目标 <span>{len(counts)} 个类别</span></div>"
111
  f"<div class='target-chips'>{chips}</div>"
112
+ "<div class='focus-help'><span>选择目标,查看局部细节</span>"
113
+ "<button type='button' class='clear-focus' hidden>显示全部</button></div>"
114
  f"<div class='targets-caption'>{caption}</div>"
115
  )
grounding_demo/model_spec.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """Public model release selected for this independent demo."""
2
+ MODEL_REPO = "GroundingPI/GroundingPI"
3
+ MODEL_REVISION = "127b0e1c54c68f24bf769a80067521b815b695dd"
4
+ MODEL_NAME = "GroundingPI"
grounding_demo/native_runtime.py CHANGED
@@ -43,21 +43,22 @@ def resolve_snapshot() -> Path:
43
 
44
 
45
  def inspect_contract(root: Path, *, require_weights: bool = True) -> dict:
46
- """Check the custom Kimi-K3-ViT package without importing its executable code."""
47
  required = [
48
  "config.json", "preprocessor_config.json", "generation_config.json",
49
  "tokenizer.json", "tokenizer_config.json", "chat_template.jinja",
50
- "configuration_oxygen_vl_v3.py",
51
- "modeling_oxygen_vl_v3.py", "modeling_kimi_k3.py",
52
- "processing_oxygen_vl_v3.py", "kimi_k3_vision_processing.py", "media_utils.py",
 
53
  ]
54
  missing = [name for name in required if not (root / name).is_file()]
55
  if missing:
56
  raise ConfigurationError(f"Model package is missing files: {', '.join(missing)}")
57
  config = json.loads((root / "config.json").read_text())
58
- if config.get("model_type") != "oxygen_vl_v3":
59
- raise ConfigurationError("This runtime requires the custom Oxygen-VL-v3 model, not standard Qwen3-VL weights.")
60
- if config.get("vision_config", {}).get("model_type") != "oxygen_k3_vision":
61
  raise ConfigurationError("The model does not use the expected Kimi-K3 ViT.")
62
  if not config.get("auto_map", {}).get("AutoModelForImageTextToText"):
63
  raise ConfigurationError("The model is missing the AutoModelForImageTextToText mapping.")
@@ -93,7 +94,7 @@ def inspect_contract(root: Path, *, require_weights: bool = True) -> dict:
93
  raise ConfigurationError("Model weight shards are incomplete. Upload the weights as well as the tokenizer and configuration.")
94
  prep = json.loads((root / "preprocessor_config.json").read_text())
95
  return {
96
- "architecture": config["architectures"], "vision": "oxygen_k3_vision",
97
  "config_sha256": hashlib.sha256((root / "config.json").read_bytes()).hexdigest(),
98
  "processor_sha256": hashlib.sha256((root / "preprocessor_config.json").read_bytes()).hexdigest(),
99
  "media_proc_cfg": prep.get("media_proc_cfg"), "gam_coordinate_tokens": 1000,
 
43
 
44
 
45
  def inspect_contract(root: Path, *, require_weights: bool = True) -> dict:
46
+ """Validate the public GroundingPI release without executing model code."""
47
  required = [
48
  "config.json", "preprocessor_config.json", "generation_config.json",
49
  "tokenizer.json", "tokenizer_config.json", "chat_template.jinja",
50
+ "configuration_groundingpi.py", "configuration_groundingpi_vision.py",
51
+ "modeling_groundingpi.py", "modeling_groundingpi_vision.py",
52
+ "processing_groundingpi.py", "image_processing_groundingpi.py", "media_utils.py",
53
+ "streammind_gate.py",
54
  ]
55
  missing = [name for name in required if not (root / name).is_file()]
56
  if missing:
57
  raise ConfigurationError(f"Model package is missing files: {', '.join(missing)}")
58
  config = json.loads((root / "config.json").read_text())
59
+ if config.get("model_type") != "groundingpi":
60
+ raise ConfigurationError("This demo requires the GroundingPI/GroundingPI model package.")
61
+ if config.get("vision_config", {}).get("model_type") != "groundingpi_vision":
62
  raise ConfigurationError("The model does not use the expected Kimi-K3 ViT.")
63
  if not config.get("auto_map", {}).get("AutoModelForImageTextToText"):
64
  raise ConfigurationError("The model is missing the AutoModelForImageTextToText mapping.")
 
94
  raise ConfigurationError("Model weight shards are incomplete. Upload the weights as well as the tokenizer and configuration.")
95
  prep = json.loads((root / "preprocessor_config.json").read_text())
96
  return {
97
+ "architecture": config["architectures"], "vision": "groundingpi_vision",
98
  "config_sha256": hashlib.sha256((root / "config.json").read_bytes()).hexdigest(),
99
  "processor_sha256": hashlib.sha256((root / "preprocessor_config.json").read_bytes()).hexdigest(),
100
  "media_proc_cfg": prep.get("media_proc_cfg"), "gam_coordinate_tokens": 1000,
requirements.txt CHANGED
@@ -20,7 +20,7 @@ h11==0.16.0
20
  hf-xet==1.6.0
21
  httpcore==1.0.9
22
  httpx==0.28.1
23
- huggingface-hub==1.4.1
24
  idna==3.19
25
  Jinja2==3.1.6
26
  markdown-it-py==4.2.0
@@ -59,7 +59,7 @@ tomlkit==0.13.3
59
  torch==2.10.0
60
  torchvision==0.25.0
61
  tqdm==4.70.1
62
- transformers==5.3.0
63
  typer==0.27.2
64
  typer-slim==0.24.0
65
  typing_extensions==4.16.0
 
20
  hf-xet==1.6.0
21
  httpcore==1.0.9
22
  httpx==0.28.1
23
+ huggingface-hub==1.5.0
24
  idna==3.19
25
  Jinja2==3.1.6
26
  markdown-it-py==4.2.0
 
59
  torch==2.10.0
60
  torchvision==0.25.0
61
  tqdm==4.70.1
62
+ transformers==5.7.0
63
  typer==0.27.2
64
  typer-slim==0.24.0
65
  typing_extensions==4.16.0
space_app.py CHANGED
@@ -1,9 +1,15 @@
1
- """HF Gradio entrypoint. The original app.py and visual assets stay unchanged."""
2
  import os
3
 
4
  from grounding_demo.client import zerogpu_configured
 
5
 
6
  os.environ.setdefault("GROUNDING_BACKEND", "zerogpu")
 
 
 
 
 
7
  os.environ["GROUNDING_BACKEND"] = os.environ["GROUNDING_BACKEND"].strip().lower()
8
  os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
9
 
 
1
+ """Chinese GroundingPI Studio with a pinned public checkpoint."""
2
  import os
3
 
4
  from grounding_demo.client import zerogpu_configured
5
+ from grounding_demo.model_spec import MODEL_REPO, MODEL_REVISION, MODEL_NAME
6
 
7
  os.environ.setdefault("GROUNDING_BACKEND", "zerogpu")
8
+ if not os.getenv("GROUNDING_MODEL_PATH"):
9
+ # This release deliberately replaces the older Space's model variables.
10
+ os.environ["GROUNDING_MODEL_REPO"] = MODEL_REPO
11
+ os.environ["GROUNDING_MODEL_REVISION"] = MODEL_REVISION
12
+ os.environ["GROUNDING_MODEL"] = MODEL_NAME
13
  os.environ["GROUNDING_BACKEND"] = os.environ["GROUNDING_BACKEND"].strip().lower()
14
  os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
15
 
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("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,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("Protocol error", result["metrics"])
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 visual targets", output[-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("Complete", result["status"])
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 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,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("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("&lt;unsafe&gt;", 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("图像已就绪", 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("图像已就绪", 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("协议错误", 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 个视觉目标", 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("解析完成", 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 个视觉目标", 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("请检查连接后重试", 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("推理未连接", output)
189
  self.assertIn("&lt;unsafe&gt;", output)
190
 
191
  def test_both_run_buttons_share_real_stream(self):
tests/test_model_release.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import tempfile
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ from grounding_demo.model_spec import MODEL_REPO, MODEL_REVISION
7
+ from grounding_demo.native_runtime import inspect_contract, ConfigurationError
8
+
9
+
10
+ class ModelReleaseTests(unittest.TestCase):
11
+ def package(self, root):
12
+ config = {
13
+ "model_type": "groundingpi", "architectures": ["GroundingPIForConditionalGeneration"],
14
+ "vision_config": {"model_type": "groundingpi_vision"}, "image_token_id": 151655,
15
+ "auto_map": {"AutoModelForImageTextToText": "modeling_groundingpi.GroundingPIForConditionalGeneration",
16
+ "AutoProcessor": "processing_groundingpi.GroundingPIProcessor"},
17
+ }
18
+ for name in ["configuration_groundingpi.py", "configuration_groundingpi_vision.py",
19
+ "modeling_groundingpi.py", "modeling_groundingpi_vision.py",
20
+ "processing_groundingpi.py", "image_processing_groundingpi.py",
21
+ "media_utils.py", "streammind_gate.py", "chat_template.jinja"]:
22
+ (root/name).write_text("test fixture")
23
+ vocab = {f"<{n}>": n for n in range(1000)}
24
+ vocab.update({token: 150000+i for i,token in enumerate(
25
+ ["<|box_start|>", "<|box_end|>", "<|object_ref_start|>", "<|object_ref_end|>"])})
26
+ vocab["<|image_pad|>"] = 151655
27
+ for name, data in {"config.json": config, "preprocessor_config.json": {"media_proc_cfg": {}},
28
+ "generation_config.json": {"eos_token_id": [151645]},
29
+ "tokenizer_config.json": {}, "tokenizer.json": {"model": {"vocab": vocab}}}.items():
30
+ (root/name).write_text(json.dumps(data))
31
+ return config
32
+
33
+ def test_release_targets_new_accounts_pinned_public_model(self):
34
+ self.assertEqual(MODEL_REPO, "GroundingPI/GroundingPI")
35
+ self.assertRegex(MODEL_REVISION, r"^[0-9a-f]{40}$")
36
+
37
+ def test_new_model_contract_is_accepted_without_executing_model_code(self):
38
+ with tempfile.TemporaryDirectory() as d:
39
+ root=Path(d);self.package(root)
40
+ report=inspect_contract(root,require_weights=False)
41
+ self.assertEqual(report["vision"],"groundingpi_vision")
42
+ self.assertEqual(report["gam_coordinate_tokens"],1000)
43
+ self.assertEqual(report["eos_token_ids"],[151645])
44
+
45
+ def test_wrong_old_architecture_is_rejected(self):
46
+ with tempfile.TemporaryDirectory() as d:
47
+ root=Path(d);config=self.package(root)
48
+ config["model_type"]="oxygen_vl_v3"
49
+ (root/"config.json").write_text(json.dumps(config))
50
+ with self.assertRaisesRegex(ConfigurationError,"GroundingPI/GroundingPI"):
51
+ inspect_contract(root,require_weights=False)
52
+
53
+ def test_production_contract_requires_complete_weights(self):
54
+ with tempfile.TemporaryDirectory() as d:
55
+ root=Path(d);self.package(root)
56
+ with self.assertRaisesRegex(ConfigurationError,"weight shards"):
57
+ inspect_contract(root)