Spaces:
Sleeping
Sleeping
Update leaderboard sorting and readability
Browse filesAdd sorting by every leaderboard item and improve text contrast.
- .gitattributes +1 -0
- LICENSE +21 -0
- README.md +63 -37
- app.py +578 -192
- assets/avbench_outline.png +3 -0
- data/leaderboard.csv +14 -0
- data/submission_template.csv +2 -0
- leaderboard.py +384 -0
- pytest.ini +2 -0
- requirements-dev.txt +1 -0
- requirements.txt +2 -16
- submission.py +311 -0
- tests/__pycache__/test_leaderboard.cpython-312-pytest-9.1.1.pyc +0 -0
- tests/__pycache__/test_submission.cpython-312-pytest-9.1.1.pyc +0 -0
- tests/test_leaderboard.py +80 -0
- tests/test_submission.py +67 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 35 |
scale-hf-logo.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 35 |
scale-hf-logo.png filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/avbench_outline.png filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) Microsoft Corporation.
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,48 +1,74 @@
|
|
| 1 |
---
|
| 2 |
-
title: AVGen
|
| 3 |
-
emoji: 🥇
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
app_file: app.py
|
| 8 |
-
pinned:
|
| 9 |
license: mit
|
| 10 |
-
short_description: Duplicate this leaderboard to initialize your own!
|
| 11 |
-
sdk_version: 5.43.1
|
| 12 |
-
tags:
|
| 13 |
-
- leaderboard
|
| 14 |
---
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
```
|
| 38 |
|
| 39 |
-
|
| 40 |
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
|
| 45 |
-
|
| 46 |
-
-
|
| 47 |
-
-
|
| 48 |
-
- the logic to allow or filter submissions in `src/submission/submit.py` and `src/submission/check_validity.py`
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AVGen-Bench Leaderboard
|
|
|
|
|
|
|
|
|
|
| 3 |
sdk: gradio
|
| 4 |
app_file: app.py
|
| 5 |
+
pinned: false
|
| 6 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
---
|
| 8 |
|
| 9 |
+
# AVGen-Bench Leaderboard
|
| 10 |
+
|
| 11 |
+
This is a Hugging Face Space for the AVGen-Bench Text-to-Audio-Video generation leaderboard.
|
| 12 |
+
|
| 13 |
+
The app loads scores from `data/leaderboard.csv`, renders an interactive leaderboard, and documents the Scheme 2 aggregate metric used by AVGen-Bench.
|
| 14 |
+
|
| 15 |
+
## Local Run
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
python -m venv .venv
|
| 19 |
+
source .venv/bin/activate
|
| 20 |
+
pip install -r requirements.txt
|
| 21 |
+
python app.py
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## Deploy to Hugging Face Spaces
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
git init
|
| 28 |
+
git branch -M main
|
| 29 |
+
git add .
|
| 30 |
+
git commit -m "Initial AVGen-Bench leaderboard Space"
|
| 31 |
+
|
| 32 |
+
hf auth login
|
| 33 |
+
hf repos create spaces/<your-username>/AVGen-Bench-Leaderboard --type space --space-sdk gradio
|
| 34 |
+
git remote add space https://huggingface.co/spaces/<your-username>/AVGen-Bench-Leaderboard
|
| 35 |
+
git push space main
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Updating Results
|
| 39 |
+
|
| 40 |
+
Edit `data/leaderboard.csv` with one row per model. The expected columns are:
|
| 41 |
+
|
| 42 |
+
```text
|
| 43 |
+
Model, Components, Component Type, Vis, Aud (PQ), AV, Lip, Text, Face, Music,
|
| 44 |
+
Speech, Lo-Phy, Hi-Phy, Holistic, Total, Source
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
`data/submission_template.csv` provides a one-row template for new submissions.
|
| 48 |
+
|
| 49 |
+
## Public Submissions
|
| 50 |
+
|
| 51 |
+
The Space includes a `Submission` tab and a Gradio API endpoint named `submit_score`.
|
| 52 |
+
Submitted entries are treated as pending review:
|
| 53 |
+
|
| 54 |
+
1. Users submit raw metric values, model metadata, a public contact, and evaluation artifact links.
|
| 55 |
+
2. The app recomputes `Total` from the raw metrics using the AVGen-Bench Scheme 2 formula.
|
| 56 |
+
3. The submission is written to a review backend; accepted entries should then be merged into `data/leaderboard.csv`.
|
| 57 |
+
|
| 58 |
+
Production backend:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
SUBMISSION_BACKEND=github_issue
|
| 62 |
+
GITHUB_REPO=<owner>/<repo>
|
| 63 |
+
GITHUB_TOKEN=<token-with-issues-write-access>
|
| 64 |
```
|
| 65 |
|
| 66 |
+
Store `GITHUB_TOKEN` as a Hugging Face Space secret, not in source control. If those variables are absent, the app falls back to `local_file` and writes JSON packets under `pending_submissions/`; for a production Space, use the GitHub issue backend or set `PENDING_SUBMISSION_DIR=/data/pending_submissions` with persistent Space storage.
|
| 67 |
|
| 68 |
+
## Sources
|
| 69 |
|
| 70 |
+
Leaderboard values and the overview figure were initialized from `microsoft/AVGen-Bench` commit `1049eab`.
|
| 71 |
|
| 72 |
+
- Project: https://github.com/microsoft/AVGen-Bench
|
| 73 |
+
- Paper: https://arxiv.org/abs/2604.08540
|
| 74 |
+
- Dataset: https://huggingface.co/datasets/microsoft/AVGen-Bench
|
|
|
app.py
CHANGED
|
@@ -1,204 +1,590 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
)
|
| 15 |
-
from src.display.css_html_js import custom_css
|
| 16 |
-
from src.display.utils import (
|
| 17 |
-
BENCHMARK_COLS,
|
| 18 |
-
COLS,
|
| 19 |
-
EVAL_COLS,
|
| 20 |
-
EVAL_TYPES,
|
| 21 |
-
AutoEvalColumn,
|
| 22 |
-
ModelType,
|
| 23 |
-
fields,
|
| 24 |
-
WeightType,
|
| 25 |
-
Precision
|
| 26 |
)
|
| 27 |
-
from
|
| 28 |
-
from src.populate import get_evaluation_queue_df, get_leaderboard_df
|
| 29 |
-
from src.submission.submit import add_new_eval
|
| 30 |
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
print(EVAL_REQUESTS_PATH)
|
| 38 |
-
snapshot_download(
|
| 39 |
-
repo_id=QUEUE_REPO, local_dir=EVAL_REQUESTS_PATH, repo_type="dataset", tqdm_class=None, etag_timeout=30, token=TOKEN
|
| 40 |
-
)
|
| 41 |
-
except Exception:
|
| 42 |
-
restart_space()
|
| 43 |
-
try:
|
| 44 |
-
print(EVAL_RESULTS_PATH)
|
| 45 |
-
snapshot_download(
|
| 46 |
-
repo_id=RESULTS_REPO, local_dir=EVAL_RESULTS_PATH, repo_type="dataset", tqdm_class=None, etag_timeout=30, token=TOKEN
|
| 47 |
-
)
|
| 48 |
-
except Exception:
|
| 49 |
-
restart_space()
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
LEADERBOARD_DF = get_leaderboard_df(EVAL_RESULTS_PATH, EVAL_REQUESTS_PATH, COLS, BENCHMARK_COLS)
|
| 53 |
-
|
| 54 |
-
(
|
| 55 |
-
finished_eval_queue_df,
|
| 56 |
-
running_eval_queue_df,
|
| 57 |
-
pending_eval_queue_df,
|
| 58 |
-
) = get_evaluation_queue_df(EVAL_REQUESTS_PATH, EVAL_COLS)
|
| 59 |
-
|
| 60 |
-
def init_leaderboard(dataframe):
|
| 61 |
-
if dataframe is None or dataframe.empty:
|
| 62 |
-
raise ValueError("Leaderboard DataFrame is empty or None.")
|
| 63 |
-
return Leaderboard(
|
| 64 |
-
value=dataframe,
|
| 65 |
-
datatype=[c.type for c in fields(AutoEvalColumn)],
|
| 66 |
-
select_columns=SelectColumns(
|
| 67 |
-
default_selection=[c.name for c in fields(AutoEvalColumn) if c.displayed_by_default],
|
| 68 |
-
cant_deselect=[c.name for c in fields(AutoEvalColumn) if c.never_hidden],
|
| 69 |
-
label="Select Columns to Display:",
|
| 70 |
-
),
|
| 71 |
-
search_columns=[AutoEvalColumn.model.name, AutoEvalColumn.license.name],
|
| 72 |
-
hide_columns=[c.name for c in fields(AutoEvalColumn) if c.hidden],
|
| 73 |
-
filter_columns=[
|
| 74 |
-
ColumnFilter(AutoEvalColumn.model_type.name, type="checkboxgroup", label="Model types"),
|
| 75 |
-
ColumnFilter(AutoEvalColumn.precision.name, type="checkboxgroup", label="Precision"),
|
| 76 |
-
ColumnFilter(
|
| 77 |
-
AutoEvalColumn.params.name,
|
| 78 |
-
type="slider",
|
| 79 |
-
min=0.01,
|
| 80 |
-
max=150,
|
| 81 |
-
label="Select the number of parameters (B)",
|
| 82 |
-
),
|
| 83 |
-
ColumnFilter(
|
| 84 |
-
AutoEvalColumn.still_on_hub.name, type="boolean", label="Deleted/incomplete", default=True
|
| 85 |
-
),
|
| 86 |
-
],
|
| 87 |
-
bool_checkboxgroup_label="Hide models",
|
| 88 |
-
interactive=False,
|
| 89 |
-
)
|
| 90 |
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
|
| 96 |
-
|
| 97 |
-
with gr.Tabs(elem_classes="tab-buttons") as tabs:
|
| 98 |
-
with gr.TabItem("🏅 LLM Benchmark", elem_id="llm-benchmark-tab-table", id=0):
|
| 99 |
-
leaderboard = init_leaderboard(LEADERBOARD_DF)
|
| 100 |
-
|
| 101 |
-
with gr.TabItem("📝 About", elem_id="llm-benchmark-tab-table", id=2):
|
| 102 |
-
gr.Markdown(LLM_BENCHMARKS_TEXT, elem_classes="markdown-text")
|
| 103 |
-
|
| 104 |
-
with gr.TabItem("🚀 Submit here! ", elem_id="llm-benchmark-tab-table", id=3):
|
| 105 |
-
with gr.Column():
|
| 106 |
-
with gr.Row():
|
| 107 |
-
gr.Markdown(EVALUATION_QUEUE_TEXT, elem_classes="markdown-text")
|
| 108 |
-
|
| 109 |
-
with gr.Column():
|
| 110 |
-
with gr.Accordion(
|
| 111 |
-
f"✅ Finished Evaluations ({len(finished_eval_queue_df)})",
|
| 112 |
-
open=False,
|
| 113 |
-
):
|
| 114 |
-
with gr.Row():
|
| 115 |
-
finished_eval_table = gr.components.Dataframe(
|
| 116 |
-
value=finished_eval_queue_df,
|
| 117 |
-
headers=EVAL_COLS,
|
| 118 |
-
datatype=EVAL_TYPES,
|
| 119 |
-
row_count=5,
|
| 120 |
-
)
|
| 121 |
-
with gr.Accordion(
|
| 122 |
-
f"🔄 Running Evaluation Queue ({len(running_eval_queue_df)})",
|
| 123 |
-
open=False,
|
| 124 |
-
):
|
| 125 |
-
with gr.Row():
|
| 126 |
-
running_eval_table = gr.components.Dataframe(
|
| 127 |
-
value=running_eval_queue_df,
|
| 128 |
-
headers=EVAL_COLS,
|
| 129 |
-
datatype=EVAL_TYPES,
|
| 130 |
-
row_count=5,
|
| 131 |
-
)
|
| 132 |
-
|
| 133 |
-
with gr.Accordion(
|
| 134 |
-
f"⏳ Pending Evaluation Queue ({len(pending_eval_queue_df)})",
|
| 135 |
-
open=False,
|
| 136 |
-
):
|
| 137 |
-
with gr.Row():
|
| 138 |
-
pending_eval_table = gr.components.Dataframe(
|
| 139 |
-
value=pending_eval_queue_df,
|
| 140 |
-
headers=EVAL_COLS,
|
| 141 |
-
datatype=EVAL_TYPES,
|
| 142 |
-
row_count=5,
|
| 143 |
-
)
|
| 144 |
-
with gr.Row():
|
| 145 |
-
gr.Markdown("# ✉️✨ Submit your model here!", elem_classes="markdown-text")
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-
with gr.
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
| 199 |
)
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
import gradio as gr
|
| 7 |
+
|
| 8 |
+
from leaderboard import (
|
| 9 |
+
DATA_PATH,
|
| 10 |
+
SORT_CHOICES,
|
| 11 |
+
filter_leaderboard,
|
| 12 |
+
load_leaderboard,
|
| 13 |
+
metric_standings,
|
| 14 |
+
model_choices,
|
| 15 |
+
render_methodology,
|
| 16 |
+
render_profile,
|
| 17 |
+
render_summary,
|
| 18 |
+
render_table,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
)
|
| 20 |
+
from submission import submit_score
|
|
|
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
+
ROOT = Path(__file__).parent
|
| 24 |
+
OVERVIEW_IMAGE = ROOT / "assets" / "avbench_outline.png"
|
| 25 |
+
SUBMISSION_TEMPLATE = ROOT / "data" / "submission_template.csv"
|
| 26 |
|
| 27 |
+
LEADERBOARD = load_leaderboard(DATA_PATH)
|
| 28 |
+
STANDINGS = metric_standings(LEADERBOARD)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
+
def update_leaderboard(component_type: str, query: str, sort_by: str, sort_order: str):
|
| 32 |
+
view = filter_leaderboard(LEADERBOARD, component_type, query, sort_by, sort_order)
|
| 33 |
+
return render_summary(LEADERBOARD, view), render_table(view, STANDINGS, sort_by, sort_order)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
+
|
| 36 |
+
def update_profile(model: str):
|
| 37 |
+
return render_profile(LEADERBOARD, model)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
CSS = """
|
| 41 |
+
:root {
|
| 42 |
+
--avgen-bg: #f6f7f9;
|
| 43 |
+
--avgen-panel: #ffffff;
|
| 44 |
+
--avgen-ink: #18202b;
|
| 45 |
+
--avgen-muted: #5f6b7a;
|
| 46 |
+
--avgen-line: #d9dee7;
|
| 47 |
+
--avgen-blue: #1d5f9f;
|
| 48 |
+
--avgen-teal: #087a73;
|
| 49 |
+
--avgen-orange: #b45309;
|
| 50 |
+
--avgen-green: #24784f;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.gradio-container {
|
| 54 |
+
max-width: 1280px !important;
|
| 55 |
+
margin: 0 auto !important;
|
| 56 |
+
background: var(--avgen-bg) !important;
|
| 57 |
+
color: var(--avgen-ink) !important;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.app-header {
|
| 61 |
+
display: grid;
|
| 62 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 63 |
+
gap: 24px;
|
| 64 |
+
align-items: end;
|
| 65 |
+
padding: 26px 0 12px;
|
| 66 |
+
border-bottom: 1px solid var(--avgen-line);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.app-header h1 {
|
| 70 |
+
margin: 0;
|
| 71 |
+
color: #111827 !important;
|
| 72 |
+
font-size: clamp(28px, 4vw, 44px);
|
| 73 |
+
font-weight: 850;
|
| 74 |
+
line-height: 1.05;
|
| 75 |
+
letter-spacing: 0;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.app-header p {
|
| 79 |
+
margin: 10px 0 0;
|
| 80 |
+
max-width: 860px;
|
| 81 |
+
color: #4b5563 !important;
|
| 82 |
+
font-size: 16px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.header-links {
|
| 86 |
+
display: flex;
|
| 87 |
+
flex-wrap: wrap;
|
| 88 |
+
gap: 8px;
|
| 89 |
+
justify-content: flex-end;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.header-links a {
|
| 93 |
+
color: var(--avgen-blue);
|
| 94 |
+
text-decoration: none;
|
| 95 |
+
border: 1px solid var(--avgen-line);
|
| 96 |
+
background: #fff;
|
| 97 |
+
padding: 8px 11px;
|
| 98 |
+
border-radius: 8px;
|
| 99 |
+
font-weight: 650;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.summary-grid {
|
| 103 |
+
display: grid;
|
| 104 |
+
grid-template-columns: repeat(5, minmax(130px, 1fr));
|
| 105 |
+
gap: 10px;
|
| 106 |
+
margin: 14px 0;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.summary-card,
|
| 110 |
+
.method-card {
|
| 111 |
+
border: 1px solid var(--avgen-line);
|
| 112 |
+
background: var(--avgen-panel);
|
| 113 |
+
border-radius: 8px;
|
| 114 |
+
padding: 12px;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.summary-card span,
|
| 118 |
+
.profile-total span,
|
| 119 |
+
.eyebrow {
|
| 120 |
+
display: block;
|
| 121 |
+
color: #4f5d6e !important;
|
| 122 |
+
font-size: 12px;
|
| 123 |
+
text-transform: uppercase;
|
| 124 |
+
letter-spacing: 0;
|
| 125 |
+
font-weight: 700;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.summary-card strong {
|
| 129 |
+
display: block;
|
| 130 |
+
margin-top: 4px;
|
| 131 |
+
color: #111827 !important;
|
| 132 |
+
font-size: 26px;
|
| 133 |
+
font-weight: 850;
|
| 134 |
+
line-height: 1;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.summary-card small {
|
| 138 |
+
display: block;
|
| 139 |
+
margin-top: 6px;
|
| 140 |
+
color: #4b5563 !important;
|
| 141 |
+
min-height: 32px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.table-shell {
|
| 145 |
+
overflow-x: auto;
|
| 146 |
+
border: 1px solid var(--avgen-line);
|
| 147 |
+
border-radius: 8px;
|
| 148 |
+
background: #fff;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.leaderboard-table {
|
| 152 |
+
width: 100%;
|
| 153 |
+
border-collapse: collapse;
|
| 154 |
+
min-width: 1180px;
|
| 155 |
+
font-size: 14px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.leaderboard-table th,
|
| 159 |
+
.leaderboard-table td {
|
| 160 |
+
padding: 11px 10px;
|
| 161 |
+
border-bottom: 1px solid #edf0f5;
|
| 162 |
+
vertical-align: middle;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.leaderboard-table tbody td {
|
| 166 |
+
color: var(--avgen-ink) !important;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.leaderboard-table thead th {
|
| 170 |
+
position: sticky;
|
| 171 |
+
top: 0;
|
| 172 |
+
background: #f1f4f8;
|
| 173 |
+
color: #202936 !important;
|
| 174 |
+
text-align: left;
|
| 175 |
+
white-space: nowrap;
|
| 176 |
+
z-index: 1;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.leaderboard-table thead th.sorted {
|
| 180 |
+
background: #e6eef8;
|
| 181 |
+
color: #111827 !important;
|
| 182 |
+
box-shadow: inset 0 -2px 0 var(--avgen-blue);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.sort-indicator {
|
| 186 |
+
display: inline-block;
|
| 187 |
+
margin-left: 6px;
|
| 188 |
+
color: var(--avgen-blue);
|
| 189 |
+
font-weight: 850;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.leaderboard-table tbody tr:hover {
|
| 193 |
+
background: #faf7f1;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.rank-cell {
|
| 197 |
+
width: 62px;
|
| 198 |
+
color: #4f5d6e !important;
|
| 199 |
+
font-weight: 700;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.model-cell {
|
| 203 |
+
min-width: 180px;
|
| 204 |
+
color: #16202d !important;
|
| 205 |
+
font-weight: 750;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.components-cell {
|
| 209 |
+
min-width: 220px;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.metric-cell {
|
| 213 |
+
text-align: right;
|
| 214 |
+
color: #253142 !important;
|
| 215 |
+
font-weight: 650;
|
| 216 |
+
font-variant-numeric: tabular-nums;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.metric-cell.best {
|
| 220 |
+
color: #0f6a43 !important;
|
| 221 |
+
font-weight: 800;
|
| 222 |
+
background: #edf8f2;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.metric-cell.second {
|
| 226 |
+
color: #8a4b08 !important;
|
| 227 |
+
font-weight: 750;
|
| 228 |
+
background: #fff7e8;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.component-badge,
|
| 232 |
+
.type-badge {
|
| 233 |
+
display: inline-flex;
|
| 234 |
+
align-items: center;
|
| 235 |
+
max-width: 100%;
|
| 236 |
+
margin: 2px 4px 2px 0;
|
| 237 |
+
padding: 3px 8px;
|
| 238 |
+
border-radius: 999px;
|
| 239 |
+
font-size: 12px;
|
| 240 |
+
font-weight: 700;
|
| 241 |
+
white-space: nowrap;
|
| 242 |
+
border: 1px solid transparent;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.component-badge.proprietary,
|
| 246 |
+
.type-badge.proprietary {
|
| 247 |
+
color: var(--avgen-orange);
|
| 248 |
+
background: #fff4e5;
|
| 249 |
+
border-color: #f2d4aa;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.component-badge.open,
|
| 253 |
+
.type-badge.opensource {
|
| 254 |
+
color: var(--avgen-blue);
|
| 255 |
+
background: #edf5ff;
|
| 256 |
+
border-color: #c7dff8;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.component-badge.neutral,
|
| 260 |
+
.type-badge.mixed {
|
| 261 |
+
color: var(--avgen-teal);
|
| 262 |
+
background: #eaf7f5;
|
| 263 |
+
border-color: #bde0dc;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.profile-panel {
|
| 267 |
+
display: grid;
|
| 268 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 269 |
+
gap: 16px;
|
| 270 |
+
border: 1px solid var(--avgen-line);
|
| 271 |
+
background: #fff;
|
| 272 |
+
border-radius: 8px;
|
| 273 |
+
padding: 18px;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.profile-panel h2 {
|
| 277 |
+
margin: 2px 0 8px;
|
| 278 |
+
font-size: 26px;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.profile-total {
|
| 282 |
+
min-width: 140px;
|
| 283 |
+
text-align: right;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.profile-total strong {
|
| 287 |
+
display: block;
|
| 288 |
+
font-size: 42px;
|
| 289 |
+
line-height: 1;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.profile-grid {
|
| 293 |
+
grid-column: 1 / -1;
|
| 294 |
+
display: grid;
|
| 295 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 296 |
+
gap: 10px;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.profile-metric {
|
| 300 |
+
border: 1px solid #edf0f5;
|
| 301 |
+
border-radius: 8px;
|
| 302 |
+
padding: 10px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.profile-metric-head {
|
| 306 |
+
display: flex;
|
| 307 |
+
justify-content: space-between;
|
| 308 |
+
gap: 10px;
|
| 309 |
+
font-variant-numeric: tabular-nums;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.bar-track {
|
| 313 |
+
height: 8px;
|
| 314 |
+
margin: 9px 0 6px;
|
| 315 |
+
border-radius: 999px;
|
| 316 |
+
background: #e7ebf1;
|
| 317 |
+
overflow: hidden;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.bar-fill {
|
| 321 |
+
height: 100%;
|
| 322 |
+
border-radius: inherit;
|
| 323 |
+
background: linear-gradient(90deg, var(--avgen-teal), var(--avgen-green));
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.profile-metric small,
|
| 327 |
+
.methodology p {
|
| 328 |
+
color: var(--avgen-muted);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.method-grid {
|
| 332 |
+
display: grid;
|
| 333 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 334 |
+
gap: 10px;
|
| 335 |
+
margin-bottom: 12px;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.method-card h3 {
|
| 339 |
+
margin: 0 0 4px;
|
| 340 |
+
font-size: 16px;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.method-card strong {
|
| 344 |
+
display: block;
|
| 345 |
+
font-size: 30px;
|
| 346 |
+
color: var(--avgen-blue);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.empty-state {
|
| 350 |
+
border: 1px solid var(--avgen-line);
|
| 351 |
+
background: #fff;
|
| 352 |
+
border-radius: 8px;
|
| 353 |
+
padding: 22px;
|
| 354 |
+
color: var(--avgen-muted);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.overview-image img {
|
| 358 |
+
border: 1px solid var(--avgen-line);
|
| 359 |
+
border-radius: 8px;
|
| 360 |
+
background: #fff;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.submission-copy {
|
| 364 |
+
color: var(--avgen-muted);
|
| 365 |
+
margin: 0 0 14px;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.submission-copy strong {
|
| 369 |
+
color: var(--avgen-ink);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.status-card {
|
| 373 |
+
border: 1px solid var(--avgen-line);
|
| 374 |
+
border-radius: 8px;
|
| 375 |
+
padding: 14px;
|
| 376 |
+
background: #fff;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.status-card strong {
|
| 380 |
+
display: block;
|
| 381 |
+
margin-bottom: 4px;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.status-card p {
|
| 385 |
+
margin: 4px 0 0;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.status-card.success {
|
| 389 |
+
border-color: #b8dbc9;
|
| 390 |
+
background: #f1faf5;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.status-card.error {
|
| 394 |
+
border-color: #efc2bd;
|
| 395 |
+
background: #fff3f1;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@media (max-width: 980px) {
|
| 399 |
+
.app-header {
|
| 400 |
+
grid-template-columns: 1fr;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.header-links {
|
| 404 |
+
justify-content: flex-start;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.summary-grid,
|
| 408 |
+
.profile-grid,
|
| 409 |
+
.method-grid {
|
| 410 |
+
grid-template-columns: 1fr 1fr;
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
@media (max-width: 640px) {
|
| 415 |
+
.summary-grid,
|
| 416 |
+
.profile-grid,
|
| 417 |
+
.method-grid {
|
| 418 |
+
grid-template-columns: 1fr;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.profile-panel {
|
| 422 |
+
grid-template-columns: 1fr;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.profile-total {
|
| 426 |
+
text-align: left;
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
"""
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
HEADER = """
|
| 433 |
+
<div class="app-header">
|
| 434 |
+
<div>
|
| 435 |
+
<h1>AVGen-Bench Leaderboard</h1>
|
| 436 |
+
<p>
|
| 437 |
+
A leaderboard for multi-granular evaluation of Text-to-Audio-Video generation,
|
| 438 |
+
covering visual/audio quality, synchronization, fine-grained controllability,
|
| 439 |
+
physical plausibility, and holistic semantic alignment.
|
| 440 |
+
</p>
|
| 441 |
+
</div>
|
| 442 |
+
<div class="header-links">
|
| 443 |
+
<a href="https://github.com/microsoft/AVGen-Bench" target="_blank" rel="noopener">GitHub</a>
|
| 444 |
+
<a href="https://arxiv.org/abs/2604.08540" target="_blank" rel="noopener">Paper</a>
|
| 445 |
+
<a href="https://huggingface.co/datasets/microsoft/AVGen-Bench" target="_blank" rel="noopener">Dataset</a>
|
| 446 |
+
</div>
|
| 447 |
+
</div>
|
| 448 |
+
"""
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
with gr.Blocks(title="AVGen-Bench Leaderboard") as demo:
|
| 452 |
+
gr.HTML(HEADER)
|
| 453 |
+
|
| 454 |
+
with gr.Tab("Leaderboard"):
|
| 455 |
+
with gr.Row():
|
| 456 |
+
component_type = gr.Dropdown(
|
| 457 |
+
choices=["All", "Proprietary", "Open-source", "Mixed"],
|
| 458 |
+
value="All",
|
| 459 |
+
label="Component type",
|
| 460 |
+
)
|
| 461 |
+
sort_by = gr.Dropdown(
|
| 462 |
+
choices=SORT_CHOICES,
|
| 463 |
+
value="Total",
|
| 464 |
+
label="Sort item",
|
| 465 |
)
|
| 466 |
+
sort_order = gr.Radio(
|
| 467 |
+
choices=["Descending", "Ascending", "Best first"],
|
| 468 |
+
value="Descending",
|
| 469 |
+
label="Sort order",
|
| 470 |
+
)
|
| 471 |
+
query = gr.Textbox(label="Search", placeholder="Model or component")
|
| 472 |
+
|
| 473 |
+
summary = gr.HTML()
|
| 474 |
+
table = gr.HTML()
|
| 475 |
+
|
| 476 |
+
with gr.Tab("Model Profile"):
|
| 477 |
+
model = gr.Dropdown(choices=model_choices(LEADERBOARD), value=model_choices(LEADERBOARD)[0], label="Model")
|
| 478 |
+
profile = gr.HTML()
|
| 479 |
|
| 480 |
+
with gr.Tab("Metric Scheme"):
|
| 481 |
+
gr.HTML(render_methodology())
|
| 482 |
+
if OVERVIEW_IMAGE.exists():
|
| 483 |
+
gr.Image(
|
| 484 |
+
value=str(OVERVIEW_IMAGE),
|
| 485 |
+
label="AVGen-Bench evaluation suite",
|
| 486 |
+
show_label=False,
|
| 487 |
+
interactive=False,
|
| 488 |
+
elem_classes=["overview-image"],
|
| 489 |
)
|
| 490 |
|
| 491 |
+
with gr.Tab("Submission"):
|
| 492 |
+
gr.HTML(
|
| 493 |
+
"""
|
| 494 |
+
<p class="submission-copy">
|
| 495 |
+
Submit raw AVGen-Bench metrics for review. The app recomputes
|
| 496 |
+
<strong>Total</strong> from the raw metrics and sends the entry to a
|
| 497 |
+
pending-review backend. Accepted entries are still merged into the
|
| 498 |
+
official leaderboard manually.
|
| 499 |
+
</p>
|
| 500 |
+
"""
|
| 501 |
+
)
|
| 502 |
+
with gr.Row():
|
| 503 |
+
submit_model = gr.Textbox(label="Model name", placeholder="Your Model")
|
| 504 |
+
submit_component_type = gr.Dropdown(
|
| 505 |
+
choices=["Proprietary", "Open-source", "Mixed"],
|
| 506 |
+
value="Open-source",
|
| 507 |
+
label="Component type",
|
| 508 |
+
)
|
| 509 |
+
submit_components = gr.Textbox(
|
| 510 |
+
label="Components",
|
| 511 |
+
placeholder="VideoModel (Open-source)|AudioModel (Open-source)",
|
| 512 |
+
)
|
| 513 |
+
with gr.Row():
|
| 514 |
+
submit_contact = gr.Textbox(label="Public contact", placeholder="GitHub handle or email")
|
| 515 |
+
submit_model_url = gr.Textbox(label="Model or paper URL", placeholder="https://...")
|
| 516 |
+
submit_results_url = gr.Textbox(label="Evaluation artifact URL", placeholder="https://...")
|
| 517 |
+
submit_notes = gr.Textbox(label="Notes", lines=3, placeholder="Optional evaluation details")
|
| 518 |
+
|
| 519 |
+
with gr.Accordion("Raw metric scores", open=True):
|
| 520 |
+
with gr.Row():
|
| 521 |
+
submit_vis = gr.Number(label="Vis")
|
| 522 |
+
submit_aud = gr.Number(label="Aud (PQ)")
|
| 523 |
+
submit_av = gr.Number(label="AV")
|
| 524 |
+
submit_lip = gr.Number(label="Lip")
|
| 525 |
+
with gr.Row():
|
| 526 |
+
submit_text = gr.Number(label="Text")
|
| 527 |
+
submit_face = gr.Number(label="Face")
|
| 528 |
+
submit_music = gr.Number(label="Music")
|
| 529 |
+
submit_speech = gr.Number(label="Speech")
|
| 530 |
+
with gr.Row():
|
| 531 |
+
submit_lophy = gr.Number(label="Lo-Phy")
|
| 532 |
+
submit_hiphy = gr.Number(label="Hi-Phy")
|
| 533 |
+
submit_holistic = gr.Number(label="Holistic")
|
| 534 |
+
|
| 535 |
+
submit_button = gr.Button("Submit for Review", variant="primary")
|
| 536 |
+
submit_status = gr.HTML()
|
| 537 |
+
submit_payload = gr.Code(label="Submission JSON", language="json")
|
| 538 |
+
gr.File(value=str(SUBMISSION_TEMPLATE), label="CSV template", interactive=False)
|
| 539 |
+
|
| 540 |
+
demo.load(
|
| 541 |
+
fn=update_leaderboard,
|
| 542 |
+
inputs=[component_type, query, sort_by, sort_order],
|
| 543 |
+
outputs=[summary, table],
|
| 544 |
+
)
|
| 545 |
+
demo.load(fn=update_profile, inputs=[model], outputs=[profile])
|
| 546 |
+
|
| 547 |
+
for control in [component_type, query, sort_by, sort_order]:
|
| 548 |
+
control.change(
|
| 549 |
+
fn=update_leaderboard,
|
| 550 |
+
inputs=[component_type, query, sort_by, sort_order],
|
| 551 |
+
outputs=[summary, table],
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
model.change(fn=update_profile, inputs=[model], outputs=[profile])
|
| 555 |
+
submit_button.click(
|
| 556 |
+
fn=submit_score,
|
| 557 |
+
inputs=[
|
| 558 |
+
submit_model,
|
| 559 |
+
submit_components,
|
| 560 |
+
submit_component_type,
|
| 561 |
+
submit_contact,
|
| 562 |
+
submit_model_url,
|
| 563 |
+
submit_results_url,
|
| 564 |
+
submit_notes,
|
| 565 |
+
submit_vis,
|
| 566 |
+
submit_aud,
|
| 567 |
+
submit_av,
|
| 568 |
+
submit_lip,
|
| 569 |
+
submit_text,
|
| 570 |
+
submit_face,
|
| 571 |
+
submit_music,
|
| 572 |
+
submit_speech,
|
| 573 |
+
submit_lophy,
|
| 574 |
+
submit_hiphy,
|
| 575 |
+
submit_holistic,
|
| 576 |
+
],
|
| 577 |
+
outputs=[submit_status, submit_payload],
|
| 578 |
+
api_name="submit_score",
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
if __name__ == "__main__":
|
| 583 |
+
launch_kwargs = {
|
| 584 |
+
"css": CSS,
|
| 585 |
+
"server_name": os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 586 |
+
}
|
| 587 |
+
port = os.environ.get("PORT") or os.environ.get("GRADIO_SERVER_PORT")
|
| 588 |
+
if port:
|
| 589 |
+
launch_kwargs["server_port"] = int(port)
|
| 590 |
+
demo.launch(**launch_kwargs)
|
assets/avbench_outline.png
ADDED
|
Git LFS Details
|
data/leaderboard.csv
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Components,Component Type,Vis,Aud (PQ),AV,Lip,Text,Face,Music,Speech,Lo-Phy,Hi-Phy,Holistic,Total,Source
|
| 2 |
+
Seedance 2.0,Seedance 2.0 (Proprietary),Proprietary,0.945,7.15,0.15,4.14,74.83,60.95,28.12,94.09,3.89,83.16,89.61,72.07,AVGen-Bench README
|
| 3 |
+
Veo 3.1-fast,Veo 3.1-fast (Proprietary),Proprietary,0.960,6.64,0.21,2.39,75.10,52.77,3.13,94.53,3.68,67.43,86.27,67.87,AVGen-Bench README
|
| 4 |
+
Veo 3.1-quality,Veo 3.1-quality (Proprietary),Proprietary,0.954,6.77,0.24,3.59,76.53,52.90,5.00,96.09,3.74,68.53,84.10,66.28,AVGen-Bench README
|
| 5 |
+
Sora-2,Sora-2 (Proprietary),Proprietary,0.848,5.91,0.25,4.50,74.84,51.17,7.81,88.63,4.05,78.95,88.89,64.16,AVGen-Bench README
|
| 6 |
+
Wan2.6,Wan2.6 (Proprietary),Proprietary,0.959,7.15,0.30,4.32,76.95,49.27,1.75,89.33,3.69,66.92,80.98,62.97,AVGen-Bench README
|
| 7 |
+
Seedance-1.5 Pro,Seedance-1.5 Pro (Proprietary),Proprietary,0.970,7.48,0.26,3.43,38.28,54.42,1.88,93.45,3.72,66.88,77.38,62.55,AVGen-Bench README
|
| 8 |
+
Kling-V2.6,Kling-V2.6 (Proprietary),Proprietary,0.906,6.93,0.21,2.30,14.52,57.33,5.00,89.62,3.84,63.92,76.74,61.82,AVGen-Bench README
|
| 9 |
+
LTX-2.3,LTX-2.3 (Open-source),Open-source,0.858,7.11,0.36,2.00,54.17,45.06,1.38,86.66,3.99,64.31,65.22,59.97,AVGen-Bench README
|
| 10 |
+
NanoBanana2 + MOVA,NanoBanana2 (Proprietary)|MOVA (Open-source),Mixed,0.890,6.71,0.44,2.70,68.26,41.33,0.59,82.45,3.91,60.95,72.48,58.10,AVGen-Bench README
|
| 11 |
+
LTX-2,LTX-2 (Open-source),Open-source,0.828,6.84,0.23,4.76,24.76,48.53,5.75,87.07,4.05,60.20,66.59,56.62,AVGen-Bench README
|
| 12 |
+
Emu3.5 + MOVA,Emu3.5 (Open-source)|MOVA (Open-source),Open-source,0.911,6.80,0.38,4.83,64.72,48.44,0.62,81.74,3.89,55.85,66.55,56.12,AVGen-Bench README
|
| 13 |
+
Wan2.2 + HunyuanVideo-Foley,Wan2.2 (Open-source)|HunyuanVideo-Foley (Open-source),Open-source,0.936,6.60,0.23,5.38,48.46,36.23,3.44,53.40,3.90,54.11,60.63,53.29,AVGen-Bench README
|
| 14 |
+
Ovi,Ovi (Open-source),Open-source,0.839,6.31,0.37,5.40,41.36,49.05,11.25,76.49,3.93,52.92,57.45,52.02,AVGen-Bench README
|
data/submission_template.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Components,Component Type,Vis,Aud (PQ),AV,Lip,Text,Face,Music,Speech,Lo-Phy,Hi-Phy,Holistic,Total,Source
|
| 2 |
+
Your Model,VideoModel (Open-source)|AudioModel (Open-source),Open-source,,,,,,,,,,,,,Your eval run or paper link
|
leaderboard.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import html
|
| 4 |
+
import math
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Iterable
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
DATA_PATH = Path(__file__).parent / "data" / "leaderboard.csv"
|
| 13 |
+
|
| 14 |
+
METRIC_COLUMNS = [
|
| 15 |
+
"Vis",
|
| 16 |
+
"Aud (PQ)",
|
| 17 |
+
"AV",
|
| 18 |
+
"Lip",
|
| 19 |
+
"Text",
|
| 20 |
+
"Face",
|
| 21 |
+
"Music",
|
| 22 |
+
"Speech",
|
| 23 |
+
"Lo-Phy",
|
| 24 |
+
"Hi-Phy",
|
| 25 |
+
"Holistic",
|
| 26 |
+
"Total",
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
SORT_COLUMNS = ["Rank", "Model", "Components", "Component Type", *METRIC_COLUMNS]
|
| 30 |
+
SORT_CHOICES = [
|
| 31 |
+
("Rank", "Rank"),
|
| 32 |
+
("Model", "Model"),
|
| 33 |
+
("Components", "Components"),
|
| 34 |
+
("Type", "Component Type"),
|
| 35 |
+
*[(metric, metric) for metric in METRIC_COLUMNS],
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
LOWER_IS_BETTER = {"AV", "Lip"}
|
| 39 |
+
|
| 40 |
+
NUMERIC_COLUMNS = METRIC_COLUMNS
|
| 41 |
+
|
| 42 |
+
FORMATTERS = {
|
| 43 |
+
"Vis": "{:.3f}",
|
| 44 |
+
"Aud (PQ)": "{:.2f}",
|
| 45 |
+
"AV": "{:.2f}",
|
| 46 |
+
"Lip": "{:.2f}",
|
| 47 |
+
"Text": "{:.2f}",
|
| 48 |
+
"Face": "{:.2f}",
|
| 49 |
+
"Music": "{:.2f}",
|
| 50 |
+
"Speech": "{:.2f}",
|
| 51 |
+
"Lo-Phy": "{:.2f}",
|
| 52 |
+
"Hi-Phy": "{:.2f}",
|
| 53 |
+
"Holistic": "{:.2f}",
|
| 54 |
+
"Total": "{:.2f}",
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
GROUP_WEIGHTS = {
|
| 58 |
+
"Basic Uni-modal": 0.2,
|
| 59 |
+
"Basic Cross-modal": 0.2,
|
| 60 |
+
"Fine-grained": 0.6,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
GROUP_DIMENSIONS = {
|
| 64 |
+
"Basic Uni-modal": ["Vis", "Aud (PQ)"],
|
| 65 |
+
"Basic Cross-modal": ["AV", "Lip"],
|
| 66 |
+
"Fine-grained": ["Text", "Face", "Music", "Speech", "Lo-Phy", "Hi-Phy", "Holistic"],
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass(frozen=True)
|
| 71 |
+
class Standing:
|
| 72 |
+
best: float | None
|
| 73 |
+
second: float | None
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def load_leaderboard(path: Path = DATA_PATH) -> pd.DataFrame:
|
| 77 |
+
df = pd.read_csv(path)
|
| 78 |
+
for column in NUMERIC_COLUMNS:
|
| 79 |
+
df[column] = pd.to_numeric(df[column], errors="coerce")
|
| 80 |
+
df = df.sort_values("Total", ascending=False, na_position="last").reset_index(drop=True)
|
| 81 |
+
df.insert(0, "Rank", range(1, len(df) + 1))
|
| 82 |
+
return df
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def filter_leaderboard(
|
| 86 |
+
df: pd.DataFrame,
|
| 87 |
+
component_type: str = "All",
|
| 88 |
+
query: str = "",
|
| 89 |
+
sort_by: str = "Total",
|
| 90 |
+
sort_order: str = "Descending",
|
| 91 |
+
) -> pd.DataFrame:
|
| 92 |
+
view = df.copy()
|
| 93 |
+
|
| 94 |
+
if component_type != "All":
|
| 95 |
+
view = view[view["Component Type"] == component_type]
|
| 96 |
+
|
| 97 |
+
query = query.strip().lower()
|
| 98 |
+
if query:
|
| 99 |
+
mask = (
|
| 100 |
+
view["Model"].str.lower().str.contains(query, regex=False)
|
| 101 |
+
| view["Components"].str.lower().str.contains(query, regex=False)
|
| 102 |
+
)
|
| 103 |
+
view = view[mask]
|
| 104 |
+
|
| 105 |
+
sort_by = _normalize_sort_column(sort_by)
|
| 106 |
+
ascending = _is_ascending_sort(sort_by, sort_order)
|
| 107 |
+
|
| 108 |
+
if sort_by in view.columns:
|
| 109 |
+
sort_kwargs = {
|
| 110 |
+
"ascending": ascending,
|
| 111 |
+
"na_position": "last",
|
| 112 |
+
"kind": "mergesort",
|
| 113 |
+
}
|
| 114 |
+
if sort_by in {"Model", "Components", "Component Type"}:
|
| 115 |
+
sort_kwargs["key"] = lambda column: column.astype(str).str.casefold()
|
| 116 |
+
view = view.sort_values(sort_by, **sort_kwargs)
|
| 117 |
+
|
| 118 |
+
return view.reset_index(drop=True)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def metric_standings(df: pd.DataFrame) -> dict[str, Standing]:
|
| 122 |
+
standings: dict[str, Standing] = {}
|
| 123 |
+
for metric in METRIC_COLUMNS:
|
| 124 |
+
values = sorted(
|
| 125 |
+
{float(v) for v in df[metric].dropna()},
|
| 126 |
+
reverse=metric not in LOWER_IS_BETTER,
|
| 127 |
+
)
|
| 128 |
+
standings[metric] = Standing(
|
| 129 |
+
best=values[0] if values else None,
|
| 130 |
+
second=values[1] if len(values) > 1 else None,
|
| 131 |
+
)
|
| 132 |
+
return standings
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def normalized_score(metric: str, value: float) -> float:
|
| 136 |
+
if metric == "Vis":
|
| 137 |
+
return _clamp(value * 100.0, 0.0, 100.0)
|
| 138 |
+
if metric == "Aud (PQ)":
|
| 139 |
+
return _clamp(value * 10.0, 0.0, 100.0)
|
| 140 |
+
if metric == "AV":
|
| 141 |
+
return _clamp(100.0 * (1.0 - value / 0.5), 0.0, 100.0)
|
| 142 |
+
if metric == "Lip":
|
| 143 |
+
return _clamp(100.0 * (1.0 - value / 8.0), 0.0, 100.0)
|
| 144 |
+
if metric == "Lo-Phy":
|
| 145 |
+
return _clamp(value * 20.0, 0.0, 100.0)
|
| 146 |
+
return _clamp(value, 0.0, 100.0)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def compute_total_from_metrics(row: pd.Series) -> float:
|
| 150 |
+
group_scores: list[float] = []
|
| 151 |
+
group_weights: list[float] = []
|
| 152 |
+
|
| 153 |
+
for group_name, metrics in GROUP_DIMENSIONS.items():
|
| 154 |
+
values = []
|
| 155 |
+
for metric in metrics:
|
| 156 |
+
value = row.get(metric)
|
| 157 |
+
if pd.isna(value):
|
| 158 |
+
continue
|
| 159 |
+
values.append(normalized_score(metric, float(value)))
|
| 160 |
+
if values:
|
| 161 |
+
group_scores.append(sum(values) / len(values))
|
| 162 |
+
group_weights.append(GROUP_WEIGHTS[group_name])
|
| 163 |
+
|
| 164 |
+
if not group_scores:
|
| 165 |
+
return float("nan")
|
| 166 |
+
|
| 167 |
+
weighted = sum(score * weight for score, weight in zip(group_scores, group_weights))
|
| 168 |
+
return weighted / sum(group_weights)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def render_summary(df: pd.DataFrame, view: pd.DataFrame) -> str:
|
| 172 |
+
top = df.sort_values("Total", ascending=False).iloc[0]
|
| 173 |
+
open_source = df[df["Component Type"] == "Open-source"].sort_values("Total", ascending=False)
|
| 174 |
+
best_open = open_source.iloc[0] if len(open_source) else None
|
| 175 |
+
best_av = df.sort_values("AV", ascending=True).iloc[0]
|
| 176 |
+
best_speech = df.sort_values("Speech", ascending=False).iloc[0]
|
| 177 |
+
|
| 178 |
+
cards = [
|
| 179 |
+
_summary_card("Models", f"{len(view)} / {len(df)}", "shown in current view"),
|
| 180 |
+
_summary_card("Top Total", _score(top["Total"]), str(top["Model"])),
|
| 181 |
+
_summary_card(
|
| 182 |
+
"Best Open-source",
|
| 183 |
+
_score(best_open["Total"]) if best_open is not None else "NA",
|
| 184 |
+
str(best_open["Model"]) if best_open is not None else "No entry",
|
| 185 |
+
),
|
| 186 |
+
_summary_card("Lowest AV Offset", _score(best_av["AV"]), str(best_av["Model"])),
|
| 187 |
+
_summary_card("Highest Speech", _score(best_speech["Speech"]), str(best_speech["Model"])),
|
| 188 |
+
]
|
| 189 |
+
return '<div class="summary-grid">' + "".join(cards) + "</div>"
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def render_table(
|
| 193 |
+
df: pd.DataFrame,
|
| 194 |
+
standings: dict[str, Standing],
|
| 195 |
+
sort_by: str = "Total",
|
| 196 |
+
sort_order: str = "Descending",
|
| 197 |
+
) -> str:
|
| 198 |
+
if df.empty:
|
| 199 |
+
return '<div class="empty-state">No matching models.</div>'
|
| 200 |
+
|
| 201 |
+
sort_by = _normalize_sort_column(sort_by)
|
| 202 |
+
headers = [
|
| 203 |
+
("Rank", "Rank"),
|
| 204 |
+
("Model", "Model"),
|
| 205 |
+
("Components", "Components"),
|
| 206 |
+
("Type", "Component Type"),
|
| 207 |
+
*[(metric, metric) for metric in METRIC_COLUMNS],
|
| 208 |
+
]
|
| 209 |
+
header_html = "".join(_header_cell(label, column, sort_by, sort_order) for label, column in headers)
|
| 210 |
+
|
| 211 |
+
rows = []
|
| 212 |
+
for _, row in df.iterrows():
|
| 213 |
+
cells = [
|
| 214 |
+
f'<td class="rank-cell">#{int(row["Rank"])}</td>',
|
| 215 |
+
f'<td class="model-cell">{html.escape(str(row["Model"]))}</td>',
|
| 216 |
+
f'<td class="components-cell">{render_component_badges(str(row["Components"]))}</td>',
|
| 217 |
+
f'<td>{_type_badge(str(row["Component Type"]))}</td>',
|
| 218 |
+
]
|
| 219 |
+
for metric in METRIC_COLUMNS:
|
| 220 |
+
cells.append(_metric_cell(metric, row[metric], standings[metric]))
|
| 221 |
+
rows.append("<tr>" + "".join(cells) + "</tr>")
|
| 222 |
+
|
| 223 |
+
return (
|
| 224 |
+
'<div class="table-shell"><table class="leaderboard-table">'
|
| 225 |
+
f"<thead><tr>{header_html}</tr></thead><tbody>{''.join(rows)}</tbody>"
|
| 226 |
+
"</table></div>"
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def render_component_badges(components: str) -> str:
|
| 231 |
+
badges = []
|
| 232 |
+
for component in _split_components(components):
|
| 233 |
+
lowered = component.lower()
|
| 234 |
+
kind = "proprietary" if "proprietary" in lowered else "open" if "open-source" in lowered else "neutral"
|
| 235 |
+
label = component.replace(" (Proprietary)", "").replace(" (Open-source)", "")
|
| 236 |
+
badges.append(f'<span class="component-badge {kind}">{html.escape(label)}</span>')
|
| 237 |
+
return "".join(badges)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def render_profile(df: pd.DataFrame, model: str) -> str:
|
| 241 |
+
if df.empty:
|
| 242 |
+
return ""
|
| 243 |
+
if not model or model not in set(df["Model"]):
|
| 244 |
+
model = str(df.sort_values("Total", ascending=False).iloc[0]["Model"])
|
| 245 |
+
|
| 246 |
+
row = df[df["Model"] == model].iloc[0]
|
| 247 |
+
metric_blocks = []
|
| 248 |
+
for metric in METRIC_COLUMNS[:-1]:
|
| 249 |
+
value = float(row[metric])
|
| 250 |
+
normalized = normalized_score(metric, value)
|
| 251 |
+
direction = "lower is better" if metric in LOWER_IS_BETTER else "higher is better"
|
| 252 |
+
metric_blocks.append(
|
| 253 |
+
f"""
|
| 254 |
+
<div class="profile-metric">
|
| 255 |
+
<div class="profile-metric-head">
|
| 256 |
+
<span>{html.escape(metric)}</span>
|
| 257 |
+
<strong>{_format_metric(metric, value)}</strong>
|
| 258 |
+
</div>
|
| 259 |
+
<div class="bar-track"><div class="bar-fill" style="width: {normalized:.1f}%"></div></div>
|
| 260 |
+
<small>{normalized:.1f} normalized, {direction}</small>
|
| 261 |
+
</div>
|
| 262 |
+
"""
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
return f"""
|
| 266 |
+
<div class="profile-panel">
|
| 267 |
+
<div>
|
| 268 |
+
<p class="eyebrow">Model profile</p>
|
| 269 |
+
<h2>{html.escape(str(row["Model"]))}</h2>
|
| 270 |
+
<div class="profile-components">{render_component_badges(str(row["Components"]))}</div>
|
| 271 |
+
</div>
|
| 272 |
+
<div class="profile-total">
|
| 273 |
+
<span>Total</span>
|
| 274 |
+
<strong>{_score(row["Total"])}</strong>
|
| 275 |
+
</div>
|
| 276 |
+
<div class="profile-grid">{''.join(metric_blocks)}</div>
|
| 277 |
+
</div>
|
| 278 |
+
"""
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def render_methodology() -> str:
|
| 282 |
+
groups = "".join(
|
| 283 |
+
f"""
|
| 284 |
+
<div class="method-card">
|
| 285 |
+
<h3>{html.escape(name)}</h3>
|
| 286 |
+
<strong>{weight:.1f}</strong>
|
| 287 |
+
<p>{html.escape(', '.join(metrics))}</p>
|
| 288 |
+
</div>
|
| 289 |
+
"""
|
| 290 |
+
for name, weight in GROUP_WEIGHTS.items()
|
| 291 |
+
for metrics in [GROUP_DIMENSIONS[name]]
|
| 292 |
+
)
|
| 293 |
+
return f"""
|
| 294 |
+
<div class="methodology">
|
| 295 |
+
<div class="method-grid">{groups}</div>
|
| 296 |
+
<p>
|
| 297 |
+
Total uses AVGen-Bench Scheme 2: group-weighted normalized metrics with
|
| 298 |
+
Vis x 100, Aud(PQ) x 10, Lo-Phy x 20, AV = 100 * max(0, 1 - AV / 0.5),
|
| 299 |
+
Lip = 100 * max(0, 1 - Lip / 8), and the remaining metrics already on
|
| 300 |
+
a 0-100 scale.
|
| 301 |
+
</p>
|
| 302 |
+
</div>
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def model_choices(df: pd.DataFrame) -> list[str]:
|
| 307 |
+
return list(df.sort_values("Total", ascending=False)["Model"])
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def _metric_cell(metric: str, value: float, standing: Standing) -> str:
|
| 311 |
+
if pd.isna(value):
|
| 312 |
+
return '<td class="metric-cell muted">NA</td>'
|
| 313 |
+
numeric = float(value)
|
| 314 |
+
classes = ["metric-cell"]
|
| 315 |
+
if _close(numeric, standing.best):
|
| 316 |
+
classes.append("best")
|
| 317 |
+
elif _close(numeric, standing.second):
|
| 318 |
+
classes.append("second")
|
| 319 |
+
return f'<td class="{" ".join(classes)}">{_format_metric(metric, numeric)}</td>'
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def _type_badge(component_type: str) -> str:
|
| 323 |
+
kind = component_type.lower().replace("-", "").replace(" ", "")
|
| 324 |
+
return f'<span class="type-badge {html.escape(kind)}">{html.escape(component_type)}</span>'
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def _summary_card(label: str, value: str, detail: str) -> str:
|
| 328 |
+
return f"""
|
| 329 |
+
<div class="summary-card">
|
| 330 |
+
<span>{html.escape(label)}</span>
|
| 331 |
+
<strong>{html.escape(value)}</strong>
|
| 332 |
+
<small>{html.escape(detail)}</small>
|
| 333 |
+
</div>
|
| 334 |
+
"""
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _header_cell(label: str, column: str, sort_by: str, sort_order: str) -> str:
|
| 338 |
+
if column != sort_by:
|
| 339 |
+
return f"<th>{html.escape(label)}</th>"
|
| 340 |
+
|
| 341 |
+
direction = "ascending" if _is_ascending_sort(sort_by, sort_order) else "descending"
|
| 342 |
+
indicator = "↑" if direction == "ascending" else "↓"
|
| 343 |
+
return (
|
| 344 |
+
f'<th class="sorted" aria-sort="{direction}">'
|
| 345 |
+
f"{html.escape(label)}"
|
| 346 |
+
f'<span class="sort-indicator" aria-hidden="true">{indicator}</span>'
|
| 347 |
+
"</th>"
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def _normalize_sort_column(sort_by: str) -> str:
|
| 352 |
+
if sort_by == "Type":
|
| 353 |
+
return "Component Type"
|
| 354 |
+
return sort_by if sort_by in SORT_COLUMNS else "Total"
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def _is_ascending_sort(sort_by: str, sort_order: str) -> bool:
|
| 358 |
+
if sort_order == "Best first":
|
| 359 |
+
return sort_by in LOWER_IS_BETTER
|
| 360 |
+
return sort_order == "Ascending"
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def _split_components(value: str) -> Iterable[str]:
|
| 364 |
+
return [part.strip() for part in value.split("|") if part.strip()]
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def _format_metric(metric: str, value: float) -> str:
|
| 368 |
+
return FORMATTERS[metric].format(float(value))
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def _score(value: float) -> str:
|
| 372 |
+
if pd.isna(value):
|
| 373 |
+
return "NA"
|
| 374 |
+
return f"{float(value):.2f}"
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def _close(left: float, right: float | None) -> bool:
|
| 378 |
+
if right is None:
|
| 379 |
+
return False
|
| 380 |
+
return math.isclose(float(left), float(right), rel_tol=0.0, abs_tol=1e-9)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def _clamp(value: float, low: float, high: float) -> float:
|
| 384 |
+
return max(low, min(high, value))
|
pytest.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
pythonpath = .
|
requirements-dev.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pytest
|
requirements.txt
CHANGED
|
@@ -1,16 +1,2 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
datasets
|
| 4 |
-
gradio
|
| 5 |
-
gradio[oauth]
|
| 6 |
-
gradio_leaderboard==0.0.13
|
| 7 |
-
gradio_client
|
| 8 |
-
huggingface-hub>=0.18.0
|
| 9 |
-
matplotlib
|
| 10 |
-
numpy
|
| 11 |
-
pandas
|
| 12 |
-
python-dateutil
|
| 13 |
-
tqdm
|
| 14 |
-
transformers
|
| 15 |
-
tokenizers>=0.15.0
|
| 16 |
-
sentencepiece
|
|
|
|
| 1 |
+
gradio>=6.0,<7
|
| 2 |
+
pandas>=2.2,<4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
submission.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import html
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import urllib.error
|
| 9 |
+
import urllib.request
|
| 10 |
+
from datetime import datetime, timezone
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
import pandas as pd
|
| 15 |
+
|
| 16 |
+
from leaderboard import compute_total_from_metrics
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
SUBMISSION_METRICS = [
|
| 20 |
+
"Vis",
|
| 21 |
+
"Aud (PQ)",
|
| 22 |
+
"AV",
|
| 23 |
+
"Lip",
|
| 24 |
+
"Text",
|
| 25 |
+
"Face",
|
| 26 |
+
"Music",
|
| 27 |
+
"Speech",
|
| 28 |
+
"Lo-Phy",
|
| 29 |
+
"Hi-Phy",
|
| 30 |
+
"Holistic",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
COMPONENT_TYPES = ["Proprietary", "Open-source", "Mixed"]
|
| 34 |
+
|
| 35 |
+
METRIC_RANGES = {
|
| 36 |
+
"Vis": (0.0, 1.0),
|
| 37 |
+
"Aud (PQ)": (0.0, 10.0),
|
| 38 |
+
"AV": (0.0, 10.0),
|
| 39 |
+
"Lip": (0.0, 100.0),
|
| 40 |
+
"Text": (0.0, 100.0),
|
| 41 |
+
"Face": (0.0, 100.0),
|
| 42 |
+
"Music": (0.0, 100.0),
|
| 43 |
+
"Speech": (0.0, 100.0),
|
| 44 |
+
"Lo-Phy": (0.0, 5.0),
|
| 45 |
+
"Hi-Phy": (0.0, 100.0),
|
| 46 |
+
"Holistic": (0.0, 100.0),
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class SubmissionError(ValueError):
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def submit_score(
|
| 55 |
+
model: str,
|
| 56 |
+
components: str,
|
| 57 |
+
component_type: str,
|
| 58 |
+
contact: str,
|
| 59 |
+
model_url: str,
|
| 60 |
+
results_url: str,
|
| 61 |
+
notes: str,
|
| 62 |
+
vis: float,
|
| 63 |
+
aud_pq: float,
|
| 64 |
+
av: float,
|
| 65 |
+
lip: float,
|
| 66 |
+
text: float,
|
| 67 |
+
face: float,
|
| 68 |
+
music: float,
|
| 69 |
+
speech: float,
|
| 70 |
+
lo_phy: float,
|
| 71 |
+
hi_phy: float,
|
| 72 |
+
holistic: float,
|
| 73 |
+
) -> tuple[str, str]:
|
| 74 |
+
try:
|
| 75 |
+
submission = build_submission(
|
| 76 |
+
model=model,
|
| 77 |
+
components=components,
|
| 78 |
+
component_type=component_type,
|
| 79 |
+
contact=contact,
|
| 80 |
+
model_url=model_url,
|
| 81 |
+
results_url=results_url,
|
| 82 |
+
notes=notes,
|
| 83 |
+
metrics={
|
| 84 |
+
"Vis": vis,
|
| 85 |
+
"Aud (PQ)": aud_pq,
|
| 86 |
+
"AV": av,
|
| 87 |
+
"Lip": lip,
|
| 88 |
+
"Text": text,
|
| 89 |
+
"Face": face,
|
| 90 |
+
"Music": music,
|
| 91 |
+
"Speech": speech,
|
| 92 |
+
"Lo-Phy": lo_phy,
|
| 93 |
+
"Hi-Phy": hi_phy,
|
| 94 |
+
"Holistic": holistic,
|
| 95 |
+
},
|
| 96 |
+
)
|
| 97 |
+
destination = persist_submission(submission)
|
| 98 |
+
status_html = render_submission_status(submission, destination)
|
| 99 |
+
return status_html, submission_to_json(submission)
|
| 100 |
+
except SubmissionError as exc:
|
| 101 |
+
return render_error_status(str(exc)), ""
|
| 102 |
+
except Exception as exc:
|
| 103 |
+
return render_error_status(f"Submission failed: {exc}"), ""
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def build_submission(
|
| 107 |
+
*,
|
| 108 |
+
model: str,
|
| 109 |
+
components: str,
|
| 110 |
+
component_type: str,
|
| 111 |
+
contact: str,
|
| 112 |
+
model_url: str,
|
| 113 |
+
results_url: str,
|
| 114 |
+
notes: str,
|
| 115 |
+
metrics: dict[str, Any],
|
| 116 |
+
) -> dict[str, Any]:
|
| 117 |
+
model = _clean_required(model, "Model name", min_len=2, max_len=120)
|
| 118 |
+
components = _clean_required(components, "Components", min_len=2, max_len=240)
|
| 119 |
+
contact = _clean_required(contact, "Public contact", min_len=2, max_len=160)
|
| 120 |
+
model_url = _clean_required(model_url, "Model or paper URL", min_len=8, max_len=500)
|
| 121 |
+
results_url = _clean_required(results_url, "Evaluation artifact URL", min_len=8, max_len=500)
|
| 122 |
+
notes = (notes or "").strip()
|
| 123 |
+
if len(notes) > 2000:
|
| 124 |
+
raise SubmissionError("Notes must be 2000 characters or fewer.")
|
| 125 |
+
if component_type not in COMPONENT_TYPES:
|
| 126 |
+
raise SubmissionError("Component type must be Proprietary, Open-source, or Mixed.")
|
| 127 |
+
_validate_url(model_url, "Model or paper URL")
|
| 128 |
+
_validate_url(results_url, "Evaluation artifact URL")
|
| 129 |
+
|
| 130 |
+
metric_values = {metric: _validate_metric(metric, metrics.get(metric)) for metric in SUBMISSION_METRICS}
|
| 131 |
+
total = compute_total_from_metrics(pd.Series(metric_values))
|
| 132 |
+
if pd.isna(total):
|
| 133 |
+
raise SubmissionError("Could not compute Total from the submitted metrics.")
|
| 134 |
+
|
| 135 |
+
return {
|
| 136 |
+
"schema_version": 1,
|
| 137 |
+
"status": "pending_review",
|
| 138 |
+
"submitted_at_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
| 139 |
+
"model": model,
|
| 140 |
+
"components": components,
|
| 141 |
+
"component_type": component_type,
|
| 142 |
+
"contact": contact,
|
| 143 |
+
"model_url": model_url,
|
| 144 |
+
"evaluation_artifact_url": results_url,
|
| 145 |
+
"notes": notes,
|
| 146 |
+
"metrics": metric_values,
|
| 147 |
+
"computed_total": round(float(total), 4),
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def persist_submission(submission: dict[str, Any]) -> dict[str, str]:
|
| 152 |
+
backend = os.environ.get("SUBMISSION_BACKEND", "").strip().lower()
|
| 153 |
+
token = os.environ.get("GITHUB_TOKEN", "").strip()
|
| 154 |
+
repo = os.environ.get("GITHUB_REPO", "").strip()
|
| 155 |
+
|
| 156 |
+
if not backend:
|
| 157 |
+
backend = "github_issue" if token and repo else "local_file"
|
| 158 |
+
|
| 159 |
+
if backend == "disabled":
|
| 160 |
+
raise SubmissionError("Public submission is currently disabled.")
|
| 161 |
+
if backend == "github_issue":
|
| 162 |
+
if not token or not repo:
|
| 163 |
+
raise SubmissionError("GITHUB_TOKEN and GITHUB_REPO are required for github_issue backend.")
|
| 164 |
+
issue_url = create_github_issue(submission, repo, token)
|
| 165 |
+
return {"backend": "github_issue", "url": issue_url}
|
| 166 |
+
if backend == "local_file":
|
| 167 |
+
path = save_submission_file(submission)
|
| 168 |
+
return {"backend": "local_file", "path": str(path)}
|
| 169 |
+
|
| 170 |
+
raise SubmissionError(f"Unknown SUBMISSION_BACKEND: {backend}")
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def create_github_issue(submission: dict[str, Any], repo: str, token: str) -> str:
|
| 174 |
+
payload = {
|
| 175 |
+
"title": f"[Leaderboard Submission] {submission['model']}",
|
| 176 |
+
"body": github_issue_body(submission),
|
| 177 |
+
"labels": ["leaderboard-submission", "needs-review"],
|
| 178 |
+
}
|
| 179 |
+
request = urllib.request.Request(
|
| 180 |
+
url=f"https://api.github.com/repos/{repo}/issues",
|
| 181 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 182 |
+
headers={
|
| 183 |
+
"Accept": "application/vnd.github+json",
|
| 184 |
+
"Authorization": f"Bearer {token}",
|
| 185 |
+
"Content-Type": "application/json",
|
| 186 |
+
"User-Agent": "AVGen-Bench-Leaderboard",
|
| 187 |
+
"X-GitHub-Api-Version": "2022-11-28",
|
| 188 |
+
},
|
| 189 |
+
method="POST",
|
| 190 |
+
)
|
| 191 |
+
try:
|
| 192 |
+
with urllib.request.urlopen(request, timeout=20) as response:
|
| 193 |
+
data = json.loads(response.read().decode("utf-8"))
|
| 194 |
+
except urllib.error.HTTPError as exc:
|
| 195 |
+
detail = exc.read().decode("utf-8", errors="replace")
|
| 196 |
+
raise SubmissionError(f"GitHub issue creation failed ({exc.code}): {detail}") from exc
|
| 197 |
+
except urllib.error.URLError as exc:
|
| 198 |
+
raise SubmissionError(f"Could not reach GitHub API: {exc.reason}") from exc
|
| 199 |
+
|
| 200 |
+
issue_url = data.get("html_url")
|
| 201 |
+
if not issue_url:
|
| 202 |
+
raise SubmissionError("GitHub API response did not include an issue URL.")
|
| 203 |
+
return str(issue_url)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def save_submission_file(submission: dict[str, Any]) -> Path:
|
| 207 |
+
root = Path(os.environ.get("PENDING_SUBMISSION_DIR", "pending_submissions"))
|
| 208 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 209 |
+
filename = f"{_slugify(submission['model'])}-{_compact_timestamp(submission['submitted_at_utc'])}.json"
|
| 210 |
+
path = root / filename
|
| 211 |
+
path.write_text(submission_to_json(submission) + "\n", encoding="utf-8")
|
| 212 |
+
return path
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def github_issue_body(submission: dict[str, Any]) -> str:
|
| 216 |
+
metrics = submission["metrics"]
|
| 217 |
+
metric_rows = "\n".join(f"| {metric} | {metrics[metric]} |" for metric in SUBMISSION_METRICS)
|
| 218 |
+
return f"""## Submission
|
| 219 |
+
|
| 220 |
+
| Field | Value |
|
| 221 |
+
|---|---|
|
| 222 |
+
| Model | {submission['model']} |
|
| 223 |
+
| Components | {submission['components']} |
|
| 224 |
+
| Component Type | {submission['component_type']} |
|
| 225 |
+
| Computed Total | {submission['computed_total']:.2f} |
|
| 226 |
+
| Contact | {submission['contact']} |
|
| 227 |
+
| Model or Paper URL | {submission['model_url']} |
|
| 228 |
+
| Evaluation Artifact URL | {submission['evaluation_artifact_url']} |
|
| 229 |
+
| Submitted At UTC | {submission['submitted_at_utc']} |
|
| 230 |
+
|
| 231 |
+
## Raw Metrics
|
| 232 |
+
|
| 233 |
+
| Metric | Value |
|
| 234 |
+
|---|---:|
|
| 235 |
+
{metric_rows}
|
| 236 |
+
|
| 237 |
+
## Notes
|
| 238 |
+
|
| 239 |
+
{submission['notes'] or 'None'}
|
| 240 |
+
|
| 241 |
+
## Machine-Readable Payload
|
| 242 |
+
|
| 243 |
+
```json
|
| 244 |
+
{submission_to_json(submission)}
|
| 245 |
+
```
|
| 246 |
+
"""
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def submission_to_json(submission: dict[str, Any]) -> str:
|
| 250 |
+
return json.dumps(submission, ensure_ascii=False, indent=2, sort_keys=True)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def render_submission_status(submission: dict[str, Any], destination: dict[str, str]) -> str:
|
| 254 |
+
if destination["backend"] == "github_issue":
|
| 255 |
+
detail = f'<a href="{html.escape(destination["url"])}" target="_blank" rel="noopener">GitHub issue</a>'
|
| 256 |
+
else:
|
| 257 |
+
detail = html.escape(destination["path"])
|
| 258 |
+
return f"""
|
| 259 |
+
<div class="status-card success">
|
| 260 |
+
<strong>Submission received for review.</strong>
|
| 261 |
+
<p>Total was recomputed from raw metrics: <b>{submission['computed_total']:.2f}</b>.</p>
|
| 262 |
+
<p>Destination: {detail}</p>
|
| 263 |
+
</div>
|
| 264 |
+
"""
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def render_error_status(message: str) -> str:
|
| 268 |
+
return f"""
|
| 269 |
+
<div class="status-card error">
|
| 270 |
+
<strong>Submission not accepted.</strong>
|
| 271 |
+
<p>{html.escape(message)}</p>
|
| 272 |
+
</div>
|
| 273 |
+
"""
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _clean_required(value: str, label: str, *, min_len: int, max_len: int) -> str:
|
| 277 |
+
value = (value or "").strip()
|
| 278 |
+
if len(value) < min_len:
|
| 279 |
+
raise SubmissionError(f"{label} is required.")
|
| 280 |
+
if len(value) > max_len:
|
| 281 |
+
raise SubmissionError(f"{label} must be {max_len} characters or fewer.")
|
| 282 |
+
return value
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def _validate_url(value: str, label: str) -> None:
|
| 286 |
+
if not re.match(r"^https?://", value):
|
| 287 |
+
raise SubmissionError(f"{label} must start with http:// or https://.")
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def _validate_metric(metric: str, value: Any) -> float:
|
| 291 |
+
if value is None or value == "":
|
| 292 |
+
raise SubmissionError(f"{metric} is required.")
|
| 293 |
+
try:
|
| 294 |
+
numeric = float(value)
|
| 295 |
+
except (TypeError, ValueError) as exc:
|
| 296 |
+
raise SubmissionError(f"{metric} must be numeric.") from exc
|
| 297 |
+
if not math.isfinite(numeric):
|
| 298 |
+
raise SubmissionError(f"{metric} must be finite.")
|
| 299 |
+
low, high = METRIC_RANGES[metric]
|
| 300 |
+
if numeric < low or numeric > high:
|
| 301 |
+
raise SubmissionError(f"{metric} must be between {low:g} and {high:g}.")
|
| 302 |
+
return round(numeric, 6)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _slugify(value: str) -> str:
|
| 306 |
+
slug = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-")
|
| 307 |
+
return slug or "submission"
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def _compact_timestamp(value: str) -> str:
|
| 311 |
+
return re.sub(r"[^0-9]", "", value)[:14]
|
tests/__pycache__/test_leaderboard.cpython-312-pytest-9.1.1.pyc
ADDED
|
Binary file (15.9 kB). View file
|
|
|
tests/__pycache__/test_submission.cpython-312-pytest-9.1.1.pyc
ADDED
|
Binary file (5.88 kB). View file
|
|
|
tests/test_leaderboard.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
from leaderboard import (
|
| 4 |
+
METRIC_COLUMNS,
|
| 5 |
+
filter_leaderboard,
|
| 6 |
+
load_leaderboard,
|
| 7 |
+
metric_standings,
|
| 8 |
+
render_component_badges,
|
| 9 |
+
render_table,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_loads_current_leaderboard_sorted_by_total():
|
| 14 |
+
df = load_leaderboard()
|
| 15 |
+
|
| 16 |
+
assert len(df) == 13
|
| 17 |
+
assert df.iloc[0]["Model"] == "Seedance 2.0"
|
| 18 |
+
assert df.iloc[0]["Rank"] == 1
|
| 19 |
+
assert df["Total"].is_monotonic_decreasing
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_filter_open_source_entries():
|
| 23 |
+
df = load_leaderboard()
|
| 24 |
+
view = filter_leaderboard(df, component_type="Open-source")
|
| 25 |
+
|
| 26 |
+
assert set(view["Component Type"]) == {"Open-source"}
|
| 27 |
+
assert "LTX-2.3" in set(view["Model"])
|
| 28 |
+
assert "Ovi" in set(view["Model"])
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_filter_sorts_every_metric_ascending_and_descending():
|
| 32 |
+
df = load_leaderboard()
|
| 33 |
+
|
| 34 |
+
for metric in METRIC_COLUMNS:
|
| 35 |
+
ascending = filter_leaderboard(df, sort_by=metric, sort_order="Ascending")
|
| 36 |
+
descending = filter_leaderboard(df, sort_by=metric, sort_order="Descending")
|
| 37 |
+
|
| 38 |
+
assert ascending[metric].is_monotonic_increasing
|
| 39 |
+
assert descending[metric].is_monotonic_decreasing
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_filter_sorts_non_metric_columns():
|
| 43 |
+
df = load_leaderboard()
|
| 44 |
+
|
| 45 |
+
by_model = filter_leaderboard(df, sort_by="Model", sort_order="Ascending")
|
| 46 |
+
by_type = filter_leaderboard(df, sort_by="Component Type", sort_order="Descending")
|
| 47 |
+
|
| 48 |
+
assert list(by_model["Model"]) == sorted(df["Model"], key=str.casefold)
|
| 49 |
+
assert list(by_type["Component Type"]) == sorted(
|
| 50 |
+
df["Component Type"],
|
| 51 |
+
key=str.casefold,
|
| 52 |
+
reverse=True,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_render_table_marks_current_sort_column():
|
| 57 |
+
df = load_leaderboard()
|
| 58 |
+
standings = metric_standings(df)
|
| 59 |
+
html = render_table(df, standings, sort_by="Speech", sort_order="Ascending")
|
| 60 |
+
|
| 61 |
+
assert '<th class="sorted" aria-sort="ascending">Speech' in html
|
| 62 |
+
assert '<span class="sort-indicator" aria-hidden="true">↑</span>' in html
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_metric_standings_handle_lower_is_better():
|
| 66 |
+
df = load_leaderboard()
|
| 67 |
+
standings = metric_standings(df)
|
| 68 |
+
|
| 69 |
+
assert math.isclose(standings["Total"].best, 72.07)
|
| 70 |
+
assert math.isclose(standings["AV"].best, 0.15)
|
| 71 |
+
assert math.isclose(standings["Lip"].best, 2.00)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_component_badges_strip_type_suffixes():
|
| 75 |
+
html = render_component_badges("NanoBanana2 (Proprietary)|MOVA (Open-source)")
|
| 76 |
+
|
| 77 |
+
assert "NanoBanana2" in html
|
| 78 |
+
assert "MOVA" in html
|
| 79 |
+
assert "(Proprietary)" not in html
|
| 80 |
+
assert "(Open-source)" not in html
|
tests/test_submission.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import math
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
from submission import SubmissionError, build_submission, save_submission_file
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
VALID_METRICS = {
|
| 10 |
+
"Vis": 0.839,
|
| 11 |
+
"Aud (PQ)": 6.31,
|
| 12 |
+
"AV": 0.37,
|
| 13 |
+
"Lip": 5.40,
|
| 14 |
+
"Text": 41.36,
|
| 15 |
+
"Face": 49.05,
|
| 16 |
+
"Music": 11.25,
|
| 17 |
+
"Speech": 76.49,
|
| 18 |
+
"Lo-Phy": 3.93,
|
| 19 |
+
"Hi-Phy": 52.92,
|
| 20 |
+
"Holistic": 57.45,
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _valid_submission(**overrides):
|
| 25 |
+
data = {
|
| 26 |
+
"model": "Ovi",
|
| 27 |
+
"components": "Ovi (Open-source)",
|
| 28 |
+
"component_type": "Open-source",
|
| 29 |
+
"contact": "maintainer@example.com",
|
| 30 |
+
"model_url": "https://example.com/model",
|
| 31 |
+
"results_url": "https://example.com/results",
|
| 32 |
+
"notes": "test submission",
|
| 33 |
+
"metrics": VALID_METRICS,
|
| 34 |
+
}
|
| 35 |
+
data.update(overrides)
|
| 36 |
+
return build_submission(**data)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_build_submission_recomputes_total():
|
| 40 |
+
submission = _valid_submission()
|
| 41 |
+
|
| 42 |
+
assert submission["status"] == "pending_review"
|
| 43 |
+
assert math.isclose(submission["computed_total"], 52.0174, abs_tol=1e-4)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_build_submission_rejects_out_of_range_metric():
|
| 47 |
+
metrics = dict(VALID_METRICS)
|
| 48 |
+
metrics["Vis"] = 1.2
|
| 49 |
+
|
| 50 |
+
with pytest.raises(SubmissionError, match="Vis must be between 0 and 1"):
|
| 51 |
+
_valid_submission(metrics=metrics)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_build_submission_requires_https_urls():
|
| 55 |
+
with pytest.raises(SubmissionError, match="Model or paper URL"):
|
| 56 |
+
_valid_submission(model_url="example.com/model")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_save_submission_file(tmp_path, monkeypatch):
|
| 60 |
+
monkeypatch.setenv("PENDING_SUBMISSION_DIR", str(tmp_path))
|
| 61 |
+
submission = _valid_submission(model="My Model")
|
| 62 |
+
|
| 63 |
+
path = save_submission_file(submission)
|
| 64 |
+
|
| 65 |
+
assert path.exists()
|
| 66 |
+
assert path.parent == tmp_path
|
| 67 |
+
assert json.loads(path.read_text(encoding="utf-8"))["model"] == "My Model"
|