Spaces:
Running on Zero
Running on Zero
Fix responsive layout, request status lifecycle and prediction inspection
Browse files- app.py +14 -7
- assets/console.js +20 -3
- assets/studio.css +98 -0
- docs/UI_STUDIO_R8.md +36 -0
- docs/ui_studio_r8_lock.json +48 -0
- grounding_demo/render.py +10 -2
- scripts/build_bundle.py +5 -1
- scripts/verify_ui.py +3 -1
- tests/test_console.py +17 -0
app.py
CHANGED
|
@@ -4,6 +4,7 @@ import base64
|
|
| 4 |
import html
|
| 5 |
import json
|
| 6 |
import os
|
|
|
|
| 7 |
from io import BytesIO
|
| 8 |
|
| 9 |
import gradio as gr
|
|
@@ -259,7 +260,11 @@ def run_inference_stream(path, query_text, route):
|
|
| 259 |
|
| 260 |
|
| 261 |
def run_console(path, query_text, route):
|
|
|
|
|
|
|
| 262 |
for state in run_inference_stream(path, query_text, route):
|
|
|
|
|
|
|
| 263 |
yield (*state, state[-1])
|
| 264 |
|
| 265 |
|
|
@@ -275,6 +280,7 @@ def service_status():
|
|
| 275 |
|
| 276 |
CSS = (ROOT / "assets" / "paper.css").read_text(encoding="utf-8")
|
| 277 |
CSS += "\n" + (ROOT / "assets" / "original.css").read_text(encoding="utf-8")
|
|
|
|
| 278 |
CSS = CSS.replace(
|
| 279 |
"__MANROPE_DATA__",
|
| 280 |
base64.b64encode((ROOT / "assets" / "Manrope-variable.ttf").read_bytes()).decode(),
|
|
@@ -282,7 +288,7 @@ CSS = CSS.replace(
|
|
| 282 |
# Gradio 5 scopes css= rules inside .contain, including media-query selectors.
|
| 283 |
# Host sizing must live in head; workbench styling remains scoped.
|
| 284 |
HOST_HEAD = """<style id="grounding-host-layout">
|
| 285 |
-
gradio-app:has(#console) .gradio-container { width:100%!important; max-width:
|
| 286 |
gradio-app:has(#console) .gradio-container>main { width:100%!important; max-width:none!important; min-width:0!important; padding:0!important; }
|
| 287 |
gradio-app:has(#console) .gradio-container .contain { max-width:none!important; }
|
| 288 |
gradio-app:has(#console) footer:not(.studio-footer) { display:none!important; }
|
|
@@ -357,10 +363,10 @@ with gr.Blocks(
|
|
| 357 |
+ html.escape(os.getenv("GROUNDING_MAX_TOKENS", "1024"))
|
| 358 |
+ "</b><small>Read-only runtime configuration. Only the current image and task prompt enter inference.</small></div>"
|
| 359 |
)
|
|
|
|
| 360 |
with gr.Column(elem_id="service-panel"):
|
| 361 |
health = gr.HTML("<div class='api-status'>Checking inference service…</div>")
|
| 362 |
health_btn = gr.Button("Refresh connection", size="sm", elem_id="health-button")
|
| 363 |
-
run_btn = gr.Button(RUN_LABEL, variant="primary", elem_id="run-button")
|
| 364 |
with gr.Column(scale=7, min_width=400, elem_id="visual-stage"):
|
| 365 |
task_help = gr.HTML(task_help_html(first_route), elem_id="task-guide")
|
| 366 |
with gr.Row(elem_id="stage-toolbar"):
|
|
@@ -374,6 +380,7 @@ with gr.Blocks(
|
|
| 374 |
height=400,
|
| 375 |
elem_id="stage-image",
|
| 376 |
show_download_button=False,
|
|
|
|
| 377 |
show_fullscreen_button=False,
|
| 378 |
)
|
| 379 |
gr.HTML(
|
|
@@ -406,6 +413,8 @@ with gr.Blocks(
|
|
| 406 |
)
|
| 407 |
gr.HTML("<p class='query-hint'>Enter to run · Shift + Enter for a new line</p>")
|
| 408 |
mobile_run = gr.Button(RUN_LABEL, variant="primary", elem_id="mobile-run-button")
|
|
|
|
|
|
|
| 409 |
with gr.Row(elem_id="review-row", equal_height=True):
|
| 410 |
with gr.Column(scale=4, min_width=320, elem_id="sandbox-panel"):
|
| 411 |
gr.HTML(
|
|
@@ -432,22 +441,20 @@ with gr.Blocks(
|
|
| 432 |
metrics = gr.HTML(metrics_html(ROUTE_META[first_route][0]))
|
| 433 |
with gr.Column(scale=6, min_width=240, elem_id="sidebar"):
|
| 434 |
gr.HTML(
|
| 435 |
-
"<div class='inspector-heading'><h2>
|
| 436 |
)
|
| 437 |
-
with gr.Column(elem_id="targets-panel"):
|
| 438 |
-
targets = gr.HTML(targets_html([]), elem_id="targets-content")
|
| 439 |
gr.HTML(
|
| 440 |
"<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>",
|
| 441 |
elem_id="detail-panel",
|
| 442 |
)
|
| 443 |
-
with gr.Accordion("Prompt Console", open=
|
| 444 |
prompt = gr.Textbox(
|
| 445 |
value=first_case["prompt"], label="COMPILED PROMPT", lines=3, interactive=False
|
| 446 |
)
|
| 447 |
gr.HTML(
|
| 448 |
"<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>"
|
| 449 |
)
|
| 450 |
-
with gr.Accordion("Structured Output", open=
|
| 451 |
normalized = gr.Code(
|
| 452 |
value="[]", language="json", label="PARSED COORDINATES · 0–999", interactive=False
|
| 453 |
)
|
|
|
|
| 4 |
import html
|
| 5 |
import json
|
| 6 |
import os
|
| 7 |
+
from uuid import uuid4
|
| 8 |
from io import BytesIO
|
| 9 |
|
| 10 |
import gradio as gr
|
|
|
|
| 260 |
|
| 261 |
|
| 262 |
def run_console(path, query_text, route):
|
| 263 |
+
# Identifies this request even when a proxy coalesces busy + final SSE events.
|
| 264 |
+
run_id = uuid4().hex
|
| 265 |
for state in run_inference_stream(path, query_text, route):
|
| 266 |
+
state = list(state)
|
| 267 |
+
state[5] = state[5].replace("data-phase=", f"data-run-id='{run_id}' data-phase=", 1)
|
| 268 |
yield (*state, state[-1])
|
| 269 |
|
| 270 |
|
|
|
|
| 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(),
|
|
|
|
| 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:24px 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; }
|
|
|
|
| 363 |
+ html.escape(os.getenv("GROUNDING_MAX_TOKENS", "1024"))
|
| 364 |
+ "</b><small>Read-only runtime configuration. Only the current image and task prompt enter inference.</small></div>"
|
| 365 |
)
|
| 366 |
+
run_btn = gr.Button(RUN_LABEL, variant="primary", elem_id="run-button")
|
| 367 |
with gr.Column(elem_id="service-panel"):
|
| 368 |
health = gr.HTML("<div class='api-status'>Checking inference service…</div>")
|
| 369 |
health_btn = gr.Button("Refresh connection", size="sm", elem_id="health-button")
|
|
|
|
| 370 |
with gr.Column(scale=7, min_width=400, elem_id="visual-stage"):
|
| 371 |
task_help = gr.HTML(task_help_html(first_route), elem_id="task-guide")
|
| 372 |
with gr.Row(elem_id="stage-toolbar"):
|
|
|
|
| 380 |
height=400,
|
| 381 |
elem_id="stage-image",
|
| 382 |
show_download_button=False,
|
| 383 |
+
show_share_button=False,
|
| 384 |
show_fullscreen_button=False,
|
| 385 |
)
|
| 386 |
gr.HTML(
|
|
|
|
| 413 |
)
|
| 414 |
gr.HTML("<p class='query-hint'>Enter to run · Shift + Enter for a new line</p>")
|
| 415 |
mobile_run = gr.Button(RUN_LABEL, variant="primary", elem_id="mobile-run-button")
|
| 416 |
+
with gr.Column(elem_id="targets-panel"):
|
| 417 |
+
targets = gr.HTML(targets_html([]), elem_id="targets-content")
|
| 418 |
with gr.Row(elem_id="review-row", equal_height=True):
|
| 419 |
with gr.Column(scale=4, min_width=320, elem_id="sandbox-panel"):
|
| 420 |
gr.HTML(
|
|
|
|
| 441 |
metrics = gr.HTML(metrics_html(ROUTE_META[first_route][0]))
|
| 442 |
with gr.Column(scale=6, min_width=240, elem_id="sidebar"):
|
| 443 |
gr.HTML(
|
| 444 |
+
"<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>"
|
| 445 |
)
|
|
|
|
|
|
|
| 446 |
gr.HTML(
|
| 447 |
"<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>",
|
| 448 |
elem_id="detail-panel",
|
| 449 |
)
|
| 450 |
+
with gr.Accordion("Prompt Console", open=False, elem_id="prompt-console"):
|
| 451 |
prompt = gr.Textbox(
|
| 452 |
value=first_case["prompt"], label="COMPILED PROMPT", lines=3, interactive=False
|
| 453 |
)
|
| 454 |
gr.HTML(
|
| 455 |
"<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>"
|
| 456 |
)
|
| 457 |
+
with gr.Accordion("Structured Output", open=False, elem_id="output-console"):
|
| 458 |
normalized = gr.Code(
|
| 459 |
value="[]", language="json", label="PARSED COORDINATES · 0–999", interactive=False
|
| 460 |
)
|
assets/console.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
| 6 |
const listen = (el, event, fn) => el.addEventListener(event, fn, { signal: abort.signal });
|
| 7 |
let observer, resize, frame, interval, selected, targetRoot, image, lastSrc, lastDraw;
|
| 8 |
let waiting = false, waitStart = 0, ready = false, watchdog, instanceIndex = 0;
|
|
|
|
| 9 |
let zoom = 1, panX = 0, panY = 0, drag, moved = false;
|
| 10 |
const $ = s => document.querySelector(s);
|
| 11 |
const svg = (tag, attrs = {}) => {
|
|
@@ -94,7 +95,9 @@
|
|
| 94 |
: { cx: p[0] / 999 * w, cy: p[1] / 999 * h, r: 13 };
|
| 95 |
const tag = p.length === 4 ? 'rect' : 'circle';
|
| 96 |
mask.append(svg(tag, { ...attr, fill: 'black' }));
|
| 97 |
-
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
defs.append(mask); layer.append(defs);
|
| 100 |
layer.append(svg('rect', { width: w, height: h, mask: 'url(#grounding-focus-mask)', class: 'focus-dimmer' }), ...marks);
|
|
@@ -153,12 +156,19 @@
|
|
| 153 |
const clock = document.createElement('time'); clock.className = 'wait-clock';
|
| 154 |
clock.setAttribute('aria-hidden', 'true'); clock.textContent = '0.0 s'; node.append(clock);
|
| 155 |
}
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
// A reverse proxy can buffer the first SSE event. Acknowledge the actual click
|
| 159 |
// immediately; backend events still own final results and measured latency.
|
| 160 |
window.groundingConsoleBegin = () => {
|
| 161 |
if (!ready || waiting) return;
|
|
|
|
|
|
|
| 162 |
clientStatus('Request submitted · Awaiting inference', 'busy'); setWaiting(true);
|
| 163 |
watchdog = setTimeout(() => {
|
| 164 |
if (!waiting) return;
|
|
@@ -173,7 +183,13 @@
|
|
| 173 |
function reconcile() {
|
| 174 |
const stage = $('#stage-image'), targets = $('#targets-content');
|
| 175 |
if (!stage || !targets) return;
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
const chips = targets.querySelector('.target-chips');
|
| 178 |
if (targetRoot !== chips) { targetRoot = chips; clearSelection(); }
|
| 179 |
const nextImage = stage.querySelector('img');
|
|
@@ -269,6 +285,7 @@
|
|
| 269 |
window.groundingConsoleCleanup = () => {
|
| 270 |
abort.abort(); observer?.disconnect(); resize?.disconnect(); cancelAnimationFrame(frame); clearInterval(interval); clearTimeout(watchdog);
|
| 271 |
if (ready) setWaiting(false); removeLayer();
|
|
|
|
| 272 |
delete window.groundingConsoleBegin;
|
| 273 |
};
|
| 274 |
}
|
|
|
|
| 6 |
const listen = (el, event, fn) => el.addEventListener(event, fn, { signal: abort.signal });
|
| 7 |
let observer, resize, frame, interval, selected, targetRoot, image, lastSrc, lastDraw;
|
| 8 |
let waiting = false, waitStart = 0, ready = false, watchdog, instanceIndex = 0;
|
| 9 |
+
let clientPending = false, previousRunId = '', clientNotice;
|
| 10 |
let zoom = 1, panX = 0, panY = 0, drag, moved = false;
|
| 11 |
const $ = s => document.querySelector(s);
|
| 12 |
const svg = (tag, attrs = {}) => {
|
|
|
|
| 95 |
: { cx: p[0] / 999 * w, cy: p[1] / 999 * h, r: 13 };
|
| 96 |
const tag = p.length === 4 ? 'rect' : 'circle';
|
| 97 |
mask.append(svg(tag, { ...attr, fill: 'black' }));
|
| 98 |
+
const outline = svg(tag, { ...attr, class: 'focus-outline', pathLength: 1 });
|
| 99 |
+
outline.style.stroke = getComputedStyle(selected.querySelector('i')).backgroundColor;
|
| 100 |
+
marks.push(svg(tag, { ...attr, class: 'focus-halo' }), outline);
|
| 101 |
}
|
| 102 |
defs.append(mask); layer.append(defs);
|
| 103 |
layer.append(svg('rect', { width: w, height: h, mask: 'url(#grounding-focus-mask)', class: 'focus-dimmer' }), ...marks);
|
|
|
|
| 156 |
const clock = document.createElement('time'); clock.className = 'wait-clock';
|
| 157 |
clock.setAttribute('aria-hidden', 'true'); clock.textContent = '0.0 s'; node.append(clock);
|
| 158 |
}
|
| 159 |
+
// This node belongs to us, outside Gradio's HTML component. Never replace
|
| 160 |
+
// Svelte-owned children: doing so can leave an orphan busy status forever.
|
| 161 |
+
clientNotice?.remove(); clientNotice = node;
|
| 162 |
+
node.classList.add('client-notice');
|
| 163 |
+
$('#stage-toolbar').append(node);
|
| 164 |
+
$('#stage-toolbar').classList.add('client-pending');
|
| 165 |
}
|
| 166 |
// A reverse proxy can buffer the first SSE event. Acknowledge the actual click
|
| 167 |
// immediately; backend events still own final results and measured latency.
|
| 168 |
window.groundingConsoleBegin = () => {
|
| 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;
|
|
|
|
| 183 |
function reconcile() {
|
| 184 |
const stage = $('#stage-image'), targets = $('#targets-content');
|
| 185 |
if (!stage || !targets) return;
|
| 186 |
+
const server = $('#stage-status .stage-status');
|
| 187 |
+
if (clientPending && (server?.dataset.runId || '') !== previousRunId) {
|
| 188 |
+
clientPending = false;
|
| 189 |
+
clientNotice?.remove(); clientNotice = null;
|
| 190 |
+
$('#stage-toolbar').classList.remove('client-pending');
|
| 191 |
+
}
|
| 192 |
+
if (!clientPending) setWaiting(server?.dataset.phase === 'busy');
|
| 193 |
const chips = targets.querySelector('.target-chips');
|
| 194 |
if (targetRoot !== chips) { targetRoot = chips; clearSelection(); }
|
| 195 |
const nextImage = stage.querySelector('img');
|
|
|
|
| 285 |
window.groundingConsoleCleanup = () => {
|
| 286 |
abort.abort(); observer?.disconnect(); resize?.disconnect(); cancelAnimationFrame(frame); clearInterval(interval); clearTimeout(watchdog);
|
| 287 |
if (ready) setWaiting(false); removeLayer();
|
| 288 |
+
clientNotice?.remove(); $('#stage-toolbar')?.classList.remove('client-pending');
|
| 289 |
delete window.groundingConsoleBegin;
|
| 290 |
};
|
| 291 |
}
|
assets/studio.css
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* GroundingPI studio r8. Shared by the standalone demo and private HF Space. */
|
| 2 |
+
#workspace-top {min-height:0;box-shadow:0 12px 36px #30204408;background:#fff}
|
| 3 |
+
#controls {flex:0 0 286px!important;min-width:0!important;width:286px;padding:24px!important;gap:20px!important;background:#fcfbfe}
|
| 4 |
+
.control-brand {container-type:inline-size;gap:12px}
|
| 5 |
+
.control-brand h2 {font-size:clamp(26px,14cqw,38px);white-space:nowrap;line-height:1.15;letter-spacing:-.045em}
|
| 6 |
+
.control-brand .xpeng-logo {width:176px;max-width:80%;height:auto}
|
| 7 |
+
.control-brand p {font-size:12px;line-height:1.65}
|
| 8 |
+
#control-settings {padding:12px!important;gap:12px!important}
|
| 9 |
+
#control-fields {flex-direction:column!important;gap:10px!important}
|
| 10 |
+
#control-fields>.form {width:100%!important;flex:0 0 auto!important}
|
| 11 |
+
#media-type {flex:0 0 auto!important}
|
| 12 |
+
.media-field {display:flex;align-items:center;justify-content:space-between;gap:12px}
|
| 13 |
+
.media-field>span {margin:0!important}.media-field>div {height:28px;background:none;border:0;padding:0}
|
| 14 |
+
.media-field>div>span {min-width:0;background:#edf3e2;color:#4c681c;box-shadow:none;font-size:11px;padding:0 10px}
|
| 15 |
+
#run-button {margin-top:0!important;min-height:48px!important;border-radius:8px!important;box-shadow:none!important;font-size:14px!important}
|
| 16 |
+
#service-panel {gap:2px!important;margin-top:0!important}
|
| 17 |
+
.api-status small {font-size:11px}
|
| 18 |
+
#service-panel #health-button {min-height:32px!important;justify-content:flex-start;padding-left:0!important}
|
| 19 |
+
#controls[inert] {opacity:1}
|
| 20 |
+
#visual-stage {--stage-gutter:24px;background:#fff}
|
| 21 |
+
#task-guide {padding:22px var(--stage-gutter) 0!important}
|
| 22 |
+
.task-guide strong {font-size:18px}.task-guide p {font-size:12px}
|
| 23 |
+
.task-guide small {font-size:11px}
|
| 24 |
+
#stage-toolbar {min-height:40px;padding:10px var(--stage-gutter)!important;gap:0!important}
|
| 25 |
+
.stage-status {padding:0;border:0;background:transparent;font-size:11px;min-height:20px;align-items:center;flex-wrap:nowrap}
|
| 26 |
+
.stage-status i {margin-top:0!important}.stage-status.busy i {margin-top:0!important}
|
| 27 |
+
.stage-status .wait-clock {white-space:nowrap;margin-left:8px}
|
| 28 |
+
#stage-toolbar.client-pending #stage-status {display:none!important}
|
| 29 |
+
.client-notice {max-width:100%;flex:1}
|
| 30 |
+
#canvas-viewport,#canvas-viewport:has(.focus-caption) {padding:0 var(--stage-gutter)!important;overflow:hidden!important}
|
| 31 |
+
#stage-image {height:clamp(300px,48vh,520px)!important;overflow:visible!important;transition:transform 180ms ease}
|
| 32 |
+
#stage-image .image-container {background:#f6f5f8!important;border-radius:6px;overflow:hidden}
|
| 33 |
+
#stage-image img {box-shadow:none;border-radius:0}
|
| 34 |
+
#stage-image .icon-button-wrapper {display:none!important}
|
| 35 |
+
#canvas-chrome {position:relative!important;inset:auto!important;flex:0 0 auto!important;background:#fff}
|
| 36 |
+
.canvas-tools {padding:8px 0;min-height:48px;gap:8px}
|
| 37 |
+
.image-dimensions {border:0;background:transparent;padding:0;color:var(--muted)}
|
| 38 |
+
.zoom-tools {border-color:#e8e4ec;border-radius:6px;box-shadow:none}
|
| 39 |
+
.zoom-tools button,.canvas-download {width:32px;height:32px}
|
| 40 |
+
.zoom-value {min-width:42px}
|
| 41 |
+
.focus-halo {fill:none;stroke:#fff;stroke-width:5;vector-effect:non-scaling-stroke;opacity:.95}
|
| 42 |
+
.focus-outline {fill:none;stroke-width:2.5;vector-effect:non-scaling-stroke;animation:none}
|
| 43 |
+
.focus-dimmer {opacity:.16;animation:none}
|
| 44 |
+
.focus-caption {width:100%!important;max-width:100%;box-sizing:border-box;margin:0 0 10px;padding:6px 8px 6px 12px;box-shadow:none;background:#f8f6fb;gap:6px}
|
| 45 |
+
.focus-caption span {max-height:none;font-size:12px;flex:1}
|
| 46 |
+
.focus-caption button {width:32px;height:32px;flex-shrink:0}
|
| 47 |
+
#query-row {padding:6px var(--stage-gutter) 14px!important}
|
| 48 |
+
#query-dock {max-width:none;width:100%;padding:10px 12px!important;box-shadow:none!important;border-color:#ddd6e6!important}
|
| 49 |
+
#query-dock textarea {font-size:12px!important;line-height:1.7!important}
|
| 50 |
+
.query-hint {text-align:left;margin:7px 0 0;font-size:10px}
|
| 51 |
+
#targets-panel {padding:0 var(--stage-gutter) 20px!important;gap:0!important;border:0!important;min-width:0!important}
|
| 52 |
+
.targets-summary {border-top:1px solid var(--line);padding-top:14px;font-size:12px}
|
| 53 |
+
.targets-summary b {font-size:16px}.targets-summary>span {font-size:11px}
|
| 54 |
+
.target-chips {display:flex!important;flex-direction:row;flex-wrap:wrap;max-height:140px!important;gap:6px;margin:10px 0 0;padding:2px}
|
| 55 |
+
.target-chip {width:auto!important;flex:0 1 auto;min-width:0;min-height:34px!important;padding:7px 10px!important;gap:7px!important;background:#fff!important;border-radius:6px!important;font-size:11px!important}
|
| 56 |
+
.target-chip>span {overflow-wrap:anywhere}.target-chip b {margin-left:3px}
|
| 57 |
+
.target-chip[aria-pressed=true] {border-color:var(--purple)!important;background:#f0eaf7!important}
|
| 58 |
+
.target-chip:focus-visible {outline:2px solid var(--purple);outline-offset:2px}
|
| 59 |
+
.focus-help {margin-top:10px;gap:12px}.targets-caption {display:none}
|
| 60 |
+
.empty-targets {min-height:0;padding:12px 0;border-top:1px solid var(--line);text-align:left;align-items:flex-start}
|
| 61 |
+
.empty-targets strong {font-size:12px}.empty-targets span {font-size:11px}.empty-targets svg {display:none}
|
| 62 |
+
#review-row {gap:16px!important}
|
| 63 |
+
#sandbox-panel {flex-basis:40%!important;box-shadow:none;padding:18px!important}
|
| 64 |
+
#review-summary {min-width:0!important;box-shadow:none}
|
| 65 |
+
#result-columns {gap:16px!important}
|
| 66 |
+
#metrics-panel {min-width:0!important;flex:1 1 45%!important}
|
| 67 |
+
#sidebar {min-width:0!important;flex:1 1 55%!important}
|
| 68 |
+
#sidebar:not(:has(.evidence-detail:not([hidden]))) {display:none!important}
|
| 69 |
+
#result-columns:not(:has(.evidence-detail:not([hidden]))) .metrics-list {display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 24px}
|
| 70 |
+
.detail-image {height:170px}.evidence-detail {max-width:none}
|
| 71 |
+
#prompt-console,#output-console {box-shadow:none!important;padding:12px 18px!important}
|
| 72 |
+
.studio-header {padding-bottom:18px}.studio-header h1 {font-size:30px}
|
| 73 |
+
@media(min-width:1500px){#controls {flex-basis:310px!important;width:310px;padding:28px!important}.control-brand .xpeng-logo {width:190px}#visual-stage {--stage-gutter:32px}#stage-image {height:clamp(420px,50vh,580px)!important}}
|
| 74 |
+
@media(max-width:1100px){#controls {flex-basis:246px!important;width:246px;padding:18px!important}.control-brand h2 {font-size:14cqw}#visual-stage {--stage-gutter:18px}#stage-image {height:clamp(300px,40vw,440px)!important}.header-scope {display:none}#review-row {flex-wrap:wrap!important}#sandbox-panel {flex:1 1 34%!important}#review-summary {flex:1 1 58%!important}}
|
| 75 |
+
@media(max-width:760px){
|
| 76 |
+
.studio-header h1 {font-size:25px}.header-description {font-size:12px}
|
| 77 |
+
#controls {width:100%;flex:0 0 auto!important;padding:16px!important;gap:12px!important}
|
| 78 |
+
.control-brand {container-type:normal;gap:10px;flex-wrap:nowrap}
|
| 79 |
+
.control-brand h2 {font-size:clamp(22px,6vw,30px);white-space:nowrap}
|
| 80 |
+
.control-brand .xpeng-logo {width:96px;max-width:30%}
|
| 81 |
+
#control-settings {padding:0!important;border:0!important;gap:10px!important}
|
| 82 |
+
#control-fields {flex-direction:row!important;align-items:center;gap:16px!important}
|
| 83 |
+
#control-fields>.form {width:0!important;flex:1 1 0!important}
|
| 84 |
+
#media-type {flex:0 0 30%!important}.media-field {display:block}.media-field>span {margin-bottom:7px!important}
|
| 85 |
+
.media-field>div {height:39px}.media-field>div>span {min-width:60px}
|
| 86 |
+
#service-panel {padding-top:6px!important}.api-status small,.connection-detail {display:none}
|
| 87 |
+
#service-panel #health-button {min-height:28px!important;font-size:11px!important}
|
| 88 |
+
#visual-stage {--stage-gutter:14px}
|
| 89 |
+
#task-guide {padding-top:16px!important}.task-guide strong {font-size:16px}
|
| 90 |
+
#stage-image {height:clamp(230px,68vw,440px)!important}
|
| 91 |
+
#stage-toolbar {padding-top:10px!important;padding-bottom:10px!important}
|
| 92 |
+
.canvas-tools {flex-wrap:wrap}.zoom-tools button,.canvas-download {width:38px;height:38px}
|
| 93 |
+
#query-row {padding-top:4px!important}#mobile-run-button {border-radius:8px!important;font-size:14px!important;margin-top:10px}
|
| 94 |
+
#targets-panel {padding-bottom:16px!important}.target-chip {min-height:40px!important}.target-chips {max-height:none!important}
|
| 95 |
+
.focus-caption button {width:38px;height:38px}.focus-caption span {font-size:11px}
|
| 96 |
+
#review-row {flex-direction:column!important}#sandbox-panel,#review-summary {flex:0 0 auto!important}
|
| 97 |
+
#metrics-panel,#sidebar {flex:0 0 auto!important}
|
| 98 |
+
}
|
docs/UI_STUDIO_R8.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GroundingPI Studio r8
|
| 2 |
+
|
| 3 |
+
User-requested layout and visualization revision, 2026-09-16. The same UI files
|
| 4 |
+
serve the standalone demo and the private Hugging Face Space.
|
| 5 |
+
|
| 6 |
+
## Changes
|
| 7 |
+
|
| 8 |
+
- Container-relative brand sizing prevents the GroundingPI title from clipping.
|
| 9 |
+
- Bounded sidebar, consistent canvas/query gutters, visible desktop action, and
|
| 10 |
+
stacked mobile controls. Native image sharing is explicitly disabled.
|
| 11 |
+
- Request identifiers let the browser distinguish consecutive identical outputs.
|
| 12 |
+
The immediate client acknowledgement owns a separate node; it never replaces
|
| 13 |
+
Gradio/Svelte-managed status DOM. Completion/error clears the timer and inert state.
|
| 14 |
+
- Category chips sit next to the canvas workflow. Selection shows original pixels,
|
| 15 |
+
exact returned geometry, category-colored focus with a contrast halo, instance
|
| 16 |
+
navigation and a magnified crop. Closing focus restores every prediction.
|
| 17 |
+
- Dense boxes use fine edges and short corner marks, with no filled interiors.
|
| 18 |
+
No prediction is suppressed, merged, moved, deduplicated or replaced.
|
| 19 |
+
- Empty detail panels stay hidden; raw prompt/output remain available in accordions.
|
| 20 |
+
|
| 21 |
+
## Contract
|
| 22 |
+
|
| 23 |
+
The 27 examples, 9 task routes, protocol parser, prompts, native inference backend,
|
| 24 |
+
model revision and generated coordinates are unchanged. Dataset annotations are
|
| 25 |
+
never substituted for predictions. The original r7 manifest remains as provenance;
|
| 26 |
+
`ui_studio_r8_lock.json` records the reviewed new UI bytes and unchanged case contract.
|
| 27 |
+
|
| 28 |
+
## Verification scope
|
| 29 |
+
|
| 30 |
+
Console request lifecycle, protocol/render regressions and packaging lock checks;
|
| 31 |
+
browser review at mobile, intermediate and desktop widths; success, error and repeat
|
| 32 |
+
requests; category/instance focus, zoom/reset, example switching and upload reset.
|
| 33 |
+
Local visual regression uses a separate, non-deployed HTTP fixture replaying a
|
| 34 |
+
recorded real 27-target HF response. Its latency is test latency. The deployed HF
|
| 35 |
+
Space is checked with a fresh real model request after release; these checks are
|
| 36 |
+
qualitative UI acceptance, not a benchmark.
|
docs/ui_studio_r8_lock.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": {
|
| 3 |
+
"app.py": "95f3f0d399588edf789fecdd7099ef4622f180cdb7fe8389989bf0951a89a78f",
|
| 4 |
+
"assets/Manrope-OFL.txt": "e01b637272e0cbdfb240184dd98ea5cc671556d9894dae2668d92ab2c906787c",
|
| 5 |
+
"assets/Manrope-variable.ttf": "d0639be45d0af36e798172419d7bd173c4bd4f29e2b76cbb69db1d11bf8b0a40",
|
| 6 |
+
"assets/SOURCES.md": "3cceb52136b55f86840e9585e3c37faed1adc762452b0fea50aecba696dac7fa",
|
| 7 |
+
"assets/console.js": "3af36a3aeafb72e85018b1c663f624b92877bad0152ee9e264a1c440d689f60e",
|
| 8 |
+
"assets/frosted.css": "393bf5f1c04a49a6e9b17e855e573646489b4add85bc7973eb6bb277686b5c80",
|
| 9 |
+
"assets/original.css": "7a244c7f65e40797850dcde7d6b6505b1b5de60aa24b129f44b33ae52291fd2e",
|
| 10 |
+
"assets/paper.css": "61fc60bf8c379a0894fbfe06574964ec2ed256791a23860fa4dbb872eb2869f9",
|
| 11 |
+
"assets/studio.css": "3275fe0212672640f477e4273a6c8052648f361c7631400aaf6ad2aaebc89185",
|
| 12 |
+
"assets/xpeng-logo.png": "ff0db78ddf8f9a122eec74e8eb79581f8e5dbc46803dddeb780a06f5ae295750",
|
| 13 |
+
"cases/dense/dense-01-o365_bbox_gt50_00009110.jpg": "398038106cbac731516dd01de25354fe4d61f3599df64baa611f1d5a99e0e8fb",
|
| 14 |
+
"cases/dense/dense-02-o365_bbox_gt50_00004676.jpg": "7ee3beb63bdfd50c183538bd56f9d7e079fa41bde78aad9d5fe0d22c041c2673",
|
| 15 |
+
"cases/dense/dense-03-o365_bbox_gt50_00011992.jpg": "eb9331146b66a1af1e8c832a22043c6f1b45d5d3bb7231816f975ced2cd88e39",
|
| 16 |
+
"cases/dense_point/dense_point-01-sensenova_sa1b_point_1067906__phrase_000.jpg": "917975c9b31791290a2a660c91af8ae0653cfe258fa0c53539bcb868867aece2",
|
| 17 |
+
"cases/dense_point/dense_point-02-sensenova_sa1b_point_1445784__phrase_002.jpg": "0b9a2281fec580c02b9292d8b3f96b675a8659d721f4dd8a1ae16a1eadbaf4e9",
|
| 18 |
+
"cases/dense_point/dense_point-03-sensenova_sa1b_point_1451598__phrase_001.jpg": "d2f0e070f49cc001ef6855964175c8c383358fcc2185d62abf29ba0de0d1be22",
|
| 19 |
+
"cases/grounding/grounding-01-object365-locate_anything_object365_object365-file-0027-row-000000119924.jpg": "2e3946eafcbdc2d2ac9e0b62343595e0b51e71b38eb15c3b0669fab46c78e9f6",
|
| 20 |
+
"cases/grounding/grounding-02-object365-locate_anything_object365_object365-file-0027-row-000001469030.jpg": "c6abef94a28290ca94e8dd043ba58e326cfcb22088b75f32274acc07af15950c",
|
| 21 |
+
"cases/grounding/grounding-03-object365-locate_anything_object365_object365-file-0027-row-000001076761.jpg": "ff9581327870a05ad8679c3e5891f27dfe55acc98ed713a8abf8d02e7604774b",
|
| 22 |
+
"cases/gui/gui-01-groundcua-locate_anything_groundcua_groundcua-file-0014-row-000000043686__gui_0008.jpg": "c9c27a9df49bac1c9664eb74ecf1fe9967e20281add73fff1273437af06b743b",
|
| 23 |
+
"cases/gui/gui-02-groundcua-locate_anything_groundcua_groundcua-file-0014-row-000000006009__gui_0028.jpg": "7ff12fde621319e2959b3112fa6c1fe4e6ef83b8943a6ee169d7fc2140f59e74",
|
| 24 |
+
"cases/gui/gui-03-groundcua-locate_anything_groundcua_groundcua-file-0014-row-000000041853__gui_0052.jpg": "8ba06f95a90cf6954aa7c8f7554b4f28d966afbfc8b8151c96e540e224139cf5",
|
| 25 |
+
"cases/layout/layout-01-layout_doclaynet_train_16966.jpg": "8ba09e49907c5d7a3a31148a3fceaf2695bd04f35a017a62ef8c455b985f7218",
|
| 26 |
+
"cases/layout/layout-02-layout_doclaynet_train_20356.jpg": "5fbf03f82cc66566e4630547b53b7f51cd0ebd772a072c1d6f37f8c0bafdb646",
|
| 27 |
+
"cases/layout/layout-03-layout_doclaynet_train_3435.jpg": "8e035b83d1ca53298816550d4891fd9f2d84f83b59bba178cafdf6bf8623a9fe",
|
| 28 |
+
"cases/ocr/ocr-01-ocr_textocr-locate_anything_ocr_textocr_textocr-file-0042-row-000000019189.jpg": "4634583edfab147e8708c6119e726fb8f394744e0225c8319136bb5000ba5d8c",
|
| 29 |
+
"cases/ocr/ocr-02-ocr_textocr-locate_anything_ocr_textocr_textocr-file-0042-row-000000018671.jpg": "8db936cadff524299c5b61b94992f3b70ec90e1dd3873f072b34d41692393bdd",
|
| 30 |
+
"cases/ocr/ocr-03-ocr_textocr-locate_anything_ocr_textocr_textocr-file-0042-row-000000021130.jpg": "d959da84d676117701e6d3920bff692aab94d6075e5714e2a324df3b61f18cf8",
|
| 31 |
+
"cases/refer_point/refer_point-01-roboafford_affordance_point_paco_491580_000.jpg": "e5233bcfadb4d6cb3be1a0ac75573183084494661dd31cc42e7282d21b6c3e13",
|
| 32 |
+
"cases/refer_point/refer_point-02-roboafford_affordance_point_paco_558444_002.jpg": "df60dcc4812e93720202874e7b88cca122b25ff2615dd31e2bd697a552bce322",
|
| 33 |
+
"cases/refer_point/refer_point-03-roboafford_affordance_point_paco_311488_000.jpg": "e4fc006bbcb49c5e57c3b002a35e5866c15c898d6930117e0f89617121cd3928",
|
| 34 |
+
"cases/referring/referring-01-humanref_nocot_lf2_00007963.jpg": "4fac0cc45af121260bc7468aa63fbe7f6dfdcbc5a2f251c04a8a0e06212048df",
|
| 35 |
+
"cases/referring/referring-02-humanref_nocot_lf2_00004121.jpg": "9b7ec8dc3ad013a57fab9d1199ee0ce14a3104f8e56f60df908b8769d32e195d",
|
| 36 |
+
"cases/referring/referring-03-humanref_nocot_lf2_00011458.jpg": "44978de1893cdcf9c0c3ed7dd40df3d3f38b7c5334ba0929d2604a2a2fb5d561",
|
| 37 |
+
"cases/visual_prompt/visual_prompt-01-sensenova_sa1b_visual_1718306.jpg": "57104d07c82cfb97a72075ef479f7d8d1c00db82386aa5c38da0ef163a82dc2f",
|
| 38 |
+
"cases/visual_prompt/visual_prompt-02-sensenova_sa1b_visual_2475968.jpg": "ba96beb1cc54682f56e34b1381fefc9b7b95905363cff917bbfdac886e265bac",
|
| 39 |
+
"cases/visual_prompt/visual_prompt-03-sensenova_sa1b_visual_1573415.jpg": "2593b4f81c4fbfd70e73df0710fb143af232126daa0a96bc86c72575713475a5",
|
| 40 |
+
"grounding_demo/catalog.py": "75e543fed7f0ca018fb41c4b528efc2901aa6a0f05232959297d6d40798f676f",
|
| 41 |
+
"grounding_demo/console.py": "dbc1a08c937f18d5c104d4ab18316937d21a2b0d2df9ba2cba296d9ae1e20f16",
|
| 42 |
+
"grounding_demo/protocol.py": "aa4e634070ab7a17c6bc477051e97cb02e772e9e37db89d5de9b6497312635b0",
|
| 43 |
+
"grounding_demo/render.py": "a4d43398a7d42a2f7884b16141bd43be3b687fdd7ce24fa89405341fb6152b21"
|
| 44 |
+
},
|
| 45 |
+
"case_contract_sha256": "d00f2091dcb3c866e5e3a331f89537839640b2374c4bc32685960706d18b58b1",
|
| 46 |
+
"cases": 27,
|
| 47 |
+
"routes": 9
|
| 48 |
+
}
|
grounding_demo/render.py
CHANGED
|
@@ -48,7 +48,11 @@ def draw_primitives(
|
|
| 48 |
overlay = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
| 49 |
draw = ImageDraw.Draw(overlay)
|
| 50 |
width, height = canvas.size
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
radius = max(6, line * 3)
|
| 53 |
font = _font(max(13, round(min(width, height) / 42)))
|
| 54 |
label_colors: dict[str, tuple[int, int, int]] = {}
|
|
@@ -65,7 +69,6 @@ def draw_primitives(
|
|
| 65 |
draw.line((x, y1, min(x + dash, x2), y1), fill=(*GREEN, 255), width=line)
|
| 66 |
draw.line((x, y2, min(x + dash, x2), y2), fill=(*GREEN, 255), width=line)
|
| 67 |
|
| 68 |
-
primitives = list(primitives)
|
| 69 |
labels = []
|
| 70 |
# Opaque RGBA writes replace earlier pixels in a Pillow layer. In particular,
|
| 71 |
# a later lake/cloud fill used to erase every small object drawn inside it.
|
|
@@ -86,6 +89,11 @@ def draw_primitives(
|
|
| 86 |
else:
|
| 87 |
x1, y1, x2, y2 = coords
|
| 88 |
draw.rectangle(coords, outline=(*color, 255), width=line)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if seen[primitive.label] == 1:
|
| 90 |
labels.append((primitive.label, color, coords))
|
| 91 |
|
|
|
|
| 48 |
overlay = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
| 49 |
draw = ImageDraw.Draw(overlay)
|
| 50 |
width, height = canvas.size
|
| 51 |
+
primitives = list(primitives)
|
| 52 |
+
dense = len(primitives) > 12
|
| 53 |
+
# Fine boundaries keep a dense scene readable; short corners make small
|
| 54 |
+
# boxes discoverable without filling, moving, or removing any prediction.
|
| 55 |
+
line = max(1 if dense else 2, round(min(width, height) / (420 if dense else 260)))
|
| 56 |
radius = max(6, line * 3)
|
| 57 |
font = _font(max(13, round(min(width, height) / 42)))
|
| 58 |
label_colors: dict[str, tuple[int, int, int]] = {}
|
|
|
|
| 69 |
draw.line((x, y1, min(x + dash, x2), y1), fill=(*GREEN, 255), width=line)
|
| 70 |
draw.line((x, y2, min(x + dash, x2), y2), fill=(*GREEN, 255), width=line)
|
| 71 |
|
|
|
|
| 72 |
labels = []
|
| 73 |
# Opaque RGBA writes replace earlier pixels in a Pillow layer. In particular,
|
| 74 |
# a later lake/cloud fill used to erase every small object drawn inside it.
|
|
|
|
| 89 |
else:
|
| 90 |
x1, y1, x2, y2 = coords
|
| 91 |
draw.rectangle(coords, outline=(*color, 255), width=line)
|
| 92 |
+
if dense:
|
| 93 |
+
corner = min(max(4, round(min(width, height) / 80)), (x2-x1)//3, (y2-y1)//3)
|
| 94 |
+
if corner > 0:
|
| 95 |
+
for x, y, dx, dy in ((x1,y1,1,1),(x2,y1,-1,1),(x1,y2,1,-1),(x2,y2,-1,-1)):
|
| 96 |
+
draw.line((x+dx*corner,y,x,y,x,y+dy*corner), fill=(*color,255), width=max(2,line))
|
| 97 |
if seen[primitive.label] == 1:
|
| 98 |
labels.append((primitive.label, color, coords))
|
| 99 |
|
scripts/build_bundle.py
CHANGED
|
@@ -17,7 +17,9 @@ def main():
|
|
| 17 |
if args.output.exists():
|
| 18 |
raise SystemExit("Output already exists; choose a new archive path.")
|
| 19 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 20 |
-
lock = ROOT / "docs/
|
|
|
|
|
|
|
| 21 |
if not lock.exists():
|
| 22 |
lock = ROOT / "docs/ui_lock.json"
|
| 23 |
if fingerprint(ROOT) != json.loads(lock.read_text()):
|
|
@@ -33,6 +35,8 @@ def main():
|
|
| 33 |
"scripts/prepare_model_repo.py", "scripts/validate_gradio.py"]
|
| 34 |
paths = [ROOT / name for name in top]
|
| 35 |
paths.extend([lock, ROOT / "docs/UI_PAPER_R7.md"])
|
|
|
|
|
|
|
| 36 |
for folder, suffixes in {"assets": {".css", ".js", ".png", ".ttf", ".txt", ".md"},
|
| 37 |
"grounding_demo": {".py"}, "tests": {".py"}}.items():
|
| 38 |
paths.extend(p for p in (ROOT / folder).rglob("*") if p.is_file() and p.suffix in suffixes)
|
|
|
|
| 17 |
if args.output.exists():
|
| 18 |
raise SystemExit("Output already exists; choose a new archive path.")
|
| 19 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 20 |
+
lock = ROOT / "docs/ui_studio_r8_lock.json"
|
| 21 |
+
if not lock.exists():
|
| 22 |
+
lock = ROOT / "docs/ui_paper_r7_lock.json"
|
| 23 |
if not lock.exists():
|
| 24 |
lock = ROOT / "docs/ui_lock.json"
|
| 25 |
if fingerprint(ROOT) != json.loads(lock.read_text()):
|
|
|
|
| 35 |
"scripts/prepare_model_repo.py", "scripts/validate_gradio.py"]
|
| 36 |
paths = [ROOT / name for name in top]
|
| 37 |
paths.extend([lock, ROOT / "docs/UI_PAPER_R7.md"])
|
| 38 |
+
if (ROOT / "docs/UI_STUDIO_R8.md").exists():
|
| 39 |
+
paths.append(ROOT / "docs/UI_STUDIO_R8.md")
|
| 40 |
for folder, suffixes in {"assets": {".css", ".js", ".png", ".ttf", ".txt", ".md"},
|
| 41 |
"grounding_demo": {".py"}, "tests": {".py"}}.items():
|
| 42 |
paths.extend(p for p in (ROOT / folder).rglob("*") if p.is_file() and p.suffix in suffixes)
|
scripts/verify_ui.py
CHANGED
|
@@ -29,7 +29,9 @@ def main():
|
|
| 29 |
parser.add_argument("--reference", type=Path, help="Original project; compare then create lock without overwrite")
|
| 30 |
args = parser.parse_args()
|
| 31 |
current = fingerprint(ROOT)
|
| 32 |
-
lock = ROOT / "docs/
|
|
|
|
|
|
|
| 33 |
if not lock.exists():
|
| 34 |
lock = ROOT / "docs/ui_lock.json"
|
| 35 |
if args.reference:
|
|
|
|
| 29 |
parser.add_argument("--reference", type=Path, help="Original project; compare then create lock without overwrite")
|
| 30 |
args = parser.parse_args()
|
| 31 |
current = fingerprint(ROOT)
|
| 32 |
+
lock = ROOT / "docs/ui_studio_r8_lock.json"
|
| 33 |
+
if not lock.exists():
|
| 34 |
+
lock = ROOT / "docs/ui_paper_r7_lock.json"
|
| 35 |
if not lock.exists():
|
| 36 |
lock = ROOT / "docs/ui_lock.json"
|
| 37 |
if args.reference:
|
tests/test_console.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import json
|
| 2 |
import html
|
| 3 |
import unittest
|
|
|
|
| 4 |
from html.parser import HTMLParser
|
| 5 |
from unittest.mock import patch
|
| 6 |
|
|
@@ -9,6 +10,22 @@ from grounding_demo.console import compile_query, query_from_prompt, targets_htm
|
|
| 9 |
|
| 10 |
|
| 11 |
class ConsoleTest(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
def test_all_27_prompts_roundtrip_exactly(self):
|
| 13 |
for row in app.CASES:
|
| 14 |
with self.subTest(case=row["id"]):
|
|
|
|
| 1 |
import json
|
| 2 |
import html
|
| 3 |
import unittest
|
| 4 |
+
import re
|
| 5 |
from html.parser import HTMLParser
|
| 6 |
from unittest.mock import patch
|
| 7 |
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class ConsoleTest(unittest.TestCase):
|
| 13 |
+
def test_request_ids_distinguish_identical_results_and_errors(self):
|
| 14 |
+
path = str(app.ROOT / app.CASES[0]["image"])
|
| 15 |
+
raw = "<|object_ref_start|>car<|object_ref_end|><|box_start|><1><2><30><40><|box_end|>"
|
| 16 |
+
runs = []
|
| 17 |
+
for error in (False, False, True):
|
| 18 |
+
with patch.object(app.GroundingClient, "infer", return_value=(raw, .1, {}),
|
| 19 |
+
side_effect=TimeoutError("test") if error else None):
|
| 20 |
+
states = list(app.run_console(path, "car", "grounding"))
|
| 21 |
+
ids = [re.search("data-run-id='([^']+)'", state[5]).group(1) for state in states]
|
| 22 |
+
self.assertEqual(ids[0], ids[-1])
|
| 23 |
+
self.assertIn("data-phase='busy'", states[0][5])
|
| 24 |
+
self.assertNotIn("data-phase='busy'", states[-1][5])
|
| 25 |
+
self.assertTrue(states[-1][-1]["interactive"])
|
| 26 |
+
runs.append(ids[0])
|
| 27 |
+
self.assertEqual(len(set(runs)), 3)
|
| 28 |
+
|
| 29 |
def test_all_27_prompts_roundtrip_exactly(self):
|
| 30 |
for row in app.CASES:
|
| 31 |
with self.subTest(case=row["id"]):
|