Yif29 commited on
Commit
dfd445e
·
verified ·
1 Parent(s): 3074e93

Update leaderboard sorting and readability

Browse files

Add sorting by every leaderboard item and improve text contrast.

.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 Bench
3
- emoji: 🥇
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: gradio
7
  app_file: app.py
8
- pinned: true
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
- # Start the configuration
17
-
18
- Most of the variables to change for a default leaderboard are in `src/env.py` (replace the path for your leaderboard) and `src/about.py` (for tasks).
19
-
20
- Results files should have the following format and be stored as json files:
21
- ```json
22
- {
23
- "config": {
24
- "model_dtype": "torch.float16", # or torch.bfloat16 or 8bit or 4bit
25
- "model_name": "path of the model on the hub: org/model",
26
- "model_sha": "revision on the hub",
27
- },
28
- "results": {
29
- "task_name": {
30
- "metric_name": score,
31
- },
32
- "task_name2": {
33
- "metric_name": score,
34
- }
35
- }
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  ```
38
 
39
- Request files are created automatically by this tool.
40
 
41
- If you encounter problem on the space, don't hesitate to restart it to remove the create eval-queue, eval-queue-bk, eval-results and eval-results-bk created folder.
42
 
43
- # Code logic for more complex edits
44
 
45
- You'll find
46
- - the main table' columns names and properties in `src/display/utils.py`
47
- - the logic to read all results and request files, then convert them in dataframe lines, in `src/leaderboard/read_evals.py`, and `src/populate.py`
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
- from gradio_leaderboard import Leaderboard, ColumnFilter, SelectColumns
3
- import pandas as pd
4
- from apscheduler.schedulers.background import BackgroundScheduler
5
- from huggingface_hub import snapshot_download
6
-
7
- from src.about import (
8
- CITATION_BUTTON_LABEL,
9
- CITATION_BUTTON_TEXT,
10
- EVALUATION_QUEUE_TEXT,
11
- INTRODUCTION_TEXT,
12
- LLM_BENCHMARKS_TEXT,
13
- TITLE,
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 src.envs import API, EVAL_REQUESTS_PATH, EVAL_RESULTS_PATH, QUEUE_REPO, REPO_ID, RESULTS_REPO, TOKEN
28
- from src.populate import get_evaluation_queue_df, get_leaderboard_df
29
- from src.submission.submit import add_new_eval
30
 
31
 
32
- def restart_space():
33
- API.restart_space(repo_id=REPO_ID)
 
34
 
35
- ### Space initialisation
36
- try:
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
- demo = gr.Blocks(css=custom_css)
93
- with demo:
94
- gr.HTML(TITLE)
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
- with gr.Row():
148
- with gr.Column():
149
- model_name_textbox = gr.Textbox(label="Model name")
150
- revision_name_textbox = gr.Textbox(label="Revision commit", placeholder="main")
151
- model_type = gr.Dropdown(
152
- choices=[t.to_str(" : ") for t in ModelType if t != ModelType.Unknown],
153
- label="Model type",
154
- multiselect=False,
155
- value=None,
156
- interactive=True,
157
- )
158
-
159
- with gr.Column():
160
- precision = gr.Dropdown(
161
- choices=[i.value.name for i in Precision if i != Precision.Unknown],
162
- label="Precision",
163
- multiselect=False,
164
- value="float16",
165
- interactive=True,
166
- )
167
- weight_type = gr.Dropdown(
168
- choices=[i.value.name for i in WeightType],
169
- label="Weights type",
170
- multiselect=False,
171
- value="Original",
172
- interactive=True,
173
- )
174
- base_model_name_textbox = gr.Textbox(label="Base model (for delta or adapter weights)")
175
-
176
- submit_button = gr.Button("Submit Eval")
177
- submission_result = gr.Markdown()
178
- submit_button.click(
179
- add_new_eval,
180
- [
181
- model_name_textbox,
182
- base_model_name_textbox,
183
- revision_name_textbox,
184
- precision,
185
- weight_type,
186
- model_type,
187
- ],
188
- submission_result,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- with gr.Row():
192
- with gr.Accordion("📙 Citation", open=False):
193
- citation_button = gr.Textbox(
194
- value=CITATION_BUTTON_TEXT,
195
- label=CITATION_BUTTON_LABEL,
196
- lines=20,
197
- elem_id="citation-button",
198
- show_copy_button=True,
 
199
  )
200
 
201
- scheduler = BackgroundScheduler()
202
- scheduler.add_job(restart_space, "interval", seconds=1800)
203
- scheduler.start()
204
- demo.queue(default_concurrency_limit=40).launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: aae95242efebfb3c6697385120098181a7e31c26421a89da1c4b12b16c078b6e
  • Pointer size: 131 Bytes
  • Size of remote file: 746 kB
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 = "&uarr;" if direction == "ascending" else "&darr;"
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
- APScheduler
2
- black
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">&uarr;</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"