diff --git "a/ui.py" "b/ui.py" --- "a/ui.py" +++ "b/ui.py" @@ -1,317 +1,423 @@ from html import escape +from math import ceil, floor, log10 +from pathlib import Path +import base64 import random import gradio as gr import pandas as pd import plotly.graph_objects as go +from model_display import display_model_name + +_LOGO_PATH = Path(__file__).resolve().parent / "pruna-logo.png" +_LOGO_DATA_URI = ( + "data:image/png;base64," + + base64.b64encode(_LOGO_PATH.read_bytes()).decode("ascii") + if _LOGO_PATH.exists() + else "https://playground.pruna.ai/logo.svg" +) + -FILTER_COLUMNS = ["Platform", "Endpoint Owner", "Model", "Optimized"] MAX_COMPARE_MODELS = 4 DEFAULT_COMPARE_PROMPTS = 3 MAX_COMPARE_PROMPTS = 8 +MAX_PARETO_METRICS = 8 +_PARETO_SLOT_COUNT = 1 + MAX_PARETO_METRICS * 8 +_PARETO_PRICE_COLUMN = "Price / Image (USD)" +_PARETO_TIME_COLUMN = "Min Generation Time (s)" +_PARETO_SCALE_CHOICES = [ + ("Linear", "Linear"), + ("Log", "Logarithmic"), +] +_PARETO_SCALE_VALUES = {value for _, value in _PARETO_SCALE_CHOICES} +_PARETO_SCALE_DEFAULT = "Linear" + +TAB_LEADERBOARDS = "leaderboards" +TAB_PARETO = "pareto" +TAB_SAMPLES = "samples" +TAB_ABOUT = "about" + +_MODEL_CHOICES_CACHE = {} +_VIEW_EVENTS = { + "show_progress": "hidden", + "trigger_mode": "always_last", + "concurrency_id": "workspace-views", + "concurrency_limit": 1, +} -ALL_LEADERBOARD_NOTE = """ -> **π‘ Note:** Each efficiency metric and quality metric captures only one dimension of -> model capacity. Rankings may vary when considering other metrics. "Endpoint Owner" -> refers to the publisher of the evaluated endpoint and may differ from the original -> model creator. "Optimized" indicates that the endpoint uses an explicit optimization -> mode or serves a modified/accelerated variant. -""" - -ONEIG_SCORES_CONTENT = """ -### Reported OneIG scores - -- **Anime Alignment** β alignment for anime and stylization prompts -- **Human Alignment** β alignment for portrait prompts -- **Object Alignment** β alignment for general-object prompts - -The leaderboard's **OneIG Overall Score** is the mean of each model's available -category scores. -""" - -ONEIG_RUN_DETAILS_CONTENT = """ -### Reading the results - -Each row records the evaluation date, generation time, price per image, and links to -the source evaluation runs when available. Use the filters in the leaderboard to -compare providers, models, and optimized variants. -""" - -P_JUDGE_SCORES_CONTENT = """ -### Reported P-Judge scores - -- **P-Judge Overall** β automatic preference / quality score for generated images - -Higher scores indicate stronger performance on the P-Judge evaluation. +ABOUT_OVERVIEW_CONTENT = """ +# About P-Bench + +P-Bench compares **text-to-image models**, including optimized or accelerated +endpoints, on **quality, speed, and price**. Each view is a **dataset** scored +with a **metric**, written as `Dataset | Metric`. There is no single score +across P-Bench. + +## How to read it + +1. Pick a **dataset** and a **metric**. +2. **Leaderboards**: ranked by that metric. Price and generation time sit in + the same table when the source publishes them. +3. **Pareto plots**: mark models that are not beaten on both higher score + and lower price (or time). Only datasets with price or generation time + can open this tab (not Arena AI). +4. **Samples**: the same prompts, side by side. Only for datasets we + generated (Qwen Image Dataset and OneIG Alignment Dataset). + +## How a score is made + +On **Qwen Image Dataset** and **OneIG Alignment Dataset**: + +1. Each endpoint is given the same prompt suite. +2. It generates one image per prompt when the run succeeds. Not every model + has every prompt or every metric. +3. Quality is scored automatically (OneIG alignment, P-Judger) and, where + available, by human preference (Datapoint Elo, Rapidata Elo). +4. Price per image and generation time are joined from the evaluation table. + +**Artificial Analysis** and **Arena AI** are external leaderboards. We import +their published Elos (and Artificial Analysis price). We do not run their +prompt suites, so samples are not shown. + +## Current datasets + +### Qwen Image Dataset +100 prompts from the 1,000-prompt Qwen Image Bench set, sampled for coverage +across its fine-grained (L3) categories. Metrics include Datapoint Elo, +Rapidata Elo, and P-Judger. Samples are available. + +### OneIG Alignment Dataset +Prompt-image **alignment** on anime / stylization (100), human / portrait +(100), and general object prompts (99). This is the alignment slice of +OneIG, not the full suite. Alignment Overall is the mean of the category +scores that exist for that row. Also includes Datapoint category Elo, +Rapidata Elo, and P-Judger. Samples are available. + +### Artificial Analysis Dataset +External text-to-image Elo and price per image from Artificial Analysis. +Their prompt set is private, so samples are not shown. Pareto plots use +price vs score only. + +### Arena AI Dataset +External text-to-image Elo (overall and category) from Arena AI. Their +prompt set is private, so samples are not shown. Price and generation time +are not in this export, so Pareto plots are unavailable. """ -P_JUDGE_RUN_DETAILS_CONTENT = """ -### Reading the results - -Each row records the evaluation date, generation time, and price per image when -available. Rankings are ordered by **P-Judge Overall**. +ABOUT_DETAILS_CONTENT = """ +# Metrics & limits + +## Metrics + +- **Alignment**: how well the image matches the prompt (OneIG category scores). +- **P-Judger**: Pruna's automatic judger. A private scoring model that rates + each (prompt, image) pair in one pass and returns an absolute quality score, + not a pairwise Elo. It is not a text-to-image model on the leaderboard. Use + it alongside Datapoint and Rapidata Elo, not instead of them. +- **Datapoint Elo**: human-preference Elo from Datapoint pairwise comparisons. +- **Rapidata Elo**: human-preference Elo from Rapidata pairwise comparisons. + Rapidata rejects prompts over 400 characters, so this Elo is on a subset + of each suite (see Setup). Rapidata is not a dataset. +- **Artificial Analysis Elo**: Elo published by Artificial Analysis on their + own dataset. +- **Arena Elo**: Elo published by Arena AI on their own dataset, plus + category Elos (branding, 3D, cartoon/anime, photorealistic, art, portraits, + text rendering). +- **Generation time**: median and minimum generation time in seconds, as + reported in the evaluation table. This is not a p95, and we do not state + warm vs cold or concurrent load. Not available for Arena AI. +- **Price**: USD per image in the evaluation table. We do not state list + price vs amount paid, or whether failed generations are included. Not + available for Arena AI. + +Scores from different datasets or metrics are **not interchangeable**. A high +OneIG alignment score is not the same quantity as a Datapoint Elo. Compare +models *within* a Dataset | Metric view. + +## Setup + +- **Evaluation window:** JulyβAugust 2026. +- **Update policy:** numbers come from evaluation snapshots in the tables, + not a live API poll. +- **Prompt counts:** OneIG Alignment uses 100 anime, 100 human, and 99 object + prompts (299 total). Qwen Image Dataset uses 100 prompts sampled from the + 1,000-prompt pool for roughly even coverage of its fine-grained (L3) + categories. Artificial Analysis and Arena AI use their own private prompt + sets. +- **Generation (Qwen and OneIG):** one image per prompt per endpoint when + the run exists. Default resolution is 1024Γ1024. Exceptions: FLUX 1.1 Pro + Ultra at 2K, FLUX 2 Flex at 1008Γ1008, and any endpoint labeled 2K. The + seed is derived from the prompt, so every model gets the same seed for the + same prompt. Steps, CFG, prompt rewrite, and safety filters follow each + endpoint's default. This does not describe Artificial Analysis or Arena AI. +- **Datapoint (Qwen and OneIG):** every model pair is compared on every + prompt, with 10 votes per battle. +- **Rapidata (Qwen and OneIG):** prompts longer than 400 characters are + dropped, leaving 212 OneIG prompts and 85 Qwen Image Dataset prompts. 4 + votes per pair; about 26,000 votes on OneIG and 35,000 on Qwen Image + Dataset. + +## Limits + +- Empty cells mean that track was not run or not reported for that model. +- Artificial Analysis and Arena AI samples, prompts, and (for Arena) price + or latency are not available to P-Bench. +- Rapidata Elo is not on the full prompt suite, so it is not directly + comparable to Datapoint Elo even on the same dataset. +- Elo ratings can shift when the comparison pool changes: treat them as + relative rankings for the snapshot, not absolute constants. +- Close scores can be a tie in practice; the table does not show confidence + intervals. +- Optimized or accelerated endpoints (when labeled) may differ from the base + model publisher's default serving stack. +- APIs and prices drift; a rank is for the snapshot, not a guarantee. + +Built by [Pruna AI](https://www.pruna.ai/). If you have questions, reach out +to us on [Discord](https://discord.gg/JFQmtFKCjd). """ -DATAPOINT_ELO_SCORES_CONTENT = """ -### Reported Datapoint Elo scores - -- **Datapoint Elo** β human-preference Elo rating from pairwise comparisons - -Elo values move whenever new models are added and re-evaluated against the field, -so the Date column reflects the leaderboard snapshot date. +FOOTER_CONTENT = """ +
""" -DATAPOINT_ELO_RUN_DETAILS_CONTENT = """ -### Reading the results - -Each row records the snapshot date, generation time, and price per image when -available. Rankings are ordered by **Datapoint Elo**. +CITATION_CONTENT = """ +```bibtex +@misc{P-Bench, + title={P-Bench: A Leaderboard for Text-to-Image Models}, + author={PrunaAI}, + year={2026}, + howpublished={\\url{https://huggingface.co/spaces/PrunaAI/P-Bench}} +} +``` """ -RAPIDATA_ELO_SCORES_CONTENT = """ -### Reported Rapidata Elo scores - -- **Rapidata Elo** β Elo rating from the Rapidata evaluation suite -Higher Elo indicates stronger relative performance on that suite. -""" +def render_header(): + gr.HTML( + f""" +Compare text-to-image models on quality, speed, and price
+- {escape(benchmark.get("card_description", ""))} -
-" + f"{escape(message)}" + "
" + ) -def render_benchmark_detail(benchmark): - gr.Markdown( - f""" - # {benchmark["title"]} +def _pareto_note_update(message): + if message: + return gr.update( + value=_pareto_unavailable_html(message), + visible=True, + ) + return gr.update(value="", visible=False) - {benchmark["intro"]} - """ - ) - with gr.Tabs(elem_classes="subtabs", selected=0) as detail_tabs: - with gr.TabItem("Leaderboard"): - render_leaderboard( - benchmark["data"], - benchmark["columns"], - note=benchmark.get("note"), - score_columns=benchmark.get("score_columns"), - overall_column=benchmark.get("overall_column"), - ) - with gr.TabItem("Graphs"): - render_benchmark_graphs(benchmark) - with gr.TabItem("Compare samples"): - render_compare_samples(benchmark) - return detail_tabs +def _pareto_plot_update(fig): + if fig is not None: + return gr.update(value=fig, visible=True) + return gr.update(value=None, visible=False) -def render_compare_samples(benchmark): - samples = benchmark.get("samples") - if not samples: - gr.Markdown( - """ - Sample comparison is not available for this benchmark yet. - When generations are linked, you will be able to pick models and browse - side-by-side outputs for the same prompts. - """ - ) - return +def _skip_all(count): + return tuple(gr.skip() for _ in range(count)) - models = samples["models"] - default_models = models[: min(2, len(models))] - gr.Markdown( - f""" -- Pick up to {MAX_COMPARE_MODELS} models, then browse shared - prompts side by side. Images come from the public generation URLs for this - benchmark. -
- """ - ) - with gr.Row(elem_classes="leaderboard-controls"): - model_picker = gr.Dropdown( - choices=models, - value=default_models, - multiselect=True, - max_choices=MAX_COMPARE_MODELS, - label="Models", - info=f"Select 1β{MAX_COMPARE_MODELS} models to compare", - scale=3, - ) - prompt_count = gr.Slider( - minimum=1, - maximum=MAX_COMPARE_PROMPTS, - value=DEFAULT_COMPARE_PROMPTS, - step=1, - label="Prompts to show", - scale=1, - ) - shuffle_button = gr.Button("Shuffle prompts", scale=1) - - gallery = gr.HTML( - value=_build_compare_samples_html( - samples, - default_models, - DEFAULT_COMPARE_PROMPTS, - seed=0, - ) - ) - seed_state = gr.State(0) - - def update_gallery(selected_models, num_prompts, seed): - return _build_compare_samples_html( - samples, - selected_models, - int(num_prompts), - seed=int(seed or 0), - ) +def _pareto_skip_updates(): + return _skip_all(_PARETO_SLOT_COUNT) - def shuffle_gallery(selected_models, num_prompts, seed): - next_seed = int(seed or 0) + 1 - return next_seed, _build_compare_samples_html( - samples, - selected_models, - int(num_prompts), - seed=next_seed, - ) - model_picker.change( - update_gallery, - inputs=[model_picker, prompt_count, seed_state], - outputs=gallery, +def _selection_key(dataset_id, metric_id, models): + return ( + dataset_id, + tuple(_normalize_metric_ids(metric_id)), + tuple(models or ()), ) - prompt_count.change( - update_gallery, - inputs=[model_picker, prompt_count, seed_state], - outputs=gallery, + + +def _applied_key(view_state): + view_state = view_state or {} + return _selection_key( + view_state.get("dataset_id"), + view_state.get("metric_id"), + view_state.get("models"), ) - shuffle_button.click( - shuffle_gallery, - inputs=[model_picker, prompt_count, seed_state], - outputs=[seed_state, gallery], + + +def _build_pareto_figure( + data, + score_column, + x_column, + x_title, + x_hover_prefix="", + x_hover_suffix="", + x_axis_type="linear", +): + scatter = ( + data[["Model", score_column, x_column]] + .dropna() + .copy() + .reset_index(drop=True) ) + if scatter.empty: + return None + x_values = scatter[x_column].astype(float).tolist() + scores = scatter[score_column].astype(float).tolist() + on_frontier = _pareto_frontier_mask(x_values, scores) -def _build_compare_samples_html(samples, selected_models, num_prompts, seed=0): - selected_models = [ - model - for model in (selected_models or []) - if model in samples["images"] - ][:MAX_COMPARE_MODELS] + dominated = scatter.loc[[not flag for flag in on_frontier]].copy() + frontier = scatter.loc[on_frontier].sort_values(x_column).copy() + if not dominated.empty: + dominated["Model"] = dominated["Model"].map(display_model_name) + if not frontier.empty: + frontier["Model"] = frontier["Model"].map(display_model_name) + hover = ( + "%{text}" + " ".join(parts) + "
" - with gr.Column(visible=True, elem_classes="benchmark-catalogue") as catalogue: - gr.Markdown( - """ - # Benchmarks - Choose a prompt suite. Each one has a **Leaderboard** table, **Graphs**, - and **Compare samples**. - """ +def _filter_row(datasets, metrics, default_dataset_id, default_metric_id=None): + metric_id = _coerce_metric( + datasets, metrics, default_dataset_id, default_metric_id + ) + with gr.Row(elem_classes="view-filters"): + dataset_dd = gr.Dropdown( + choices=_dataset_choices(datasets), + value=default_dataset_id, + label="Dataset", + type="value", + filterable=False, + scale=2, + min_width=160, ) - card_rows = [benchmarks[i : i + 2] for i in range(0, len(benchmarks), 2)] - for row in card_rows: - with gr.Row(equal_height=True, elem_classes="benchmark-catalogue-row"): - for benchmark in row: - with gr.Column(scale=1, min_width=280): - with gr.Group(elem_classes="benchmark-card"): - gr.Markdown( - f""" - ## {benchmark.get("emoji", "π")} {benchmark["title"]} - - {benchmark["card_description"]} - """ - ) - open_buttons.append( - ( - benchmark["id"], - gr.Button("View benchmark β", variant="primary"), - ) - ) + metric_dd = gr.Dropdown( + choices=_metric_dropdown_choices(datasets, metrics, default_dataset_id), + value=_metric_dropdown_value(metric_id), + label="Metric", + type="value", + multiselect=True, + allow_custom_value=False, + filterable=True, + scale=2, + min_width=180, + elem_classes="filter-chips", + ) + models_dd = gr.Dropdown( + choices=_model_choices(datasets, default_dataset_id), + value=[], + multiselect=True, + label="Models", + type="value", + allow_custom_value=False, + filterable=True, + scale=2, + min_width=180, + elem_classes="filter-chips", + ) + return dataset_dd, metric_dd, models_dd - for benchmark in benchmarks: - with gr.Column(visible=False) as detail: - back_button = gr.Button("β All benchmarks", size="sm") - render_benchmark_detail(benchmark) - detail_entries.append((benchmark["id"], detail, back_button)) - nav_outputs = [catalogue, *[detail for _, detail, _ in detail_entries]] +def render_image_workspace(datasets, metrics, default_dataset_id, default_metric_id): + default_metric_id = _coerce_metric( + datasets, metrics, default_dataset_id, default_metric_id + ) + initial = resolve_view(datasets, metrics, default_dataset_id, default_metric_id) + initial_data = initial["data"] + initial_columns = initial["columns"] + initial_samples = initial.get("samples") + with gr.Column(elem_classes="workspace-shell"): + with gr.Column(elem_classes="workspace-filters") as filters_host: + gr.Markdown( + "" + "These filters apply to Leaderboards, Pareto plots, and Samples. " + "On Samples, only datasets we have generations for are listed. " + "On Pareto plots, only datasets with price or generation time " + "are listed. Search in Models, or leave it empty to include " + "every model." + "
", + elem_classes="filter-help-host", + ) + dataset_dd, metric_dd, models_dd = _filter_row( + datasets, metrics, default_dataset_id, None + ) + with gr.Tabs(elem_classes="main-tabs") as main_tabs: + with gr.TabItem("Leaderboards", id=TAB_LEADERBOARDS) as lb_tab: + lb_note = gr.Markdown( + _leaderboard_intro_markdown(initial.get("note")), + elem_classes="view-help-host", + ) + platform_choices = _filter_choices(initial_data, "Platform") + owner_choices = _filter_choices(initial_data, "Endpoint Owner") + optimized_choices = _filter_choices(initial_data, "Optimized") + with gr.Row( + elem_classes="leaderboard-controls", + visible=bool( + platform_choices or owner_choices or optimized_choices + ), + ) as lb_controls: + platform = gr.Dropdown( + choices=platform_choices, + value=[], + label="Providers", + multiselect=True, + allow_custom_value=False, + filterable=False, + scale=1, + visible=bool(platform_choices), + ) + owner = gr.Dropdown( + choices=owner_choices, + value=[], + label="Endpoint owners", + multiselect=True, + allow_custom_value=False, + filterable=False, + scale=1, + visible=bool(owner_choices), + ) + optimized = gr.Dropdown( + choices=optimized_choices, + value=[], + label="Optimized", + multiselect=True, + allow_custom_value=False, + filterable=False, + scale=1, + visible=bool(optimized_choices), + ) + ranking = gr.HTML( + _leaderboard_html( + _assign_leaderboard_ranks( + initial_data, initial.get("score_column") + ), + initial_columns, + ), + padding=False, + elem_classes="ranking-table-host", + ) + + with gr.TabItem( + "Pareto Plots", + id=TAB_PARETO, + interactive=_dataset_has_pareto(datasets, default_dataset_id), + ) as pp_tab: + gr.Markdown( + "" + "Score against price and generation time. Green points are on " + "the frontier; lavender points sit below it. Hover a point to " + "see which model it is." + "
" + "" + "You can switch between linear and logarithmic scale for all " + "the plots, or individually for each plots." + "
", + elem_classes="view-help-host", + ) + with gr.Row( + equal_height=False, + elem_classes="pareto-scale-all-row", + ): + gr.HTML( + "All plots", + padding=False, + ) + pareto_all_scale = _pareto_scale_radio( + "pareto-scale-toggle-all", + ) + pareto_dataset_note = gr.HTML( + "", + padding=False, + visible=False, + elem_classes="pareto-note", + ) + pareto_slots = [] + for slot_index in range(MAX_PARETO_METRICS): + with gr.Column( + visible=False, + elem_classes="pareto-metric-block", + ) as slot_group: + slot_title = gr.Markdown( + "", + elem_classes="pareto-metric-title", + ) + with gr.Row( + equal_height=True, + elem_classes="pareto-layout", + ) as slot_layout: + with gr.Column( + scale=1, + min_width=320, + elem_classes="pareto-col", + ) as slot_price_col: + slot_price_scale = _pareto_plot_heading( + "Price vs score" + ) + slot_price = gr.Plot( + value=None, + show_label=False, + elem_classes="pareto-plot", + ) + with gr.Column( + scale=1, + min_width=320, + elem_classes="pareto-col", + ) as slot_time_col: + slot_time_scale = _pareto_plot_heading( + "Min generation time vs score" + ) + slot_time = gr.Plot( + value=None, + show_label=False, + elem_classes="pareto-plot", + ) + slot_note = gr.HTML( + "", + padding=False, + visible=False, + elem_classes="pareto-note", + ) + pareto_slots.append( + ( + slot_group, + slot_title, + slot_note, + slot_layout, + slot_price_col, + slot_price, + slot_price_scale, + slot_time_col, + slot_time, + slot_time_scale, + ) + ) - def show_catalogue(_evt=None): + with gr.TabItem( + "Samples", + id=TAB_SAMPLES, + interactive=_dataset_has_samples(datasets, default_dataset_id), + ) as sm_tab: + with gr.Column(visible=bool(initial_samples)) as samples_panel: + gr.Markdown( + f"" + f"The same prompts, side by side. Select up to " + f"{MAX_COMPARE_MODELS} models above, or leave " + f"Models empty for two defaults." + f"
", + elem_classes="view-help-host", + ) + with gr.Row(equal_height=False, elem_classes="compare-controls"): + prompt_count = gr.Slider( + minimum=1, + maximum=MAX_COMPARE_PROMPTS, + value=DEFAULT_COMPARE_PROMPTS, + step=1, + label="Prompts to show", + container=False, + show_reset_button=False, + scale=1, + min_width=180, + elem_classes="compare-prompt-count", + ) + shuffle_button = gr.Button( + "Shuffle prompts", + variant="primary", + scale=0, + min_width=140, + elem_classes="compare-shuffle", + ) + gallery = gr.HTML( + value=_samples_html( + initial_samples, [], DEFAULT_COMPARE_PROMPTS, seed=0 + ), + elem_classes="compare-gallery", + ) + seed_state = gr.State(0) + + with gr.TabItem("About", id=TAB_ABOUT) as about_tab: + render_about() + + def _synced_filters(dataset_id, metric_id, models, *, clear_metric=False): + if clear_metric: + metric_id = [] + else: + metric_id = _coerce_metric(datasets, metrics, dataset_id, metric_id) + model_choices = _model_choices(datasets, dataset_id) + model_values = set(_model_choice_values(model_choices)) + models = [model for model in (models or []) if model in model_values] + metric_choices = _metric_dropdown_choices(datasets, metrics, dataset_id) return ( - gr.Column(visible=True), - *[gr.Column(visible=False) for _ in detail_entries], + dataset_id, + metric_id, + models, + gr.update( + choices=metric_choices, + value=_metric_dropdown_value(metric_id), + ), + gr.update(choices=model_choices, value=models), ) - def show_detail(selected_id): + def _leaderboard_extras(data, platform_value, owner_value, optimized_value): + platform_choices = _filter_choices(data, "Platform") + owner_choices = _filter_choices(data, "Endpoint Owner") + optimized_choices = _filter_choices(data, "Optimized") + platform_value = [ + value for value in (platform_value or []) if value in platform_choices + ] + owner_value = [ + value for value in (owner_value or []) if value in owner_choices + ] + optimized_value = [ + value for value in (optimized_value or []) if value in optimized_choices + ] return ( - gr.Column(visible=False), - *[ - gr.Column(visible=(benchmark_id == selected_id)) - for benchmark_id, _, _ in detail_entries - ], + gr.update( + choices=platform_choices, + value=platform_value, + visible=bool(platform_choices), + ), + gr.update( + choices=owner_choices, + value=owner_value, + visible=bool(owner_choices), + ), + gr.update( + choices=optimized_choices, + value=optimized_value, + visible=bool(optimized_choices), + ), + platform_value, + owner_value, + optimized_value, + gr.update( + visible=bool( + platform_choices or owner_choices or optimized_choices + ) + ), ) - for benchmark_id, button in open_buttons: - button.click( - lambda selected_id=benchmark_id: show_detail(selected_id), - outputs=nav_outputs, + def _content_flags(tab): + return { + "include_leaderboard": tab == TAB_LEADERBOARDS, + "include_pareto": tab == TAB_PARETO, + "include_samples": tab == TAB_SAMPLES, + } + + def _commit_state( + view_state, + dataset_id, + metric_id, + models, + tab, + flags, + extras=None, + ): + prev = dict(view_state or {}) + extras = extras or {} + return { + "dataset_id": dataset_id, + "metric_id": metric_id, + "models": list(models or []), + "current_tab": tab, + "platform": list( + extras.get("platform", prev.get("platform") or []) + ), + "owner": list(extras.get("owner", prev.get("owner") or [])), + "optimized": list( + extras.get("optimized", prev.get("optimized") or []) + ), + "price_scales": _normalize_pareto_scales(prev.get("price_scales")), + "time_scales": _normalize_pareto_scales(prev.get("time_scales")), + "stale": { + TAB_LEADERBOARDS: not flags["include_leaderboard"], + TAB_PARETO: not flags["include_pareto"], + TAB_SAMPLES: not flags["include_samples"], + }, + } + + def _save_leaderboard_filters( + view_state, platform_value, owner_value, optimized_value + ): + view_state["platform"] = list(platform_value or []) + view_state["owner"] = list(owner_value or []) + view_state["optimized"] = list(optimized_value or []) + return view_state + + def _restore_leaderboard_filters( + view_state, platform_value, owner_value, optimized_value + ): + stored_platform = (view_state or {}).get("platform") or [] + stored_owner = (view_state or {}).get("owner") or [] + stored_optimized = (view_state or {}).get("optimized") or [] + view = resolve_view( + datasets, + metrics, + (view_state or {}).get("dataset_id"), + (view_state or {}).get("metric_id"), + ) + extras = _leaderboard_extras( + view["data"] if view else None, + stored_platform, + stored_owner, + stored_optimized, + ) + platform_update = extras[0] if list(platform_value or []) != extras[3] else gr.skip() + owner_update = extras[1] if list(owner_value or []) != extras[4] else gr.skip() + optimized_update = extras[2] if list(optimized_value or []) != extras[5] else gr.skip() + return ( + extras[6], + platform_update, + owner_update, + optimized_update, + extras[3], + extras[4], + extras[5], ) - for _, _, back_button in detail_entries: - back_button.click(show_catalogue, outputs=nav_outputs) + def _views( + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + *, + include_leaderboard=True, + include_pareto=False, + include_samples=False, + price_scales=None, + time_scales=None, + ): + view = resolve_view(datasets, metrics, dataset_id, metric_id) + data = view["data"] + if include_leaderboard: + note = _leaderboard_intro_markdown(view.get("note")) + ranking_html = _leaderboard_html( + _filter_leaderboard( + _assign_leaderboard_ranks(data, view["score_column"]), + platform_value or [], + owner_value or [], + optimized_value or [], + models=models, + ), + view["columns"], + ) + else: + note = gr.skip() + ranking_html = gr.skip() + if include_pareto: + pareto_data = _filter_leaderboard(data, [], [], [], models=models) + pareto_updates = _pareto_slot_updates( + pareto_data, + view["score_columns"], + price_scales=price_scales, + time_scales=time_scales, + ) + else: + pareto_updates = _pareto_skip_updates() + if include_samples: + samples = view.get("samples") + samples_html = _samples_html( + samples, + models, + int(num_prompts or DEFAULT_COMPARE_PROMPTS), + int(seed or 0), + ) + samples_visible = gr.update(visible=bool(samples)) + else: + samples_html = gr.skip() + samples_visible = gr.skip() + return ( + note, + ranking_html, + *pareto_updates, + samples_html, + samples_visible, + ) - return show_catalogue, nav_outputs + def _apply_filter_change( + source, + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ): + view_state = dict(view_state or {}) + tab = view_state.get("current_tab") or TAB_LEADERBOARDS + selected_raw = _normalize_metric_ids(metric_id) + incoming_models = list(models or []) + dataset_changed = source == "dataset" and dataset_id != view_state.get( + "dataset_id" + ) + if source == "dataset": + synced = _synced_filters( + dataset_id, metric_id, models, clear_metric=dataset_changed + ) + dataset_id, metric_id, models = synced[:3] + metric_update, models_update = synced[3], synced[4] + else: + metric_id = _coerce_metric(datasets, metrics, dataset_id, metric_id) + if source == "models": + allowed = set( + _model_choice_values(_model_choices(datasets, dataset_id)) + ) + models = [model for model in incoming_models if model in allowed] + models_update = ( + gr.update(value=models) + if models != incoming_models + else gr.skip() + ) + else: + models = incoming_models + models_update = gr.skip() + metric_update = ( + gr.update( + choices=_metric_dropdown_choices(datasets, metrics, dataset_id), + value=_metric_dropdown_value(metric_id), + ) + if source == "metric" and ALL_METRICS_ID in selected_raw + else gr.skip() + ) -def _pareto_frontier_mask(x_values, scores): - """True for non-dominated points when maximizing score and minimizing x.""" - n = len(x_values) - mask = [True] * n - for i in range(n): - for j in range(n): - if i == j: - continue - better_or_equal = x_values[j] <= x_values[i] and scores[j] >= scores[i] - strictly_better = x_values[j] < x_values[i] or scores[j] > scores[i] - if better_or_equal and strictly_better: - mask[i] = False - break - return mask + unchanged = _applied_key(view_state) == _selection_key( + dataset_id, metric_id, models + ) + if ( + not dataset_changed + and not (source == "metric" and ALL_METRICS_ID in selected_raw) + and unchanged + ): + return None + selected_tab = tab + extras = ( + list(platform_value or []), + list(owner_value or []), + list(optimized_value or []), + ) + extra_updates = None + can_pareto = _dataset_has_pareto(datasets, dataset_id) + can_samples = _dataset_has_samples(datasets, dataset_id) + if source == "dataset": + if tab == TAB_SAMPLES and not can_samples: + selected_tab = TAB_LEADERBOARDS + elif tab == TAB_PARETO and not can_pareto: + selected_tab = TAB_LEADERBOARDS + view = resolve_view(datasets, metrics, dataset_id, metric_id) + extra_updates = _leaderboard_extras( + view["data"] if view else None, + platform_value, + owner_value, + optimized_value, + ) + extras = extra_updates[3:6] -def _build_pareto_figure( - data, - score_column, - x_column, - x_title, - x_hover_prefix="", - x_hover_suffix="", -): - scatter = ( - data[["Model", score_column, x_column]] - .dropna() - .copy() - .reset_index(drop=True) - ) - if scatter.empty: - return None + flags = _content_flags(selected_tab) + extras_payload = ( + { + "platform": extras[0], + "owner": extras[1], + "optimized": extras[2], + } + if selected_tab == TAB_LEADERBOARDS + else {} + ) + return { + "dataset_id": dataset_id, + "metric_update": metric_update, + "models_update": models_update, + "selected_tab": selected_tab, + "tab": tab, + "extra_updates": extra_updates, + "can_pareto": can_pareto, + "can_samples": can_samples, + "views": _views( + dataset_id, + metric_id, + models, + extras[0], + extras[1], + extras[2], + num_prompts, + seed, + price_scales=view_state.get("price_scales"), + time_scales=view_state.get("time_scales"), + **flags, + ), + "state": _commit_state( + view_state, + dataset_id, + metric_id, + models, + selected_tab, + flags, + extras=extras_payload, + ), + } + + def on_dataset( + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ): + result = _apply_filter_change( + "dataset", + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ) + if result is None: + return _skip_all(len(dataset_outputs)) + extras = result["extra_updates"] + return ( + _dataset_dropdown_update( + datasets, result["selected_tab"], result["dataset_id"] + ), + result["metric_update"], + result["models_update"], + extras[6], + extras[0], + extras[1], + extras[2], + *result["views"], + gr.update(interactive=result["can_pareto"]), + gr.update(interactive=result["can_samples"]), + gr.update(selected=result["selected_tab"]) + if result["selected_tab"] != result["tab"] + else gr.skip(), + result["state"], + ) - x_values = scatter[x_column].astype(float).tolist() - scores = scatter[score_column].astype(float).tolist() - on_frontier = _pareto_frontier_mask(x_values, scores) + def on_metric( + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ): + result = _apply_filter_change( + "metric", + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ) + if result is None: + return _skip_all(len(metric_outputs)) + return (result["metric_update"], *result["views"], result["state"]) + + def on_models( + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ): + result = _apply_filter_change( + "models", + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ) + if result is None: + return _skip_all(len(models_outputs)) + return (result["models_update"], *result["views"], result["state"]) + + def on_tab_select( + tab, + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + view_state, + ): + view_state = dict(view_state or {}) + prev_tab = view_state.get("current_tab") or TAB_LEADERBOARDS + requested_tab = tab + if tab == TAB_SAMPLES and not _dataset_has_samples(datasets, dataset_id): + tab = TAB_LEADERBOARDS + elif tab == TAB_PARETO and not _dataset_has_pareto(datasets, dataset_id): + tab = TAB_LEADERBOARDS + if prev_tab == TAB_LEADERBOARDS: + _save_leaderboard_filters( + view_state, + platform_value, + owner_value, + optimized_value, + ) + dataset_update = _dataset_dropdown_update(datasets, tab, dataset_id) + metric_id = _coerce_metric(datasets, metrics, dataset_id, metric_id) + models = list(models or []) + view_state["current_tab"] = tab + view_state["dataset_id"] = dataset_id + view_state["metric_id"] = metric_id + view_state["models"] = models + show_filters = tab != TAB_ABOUT + show_metric = tab in (TAB_LEADERBOARDS, TAB_PARETO) + was_filters = prev_tab != TAB_ABOUT + was_metric = prev_tab in (TAB_LEADERBOARDS, TAB_PARETO) + filters_vis = ( + gr.update(visible=show_filters) + if show_filters != was_filters + else gr.skip() + ) + metric_vis = ( + gr.update(visible=show_metric) + if show_metric != was_metric + else gr.skip() + ) + if tab == TAB_LEADERBOARDS: + restored = _restore_leaderboard_filters( + view_state, + platform_value, + owner_value, + optimized_value, + ) + platform_value = restored[4] + owner_value = restored[5] + optimized_value = restored[6] + lb_filters = restored[:4] + else: + lb_filters = _skip_all(4) + stale = dict(view_state.get("stale") or {}) + chrome = ( + filters_vis, + dataset_update, + metric_vis, + gr.skip(), + *lb_filters, + ) + tab_select = ( + gr.update(selected=tab) + if tab != requested_tab + else gr.skip() + ) + if tab == TAB_ABOUT or not stale.get(tab, True): + return ( + *chrome, + *_skip_all(len(view_outputs)), + tab_select, + view_state, + ) + flags = _content_flags(tab) + views = _views( + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + num_prompts, + seed, + price_scales=view_state.get("price_scales"), + time_scales=view_state.get("time_scales"), + **flags, + ) + stale[tab] = False + view_state["stale"] = stale + return (*chrome, *views, tab_select, view_state) + + def on_leaderboard_filters( + dataset_id, + metric_id, + models, + platform_value, + owner_value, + optimized_value, + view_state, + ): + view_state = dict(view_state or {}) + _save_leaderboard_filters( + view_state, + platform_value, + owner_value, + optimized_value, + ) + view = resolve_view(datasets, metrics, dataset_id, metric_id) + return ( + _leaderboard_html( + _filter_leaderboard( + _assign_leaderboard_ranks(view["data"], view["score_column"]), + platform_value or [], + owner_value or [], + optimized_value or [], + models=models, + ), + view["columns"], + ), + view_state, + ) - dominated = scatter.loc[[not flag for flag in on_frontier]] - frontier = scatter.loc[on_frontier].sort_values(x_column) - hover = ( - "%{text}