FeilongTang commited on
Commit
82a1045
Β·
1 Parent(s): df94bff

Fourth UI pass: stats tiles + tame Gradio empty placeholders

Browse files

Summary 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.

Files changed (1) hide show
  1. app.py +146 -40
app.py CHANGED
@@ -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
- /* Empty-state placeholder for outputs */
999
- .ovc-empty {
1000
- text-align: center;
1001
- color: #94a3b8;
1002
- font-size: 0.88rem;
1003
- padding: 26px 18px;
1004
- border: 1px dashed rgba(148,163,184,0.45);
1005
- border-radius: 12px;
1006
- background: rgba(148,163,184,0.04);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
  }
1008
- .ovc-empty b { color: #64748b; font-weight: 600; }
1009
- .ovc-empty kbd {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
  background: var(--ovc-grad);
1011
- color: #fff;
1012
- padding: 1px 7px;
1013
- border-radius: 6px;
1014
- font-size: 0.78rem;
1015
- font-family: inherit;
 
 
 
 
1016
  font-weight: 600;
1017
  }
1018
  """
@@ -1096,21 +1203,18 @@ FLOW_HTML = """
1096
  </div>
1097
  """
1098
 
1099
- EMPTY_VIS = (
1100
- '<div class="ovc-empty">'
1101
- 'Upload a video on the left, hit <kbd>Run pipeline</kbd> '
1102
- 'β€” or click any row in <b>Demo video</b> below to try the bundled clip. '
1103
- 'The selected patches will animate here.'
 
 
 
 
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("### Run summary")
1251
  gr.Markdown(
1252
- "<small>The full per-run JSON is collapsed below "
1253
- "to keep things tidy. Click to expand.</small>"
 
1254
  )
1255
- with gr.Accordion("Show full run info (JSON)", open=False):
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