Reza2kn commited on
Commit
f8c83b8
·
verified ·
1 Parent(s): 4803715

Switch leaderboard to interactive Gradio dataframe

Browse files
Files changed (5) hide show
  1. README.md +4 -1
  2. __pycache__/app.cpython-311.pyc +0 -0
  3. app.py +260 -43
  4. index.html +464 -131
  5. requirements.txt +1 -4
README.md CHANGED
@@ -3,7 +3,10 @@ title: Persian ASR Double Benchmark
3
  emoji: 🎙️
4
  colorFrom: red
5
  colorTo: pink
6
- sdk: static
 
 
 
7
  pinned: false
8
  ---
9
 
 
3
  emoji: 🎙️
4
  colorFrom: red
5
  colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: 5.22.0
8
+ app_file: app.py
9
+ python_version: 3.11
10
  pinned: false
11
  ---
12
 
__pycache__/app.cpython-311.pyc ADDED
Binary file (11.7 kB). View file
 
app.py CHANGED
@@ -5,58 +5,275 @@ import gradio as gr
5
  import pandas as pd
6
 
7
 
8
- DATA = Path(__file__).parent / "results.json"
9
-
10
-
11
- def load_rows():
12
- rows = json.loads(DATA.read_text())
13
- df = pd.DataFrame(rows)
14
- if df.empty:
15
- return df
16
- ordered = [
17
- "model",
18
- "family",
19
- "precision",
20
- "params_b",
21
- "gold69_wer",
22
- "gold69_cer",
23
- "fleurs_wer",
24
- "fleurs_cer",
25
- "mean_decode_ms",
26
- "status",
27
- "repo",
28
- "notes",
29
- ]
30
- df = df[[c for c in ordered if c in df.columns]]
31
- for col in ["gold69_wer", "gold69_cer", "fleurs_wer", "fleurs_cer"]:
32
- if col in df.columns:
33
- df[col] = df[col].map(lambda x: None if pd.isna(x) else round(float(x), 2))
34
- if "params_b" in df.columns:
35
- df["params_b"] = df["params_b"].map(lambda x: None if pd.isna(x) else round(float(x), 3))
36
- return df.sort_values(["fleurs_wer", "gold69_wer"], na_position="last")
37
-
38
-
39
- with gr.Blocks(title="Persian ASR Double Benchmark") as demo:
40
- gr.Markdown(
41
- """
42
- # Persian ASR Double Benchmark
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- Full-precision ASR benchmark on two Persian eval sets. Lower WER/CER is better.
45
 
46
- `gold69` is the Golha 69-clip stress/domain set. `fleurs` is the Persian FLEURS eval parquet set.
47
- Rows are added as models finish, so pending and failed runs can stay visible without pretending they are results.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  """
49
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  table = gr.Dataframe(
51
- value=load_rows,
 
 
 
52
  interactive=False,
53
  wrap=True,
54
- label="Leaderboard",
 
 
 
 
 
 
 
 
 
 
55
  )
56
- refresh = gr.Button("Refresh")
57
- refresh.click(load_rows, outputs=table)
 
 
58
 
59
 
60
  if __name__ == "__main__":
61
  demo.launch()
62
-
 
5
  import pandas as pd
6
 
7
 
8
+ ROOT = Path(__file__).parent
9
+ RESULTS_PATH = ROOT / "results.json"
10
+ MODELS_PATH = ROOT / "models.json"
11
+
12
+ COLUMNS = [
13
+ "Rank",
14
+ "Model",
15
+ "Family",
16
+ "Params",
17
+ "gold69 WER",
18
+ "gold69 CER",
19
+ "FLEURS WER",
20
+ "FLEURS CER",
21
+ "Decode",
22
+ "Status",
23
+ "Notes",
24
+ ]
25
+
26
+ DATATYPES = [
27
+ "number",
28
+ "markdown",
29
+ "str",
30
+ "number",
31
+ "number",
32
+ "number",
33
+ "number",
34
+ "number",
35
+ "number",
36
+ "str",
37
+ "str",
38
+ ]
39
+
40
+ STATUS_RANK = {"complete": 0, "running": 1, "queued": 2, "failed": 3}
41
+ SORTS = {
42
+ "gold69 WER": ("gold69 WER", True),
43
+ "FLEURS WER": ("FLEURS WER", True),
44
+ "Params": ("Params", True),
45
+ "Decode": ("Decode", True),
46
+ "Status": ("_status_rank", True),
47
+ "Model": ("_model_text", True),
48
+ }
49
+
50
+
51
+ def read_json(path: Path) -> list[dict]:
52
+ if not path.exists():
53
+ return []
54
+ return json.loads(path.read_text())
55
+
56
+
57
+ def pct(value):
58
+ return None if value is None or pd.isna(value) else round(float(value), 2)
59
+
60
+
61
+ def num(value, digits=3):
62
+ return None if value is None or pd.isna(value) else round(float(value), digits)
63
+
64
+
65
+ def model_link(row: dict) -> str:
66
+ model = row.get("model", "")
67
+ repo = row.get("repo", "")
68
+ if isinstance(repo, str) and repo.startswith("http"):
69
+ return f"[{model}]({repo})"
70
+ return model
71
+
72
+
73
+ def combined_rows() -> pd.DataFrame:
74
+ result_rows = {row.get("model"): row for row in read_json(RESULTS_PATH)}
75
+ model_rows = read_json(MODELS_PATH)
76
+
77
+ merged = []
78
+ seen = set()
79
+ for model_row in model_rows:
80
+ model = model_row.get("model")
81
+ row = {**model_row, **result_rows.get(model, {})}
82
+ merged.append(row)
83
+ seen.add(model)
84
+
85
+ for model, row in result_rows.items():
86
+ if model not in seen:
87
+ merged.append(row)
88
+
89
+ records = []
90
+ completed_rank = 1
91
+ for row in sorted(merged, key=lambda item: (STATUS_RANK.get(item.get("status", "queued"), 9), item.get("gold69_wer", 999), item.get("model", ""))):
92
+ is_complete = row.get("status") == "complete"
93
+ records.append(
94
+ {
95
+ "Rank": completed_rank if is_complete else None,
96
+ "Model": model_link(row),
97
+ "Family": row.get("family", ""),
98
+ "Params": num(row.get("params_b"), 3),
99
+ "gold69 WER": pct(row.get("gold69_wer")),
100
+ "gold69 CER": pct(row.get("gold69_cer")),
101
+ "FLEURS WER": pct(row.get("fleurs_wer")),
102
+ "FLEURS CER": pct(row.get("fleurs_cer")),
103
+ "Decode": num(row.get("mean_decode_ms"), 1),
104
+ "Status": row.get("status", "queued"),
105
+ "Notes": row.get("notes", ""),
106
+ "_status_rank": STATUS_RANK.get(row.get("status", "queued"), 9),
107
+ "_model_text": row.get("model", ""),
108
+ }
109
+ )
110
+ if is_complete:
111
+ completed_rank += 1
112
 
113
+ return pd.DataFrame(records)
114
 
115
+
116
+ def display_frame(sort_by: str = "gold69 WER", status: str = "All") -> pd.DataFrame:
117
+ df = combined_rows()
118
+ if status != "All":
119
+ df = df[df["Status"].eq(status)]
120
+
121
+ sort_col, ascending = SORTS.get(sort_by, SORTS["gold69 WER"])
122
+ if sort_col in df.columns:
123
+ df = df.sort_values(sort_col, ascending=ascending, na_position="last")
124
+
125
+ return df[COLUMNS].reset_index(drop=True)
126
+
127
+
128
+ def summary_cards() -> str:
129
+ df = combined_rows()
130
+ completed = df[df["Status"].eq("complete")]
131
+ best_gold = completed["gold69 WER"].min() if not completed.empty else None
132
+ best_fleurs = completed["FLEURS WER"].min() if not completed.empty else None
133
+ return f"""
134
+ <div class="cards">
135
+ <div><span>Completed models</span><strong>{len(completed)}</strong></div>
136
+ <div><span>Best gold69 WER</span><strong>{best_gold:.2f}%</strong></div>
137
+ <div><span>Best FLEURS WER</span><strong>{best_fleurs:.2f}%</strong></div>
138
+ <div><span>Queued baselines</span><strong>{len(df) - len(completed)}</strong></div>
139
+ </div>
140
+ """
141
+
142
+
143
+ CSS = """
144
+ :root {
145
+ --body-background-fill: #12070a;
146
+ --body-text-color: #fff7f7;
147
+ --block-background-fill: #241014;
148
+ --block-border-color: #5b2430;
149
+ --button-primary-background-fill: #ef4444;
150
+ --button-primary-background-fill-hover: #dc2626;
151
+ }
152
+ .gradio-container {
153
+ max-width: 1220px !important;
154
+ margin: 0 auto !important;
155
+ }
156
+ .hero h1 {
157
+ font-size: clamp(34px, 5vw, 58px);
158
+ line-height: 1.05;
159
+ margin-bottom: 8px;
160
+ }
161
+ .hero p {
162
+ color: #f0b7bd;
163
+ font-size: 18px;
164
+ line-height: 1.55;
165
+ }
166
+ .cards {
167
+ display: grid;
168
+ grid-template-columns: repeat(4, minmax(0, 1fr));
169
+ gap: 10px;
170
+ margin: 12px 0 18px;
171
+ }
172
+ .cards div {
173
+ border: 1px solid #5b2430;
174
+ background: linear-gradient(180deg, #35151c, #241014);
175
+ border-radius: 8px;
176
+ padding: 14px 16px;
177
+ }
178
+ .cards span {
179
+ display: block;
180
+ color: #f0b7bd;
181
+ margin-bottom: 6px;
182
+ }
183
+ .cards strong {
184
+ font-size: 28px;
185
+ }
186
+ #leaderboard table {
187
+ table-layout: auto !important;
188
+ }
189
+ #leaderboard table,
190
+ #leaderboard thead,
191
+ #leaderboard tbody,
192
+ #leaderboard th,
193
+ #leaderboard td {
194
+ color: #2a090e !important;
195
+ }
196
+ #leaderboard * {
197
+ color: #2a090e !important;
198
+ }
199
+ #leaderboard th,
200
+ #leaderboard td {
201
+ font-size: 15px !important;
202
+ line-height: 1.32 !important;
203
+ vertical-align: top !important;
204
+ }
205
+ #leaderboard th {
206
+ background: #ffe4e6 !important;
207
+ color: #4c0519 !important;
208
+ }
209
+ #leaderboard td {
210
+ white-space: normal !important;
211
+ overflow-wrap: anywhere !important;
212
+ }
213
+ #leaderboard td a {
214
+ color: #1d4ed8 !important;
215
+ font-weight: 700;
216
+ }
217
+ @media (max-width: 760px) {
218
+ .cards { grid-template-columns: 1fr; }
219
+ .hero h1 { font-size: 32px; }
220
+ }
221
+ """
222
+
223
+
224
+ with gr.Blocks(title="Persian ASR Double Benchmark", css=CSS, theme=gr.themes.Soft(primary_hue="red", neutral_hue="rose")) as demo:
225
+ gr.HTML(
226
+ """
227
+ <section class="hero">
228
+ <h1>Persian ASR Double Benchmark</h1>
229
+ <p>
230
+ A same-test leaderboard for Persian speech recognition models. gold69 is the tougher real-world set;
231
+ FLEURS is the cleaner public reference set. Lower WER/CER is better.
232
+ </p>
233
+ </section>
234
  """
235
  )
236
+ gr.HTML(summary_cards)
237
+
238
+ with gr.Row():
239
+ sort_by = gr.Dropdown(
240
+ choices=list(SORTS.keys()),
241
+ value="gold69 WER",
242
+ label="Default sort",
243
+ scale=1,
244
+ )
245
+ status = gr.Dropdown(
246
+ choices=["All", "complete", "running", "queued", "failed"],
247
+ value="All",
248
+ label="Status filter",
249
+ scale=1,
250
+ )
251
+ refresh = gr.Button("Refresh", variant="primary", scale=0)
252
+
253
  table = gr.Dataframe(
254
+ value=display_frame,
255
+ inputs=[sort_by, status],
256
+ headers=COLUMNS,
257
+ datatype=DATATYPES,
258
  interactive=False,
259
  wrap=True,
260
+ line_breaks=True,
261
+ label="Sortable leaderboard",
262
+ elem_id="leaderboard",
263
+ max_height=760,
264
+ show_search="filter",
265
+ show_copy_button=True,
266
+ show_fullscreen_button=True,
267
+ show_row_numbers=True,
268
+ pinned_columns=2,
269
+ max_chars=120,
270
+ column_widths=[72, 270, 190, 100, 116, 116, 116, 116, 96, 104, 420],
271
  )
272
+
273
+ refresh.click(display_frame, inputs=[sort_by, status], outputs=table)
274
+ sort_by.change(display_frame, inputs=[sort_by, status], outputs=table)
275
+ status.change(display_frame, inputs=[sort_by, status], outputs=table)
276
 
277
 
278
  if __name__ == "__main__":
279
  demo.launch()
 
index.html CHANGED
@@ -6,203 +6,430 @@
6
  <title>Persian ASR Double Benchmark</title>
7
  <style>
8
  :root {
9
- color-scheme: light dark;
10
- --bg: #15070a;
11
- --panel: #231015;
12
- --panel2: #32151b;
13
  --ink: #fff7f7;
14
- --muted: #f2b8bd;
15
- --line: #54242d;
 
16
  --accent: #ef4444;
17
- --accent2: #fb7185;
18
  --gold: #fbbf24;
 
 
19
  }
 
 
 
 
 
 
 
 
 
20
  body {
21
  margin: 0;
22
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
23
  background:
24
- radial-gradient(circle at top left, rgba(239, 68, 68, 0.28), transparent 34%),
25
- linear-gradient(180deg, #20080d 0%, var(--bg) 46%, #0f0507 100%);
26
  color: var(--ink);
27
  }
 
28
  main {
29
- max-width: 1120px;
30
  margin: 0 auto;
31
- padding: 34px 16px 48px;
32
  }
 
33
  header {
34
- margin-bottom: 24px;
 
 
35
  }
 
36
  .eyebrow {
 
37
  color: #fecdd3;
38
- font-size: 16px;
39
- font-weight: 800;
40
  letter-spacing: 0.08em;
41
  text-transform: uppercase;
42
- margin-bottom: 10px;
43
  }
 
44
  h1 {
45
- margin: 0 0 10px;
46
- font-size: clamp(38px, 5.8vw, 68px);
 
 
47
  letter-spacing: 0;
48
- line-height: 1.02;
49
  }
 
50
  p {
 
 
51
  color: var(--muted);
52
- line-height: 1.55;
53
- max-width: 920px;
54
  font-size: 20px;
 
 
55
  }
 
56
  .stats {
57
  display: grid;
58
- grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
59
  gap: 10px;
60
- margin: 24px 0;
61
  }
 
62
  .stat {
 
63
  border: 1px solid var(--line);
64
- background: linear-gradient(180deg, var(--panel2), var(--panel));
65
  border-radius: 8px;
66
  padding: 14px 16px;
67
- box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
68
  }
 
69
  .stat span {
70
  display: block;
71
  color: var(--muted);
72
- font-size: 16px;
73
  margin-bottom: 6px;
74
  }
 
75
  .stat strong {
76
- font-size: 30px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
78
- .table-wrap {
79
- overflow-x: auto;
 
 
 
 
80
  border: 1px solid var(--line);
81
  border-radius: 8px;
82
- background: var(--panel);
83
- box-shadow: 0 18px 60px rgba(0, 0, 0, 0.28);
84
  }
85
- table {
86
- width: 100%;
87
- border-collapse: collapse;
88
- table-layout: fixed;
 
 
 
 
 
89
  }
90
- th, td {
91
- padding: 10px 9px;
92
- text-align: left;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  border-bottom: 1px solid var(--line);
94
- vertical-align: top;
95
- font-size: 16px;
96
- overflow-wrap: anywhere;
97
  }
98
- th {
 
 
 
 
 
99
  color: #ffe4e6;
100
- font-weight: 700;
101
- background: #3a141c;
102
- position: sticky;
103
- top: 0;
 
 
 
 
 
104
  }
105
- tbody tr:nth-child(even) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  background: rgba(255, 255, 255, 0.025);
107
  }
108
- tr:last-child td {
109
- border-bottom: 0;
 
 
 
 
 
110
  }
111
- .num {
 
112
  text-align: right;
113
  font-variant-numeric: tabular-nums;
114
  white-space: nowrap;
115
  }
116
- th:nth-child(1), td:nth-child(1) { width: 22%; }
117
- th:nth-child(2), td:nth-child(2) { width: 14%; }
118
- th:nth-child(3), td:nth-child(3) { width: 7%; }
119
- th:nth-child(4), td:nth-child(4),
120
- th:nth-child(5), td:nth-child(5),
121
- th:nth-child(6), td:nth-child(6),
122
- th:nth-child(7), td:nth-child(7) { width: 8%; }
123
- th:nth-child(8), td:nth-child(8) { width: 7%; }
124
- th:nth-child(9), td:nth-child(9) { width: 7%; }
125
- th:nth-child(10), td:nth-child(10) { width: 22%; }
126
- .badge {
 
 
 
 
 
 
 
 
 
 
 
 
127
  display: inline-block;
128
- padding: 3px 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  border: 1px solid #7f1d1d;
130
  border-radius: 999px;
131
  color: #fecdd3;
132
- font-size: 15px;
 
133
  white-space: nowrap;
134
  }
 
135
  .complete {
136
  color: #fee2e2;
137
  border-color: #ef4444;
138
  background: rgba(239, 68, 68, 0.16);
139
  }
 
140
  .queued {
141
  color: #fde68a;
142
  border-color: #b45309;
143
- background: rgba(251, 191, 36, 0.1);
144
  }
 
145
  .running {
146
  color: #ffedd5;
147
  border-color: #f97316;
148
  background: rgba(249, 115, 22, 0.16);
149
  }
150
- a {
151
- color: #fecdd3;
152
- text-decoration: none;
 
 
 
 
 
 
153
  }
154
- a:hover {
155
- text-decoration: underline;
 
156
  }
 
157
  footer {
158
  margin-top: 18px;
159
  color: var(--muted);
160
  font-size: 16px;
 
161
  }
162
- @media (max-width: 760px) {
163
- main { padding-inline: 10px; }
164
- th, td { padding: 8px 6px; font-size: 13px; }
165
- .badge { padding: 2px 6px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  }
167
  </style>
168
  </head>
169
  <body>
170
  <main>
171
  <header>
172
- <div class="eyebrow">🎧 Persian speech recognition leaderboard</div>
173
  <h1>Which Persian ASR models actually hear better?</h1>
174
  <p>
175
  This leaderboard compares Persian automatic speech recognition models on two complementary tests.
176
- <strong>gold69</strong> is a small but tougher real-world Persian set, while <strong>FLEURS</strong>
177
- is a cleaner public-style reference set. Lower WER/CER means fewer transcription mistakes. 🔥
178
  </p>
179
  </header>
180
 
181
  <section class="stats" id="stats"></section>
182
- <section class="table-wrap">
183
- <table>
184
- <thead>
185
- <tr>
186
- <th>Model</th>
187
- <th>Family</th>
188
- <th class="num">Params / Size</th>
189
- <th class="num">gold69 WER</th>
190
- <th class="num">gold69 CER</th>
191
- <th class="num">FLEURS WER</th>
192
- <th class="num">FLEURS CER</th>
193
- <th class="num">Decode</th>
194
- <th>Status</th>
195
- <th>Notes</th>
196
- </tr>
197
- </thead>
198
- <tbody id="rows"></tbody>
199
- </table>
200
  </section>
 
201
  <footer>
202
- 🧪 WER is word error rate; CER is character error rate. Both are normalized with the same Persian text cleanup.
203
  Public baseline rows are added as their runs complete on the same two-test setup.
204
  </footer>
205
  </main>
 
206
  <script>
207
  const results = [
208
  {
@@ -216,7 +443,7 @@
216
  fleurs_cer: 5.764774608119949,
217
  mean_decode_ms: 18.51,
218
  status: "complete",
219
- notes: "Small, fast CTC model trained on a larger relabeled Persian batch. Stronger on the harder gold69 set."
220
  },
221
  {
222
  model: "student_mms1b",
@@ -242,7 +469,7 @@
242
  fleurs_cer: 4.992697887255379,
243
  mean_decode_ms: 714.62,
244
  status: "complete",
245
- notes: "Persian Whisper Large v3 finetune. Public baseline run completed on the same double benchmark. 🎙️"
246
  }
247
  ];
248
 
@@ -297,50 +524,156 @@
297
  }
298
  ];
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  function pct(v) {
301
- return typeof v === "number" ? `${v.toFixed(2)}%` : "";
302
  }
 
303
  function params(v) {
304
- if (typeof v === "number") return `${v.toFixed(3)}B`;
305
- return "";
306
  }
 
307
  function ms(v) {
308
- return typeof v === "number" ? `${v.toFixed(1)} ms` : "";
309
  }
310
- function modelCell(row) {
311
- const label = row.model;
312
- if (row.repo && row.repo.startsWith("http")) return `<a href="${row.repo}">${label}</a>`;
313
- return label;
 
 
 
 
314
  }
315
 
316
- const allRows = [
317
- ...results.sort((a, b) => (a.gold69_wer ?? 999) - (b.gold69_wer ?? 999)),
318
- ...queued
319
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
- document.getElementById("rows").innerHTML = allRows.map(row => `
322
- <tr>
323
- <td>${modelCell(row)}</td>
324
- <td>${row.family || ""}</td>
325
- <td class="num">${row.size_label || params(row.params_b)}</td>
326
- <td class="num">${pct(row.gold69_wer)}</td>
327
- <td class="num">${pct(row.gold69_cer)}</td>
328
- <td class="num">${pct(row.fleurs_wer)}</td>
329
- <td class="num">${pct(row.fleurs_cer)}</td>
330
- <td class="num">${ms(row.mean_decode_ms)}</td>
331
- <td><span class="badge ${row.status === "complete" ? "complete" : row.status === "running" ? "running" : "queued"}">${row.status}</span></td>
332
- <td>${row.notes || ""}</td>
333
- </tr>
334
- `).join("");
335
-
336
- const bestGold = results.reduce((a, b) => a.gold69_wer < b.gold69_wer ? a : b);
337
- const bestFleurs = results.reduce((a, b) => a.fleurs_wer < b.fleurs_wer ? a : b);
338
- document.getElementById("stats").innerHTML = `
339
- <div class="stat"><span>✅ Completed models</span><strong>${results.length}</strong></div>
340
- <div class="stat"><span>🔥 Best hard-set WER</span><strong>${pct(bestGold.gold69_wer)}</strong></div>
341
- <div class="stat"><span>🌸 Best FLEURS WER</span><strong>${pct(bestFleurs.fleurs_wer)}</strong></div>
342
- <div class="stat"><span>⏳ Queued baselines</span><strong>${queued.length}</strong></div>
343
- `;
344
  </script>
345
  </body>
346
  </html>
 
6
  <title>Persian ASR Double Benchmark</title>
7
  <style>
8
  :root {
9
+ color-scheme: dark;
10
+ --bg: #12070a;
11
+ --panel: #241014;
12
+ --panel-2: #35151c;
13
  --ink: #fff7f7;
14
+ --muted: #f0b7bd;
15
+ --soft: #ffd8dd;
16
+ --line: #5b2430;
17
  --accent: #ef4444;
18
+ --accent-2: #fb7185;
19
  --gold: #fbbf24;
20
+ --green: #34d399;
21
+ --shadow: rgba(0, 0, 0, 0.32);
22
  }
23
+
24
+ * { box-sizing: border-box; }
25
+
26
+ html,
27
+ body {
28
+ max-width: 100%;
29
+ overflow-x: hidden;
30
+ }
31
+
32
  body {
33
  margin: 0;
34
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
35
  background:
36
+ radial-gradient(circle at 16% 0%, rgba(239, 68, 68, 0.30), transparent 34%),
37
+ linear-gradient(180deg, #21080d 0%, var(--bg) 50%, #0c0406 100%);
38
  color: var(--ink);
39
  }
40
+
41
  main {
42
+ width: min(calc(100% - 28px), 1180px);
43
  margin: 0 auto;
44
+ padding: 34px 0 48px;
45
  }
46
+
47
  header {
48
+ display: grid;
49
+ gap: 16px;
50
+ margin-bottom: 22px;
51
  }
52
+
53
  .eyebrow {
54
+ max-width: 100%;
55
  color: #fecdd3;
56
+ font-size: 15px;
57
+ font-weight: 850;
58
  letter-spacing: 0.08em;
59
  text-transform: uppercase;
60
+ overflow-wrap: anywhere;
61
  }
62
+
63
  h1 {
64
+ max-width: min(100%, 960px);
65
+ margin: 0;
66
+ font-size: clamp(36px, 5vw, 62px);
67
+ line-height: 1.04;
68
  letter-spacing: 0;
69
+ overflow-wrap: anywhere;
70
  }
71
+
72
  p {
73
+ max-width: min(100%, 940px);
74
+ margin: 0;
75
  color: var(--muted);
 
 
76
  font-size: 20px;
77
+ line-height: 1.55;
78
+ overflow-wrap: anywhere;
79
  }
80
+
81
  .stats {
82
  display: grid;
83
+ grid-template-columns: repeat(4, minmax(0, 1fr));
84
  gap: 10px;
85
+ margin: 24px 0 14px;
86
  }
87
+
88
  .stat {
89
+ min-width: 0;
90
  border: 1px solid var(--line);
91
+ background: linear-gradient(180deg, var(--panel-2), var(--panel));
92
  border-radius: 8px;
93
  padding: 14px 16px;
94
+ box-shadow: 0 16px 40px var(--shadow);
95
  }
96
+
97
  .stat span {
98
  display: block;
99
  color: var(--muted);
100
+ font-size: 15px;
101
  margin-bottom: 6px;
102
  }
103
+
104
  .stat strong {
105
+ display: block;
106
+ font-size: 29px;
107
+ line-height: 1.1;
108
+ overflow-wrap: anywhere;
109
+ }
110
+
111
+ .toolbar {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: space-between;
115
+ gap: 12px;
116
+ margin: 18px 0 10px;
117
+ }
118
+
119
+ .hint {
120
+ color: var(--muted);
121
+ font-size: 15px;
122
+ line-height: 1.35;
123
+ max-width: 100%;
124
+ overflow-wrap: anywhere;
125
  }
126
+
127
+ .segmented {
128
+ display: inline-flex;
129
+ gap: 4px;
130
+ max-width: 100%;
131
+ padding: 4px;
132
  border: 1px solid var(--line);
133
  border-radius: 8px;
134
+ background: rgba(0, 0, 0, 0.16);
 
135
  }
136
+
137
+ .segmented button,
138
+ .sort-button {
139
+ border: 0;
140
+ border-radius: 6px;
141
+ color: var(--soft);
142
+ background: transparent;
143
+ font: inherit;
144
+ cursor: pointer;
145
  }
146
+
147
+ .segmented button {
148
+ padding: 8px 11px;
149
+ font-size: 15px;
150
+ white-space: nowrap;
151
+ }
152
+
153
+ .segmented button[aria-pressed="true"] {
154
+ background: rgba(239, 68, 68, 0.22);
155
+ color: #fff;
156
+ }
157
+
158
+ .leaderboard {
159
+ border: 1px solid var(--line);
160
+ border-radius: 8px;
161
+ overflow: hidden;
162
+ background: rgba(35, 16, 21, 0.94);
163
+ box-shadow: 0 20px 58px var(--shadow);
164
+ }
165
+
166
+ .grid-head,
167
+ .row-main {
168
+ display: grid;
169
+ grid-template-columns: minmax(260px, 1.8fr) minmax(114px, 0.58fr) minmax(122px, 0.72fr) minmax(122px, 0.72fr) minmax(102px, 0.58fr) minmax(98px, 0.52fr);
170
+ gap: 0;
171
+ align-items: stretch;
172
+ }
173
+
174
+ .grid-head {
175
+ background: #3a141c;
176
  border-bottom: 1px solid var(--line);
 
 
 
177
  }
178
+
179
+ .sort-button {
180
+ width: 100%;
181
+ min-height: 46px;
182
+ padding: 10px 12px;
183
+ text-align: left;
184
  color: #ffe4e6;
185
+ font-size: 15px;
186
+ font-weight: 800;
187
+ white-space: normal;
188
+ }
189
+
190
+ .sort-button:hover,
191
+ .sort-button:focus-visible {
192
+ outline: none;
193
+ background: rgba(255, 255, 255, 0.055);
194
  }
195
+
196
+ .sort-button.num { text-align: right; }
197
+
198
+ .sort-indicator {
199
+ color: var(--accent-2);
200
+ font-size: 13px;
201
+ margin-left: 4px;
202
+ }
203
+
204
+ .leader-row {
205
+ border-bottom: 1px solid var(--line);
206
+ }
207
+
208
+ .leader-row:last-child { border-bottom: 0; }
209
+
210
+ .leader-row:nth-child(even) {
211
  background: rgba(255, 255, 255, 0.025);
212
  }
213
+
214
+ .cell {
215
+ min-width: 0;
216
+ padding: 13px 12px;
217
+ font-size: 16px;
218
+ line-height: 1.35;
219
+ overflow-wrap: anywhere;
220
  }
221
+
222
+ .cell.num {
223
  text-align: right;
224
  font-variant-numeric: tabular-nums;
225
  white-space: nowrap;
226
  }
227
+
228
+ .model-name {
229
+ display: flex;
230
+ align-items: flex-start;
231
+ gap: 9px;
232
+ min-width: 0;
233
+ font-weight: 820;
234
+ }
235
+
236
+ .rank {
237
+ flex: 0 0 auto;
238
+ min-width: 28px;
239
+ padding-top: 1px;
240
+ color: var(--accent-2);
241
+ font-variant-numeric: tabular-nums;
242
+ }
243
+
244
+ .model-copy {
245
+ min-width: 0;
246
+ max-width: 100%;
247
+ }
248
+
249
+ .model-link {
250
  display: inline-block;
251
+ max-width: 100%;
252
+ color: #fff1f2;
253
+ text-decoration: none;
254
+ overflow-wrap: anywhere;
255
+ word-break: break-word;
256
+ }
257
+
258
+ .model-link:hover { text-decoration: underline; }
259
+
260
+ .family {
261
+ display: block;
262
+ margin-top: 4px;
263
+ color: var(--muted);
264
+ font-size: 14px;
265
+ font-weight: 650;
266
+ }
267
+
268
+ .badge {
269
+ display: inline-flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ min-width: 82px;
273
+ padding: 5px 8px;
274
  border: 1px solid #7f1d1d;
275
  border-radius: 999px;
276
  color: #fecdd3;
277
+ font-size: 14px;
278
+ font-weight: 760;
279
  white-space: nowrap;
280
  }
281
+
282
  .complete {
283
  color: #fee2e2;
284
  border-color: #ef4444;
285
  background: rgba(239, 68, 68, 0.16);
286
  }
287
+
288
  .queued {
289
  color: #fde68a;
290
  border-color: #b45309;
291
+ background: rgba(251, 191, 36, 0.10);
292
  }
293
+
294
  .running {
295
  color: #ffedd5;
296
  border-color: #f97316;
297
  background: rgba(249, 115, 22, 0.16);
298
  }
299
+
300
+ .details {
301
+ display: grid;
302
+ grid-template-columns: minmax(0, 1fr);
303
+ gap: 6px;
304
+ padding: 0 12px 14px 52px;
305
+ color: var(--muted);
306
+ font-size: 15px;
307
+ line-height: 1.45;
308
  }
309
+
310
+ .details strong {
311
+ color: #ffe4e6;
312
  }
313
+
314
  footer {
315
  margin-top: 18px;
316
  color: var(--muted);
317
  font-size: 16px;
318
+ line-height: 1.45;
319
  }
320
+
321
+ @media (max-width: 940px) {
322
+ main { width: min(calc(100% - 20px), 1180px); }
323
+ .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
324
+ .grid-head { display: none; }
325
+ .leaderboard {
326
+ display: grid;
327
+ gap: 10px;
328
+ padding: 10px;
329
+ border-radius: 8px;
330
+ background: transparent;
331
+ box-shadow: none;
332
+ border: 0;
333
+ }
334
+ .leader-row {
335
+ border: 1px solid var(--line);
336
+ border-radius: 8px;
337
+ overflow: hidden;
338
+ background: var(--panel);
339
+ box-shadow: 0 14px 34px var(--shadow);
340
+ }
341
+ .row-main {
342
+ grid-template-columns: repeat(2, minmax(0, 1fr));
343
+ }
344
+ .cell:first-child {
345
+ grid-column: 1 / -1;
346
+ }
347
+ .cell {
348
+ padding: 11px 12px;
349
+ font-size: 16px;
350
+ }
351
+ .cell.num {
352
+ text-align: left;
353
+ white-space: normal;
354
+ }
355
+ .cell::before {
356
+ display: block;
357
+ margin-bottom: 4px;
358
+ color: var(--muted);
359
+ font-size: 12px;
360
+ font-weight: 820;
361
+ text-transform: uppercase;
362
+ letter-spacing: 0.04em;
363
+ }
364
+ .cell[data-label]::before { content: attr(data-label); }
365
+ .details {
366
+ padding: 0 12px 13px;
367
+ }
368
+ .toolbar {
369
+ align-items: flex-start;
370
+ flex-direction: column;
371
+ }
372
+ .segmented {
373
+ width: 100%;
374
+ overflow-x: auto;
375
+ scrollbar-width: thin;
376
+ }
377
+ .segmented button {
378
+ flex: 1 1 0;
379
+ min-width: max-content;
380
+ }
381
+ }
382
+
383
+ @media (max-width: 560px) {
384
+ main { width: min(calc(100% - 16px), 1180px); padding-top: 24px; }
385
+ h1 { font-size: 31px; }
386
+ p { font-size: 18px; }
387
+ .stats { grid-template-columns: 1fr; }
388
+ .row-main { grid-template-columns: 1fr; }
389
+ .stat strong { font-size: 27px; }
390
+ .model-name { gap: 7px; }
391
+ .rank { min-width: 24px; }
392
+ .segmented button {
393
+ padding-inline: 9px;
394
+ }
395
  }
396
  </style>
397
  </head>
398
  <body>
399
  <main>
400
  <header>
401
+ <div class="eyebrow">Persian speech recognition leaderboard</div>
402
  <h1>Which Persian ASR models actually hear better?</h1>
403
  <p>
404
  This leaderboard compares Persian automatic speech recognition models on two complementary tests.
405
+ <strong>gold69</strong> is a small, tougher real-world Persian set. <strong>FLEURS</strong>
406
+ is cleaner and more public-benchmark-like. Lower WER/CER means fewer transcription mistakes.
407
  </p>
408
  </header>
409
 
410
  <section class="stats" id="stats"></section>
411
+
412
+ <section class="toolbar" aria-label="Leaderboard controls">
413
+ <div class="hint">Click a column to sort. Queued models stay visible, but completed rows rank first by default.</div>
414
+ <div class="segmented" role="group" aria-label="Quick sort">
415
+ <button type="button" data-preset="gold69_wer" aria-pressed="true">gold69 WER</button>
416
+ <button type="button" data-preset="fleurs_wer" aria-pressed="false">FLEURS WER</button>
417
+ <button type="button" data-preset="params_b" aria-pressed="false">Params</button>
418
+ <button type="button" data-preset="status_rank" aria-pressed="false">Status</button>
419
+ </div>
420
+ </section>
421
+
422
+ <section class="leaderboard" aria-label="Persian ASR leaderboard">
423
+ <div class="grid-head" id="head"></div>
424
+ <div id="rows"></div>
 
 
 
 
425
  </section>
426
+
427
  <footer>
428
+ WER is word error rate; CER is character error rate. Both are normalized with the same Persian text cleanup.
429
  Public baseline rows are added as their runs complete on the same two-test setup.
430
  </footer>
431
  </main>
432
+
433
  <script>
434
  const results = [
435
  {
 
443
  fleurs_cer: 5.764774608119949,
444
  mean_decode_ms: 18.51,
445
  status: "complete",
446
+ notes: "Small, fast CTC model trained on a larger relabeled Persian batch. Stronger on the harder gold69 set."
447
  },
448
  {
449
  model: "student_mms1b",
 
469
  fleurs_cer: 4.992697887255379,
470
  mean_decode_ms: 714.62,
471
  status: "complete",
472
+ notes: "Persian Whisper Large v3 finetune. Public baseline run completed on the same double benchmark."
473
  }
474
  ];
475
 
 
524
  }
525
  ];
526
 
527
+ const columns = [
528
+ { key: "model", label: "Model", type: "text" },
529
+ { key: "params_b", label: "Params", type: "number", className: "num", formatter: params },
530
+ { key: "gold69_wer", label: "gold69 WER", type: "number", className: "num", formatter: pct },
531
+ { key: "fleurs_wer", label: "FLEURS WER", type: "number", className: "num", formatter: pct },
532
+ { key: "mean_decode_ms", label: "Decode", type: "number", className: "num", formatter: ms },
533
+ { key: "status_rank", label: "Status", type: "number", formatter: statusBadge }
534
+ ];
535
+
536
+ const statusRank = { complete: 0, running: 1, queued: 2 };
537
+ const allRows = [...results, ...queued].map(row => ({
538
+ ...row,
539
+ status_rank: statusRank[row.status] ?? 3
540
+ }));
541
+
542
+ let sortState = { key: "gold69_wer", direction: "asc" };
543
+
544
  function pct(v) {
545
+ return typeof v === "number" ? `${v.toFixed(2)}%` : "pending";
546
  }
547
+
548
  function params(v) {
549
+ return typeof v === "number" ? `${v.toFixed(3)}B` : "pending";
 
550
  }
551
+
552
  function ms(v) {
553
+ return typeof v === "number" ? `${v.toFixed(1)} ms` : "pending";
554
  }
555
+
556
+ function escapeHtml(value) {
557
+ return String(value ?? "")
558
+ .replaceAll("&", "&amp;")
559
+ .replaceAll("<", "&lt;")
560
+ .replaceAll(">", "&gt;")
561
+ .replaceAll('"', "&quot;")
562
+ .replaceAll("'", "&#039;");
563
  }
564
 
565
+ function modelCell(row, rank) {
566
+ const safeModel = escapeHtml(row.model);
567
+ const model = row.repo && row.repo.startsWith("http")
568
+ ? `<a class="model-link" href="${escapeHtml(row.repo)}" target="_blank" rel="noreferrer">${safeModel}</a>`
569
+ : `<span class="model-link">${safeModel}</span>`;
570
+ return `
571
+ <div class="model-name">
572
+ <span class="rank">${rank}</span>
573
+ <span class="model-copy">${model}<span class="family">${escapeHtml(row.family || "")}</span></span>
574
+ </div>
575
+ `;
576
+ }
577
+
578
+ function statusBadge(_value, row) {
579
+ const status = escapeHtml(row.status || "queued");
580
+ return `<span class="badge ${status}">${status}</span>`;
581
+ }
582
+
583
+ function compareRows(a, b) {
584
+ const column = columns.find(item => item.key === sortState.key);
585
+ const direction = sortState.direction === "asc" ? 1 : -1;
586
+ const aMissing = a[sortState.key] === undefined || a[sortState.key] === null;
587
+ const bMissing = b[sortState.key] === undefined || b[sortState.key] === null;
588
+ if (aMissing && bMissing) return a.status_rank - b.status_rank || a.model.localeCompare(b.model);
589
+ if (aMissing) return 1;
590
+ if (bMissing) return -1;
591
+ if (column?.type === "text") {
592
+ return direction * String(a[sortState.key]).localeCompare(String(b[sortState.key]));
593
+ }
594
+ return direction * (Number(a[sortState.key]) - Number(b[sortState.key]));
595
+ }
596
+
597
+ function setSort(key) {
598
+ if (sortState.key === key) {
599
+ sortState = { key, direction: sortState.direction === "asc" ? "desc" : "asc" };
600
+ } else {
601
+ sortState = { key, direction: "asc" };
602
+ }
603
+ render();
604
+ }
605
+
606
+ function renderHead() {
607
+ document.getElementById("head").innerHTML = columns.map(column => {
608
+ const active = sortState.key === column.key;
609
+ const marker = active ? (sortState.direction === "asc" ? "↑" : "↓") : "";
610
+ return `
611
+ <button type="button" class="sort-button ${column.className || ""}" data-sort="${column.key}" aria-sort="${active ? sortState.direction : "none"}">
612
+ ${column.label}<span class="sort-indicator">${marker}</span>
613
+ </button>
614
+ `;
615
+ }).join("");
616
+ }
617
+
618
+ function renderRows() {
619
+ const sorted = [...allRows].sort(compareRows);
620
+ document.getElementById("rows").innerHTML = sorted.map((row, index) => {
621
+ const rank = row.status === "complete" ? `#${index + 1}` : "—";
622
+ return `
623
+ <article class="leader-row">
624
+ <div class="row-main">
625
+ <div class="cell" data-label="Model">${modelCell(row, rank)}</div>
626
+ <div class="cell num" data-label="Params">${params(row.params_b)}</div>
627
+ <div class="cell num" data-label="gold69 WER">${pct(row.gold69_wer)}</div>
628
+ <div class="cell num" data-label="FLEURS WER">${pct(row.fleurs_wer)}</div>
629
+ <div class="cell num" data-label="Decode">${ms(row.mean_decode_ms)}</div>
630
+ <div class="cell" data-label="Status">${statusBadge(row.status_rank, row)}</div>
631
+ </div>
632
+ <div class="details">
633
+ <div><strong>CER:</strong> gold69 ${pct(row.gold69_cer)} · FLEURS ${pct(row.fleurs_cer)}</div>
634
+ <div>${escapeHtml(row.notes || "")}</div>
635
+ </div>
636
+ </article>
637
+ `;
638
+ }).join("");
639
+ }
640
+
641
+ function renderStats() {
642
+ const completed = allRows.filter(row => row.status === "complete");
643
+ const bestGold = completed.reduce((best, row) => !best || row.gold69_wer < best.gold69_wer ? row : best, null);
644
+ const bestFleurs = completed.reduce((best, row) => !best || row.fleurs_wer < best.fleurs_wer ? row : best, null);
645
+ document.getElementById("stats").innerHTML = `
646
+ <div class="stat"><span>Completed models</span><strong>${completed.length}</strong></div>
647
+ <div class="stat"><span>Best hard-set WER</span><strong>${pct(bestGold?.gold69_wer)}</strong></div>
648
+ <div class="stat"><span>Best FLEURS WER</span><strong>${pct(bestFleurs?.fleurs_wer)}</strong></div>
649
+ <div class="stat"><span>Queued baselines</span><strong>${allRows.length - completed.length}</strong></div>
650
+ `;
651
+ }
652
+
653
+ function renderPresets() {
654
+ document.querySelectorAll("[data-preset]").forEach(button => {
655
+ button.setAttribute("aria-pressed", String(button.dataset.preset === sortState.key));
656
+ });
657
+ }
658
+
659
+ function render() {
660
+ renderHead();
661
+ renderRows();
662
+ renderStats();
663
+ renderPresets();
664
+ }
665
+
666
+ document.addEventListener("click", event => {
667
+ const sortButton = event.target.closest("[data-sort]");
668
+ if (sortButton) setSort(sortButton.dataset.sort);
669
+ const presetButton = event.target.closest("[data-preset]");
670
+ if (presetButton) {
671
+ sortState = { key: presetButton.dataset.preset, direction: "asc" };
672
+ render();
673
+ }
674
+ });
675
 
676
+ render();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  </script>
678
  </body>
679
  </html>
requirements.txt CHANGED
@@ -1,4 +1 @@
1
- gradio==4.44.1
2
- huggingface_hub>=0.30,<1.0
3
- pandas>=2.0
4
- audioop-lts>=0.2.2; python_version >= "3.13"
 
1
+ pandas>=2.0,<3