Fourth UI pass: stats tiles + tame Gradio empty placeholders
Browse filesSummary tiles
- render_stats_html() formats the run's headline numbers as a grid
of brand-colored tiles (selected patches, groups, sampled frames,
canvas, uniform baseline, saliency, patch grid, codec, elapsed).
- process() now returns a 5th output and the Run button wires it
into a new gr.HTML at the very top of the output column.
- Empty state is itself a tile-styled placeholder so the column
never feels broken.
Empty placeholder polish (fix for 'output area has ugly black frame')
- Drop the redundant ovc-empty placeholder gr.HTMLs that sat above
each output component; they overlapped Gradio's own empty state
and looked duplicated.
- Blanket-override Gradio's dark default backgrounds inside .ovc-card
(.video-container / .image-container / .image-frame / .preview /
.plot-container / video / [data-testid="video"|"image"] / .icon-button
/ .upload-container) so the inner zones are transparent.
- Repaint those wrappers with a soft brand-tinted gradient + dashed
border so the empty shape reads as "nothing here yet" rather than
"broken player".
JSON pane renamed to "Raw JSON" so it does not collide with the new
top-of-column "Run summary" tile card.
|
@@ -452,6 +452,41 @@ def pack_canvas(
|
|
| 452 |
return canvas, n
|
| 453 |
|
| 454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
def make_charts(
|
| 456 |
grids: List[np.ndarray],
|
| 457 |
masks: List[np.ndarray],
|
|
@@ -574,7 +609,7 @@ def process(
|
|
| 574 |
progress=gr.Progress(track_tqdm=False),
|
| 575 |
):
|
| 576 |
if not video_path:
|
| 577 |
-
return None, None, "Please upload a video.", None
|
| 578 |
|
| 579 |
t0 = time.time()
|
| 580 |
progress(0.05, desc="Reading metadata")
|
|
@@ -584,7 +619,7 @@ def process(
|
|
| 584 |
return None, None, json.dumps(
|
| 585 |
{"error": "Could not read frame count.", "metadata": meta},
|
| 586 |
indent=2, ensure_ascii=False,
|
| 587 |
-
), None
|
| 588 |
|
| 589 |
progress(0.10, desc="Sampling frames")
|
| 590 |
fps = float(meta.get("fps") or 0.0)
|
|
@@ -614,7 +649,7 @@ def process(
|
|
| 614 |
return None, None, json.dumps(
|
| 615 |
{"error": "Failed to decode frames.", "metadata": meta},
|
| 616 |
indent=2, ensure_ascii=False,
|
| 617 |
-
), None
|
| 618 |
|
| 619 |
progress(0.25, desc="smart_resize")
|
| 620 |
resized = [smart_resize(f, int(max_pixels), int(patch_size)) for f in raw]
|
|
@@ -729,11 +764,27 @@ def process(
|
|
| 729 |
groups=groups, gop_label=gop_resolved,
|
| 730 |
)
|
| 731 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
progress(1.0, desc="Done")
|
| 733 |
return (
|
| 734 |
vis_path, canvas_path,
|
| 735 |
json.dumps(info, indent=2, ensure_ascii=False),
|
| 736 |
chart_fig,
|
|
|
|
| 737 |
)
|
| 738 |
|
| 739 |
|
|
@@ -995,24 +1046,80 @@ CUSTOM_CSS = """
|
|
| 995 |
}
|
| 996 |
#ovc-flow .arrow { color: #94a3b8; font-size: 0.95rem; user-select: none; }
|
| 997 |
|
| 998 |
-
/*
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1007 |
}
|
| 1008 |
-
|
| 1009 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
background: var(--ovc-grad);
|
| 1011 |
-
color:
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
font-weight: 600;
|
| 1017 |
}
|
| 1018 |
"""
|
|
@@ -1096,21 +1203,18 @@ FLOW_HTML = """
|
|
| 1096 |
</div>
|
| 1097 |
"""
|
| 1098 |
|
| 1099 |
-
|
| 1100 |
-
'<div class="ovc-
|
| 1101 |
-
'
|
| 1102 |
-
'
|
| 1103 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1104 |
'</div>'
|
| 1105 |
-
|
| 1106 |
-
EMPTY_CHART = (
|
| 1107 |
-
'<div class="ovc-empty">'
|
| 1108 |
-
'Codec-vs-uniform timeline charts will appear here after a run.'
|
| 1109 |
'</div>'
|
| 1110 |
-
)
|
| 1111 |
-
EMPTY_CANVAS = (
|
| 1112 |
-
'<div class="ovc-empty">'
|
| 1113 |
-
'The packed canvas (LLaVA-OneVision input) will appear here.'
|
| 1114 |
'</div>'
|
| 1115 |
)
|
| 1116 |
|
|
@@ -1217,9 +1321,12 @@ with gr.Blocks(**_BLOCK_KW) as demo:
|
|
| 1217 |
|
| 1218 |
# βββ Outputs (wide column) βββββββββββββββββββββββββββββββββββββββ
|
| 1219 |
with gr.Column(scale=6, min_width=420):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1220 |
with gr.Group(elem_classes="ovc-card ovc-card-primary"):
|
| 1221 |
gr.Markdown("### Patch selection visualization")
|
| 1222 |
-
gr.HTML(EMPTY_VIS)
|
| 1223 |
vis_out = gr.Video(
|
| 1224 |
label="", show_label=False, autoplay=True, height=420,
|
| 1225 |
)
|
|
@@ -1234,25 +1341,24 @@ with gr.Blocks(**_BLOCK_KW) as demo:
|
|
| 1234 |
"spend the same budget β spread evenly in time, every "
|
| 1235 |
"patch in the chosen frames kept.</small>"
|
| 1236 |
)
|
| 1237 |
-
gr.HTML(EMPTY_CHART)
|
| 1238 |
chart_out = gr.Plot(label="", show_label=False)
|
| 1239 |
|
| 1240 |
with gr.Row():
|
| 1241 |
with gr.Column(scale=1):
|
| 1242 |
with gr.Group(elem_classes="ovc-card"):
|
| 1243 |
gr.Markdown("### Packed canvas")
|
| 1244 |
-
gr.HTML(EMPTY_CANVAS)
|
| 1245 |
canvas_out = gr.Image(
|
| 1246 |
label="", show_label=False, height=320,
|
| 1247 |
)
|
| 1248 |
with gr.Column(scale=1):
|
| 1249 |
with gr.Group(elem_classes="ovc-card"):
|
| 1250 |
-
gr.Markdown("###
|
| 1251 |
gr.Markdown(
|
| 1252 |
-
"<small>
|
| 1253 |
-
"
|
|
|
|
| 1254 |
)
|
| 1255 |
-
with gr.Accordion("Show full
|
| 1256 |
info_out = gr.Code(
|
| 1257 |
label="", language="json", lines=18,
|
| 1258 |
)
|
|
@@ -1326,7 +1432,7 @@ with gr.Blocks(**_BLOCK_KW) as demo:
|
|
| 1326 |
saliency_signal, score_log_scale, bitcost_pct, fade_strength,
|
| 1327 |
gop,
|
| 1328 |
],
|
| 1329 |
-
outputs=[vis_out, canvas_out, info_out, chart_out],
|
| 1330 |
)
|
| 1331 |
|
| 1332 |
|
|
|
|
| 452 |
return canvas, n
|
| 453 |
|
| 454 |
|
| 455 |
+
def render_stats_html(
|
| 456 |
+
*, total_selected: int, n_groups: int, gop_label: str,
|
| 457 |
+
n_sampled: int, total_frames: int, canvas_resolution: str,
|
| 458 |
+
saliency_signal: str, score_log_scale: bool, elapsed_sec: float,
|
| 459 |
+
grid_label: str, n_uniform: int, codec: str = "",
|
| 460 |
+
) -> str:
|
| 461 |
+
"""Format the run's headline numbers as a grid of brand-colored tiles."""
|
| 462 |
+
sig = saliency_signal + (" Β· log" if score_log_scale else "")
|
| 463 |
+
gop_disp = (
|
| 464 |
+
gop_label if gop_label in ("global", "dynamic") else f"GOP={gop_label}"
|
| 465 |
+
)
|
| 466 |
+
cells = [
|
| 467 |
+
("selected patches", f"{total_selected:,}"),
|
| 468 |
+
("groups", f"{n_groups} Β· {gop_disp}"),
|
| 469 |
+
("sampled frames", f"{n_sampled} / {total_frames}"),
|
| 470 |
+
("canvas", canvas_resolution),
|
| 471 |
+
("uniform baseline", f"{n_uniform} frame{'s' if n_uniform != 1 else ''}"),
|
| 472 |
+
("saliency", sig),
|
| 473 |
+
("patch grid / frame", grid_label),
|
| 474 |
+
("elapsed", f"{elapsed_sec:.2f}s"),
|
| 475 |
+
]
|
| 476 |
+
if codec:
|
| 477 |
+
cells.insert(2, ("input codec", codec))
|
| 478 |
+
parts = ['<div class="ovc-stats">']
|
| 479 |
+
for label, value in cells:
|
| 480 |
+
parts.append(
|
| 481 |
+
f'<div class="ovc-stat">'
|
| 482 |
+
f'<div class="value">{value}</div>'
|
| 483 |
+
f'<div class="label">{label}</div>'
|
| 484 |
+
f'</div>'
|
| 485 |
+
)
|
| 486 |
+
parts.append("</div>")
|
| 487 |
+
return "".join(parts)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
def make_charts(
|
| 491 |
grids: List[np.ndarray],
|
| 492 |
masks: List[np.ndarray],
|
|
|
|
| 609 |
progress=gr.Progress(track_tqdm=False),
|
| 610 |
):
|
| 611 |
if not video_path:
|
| 612 |
+
return None, None, "Please upload a video.", None, EMPTY_STATS
|
| 613 |
|
| 614 |
t0 = time.time()
|
| 615 |
progress(0.05, desc="Reading metadata")
|
|
|
|
| 619 |
return None, None, json.dumps(
|
| 620 |
{"error": "Could not read frame count.", "metadata": meta},
|
| 621 |
indent=2, ensure_ascii=False,
|
| 622 |
+
), None, EMPTY_STATS
|
| 623 |
|
| 624 |
progress(0.10, desc="Sampling frames")
|
| 625 |
fps = float(meta.get("fps") or 0.0)
|
|
|
|
| 649 |
return None, None, json.dumps(
|
| 650 |
{"error": "Failed to decode frames.", "metadata": meta},
|
| 651 |
indent=2, ensure_ascii=False,
|
| 652 |
+
), None, EMPTY_STATS
|
| 653 |
|
| 654 |
progress(0.25, desc="smart_resize")
|
| 655 |
resized = [smart_resize(f, int(max_pixels), int(patch_size)) for f in raw]
|
|
|
|
| 764 |
groups=groups, gop_label=gop_resolved,
|
| 765 |
)
|
| 766 |
|
| 767 |
+
stats_html = render_stats_html(
|
| 768 |
+
total_selected=int(actual_selected),
|
| 769 |
+
n_groups=len(groups),
|
| 770 |
+
gop_label=gop_resolved,
|
| 771 |
+
n_sampled=len(fids),
|
| 772 |
+
total_frames=int(total),
|
| 773 |
+
canvas_resolution=f"{canvas.shape[1]}Γ{canvas.shape[0]}",
|
| 774 |
+
saliency_signal=saliency_signal,
|
| 775 |
+
score_log_scale=bool(score_log_scale),
|
| 776 |
+
elapsed_sec=round(time.time() - t0, 2),
|
| 777 |
+
grid_label=f"{hb}Γ{wb}",
|
| 778 |
+
n_uniform=int(n_uniform),
|
| 779 |
+
codec=str(meta.get("codec") or ""),
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
progress(1.0, desc="Done")
|
| 783 |
return (
|
| 784 |
vis_path, canvas_path,
|
| 785 |
json.dumps(info, indent=2, ensure_ascii=False),
|
| 786 |
chart_fig,
|
| 787 |
+
stats_html,
|
| 788 |
)
|
| 789 |
|
| 790 |
|
|
|
|
| 1046 |
}
|
| 1047 |
#ovc-flow .arrow { color: #94a3b8; font-size: 0.95rem; user-select: none; }
|
| 1048 |
|
| 1049 |
+
/* Tame Gradio's dark default placeholders inside our cards: blanket-override
|
| 1050 |
+
any background on the inner wrappers, then paint a brand-tinted gradient on
|
| 1051 |
+
the canonical containers. This lights up the empty Video/Image/Plot zones
|
| 1052 |
+
so they no longer look like black holes. */
|
| 1053 |
+
.ovc-card .video-container,
|
| 1054 |
+
.ovc-card .image-container,
|
| 1055 |
+
.ovc-card .image-frame,
|
| 1056 |
+
.ovc-card .preview,
|
| 1057 |
+
.ovc-card .plot-container,
|
| 1058 |
+
.ovc-card .empty,
|
| 1059 |
+
.ovc-card video,
|
| 1060 |
+
.ovc-card [data-testid="video"],
|
| 1061 |
+
.ovc-card [data-testid="image"],
|
| 1062 |
+
.ovc-card .icon-button,
|
| 1063 |
+
.ovc-card .options,
|
| 1064 |
+
.ovc-card .source-selection,
|
| 1065 |
+
.ovc-card .upload-container {
|
| 1066 |
+
background: transparent !important;
|
| 1067 |
+
background-color: transparent !important;
|
| 1068 |
+
}
|
| 1069 |
+
.ovc-card .container,
|
| 1070 |
+
.ovc-card .wrap,
|
| 1071 |
+
.ovc-card .video-container,
|
| 1072 |
+
.ovc-card .image-container,
|
| 1073 |
+
.ovc-card .plot-container {
|
| 1074 |
+
border-radius: 12px !important;
|
| 1075 |
+
}
|
| 1076 |
+
.ovc-card .video-container,
|
| 1077 |
+
.ovc-card .image-container,
|
| 1078 |
+
.ovc-card .plot-container,
|
| 1079 |
+
.ovc-card-primary .video-container,
|
| 1080 |
+
.ovc-card-primary .image-container,
|
| 1081 |
+
.ovc-card-primary .plot-container {
|
| 1082 |
+
background: linear-gradient(180deg, rgba(99,102,241,0.05), rgba(6,182,212,0.02)) !important;
|
| 1083 |
+
border: 1px dashed rgba(148,163,184,0.32) !important;
|
| 1084 |
+
}
|
| 1085 |
+
.ovc-card .gradio-video, .ovc-card .gradio-image, .ovc-card .gradio-plot {
|
| 1086 |
+
border-color: rgba(148,163,184,0.22) !important;
|
| 1087 |
+
background: transparent !important;
|
| 1088 |
+
}
|
| 1089 |
+
/* Empty placeholder text inside Gradio components */
|
| 1090 |
+
.ovc-card .empty, .ovc-card .empty p, .ovc-card .empty span {
|
| 1091 |
+
color: #94a3b8 !important;
|
| 1092 |
}
|
| 1093 |
+
|
| 1094 |
+
/* Stats tile grid (rendered into a gr.HTML by render_stats_html) */
|
| 1095 |
+
.ovc-stats {
|
| 1096 |
+
display: grid;
|
| 1097 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 1098 |
+
gap: 10px;
|
| 1099 |
+
}
|
| 1100 |
+
.ovc-stat {
|
| 1101 |
+
padding: 12px 14px;
|
| 1102 |
+
border-radius: 14px;
|
| 1103 |
+
background: linear-gradient(135deg, rgba(79,70,229,0.07), rgba(6,182,212,0.04));
|
| 1104 |
+
border: 1px solid rgba(99,102,241,0.18);
|
| 1105 |
+
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
| 1106 |
+
}
|
| 1107 |
+
.ovc-stat:hover {
|
| 1108 |
+
transform: translateY(-1px);
|
| 1109 |
+
box-shadow: 0 6px 18px rgba(79,70,229,0.10);
|
| 1110 |
+
}
|
| 1111 |
+
.ovc-stat .value {
|
| 1112 |
+
font-size: 1.55rem; font-weight: 800;
|
| 1113 |
background: var(--ovc-grad);
|
| 1114 |
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
| 1115 |
+
letter-spacing: -0.02em;
|
| 1116 |
+
line-height: 1.1;
|
| 1117 |
+
word-break: break-word;
|
| 1118 |
+
}
|
| 1119 |
+
.ovc-stat .label {
|
| 1120 |
+
font-size: 0.74rem; color: #64748b;
|
| 1121 |
+
text-transform: uppercase; letter-spacing: 0.06em;
|
| 1122 |
+
margin-top: 4px;
|
| 1123 |
font-weight: 600;
|
| 1124 |
}
|
| 1125 |
"""
|
|
|
|
| 1203 |
</div>
|
| 1204 |
"""
|
| 1205 |
|
| 1206 |
+
EMPTY_STATS = (
|
| 1207 |
+
'<div class="ovc-stats">'
|
| 1208 |
+
'<div class="ovc-stat" style="grid-column: 1 / -1; text-align:center; '
|
| 1209 |
+
'background: linear-gradient(135deg, rgba(79,70,229,0.04), rgba(6,182,212,0.03));">'
|
| 1210 |
+
'<div class="value" style="font-size:1.05rem; font-weight:600; '
|
| 1211 |
+
'-webkit-background-clip: initial; background-clip: initial; color: #64748b;">'
|
| 1212 |
+
'Hit <span style="background: var(--ovc-grad); -webkit-background-clip:text; '
|
| 1213 |
+
'background-clip:text; color:transparent;">Run pipeline</span>, '
|
| 1214 |
+
'or pick a row in <i>Demo video</i> below.'
|
| 1215 |
'</div>'
|
| 1216 |
+
'<div class="label" style="margin-top:6px;">key numbers will land here</div>'
|
|
|
|
|
|
|
|
|
|
| 1217 |
'</div>'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1218 |
'</div>'
|
| 1219 |
)
|
| 1220 |
|
|
|
|
| 1321 |
|
| 1322 |
# βββ Outputs (wide column) βββββββββββββββββββββββββββββββββββββββ
|
| 1323 |
with gr.Column(scale=6, min_width=420):
|
| 1324 |
+
with gr.Group(elem_classes="ovc-card ovc-card-primary"):
|
| 1325 |
+
gr.Markdown("### Run summary")
|
| 1326 |
+
stats_out = gr.HTML(value=EMPTY_STATS)
|
| 1327 |
+
|
| 1328 |
with gr.Group(elem_classes="ovc-card ovc-card-primary"):
|
| 1329 |
gr.Markdown("### Patch selection visualization")
|
|
|
|
| 1330 |
vis_out = gr.Video(
|
| 1331 |
label="", show_label=False, autoplay=True, height=420,
|
| 1332 |
)
|
|
|
|
| 1341 |
"spend the same budget β spread evenly in time, every "
|
| 1342 |
"patch in the chosen frames kept.</small>"
|
| 1343 |
)
|
|
|
|
| 1344 |
chart_out = gr.Plot(label="", show_label=False)
|
| 1345 |
|
| 1346 |
with gr.Row():
|
| 1347 |
with gr.Column(scale=1):
|
| 1348 |
with gr.Group(elem_classes="ovc-card"):
|
| 1349 |
gr.Markdown("### Packed canvas")
|
|
|
|
| 1350 |
canvas_out = gr.Image(
|
| 1351 |
label="", show_label=False, height=320,
|
| 1352 |
)
|
| 1353 |
with gr.Column(scale=1):
|
| 1354 |
with gr.Group(elem_classes="ovc-card"):
|
| 1355 |
+
gr.Markdown("### Raw JSON")
|
| 1356 |
gr.Markdown(
|
| 1357 |
+
"<small>Full reproducible record of this run "
|
| 1358 |
+
"(params, frame ids, group spans). Collapsed by "
|
| 1359 |
+
"default β click to expand.</small>"
|
| 1360 |
)
|
| 1361 |
+
with gr.Accordion("Show full JSON", open=False):
|
| 1362 |
info_out = gr.Code(
|
| 1363 |
label="", language="json", lines=18,
|
| 1364 |
)
|
|
|
|
| 1432 |
saliency_signal, score_log_scale, bitcost_pct, fade_strength,
|
| 1433 |
gop,
|
| 1434 |
],
|
| 1435 |
+
outputs=[vis_out, canvas_out, info_out, chart_out, stats_out],
|
| 1436 |
)
|
| 1437 |
|
| 1438 |
|