Kimyayd commited on
Commit
52ea335
·
verified ·
1 Parent(s): 28b3757

FonBench — classement statique, copie de Kimyayd/FonBench

Browse files
Files changed (3) hide show
  1. README.md +47 -4
  2. app.js +463 -0
  3. index.html +174 -17
README.md CHANGED
@@ -1,10 +1,53 @@
1
  ---
2
  title: FonBench
3
- emoji: 📚
4
- colorFrom: gray
5
  colorTo: yellow
6
  sdk: static
7
- pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: FonBench
3
+ emoji: 🗣️
4
+ colorFrom: indigo
5
  colorTo: yellow
6
  sdk: static
7
+ pinned: true
8
+ license: apache-2.0
9
+ short_description: The public speech-recognition leaderboard for Fon
10
  ---
11
 
12
+ # FonBench 🇧🇯
13
+
14
+ The public speech-recognition leaderboard for **Fon**, a tonal language
15
+ spoken by over two million people in Benin.
16
+
17
+ This page carries the leaderboard itself: rankings, queue and submission
18
+ form. It reads its data live from the same database as the evaluation
19
+ service, so nothing here is a stale copy.
20
+
21
+ Evaluation runs elsewhere — a static Space cannot execute Python. Models
22
+ submitted here are picked up by the evaluator at
23
+ [Kimyayd/FonBench](https://huggingface.co/spaces/Kimyayd/FonBench), which
24
+ holds the GPU and the read access to the private test set.
25
+
26
+ ## Metrics
27
+
28
+ Fon is written with tones (á, ɔ́, ě…) that change the meaning of words, but
29
+ corpora don't follow the same convention — some mark no tone at all. A raw
30
+ WER is therefore not comparable from one corpus to the next.
31
+
32
+ - **WER_seg** — word errors with tones stripped. Comparable everywhere.
33
+ - **WER_ton** — errors on tone marks alone. Not computed when the corpus
34
+ doesn't annotate tones, so the figure is never misleading.
35
+ - **T-WER** = `WER_seg + 2 × WER_ton` — the headline metric.
36
+ - **RTFx** — seconds of audio per second of compute. Higher is faster;
37
+ hardware-dependent, so only compare at equal hardware.
38
+
39
+ ## Test set
40
+
41
+ 2,555 utterances, 4.98 hours, 45 speakers, **not published** — a test set
42
+ that circulates stops being a test set. Its 45 speakers are strictly
43
+ disjoint from the 471 training speakers, verified, zero in common. It is
44
+ not secret: request access to `JMLdata/fon-test-v1` and you can recompute
45
+ any row yourself.
46
+
47
+ ## Verify any number
48
+
49
+ Scoring code, a standalone evaluator and one script per evaluated model:
50
+ [github.com/Izzoudine/EvalScripts](https://github.com/Izzoudine/EvalScripts).
51
+ Expect agreement within ±0.0002 — CTC padding depends on batch composition,
52
+ and we would rather document that than round the published figures to three
53
+ decimals.
app.js ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* FonBench — version statique du classement.
2
+ *
3
+ * Un Space statique ne peut pas exécuter de Python : cette copie affiche le
4
+ * classement, la file et le formulaire, mais l'évaluation reste sur le Space
5
+ * Gradio qui détient le GPU et l'accès au corpus privé. Les données sont lues
6
+ * en direct depuis la même base : rien ici n'est une copie figée.
7
+ *
8
+ * La clé ci-dessous est publique par conception — c'est la Row Level Security
9
+ * qui décide de ce qui est lisible, pas le secret de la clé.
10
+ */
11
+ const SUPABASE = "https://cqdimvcnmhrsdcoobkmd.supabase.co/rest/v1";
12
+ const ANON = "sb_publishable_MapYll-_Y0hNoLYOfaDR3w_tsTEHyiz";
13
+ const H = { apikey: ANON };
14
+
15
+ const EVALUATEUR = "https://huggingface.co/spaces/Kimyayd/FonBench";
16
+
17
+ const TYPES = { base: "🌍 General", maison: "🔧 FonBench", tiers: "👥 Community" };
18
+ // « Fine-tuned » réunit les deux origines : c'est la question la plus
19
+ // fréquente — quels modèles ont vu du fongbe.
20
+ const VUES = [
21
+ ["All models", null],
22
+ ["🎯 Fine-tuned on Fon", ["maison", "tiers"]],
23
+ ["🔧 By FonBench", ["maison"]],
24
+ ["👥 By the community", ["tiers"]],
25
+ ["🌍 General-purpose", ["base"]],
26
+ ];
27
+
28
+ const ONGLETS = [
29
+ ["leaderboard", "Leaderboard"], ["queue", "Queue"],
30
+ ["submit", "Submit a model"], ["finetuning", "Fine-tuning"], ["about", "About"],
31
+ ];
32
+
33
+ let etat = { vue: 0, benchmarks: [], resultats: [], tri: null, sens: 1 };
34
+
35
+ /* --- accès aux données ------------------------------------------------ */
36
+
37
+ async function lire(chemin) {
38
+ const r = await fetch(`${SUPABASE}/${chemin}`, { headers: H });
39
+ if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
40
+ return r.json();
41
+ }
42
+
43
+ /* --- mise en forme ----------------------------------------------------- */
44
+
45
+ const pct = (x) => (x === null || x === undefined ? "—" : (x * 100).toFixed(1));
46
+
47
+ function taille(n) {
48
+ if (!n) return "—";
49
+ return n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : `${Math.round(n / 1e6)}M`;
50
+ }
51
+
52
+ /* La colonne model_kind a d'abord valu « fongbe » ou « base » avant de passer
53
+ * à trois valeurs. On retombe sur le préfixe du dépôt le cas échéant, sinon
54
+ * une base à l'ancien schéma ferait disparaître des lignes entières. */
55
+ function categorie(r) {
56
+ const k = r.model_kind;
57
+ if (["base", "maison", "tiers"].includes(k)) return k;
58
+ if (k === "fongbe") return r.model_id.startsWith("fonbench/") ? "maison" : "tiers";
59
+ return "base";
60
+ }
61
+
62
+ function lienModele(id, checkpoint) {
63
+ if (!id) return "—";
64
+ const url = `https://huggingface.co/${id}`;
65
+ const base = `<a href="${url}" target="_blank" rel="noopener">${id}</a>`;
66
+ return checkpoint ? `${base}<br><small>↳ ${checkpoint}</small>` : base;
67
+ }
68
+
69
+ const esc = (s) =>
70
+ String(s).replace(/[&<>"]/g, (c) =>
71
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
72
+
73
+ /* --- classement -------------------------------------------------------- */
74
+
75
+ const COLONNES = [
76
+ ["#", null], ["Model", null], ["Type", null],
77
+ ["T-WER", "twer"], ["WER_seg", "wer_notone"], ["WER_ton", "wer_ton"],
78
+ ["WER", "wer"], ["CER", "cer"], ["MER", "mer"], ["WIL", "wil"],
79
+ ["RTFx", "rtfx"], ["Size", "model_params"],
80
+ ["Base model", null], ["Trained on", null],
81
+ ["Architecture", null], ["Decoder", null],
82
+ ];
83
+
84
+ function entraineSur(r) {
85
+ if (!r.train_data) return "not declared";
86
+ if (r.train_data === "aucun fongbe") return "no Fon data";
87
+ return r.train_hours ? `${r.train_data} · ${+r.train_hours}h` : r.train_data;
88
+ }
89
+
90
+ function dessinerClassement() {
91
+ const rows = etat.resultats;
92
+ const bench = etat.benchmarks.find((b) => b.id === document.getElementById("bench").value) || {};
93
+
94
+ // Renseigne les listes d'architectures et de décodeurs à partir des données.
95
+ for (const [id, champ] of [["arch", "architecture"], ["dec", "decoder_type"]]) {
96
+ const sel = document.getElementById(id);
97
+ const vals = [...new Set(rows.map((r) => r[champ]).filter(Boolean))].sort();
98
+ const courant = sel.value;
99
+ sel.innerHTML = '<option value="">all</option>' +
100
+ vals.map((v) => `<option${v === courant ? " selected" : ""}>${esc(v)}</option>`).join("");
101
+ }
102
+
103
+ let sel = rows.slice();
104
+ const vises = VUES[etat.vue][1];
105
+ if (vises) sel = sel.filter((r) => vises.includes(categorie(r)));
106
+ const a = document.getElementById("arch").value;
107
+ const d = document.getElementById("dec").value;
108
+ if (a) sel = sel.filter((r) => r.architecture === a);
109
+ if (d) sel = sel.filter((r) => r.decoder_type === d);
110
+ if (document.getElementById("contam").checked)
111
+ sel = sel.filter((r) => !r.contamination_flag);
112
+
113
+ // Le corpus annote-t-il les tons ? Si oui le T-WER fait foi, sinon le
114
+ // WER_seg : comparer des WER bruts entre conventions tonales différentes
115
+ // n'aurait aucun sens.
116
+ const tonal = rows.some((r) => r.twer !== null && r.twer !== undefined);
117
+ const metrique = tonal ? "twer" : "wer_notone";
118
+ const grand = 9e9;
119
+
120
+ if (etat.tri) {
121
+ sel.sort((x, y) => etat.sens * ((x[etat.tri] ?? grand) - (y[etat.tri] ?? grand)));
122
+ } else {
123
+ const mode = document.getElementById("sort").value;
124
+ if (mode === "speed") sel.sort((x, y) => (y.rtfx ?? 0) - (x.rtfx ?? 0));
125
+ else if (mode === "size") sel.sort((x, y) => (x.model_params ?? 0) - (y.model_params ?? 0));
126
+ else sel.sort((x, y) => (x[metrique] ?? grand) - (y[metrique] ?? grand));
127
+ }
128
+
129
+ const medaille = { 1: "🥇", 2: "🥈", 3: "🥉" };
130
+ const corps = sel.map((r, i) => {
131
+ const ck = (r.model_revision || "").split(":");
132
+ const nom = lienModele(r.model_id, ck.length > 1 ? esc(ck.slice(1).join(":")) : null)
133
+ + (r.contamination_flag ? " ⚠️" : "");
134
+ return `<tr>
135
+ <td class="num">${i + 1} ${medaille[i + 1] || ""}</td>
136
+ <td>${nom}</td><td>${TYPES[categorie(r)]}</td>
137
+ <td class="num">${pct(r.twer)}</td><td class="num">${pct(r.wer_notone)}</td>
138
+ <td class="num">${pct(r.wer_ton)}</td><td class="num">${pct(r.wer)}</td>
139
+ <td class="num">${pct(r.cer)}</td><td class="num">${pct(r.mer)}</td>
140
+ <td class="num">${pct(r.wil)}</td>
141
+ <td class="num">${r.rtfx ? (+r.rtfx).toFixed(1) + "×" : "—"}</td>
142
+ <td class="num">${taille(r.model_params)}</td>
143
+ <td>${lienModele(r.base_model, null)}</td>
144
+ <td>${esc(entraineSur(r))}</td>
145
+ <td>${esc(r.architecture || "—")}</td><td>${esc(r.decoder_type || "—")}</td>
146
+ </tr>`;
147
+ }).join("");
148
+
149
+ document.getElementById("board").innerHTML =
150
+ `<thead><tr>${COLONNES.map(([t, c], i) =>
151
+ `<th data-col="${c || ""}" title="${c ? "Trier" : ""}">${t}${
152
+ etat.tri === c && c ? (etat.sens > 0 ? " ▲" : " ▼") : ""}</th>`).join("")}
153
+ </tr></thead><tbody>${corps || '<tr><td colspan="16">Aucun modèle.</td></tr>'}</tbody>`;
154
+
155
+ document.querySelectorAll("#board th[data-col]").forEach((th) => {
156
+ const c = th.dataset.col;
157
+ if (!c) return;
158
+ th.onclick = () => {
159
+ etat.sens = etat.tri === c ? -etat.sens : 1;
160
+ etat.tri = c;
161
+ dessinerClassement();
162
+ };
163
+ });
164
+
165
+ const compte = { maison: 0, tiers: 0, base: 0 };
166
+ sel.forEach((r) => compte[categorie(r)]++);
167
+ const drapeau = `<svg viewBox="0 0 30 20" width="20" height="13"
168
+ style="border-radius:2px"><rect width="30" height="20" fill="#FCD116"/>
169
+ <rect y="10" width="30" height="10" fill="#E8112D"/>
170
+ <rect width="12" height="20" fill="#008751"/></svg>`;
171
+ document.getElementById("meta").innerHTML =
172
+ `<span>${drapeau} <b>${esc(bench.name || "")}</b></span>
173
+ <span><b>${bench.num_utterances ?? "?"}</b> utterances</span>
174
+ <span><b>${bench.duration_hours ?? "?"}</b> hours</span>
175
+ ${bench.is_private ? '<span class="badge">private test set</span>' : ""}
176
+ <span>ranked by <b>${tonal ? "T-WER" : "WER_seg"}</b></span>
177
+ <span><b>${sel.length}</b> of ${rows.length} models shown</span>
178
+ <span>🔧 <b>${compte.maison}</b>&nbsp;fine-tuned by FonBench · 👥
179
+ <b>${compte.tiers}</b>&nbsp;fine-tuned by the community · 🌍
180
+ <b>${compte.base}</b>&nbsp;general-purpose</span>`;
181
+
182
+ document.getElementById("legend").innerHTML =
183
+ "All error rates in <b>%</b>, lower is better — except <b>RTFx</b>, where " +
184
+ "higher means faster. <b>⚠️</b> marks a model likely trained on this test " +
185
+ "set: its score is not comparable. Click a column header to sort.";
186
+ }
187
+
188
+ async function chargerClassement() {
189
+ const id = document.getElementById("bench").value;
190
+ try {
191
+ etat.resultats = await lire(
192
+ `results?is_hidden=eq.false&benchmark_id=eq.${encodeURIComponent(id)}&select=*`);
193
+ dessinerClassement();
194
+ } catch (e) {
195
+ document.getElementById("board").innerHTML =
196
+ `<tbody><tr><td>Database unreachable: ${esc(e.message)}</td></tr></tbody>`;
197
+ }
198
+ }
199
+
200
+ /* --- file d'attente ---------------------------------------------------- */
201
+
202
+ async function chargerFile() {
203
+ const etats = {
204
+ pending: "⏳ pending", running: "⚙️ running", done: "✅ done",
205
+ failed: "❌ failed", rejected: "🚫 rejected",
206
+ };
207
+ try {
208
+ const rows = await lire("public_queue?select=*&order=created_at.desc&limit=50");
209
+ const corps = rows.map((r) => {
210
+ const t = r.progress_total || 0, f = r.progress_done || 0;
211
+ let av = t ? `${f}/${t}` : "—";
212
+ if (t && r.status === "running") av += ` (${Math.floor((f * 100) / t)}%)`;
213
+ return `<tr><td>${esc(r.model_id)}</td><td>${etats[r.status] || esc(r.status)}</td>
214
+ <td class="num">${av}</td><td>${esc((r.error_message || "").slice(0, 120))}</td>
215
+ <td>${esc((r.created_at || "").slice(0, 10))}</td></tr>`;
216
+ }).join("");
217
+ document.getElementById("queue").innerHTML =
218
+ `<thead><tr><th>Model</th><th>Status</th><th>Progress</th><th>Details</th>
219
+ <th>Submitted</th></tr></thead>
220
+ <tbody>${corps || '<tr><td colspan="5">Queue empty.</td></tr>'}</tbody>`;
221
+ } catch (e) {
222
+ document.getElementById("queue").innerHTML =
223
+ `<tbody><tr><td>Database unreachable: ${esc(e.message)}</td></tr></tbody>`;
224
+ }
225
+ }
226
+
227
+ /* --- soumission -------------------------------------------------------- */
228
+
229
+ async function soumettre(ev) {
230
+ ev.preventDefault();
231
+ const sortie = document.getElementById("submit-out");
232
+ const val = (id) => document.getElementById(id).value.trim();
233
+ const modele = val("f-model");
234
+ const bouts = modele.split("/");
235
+ if (bouts.length !== 2 || !bouts[0] || !bouts[1]) {
236
+ sortie.innerHTML = '<div class="msg"><b>❌ Invalid format</b><br>The identifier ' +
237
+ "must look like <code>organisation/name</code>, exactly as it appears in " +
238
+ "the model URL on Hugging Face.</div>";
239
+ return;
240
+ }
241
+ const corps = { model_id: modele, benchmark_id: val("f-bench") };
242
+ for (const [champ, id] of [["hf_username", "f-user"], ["contact", "f-contact"],
243
+ ["note", "f-note"], ["train_data", "f-data"],
244
+ ["base_model", "f-base"]]) {
245
+ if (val(id)) corps[champ] = val(id);
246
+ }
247
+ if (val("f-hours")) corps.train_hours = parseFloat(val("f-hours"));
248
+
249
+ try {
250
+ const r = await fetch(`${SUPABASE}/public_requests`, {
251
+ method: "POST",
252
+ headers: { ...H, "Content-Type": "application/json", Prefer: "return=minimal" },
253
+ body: JSON.stringify(corps),
254
+ });
255
+ if (!r.ok) {
256
+ // La base applique elle-même les garde-fous (doublon, débit, file
257
+ // pleine) et renvoie un message déjà rédigé.
258
+ let detail = await r.text();
259
+ try { detail = JSON.parse(detail).message || detail; } catch (_) {}
260
+ sortie.innerHTML = `<div class="msg"><b>❌ Submission rejected</b><br>${esc(detail)}</div>`;
261
+ return;
262
+ }
263
+ sortie.innerHTML = `<div class="msg"><b>✅ <code>${esc(modele)}</code> is queued</b><br>
264
+ Evaluation runs in slices on the GPU of
265
+ <a href="${EVALUATEUR}" target="_blank" rel="noopener">Kimyayd/FonBench</a>.
266
+ Depending on the available quota, expect anywhere from a few minutes to a
267
+ few hours. The score will appear in the leaderboard once computed — follow
268
+ progress in the <b>Queue</b> tab.</div>`;
269
+ chargerFile();
270
+ } catch (e) {
271
+ sortie.innerHTML = `<div class="msg"><b>❌ Could not submit</b><br>${esc(e.message)}</div>`;
272
+ }
273
+ }
274
+
275
+ /* --- textes longs ------------------------------------------------------ */
276
+
277
+ const NOTE_METRIQUES = `
278
+ <h3>Reading the table</h3>
279
+ <p>Fon is a <b>tonal</b> language: tones are written with diacritics (á, ɔ́, ě…)
280
+ and change the meaning of words. But Fon corpora don't follow the same
281
+ convention — some mark no tone at all. A raw WER therefore isn't comparable
282
+ from one corpus to the next. Hence three families of measures.</p>
283
+ <table>
284
+ <tr><th>Metric</th><th>What it tells you</th></tr>
285
+ <tr><td><b>WER_seg</b></td><td>Word errors with <b>tones stripped</b>. Measures
286
+ phonetic accuracy and stays comparable across every corpus.</td></tr>
287
+ <tr><td><b>WER_ton</b></td><td>Errors on tone marks alone. Shown as “—” when the
288
+ corpus doesn't annotate tones, so the figure is never misleading.</td></tr>
289
+ <tr><td><b>T-WER</b></td><td><code>WER_seg + 2 × WER_ton</code>. The headline
290
+ metric: it penalises tone mistakes twice over.</td></tr>
291
+ <tr><td>WER, CER</td><td>Word and character errors, tones included.</td></tr>
292
+ <tr><td>MER, WIL</td><td><i>Match error rate</i> and <i>word information lost</i>
293
+ — more robust when a model produces many insertions.</td></tr>
294
+ <tr><td>RTFx</td><td>Seconds of audio per second of compute. <b>Higher is
295
+ faster.</b></td></tr>
296
+ </table>
297
+ <p><b>Model types.</b> 🌍 <i>General-purpose</i> models were not built for Fon:
298
+ some are multilingual systems that do cover it among a thousand other languages
299
+ (the MMS family ships a Fon adapter), others are multilingual without Fon, and
300
+ others again are monolingual systems for English or French. They are not
301
+ <i>base</i> models in the pretrained sense — <code>wav2vec2-large-960h-lv60-self</code>
302
+ is a finished English recogniser, not a starting point. The <b>Trained on</b>
303
+ column says exactly what each one saw. 🔧 <i>FonBench</i> and 👥 <i>Community</i>
304
+ models were fine-tuned on Fon.</p>
305
+ <p>Truly <i>base</i> models — raw pretrained checkpoints such as
306
+ <code>facebook/wav2vec2-large-xlsr-53</code> — cannot appear here at all: without
307
+ a CTC head or a vocabulary they transcribe nothing. They show up only in the
308
+ <b>Base model</b> column, as the starting point of the models fine-tuned from
309
+ them.</p>
310
+ <p><b>Speed.</b> RTFx depends on the hardware, recorded with each score. Only
311
+ compare speeds at equal hardware.</p>`;
312
+
313
+ const FINE_TUNING = `
314
+ <h3>The FonBench fine-tuning runs</h3>
315
+ <p>Four pretrained models were fine-tuned on Fon under strictly identical
316
+ conditions, so that any gap between them comes from the starting model alone.</p>
317
+ <p><b>The data.</b> The training corpus holds 44,225 utterances, of which
318
+ <b>13,716 were kept — 30.00 hours from 471 speakers</b>. The sample is not drawn
319
+ at random: it is built by taking turns across speakers, each contributing a
320
+ little before any one of them dominates. For generalisation, the diversity of
321
+ voices matters more than raw volume.</p>
322
+ <p>The 471 training speakers and the 45 test speakers are <b>strictly
323
+ disjoint</b> — verified, zero in common. A temporal cutoff separates the
324
+ transcripts; 20 sentences out of 2,555 (0.8%) do appear in both, too few to move
325
+ a score but worth stating rather than claiming a perfect separation.</p>
326
+ <p><b>The protocol.</b> Identical across all four: 3 epochs, i.e. 2,574 steps
327
+ with 257 of warmup. CTC decoding, character vocabulary built from the training
328
+ corpus, feature extractor taken from the base model. A single 24 GB L4,
329
+ <code>fp16</code>, gradient checkpointing.</p>
330
+ <table>
331
+ <tr><th>Resulting model</th><th>Base</th><th>LR</th><th>Batch</th>
332
+ <th>Wall time</th><th>Dev loss</th><th>WER_seg</th></tr>
333
+ <tr><td><code>wav2vec2-large-xlsr-53-fon-30h</code></td><td>wav2vec2-large-xlsr-53</td>
334
+ <td>3·10⁻⁴</td><td>8×2</td><td>54 min</td><td><b>0.468</b></td><td><b>38.6%</b></td></tr>
335
+ <tr><td><code>mms-300m-fon-30h</code></td><td>mms-300m</td><td>3·10⁻⁴</td>
336
+ <td>8×2</td><td>not retained</td><td>not retained</td><td><b>42.8%</b></td></tr>
337
+ <tr><td><code>w2v-bert-2.0-fon-30h</code></td><td>w2v-bert-2.0</td><td>3·10⁻⁵</td>
338
+ <td>4×4</td><td>116 min</td><td>3.102</td><td>96.9%</td></tr>
339
+ <tr><td><code>AfriHuBERT-fon-30h</code></td><td>ajesujoba/AfriHuBERT</td>
340
+ <td>3·10⁻⁵</td><td>8×2</td><td>24 min</td><td>3.078</td><td>100.0%</td></tr>
341
+ </table>
342
+ <p><b>Fine-tuning makes the difference, not the base.</b>
343
+ <code>chrisjay/fonxlsr</code> starts from exactly the same model as our best run
344
+ — <code>facebook/wav2vec2-large-xlsr-53</code>, confirmed in its configuration —
345
+ and reaches 69.3% where we reach 38.6%. The error is cut by a factor of 1.8 with
346
+ 30 hours of well-chosen data.</p>
347
+ <p><b>Two runs out of four failed</b>, and that deserves saying plainly.
348
+ <code>w2v-bert-2.0</code> and <code>AfriHuBERT</code> do learn: their loss drops
349
+ clearly during training. But it plateaus around 3.1 against 0.47 for the run that
350
+ succeeds — the signature of settling into the trivial CTC solution, emitting the
351
+ blank symbol everywhere. This is not a learning-rate problem: both were first run
352
+ at 3·10⁻⁴, then rerun at 3·10⁻⁵ — the figures in the table — for exactly the same
353
+ outcome. The same collapse was later reproduced independently by three
354
+ <code>afrihubert-fon-asr-vanilla</code> runs from another team, all between 97.8%
355
+ and 99.7%; the same encoder preceded by continued pretraining on 960 h of Fon
356
+ reaches 18.3%. Four failures and one success point at the same cause.</p>
357
+ <p>Both failures stay in the leaderboard. Removing them would paint a flattering
358
+ and false picture of what fine-tuning guarantees.</p>
359
+ <p><b>Reproducing.</b> The training and evaluation code is published at
360
+ <a href="https://github.com/Izzoudine/EvalScripts" target="_blank" rel="noopener">
361
+ github.com/Izzoudine/EvalScripts</a> — one script per evaluated model.</p>`;
362
+
363
+ const A_PROPOS = `
364
+ <h3>FonBench</h3>
365
+ <p>The first public speech-recognition leaderboard for <b>Fon</b>, a tonal
366
+ language spoken by over two million people in Benin. The project answers a
367
+ concrete difficulty: until now, published Fon results were not comparable with
368
+ one another, for lack of a shared protocol and a shared test set.</p>
369
+ <h3>Why the test set is not published</h3>
370
+ <p>Public Fon corpora have been circulating since 2016, and several released
371
+ models were trained on them. Their WER on that data then looks remarkable —
372
+ while saying nothing about their real ability to transcribe an unseen voice. We
373
+ measured it: one of the test sets we were using shared <b>83% of its
374
+ utterances</b> with the training split of a public dataset.</p>
375
+ <p>The main test set (<b>2,555 utterances, 4.98 hours, 45 speakers</b>) is
376
+ therefore not distributed. Only aggregate scores are made public. It is not
377
+ secret, though: request access to <code>JMLdata/fon-test-v1</code> and you can
378
+ recompute any row yourself.</p>
379
+ <h3>The protocol</h3>
380
+ <ul>
381
+ <li><b>Pinned revision.</b> Every score is tied to the exact commit hash of the
382
+ repository evaluated.</li>
383
+ <li><b>Shared normalisation.</b> The same scoring code for every model, tones
384
+ included. It is open: <code>fonbench_eval.py</code>.</li>
385
+ <li><b>No arbitrary code.</b> Models are loaded with
386
+ <code>trust_remote_code=False</code>.</li>
387
+ <li><b>No duplicate work.</b> A (model, revision, corpus) triple is never
388
+ re-evaluated.</li>
389
+ </ul>
390
+ <h3>Where things run</h3>
391
+ <p>This page is a <i>static</i> Space: it renders the leaderboard and accepts
392
+ submissions, but cannot execute Python. Evaluation runs on
393
+ <a href="${EVALUATEUR}" target="_blank" rel="noopener">Kimyayd/FonBench</a>,
394
+ which holds the GPU and the read access to the private test set. Both read and
395
+ write the same database, so what you see here is live, not a copy.</p>
396
+ <h3>Verify any number</h3>
397
+ <p>Scoring code, a standalone evaluator and one script per evaluated model:
398
+ <a href="https://github.com/Izzoudine/EvalScripts" target="_blank" rel="noopener">
399
+ github.com/Izzoudine/EvalScripts</a>. Expect agreement within ±0.0002 — CTC
400
+ padding depends on batch composition, and we would rather document that than
401
+ round the published figures to three decimals.</p>`;
402
+
403
+ /* --- démarrage --------------------------------------------------------- */
404
+
405
+ function onglets() {
406
+ const nav = document.getElementById("tabs");
407
+ nav.innerHTML = ONGLETS.map(([id, titre], i) =>
408
+ `<button data-tab="${id}" aria-selected="${i === 0}">${titre}</button>`).join("");
409
+ nav.querySelectorAll("button").forEach((b) => {
410
+ b.onclick = () => {
411
+ nav.querySelectorAll("button").forEach((x) =>
412
+ x.setAttribute("aria-selected", x === b));
413
+ ONGLETS.forEach(([id]) =>
414
+ document.getElementById(`tab-${id}`).classList.toggle("hidden", id !== b.dataset.tab));
415
+ if (b.dataset.tab === "queue") chargerFile();
416
+ };
417
+ });
418
+ }
419
+
420
+ async function demarrer() {
421
+ onglets();
422
+ document.getElementById("metrics-note").innerHTML = NOTE_METRIQUES;
423
+ document.getElementById("ft").innerHTML = FINE_TUNING;
424
+ document.getElementById("about").innerHTML = A_PROPOS;
425
+
426
+ document.getElementById("views").innerHTML = VUES.map(([t], i) =>
427
+ `<button data-vue="${i}" aria-pressed="${i === 0}">${t}</button>`).join("");
428
+ document.querySelectorAll("#views button").forEach((b) => {
429
+ b.onclick = () => {
430
+ etat.vue = +b.dataset.vue;
431
+ document.querySelectorAll("#views button").forEach((x) =>
432
+ x.setAttribute("aria-pressed", x === b));
433
+ dessinerClassement();
434
+ };
435
+ });
436
+
437
+ try {
438
+ etat.benchmarks = await lire("benchmarks?is_active=eq.true&select=*&order=id");
439
+ } catch (e) {
440
+ document.getElementById("meta").textContent = `Database unreachable: ${e.message}`;
441
+ return;
442
+ }
443
+ const options = etat.benchmarks.map((b) =>
444
+ `<option value="${esc(b.id)}"${b.id === "jml-test-v1" ? " selected" : ""}>
445
+ ${esc(b.name)} (${b.num_utterances ?? "?"} utterances)</option>`).join("");
446
+ document.getElementById("bench").innerHTML = options;
447
+ document.getElementById("f-bench").innerHTML = options;
448
+
449
+ ["bench", "sort", "arch", "dec"].forEach((id) => {
450
+ document.getElementById(id).onchange = () => {
451
+ etat.tri = null;
452
+ if (id === "bench") chargerClassement(); else dessinerClassement();
453
+ };
454
+ });
455
+ document.getElementById("contam").onchange = dessinerClassement;
456
+ document.getElementById("refresh").onclick = chargerClassement;
457
+ document.getElementById("refresh-queue").onclick = chargerFile;
458
+ document.getElementById("submit-form").onsubmit = soumettre;
459
+
460
+ chargerClassement();
461
+ }
462
+
463
+ demarrer();
index.html CHANGED
@@ -1,19 +1,176 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>FonBench Fon ASR leaderboard</title>
7
+ <style>
8
+ /* Indigo profond et ocre — les couleurs des tentures appliquées d'Abomey. */
9
+ :root{
10
+ --indigo:#3b3b7a; --ocre:#c8873b; --ink:#1b1b2e; --muted:#6b6b7b;
11
+ --line:rgba(120,120,160,.22); --fond:#fff; --carte:#fafaff;
12
+ }
13
+ @media (prefers-color-scheme:dark){
14
+ :root{--indigo:#a9a9f0;--ink:#ececf6;--muted:#9a9ab0;--fond:#0e0e18;--carte:#16161f;}
15
+ }
16
+ *{box-sizing:border-box}
17
+ body{margin:0;background:var(--fond);color:var(--ink);
18
+ font:15px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
19
+ .wrap{max-width:1500px;margin:0 auto;padding:0 1.1rem 3rem}
20
+
21
+ header{padding:1.6rem 0 .6rem;border-bottom:1px solid var(--line)}
22
+ h1{display:flex;align-items:center;gap:.55rem;margin:0;font-size:2.4rem;
23
+ font-weight:800;letter-spacing:-.03em;line-height:1.05;color:var(--indigo)}
24
+ h1 span{color:var(--ocre)}
25
+ h1 svg{flex:0 0 auto;border-radius:2px;box-shadow:0 0 0 1px rgba(0,0,0,.12)}
26
+ .sub{color:var(--muted);margin:.35rem 0 0;font-size:1.02rem}
27
+
28
+ nav{display:flex;gap:.2rem;flex-wrap:wrap;margin:1rem 0 .2rem;
29
+ border-bottom:1px solid var(--line)}
30
+ nav button{background:none;border:none;border-bottom:2px solid transparent;
31
+ padding:.6rem .9rem;font:inherit;font-weight:600;color:var(--muted);cursor:pointer}
32
+ nav button[aria-selected=true]{color:var(--indigo);border-bottom-color:var(--indigo)}
33
+ nav button:hover{color:var(--ink)}
34
+
35
+ .filters{display:flex;flex-wrap:wrap;gap:.9rem;margin:1rem 0 .4rem}
36
+ .field{display:flex;flex-direction:column;gap:.28rem;min-width:170px;flex:1}
37
+ .field>label{font-size:.7rem;font-weight:700;letter-spacing:.07em;
38
+ text-transform:uppercase;color:var(--muted)}
39
+ select,input,textarea{font:inherit;padding:.45rem .6rem;border-radius:9px;
40
+ border:1px solid var(--line);background:var(--carte);color:var(--ink);width:100%}
41
+ .views{display:flex;flex-wrap:wrap;gap:.35rem;margin:.2rem 0 .5rem}
42
+ .views button{border:1px solid var(--line);background:var(--carte);color:var(--ink);
43
+ border-radius:999px;padding:.32rem .9rem;font:inherit;font-weight:600;
44
+ font-size:.86rem;cursor:pointer;transition:background .12s}
45
+ .views button:hover{background:rgba(120,120,180,.12)}
46
+ .views button[aria-pressed=true]{background:var(--indigo);color:#fff;
47
+ border-color:var(--indigo)}
48
+ .row2{display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;margin-bottom:.5rem}
49
+ .row2 label{font-size:.88rem;color:var(--muted);display:flex;align-items:center;gap:.4rem}
50
+ .row2 input[type=checkbox]{width:auto}
51
+ button.act{border:1px solid var(--line);background:var(--carte);color:var(--ink);
52
+ border-radius:9px;padding:.4rem .9rem;font:inherit;font-weight:600;cursor:pointer}
53
+
54
+ .meta{display:flex;flex-wrap:wrap;gap:.45rem 1.4rem;align-items:center;
55
+ padding:.7rem .9rem;margin:.2rem 0 .7rem;border:1px solid var(--line);
56
+ border-radius:12px;font-size:.88rem;color:var(--muted)}
57
+ .meta>span{display:inline-flex;align-items:center;gap:.35rem}
58
+ .meta b{color:var(--ink);font-weight:650}
59
+ .badge{display:inline-block;padding:.12rem .55rem;border-radius:999px;
60
+ font-size:.72rem;font-weight:700;background:var(--ocre);color:#fff}
61
+ .legend{font-size:.82rem;color:var(--muted);margin:.15rem 0 .9rem}
62
+
63
+ .tablebox{overflow-x:auto;border:1px solid var(--line);border-radius:12px}
64
+ table{border-collapse:collapse;width:100%;font-size:.86rem}
65
+ th,td{padding:.45rem .6rem;text-align:left;white-space:nowrap;
66
+ border-bottom:1px solid var(--line)}
67
+ th{position:sticky;top:0;background:var(--carte);font-weight:700;
68
+ font-size:.75rem;letter-spacing:.04em;text-transform:uppercase;
69
+ color:var(--muted);cursor:pointer;user-select:none}
70
+ th:hover{color:var(--ink)}
71
+ tbody tr:hover{background:rgba(120,120,180,.07)}
72
+ td.num{text-align:right;font-variant-numeric:tabular-nums}
73
+ a{color:var(--indigo)}
74
+ .note{font-size:.9rem;line-height:1.6;max-width:62rem}
75
+ .note h3{margin-top:1.5rem;font-size:1.05rem}
76
+ .note table{font-size:.86rem;margin:.6rem 0;width:auto}
77
+ .note th{position:static;cursor:default;text-transform:none;font-size:.8rem}
78
+ .hidden{display:none}
79
+ .msg{padding:.7rem .9rem;border-radius:10px;border:1px solid var(--line);
80
+ background:var(--carte);margin:.7rem 0}
81
+ form{max-width:44rem}
82
+ form .field{margin-bottom:.8rem}
83
+ .hint{font-size:.8rem;color:var(--muted);margin-top:.2rem}
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <div class="wrap">
88
+ <header>
89
+ <h1>Fon<span>Bench</span>
90
+ <!-- Drapeau béninois en SVG : l'emoji est une paire d'indicateurs
91
+ régionaux que Windows ne compose pas, il s'affiche « BJ ». -->
92
+ <svg viewBox="0 0 30 20" width="39" height="26" role="img" aria-label="Benin">
93
+ <rect width="30" height="20" fill="#FCD116"/>
94
+ <rect y="10" width="30" height="10" fill="#E8112D"/>
95
+ <rect width="12" height="20" fill="#008751"/>
96
+ </svg>
97
+ </h1>
98
+ <p class="sub">The public speech-recognition leaderboard for Fon, a tonal
99
+ language of Benin</p>
100
+ </header>
101
+
102
+ <nav id="tabs"></nav>
103
+
104
+ <section id="tab-leaderboard">
105
+ <div class="filters">
106
+ <div class="field"><label for="bench">Test set</label>
107
+ <select id="bench"></select></div>
108
+ <div class="field"><label for="sort">Sort by</label>
109
+ <select id="sort">
110
+ <option value="quality">Quality (ranking metric)</option>
111
+ <option value="speed">Speed (RTFx)</option>
112
+ <option value="size">Model size</option>
113
+ </select></div>
114
+ <div class="field"><label for="arch">Architecture</label>
115
+ <select id="arch"><option value="">all</option></select></div>
116
+ <div class="field"><label for="dec">Decoder</label>
117
+ <select id="dec"><option value="">all</option></select></div>
118
+ </div>
119
+ <div class="views" id="views"></div>
120
+ <div class="row2">
121
+ <label><input type="checkbox" id="contam"> Hide contaminated models</label>
122
+ <button class="act" id="refresh">Refresh</button>
123
+ </div>
124
+ <div class="meta" id="meta"></div>
125
+ <p class="legend" id="legend"></p>
126
+ <div class="tablebox"><table id="board"></table></div>
127
+ <div class="note" id="metrics-note"></div>
128
+ </section>
129
+
130
+ <section id="tab-queue" class="hidden">
131
+ <p class="note">Evaluation runs on the GPU of
132
+ <a href="https://huggingface.co/spaces/Kimyayd/FonBench" target="_blank"
133
+ rel="noopener">Kimyayd/FonBench</a>, in slices. The compute itself is
134
+ fast — a few minutes for a CTC model — but the daily GPU quota is limited,
135
+ so a run may spread over several hours, pausing between slices. An
136
+ interrupted run resumes exactly where it stopped.</p>
137
+ <button class="act" id="refresh-queue">Refresh</button>
138
+ <div class="tablebox" style="margin-top:.7rem"><table id="queue"></table></div>
139
+ </section>
140
+
141
+ <section id="tab-submit" class="hidden">
142
+ <p class="note">The repository must be <b>public</b> and must not rely on
143
+ custom code: <code>trust_remote_code</code> is disabled. Both
144
+ <code>safetensors</code> and <code>.bin</code> weights are accepted. No
145
+ account required.</p>
146
+ <form id="submit-form">
147
+ <div class="field"><label for="f-model">Hugging Face model ID</label>
148
+ <input id="f-model" placeholder="organisation/model-name" required>
149
+ <span class="hint">Exactly as it appears in the model URL.</span></div>
150
+ <div class="field"><label for="f-base">Base model (optional)</label>
151
+ <input id="f-base" placeholder="facebook/wav2vec2-large-xlsr-53"></div>
152
+ <div class="field"><label for="f-data">Training data (optional)</label>
153
+ <input id="f-data" placeholder="ALFFA, Zenodo, private corpus…">
154
+ <span class="hint">Leave empty for a model never fine-tuned on Fon.</span></div>
155
+ <div class="field"><label for="f-hours">Hours of Fon audio (optional)</label>
156
+ <input id="f-hours" type="number" min="0" step="0.1"></div>
157
+ <div class="field"><label for="f-user">Your HF username (optional)</label>
158
+ <input id="f-user"></div>
159
+ <div class="field"><label for="f-contact">Contact (optional)</label>
160
+ <input id="f-contact"></div>
161
+ <div class="field"><label for="f-note">Note (optional)</label>
162
+ <textarea id="f-note" rows="2"></textarea></div>
163
+ <div class="field"><label for="f-bench">Test set</label>
164
+ <select id="f-bench"></select></div>
165
+ <button class="act" type="submit" style="background:var(--indigo);color:#fff;
166
+ border-color:var(--indigo)">Submit</button>
167
+ </form>
168
+ <div id="submit-out"></div>
169
+ </section>
170
+
171
+ <section id="tab-finetuning" class="hidden"><div class="note" id="ft"></div></section>
172
+ <section id="tab-about" class="hidden"><div class="note" id="about"></div></section>
173
+ </div>
174
+ <script src="app.js"></script>
175
+ </body>
176
  </html>