Spaces:
Running on Zero
Running on Zero
Restore native Gradio UI and use GroundingPI account model
Browse filesUse 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 +2 -2
- README.md +20 -99
- app.py +77 -93
- assets/console.js +9 -9
- assets/native.css +41 -0
- constraints-app.txt +2 -2
- docs/HF_SPACE_DEPLOYMENT.md +10 -64
- docs/VERIFICATION.md +4 -41
- docs/ZEROGPU_DEPLOYMENT.md +5 -43
- docs/ui_case_pool_22_lock.json +4 -3
- grounding_demo/console.py +23 -68
- grounding_demo/model_spec.py +4 -0
- grounding_demo/native_runtime.py +9 -8
- requirements.txt +2 -2
- space_app.py +7 -1
- tests/test_console.py +8 -8
- tests/test_model_release.py +57 -0
.env.example
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Hugging Face Space -> Settings -> Variables
|
| 2 |
GROUNDING_BACKEND=zerogpu
|
| 3 |
-
GROUNDING_MODEL_REPO=
|
| 4 |
-
GROUNDING_MODEL_REVISION=
|
| 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:
|
| 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 |
-
-
|
| 14 |
---
|
| 15 |
|
| 16 |
-
# GroundingPI ·
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 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 |
-
|
| 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 |
-
##
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 83 |
|
| 84 |
-
|
| 85 |
-
this directory to a **Gradio Space**, not the parent release workspace and not a ZIP.
|
| 86 |
|
| 87 |
-
|
| 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 |
-
|
| 93 |
-
inference; it is not an instruction to reload model weights on every request.
|
| 94 |
-
See [runtime design](docs/ZEROGPU_DEPLOYMENT.md).
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 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 |
-
|
| 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": ("
|
| 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,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,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>{'
|
| 276 |
-
f"<small>{'
|
| 277 |
-
f"<details class='connection-detail'><summary>
|
| 278 |
)
|
| 279 |
|
| 280 |
|
| 281 |
-
CSS = (ROOT / "assets" / "
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 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 ·
|
| 307 |
-
theme=gr.themes.
|
| 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.
|
| 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>
|
| 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),
|
| 367 |
type="filepath",
|
| 368 |
interactive=False,
|
| 369 |
show_label=False,
|
| 370 |
-
height=
|
| 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="
|
| 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,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="
|
| 398 |
-
show_label=
|
| 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),
|
| 415 |
columns=3,
|
| 416 |
rows=1,
|
| 417 |
-
height=
|
| 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("
|
| 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.
|
| 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": ("目标检测", "开放词汇目标定位"),
|
| 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 = `
|
| 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 = `实例 ${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.
|
| 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.
|
| 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 |
-
#
|
| 2 |
|
| 3 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 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 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
`
|
| 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 |
-
|
| 12 |
-
server. An explicitly configured HTTP backend exists for approved deployments, but
|
| 13 |
-
no automatic backend fallback is used.
|
| 14 |
|
| 15 |
-
|
| 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": "
|
| 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/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": "
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
],
|
| 32 |
-
"
|
| 33 |
-
|
| 34 |
-
|
| 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 |
-
("
|
| 115 |
-
("
|
| 116 |
-
("
|
| 117 |
-
("
|
| 118 |
-
("
|
| 119 |
-
("
|
| 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 = "
|
| 131 |
hint = (
|
| 132 |
-
"
|
| 133 |
if pending
|
| 134 |
-
else "
|
| 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'
|
| 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 = "
|
| 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>
|
| 156 |
f"<div class='target-chips'>{chips}</div>"
|
| 157 |
-
"<div class='focus-help'><span>
|
| 158 |
-
"<button type='button' class='clear-focus' hidden>
|
| 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 |
-
"""
|
| 47 |
required = [
|
| 48 |
"config.json", "preprocessor_config.json", "generation_config.json",
|
| 49 |
"tokenizer.json", "tokenizer_config.json", "chat_template.jinja",
|
| 50 |
-
"
|
| 51 |
-
"
|
| 52 |
-
"
|
|
|
|
| 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") != "
|
| 59 |
-
raise ConfigurationError("This
|
| 60 |
-
if config.get("vision_config", {}).get("model_type") != "
|
| 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": "
|
| 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.
|
| 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.
|
| 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 |
-
"""
|
| 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("
|
| 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("图像已就绪", 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("<unsafe>", 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)
|