Files changed (4) hide show
  1. app.py +120 -13
  2. data/video-editing-leaderboard.csv +12 -0
  3. model_display.py +8 -1
  4. ui.py +311 -52
app.py CHANGED
@@ -581,14 +581,19 @@ button.theme-toggle[data-mode="light"] .theme-icon-moon { display: block !import
581
  .prose .ranking-table td {
582
  padding: 8px 10px !important;
583
  }
 
 
 
 
584
  .ranking-table .rank,
585
  .prose .ranking-table .rank,
586
  .ranking-table th.rank {
587
  position: sticky !important;
588
  left: 0 !important;
589
- width: 2.4rem;
590
- min-width: 2.4rem;
591
- box-shadow: 6px 0 8px -6px rgba(0, 0, 0, 0.45);
 
592
  }
593
  .ranking-table .model-cell,
594
  .prose .ranking-table .model-cell {
@@ -598,6 +603,7 @@ button.theme-toggle[data-mode="light"] .theme-icon-moon { display: block !import
598
  min-width: 140px;
599
  max-width: none;
600
  background: transparent !important;
 
601
  }
602
  .ranking-table th.model-cell,
603
  .prose .ranking-table th.model-cell {
@@ -608,6 +614,7 @@ button.theme-toggle[data-mode="light"] .theme-icon-moon { display: block !import
608
  min-width: 140px;
609
  max-width: none;
610
  background: var(--pruna-bg-header) !important;
 
611
  }
612
  .ranking-table tbody tr:hover .model-cell {
613
  background: var(--pruna-table-hover) !important;
@@ -1107,11 +1114,11 @@ button.theme-toggle[data-mode="light"] .theme-icon-moon { display: block !import
1107
  max-height: min(70vh, 720px);
1108
  overflow-x: auto;
1109
  overflow-y: auto;
1110
- -webkit-overflow-scrolling: touch;
1111
- overscroll-behavior-x: contain;
1112
  }
1113
  .ranking-table,
1114
  .prose .ranking-table {
 
1115
  width: 100%;
1116
  margin: 0 !important;
1117
  overflow: visible;
@@ -1187,10 +1194,14 @@ button.theme-toggle[data-mode="light"] .theme-icon-moon { display: block !import
1187
  .prose .ranking-table .rank {
1188
  position: sticky;
1189
  left: 0;
1190
- z-index: 1;
1191
  box-sizing: border-box;
1192
- width: 3.25rem;
1193
- min-width: 3.25rem;
 
 
 
 
1194
  color: var(--pruna-lavender) !important;
1195
  font-weight: 700 !important;
1196
  text-align: center;
@@ -1207,18 +1218,21 @@ button.theme-toggle[data-mode="light"] .theme-icon-moon { display: block !import
1207
  .ranking-table .model-cell,
1208
  .prose .ranking-table .model-cell {
1209
  position: sticky;
1210
- left: 3.25rem;
1211
- z-index: 1;
 
1212
  min-width: 180px;
1213
  max-width: 260px;
1214
  background: var(--pruna-table-sticky) !important;
 
1215
  }
1216
  .ranking-table th.model-cell,
1217
  .prose .ranking-table th.model-cell {
1218
  top: 0;
1219
- left: 3.25rem;
1220
  z-index: 5;
1221
  background: var(--pruna-bg-header) !important;
 
1222
  }
1223
  .ranking-table tbody tr:hover .rank,
1224
  .ranking-table tbody tr:hover .model-cell {
@@ -2301,6 +2315,60 @@ def load_qwen_combined_dataframe(path):
2301
  return df.reset_index(drop=True)
2302
 
2303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2304
  df = load_oneig_dataframe(oneig_path)
2305
 
2306
  oneig_metric_columns = [
@@ -2360,10 +2428,15 @@ arena_path = _resolve_data_path(
2360
  data_dir / "arena_ai_text_to_image_leaderboard.csv",
2361
  space_root.parent / "arena_ai_text_to_image_leaderboard.csv",
2362
  )
 
 
 
 
2363
 
2364
  qwen_df = load_qwen_combined_dataframe(qwen_path)
2365
  aa_df = load_artificial_analysis_dataframe(aa_path)
2366
  arena_df = load_arena_ai_dataframe(arena_path)
 
2367
  qwen_display_columns = [
2368
  col
2369
  for col in [
@@ -2395,6 +2468,18 @@ arena_display_columns = [
2395
  ]
2396
  if col in arena_df.columns
2397
  ]
 
 
 
 
 
 
 
 
 
 
 
 
2398
 
2399
  oneig_samples = load_sample_comparison_data(oneig_combined_dir)
2400
  qwen_samples = load_sample_comparison_data(qwen_combined_dir)
@@ -2456,11 +2541,27 @@ arena_metric_ids = _metric_ids_for(
2456
  "arena_text",
2457
  ],
2458
  )
 
2459
 
2460
  datasets = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2461
  {
2462
  "id": "qwen",
2463
  "name": "Qwen Image Dataset",
 
2464
  "data": qwen_df,
2465
  "columns": qwen_display_columns,
2466
  "metric_ids": qwen_metric_ids,
@@ -2470,6 +2571,7 @@ datasets = [
2470
  {
2471
  "id": "oneig",
2472
  "name": "OneIG Alignment Dataset",
 
2473
  "data": oneig_df,
2474
  "columns": oneig_display_columns,
2475
  "metric_ids": oneig_metric_ids,
@@ -2482,6 +2584,7 @@ datasets = [
2482
  {
2483
  "id": "artificial_analysis",
2484
  "name": "Artificial Analysis Dataset",
 
2485
  "data": aa_df,
2486
  "columns": aa_display_columns,
2487
  "metric_ids": aa_metric_ids,
@@ -2491,6 +2594,7 @@ datasets = [
2491
  {
2492
  "id": "arena_ai",
2493
  "name": "Arena AI Dataset",
 
2494
  "data": arena_df,
2495
  "columns": arena_display_columns,
2496
  "metric_ids": arena_metric_ids,
@@ -2501,8 +2605,11 @@ datasets = [
2501
  datasets = [dataset for dataset in datasets if dataset["metric_ids"]]
2502
 
2503
  DEFAULT_DATASET_ID = next(
2504
- (dataset["id"] for dataset in datasets if dataset["id"] == "qwen"),
2505
- datasets[0]["id"] if datasets else None,
 
 
 
2506
  )
2507
  DEFAULT_METRIC_ID = None
2508
 
 
581
  .prose .ranking-table td {
582
  padding: 8px 10px !important;
583
  }
584
+ .ranking-table,
585
+ .prose .ranking-table {
586
+ --rank-col-width: 3.25rem;
587
+ }
588
  .ranking-table .rank,
589
  .prose .ranking-table .rank,
590
  .ranking-table th.rank {
591
  position: sticky !important;
592
  left: 0 !important;
593
+ width: var(--rank-col-width);
594
+ min-width: var(--rank-col-width);
595
+ max-width: var(--rank-col-width);
596
+ box-shadow: none;
597
  }
598
  .ranking-table .model-cell,
599
  .prose .ranking-table .model-cell {
 
603
  min-width: 140px;
604
  max-width: none;
605
  background: transparent !important;
606
+ box-shadow: none !important;
607
  }
608
  .ranking-table th.model-cell,
609
  .prose .ranking-table th.model-cell {
 
614
  min-width: 140px;
615
  max-width: none;
616
  background: var(--pruna-bg-header) !important;
617
+ box-shadow: 0 1px 0 var(--pruna-hairline) !important;
618
  }
619
  .ranking-table tbody tr:hover .model-cell {
620
  background: var(--pruna-table-hover) !important;
 
1114
  max-height: min(70vh, 720px);
1115
  overflow-x: auto;
1116
  overflow-y: auto;
1117
+ overscroll-behavior: none;
 
1118
  }
1119
  .ranking-table,
1120
  .prose .ranking-table {
1121
+ --rank-col-width: 4.25rem;
1122
  width: 100%;
1123
  margin: 0 !important;
1124
  overflow: visible;
 
1194
  .prose .ranking-table .rank {
1195
  position: sticky;
1196
  left: 0;
1197
+ z-index: 2;
1198
  box-sizing: border-box;
1199
+ width: var(--rank-col-width);
1200
+ min-width: var(--rank-col-width);
1201
+ max-width: var(--rank-col-width);
1202
+ padding-left: 0.5rem !important;
1203
+ padding-right: 0.5rem !important;
1204
+ overflow: hidden;
1205
  color: var(--pruna-lavender) !important;
1206
  font-weight: 700 !important;
1207
  text-align: center;
 
1218
  .ranking-table .model-cell,
1219
  .prose .ranking-table .model-cell {
1220
  position: sticky;
1221
+ left: var(--rank-col-width);
1222
+ z-index: 2;
1223
+ box-sizing: border-box;
1224
  min-width: 180px;
1225
  max-width: 260px;
1226
  background: var(--pruna-table-sticky) !important;
1227
+ box-shadow: 8px 0 10px -8px rgba(0, 0, 0, 0.35) !important;
1228
  }
1229
  .ranking-table th.model-cell,
1230
  .prose .ranking-table th.model-cell {
1231
  top: 0;
1232
+ left: var(--rank-col-width);
1233
  z-index: 5;
1234
  background: var(--pruna-bg-header) !important;
1235
+ box-shadow: 0 1px 0 var(--pruna-hairline), 8px 0 10px -8px rgba(0, 0, 0, 0.35) !important;
1236
  }
1237
  .ranking-table tbody tr:hover .rank,
1238
  .ranking-table tbody tr:hover .model-cell {
 
2315
  return df.reset_index(drop=True)
2316
 
2317
 
2318
+ def load_video_editing_dataframe(path):
2319
+ """Load the video-to-video editing leaderboard."""
2320
+ df = pd.read_csv(path, na_values=["N/A", "n/a", ""])
2321
+ df = df.rename(
2322
+ columns={
2323
+ "display_name": "Model",
2324
+ "elo": "Datapoint Elo",
2325
+ "min_generation_s": "Min Generation Time (s)",
2326
+ "median_generation_s": "Median Generation Time (s)",
2327
+ "p20_generation_s": "P20 Generation Time (s)",
2328
+ "generation_s_per_output_video_s": "Time / Output Video Second (s)",
2329
+ "predict_time_s_per_output_video_s": "Predict Time / Output Video Second (s)",
2330
+ "model_execution_time_s_per_output_video_s": (
2331
+ "Execution Time / Output Video Second (s)"
2332
+ ),
2333
+ "price": "Price / Second of Video (USD)",
2334
+ }
2335
+ )
2336
+ df = df.drop(columns=["wandb_run_ids", "n_generations"], errors="ignore")
2337
+ df["Model"] = df["Model"].astype(str).str.strip()
2338
+ df = _as_numeric(
2339
+ df,
2340
+ [
2341
+ "Datapoint Elo",
2342
+ "Min Generation Time (s)",
2343
+ "Median Generation Time (s)",
2344
+ "P20 Generation Time (s)",
2345
+ "Time / Output Video Second (s)",
2346
+ "Predict Time / Output Video Second (s)",
2347
+ "Execution Time / Output Video Second (s)",
2348
+ "Price / Second of Video (USD)",
2349
+ ],
2350
+ )
2351
+ end_to_end = df.get("Time / Output Video Second (s)")
2352
+ execution = df.get("Execution Time / Output Video Second (s)")
2353
+ if end_to_end is not None:
2354
+ if "model_id" in df.columns:
2355
+ is_ours = df["model_id"].astype(str).str.lower().str.startswith(
2356
+ "p_video_edit"
2357
+ )
2358
+ else:
2359
+ is_ours = df["Model"].astype(str).str.casefold().str.startswith(
2360
+ "p-video-edit"
2361
+ )
2362
+ if execution is None:
2363
+ pareto_time = end_to_end
2364
+ else:
2365
+ ours_time = execution.where(execution.notna(), end_to_end)
2366
+ pareto_time = end_to_end.where(~is_ours, ours_time)
2367
+ df["Pareto Time / Output Video Second (s)"] = pareto_time
2368
+ df = df.drop(columns=["model_id"], errors="ignore")
2369
+ return df.reset_index(drop=True)
2370
+
2371
+
2372
  df = load_oneig_dataframe(oneig_path)
2373
 
2374
  oneig_metric_columns = [
 
2428
  data_dir / "arena_ai_text_to_image_leaderboard.csv",
2429
  space_root.parent / "arena_ai_text_to_image_leaderboard.csv",
2430
  )
2431
+ video_path = _resolve_data_path(
2432
+ data_dir / "video-editing-leaderboard.csv",
2433
+ space_root.parent / "video-editing-leaderboard.csv",
2434
+ )
2435
 
2436
  qwen_df = load_qwen_combined_dataframe(qwen_path)
2437
  aa_df = load_artificial_analysis_dataframe(aa_path)
2438
  arena_df = load_arena_ai_dataframe(arena_path)
2439
+ video_df = load_video_editing_dataframe(video_path)
2440
  qwen_display_columns = [
2441
  col
2442
  for col in [
 
2468
  ]
2469
  if col in arena_df.columns
2470
  ]
2471
+ video_display_columns = [
2472
+ col
2473
+ for col in [
2474
+ "Model",
2475
+ "Datapoint Elo",
2476
+ "Time / Output Video Second (s)",
2477
+ "Median Generation Time (s)",
2478
+ "Min Generation Time (s)",
2479
+ "Price / Second of Video (USD)",
2480
+ ]
2481
+ if col in video_df.columns
2482
+ ]
2483
 
2484
  oneig_samples = load_sample_comparison_data(oneig_combined_dir)
2485
  qwen_samples = load_sample_comparison_data(qwen_combined_dir)
 
2541
  "arena_text",
2542
  ],
2543
  )
2544
+ video_metric_ids = _metric_ids_for(video_df, ["datapoint_elo"])
2545
 
2546
  datasets = [
2547
+ {
2548
+ "id": "video_editing",
2549
+ "name": "Pruna Internal Video-Edit Benchmark",
2550
+ "modality": "video_to_video",
2551
+ "data": video_df,
2552
+ "columns": video_display_columns,
2553
+ "metric_ids": video_metric_ids,
2554
+ "note": (
2555
+ "Datapoint Elo from pairwise video-edit preference. Price is USD "
2556
+ "per second of output video. Generation time per second of video "
2557
+ "is end-to-end wall time to produce one second of output."
2558
+ ),
2559
+ "samples": None,
2560
+ },
2561
  {
2562
  "id": "qwen",
2563
  "name": "Qwen Image Dataset",
2564
+ "modality": "text_to_image",
2565
  "data": qwen_df,
2566
  "columns": qwen_display_columns,
2567
  "metric_ids": qwen_metric_ids,
 
2571
  {
2572
  "id": "oneig",
2573
  "name": "OneIG Alignment Dataset",
2574
+ "modality": "text_to_image",
2575
  "data": oneig_df,
2576
  "columns": oneig_display_columns,
2577
  "metric_ids": oneig_metric_ids,
 
2584
  {
2585
  "id": "artificial_analysis",
2586
  "name": "Artificial Analysis Dataset",
2587
+ "modality": "text_to_image",
2588
  "data": aa_df,
2589
  "columns": aa_display_columns,
2590
  "metric_ids": aa_metric_ids,
 
2594
  {
2595
  "id": "arena_ai",
2596
  "name": "Arena AI Dataset",
2597
+ "modality": "text_to_image",
2598
  "data": arena_df,
2599
  "columns": arena_display_columns,
2600
  "metric_ids": arena_metric_ids,
 
2605
  datasets = [dataset for dataset in datasets if dataset["metric_ids"]]
2606
 
2607
  DEFAULT_DATASET_ID = next(
2608
+ (dataset["id"] for dataset in datasets if dataset["id"] == "video_editing"),
2609
+ next(
2610
+ (dataset["id"] for dataset in datasets if dataset["id"] == "qwen"),
2611
+ datasets[0]["id"] if datasets else None,
2612
+ ),
2613
  )
2614
  DEFAULT_METRIC_ID = None
2615
 
data/video-editing-leaderboard.csv ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_id,display_name,elo,wandb_run_ids,n_generations,min_generation_s,median_generation_s,p20_generation_s,generation_s_per_output_video_s,predict_time_s_per_output_video_s,model_execution_time_s_per_output_video_s,price
2
+ gemini_omni_flash_edit__fal,Gemini Omni Flash Edit,1054,3iuctwj9,56,32.574746752001374,58.2327566820004,50.44402900500063,13.78,,,0.13
3
+ grok_imagine_video__replicate,Grok Imagine Video,974,q0ogvekx,59,31.640928319000523,43.75649321700257,32.88782280539963,11.04,,,0.05
4
+ happyhorse_1_0__wavespeed,HappyHorse 1.0,1048,k4zj5wqk,71,106.5824136010051,201.2431439649954,141.46595527700265,46.73,,,0.14
5
+ ltx_2_3_quality_reference_video_to_video__fal,LTX 2.3 Video Edit,911,p19ks8ad,73,60.768441981999786,73.62109239100027,70.20137865180223,15.83,,,0.054
6
+ lucy_edit_pro__fal,Lucy Edit Pro,879,na6bz9xx,72,118.3459930579993,136.44442766549764,124.13885582720104,27.18,,,0.15
7
+ minimax_h3_reference_to_video__fal,MiniMax H3 Reference-to-Video,1060,639xmakz,63,180.8385945170012,308.29198316100155,248.2394383729996,58.31,,,0.06
8
+ p_video_edit_preview__replicate_final,P-Video-Edit,1000,m8irpsa6,73,31.41135125700021,86.86665409700072,57.21095684959946,23.18,17.99,12.06,0.045
9
+ p_video_edit_preview__replicate_final__draft,P-Video-Edit Draft,994,6f88e6mx,73,23.04809326099712,42.9797806409988,33.19193872759861,11.48,11.28,4.46,0.025
10
+ seedance_2_5_video_edit_turbo__wavespeed,Seedance 2.5 Video Edit Turbo,1063,2nr274vp,68,142.49452170499717,293.7401115540015,223.11569286320045,59.93,,,0.24
11
+ wan_2_7_video_edit__wavespeed,Wan 2.7 Video Edit,1055,9lq803be,66,134.3744560209998,313.957433804002,219.2491260079987,68.04,,,0.2
12
+
model_display.py CHANGED
@@ -88,6 +88,13 @@ MODEL_DISPLAY_NAMES = {
88
  "p_image_2_ideogram_high_2k": "P-Image-Ideogram High 2K",
89
  "P-Image-Ideogram (High)": "P-Image-Ideogram High",
90
  "p_image_2_ideogram_very_high_1k": "P-Image-Ideogram Very High 1K",
 
 
 
 
 
 
 
91
  # Others overlapping P-Bench
92
  "z_image": "Z-Image",
93
  "glm_image": "GLM-Image",
@@ -210,6 +217,6 @@ def display_model_name(model_id) -> str:
210
  if raw in MODEL_DISPLAY_NAMES:
211
  return MODEL_DISPLAY_NAMES[raw]
212
  # Already a human label (spaces / punctuation) — keep as-is.
213
- if re.search(r"[\s.\[\]()]", raw):
214
  return raw
215
  return _prettify_snake_case(raw)
 
88
  "p_image_2_ideogram_high_2k": "P-Image-Ideogram High 2K",
89
  "P-Image-Ideogram (High)": "P-Image-Ideogram High",
90
  "p_image_2_ideogram_very_high_1k": "P-Image-Ideogram Very High 1K",
91
+ # P-Video-Edit
92
+ "P-Video-Edit": "P-Video-Edit",
93
+ "P-Video-Edit Draft": "P-Video-Edit Draft",
94
+ "P-Video Edit Final": "P-Video-Edit",
95
+ "P-Video Edit Final (draft)": "P-Video-Edit Draft",
96
+ "p_video_edit_preview__replicate_final": "P-Video-Edit",
97
+ "p_video_edit_preview__replicate_final__draft": "P-Video-Edit Draft",
98
  # Others overlapping P-Bench
99
  "z_image": "Z-Image",
100
  "glm_image": "GLM-Image",
 
217
  if raw in MODEL_DISPLAY_NAMES:
218
  return MODEL_DISPLAY_NAMES[raw]
219
  # Already a human label (spaces / punctuation) — keep as-is.
220
+ if re.search(r"[\s.\[\]()-]", raw):
221
  return raw
222
  return _prettify_snake_case(raw)
ui.py CHANGED
@@ -25,19 +25,41 @@ MAX_COMPARE_PROMPTS = 8
25
  MAX_PARETO_METRICS = 8
26
  _PARETO_SLOT_COUNT = 1 + MAX_PARETO_METRICS * 8
27
  _PARETO_PRICE_COLUMN = "Price / Image (USD)"
 
 
28
  _PARETO_TIME_COLUMN = "Min Generation Time (s)"
 
 
 
 
 
 
 
 
 
 
29
  _PARETO_SCALE_CHOICES = [
30
  ("Log", "Logarithmic"),
31
  ("Linear", "Linear"),
32
  ]
33
  _PARETO_SCALE_VALUES = {value for _, value in _PARETO_SCALE_CHOICES}
34
  _PARETO_SCALE_DEFAULT = "Logarithmic"
 
 
 
35
 
36
  TAB_LEADERBOARDS = "leaderboards"
37
  TAB_PARETO = "pareto"
38
  TAB_SAMPLES = "samples"
39
  TAB_ABOUT = "about"
40
 
 
 
 
 
 
 
 
41
  _MODEL_CHOICES_CACHE = {}
42
  _VIEW_EVENTS = {
43
  "show_progress": "hidden",
@@ -49,14 +71,15 @@ _VIEW_EVENTS = {
49
  ABOUT_OVERVIEW_CONTENT = """
50
  # About P-Bench
51
 
52
- P-Bench compares **text-to-image models**, including optimized or accelerated
53
- endpoints, on **quality, speed, and price**. Each view is a **dataset** scored
54
- with a **metric**, written as `Dataset | Metric`. There is no single score
55
- across P-Bench.
56
 
57
  ## How to read it
58
 
59
- 1. Pick a **dataset** and a **metric**.
 
60
  2. **Leaderboards**: ranked by that metric. Price and generation time sit in
61
  the same table when the source publishes them.
62
  3. **Pareto plots**: mark models that are not beaten on both higher score
@@ -82,6 +105,17 @@ prompt suites, so samples are not shown.
82
 
83
  ## Current datasets
84
 
 
 
 
 
 
 
 
 
 
 
 
85
  ### Qwen Image Dataset
86
  100 prompts from the 1,000-prompt Qwen Image Bench set, sampled for coverage
87
  across its fine-grained (L3) categories. Metrics include Datapoint Elo,
@@ -124,12 +158,14 @@ ABOUT_DETAILS_CONTENT = """
124
  - **Arena Elo**: Elo published by Arena AI on their own dataset, plus
125
  category Elos (branding, 3D, cartoon/anime, photorealistic, art, portraits,
126
  text rendering).
127
- - **Generation time**: median and minimum generation time in seconds, as
128
- reported in the evaluation table. This is not a p95, and we do not state
129
- warm vs cold or concurrent load. Not available for Arena AI.
130
- - **Price**: USD per image in the evaluation table. We do not state list
131
- price vs amount paid, or whether failed generations are included. Not
132
- available for Arena AI.
 
 
133
 
134
  Scores from different datasets or metrics are **not interchangeable**. A high
135
  OneIG alignment score is not the same quantity as a Datapoint Elo. Compare
@@ -222,7 +258,7 @@ def render_header():
222
  </svg>
223
  </button>
224
  </div>
225
- <p class="app-header-tagline">Compare text-to-image models on quality, speed, and price</p>
226
  </header>
227
  """,
228
  padding=False,
@@ -237,10 +273,42 @@ def _item(items, item_id):
237
  return items[0] if items else None
238
 
239
 
240
- def _dataset_choices(datasets, *, require_samples=False, require_pareto=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  return [
242
  (dataset["name"], dataset["id"])
243
- for dataset in datasets
244
  if (not require_samples or dataset.get("samples"))
245
  and (not require_pareto or _dataset_has_pareto(datasets, dataset["id"]))
246
  ]
@@ -259,17 +327,39 @@ def _sample_model_ids(datasets, dataset_id):
259
  return set(samples.get("models") or [])
260
 
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  def _dataset_has_pareto(datasets, dataset_id):
263
  dataset = _item(datasets, dataset_id)
264
- columns = getattr(dataset.get("data") if dataset else None, "columns", [])
265
- return _PARETO_PRICE_COLUMN in columns or _PARETO_TIME_COLUMN in columns
 
 
 
266
 
267
 
268
- def _dataset_dropdown_update(datasets, tab, dataset_id):
269
  """Limit the dataset list to what the current tab can show."""
 
 
270
  return gr.update(
271
  choices=_dataset_choices(
272
  datasets,
 
273
  require_samples=tab == TAB_SAMPLES
274
  and _dataset_has_samples(datasets, dataset_id),
275
  require_pareto=tab == TAB_PARETO
@@ -375,9 +465,11 @@ _LEADERBOARD_IDENTITY_COLUMNS = [
375
  "Optimized",
376
  ]
377
  _LEADERBOARD_META_COLUMNS = [
 
378
  "Median Generation Time (s)",
379
  "Min Generation Time (s)",
380
  "Price / Image (USD)",
 
381
  "Evaluation Date (UTC)",
382
  "Date",
383
  ]
@@ -604,7 +696,9 @@ def _display_label(column):
604
  "Arena Text Rendering Elo": "Text Rendering",
605
  "Median Generation Time (s)": "Median generation time",
606
  "Min Generation Time (s)": "Min generation time",
 
607
  "Price / Image (USD)": "Price per image",
 
608
  "Evaluation Date (UTC)": "Date",
609
  "Date": "Date",
610
  }
@@ -679,6 +773,23 @@ def _applied_key(view_state):
679
  )
680
 
681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  def _build_pareto_figure(
683
  data,
684
  score_column,
@@ -703,6 +814,8 @@ def _build_pareto_figure(
703
 
704
  dominated = scatter.loc[[not flag for flag in on_frontier]].copy()
705
  frontier = scatter.loc[on_frontier].sort_values(x_column).copy()
 
 
706
  if not dominated.empty:
707
  dominated["Model"] = dominated["Model"].map(display_model_name)
708
  if not frontier.empty:
@@ -723,10 +836,11 @@ def _build_pareto_figure(
723
  name="Below frontier",
724
  text=dominated["Model"],
725
  hovertemplate=hover,
 
726
  marker={
727
  "size": 9,
728
- "color": "#d8b4fe",
729
- "opacity": 0.8,
730
  "line": {"width": 0},
731
  },
732
  )
@@ -740,14 +854,51 @@ def _build_pareto_figure(
740
  name="On frontier",
741
  text=frontier["Model"],
742
  hovertemplate=hover,
743
- line={"color": "#69a45c", "width": 2.5},
 
744
  marker={
745
  "size": 12,
746
- "color": "#69a45c",
747
- "line": {"width": 1.5, "color": "#86c077"},
748
  },
749
  )
750
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
 
752
  score_label = _display_label(score_column)
753
  fig.update_layout(
@@ -887,23 +1038,43 @@ def _pareto_pair(
887
  if data is None or not score_column or score_column not in data.columns:
888
  return None, score_missing, None, score_missing
889
 
 
 
 
 
 
 
 
890
  price_fig, price_message = _pareto_axis(
891
  data,
892
  score_column,
893
- _PARETO_PRICE_COLUMN,
894
- "Price per image (USD)",
895
- "Price per image isn't available for this dataset.",
896
  "No models have both a score and a price for this metric.",
897
  x_hover_prefix="$",
898
  x_axis_type=_pareto_axis_type(price_scale),
899
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  time_fig, time_message = _pareto_axis(
901
  data,
902
  score_column,
903
- _PARETO_TIME_COLUMN,
904
- "Min generation time (s)",
905
- "Min generation time isn't available for this dataset.",
906
- "No models have both a score and a min generation time for this metric.",
907
  x_hover_suffix="s",
908
  x_axis_type=_pareto_axis_type(latency_scale),
909
  )
@@ -911,29 +1082,29 @@ def _pareto_pair(
911
 
912
 
913
  def _pareto_dataset_message(data):
914
- has_price = data is not None and _PARETO_PRICE_COLUMN in data.columns
915
- has_time = data is not None and _PARETO_TIME_COLUMN in data.columns
916
  if has_price or has_time:
917
  return None
918
  return (
919
- "Price per image and min generation time aren't available for "
920
  "this dataset, so these plots can't be drawn."
921
  )
922
 
923
 
924
  def _pareto_slot_note(price_fig, price_message, time_fig, time_message, data):
925
- has_price = data is not None and _PARETO_PRICE_COLUMN in data.columns
926
- has_time = data is not None and _PARETO_TIME_COLUMN in data.columns
927
  notes = []
928
  if has_price and not has_time:
929
  notes.append(
930
- "Min generation time isn't available for this dataset, so only "
931
  "price vs score is shown."
932
  )
933
  elif has_time and not has_price:
934
  notes.append(
935
- "Price per image isn't available for this dataset, so only min "
936
- "generation time vs score is shown."
937
  )
938
  if price_fig is None and has_price:
939
  notes.append(price_message)
@@ -954,8 +1125,8 @@ def _pareto_slot_updates(
954
  score_columns = [column for column in (score_columns or []) if column]
955
  price_scales = _normalize_pareto_scales(price_scales)
956
  time_scales = _normalize_pareto_scales(time_scales)
957
- has_price = data is not None and _PARETO_PRICE_COLUMN in data.columns
958
- has_time = data is not None and _PARETO_TIME_COLUMN in data.columns
959
  dataset_note = _pareto_dataset_message(data)
960
  updates = [_pareto_note_update(dataset_note)]
961
  hide_all_slots = not has_price and not has_time
@@ -1123,9 +1294,19 @@ def _filter_row(datasets, metrics, default_dataset_id, default_metric_id=None):
1123
  metric_id = _coerce_metric(
1124
  datasets, metrics, default_dataset_id, default_metric_id
1125
  )
 
1126
  with gr.Row(elem_classes="view-filters"):
 
 
 
 
 
 
 
 
 
1127
  dataset_dd = gr.Dropdown(
1128
- choices=_dataset_choices(datasets),
1129
  value=default_dataset_id,
1130
  label="Dataset",
1131
  type="value",
@@ -1157,7 +1338,7 @@ def _filter_row(datasets, metrics, default_dataset_id, default_metric_id=None):
1157
  min_width=180,
1158
  elem_classes="filter-chips",
1159
  )
1160
- return dataset_dd, metric_dd, models_dd
1161
 
1162
 
1163
  def render_image_workspace(datasets, metrics, default_dataset_id, default_metric_id):
@@ -1172,15 +1353,16 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
1172
  with gr.Column(elem_classes="workspace-filters") as filters_host:
1173
  gr.Markdown(
1174
  "<p class='filter-help'>"
1175
- "These filters apply to Leaderboards, Pareto plots, and Samples. "
1176
- "On Samples, only datasets and models we have generations for "
1177
- "are listed. On Pareto plots, only datasets with price or "
1178
- "generation time are listed. Search in Models, or leave it "
1179
- "empty to include every model."
 
1180
  "</p>",
1181
  elem_classes="filter-help-host",
1182
  )
1183
- dataset_dd, metric_dd, models_dd = _filter_row(
1184
  datasets, metrics, default_dataset_id, None
1185
  )
1186
  with gr.Tabs(elem_classes="main-tabs") as main_tabs:
@@ -1306,7 +1488,7 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
1306
  elem_classes="pareto-col",
1307
  ) as slot_time_col:
1308
  slot_time_scale = _pareto_plot_heading(
1309
- "Min generation time vs score"
1310
  )
1311
  slot_time = gr.Plot(
1312
  value=None,
@@ -1460,8 +1642,14 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
1460
  ):
1461
  prev = dict(view_state or {})
1462
  extras = extras or {}
 
 
 
 
1463
  return {
1464
  "dataset_id": dataset_id,
 
 
1465
  "metric_id": metric_id,
1466
  "models": list(models or []),
1467
  "current_tab": tab,
@@ -1599,13 +1787,14 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
1599
  tab = view_state.get("current_tab") or TAB_LEADERBOARDS
1600
  selected_raw = _normalize_metric_ids(metric_id)
1601
  incoming_models = list(models or [])
1602
- dataset_changed = source == "dataset" and dataset_id != view_state.get(
 
1603
  "dataset_id"
1604
  )
1605
  can_pareto = _dataset_has_pareto(datasets, dataset_id)
1606
  can_samples = _dataset_has_samples(datasets, dataset_id)
1607
  selected_tab = tab
1608
- if source == "dataset":
1609
  if tab == TAB_SAMPLES and not can_samples:
1610
  selected_tab = TAB_LEADERBOARDS
1611
  elif tab == TAB_PARETO and not can_pareto:
@@ -1614,7 +1803,7 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
1614
  dataset_id,
1615
  metric_id,
1616
  models,
1617
- clear_metric=dataset_changed,
1618
  require_samples=selected_tab == TAB_SAMPLES,
1619
  )
1620
  dataset_id, metric_id, models = synced[:3]
@@ -1659,7 +1848,7 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
1659
  list(optimized_value or []),
1660
  )
1661
  extra_updates = None
1662
- if source == "dataset":
1663
  view = resolve_view(datasets, metrics, dataset_id, metric_id)
1664
  extra_updates = _leaderboard_extras(
1665
  view["data"] if view else None,
@@ -1712,6 +1901,67 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
1712
  ),
1713
  }
1714
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1715
  def on_dataset(
1716
  dataset_id,
1717
  metric_id,
@@ -2061,9 +2311,12 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
2061
  handler.__name__ = f"on_tab_{tab}"
2062
  return handler
2063
 
 
2064
  view_state = gr.State(
2065
  {
2066
  "dataset_id": default_dataset_id,
 
 
2067
  "metric_id": None,
2068
  "models": [],
2069
  "current_tab": TAB_LEADERBOARDS,
@@ -2145,6 +2398,12 @@ def render_image_workspace(datasets, metrics, default_dataset_id, default_metric
2145
  main_tabs,
2146
  view_state,
2147
  ]
 
 
 
 
 
 
2148
  dataset_dd.change(
2149
  on_dataset,
2150
  inputs=filter_inputs,
 
25
  MAX_PARETO_METRICS = 8
26
  _PARETO_SLOT_COUNT = 1 + MAX_PARETO_METRICS * 8
27
  _PARETO_PRICE_COLUMN = "Price / Image (USD)"
28
+ _PARETO_VIDEO_PRICE_COLUMN = "Price / Second of Video (USD)"
29
+ _PARETO_PRICE_COLUMNS = (_PARETO_PRICE_COLUMN, _PARETO_VIDEO_PRICE_COLUMN)
30
  _PARETO_TIME_COLUMN = "Min Generation Time (s)"
31
+ _PARETO_VIDEO_TIME_COLUMN = "Pareto Time / Output Video Second (s)"
32
+ _PARETO_TIME_COLUMNS = (_PARETO_VIDEO_TIME_COLUMN, _PARETO_TIME_COLUMN)
33
+ _PARETO_PRICE_TITLES = {
34
+ _PARETO_PRICE_COLUMN: "Price per image (USD)",
35
+ _PARETO_VIDEO_PRICE_COLUMN: "Price per second of video (USD)",
36
+ }
37
+ _PARETO_TIME_TITLES = {
38
+ _PARETO_TIME_COLUMN: "Min generation time (s)",
39
+ _PARETO_VIDEO_TIME_COLUMN: "Generation time per second of video",
40
+ }
41
  _PARETO_SCALE_CHOICES = [
42
  ("Log", "Logarithmic"),
43
  ("Linear", "Linear"),
44
  ]
45
  _PARETO_SCALE_VALUES = {value for _, value in _PARETO_SCALE_CHOICES}
46
  _PARETO_SCALE_DEFAULT = "Logarithmic"
47
+ _PARETO_PRUNA_COLOR = "#c084fc"
48
+ _PARETO_OTHER_COLOR = "#9aa3b5"
49
+ _PARETO_FRONTIER_OUTLINE = "#3fa87e"
50
 
51
  TAB_LEADERBOARDS = "leaderboards"
52
  TAB_PARETO = "pareto"
53
  TAB_SAMPLES = "samples"
54
  TAB_ABOUT = "about"
55
 
56
+ MODALITY_VIDEO_TO_VIDEO = "video_to_video"
57
+ MODALITY_TEXT_TO_IMAGE = "text_to_image"
58
+ MODALITY_CHOICES = [
59
+ ("Video to Video", MODALITY_VIDEO_TO_VIDEO),
60
+ ("Text to Image", MODALITY_TEXT_TO_IMAGE),
61
+ ]
62
+
63
  _MODEL_CHOICES_CACHE = {}
64
  _VIEW_EVENTS = {
65
  "show_progress": "hidden",
 
71
  ABOUT_OVERVIEW_CONTENT = """
72
  # About P-Bench
73
 
74
+ P-Bench compares **text-to-image** and **video-to-video** models, including
75
+ optimized or accelerated endpoints, on **quality, speed, and price**. Each
76
+ view is a **dataset** scored with a **metric**, written as `Dataset | Metric`.
77
+ There is no single score across P-Bench.
78
 
79
  ## How to read it
80
 
81
+ 1. Pick a **type** (Video to Video or Text to Image), then a **dataset**
82
+ and a **metric**.
83
  2. **Leaderboards**: ranked by that metric. Price and generation time sit in
84
  the same table when the source publishes them.
85
  3. **Pareto plots**: mark models that are not beaten on both higher score
 
105
 
106
  ## Current datasets
107
 
108
+ ### Pruna Internal Video-Edit Benchmark
109
+ Pruna's internal video-to-video editing benchmark, collected by our
110
+ research engineers. It combines prompts from public video-editing
111
+ benchmarks with use-case examples we gathered for advertisement,
112
+ e-commerce, real estate, concept art, and similar work. The suite also
113
+ covers camera-angle and movement changes, lighting, and text in video
114
+ (altering, adding, or removing it). Quality is Datapoint Elo from
115
+ pairwise preference. Price is USD per second of output video;
116
+ generation time is wall time per second of output video. Samples are
117
+ not shown yet.
118
+
119
  ### Qwen Image Dataset
120
  100 prompts from the 1,000-prompt Qwen Image Bench set, sampled for coverage
121
  across its fine-grained (L3) categories. Metrics include Datapoint Elo,
 
158
  - **Arena Elo**: Elo published by Arena AI on their own dataset, plus
159
  category Elos (branding, 3D, cartoon/anime, photorealistic, art, portraits,
160
  text rendering).
161
+ - **Generation time**: median and minimum generation time in seconds for
162
+ images, as reported in the evaluation table. For video, generation time
163
+ per second of output video is the more informative figure (end-to-end
164
+ wall time). This is not a p95, and we do not state warm vs cold or
165
+ concurrent load. Not available for Arena AI.
166
+ - **Price**: USD per image for text-to-image, or USD per second of output
167
+ video for video-to-video. We do not state list price vs amount paid, or
168
+ whether failed generations are included. Not available for Arena AI.
169
 
170
  Scores from different datasets or metrics are **not interchangeable**. A high
171
  OneIG alignment score is not the same quantity as a Datapoint Elo. Compare
 
258
  </svg>
259
  </button>
260
  </div>
261
+ <p class="app-header-tagline">Compare models on quality, speed, and price</p>
262
  </header>
263
  """,
264
  padding=False,
 
273
  return items[0] if items else None
274
 
275
 
276
+ def _dataset_modality(dataset):
277
+ return (dataset or {}).get("modality") or MODALITY_TEXT_TO_IMAGE
278
+
279
+
280
+ def _datasets_for_modality(datasets, modality):
281
+ if not modality:
282
+ return list(datasets)
283
+ scoped = [
284
+ dataset
285
+ for dataset in datasets
286
+ if _dataset_modality(dataset) == modality
287
+ ]
288
+ return scoped or list(datasets)
289
+
290
+
291
+ def _modality_choices(datasets):
292
+ present = {_dataset_modality(dataset) for dataset in datasets}
293
+ return [
294
+ (label, value) for label, value in MODALITY_CHOICES if value in present
295
+ ]
296
+
297
+
298
+ def _default_dataset_id(datasets, modality, preferred=None):
299
+ scoped = _datasets_for_modality(datasets, modality)
300
+ if preferred and any(dataset["id"] == preferred for dataset in scoped):
301
+ return preferred
302
+ return scoped[0]["id"] if scoped else None
303
+
304
+
305
+ def _dataset_choices(
306
+ datasets, *, modality=None, require_samples=False, require_pareto=False
307
+ ):
308
+ scoped = _datasets_for_modality(datasets, modality)
309
  return [
310
  (dataset["name"], dataset["id"])
311
+ for dataset in scoped
312
  if (not require_samples or dataset.get("samples"))
313
  and (not require_pareto or _dataset_has_pareto(datasets, dataset["id"]))
314
  ]
 
327
  return set(samples.get("models") or [])
328
 
329
 
330
+ def _pareto_price_column(data):
331
+ columns = getattr(data, "columns", []) if data is not None else []
332
+ for column in _PARETO_PRICE_COLUMNS:
333
+ if column in columns:
334
+ return column
335
+ return None
336
+
337
+
338
+ def _pareto_time_column(data):
339
+ columns = getattr(data, "columns", []) if data is not None else []
340
+ for column in _PARETO_TIME_COLUMNS:
341
+ if column in columns:
342
+ return column
343
+ return None
344
+
345
+
346
  def _dataset_has_pareto(datasets, dataset_id):
347
  dataset = _item(datasets, dataset_id)
348
+ data = dataset.get("data") if dataset else None
349
+ return (
350
+ _pareto_price_column(data) is not None
351
+ or _pareto_time_column(data) is not None
352
+ )
353
 
354
 
355
+ def _dataset_dropdown_update(datasets, tab, dataset_id, modality=None):
356
  """Limit the dataset list to what the current tab can show."""
357
+ if modality is None:
358
+ modality = _dataset_modality(_item(datasets, dataset_id))
359
  return gr.update(
360
  choices=_dataset_choices(
361
  datasets,
362
+ modality=modality,
363
  require_samples=tab == TAB_SAMPLES
364
  and _dataset_has_samples(datasets, dataset_id),
365
  require_pareto=tab == TAB_PARETO
 
465
  "Optimized",
466
  ]
467
  _LEADERBOARD_META_COLUMNS = [
468
+ "Time / Output Video Second (s)",
469
  "Median Generation Time (s)",
470
  "Min Generation Time (s)",
471
  "Price / Image (USD)",
472
+ "Price / Second of Video (USD)",
473
  "Evaluation Date (UTC)",
474
  "Date",
475
  ]
 
696
  "Arena Text Rendering Elo": "Text Rendering",
697
  "Median Generation Time (s)": "Median generation time",
698
  "Min Generation Time (s)": "Min generation time",
699
+ "Time / Output Video Second (s)": "Generation time per second of video",
700
  "Price / Image (USD)": "Price per image",
701
+ "Price / Second of Video (USD)": "Price per second of video",
702
  "Evaluation Date (UTC)": "Date",
703
  "Date": "Date",
704
  }
 
773
  )
774
 
775
 
776
+ def _is_pruna_model(model_id) -> bool:
777
+ raw = str(model_id or "").casefold()
778
+ label = display_model_name(model_id).casefold()
779
+ return any(
780
+ value.startswith(prefix)
781
+ for value in (raw, label)
782
+ for prefix in ("p-image", "p_image", "p-video", "p_video")
783
+ )
784
+
785
+
786
+ def _pareto_fill_colors(models):
787
+ return [
788
+ _PARETO_PRUNA_COLOR if _is_pruna_model(model) else _PARETO_OTHER_COLOR
789
+ for model in models
790
+ ]
791
+
792
+
793
  def _build_pareto_figure(
794
  data,
795
  score_column,
 
814
 
815
  dominated = scatter.loc[[not flag for flag in on_frontier]].copy()
816
  frontier = scatter.loc[on_frontier].sort_values(x_column).copy()
817
+ dominated_colors = _pareto_fill_colors(dominated["Model"]) if not dominated.empty else []
818
+ frontier_colors = _pareto_fill_colors(frontier["Model"]) if not frontier.empty else []
819
  if not dominated.empty:
820
  dominated["Model"] = dominated["Model"].map(display_model_name)
821
  if not frontier.empty:
 
836
  name="Below frontier",
837
  text=dominated["Model"],
838
  hovertemplate=hover,
839
+ showlegend=False,
840
  marker={
841
  "size": 9,
842
+ "color": dominated_colors,
843
+ "opacity": 0.85,
844
  "line": {"width": 0},
845
  },
846
  )
 
854
  name="On frontier",
855
  text=frontier["Model"],
856
  hovertemplate=hover,
857
+ showlegend=False,
858
+ line={"color": _PARETO_FRONTIER_OUTLINE, "width": 2.5},
859
  marker={
860
  "size": 12,
861
+ "color": frontier_colors,
862
+ "line": {"width": 2.5, "color": _PARETO_FRONTIER_OUTLINE},
863
  },
864
  )
865
  )
866
+ for name, marker in (
867
+ (
868
+ "Pruna",
869
+ {
870
+ "size": 10,
871
+ "color": _PARETO_PRUNA_COLOR,
872
+ "line": {"width": 0},
873
+ },
874
+ ),
875
+ (
876
+ "Other models",
877
+ {
878
+ "size": 10,
879
+ "color": _PARETO_OTHER_COLOR,
880
+ "line": {"width": 0},
881
+ },
882
+ ),
883
+ (
884
+ "On frontier",
885
+ {
886
+ "size": 12,
887
+ "color": "rgba(0,0,0,0)",
888
+ "line": {"width": 2.5, "color": _PARETO_FRONTIER_OUTLINE},
889
+ },
890
+ ),
891
+ ):
892
+ fig.add_trace(
893
+ go.Scatter(
894
+ x=[None],
895
+ y=[None],
896
+ mode="markers",
897
+ name=name,
898
+ marker=marker,
899
+ hoverinfo="skip",
900
+ )
901
+ )
902
 
903
  score_label = _display_label(score_column)
904
  fig.update_layout(
 
1038
  if data is None or not score_column or score_column not in data.columns:
1039
  return None, score_missing, None, score_missing
1040
 
1041
+ price_column = _pareto_price_column(data) or _PARETO_PRICE_COLUMN
1042
+ price_title = _PARETO_PRICE_TITLES.get(price_column, "Price (USD)")
1043
+ price_missing = (
1044
+ "Price per second of video isn't available for this dataset."
1045
+ if price_column == _PARETO_VIDEO_PRICE_COLUMN
1046
+ else "Price per image isn't available for this dataset."
1047
+ )
1048
  price_fig, price_message = _pareto_axis(
1049
  data,
1050
  score_column,
1051
+ price_column,
1052
+ price_title,
1053
+ price_missing,
1054
  "No models have both a score and a price for this metric.",
1055
  x_hover_prefix="$",
1056
  x_axis_type=_pareto_axis_type(price_scale),
1057
  )
1058
+ time_column = _pareto_time_column(data) or _PARETO_TIME_COLUMN
1059
+ time_title = _PARETO_TIME_TITLES.get(time_column, "Generation time (s)")
1060
+ time_missing = (
1061
+ "Generation time per second of video isn't available for this dataset."
1062
+ if time_column == _PARETO_VIDEO_TIME_COLUMN
1063
+ else "Min generation time isn't available for this dataset."
1064
+ )
1065
+ time_empty = (
1066
+ "No models have both a score and generation time per second of "
1067
+ "video for this metric."
1068
+ if time_column == _PARETO_VIDEO_TIME_COLUMN
1069
+ else "No models have both a score and a min generation time for this metric."
1070
+ )
1071
  time_fig, time_message = _pareto_axis(
1072
  data,
1073
  score_column,
1074
+ time_column,
1075
+ time_title,
1076
+ time_missing,
1077
+ time_empty,
1078
  x_hover_suffix="s",
1079
  x_axis_type=_pareto_axis_type(latency_scale),
1080
  )
 
1082
 
1083
 
1084
  def _pareto_dataset_message(data):
1085
+ has_price = _pareto_price_column(data) is not None
1086
+ has_time = _pareto_time_column(data) is not None
1087
  if has_price or has_time:
1088
  return None
1089
  return (
1090
+ "Price and generation time aren't available for "
1091
  "this dataset, so these plots can't be drawn."
1092
  )
1093
 
1094
 
1095
  def _pareto_slot_note(price_fig, price_message, time_fig, time_message, data):
1096
+ has_price = _pareto_price_column(data) is not None
1097
+ has_time = _pareto_time_column(data) is not None
1098
  notes = []
1099
  if has_price and not has_time:
1100
  notes.append(
1101
+ "Generation time isn't available for this dataset, so only "
1102
  "price vs score is shown."
1103
  )
1104
  elif has_time and not has_price:
1105
  notes.append(
1106
+ "Price isn't available for this dataset, so only "
1107
+ "time vs score is shown."
1108
  )
1109
  if price_fig is None and has_price:
1110
  notes.append(price_message)
 
1125
  score_columns = [column for column in (score_columns or []) if column]
1126
  price_scales = _normalize_pareto_scales(price_scales)
1127
  time_scales = _normalize_pareto_scales(time_scales)
1128
+ has_price = _pareto_price_column(data) is not None
1129
+ has_time = _pareto_time_column(data) is not None
1130
  dataset_note = _pareto_dataset_message(data)
1131
  updates = [_pareto_note_update(dataset_note)]
1132
  hide_all_slots = not has_price and not has_time
 
1294
  metric_id = _coerce_metric(
1295
  datasets, metrics, default_dataset_id, default_metric_id
1296
  )
1297
+ default_modality = _dataset_modality(_item(datasets, default_dataset_id))
1298
  with gr.Row(elem_classes="view-filters"):
1299
+ modality_dd = gr.Dropdown(
1300
+ choices=_modality_choices(datasets),
1301
+ value=default_modality,
1302
+ label="Type",
1303
+ type="value",
1304
+ filterable=False,
1305
+ scale=1,
1306
+ min_width=150,
1307
+ )
1308
  dataset_dd = gr.Dropdown(
1309
+ choices=_dataset_choices(datasets, modality=default_modality),
1310
  value=default_dataset_id,
1311
  label="Dataset",
1312
  type="value",
 
1338
  min_width=180,
1339
  elem_classes="filter-chips",
1340
  )
1341
+ return modality_dd, dataset_dd, metric_dd, models_dd
1342
 
1343
 
1344
  def render_image_workspace(datasets, metrics, default_dataset_id, default_metric_id):
 
1353
  with gr.Column(elem_classes="workspace-filters") as filters_host:
1354
  gr.Markdown(
1355
  "<p class='filter-help'>"
1356
+ "Start with Type to switch between Video to Video and Text "
1357
+ "to Image. The rest of the filters follow you across "
1358
+ "Leaderboards, Pareto plots, and Samples. Samples only "
1359
+ "lists datasets and models we have generations for; Pareto "
1360
+ "plots only lists datasets with price or generation time. "
1361
+ "Search in Models, or leave it empty to include every model."
1362
  "</p>",
1363
  elem_classes="filter-help-host",
1364
  )
1365
+ modality_dd, dataset_dd, metric_dd, models_dd = _filter_row(
1366
  datasets, metrics, default_dataset_id, None
1367
  )
1368
  with gr.Tabs(elem_classes="main-tabs") as main_tabs:
 
1488
  elem_classes="pareto-col",
1489
  ) as slot_time_col:
1490
  slot_time_scale = _pareto_plot_heading(
1491
+ "Time vs score"
1492
  )
1493
  slot_time = gr.Plot(
1494
  value=None,
 
1642
  ):
1643
  prev = dict(view_state or {})
1644
  extras = extras or {}
1645
+ modality = _dataset_modality(_item(datasets, dataset_id))
1646
+ last_by_modality = dict(prev.get("dataset_by_modality") or {})
1647
+ if dataset_id:
1648
+ last_by_modality[modality] = dataset_id
1649
  return {
1650
  "dataset_id": dataset_id,
1651
+ "modality": modality,
1652
+ "dataset_by_modality": last_by_modality,
1653
  "metric_id": metric_id,
1654
  "models": list(models or []),
1655
  "current_tab": tab,
 
1787
  tab = view_state.get("current_tab") or TAB_LEADERBOARDS
1788
  selected_raw = _normalize_metric_ids(metric_id)
1789
  incoming_models = list(models or [])
1790
+ filter_changed = source in {"dataset", "modality"}
1791
+ dataset_changed = filter_changed and dataset_id != view_state.get(
1792
  "dataset_id"
1793
  )
1794
  can_pareto = _dataset_has_pareto(datasets, dataset_id)
1795
  can_samples = _dataset_has_samples(datasets, dataset_id)
1796
  selected_tab = tab
1797
+ if filter_changed:
1798
  if tab == TAB_SAMPLES and not can_samples:
1799
  selected_tab = TAB_LEADERBOARDS
1800
  elif tab == TAB_PARETO and not can_pareto:
 
1803
  dataset_id,
1804
  metric_id,
1805
  models,
1806
+ clear_metric=source == "modality" or dataset_changed,
1807
  require_samples=selected_tab == TAB_SAMPLES,
1808
  )
1809
  dataset_id, metric_id, models = synced[:3]
 
1848
  list(optimized_value or []),
1849
  )
1850
  extra_updates = None
1851
+ if filter_changed:
1852
  view = resolve_view(datasets, metrics, dataset_id, metric_id)
1853
  extra_updates = _leaderboard_extras(
1854
  view["data"] if view else None,
 
1901
  ),
1902
  }
1903
 
1904
+ def on_modality(
1905
+ modality,
1906
+ dataset_id,
1907
+ metric_id,
1908
+ models,
1909
+ platform_value,
1910
+ owner_value,
1911
+ optimized_value,
1912
+ num_prompts,
1913
+ seed,
1914
+ view_state,
1915
+ ):
1916
+ view_state = dict(view_state or {})
1917
+ last_by_modality = dict(view_state.get("dataset_by_modality") or {})
1918
+ current_modality = view_state.get("modality") or _dataset_modality(
1919
+ _item(datasets, dataset_id)
1920
+ )
1921
+ if dataset_id:
1922
+ last_by_modality[current_modality] = dataset_id
1923
+ dataset_id = _default_dataset_id(
1924
+ datasets, modality, last_by_modality.get(modality)
1925
+ )
1926
+ view_state["modality"] = modality
1927
+ view_state["dataset_by_modality"] = last_by_modality
1928
+ result = _apply_filter_change(
1929
+ "modality",
1930
+ dataset_id,
1931
+ metric_id,
1932
+ models,
1933
+ platform_value,
1934
+ owner_value,
1935
+ optimized_value,
1936
+ num_prompts,
1937
+ seed,
1938
+ view_state,
1939
+ )
1940
+ if result is None:
1941
+ return _skip_all(len(dataset_outputs))
1942
+ extras = result["extra_updates"]
1943
+ return (
1944
+ _dataset_dropdown_update(
1945
+ datasets,
1946
+ result["selected_tab"],
1947
+ result["dataset_id"],
1948
+ modality=modality,
1949
+ ),
1950
+ result["metric_update"],
1951
+ result["models_update"],
1952
+ extras[6],
1953
+ extras[0],
1954
+ extras[1],
1955
+ extras[2],
1956
+ *result["views"],
1957
+ gr.update(interactive=result["can_pareto"]),
1958
+ gr.update(interactive=result["can_samples"]),
1959
+ gr.update(selected=result["selected_tab"])
1960
+ if result["selected_tab"] != result["tab"]
1961
+ else gr.skip(),
1962
+ result["state"],
1963
+ )
1964
+
1965
  def on_dataset(
1966
  dataset_id,
1967
  metric_id,
 
2311
  handler.__name__ = f"on_tab_{tab}"
2312
  return handler
2313
 
2314
+ default_modality = _dataset_modality(_item(datasets, default_dataset_id))
2315
  view_state = gr.State(
2316
  {
2317
  "dataset_id": default_dataset_id,
2318
+ "modality": default_modality,
2319
+ "dataset_by_modality": {default_modality: default_dataset_id},
2320
  "metric_id": None,
2321
  "models": [],
2322
  "current_tab": TAB_LEADERBOARDS,
 
2398
  main_tabs,
2399
  view_state,
2400
  ]
2401
+ modality_dd.change(
2402
+ on_modality,
2403
+ inputs=[modality_dd, *filter_inputs],
2404
+ outputs=dataset_outputs,
2405
+ **_VIEW_EVENTS,
2406
+ )
2407
  dataset_dd.change(
2408
  on_dataset,
2409
  inputs=filter_inputs,