Show partial submissions only on tabs they were scored on
#3
by atdokmeci - opened
- util/render.py +440 -435
util/render.py
CHANGED
|
@@ -1,435 +1,440 @@
|
|
| 1 |
-
"""Build per-pillar HTML leaderboard tables for the VANTAGE-Bench leaderboard.
|
| 2 |
-
|
| 3 |
-
Primary API
|
| 4 |
-
-----------
|
| 5 |
-
build_all_html_tables(filtered_models, global_ranks) -> dict[str, str]
|
| 6 |
-
Returns one hand-rendered HTML <table> string per pillar key in
|
| 7 |
-
config.PILLARS ('overall', 'spatial', 'st', 'temporal', 'semantic'),
|
| 8 |
-
all sharing the Overall tab's visual style (striping, hover, badges,
|
| 9 |
-
bold column-max, row data-id for the click bridge in app.py).
|
| 10 |
-
|
| 11 |
-
make_radar_svg(model) -> str
|
| 12 |
-
4-axis radar chart SVG (220 × 160 px) for the model detail side panel.
|
| 13 |
-
Axes: Semantic · Spatial · Sp-Temp · Temporal (clockwise from top).
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
from __future__ import annotations
|
| 17 |
-
|
| 18 |
-
import math
|
| 19 |
-
|
| 20 |
-
from .config import PILLARS, TASKS, TASK_METRIC_LABELS
|
| 21 |
-
from .data import ModelRecord
|
| 22 |
-
|
| 23 |
-
_MISSING = "—"
|
| 24 |
-
|
| 25 |
-
# Short task key (config.TASKS) → JSON score field in ModelRecord.scores.
|
| 26 |
-
_TASK_JSON_FIELD: dict[str, str] = {
|
| 27 |
-
"loc": "2d_localization",
|
| 28 |
-
"ground": "2d_referring_expressions",
|
| 29 |
-
"pointing": "2d_spatial_pointing",
|
| 30 |
-
"sot": "single_object_tracking",
|
| 31 |
-
"temploc": "temporal_localization",
|
| 32 |
-
"dvc": "dense_video_captioning",
|
| 33 |
-
"ev": "event_verification",
|
| 34 |
-
"vqa": "video_qa",
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
# Overall tab: ordered (display_label, json_field) pairs.
|
| 38 |
-
# Task columns ordered to align with pillar super-header groups injected by JS
|
| 39 |
-
# (see _RESIZE_JS in app.py): Spatial (3) · Spatio-Temp (1) · Temporal (2) · Semantic (2).
|
| 40 |
-
_OVERALL_SCORE_COLS: list[tuple[str, str]] = [
|
| 41 |
-
# Spatial
|
| 42 |
-
("Obj Loc", "2d_localization"),
|
| 43 |
-
("Ref Exp", "2d_referring_expressions"),
|
| 44 |
-
("Pointing", "2d_spatial_pointing"),
|
| 45 |
-
# Spatio-Temporal
|
| 46 |
-
("SOT", "single_object_tracking"),
|
| 47 |
-
# Temporal
|
| 48 |
-
("Temp Loc", "temporal_localization"),
|
| 49 |
-
("DVC", "dense_video_captioning"),
|
| 50 |
-
# Semantic
|
| 51 |
-
("Event Ver", "event_verification"),
|
| 52 |
-
("VQA", "video_qa"),
|
| 53 |
-
]
|
| 54 |
-
|
| 55 |
-
# Pillar group spec for the Overall-tab super-header row.
|
| 56 |
-
# Each entry: (colspan, label). Two empty leading cells cover the # and Name columns.
|
| 57 |
-
OVERALL_PILLAR_GROUPS: list[tuple[int, str]] = [
|
| 58 |
-
(2, ""), # # + Name
|
| 59 |
-
(3, "Spatial"),
|
| 60 |
-
(1, "Spatio-Temp"),
|
| 61 |
-
(2, "Temporal"),
|
| 62 |
-
(2, "Semantic"),
|
| 63 |
-
]
|
| 64 |
-
|
| 65 |
-
# Pillar aggregate score column shown immediately after Name on each pillar tab.
|
| 66 |
-
# Reuses the same pre-computed pillar field from the model record — no recomputation.
|
| 67 |
-
_PILLAR_AGGREGATE_COL: dict[str, tuple[str, str]] = {
|
| 68 |
-
"spatial": ("Spatial", "spatial"),
|
| 69 |
-
"st": ("Sp-Temp", "spatio_temporal"),
|
| 70 |
-
"temporal": ("Temporal", "temporal"),
|
| 71 |
-
"semantic": ("Semantic", "semantic"),
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# -- Shared primitives -----------------------------------------------------
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def _sort_by_rank(
|
| 79 |
-
models: list[ModelRecord], rank_map: dict[str, int]
|
| 80 |
-
) -> list[ModelRecord]:
|
| 81 |
-
sentinel = float("inf")
|
| 82 |
-
return sorted(models, key=lambda m: (rank_map.get(m.id, sentinel), m.name))
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def _column_maxes(
|
| 86 |
-
models: list[ModelRecord], json_fields: list[str]
|
| 87 |
-
) -> dict[str, float]:
|
| 88 |
-
"""Maximum score per field across the given model set."""
|
| 89 |
-
maxes: dict[str, float] = {}
|
| 90 |
-
for f in json_fields:
|
| 91 |
-
vals = [m.scores[f] for m in models if f in m.scores]
|
| 92 |
-
if vals:
|
| 93 |
-
maxes[f] = max(vals)
|
| 94 |
-
return maxes
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def _model_html(m: ModelRecord, rank_one_id: str | None) -> str:
|
| 98 |
-
"""HTML cell for the Model column: name + inline badges, org sub-line, type badges."""
|
| 99 |
-
name_part = f"<b>{m.name}</b>" if m.id == rank_one_id else m.name
|
| 100 |
-
if m.model_url:
|
| 101 |
-
name_core = f'<a href="{m.model_url}" target="_blank" class="mc-link">{name_part}</a>'
|
| 102 |
-
else:
|
| 103 |
-
name_core = name_part
|
| 104 |
-
verified_badge = '<span class="b b-verified">✓</span>' if m.verified else ""
|
| 105 |
-
name_html = f'<span class="mc-name-row">{name_core}{verified_badge}</span>'
|
| 106 |
-
|
| 107 |
-
badges: list[str] = []
|
| 108 |
-
if m.result_type == "ensemble":
|
| 109 |
-
badges.append('<span class="b b-ensemble">system / pipeline</span>')
|
| 110 |
-
else:
|
| 111 |
-
badges.append('<span class="b b-single">single</span>')
|
| 112 |
-
if m.type == "open":
|
| 113 |
-
badges.append('<span class="b b-open">open</span>')
|
| 114 |
-
else:
|
| 115 |
-
badges.append('<span class="b b-prop">prop.</span>')
|
| 116 |
-
if m.is_new:
|
| 117 |
-
badges.append('<span class="b b-new">new</span>')
|
| 118 |
-
badge_html = "".join(badges)
|
| 119 |
-
return (
|
| 120 |
-
f'<span data-n="{m.name}" class="mc">'
|
| 121 |
-
f'{name_html}'
|
| 122 |
-
f'<span class="mc-org">{m.organization}</span>'
|
| 123 |
-
f'<span class="mc-badges">{badge_html}</span>'
|
| 124 |
-
f'</span>'
|
| 125 |
-
)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def _score_cols_for_pillar(
|
| 129 |
-
pillar: str,
|
| 130 |
-
selected_tasks: list[str] | None = None,
|
| 131 |
-
) -> list[tuple[str, str]]:
|
| 132 |
-
"""Return [(display_label, json_field)] for one pillar.
|
| 133 |
-
|
| 134 |
-
selected_tasks: JSON field names to include (Gradio column-toggle).
|
| 135 |
-
None means show all. Only applies to non-overall pillars.
|
| 136 |
-
"""
|
| 137 |
-
if pillar == "overall":
|
| 138 |
-
return list(_OVERALL_SCORE_COLS)
|
| 139 |
-
task_keys = PILLARS[pillar]
|
| 140 |
-
if selected_tasks is not None:
|
| 141 |
-
task_keys = [tk for tk in task_keys if _TASK_JSON_FIELD[tk] in selected_tasks]
|
| 142 |
-
cols: list[tuple[str, str]] = []
|
| 143 |
-
# Lead each pillar tab with its pre-computed pillar aggregate score.
|
| 144 |
-
agg = _PILLAR_AGGREGATE_COL.get(pillar)
|
| 145 |
-
if agg is not None:
|
| 146 |
-
cols.append(agg)
|
| 147 |
-
for tk in task_keys:
|
| 148 |
-
json_field = _TASK_JSON_FIELD[tk]
|
| 149 |
-
metric = TASK_METRIC_LABELS.get(json_field, "")
|
| 150 |
-
label = f"{TASKS[tk]} ({metric})" if metric else TASKS[tk]
|
| 151 |
-
cols.append((label, json_field))
|
| 152 |
-
return cols
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
# -- Primary API -----------------------------------------------------------
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def build_overall_html_table(
|
| 159 |
-
filtered_models: list[ModelRecord],
|
| 160 |
-
rank_map: dict[str, int],
|
| 161 |
-
) -> str:
|
| 162 |
-
"""Hand-rendered HTML <table> for the Overall tab.
|
| 163 |
-
|
| 164 |
-
Used instead of gr.Dataframe because Gradio's DataFrame component
|
| 165 |
-
does not cleanly support multi-level (grouped) column headers.
|
| 166 |
-
Renders a two-row header:
|
| 167 |
-
# · Name · Overall span both rows (rowspan=2)
|
| 168 |
-
Spatial (×3) | Spatio-Temp (×1) | Temporal (×2) | Semantic (×2)
|
| 169 |
-
Obj Loc | Ref Exp | Pointing | SOT | Temp Loc | DVC | Event Ver | VQA
|
| 170 |
-
|
| 171 |
-
The Overall column sits between Name and the pillar groups as a
|
| 172 |
-
standalone (non-grouped) column showing each model's stored overall
|
| 173 |
-
score from ModelRecord.scores["overall"].
|
| 174 |
-
|
| 175 |
-
Preserves: striping, hover, bold column-max, model badges, rank order,
|
| 176 |
-
scroll behavior.
|
| 177 |
-
"""
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
#
|
| 191 |
-
#
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
#
|
| 197 |
-
#
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
parts.append('<
|
| 202 |
-
parts.append('<col class="col-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
parts.append('<
|
| 212 |
-
parts.append('<
|
| 213 |
-
parts.append('<th class="lb-corner
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
parts.append('<tr
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
parts.append(f'<
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
if
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
""
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
parts.append('<
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
parts.append('<
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
parts.append(
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
parts.append(f'<td class="{
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
"""
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
)
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
f
|
| 418 |
-
f
|
| 419 |
-
|
| 420 |
-
)
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build per-pillar HTML leaderboard tables for the VANTAGE-Bench leaderboard.
|
| 2 |
+
|
| 3 |
+
Primary API
|
| 4 |
+
-----------
|
| 5 |
+
build_all_html_tables(filtered_models, global_ranks) -> dict[str, str]
|
| 6 |
+
Returns one hand-rendered HTML <table> string per pillar key in
|
| 7 |
+
config.PILLARS ('overall', 'spatial', 'st', 'temporal', 'semantic'),
|
| 8 |
+
all sharing the Overall tab's visual style (striping, hover, badges,
|
| 9 |
+
bold column-max, row data-id for the click bridge in app.py).
|
| 10 |
+
|
| 11 |
+
make_radar_svg(model) -> str
|
| 12 |
+
4-axis radar chart SVG (220 × 160 px) for the model detail side panel.
|
| 13 |
+
Axes: Semantic · Spatial · Sp-Temp · Temporal (clockwise from top).
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import math
|
| 19 |
+
|
| 20 |
+
from .config import PILLARS, TASKS, TASK_METRIC_LABELS
|
| 21 |
+
from .data import ModelRecord
|
| 22 |
+
|
| 23 |
+
_MISSING = "—"
|
| 24 |
+
|
| 25 |
+
# Short task key (config.TASKS) → JSON score field in ModelRecord.scores.
|
| 26 |
+
_TASK_JSON_FIELD: dict[str, str] = {
|
| 27 |
+
"loc": "2d_localization",
|
| 28 |
+
"ground": "2d_referring_expressions",
|
| 29 |
+
"pointing": "2d_spatial_pointing",
|
| 30 |
+
"sot": "single_object_tracking",
|
| 31 |
+
"temploc": "temporal_localization",
|
| 32 |
+
"dvc": "dense_video_captioning",
|
| 33 |
+
"ev": "event_verification",
|
| 34 |
+
"vqa": "video_qa",
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# Overall tab: ordered (display_label, json_field) pairs.
|
| 38 |
+
# Task columns ordered to align with pillar super-header groups injected by JS
|
| 39 |
+
# (see _RESIZE_JS in app.py): Spatial (3) · Spatio-Temp (1) · Temporal (2) · Semantic (2).
|
| 40 |
+
_OVERALL_SCORE_COLS: list[tuple[str, str]] = [
|
| 41 |
+
# Spatial
|
| 42 |
+
("Obj Loc", "2d_localization"),
|
| 43 |
+
("Ref Exp", "2d_referring_expressions"),
|
| 44 |
+
("Pointing", "2d_spatial_pointing"),
|
| 45 |
+
# Spatio-Temporal
|
| 46 |
+
("SOT", "single_object_tracking"),
|
| 47 |
+
# Temporal
|
| 48 |
+
("Temp Loc", "temporal_localization"),
|
| 49 |
+
("DVC", "dense_video_captioning"),
|
| 50 |
+
# Semantic
|
| 51 |
+
("Event Ver", "event_verification"),
|
| 52 |
+
("VQA", "video_qa"),
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
# Pillar group spec for the Overall-tab super-header row.
|
| 56 |
+
# Each entry: (colspan, label). Two empty leading cells cover the # and Name columns.
|
| 57 |
+
OVERALL_PILLAR_GROUPS: list[tuple[int, str]] = [
|
| 58 |
+
(2, ""), # # + Name
|
| 59 |
+
(3, "Spatial"),
|
| 60 |
+
(1, "Spatio-Temp"),
|
| 61 |
+
(2, "Temporal"),
|
| 62 |
+
(2, "Semantic"),
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
# Pillar aggregate score column shown immediately after Name on each pillar tab.
|
| 66 |
+
# Reuses the same pre-computed pillar field from the model record — no recomputation.
|
| 67 |
+
_PILLAR_AGGREGATE_COL: dict[str, tuple[str, str]] = {
|
| 68 |
+
"spatial": ("Spatial", "spatial"),
|
| 69 |
+
"st": ("Sp-Temp", "spatio_temporal"),
|
| 70 |
+
"temporal": ("Temporal", "temporal"),
|
| 71 |
+
"semantic": ("Semantic", "semantic"),
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# -- Shared primitives -----------------------------------------------------
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _sort_by_rank(
|
| 79 |
+
models: list[ModelRecord], rank_map: dict[str, int]
|
| 80 |
+
) -> list[ModelRecord]:
|
| 81 |
+
sentinel = float("inf")
|
| 82 |
+
return sorted(models, key=lambda m: (rank_map.get(m.id, sentinel), m.name))
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _column_maxes(
|
| 86 |
+
models: list[ModelRecord], json_fields: list[str]
|
| 87 |
+
) -> dict[str, float]:
|
| 88 |
+
"""Maximum score per field across the given model set."""
|
| 89 |
+
maxes: dict[str, float] = {}
|
| 90 |
+
for f in json_fields:
|
| 91 |
+
vals = [m.scores[f] for m in models if f in m.scores]
|
| 92 |
+
if vals:
|
| 93 |
+
maxes[f] = max(vals)
|
| 94 |
+
return maxes
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _model_html(m: ModelRecord, rank_one_id: str | None) -> str:
|
| 98 |
+
"""HTML cell for the Model column: name + inline badges, org sub-line, type badges."""
|
| 99 |
+
name_part = f"<b>{m.name}</b>" if m.id == rank_one_id else m.name
|
| 100 |
+
if m.model_url:
|
| 101 |
+
name_core = f'<a href="{m.model_url}" target="_blank" class="mc-link">{name_part}</a>'
|
| 102 |
+
else:
|
| 103 |
+
name_core = name_part
|
| 104 |
+
verified_badge = '<span class="b b-verified">✓</span>' if m.verified else ""
|
| 105 |
+
name_html = f'<span class="mc-name-row">{name_core}{verified_badge}</span>'
|
| 106 |
+
|
| 107 |
+
badges: list[str] = []
|
| 108 |
+
if m.result_type == "ensemble":
|
| 109 |
+
badges.append('<span class="b b-ensemble">system / pipeline</span>')
|
| 110 |
+
else:
|
| 111 |
+
badges.append('<span class="b b-single">single</span>')
|
| 112 |
+
if m.type == "open":
|
| 113 |
+
badges.append('<span class="b b-open">open</span>')
|
| 114 |
+
else:
|
| 115 |
+
badges.append('<span class="b b-prop">prop.</span>')
|
| 116 |
+
if m.is_new:
|
| 117 |
+
badges.append('<span class="b b-new">new</span>')
|
| 118 |
+
badge_html = "".join(badges)
|
| 119 |
+
return (
|
| 120 |
+
f'<span data-n="{m.name}" class="mc">'
|
| 121 |
+
f'{name_html}'
|
| 122 |
+
f'<span class="mc-org">{m.organization}</span>'
|
| 123 |
+
f'<span class="mc-badges">{badge_html}</span>'
|
| 124 |
+
f'</span>'
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _score_cols_for_pillar(
|
| 129 |
+
pillar: str,
|
| 130 |
+
selected_tasks: list[str] | None = None,
|
| 131 |
+
) -> list[tuple[str, str]]:
|
| 132 |
+
"""Return [(display_label, json_field)] for one pillar.
|
| 133 |
+
|
| 134 |
+
selected_tasks: JSON field names to include (Gradio column-toggle).
|
| 135 |
+
None means show all. Only applies to non-overall pillars.
|
| 136 |
+
"""
|
| 137 |
+
if pillar == "overall":
|
| 138 |
+
return list(_OVERALL_SCORE_COLS)
|
| 139 |
+
task_keys = PILLARS[pillar]
|
| 140 |
+
if selected_tasks is not None:
|
| 141 |
+
task_keys = [tk for tk in task_keys if _TASK_JSON_FIELD[tk] in selected_tasks]
|
| 142 |
+
cols: list[tuple[str, str]] = []
|
| 143 |
+
# Lead each pillar tab with its pre-computed pillar aggregate score.
|
| 144 |
+
agg = _PILLAR_AGGREGATE_COL.get(pillar)
|
| 145 |
+
if agg is not None:
|
| 146 |
+
cols.append(agg)
|
| 147 |
+
for tk in task_keys:
|
| 148 |
+
json_field = _TASK_JSON_FIELD[tk]
|
| 149 |
+
metric = TASK_METRIC_LABELS.get(json_field, "")
|
| 150 |
+
label = f"{TASKS[tk]} ({metric})" if metric else TASKS[tk]
|
| 151 |
+
cols.append((label, json_field))
|
| 152 |
+
return cols
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# -- Primary API -----------------------------------------------------------
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def build_overall_html_table(
|
| 159 |
+
filtered_models: list[ModelRecord],
|
| 160 |
+
rank_map: dict[str, int],
|
| 161 |
+
) -> str:
|
| 162 |
+
"""Hand-rendered HTML <table> for the Overall tab.
|
| 163 |
+
|
| 164 |
+
Used instead of gr.Dataframe because Gradio's DataFrame component
|
| 165 |
+
does not cleanly support multi-level (grouped) column headers.
|
| 166 |
+
Renders a two-row header:
|
| 167 |
+
# · Name · Overall span both rows (rowspan=2)
|
| 168 |
+
Spatial (×3) | Spatio-Temp (×1) | Temporal (×2) | Semantic (×2)
|
| 169 |
+
Obj Loc | Ref Exp | Pointing | SOT | Temp Loc | DVC | Event Ver | VQA
|
| 170 |
+
|
| 171 |
+
The Overall column sits between Name and the pillar groups as a
|
| 172 |
+
standalone (non-grouped) column showing each model's stored overall
|
| 173 |
+
score from ModelRecord.scores["overall"].
|
| 174 |
+
|
| 175 |
+
Preserves: striping, hover, bold column-max, model badges, rank order,
|
| 176 |
+
scroll behavior.
|
| 177 |
+
"""
|
| 178 |
+
# Partial submissions without an overall score are left off this tab.
|
| 179 |
+
filtered_models = [m for m in filtered_models if "overall" in m.scores]
|
| 180 |
+
sorted_models = _sort_by_rank(filtered_models, rank_map)
|
| 181 |
+
rank_one_id = next(
|
| 182 |
+
(m.id for m in sorted_models if rank_map.get(m.id) == 1), None
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
score_cols = _OVERALL_SCORE_COLS
|
| 186 |
+
json_fields = [f for _, f in score_cols] + ["overall"]
|
| 187 |
+
col_max = _column_maxes(filtered_models, json_fields)
|
| 188 |
+
overall_max = col_max.get("overall")
|
| 189 |
+
|
| 190 |
+
# Render the table directly (no .table-wrap wrapper, no resize handle):
|
| 191 |
+
# the gr.HTML element itself acts as the rectangular container, with
|
| 192 |
+
# styling on .lb-table-overall in css.py. This avoids Gradio's
|
| 193 |
+
# rounded/clipped DataFrame-shell visuals.
|
| 194 |
+
parts: list[str] = ['<table class="lb-overall-table">']
|
| 195 |
+
|
| 196 |
+
# Column-width control via <colgroup> (avoids nth-child collisions across
|
| 197 |
+
# the two header rows). The Spatio-Temporal column gets a wider class
|
| 198 |
+
# because its lone task (SOT) sits under the long "Spatio-Temp" pillar
|
| 199 |
+
# super-header — we want that header to fit on one line.
|
| 200 |
+
_ST_FIELD = "single_object_tracking"
|
| 201 |
+
parts.append('<colgroup>')
|
| 202 |
+
parts.append('<col class="col-rank">')
|
| 203 |
+
parts.append('<col class="col-name">')
|
| 204 |
+
parts.append('<col class="col-overall">')
|
| 205 |
+
for _, field in score_cols:
|
| 206 |
+
cls = "col-score col-score-st" if field == _ST_FIELD else "col-score"
|
| 207 |
+
parts.append(f'<col class="{cls}">')
|
| 208 |
+
parts.append('</colgroup>')
|
| 209 |
+
|
| 210 |
+
# Header: two rows. # / Name / Overall span both via rowspan=2.
|
| 211 |
+
parts.append('<thead>')
|
| 212 |
+
parts.append('<tr class="lb-group-row">')
|
| 213 |
+
parts.append('<th class="lb-corner" rowspan="2">#</th>')
|
| 214 |
+
parts.append('<th class="lb-corner" rowspan="2">Name</th>')
|
| 215 |
+
parts.append('<th class="lb-corner lb-corner-num" rowspan="2">Overall</th>')
|
| 216 |
+
for span, label in [(3, "Spatial"), (1, "Spatio-Temp"),
|
| 217 |
+
(2, "Temporal"), (2, "Semantic")]:
|
| 218 |
+
parts.append(f'<th class="lb-group" colspan="{span}">{label}</th>')
|
| 219 |
+
parts.append('</tr>')
|
| 220 |
+
|
| 221 |
+
parts.append('<tr class="lb-task-row">')
|
| 222 |
+
for label, _ in score_cols:
|
| 223 |
+
parts.append(f'<th>{label}</th>')
|
| 224 |
+
parts.append('</tr>')
|
| 225 |
+
parts.append('</thead>')
|
| 226 |
+
|
| 227 |
+
# Body
|
| 228 |
+
# Total column count: # + Name + Overall + 8 task scores = 11.
|
| 229 |
+
total_cols = 3 + len(score_cols)
|
| 230 |
+
parts.append('<tbody>')
|
| 231 |
+
if not sorted_models:
|
| 232 |
+
parts.append(
|
| 233 |
+
f'<tr><td colspan="{total_cols}" class="lb-empty-row">'
|
| 234 |
+
f'No models match — adjust the filters.</td></tr>'
|
| 235 |
+
)
|
| 236 |
+
for i, m in enumerate(sorted_models, 1):
|
| 237 |
+
parts.append(f'<tr data-id="{m.id}">')
|
| 238 |
+
parts.append(f'<td class="lb-rank">{i}</td>')
|
| 239 |
+
parts.append(f'<td class="lb-name">{_model_html(m, rank_one_id)}</td>')
|
| 240 |
+
|
| 241 |
+
# Overall cell — bold if it's the column max.
|
| 242 |
+
ov = m.scores.get("overall")
|
| 243 |
+
if ov is None:
|
| 244 |
+
parts.append(f'<td class="lb-score lb-overall">{_MISSING}</td>')
|
| 245 |
+
else:
|
| 246 |
+
is_max = overall_max is not None and ov == overall_max
|
| 247 |
+
cls = "lb-score lb-overall lb-max" if is_max else "lb-score lb-overall"
|
| 248 |
+
parts.append(f'<td class="{cls}">{ov:.2f}</td>')
|
| 249 |
+
|
| 250 |
+
for _, f in score_cols:
|
| 251 |
+
v = m.scores.get(f)
|
| 252 |
+
if v is None:
|
| 253 |
+
parts.append(f'<td class="lb-score">{_MISSING}</td>')
|
| 254 |
+
else:
|
| 255 |
+
m_val = col_max.get(f)
|
| 256 |
+
is_max = m_val is not None and v == m_val
|
| 257 |
+
cls = "lb-score lb-max" if is_max else "lb-score"
|
| 258 |
+
parts.append(f'<td class="{cls}">{v:.2f}</td>')
|
| 259 |
+
parts.append('</tr>')
|
| 260 |
+
parts.append('</tbody>')
|
| 261 |
+
parts.append('</table>')
|
| 262 |
+
return "".join(parts)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def build_pillar_html_table(
|
| 266 |
+
pillar: str,
|
| 267 |
+
filtered_models: list[ModelRecord],
|
| 268 |
+
rank_map: dict[str, int],
|
| 269 |
+
) -> str:
|
| 270 |
+
"""Hand-rendered HTML <table> for a pillar tab (Spatial / Sp-Temp /
|
| 271 |
+
Temporal / Semantic) — mirrors build_overall_html_table's markup and
|
| 272 |
+
CSS classes so every tab shares one visual style.
|
| 273 |
+
|
| 274 |
+
Single-row header (no pillar super-header grouping needed on these
|
| 275 |
+
tabs). The pillar's own aggregate score is the first score column and
|
| 276 |
+
gets the same purple-outlined "headline" treatment as the Overall
|
| 277 |
+
column on the Overall tab (`lb-agg` mirrors `lb-overall`).
|
| 278 |
+
"""
|
| 279 |
+
agg_field = _PILLAR_AGGREGATE_COL[pillar][1] if pillar in _PILLAR_AGGREGATE_COL else None
|
| 280 |
+
# Partial submissions appear only on the pillars they were scored on.
|
| 281 |
+
if agg_field is not None:
|
| 282 |
+
filtered_models = [m for m in filtered_models if agg_field in m.scores]
|
| 283 |
+
sorted_models = _sort_by_rank(filtered_models, rank_map)
|
| 284 |
+
rank_one_id = next(
|
| 285 |
+
(m.id for m in sorted_models if rank_map.get(m.id) == 1), None
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
score_cols = _score_cols_for_pillar(pillar)
|
| 289 |
+
json_fields = [f for _, f in score_cols]
|
| 290 |
+
col_max = _column_maxes(filtered_models, json_fields)
|
| 291 |
+
|
| 292 |
+
parts: list[str] = ['<table class="lb-pillar-table">']
|
| 293 |
+
|
| 294 |
+
parts.append('<colgroup>')
|
| 295 |
+
parts.append('<col class="col-rank">')
|
| 296 |
+
parts.append('<col class="col-name">')
|
| 297 |
+
for _, field in score_cols:
|
| 298 |
+
cls = "col-score col-agg" if field == agg_field else "col-score"
|
| 299 |
+
parts.append(f'<col class="{cls}">')
|
| 300 |
+
parts.append('</colgroup>')
|
| 301 |
+
|
| 302 |
+
parts.append('<thead>')
|
| 303 |
+
parts.append('<tr class="lb-header-row">')
|
| 304 |
+
parts.append('<th class="lb-corner">#</th>')
|
| 305 |
+
parts.append('<th class="lb-corner">Name</th>')
|
| 306 |
+
for label, field in score_cols:
|
| 307 |
+
if field == agg_field:
|
| 308 |
+
parts.append(f'<th class="lb-agg-th">{label}</th>')
|
| 309 |
+
else:
|
| 310 |
+
parts.append(f'<th>{label}</th>')
|
| 311 |
+
parts.append('</tr>')
|
| 312 |
+
parts.append('</thead>')
|
| 313 |
+
|
| 314 |
+
total_cols = 2 + len(score_cols)
|
| 315 |
+
parts.append('<tbody>')
|
| 316 |
+
if not sorted_models:
|
| 317 |
+
parts.append(
|
| 318 |
+
f'<tr><td colspan="{total_cols}" class="lb-empty-row">'
|
| 319 |
+
f'No models match — adjust the filters.</td></tr>'
|
| 320 |
+
)
|
| 321 |
+
for i, m in enumerate(sorted_models, 1):
|
| 322 |
+
parts.append(f'<tr data-id="{m.id}">')
|
| 323 |
+
parts.append(f'<td class="lb-rank">{i}</td>')
|
| 324 |
+
parts.append(f'<td class="lb-name">{_model_html(m, rank_one_id)}</td>')
|
| 325 |
+
|
| 326 |
+
for _, f in score_cols:
|
| 327 |
+
is_agg = f == agg_field
|
| 328 |
+
v = m.scores.get(f)
|
| 329 |
+
base_cls = "lb-score lb-agg" if is_agg else "lb-score"
|
| 330 |
+
if v is None:
|
| 331 |
+
parts.append(f'<td class="{base_cls}">{_MISSING}</td>')
|
| 332 |
+
else:
|
| 333 |
+
m_val = col_max.get(f)
|
| 334 |
+
is_max = m_val is not None and v == m_val
|
| 335 |
+
cls = f"{base_cls} lb-max" if is_max else base_cls
|
| 336 |
+
parts.append(f'<td class="{cls}">{v:.2f}</td>')
|
| 337 |
+
parts.append('</tr>')
|
| 338 |
+
parts.append('</tbody>')
|
| 339 |
+
parts.append('</table>')
|
| 340 |
+
return "".join(parts)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def build_all_html_tables(
|
| 344 |
+
filtered_models: list[ModelRecord],
|
| 345 |
+
global_ranks: dict[str, dict[str, int]],
|
| 346 |
+
) -> dict[str, str]:
|
| 347 |
+
"""Return {pillar_key: html_table_str} for every tab, Overall included.
|
| 348 |
+
|
| 349 |
+
Replaces build_all_tables + build_overall_html_table as the single
|
| 350 |
+
entry point once every tab renders as a hand-built HTML table instead
|
| 351 |
+
of a mix of gr.HTML (Overall) and gr.Dataframe (the four pillars).
|
| 352 |
+
"""
|
| 353 |
+
result: dict[str, str] = {
|
| 354 |
+
"overall": build_overall_html_table(filtered_models, global_ranks["overall"]),
|
| 355 |
+
}
|
| 356 |
+
for pillar in PILLARS:
|
| 357 |
+
if pillar == "overall":
|
| 358 |
+
continue
|
| 359 |
+
result[pillar] = build_pillar_html_table(
|
| 360 |
+
pillar, filtered_models, global_ranks[pillar]
|
| 361 |
+
)
|
| 362 |
+
return result
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
# -- Radar SVG ------------------------------------------------------------
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def make_radar_svg(m: ModelRecord) -> str:
|
| 369 |
+
"""4-axis radar chart SVG for the model detail side panel.
|
| 370 |
+
|
| 371 |
+
Axes (clockwise from top): Semantic · Spatial · Sp-Temp · Temporal.
|
| 372 |
+
Canvas: 220 × 160 px. Scores assumed in [0, 100].
|
| 373 |
+
"""
|
| 374 |
+
W, H = 220, 160
|
| 375 |
+
cx, cy = W / 2, 82.0 # slightly below centre for top-label room
|
| 376 |
+
chart_r = 50.0 # polygon radius
|
| 377 |
+
label_r = 67.0 # label ring radius
|
| 378 |
+
|
| 379 |
+
labels = ["Semantic", "Spatial", "Sp-Temp", "Temporal"]
|
| 380 |
+
fields = ["semantic", "spatial", "spatio_temporal", "temporal"]
|
| 381 |
+
vals: list[float] = []
|
| 382 |
+
for f in fields:
|
| 383 |
+
raw = m.scores.get(f)
|
| 384 |
+
v = float(raw) / 100.0 if raw is not None else 0.0
|
| 385 |
+
vals.append(max(0.0, min(1.0, v)))
|
| 386 |
+
|
| 387 |
+
N = 4
|
| 388 |
+
step = 2 * math.pi / N
|
| 389 |
+
off = -math.pi / 2 # index 0 points straight up
|
| 390 |
+
|
| 391 |
+
parts: list[str] = [
|
| 392 |
+
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
| 393 |
+
f'width="{W}" height="{H}" viewBox="0 0 {W} {H}">'
|
| 394 |
+
]
|
| 395 |
+
|
| 396 |
+
# Grid rings (4 concentric)
|
| 397 |
+
for g in range(1, 5):
|
| 398 |
+
rg = chart_r * g / 4
|
| 399 |
+
parts.append(
|
| 400 |
+
f'<circle cx="{cx}" cy="{cy}" r="{rg:.1f}" '
|
| 401 |
+
f'fill="none" stroke="#e5e7eb" stroke-width="0.5"/>'
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Axis lines
|
| 405 |
+
for i in range(N):
|
| 406 |
+
a = off + i * step
|
| 407 |
+
x2 = cx + chart_r * math.cos(a)
|
| 408 |
+
y2 = cy + chart_r * math.sin(a)
|
| 409 |
+
parts.append(
|
| 410 |
+
f'<line x1="{cx}" y1="{cy}" '
|
| 411 |
+
f'x2="{x2:.1f}" y2="{y2:.1f}" '
|
| 412 |
+
f'stroke="#e5e7eb" stroke-width="0.5"/>'
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
# Data polygon
|
| 416 |
+
poly_pts = " ".join(
|
| 417 |
+
f"{cx + chart_r * v * math.cos(off + i * step):.1f},"
|
| 418 |
+
f"{cy + chart_r * v * math.sin(off + i * step):.1f}"
|
| 419 |
+
for i, v in enumerate(vals)
|
| 420 |
+
)
|
| 421 |
+
parts.append(
|
| 422 |
+
f'<polygon points="{poly_pts}" '
|
| 423 |
+
f'fill="rgba(37,99,235,0.12)" stroke="#2563eb" '
|
| 424 |
+
f'stroke-width="1.5" stroke-linejoin="round"/>'
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
# Labels
|
| 428 |
+
for i, lbl in enumerate(labels):
|
| 429 |
+
a = off + i * step
|
| 430 |
+
lx = cx + label_r * math.cos(a)
|
| 431 |
+
ly = cy + label_r * math.sin(a)
|
| 432 |
+
parts.append(
|
| 433 |
+
f'<text x="{lx:.1f}" y="{ly:.1f}" '
|
| 434 |
+
f'text-anchor="middle" dominant-baseline="middle" '
|
| 435 |
+
f'font-size="9" fill="#6b7280" font-family="sans-serif">'
|
| 436 |
+
f'{lbl}</text>'
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
parts.append("</svg>")
|
| 440 |
+
return "".join(parts)
|