Spaces:
Running
Running
Commit ·
2cdca7d
0
Parent(s):
pazabench version 2 release
Browse files- .gitattributes +35 -0
- .gitignore +51 -0
- README.md +18 -0
- app.py +743 -0
- es-metadata.yml +8 -0
- requirements.txt +125 -0
- src/about.py +412 -0
- src/assets/PazaBench-Banner-Mobile.png +3 -0
- src/assets/PazaBench-Banner2.png +3 -0
- src/constants.py +168 -0
- src/data/african_countries.json +56 -0
- src/data/changelog_cards.html +88 -0
- src/data/country_names.json +40 -0
- src/data/country_to_region_map.json +45 -0
- src/data/dataset_group_metadata.json +177 -0
- src/data/language_country_map.json +214 -0
- src/data/language_name_mapping.json +5 -0
- src/data/language_to_countries_map.json +212 -0
- src/data/model_family_metadata.json +66 -0
- src/data_processing.py +496 -0
- src/display/css_html_js.py +2205 -0
- src/display/formatting.py +29 -0
- src/display/model_family_colors.csv +96 -0
- src/display/styling.py +46 -0
- src/display/utils.py +80 -0
- src/envs.py +32 -0
- src/language_metadata.py +35 -0
- src/leaderboard/read_evals.py +195 -0
- src/model_counts.py +100 -0
- src/populate.py +46 -0
- src/submission/check_validity.py +97 -0
- src/submission/submit.py +181 -0
- src/telemetry.py +463 -0
- src/visualizations.py +1193 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.zip 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 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
auto_evals/
|
| 2 |
+
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
__pycache__/
|
| 5 |
+
.env
|
| 6 |
+
.ipynb_checkpoints
|
| 7 |
+
*ipynb
|
| 8 |
+
.vscode/
|
| 9 |
+
|
| 10 |
+
eval-queue/
|
| 11 |
+
eval-results/
|
| 12 |
+
eval-queue-bk/
|
| 13 |
+
eval-results-bk/
|
| 14 |
+
logs/
|
| 15 |
+
|
| 16 |
+
results/
|
| 17 |
+
*.pem
|
| 18 |
+
|
| 19 |
+
deployment.zip
|
| 20 |
+
|
| 21 |
+
# Deployment files
|
| 22 |
+
download_data_assets.sh
|
| 23 |
+
deployment.sh
|
| 24 |
+
dev-results_summary.csv
|
| 25 |
+
# docker-compose.yml
|
| 26 |
+
docker-compose.dev.yml
|
| 27 |
+
# Dockerfile
|
| 28 |
+
pyproject.toml
|
| 29 |
+
uv.lock
|
| 30 |
+
startup.sh
|
| 31 |
+
.dockerignore
|
| 32 |
+
results_summary.csv
|
| 33 |
+
upload_results_summary.py
|
| 34 |
+
*results_summary*
|
| 35 |
+
|
| 36 |
+
# OS/IDE files
|
| 37 |
+
.DS_Store
|
| 38 |
+
*.swp
|
| 39 |
+
*.swo
|
| 40 |
+
Thumbs.db
|
| 41 |
+
|
| 42 |
+
# Gradio cache
|
| 43 |
+
.gradio/
|
| 44 |
+
|
| 45 |
+
.env.example
|
| 46 |
+
src/aggregate_results.py
|
| 47 |
+
docker-compose.yml
|
| 48 |
+
.gitignore
|
| 49 |
+
src/aggregate_results.py
|
| 50 |
+
Dockerfile
|
| 51 |
+
|
README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: PazaBench
|
| 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: ASR Leaderboard for low resource languages
|
| 11 |
+
sdk_version: 5.43.1
|
| 12 |
+
tags:
|
| 13 |
+
- leaderboard
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# PazaBench Leaderboard
|
| 17 |
+
|
| 18 |
+
An Automatic Speech Recognition (ASR) benchmark for low-resource languages developed by **Microsoft Research Africa, Nairobi Lab**. Launching with **39 African Languages** across **52 State-of-the-Art ASR and Language Models**, PazaBench compares three key metrics: **Character Error Rate (CER)**, **Word Error Rate (WER)**, and **RTFx (Inverse Real-Time Factor)**.
|
app.py
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Any
|
| 3 |
+
|
| 4 |
+
import gradio as gr
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
| 7 |
+
from huggingface_hub import snapshot_download
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
from src.about import (
|
| 11 |
+
CITATION_BUTTON_LABEL,
|
| 12 |
+
CITATION_BUTTON_TEXT,
|
| 13 |
+
CHANGELOG_HEADER,
|
| 14 |
+
CHANGELOG_CARDS_HTML,
|
| 15 |
+
EVALUATION_LANGUAGE_TEXT,
|
| 16 |
+
get_dataset_group_label,
|
| 17 |
+
get_introduction_text,
|
| 18 |
+
get_llm_benchmarks_text,
|
| 19 |
+
get_llm_benchmarks_datasets_text,
|
| 20 |
+
)
|
| 21 |
+
from src.constants import (
|
| 22 |
+
ASR_DISPLAY_COLUMNS,
|
| 23 |
+
FILTER_COLUMN_ORDER,
|
| 24 |
+
FILTER_PARAM_MAP,
|
| 25 |
+
INTERPRETATIONS,
|
| 26 |
+
METRIC_CONFIGS,
|
| 27 |
+
)
|
| 28 |
+
from src.display.css_html_js import (
|
| 29 |
+
custom_css,
|
| 30 |
+
fix_aria_hidden_focus_js,
|
| 31 |
+
light_mode_head,
|
| 32 |
+
chart_gallery_html,
|
| 33 |
+
section_divider_html,
|
| 34 |
+
footer_html,
|
| 35 |
+
)
|
| 36 |
+
from src.display.utils import EVAL_COLS
|
| 37 |
+
from src.language_metadata import get_all_regions
|
| 38 |
+
from src.envs import API, EVAL_REQUESTS_PATH, QUEUE_REPO, REPO_ID, TOKEN, HF_ENABLED
|
| 39 |
+
from src.populate import get_evaluation_queue_df
|
| 40 |
+
from src.submission.submit import add_language_eval_request
|
| 41 |
+
from src.visualizations import (
|
| 42 |
+
create_model_leaderboard,
|
| 43 |
+
create_cer_leaderboard,
|
| 44 |
+
create_speed_accuracy_scatter,
|
| 45 |
+
create_wer_cer_correlation,
|
| 46 |
+
create_language_coverage_chart,
|
| 47 |
+
create_language_location_map,
|
| 48 |
+
get_language_sample_info,
|
| 49 |
+
get_all_languages,
|
| 50 |
+
)
|
| 51 |
+
from src.data_processing import (
|
| 52 |
+
load_asr_results,
|
| 53 |
+
get_filter_options,
|
| 54 |
+
filter_asr_dataframe,
|
| 55 |
+
compute_metric_tables,
|
| 56 |
+
compute_metric_dataframes,
|
| 57 |
+
strip_dataset_labels,
|
| 58 |
+
VIEW_MODE_COLUMNS,
|
| 59 |
+
DEFAULT_VIEW_MODE,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
from src.telemetry import (
|
| 63 |
+
track_event,
|
| 64 |
+
track_filter_change,
|
| 65 |
+
track_visualization_view,
|
| 66 |
+
track_error,
|
| 67 |
+
)
|
| 68 |
+
# App-specific constants
|
| 69 |
+
SORT_DIRECTION_CHOICES = ["Ascending", "Descending"]
|
| 70 |
+
DEFAULT_SORT_DIRECTION = "Ascending"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _sorted_column_values(frame: pd.DataFrame, column: str) -> list[str]:
|
| 74 |
+
"""Get sorted unique values from a column."""
|
| 75 |
+
if column not in frame.columns or frame.empty:
|
| 76 |
+
return []
|
| 77 |
+
values = sorted({value for value in frame[column].dropna().unique() if value != "Unknown"})
|
| 78 |
+
if (frame[column] == "Unknown").any():
|
| 79 |
+
values.append("Unknown")
|
| 80 |
+
return values
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def update_metric_tables(models, languages, dataset_groups, view_mode, african_regions=None):
|
| 84 |
+
"""Update HTML metric tables based on current filters."""
|
| 85 |
+
clean_dataset_groups = strip_dataset_labels(dataset_groups)
|
| 86 |
+
tables = compute_metric_tables(
|
| 87 |
+
models or [],
|
| 88 |
+
languages or [],
|
| 89 |
+
clean_dataset_groups,
|
| 90 |
+
view_mode,
|
| 91 |
+
african_regions=african_regions or [],
|
| 92 |
+
asr_results_df=ASR_RESULTS_DF,
|
| 93 |
+
)
|
| 94 |
+
return [tables.get("cer", ""), tables.get("wer", ""), tables.get("rtfx", "")]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def update_metric_dataframes(models, languages, dataset_groups, view_mode, african_regions=None, language_support=None):
|
| 98 |
+
"""Update metric dataframes for interactive sorting."""
|
| 99 |
+
clean_dataset_groups = strip_dataset_labels(dataset_groups)
|
| 100 |
+
dataframes = compute_metric_dataframes(
|
| 101 |
+
models or [],
|
| 102 |
+
languages or [],
|
| 103 |
+
clean_dataset_groups,
|
| 104 |
+
view_mode,
|
| 105 |
+
african_regions=african_regions or [],
|
| 106 |
+
language_support=language_support or [],
|
| 107 |
+
asr_results_df=ASR_RESULTS_DF,
|
| 108 |
+
)
|
| 109 |
+
return [
|
| 110 |
+
dataframes.get("cer", pd.DataFrame()),
|
| 111 |
+
dataframes.get("wer", pd.DataFrame()),
|
| 112 |
+
dataframes.get("rtfx", pd.DataFrame())
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _language_support_to_list(value: str | None) -> list[str]:
|
| 117 |
+
"""Convert the single-select 'Model Language support' choice to the backend list form."""
|
| 118 |
+
if not value or value == "All":
|
| 119 |
+
return []
|
| 120 |
+
return [value]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def refresh_filter_dropdowns(
|
| 124 |
+
models: list[str] | None,
|
| 125 |
+
languages: list[str] | None,
|
| 126 |
+
dataset_groups: list[str] | None,
|
| 127 |
+
african_regions: list[str] | None = None,
|
| 128 |
+
):
|
| 129 |
+
"""Refresh filter dropdown options based on current selections."""
|
| 130 |
+
if ASR_RESULTS_DF.empty:
|
| 131 |
+
empty_update = gr.update(choices=[], value=None)
|
| 132 |
+
return [empty_update] * (len(FILTER_COLUMN_ORDER) + 1)
|
| 133 |
+
|
| 134 |
+
clean_dataset_groups = strip_dataset_labels(dataset_groups) if dataset_groups else []
|
| 135 |
+
|
| 136 |
+
selections = {
|
| 137 |
+
"models": list(models or []),
|
| 138 |
+
"languages": list(languages or []),
|
| 139 |
+
"dataset_groups": clean_dataset_groups,
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
updates: list[Any] = []
|
| 143 |
+
for column in FILTER_COLUMN_ORDER:
|
| 144 |
+
param = FILTER_PARAM_MAP[column]
|
| 145 |
+
filter_kwargs = {key: list(value) for key, value in selections.items()}
|
| 146 |
+
filter_kwargs[param] = []
|
| 147 |
+
filtered = filter_asr_dataframe(
|
| 148 |
+
ASR_RESULTS_DF,
|
| 149 |
+
models=filter_kwargs["models"],
|
| 150 |
+
languages=filter_kwargs["languages"],
|
| 151 |
+
dataset_groups=filter_kwargs["dataset_groups"],
|
| 152 |
+
african_regions=african_regions,
|
| 153 |
+
)
|
| 154 |
+
options = _sorted_column_values(filtered, column)
|
| 155 |
+
current_selection = [value for value in selections[param] if value in options]
|
| 156 |
+
|
| 157 |
+
if column == "dataset_group":
|
| 158 |
+
labeled_options = [get_dataset_group_label(dg, ASR_RESULTS_DF) for dg in options]
|
| 159 |
+
labeled_selection = [get_dataset_group_label(dg, ASR_RESULTS_DF) for dg in current_selection] if current_selection else None
|
| 160 |
+
updates.append(gr.update(choices=labeled_options, value=labeled_selection))
|
| 161 |
+
else:
|
| 162 |
+
updates.append(gr.update(choices=options, value=current_selection or None))
|
| 163 |
+
|
| 164 |
+
updates.append(gr.update(choices=get_all_regions(), value=african_regions or None))
|
| 165 |
+
return updates
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# Initialize data
|
| 169 |
+
try:
|
| 170 |
+
ASR_RESULTS_DF = load_asr_results()
|
| 171 |
+
ASR_RESULTS_ERROR: str | None = None
|
| 172 |
+
except (FileNotFoundError, ValueError) as err:
|
| 173 |
+
ASR_RESULTS_DF = pd.DataFrame(columns=ASR_DISPLAY_COLUMNS)
|
| 174 |
+
ASR_RESULTS_ERROR = str(err)
|
| 175 |
+
track_error(err, {"source": "results_csv"}, context="data_load")
|
| 176 |
+
|
| 177 |
+
ASR_FILTER_OPTIONS = get_filter_options(ASR_RESULTS_DF) if ASR_RESULTS_ERROR is None else {
|
| 178 |
+
"model": [],
|
| 179 |
+
"language": [],
|
| 180 |
+
"dataset_group": [],
|
| 181 |
+
"dataset_group_labels": [],
|
| 182 |
+
"african_region": [],
|
| 183 |
+
"language_support": [],
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
if ASR_RESULTS_ERROR is None:
|
| 187 |
+
ASR_INITIAL_TABLES = compute_metric_tables([], [], [], DEFAULT_VIEW_MODE, asr_results_df=ASR_RESULTS_DF)
|
| 188 |
+
ASR_INITIAL_DATAFRAMES = compute_metric_dataframes([], [], [], DEFAULT_VIEW_MODE, language_support=[], asr_results_df=ASR_RESULTS_DF)
|
| 189 |
+
else:
|
| 190 |
+
ASR_INITIAL_TABLES = {metric: f"<p class='metric-table-empty'>⚠️ {ASR_RESULTS_ERROR}</p>" for metric in METRIC_CONFIGS}
|
| 191 |
+
ASR_INITIAL_DATAFRAMES = {metric: pd.DataFrame() for metric in METRIC_CONFIGS}
|
| 192 |
+
|
| 193 |
+
(
|
| 194 |
+
finished_eval_queue_df,
|
| 195 |
+
running_eval_queue_df,
|
| 196 |
+
pending_eval_queue_df,
|
| 197 |
+
) = get_evaluation_queue_df(EVAL_REQUESTS_PATH, EVAL_COLS)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def restart_space():
|
| 201 |
+
"""Restart the HuggingFace Space (only if HF is enabled)."""
|
| 202 |
+
if HF_ENABLED and API:
|
| 203 |
+
try:
|
| 204 |
+
API.restart_space(repo_id=REPO_ID)
|
| 205 |
+
except Exception as e:
|
| 206 |
+
print(f"Could not restart space: {e}")
|
| 207 |
+
else:
|
| 208 |
+
print("HuggingFace Hub disabled, skipping space restart")
|
| 209 |
+
|
| 210 |
+
### Space initialisation
|
| 211 |
+
if HF_ENABLED:
|
| 212 |
+
try:
|
| 213 |
+
print(f"Downloading requests from {QUEUE_REPO}...")
|
| 214 |
+
print(EVAL_REQUESTS_PATH)
|
| 215 |
+
snapshot_download(
|
| 216 |
+
repo_id=QUEUE_REPO, local_dir=EVAL_REQUESTS_PATH, repo_type="dataset", tqdm_class=None, etag_timeout=30, token=TOKEN
|
| 217 |
+
)
|
| 218 |
+
except Exception as e:
|
| 219 |
+
print(f"Could not download requests: {e}")
|
| 220 |
+
else:
|
| 221 |
+
print("HuggingFace Hub disabled, skipping dataset downloads")
|
| 222 |
+
|
| 223 |
+
demo = gr.Blocks(css=custom_css, head=light_mode_head)
|
| 224 |
+
with demo:
|
| 225 |
+
# Inject accessibility fix for aria-hidden focusable elements (WCAG 4.1.2)
|
| 226 |
+
gr.HTML(fix_aria_hidden_focus_js)
|
| 227 |
+
gr.Image(
|
| 228 |
+
value="src/assets/PazaBench-Banner2.png",
|
| 229 |
+
show_label=False,
|
| 230 |
+
container=False,
|
| 231 |
+
elem_id="banner-image",
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
with gr.Tabs(elem_classes="tab-buttons") as tabs:
|
| 235 |
+
with gr.TabItem("🎙️ ASR Metrics", elem_id="asr-results-tab", id=0):
|
| 236 |
+
if ASR_RESULTS_ERROR:
|
| 237 |
+
gr.Markdown(f"⚠️ {ASR_RESULTS_ERROR}")
|
| 238 |
+
else:
|
| 239 |
+
gr.Markdown(get_introduction_text(ASR_RESULTS_DF), elem_classes="markdown-text")
|
| 240 |
+
gr.Markdown(
|
| 241 |
+
"### Compare models at a glance. Use the filters below to customize your view "
|
| 242 |
+
"or explore the data directly in the tables.",
|
| 243 |
+
elem_classes="explainer-text"
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# Filter and customise accordion (collapsed by default)
|
| 247 |
+
with gr.Accordion("🔧 Filter and customise", open=False, elem_id="filter-customize-accordion"):
|
| 248 |
+
gr.Markdown(
|
| 249 |
+
"Compare all models by default, or narrow the view by model, language, region, or dataset. "
|
| 250 |
+
"Choose whether columns show model families or individual models."
|
| 251 |
+
)
|
| 252 |
+
with gr.Row():
|
| 253 |
+
model_filter = gr.Dropdown(
|
| 254 |
+
choices=ASR_FILTER_OPTIONS["model"],
|
| 255 |
+
multiselect=True,
|
| 256 |
+
label="Model",
|
| 257 |
+
value=None,
|
| 258 |
+
allow_custom_value=False,
|
| 259 |
+
)
|
| 260 |
+
language_filter = gr.Dropdown(
|
| 261 |
+
choices=ASR_FILTER_OPTIONS["language"],
|
| 262 |
+
multiselect=True,
|
| 263 |
+
label="Language",
|
| 264 |
+
value=None,
|
| 265 |
+
allow_custom_value=False,
|
| 266 |
+
)
|
| 267 |
+
dataset_filter = gr.Dropdown(
|
| 268 |
+
choices=ASR_FILTER_OPTIONS.get("dataset_group_labels", ASR_FILTER_OPTIONS["dataset_group"]),
|
| 269 |
+
multiselect=True,
|
| 270 |
+
label="Dataset group",
|
| 271 |
+
value=None,
|
| 272 |
+
allow_custom_value=False,
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
with gr.Row():
|
| 276 |
+
african_region_filter = gr.Dropdown(
|
| 277 |
+
choices=ASR_FILTER_OPTIONS.get("african_region", []),
|
| 278 |
+
multiselect=True,
|
| 279 |
+
label="African Region",
|
| 280 |
+
value=None,
|
| 281 |
+
allow_custom_value=False,
|
| 282 |
+
info="Filter languages by geographic region",
|
| 283 |
+
)
|
| 284 |
+
language_support_filter = gr.Dropdown(
|
| 285 |
+
choices=[("All", "All"), ("Multilingual", "Multilingual"), ("English Only", "English-only")],
|
| 286 |
+
multiselect=False,
|
| 287 |
+
label="Model Language support",
|
| 288 |
+
value="All",
|
| 289 |
+
allow_custom_value=False,
|
| 290 |
+
info="All models, multilingual or English-only",
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
view_mode_radio = gr.Radio(
|
| 294 |
+
choices=list(VIEW_MODE_COLUMNS.keys()),
|
| 295 |
+
value=DEFAULT_VIEW_MODE,
|
| 296 |
+
label="Columns show",
|
| 297 |
+
interactive=True,
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
apply_filters_button = gr.Button("Update dashboard", variant="primary")
|
| 301 |
+
|
| 302 |
+
gr.Markdown(
|
| 303 |
+
"💡 **Leaderboard Guide:** *Languages are ordered alphabetically.* Model families are **ranked from left to right by the average performance across languages**. *Click on filter & customize above to filter languages or model.*",
|
| 304 |
+
elem_classes="markdown-text"
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
with gr.Tabs(elem_classes="metric-tabs"):
|
| 308 |
+
with gr.TabItem("CER ⬇️"):
|
| 309 |
+
gr.Markdown("📉 **Lower is better** — Character Error Rate measures the percentage of characters incorrectly transcribed. This is especially important for languages with rich word forms, where meaning is built by combining word parts, therefore errors at the character level can significantly impact meaning.<br><br>*See 📊 Visualizations tab for CER Performance chart.*", elem_classes="metric-note")
|
| 310 |
+
cer_table = gr.Dataframe(
|
| 311 |
+
value=ASR_INITIAL_DATAFRAMES.get("cer", pd.DataFrame()),
|
| 312 |
+
elem_id="cer-leaderboard-table",
|
| 313 |
+
interactive=False,
|
| 314 |
+
wrap=False,
|
| 315 |
+
max_height=800,
|
| 316 |
+
)
|
| 317 |
+
with gr.TabItem("WER ⬇️"):
|
| 318 |
+
gr.Markdown("📉 **Lower is better** — Word Error Rate measures the percentage of words incorrectly transcribed.<br><br>*See 📊 Visualizations tab for WER Performance chart.*", elem_classes="metric-note")
|
| 319 |
+
wer_table = gr.Dataframe(
|
| 320 |
+
value=ASR_INITIAL_DATAFRAMES.get("wer", pd.DataFrame()),
|
| 321 |
+
elem_id="wer-leaderboard-table",
|
| 322 |
+
interactive=False,
|
| 323 |
+
wrap=False,
|
| 324 |
+
max_height=800,
|
| 325 |
+
)
|
| 326 |
+
with gr.TabItem("RTFx ⬆️"):
|
| 327 |
+
gr.Markdown("📈 **Higher is better** — Inverse Real-Time Factor (RTFx) measures how fast transcription runs relative to real-time audio duration.<br><br>*See 📊 Visualizations tab for Speed vs Accuracy chart.*", elem_classes="metric-note")
|
| 328 |
+
rtfx_table = gr.Dataframe(
|
| 329 |
+
value=ASR_INITIAL_DATAFRAMES.get("rtfx", pd.DataFrame()),
|
| 330 |
+
elem_id="rtfx-leaderboard-table",
|
| 331 |
+
interactive=False,
|
| 332 |
+
wrap=False,
|
| 333 |
+
max_height=800,
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
def update_metric_dataframes_with_telemetry(models, languages, dataset_groups, view_mode, african_regions, language_support):
|
| 337 |
+
"""Wrapper that adds telemetry tracking to filter updates."""
|
| 338 |
+
language_support_selected = _language_support_to_list(language_support)
|
| 339 |
+
track_event("dashboard_update", {
|
| 340 |
+
# Counts for quick filtering
|
| 341 |
+
"models_count": len(models) if models else 0,
|
| 342 |
+
"languages_count": len(languages) if languages else 0,
|
| 343 |
+
"datasets_count": len(dataset_groups) if dataset_groups else 0,
|
| 344 |
+
"regions_count": len(african_regions) if african_regions else 0,
|
| 345 |
+
"language_support_count": len(language_support_selected),
|
| 346 |
+
# Actual filter values (limit to prevent huge payloads)
|
| 347 |
+
"models": ",".join(models[:10]) if models else "",
|
| 348 |
+
"languages": ",".join(languages[:10]) if languages else "",
|
| 349 |
+
"dataset_groups": ",".join(dataset_groups[:10]) if dataset_groups else "",
|
| 350 |
+
"african_regions": ",".join(african_regions[:10]) if african_regions else "",
|
| 351 |
+
"language_support": ",".join(language_support_selected),
|
| 352 |
+
"view_mode": view_mode or "",
|
| 353 |
+
"tab": "asr_metrics",
|
| 354 |
+
})
|
| 355 |
+
|
| 356 |
+
return update_metric_dataframes(models, languages, dataset_groups, view_mode, african_regions, language_support_selected)
|
| 357 |
+
|
| 358 |
+
apply_filters_button.click(
|
| 359 |
+
update_metric_dataframes_with_telemetry,
|
| 360 |
+
inputs=[
|
| 361 |
+
model_filter,
|
| 362 |
+
language_filter,
|
| 363 |
+
dataset_filter,
|
| 364 |
+
view_mode_radio,
|
| 365 |
+
african_region_filter,
|
| 366 |
+
language_support_filter,
|
| 367 |
+
],
|
| 368 |
+
outputs=[cer_table, wer_table, rtfx_table],
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
with gr.TabItem("📊 Visualizations", elem_id="visualizations-tab", id=1):
|
| 372 |
+
if ASR_RESULTS_ERROR:
|
| 373 |
+
gr.Markdown(f"⚠️ {ASR_RESULTS_ERROR}")
|
| 374 |
+
else:
|
| 375 |
+
gr.Markdown(
|
| 376 |
+
"### Explore key visualizations revealing insights about model performance, "
|
| 377 |
+
"accuracy and efficiency trade-offs and cross-metric correlations across the benchmark.",
|
| 378 |
+
elem_classes="markdown-text"
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
# Chart Gallery - Quick Navigation
|
| 382 |
+
gr.Markdown("### 📋 Available Charts", elem_classes="markdown-text")
|
| 383 |
+
gr.HTML(chart_gallery_html)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
# Plot 1: CER Performance Leaderboard (with language filter)
|
| 387 |
+
gr.HTML("<div id='cer-leaderboard-section'></div>")
|
| 388 |
+
gr.Markdown("## 🏆 CER Performance Leaderboard", elem_classes="markdown-text")
|
| 389 |
+
gr.Markdown("*Shows model families by default. Select language(s) above to see top 15 individual models. CER is the preferred metric for low-resource and agglutinative languages.*", elem_classes="markdown-text")
|
| 390 |
+
with gr.Accordion("ℹ️ How to interpret this chart", open=False):
|
| 391 |
+
gr.Markdown(INTERPRETATIONS['cer_leaderboard'])
|
| 392 |
+
viz_language_filter_cer_leaderboard = gr.Dropdown(
|
| 393 |
+
choices=ASR_FILTER_OPTIONS["language"],
|
| 394 |
+
multiselect=True,
|
| 395 |
+
label="Filter by Language",
|
| 396 |
+
value=None,
|
| 397 |
+
allow_custom_value=False,
|
| 398 |
+
)
|
| 399 |
+
plot_cer_leaderboard = gr.Plot(value=create_cer_leaderboard(ASR_RESULTS_DF), elem_classes="viz-scroll-plot")
|
| 400 |
+
gr.Markdown("💡 *For the best experience, view these charts on a desktop or tablet in landscape mode.*", elem_classes="markdown-text")
|
| 401 |
+
|
| 402 |
+
# Plot 2: WER Performance Leaderboard (with language filter)
|
| 403 |
+
gr.HTML("<div id='leaderboard-section'></div>")
|
| 404 |
+
gr.Markdown("## 📊 WER Performance Leaderboard", elem_classes="markdown-text")
|
| 405 |
+
gr.Markdown("*Shows model families by default. Select language(s) above to see top 15 individual models.*", elem_classes="markdown-text")
|
| 406 |
+
with gr.Accordion("ℹ️ How to interpret this chart", open=False):
|
| 407 |
+
gr.Markdown(INTERPRETATIONS['leaderboard'])
|
| 408 |
+
viz_language_filter_leaderboard = gr.Dropdown(
|
| 409 |
+
choices=ASR_FILTER_OPTIONS["language"],
|
| 410 |
+
multiselect=True,
|
| 411 |
+
label="Filter by Language",
|
| 412 |
+
value=None,
|
| 413 |
+
allow_custom_value=False,
|
| 414 |
+
)
|
| 415 |
+
plot_leaderboard = gr.Plot(value=create_model_leaderboard(ASR_RESULTS_DF), elem_classes="viz-scroll-plot")
|
| 416 |
+
gr.Markdown("💡 *For the best experience, view these charts on a desktop or tablet in landscape mode.*", elem_classes="markdown-text")
|
| 417 |
+
|
| 418 |
+
# Plot 3: Speed vs Accuracy Tradeoff (Individual Models only)
|
| 419 |
+
gr.HTML("<div id='speed-accuracy-section'></div>")
|
| 420 |
+
gr.Markdown("## 🚀 Speed versus Accuracy Tradeoffs", elem_classes="markdown-text")
|
| 421 |
+
with gr.Accordion("ℹ️ How to interpret this chart", open=False):
|
| 422 |
+
gr.Markdown(INTERPRETATIONS['speed_accuracy'])
|
| 423 |
+
viz_language_filter_speed = gr.Dropdown(
|
| 424 |
+
choices=ASR_FILTER_OPTIONS["language"],
|
| 425 |
+
multiselect=True,
|
| 426 |
+
label="Filter by Language",
|
| 427 |
+
value=None,
|
| 428 |
+
allow_custom_value=False,
|
| 429 |
+
)
|
| 430 |
+
plot_speed_accuracy = gr.Plot(value=create_speed_accuracy_scatter(ASR_RESULTS_DF, view_mode="individual_model"), elem_classes="viz-scroll-plot")
|
| 431 |
+
gr.Markdown("💡 *For the best experience, view these charts on a desktop or tablet in landscape mode.*", elem_classes="markdown-text")
|
| 432 |
+
|
| 433 |
+
# Plot 4: CER vs WER Correlation (with language filter)
|
| 434 |
+
gr.HTML("<div id='correlation-section'></div>")
|
| 435 |
+
gr.Markdown("## 🔍 CER versus WER Correlation", elem_classes="markdown-text")
|
| 436 |
+
with gr.Accordion("ℹ️ How to interpret this chart", open=False):
|
| 437 |
+
gr.Markdown(INTERPRETATIONS['correlation'])
|
| 438 |
+
with gr.Row():
|
| 439 |
+
viz_language_filter_correlation = gr.Dropdown(
|
| 440 |
+
choices=ASR_FILTER_OPTIONS["language"],
|
| 441 |
+
multiselect=True,
|
| 442 |
+
label="Filter by Language",
|
| 443 |
+
value=["Swahili"],
|
| 444 |
+
allow_custom_value=False,
|
| 445 |
+
scale=3,
|
| 446 |
+
)
|
| 447 |
+
top_n_models_slider = gr.Slider(
|
| 448 |
+
minimum=0,
|
| 449 |
+
maximum=20,
|
| 450 |
+
step=1,
|
| 451 |
+
value=0,
|
| 452 |
+
label="Top N Models (0 = all)",
|
| 453 |
+
scale=1,
|
| 454 |
+
)
|
| 455 |
+
plot_correlation = gr.Plot(value=create_wer_cer_correlation(ASR_RESULTS_DF, languages=["Swahili"]), elem_classes="viz-scroll-plot")
|
| 456 |
+
gr.Markdown("💡 *For the best experience, view these charts on a desktop or tablet in landscape mode.*", elem_classes="markdown-text")
|
| 457 |
+
|
| 458 |
+
# Visualization update functions with telemetry
|
| 459 |
+
def update_wer_leaderboard_with_telemetry(langs):
|
| 460 |
+
"""Update WER leaderboard with telemetry tracking."""
|
| 461 |
+
track_visualization_view("wer_leaderboard", languages=langs if langs else None)
|
| 462 |
+
return create_model_leaderboard(ASR_RESULTS_DF, languages=langs if langs else None)
|
| 463 |
+
|
| 464 |
+
def update_cer_leaderboard_with_telemetry(langs):
|
| 465 |
+
"""Update CER leaderboard with telemetry tracking."""
|
| 466 |
+
track_visualization_view("cer_leaderboard", languages=langs if langs else None)
|
| 467 |
+
return create_cer_leaderboard(ASR_RESULTS_DF, languages=langs if langs else None)
|
| 468 |
+
|
| 469 |
+
def update_speed_accuracy_with_telemetry(langs):
|
| 470 |
+
"""Update speed-accuracy chart with telemetry tracking."""
|
| 471 |
+
track_visualization_view("speed_accuracy", languages=langs if langs else None)
|
| 472 |
+
return create_speed_accuracy_scatter(ASR_RESULTS_DF, view_mode="individual_model", languages=langs if langs else None)
|
| 473 |
+
|
| 474 |
+
def update_correlation_with_telemetry(langs, top_n):
|
| 475 |
+
"""Update correlation chart with telemetry tracking."""
|
| 476 |
+
track_visualization_view(
|
| 477 |
+
"wer_cer_correlation",
|
| 478 |
+
properties={"top_n_models": str(int(top_n)) if top_n else "all"},
|
| 479 |
+
languages=langs if langs else None
|
| 480 |
+
)
|
| 481 |
+
return create_wer_cer_correlation(
|
| 482 |
+
ASR_RESULTS_DF,
|
| 483 |
+
languages=langs if langs else None,
|
| 484 |
+
top_n_models=int(top_n) if top_n else None
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
# Update plots when language filters change
|
| 488 |
+
viz_language_filter_leaderboard.change(
|
| 489 |
+
fn=update_wer_leaderboard_with_telemetry,
|
| 490 |
+
inputs=[viz_language_filter_leaderboard],
|
| 491 |
+
outputs=[plot_leaderboard]
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
viz_language_filter_cer_leaderboard.change(
|
| 495 |
+
fn=update_cer_leaderboard_with_telemetry,
|
| 496 |
+
inputs=[viz_language_filter_cer_leaderboard],
|
| 497 |
+
outputs=[plot_cer_leaderboard]
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
viz_language_filter_speed.change(
|
| 501 |
+
fn=update_speed_accuracy_with_telemetry,
|
| 502 |
+
inputs=[viz_language_filter_speed],
|
| 503 |
+
outputs=[plot_speed_accuracy]
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
# Update correlation chart when language or top N changes
|
| 507 |
+
viz_language_filter_correlation.change(
|
| 508 |
+
fn=update_correlation_with_telemetry,
|
| 509 |
+
inputs=[viz_language_filter_correlation, top_n_models_slider],
|
| 510 |
+
outputs=[plot_correlation]
|
| 511 |
+
)
|
| 512 |
+
top_n_models_slider.change(
|
| 513 |
+
fn=update_correlation_with_telemetry,
|
| 514 |
+
inputs=[viz_language_filter_correlation, top_n_models_slider],
|
| 515 |
+
outputs=[plot_correlation]
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
with gr.TabItem("📝 Benchmark Inputs", elem_id="llm-benchmark-tab-table", id=2):
|
| 519 |
+
gr.Markdown(get_llm_benchmarks_text(ASR_RESULTS_DF), elem_classes="markdown-text")
|
| 520 |
+
|
| 521 |
+
gr.Markdown("---", elem_classes="markdown-text")
|
| 522 |
+
gr.Markdown("## 🗺️ Language Coverage", elem_classes="markdown-text")
|
| 523 |
+
gr.Markdown(
|
| 524 |
+
"Explore where each language in PazaBench is spoken across Africa and see the sample distribution by language.",
|
| 525 |
+
elem_classes="markdown-text"
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
# Row 1: Language selector map and PazaBench overview with sample distribution
|
| 529 |
+
gr.Markdown("#### 🔍 Explore by Language", elem_classes="markdown-text")
|
| 530 |
+
with gr.Row(elem_id="language-coverage-row"):
|
| 531 |
+
with gr.Column(scale=1):
|
| 532 |
+
benchmark_language_filter = gr.Dropdown(
|
| 533 |
+
choices=get_all_languages(ASR_RESULTS_DF),
|
| 534 |
+
value=None,
|
| 535 |
+
label="Select a Language",
|
| 536 |
+
multiselect=True,
|
| 537 |
+
allow_custom_value=False,
|
| 538 |
+
info="Select languages to see where they are spoken"
|
| 539 |
+
)
|
| 540 |
+
language_location_map = gr.Plot(value=create_language_location_map())
|
| 541 |
+
gr.Markdown("💡 *Select a language to see the countries where it is spoken.*", elem_classes="markdown-text")
|
| 542 |
+
with gr.Column(scale=2):
|
| 543 |
+
language_sample_info = gr.HTML(
|
| 544 |
+
value=get_language_sample_info(None, ASR_RESULTS_DF),
|
| 545 |
+
)
|
| 546 |
+
# Sample Distribution by Language (stacked under PazaBench overview)
|
| 547 |
+
gr.Markdown("#### 📊 Sample Distribution by Language", elem_classes="markdown-text")
|
| 548 |
+
language_coverage_plot = gr.Plot(
|
| 549 |
+
value=create_language_coverage_chart(asr_df=ASR_RESULTS_DF),
|
| 550 |
+
elem_id="language-coverage-plot",
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
# Event handlers for language selector with telemetry
|
| 554 |
+
def update_benchmark_language_views(langs):
|
| 555 |
+
"""Update all language-related views at once to avoid infinite loops."""
|
| 556 |
+
# Track telemetry
|
| 557 |
+
if langs:
|
| 558 |
+
track_filter_change("language", langs, tab="benchmark_inputs")
|
| 559 |
+
track_visualization_view("language_location_map", languages=langs)
|
| 560 |
+
track_visualization_view("language_coverage", languages=langs)
|
| 561 |
+
|
| 562 |
+
# Return all three outputs at once
|
| 563 |
+
return (
|
| 564 |
+
create_language_location_map(langs if langs else None),
|
| 565 |
+
get_language_sample_info(langs if langs else None, ASR_RESULTS_DF),
|
| 566 |
+
create_language_coverage_chart(langs if langs else None, asr_df=ASR_RESULTS_DF),
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
benchmark_language_filter.change(
|
| 570 |
+
fn=update_benchmark_language_views,
|
| 571 |
+
inputs=[benchmark_language_filter],
|
| 572 |
+
outputs=[language_location_map, language_sample_info, language_coverage_plot]
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
gr.Markdown(get_llm_benchmarks_datasets_text(ASR_RESULTS_DF), elem_classes="markdown-text")
|
| 576 |
+
|
| 577 |
+
with gr.TabItem("➕ Request Language Eval", elem_id="lang-eval-tab", id=3):
|
| 578 |
+
gr.Markdown(EVALUATION_LANGUAGE_TEXT, elem_classes="markdown-text")
|
| 579 |
+
|
| 580 |
+
with gr.Row():
|
| 581 |
+
dataset_location = gr.Radio(
|
| 582 |
+
choices=["HuggingFace", "Other"],
|
| 583 |
+
value="HuggingFace",
|
| 584 |
+
label="Dataset Location *",
|
| 585 |
+
info="Select where your dataset is hosted"
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
with gr.Row():
|
| 589 |
+
with gr.Column():
|
| 590 |
+
lang_dataset_name = gr.Textbox(
|
| 591 |
+
label="Dataset Name *",
|
| 592 |
+
placeholder="e.g., username/dataset-name",
|
| 593 |
+
info="Full Hugging Face dataset ID",
|
| 594 |
+
visible=True
|
| 595 |
+
)
|
| 596 |
+
lang_dataset_url = gr.Textbox(
|
| 597 |
+
label="Dataset URL *",
|
| 598 |
+
placeholder="https://example.com/dataset",
|
| 599 |
+
info="Full URL to the publicly available dataset",
|
| 600 |
+
visible=False
|
| 601 |
+
)
|
| 602 |
+
lang_dataset_config = gr.Textbox(
|
| 603 |
+
label="Dataset Subset/Language",
|
| 604 |
+
placeholder="default",
|
| 605 |
+
info="Dataset subset or language configuration (leave empty for default)"
|
| 606 |
+
)
|
| 607 |
+
lang_dataset_split = gr.Textbox(
|
| 608 |
+
label="Dataset Split",
|
| 609 |
+
placeholder="test",
|
| 610 |
+
info="Split to evaluate (default: test)"
|
| 611 |
+
)
|
| 612 |
+
with gr.Column():
|
| 613 |
+
lang_license = gr.Textbox(
|
| 614 |
+
label="License *",
|
| 615 |
+
placeholder="e.g., CC-BY-4.0, MIT, Apache-2.0",
|
| 616 |
+
info="License of the dataset (required)"
|
| 617 |
+
)
|
| 618 |
+
lang_audio_column = gr.Textbox(
|
| 619 |
+
label="Audio Column",
|
| 620 |
+
placeholder="audio",
|
| 621 |
+
info="Name of the column containing audio data (optional, default: audio)"
|
| 622 |
+
)
|
| 623 |
+
lang_text_column = gr.Textbox(
|
| 624 |
+
label="Text/Transcript Column",
|
| 625 |
+
placeholder="text",
|
| 626 |
+
info="Name of the column containing transcriptions (optional, default: text)"
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
def toggle_dataset_inputs(location):
|
| 630 |
+
"""Toggle visibility of dataset name vs URL based on location selection."""
|
| 631 |
+
if location == "HuggingFace":
|
| 632 |
+
return gr.update(visible=True), gr.update(visible=False)
|
| 633 |
+
else:
|
| 634 |
+
return gr.update(visible=False), gr.update(visible=True)
|
| 635 |
+
|
| 636 |
+
dataset_location.change(
|
| 637 |
+
toggle_dataset_inputs,
|
| 638 |
+
inputs=[dataset_location],
|
| 639 |
+
outputs=[lang_dataset_name, lang_dataset_url]
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
lang_submit_button = gr.Button("Submit Language Evaluation Request", variant="primary")
|
| 643 |
+
lang_submission_result = gr.Markdown()
|
| 644 |
+
|
| 645 |
+
def submit_language_eval_with_telemetry(
|
| 646 |
+
location, dataset_name, dataset_url, dataset_config, dataset_split,
|
| 647 |
+
audio_column, text_column, license
|
| 648 |
+
):
|
| 649 |
+
"""Wrapper that adds telemetry to submission attempts and resets form on success."""
|
| 650 |
+
from src.telemetry import track_submission_attempt
|
| 651 |
+
|
| 652 |
+
result = add_language_eval_request(
|
| 653 |
+
location, dataset_name, dataset_url, dataset_config, dataset_split,
|
| 654 |
+
audio_column, text_column, license
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
# Determine if submission was successful based on result message
|
| 658 |
+
success = result and ("success" in result.lower() or "submitted" in result.lower())
|
| 659 |
+
error_msg = None if success else result
|
| 660 |
+
|
| 661 |
+
# Use dataset name or URL based on location
|
| 662 |
+
ds_identifier = dataset_name if location == "HuggingFace" else dataset_url
|
| 663 |
+
track_submission_attempt(
|
| 664 |
+
dataset_name=ds_identifier or "unknown",
|
| 665 |
+
success=success,
|
| 666 |
+
error_message=error_msg
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
# On success, reset form fields; on error, keep values so user can fix and retry
|
| 670 |
+
if success:
|
| 671 |
+
# Reset: location, name, url, config, split, audio_col, text_col, license
|
| 672 |
+
return (
|
| 673 |
+
result, "HuggingFace", "", "", "", "", "", "", "",
|
| 674 |
+
gr.update(visible=True), gr.update(visible=False),
|
| 675 |
+
)
|
| 676 |
+
else:
|
| 677 |
+
return (result,) + tuple(gr.update() for _ in range(10))
|
| 678 |
+
|
| 679 |
+
lang_submit_button.click(
|
| 680 |
+
submit_language_eval_with_telemetry,
|
| 681 |
+
[
|
| 682 |
+
dataset_location,
|
| 683 |
+
lang_dataset_name,
|
| 684 |
+
lang_dataset_url,
|
| 685 |
+
lang_dataset_config,
|
| 686 |
+
lang_dataset_split,
|
| 687 |
+
lang_audio_column,
|
| 688 |
+
lang_text_column,
|
| 689 |
+
lang_license,
|
| 690 |
+
],
|
| 691 |
+
[
|
| 692 |
+
lang_submission_result,
|
| 693 |
+
dataset_location,
|
| 694 |
+
lang_dataset_name,
|
| 695 |
+
lang_dataset_url,
|
| 696 |
+
lang_dataset_config,
|
| 697 |
+
lang_dataset_split,
|
| 698 |
+
lang_audio_column,
|
| 699 |
+
lang_text_column,
|
| 700 |
+
lang_license,
|
| 701 |
+
lang_dataset_name, # visibility update
|
| 702 |
+
lang_dataset_url, # visibility update
|
| 703 |
+
],
|
| 704 |
+
)
|
| 705 |
+
|
| 706 |
+
with gr.TabItem("🗓️ Change Log", elem_id="changelog-tab", id=4):
|
| 707 |
+
gr.Markdown(CHANGELOG_HEADER, elem_classes="markdown-text")
|
| 708 |
+
gr.HTML(CHANGELOG_CARDS_HTML)
|
| 709 |
+
|
| 710 |
+
with gr.Row():
|
| 711 |
+
with gr.Accordion("📙 Citation", open=False):
|
| 712 |
+
citation_button = gr.Textbox(
|
| 713 |
+
value=CITATION_BUTTON_TEXT,
|
| 714 |
+
label=CITATION_BUTTON_LABEL,
|
| 715 |
+
lines=20,
|
| 716 |
+
elem_id="citation-button",
|
| 717 |
+
)
|
| 718 |
+
|
| 719 |
+
# Footer with links visible across all pages and tabs
|
| 720 |
+
gr.HTML(footer_html)
|
| 721 |
+
|
| 722 |
+
# Only start scheduler if HuggingFace Hub is enabled
|
| 723 |
+
if HF_ENABLED:
|
| 724 |
+
scheduler = BackgroundScheduler()
|
| 725 |
+
scheduler.add_job(restart_space, "interval", seconds=1800)
|
| 726 |
+
scheduler.start()
|
| 727 |
+
print("Background scheduler started for HuggingFace Space restart")
|
| 728 |
+
else:
|
| 729 |
+
print("HuggingFace Hub disabled, background scheduler not started")
|
| 730 |
+
|
| 731 |
+
# Resolve server port from PORT / WEBSITES_PORT / GRADIO_SERVER_PORT
|
| 732 |
+
port = int(os.environ.get("PORT", os.environ.get("WEBSITES_PORT", os.environ.get("GRADIO_SERVER_PORT", 7860))))
|
| 733 |
+
print(f"Starting Gradio server on port {port}...")
|
| 734 |
+
|
| 735 |
+
demo.queue(
|
| 736 |
+
default_concurrency_limit=40
|
| 737 |
+
).launch(
|
| 738 |
+
server_name="0.0.0.0",
|
| 739 |
+
server_port=port,
|
| 740 |
+
share=True,
|
| 741 |
+
pwa=True,
|
| 742 |
+
)
|
| 743 |
+
|
es-metadata.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
schemaVersion: 0.0.1
|
| 2 |
+
isProduction: true
|
| 3 |
+
accountableOwners:
|
| 4 |
+
service: 6ed18b01-b071-4f02-b219-2224b1f0a421
|
| 5 |
+
routing:
|
| 6 |
+
defaultAreaPath:
|
| 7 |
+
org: msr-africa
|
| 8 |
+
path: Gecko MSR Africa
|
requirements.txt
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiofiles==24.1.0
|
| 2 |
+
aiohappyeyeballs==2.6.1
|
| 3 |
+
aiohttp==3.13.3
|
| 4 |
+
aiosignal==1.4.0
|
| 5 |
+
annotated-doc==0.0.4
|
| 6 |
+
annotated-types==0.7.0
|
| 7 |
+
anyio==4.12.1
|
| 8 |
+
apscheduler==3.11.2
|
| 9 |
+
asgiref==3.11.0
|
| 10 |
+
attrs==25.4.0
|
| 11 |
+
authlib==1.6.6
|
| 12 |
+
azure-core==1.38.0
|
| 13 |
+
azure-core-tracing-opentelemetry==1.0.0b12
|
| 14 |
+
azure-identity==1.25.1
|
| 15 |
+
azure-monitor-opentelemetry==1.8.4
|
| 16 |
+
azure-monitor-opentelemetry-exporter==1.0.0b46
|
| 17 |
+
black==25.12.0
|
| 18 |
+
brotli==1.2.0
|
| 19 |
+
certifi==2026.1.4
|
| 20 |
+
cffi==2.0.0
|
| 21 |
+
charset-normalizer==3.4.4
|
| 22 |
+
click==8.3.1
|
| 23 |
+
cryptography==46.0.3
|
| 24 |
+
datasets==4.4.2
|
| 25 |
+
dill==0.4.0
|
| 26 |
+
evaluate==0.4.6
|
| 27 |
+
fastapi==0.128.0
|
| 28 |
+
ffmpy==1.0.0
|
| 29 |
+
filelock==3.20.2
|
| 30 |
+
frozenlist==1.8.0
|
| 31 |
+
fsspec==2025.10.0
|
| 32 |
+
gradio==6.2.0
|
| 33 |
+
gradio-client==2.0.2
|
| 34 |
+
groovy==0.1.2
|
| 35 |
+
h11==0.16.0
|
| 36 |
+
hf-xet==1.2.0
|
| 37 |
+
httpcore==1.0.9
|
| 38 |
+
httpx==0.28.1
|
| 39 |
+
huggingface-hub==0.36.0
|
| 40 |
+
idna==3.11
|
| 41 |
+
importlib-metadata==8.7.1
|
| 42 |
+
isodate==0.7.2
|
| 43 |
+
itsdangerous==2.2.0
|
| 44 |
+
jinja2==3.1.6
|
| 45 |
+
jiwer==4.0.0
|
| 46 |
+
markdown-it-py==4.0.0
|
| 47 |
+
markupsafe==3.0.3
|
| 48 |
+
mdurl==0.1.2
|
| 49 |
+
msal==1.34.0
|
| 50 |
+
msal-extensions==1.3.1
|
| 51 |
+
msrest==0.7.1
|
| 52 |
+
multidict==6.7.0
|
| 53 |
+
multiprocess==0.70.18
|
| 54 |
+
mypy-extensions==1.1.0
|
| 55 |
+
narwhals==2.15.0
|
| 56 |
+
numpy==2.4.0
|
| 57 |
+
oauthlib==3.3.1
|
| 58 |
+
opentelemetry-api==1.39.0
|
| 59 |
+
opentelemetry-instrumentation==0.60b0
|
| 60 |
+
opentelemetry-instrumentation-asgi==0.60b0
|
| 61 |
+
opentelemetry-instrumentation-dbapi==0.60b0
|
| 62 |
+
opentelemetry-instrumentation-django==0.60b0
|
| 63 |
+
opentelemetry-instrumentation-fastapi==0.60b0
|
| 64 |
+
opentelemetry-instrumentation-flask==0.60b0
|
| 65 |
+
opentelemetry-instrumentation-psycopg2==0.60b0
|
| 66 |
+
opentelemetry-instrumentation-requests==0.60b0
|
| 67 |
+
opentelemetry-instrumentation-urllib==0.60b0
|
| 68 |
+
opentelemetry-instrumentation-urllib3==0.60b0
|
| 69 |
+
opentelemetry-instrumentation-wsgi==0.60b0
|
| 70 |
+
opentelemetry-resource-detector-azure==0.1.5
|
| 71 |
+
opentelemetry-sdk==1.39.0
|
| 72 |
+
opentelemetry-semantic-conventions==0.60b0
|
| 73 |
+
opentelemetry-util-http==0.60b0
|
| 74 |
+
orjson==3.11.5
|
| 75 |
+
packaging==25.0
|
| 76 |
+
pandas==2.3.3
|
| 77 |
+
pathspec==1.0.2
|
| 78 |
+
patsy==1.0.2
|
| 79 |
+
pillow==11.3.0
|
| 80 |
+
platformdirs==4.5.1
|
| 81 |
+
plotly==6.5.1
|
| 82 |
+
propcache==0.4.1
|
| 83 |
+
psutil==7.2.1
|
| 84 |
+
pyarrow==22.0.0
|
| 85 |
+
pycparser==2.23
|
| 86 |
+
pydantic==2.12.3
|
| 87 |
+
pydantic-core==2.41.4
|
| 88 |
+
pydub==0.25.1
|
| 89 |
+
pygments==2.19.2
|
| 90 |
+
pyjwt==2.10.1
|
| 91 |
+
python-dateutil==2.9.0.post0
|
| 92 |
+
python-dotenv==1.2.1
|
| 93 |
+
python-multipart==0.0.21
|
| 94 |
+
pytokens==0.3.0
|
| 95 |
+
pytz==2025.2
|
| 96 |
+
pyyaml==6.0.3
|
| 97 |
+
rapidfuzz==3.14.3
|
| 98 |
+
regex==2025.11.3
|
| 99 |
+
requests==2.32.5
|
| 100 |
+
requests-oauthlib==2.0.0
|
| 101 |
+
rich==14.2.0
|
| 102 |
+
safehttpx==0.1.7
|
| 103 |
+
safetensors==0.7.0
|
| 104 |
+
scipy==1.16.3
|
| 105 |
+
semantic-version==2.10.0
|
| 106 |
+
sentencepiece==0.2.1
|
| 107 |
+
shellingham==1.5.4
|
| 108 |
+
six==1.17.0
|
| 109 |
+
starlette==0.50.0
|
| 110 |
+
statsmodels==0.14.6
|
| 111 |
+
tokenizers==0.22.2
|
| 112 |
+
tomlkit==0.13.3
|
| 113 |
+
tqdm==4.67.1
|
| 114 |
+
transformers==4.57.3
|
| 115 |
+
typer==0.21.1
|
| 116 |
+
typing-extensions==4.15.0
|
| 117 |
+
typing-inspection==0.4.2
|
| 118 |
+
tzdata==2025.3
|
| 119 |
+
tzlocal==5.3.1
|
| 120 |
+
urllib3==2.6.3
|
| 121 |
+
uvicorn==0.40.0
|
| 122 |
+
wrapt==1.17.3
|
| 123 |
+
xxhash==3.6.0
|
| 124 |
+
yarl==1.22.0
|
| 125 |
+
zipp==3.23.0
|
src/about.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from collections.abc import Iterable
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
# Your leaderboard name
|
| 9 |
+
TITLE = """<h1 align="center" id="space-title">PazaBench Leaderboard</h1>"""
|
| 10 |
+
|
| 11 |
+
_DATA_DIR = Path(__file__).parent / "data"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _load_json(name: str):
|
| 15 |
+
"""Load a JSON data file bundled under ``src/data/``."""
|
| 16 |
+
with open(_DATA_DIR / name, encoding="utf-8") as data_file:
|
| 17 |
+
return json.load(data_file)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _load_text(name: str) -> str:
|
| 21 |
+
"""Load a text/HTML template bundled under ``src/data/``."""
|
| 22 |
+
with open(_DATA_DIR / name, encoding="utf-8") as data_file:
|
| 23 |
+
return data_file.read()
|
| 24 |
+
|
| 25 |
+
# Dataset-group and model-family metadata are stored in src/data/*.json
|
| 26 |
+
DATASET_GROUP_METADATA = _load_json("dataset_group_metadata.json")
|
| 27 |
+
|
| 28 |
+
MODEL_FAMILY_METADATA = _load_json("model_family_metadata.json")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _is_loaded_frame(asr_results_df: Any | None) -> bool:
|
| 32 |
+
return asr_results_df is not None and not getattr(asr_results_df, "empty", True)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _format_count(value: int | float | None) -> str:
|
| 36 |
+
if value is None:
|
| 37 |
+
return "0"
|
| 38 |
+
return f"{int(value):,}"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _to_int(value: Any) -> int:
|
| 42 |
+
try:
|
| 43 |
+
return int(float(value))
|
| 44 |
+
except (TypeError, ValueError):
|
| 45 |
+
return 0
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _plural(word: str, count: int) -> str:
|
| 49 |
+
if count != 1 and word.endswith("family"):
|
| 50 |
+
return f"{word[:-1]}ies"
|
| 51 |
+
if count != 1 and word.endswith("y"):
|
| 52 |
+
return f"{word[:-1]}ies"
|
| 53 |
+
return word if count == 1 else f"{word}s"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _sorted_unique(values: Iterable[Any]) -> list[str]:
|
| 57 |
+
return sorted({str(value) for value in values if str(value).strip() and str(value) != "Unknown"})
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _format_inline_list(values: list[str], max_items: int = 8) -> str:
|
| 61 |
+
if not values:
|
| 62 |
+
return "not available"
|
| 63 |
+
visible = values[:max_items]
|
| 64 |
+
rendered = ", ".join(visible)
|
| 65 |
+
remaining = len(values) - len(visible)
|
| 66 |
+
if remaining > 0:
|
| 67 |
+
rendered = f"{rendered}, and {_format_count(remaining)} more"
|
| 68 |
+
return rendered
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _format_language_summary(languages: list[str]) -> str:
|
| 72 |
+
language_count = len(languages)
|
| 73 |
+
if language_count == 0:
|
| 74 |
+
return "Not available"
|
| 75 |
+
language_list = _format_inline_list(languages, max_items=12)
|
| 76 |
+
return f"{language_list} (**{_format_count(language_count)} {_plural('language', language_count)}**)"
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _split_level_frame(asr_results_df: Any | None):
|
| 80 |
+
if not _is_loaded_frame(asr_results_df):
|
| 81 |
+
return None
|
| 82 |
+
|
| 83 |
+
required_columns = ["dataset_group", "split", "language"]
|
| 84 |
+
if any(column not in asr_results_df.columns for column in required_columns):
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
columns = required_columns + (["num_samples"] if "num_samples" in asr_results_df.columns else [])
|
| 88 |
+
split_frame = asr_results_df.loc[:, columns].copy()
|
| 89 |
+
split_frame = split_frame.dropna(subset=required_columns)
|
| 90 |
+
split_frame = split_frame[split_frame["language"] != "Unknown"]
|
| 91 |
+
|
| 92 |
+
if "num_samples" in split_frame.columns:
|
| 93 |
+
split_frame["num_samples"] = split_frame["num_samples"].apply(_to_int)
|
| 94 |
+
return (
|
| 95 |
+
split_frame
|
| 96 |
+
.groupby(required_columns, dropna=False, as_index=False)["num_samples"]
|
| 97 |
+
.max()
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return split_frame.drop_duplicates(required_columns)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def get_benchmark_summary(asr_results_df: Any | None = None) -> dict[str, int]:
|
| 104 |
+
"""Return benchmark coverage counts derived from the loaded results dataframe."""
|
| 105 |
+
if not _is_loaded_frame(asr_results_df):
|
| 106 |
+
metadata_languages = {
|
| 107 |
+
language
|
| 108 |
+
for metadata in DATASET_GROUP_METADATA.values()
|
| 109 |
+
for language in metadata.get("languages", [])
|
| 110 |
+
}
|
| 111 |
+
return {
|
| 112 |
+
"dataset_groups": len(DATASET_GROUP_METADATA),
|
| 113 |
+
"splits": 0,
|
| 114 |
+
"languages": len(metadata_languages),
|
| 115 |
+
"samples": 0,
|
| 116 |
+
"model_families": len(MODEL_FAMILY_METADATA),
|
| 117 |
+
"models": 0,
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
split_frame = _split_level_frame(asr_results_df)
|
| 121 |
+
sample_count = 0
|
| 122 |
+
if split_frame is not None and "num_samples" in split_frame.columns:
|
| 123 |
+
sample_count = int(split_frame["num_samples"].sum())
|
| 124 |
+
|
| 125 |
+
return {
|
| 126 |
+
"dataset_groups": int(asr_results_df["dataset_group"].nunique()) if "dataset_group" in asr_results_df.columns else 0,
|
| 127 |
+
"splits": int(asr_results_df["split"].nunique()) if "split" in asr_results_df.columns else 0,
|
| 128 |
+
"languages": int(asr_results_df["language"].nunique()) if "language" in asr_results_df.columns else 0,
|
| 129 |
+
"samples": sample_count,
|
| 130 |
+
"model_families": int(asr_results_df["model_family"].nunique()) if "model_family" in asr_results_df.columns else 0,
|
| 131 |
+
"models": int(asr_results_df["model"].nunique()) if "model" in asr_results_df.columns else 0,
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _dataset_link(dataset_group: str) -> str:
|
| 136 |
+
metadata = DATASET_GROUP_METADATA.get(dataset_group, {})
|
| 137 |
+
url = metadata.get("url")
|
| 138 |
+
return f"[{dataset_group}]({url})" if url else dataset_group
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _model_link(model_id: str) -> str:
|
| 142 |
+
return f"[{model_id}](https://huggingface.co/{model_id})"
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _family_display_name(model_family: str) -> str:
|
| 146 |
+
metadata = MODEL_FAMILY_METADATA.get(model_family)
|
| 147 |
+
if metadata:
|
| 148 |
+
return metadata["display_name"]
|
| 149 |
+
return model_family.replace("_", " ").replace("-", " ").title()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _family_description(model_family: str) -> str:
|
| 153 |
+
metadata = MODEL_FAMILY_METADATA.get(model_family)
|
| 154 |
+
return metadata["description"] if metadata else "ASR model family in the loaded benchmark results."
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_dataset_group_languages(dataset_group: str, asr_results_df: Any | None = None) -> list[str]:
|
| 158 |
+
"""Return list of languages for a dataset group, preferring loaded results over static metadata."""
|
| 159 |
+
split_frame = _split_level_frame(asr_results_df)
|
| 160 |
+
if split_frame is not None:
|
| 161 |
+
dataset_rows = split_frame[split_frame["dataset_group"] == dataset_group]
|
| 162 |
+
if not dataset_rows.empty:
|
| 163 |
+
return _sorted_unique(dataset_rows["language"].tolist())
|
| 164 |
+
|
| 165 |
+
metadata = DATASET_GROUP_METADATA.get(dataset_group)
|
| 166 |
+
if metadata:
|
| 167 |
+
return _sorted_unique(metadata.get("languages", []))
|
| 168 |
+
return []
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def get_dataset_group_label(dataset_group: str, asr_results_df: Any | None = None) -> str:
|
| 172 |
+
"""Return a formatted label with description and language count."""
|
| 173 |
+
meta = DATASET_GROUP_METADATA.get(dataset_group)
|
| 174 |
+
languages = get_dataset_group_languages(dataset_group, asr_results_df)
|
| 175 |
+
if meta:
|
| 176 |
+
language_count = len(languages)
|
| 177 |
+
if language_count > 0:
|
| 178 |
+
return f"{dataset_group} ({meta['description']}, {_format_count(language_count)} {_plural('language', language_count)})"
|
| 179 |
+
return f"{dataset_group} ({meta['description']})"
|
| 180 |
+
return dataset_group
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def get_introduction_text(asr_results_df: Any | None = None) -> str:
|
| 184 |
+
"""Build the homepage introduction with live counts."""
|
| 185 |
+
summary = get_benchmark_summary(asr_results_df)
|
| 186 |
+
if _is_loaded_frame(asr_results_df):
|
| 187 |
+
launch_sentence = (
|
| 188 |
+
f"Launching with **{_format_count(summary['languages'])} African "
|
| 189 |
+
f"{_plural('Language', summary['languages'])}** across "
|
| 190 |
+
f"**{_format_count(summary['models'])} State-of-the-Art ASR** and **Language Models**, "
|
| 191 |
+
"PazaBench compares three key metrics: **Character Error Rate (CER)**, "
|
| 192 |
+
"**Word Error Rate (WER)**, and **RTFx (Inverse Real-Time Factor)**."
|
| 193 |
+
)
|
| 194 |
+
else:
|
| 195 |
+
launch_sentence = (
|
| 196 |
+
"PazaBench compares three key metrics: **Character Error Rate (CER)**, "
|
| 197 |
+
"**Word Error Rate (WER)**, and **RTFx (Inverse Real-Time Factor)**."
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
return f"""
|
| 201 |
+
The PazaBench Leaderboard is an Automatic Speech Recognition (ASR) benchmark for low-resource languages developed by the **[Microsoft Research Africa, Nairobi Lab](https://www.microsoft.com/en-us/research/lab/microsoft-research-lab-africa-nairobi/)**. {launch_sentence}
|
| 202 |
+
"""
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def get_llm_benchmarks_text(asr_results_df: Any | None = None) -> str:
|
| 206 |
+
"""Build the Benchmark Inputs overview from the live results."""
|
| 207 |
+
summary = get_benchmark_summary(asr_results_df)
|
| 208 |
+
|
| 209 |
+
if _is_loaded_frame(asr_results_df) and summary["dataset_groups"]:
|
| 210 |
+
dataset_sentence = (
|
| 211 |
+
f"The PazaBench evaluation dataset is unified from **{_format_count(summary['dataset_groups'])} "
|
| 212 |
+
f"{_plural('dataset', summary['dataset_groups'])}** listed below. It captures "
|
| 213 |
+
f"**{_format_count(summary['splits'])} test {_plural('split', summary['splits'])}** across the listed "
|
| 214 |
+
f"**{_format_count(summary['languages'])} {_plural('language', summary['languages'])}** and adds up to "
|
| 215 |
+
f"**{_format_count(summary['samples'])} {_plural('sample', summary['samples'])}**. The dataset consists of "
|
| 216 |
+
"16 kHz mono speech audio recordings with aligned transcriptions and accompanying split-level metadata. "
|
| 217 |
+
"For each language, data from all available sources are unified to provide a balanced measure of model performance."
|
| 218 |
+
)
|
| 219 |
+
else:
|
| 220 |
+
dataset_sentence = (
|
| 221 |
+
"The PazaBench evaluation dataset unifies public speech datasets into benchmark-ready "
|
| 222 |
+
"speech/transcript pairs with aligned transcriptions and split-level metadata."
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
return f"""
|
| 226 |
+
## PazaBench Inputs
|
| 227 |
+
|
| 228 |
+
### Evaluation Dataset
|
| 229 |
+
|
| 230 |
+
{dataset_sentence}
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _build_dataset_table(asr_results_df: Any | None = None) -> str:
|
| 235 |
+
split_frame = _split_level_frame(asr_results_df)
|
| 236 |
+
if split_frame is not None:
|
| 237 |
+
dataset_groups = _sorted_unique(split_frame["dataset_group"].tolist())
|
| 238 |
+
else:
|
| 239 |
+
dataset_groups = sorted(DATASET_GROUP_METADATA)
|
| 240 |
+
|
| 241 |
+
group_rows = []
|
| 242 |
+
for dataset_group in dataset_groups:
|
| 243 |
+
metadata = DATASET_GROUP_METADATA.get(dataset_group, {})
|
| 244 |
+
languages = get_dataset_group_languages(dataset_group, asr_results_df)
|
| 245 |
+
sample_count = None
|
| 246 |
+
if split_frame is not None:
|
| 247 |
+
dataset_rows = split_frame[split_frame["dataset_group"] == dataset_group]
|
| 248 |
+
if "num_samples" in dataset_rows.columns:
|
| 249 |
+
sample_count = int(dataset_rows["num_samples"].sum())
|
| 250 |
+
group_rows.append((dataset_group, metadata, languages, sample_count))
|
| 251 |
+
|
| 252 |
+
# Order by Total Samples descending.
|
| 253 |
+
group_rows.sort(key=lambda row: row[3] if row[3] is not None else -1, reverse=True)
|
| 254 |
+
|
| 255 |
+
rows = [
|
| 256 |
+
"| Dataset | Description | Languages | Total Samples | License |",
|
| 257 |
+
"|---------|-------------|-----------|---------------|---------|",
|
| 258 |
+
]
|
| 259 |
+
for dataset_group, metadata, languages, sample_count in group_rows:
|
| 260 |
+
rows.append(
|
| 261 |
+
"| "
|
| 262 |
+
+ " | ".join([
|
| 263 |
+
_dataset_link(dataset_group),
|
| 264 |
+
str(metadata.get("description", "Benchmark speech dataset")),
|
| 265 |
+
_format_language_summary(languages),
|
| 266 |
+
_format_count(sample_count) if sample_count is not None else "Not loaded",
|
| 267 |
+
str(metadata.get("license", "See dataset card")),
|
| 268 |
+
])
|
| 269 |
+
+ " |"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
return "\n".join(rows)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def _build_model_table(asr_results_df: Any | None = None) -> str:
|
| 276 |
+
rows = [
|
| 277 |
+
"| Model Family | Model |",
|
| 278 |
+
"|--------------|-------|",
|
| 279 |
+
]
|
| 280 |
+
|
| 281 |
+
if _is_loaded_frame(asr_results_df) and {"model_family", "model"}.issubset(asr_results_df.columns):
|
| 282 |
+
grouped = (
|
| 283 |
+
asr_results_df.loc[:, ["model_family", "model"]]
|
| 284 |
+
.dropna()
|
| 285 |
+
.drop_duplicates()
|
| 286 |
+
.sort_values(["model_family", "model"])
|
| 287 |
+
.groupby("model_family", sort=True)["model"]
|
| 288 |
+
.apply(list)
|
| 289 |
+
)
|
| 290 |
+
families = grouped.index.tolist()
|
| 291 |
+
else:
|
| 292 |
+
grouped = {}
|
| 293 |
+
families = sorted(MODEL_FAMILY_METADATA)
|
| 294 |
+
|
| 295 |
+
# Order: Paza first, then alphabetical by display name.
|
| 296 |
+
families = sorted(families, key=lambda family: _family_display_name(family).lower())
|
| 297 |
+
families = (
|
| 298 |
+
[family for family in families if _family_display_name(family).lower().startswith("paza")]
|
| 299 |
+
+ [family for family in families if not _family_display_name(family).lower().startswith("paza")]
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
for model_family in families:
|
| 303 |
+
models = grouped[model_family] if hasattr(grouped, "__getitem__") and model_family in grouped else []
|
| 304 |
+
model_links = ", ".join(_model_link(model) for model in models) if models else "Generated from loaded results"
|
| 305 |
+
rows.append(
|
| 306 |
+
"| "
|
| 307 |
+
+ " | ".join([
|
| 308 |
+
_family_display_name(model_family),
|
| 309 |
+
model_links,
|
| 310 |
+
])
|
| 311 |
+
+ " |"
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
return "\n".join(rows)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def get_llm_benchmarks_datasets_text(asr_results_df: Any | None = None) -> str:
|
| 318 |
+
"""Build the dataset and model inventory tables."""
|
| 319 |
+
summary = get_benchmark_summary(asr_results_df)
|
| 320 |
+
dataset_table = _build_dataset_table(asr_results_df)
|
| 321 |
+
model_table = _build_model_table(asr_results_df)
|
| 322 |
+
|
| 323 |
+
if _is_loaded_frame(asr_results_df):
|
| 324 |
+
models_intro = (
|
| 325 |
+
f"PazaBench evaluates **{_format_count(summary['model_families'])} SOTA ASR model "
|
| 326 |
+
f"{_plural('family', summary['model_families'])} across {_format_count(summary['models'])} "
|
| 327 |
+
f"individual {_plural('model', summary['models'])}** listed below:"
|
| 328 |
+
)
|
| 329 |
+
else:
|
| 330 |
+
models_intro = "PazaBench evaluates the SOTA ASR model families listed below:"
|
| 331 |
+
|
| 332 |
+
return f"""
|
| 333 |
+
## Datasets
|
| 334 |
+
|
| 335 |
+
{dataset_table}
|
| 336 |
+
|
| 337 |
+
---
|
| 338 |
+
|
| 339 |
+
## Evaluated Models
|
| 340 |
+
|
| 341 |
+
{models_intro}
|
| 342 |
+
|
| 343 |
+
{model_table}
|
| 344 |
+
|
| 345 |
+
**Whisper Post-Processing:** Whisper model results include a duration-based truncation step to mitigate hallucination and known over-generation behavior.
|
| 346 |
+
|
| 347 |
+
---
|
| 348 |
+
|
| 349 |
+
## Acknowledgements
|
| 350 |
+
|
| 351 |
+
We gratefully acknowledge the dataset creators and leaderboard teams whose contributions made PazaBench possible:
|
| 352 |
+
|
| 353 |
+
**Datasets:** We extend our gratitude to the creators, community contributors, and maintainers of [African Next Voices Kenya](https://huggingface.co/datasets/MCAA1-MSU/anv_data_ke), [African Next Voices South Africa](https://huggingface.co/datasets/dsfsi-anv/za-african-next-voices), [ALFFA](https://openslr.org/25/), [DigiGreen Kikuyu ASR](https://huggingface.co/datasets/DigiGreen/KikuyuASR_trainingdataset), [Google FLEURS](https://huggingface.co/datasets/google/fleurs), [Mozilla Common Voice](https://commonvoice.mozilla.org/), [Naija Voices](https://huggingface.co/datasets/naijavoices/naijavoices-dataset), [WAXAL](https://huggingface.co/datasets/google/WaxalNLP), [Alaleye Fongbe](https://huggingface.co/datasets/alaleye/fon), [Sartify Sukuma Voices](https://huggingface.co/datasets/sartifyllc/Sukuma-Voices), and [CLEAR-Global TWB Voice 1.0](https://huggingface.co/datasets/CLEAR-Global/TWB-Voice-1.0) whose efforts have been invaluable in advancing African languages speech data.
|
| 354 |
+
|
| 355 |
+
**Reference Implementation:** We recognize the foundational work of the [Open ASR Leaderboard](https://huggingface.co/spaces/hf-audio/open_asr_leaderboard) by Hugging Face Audio team and appreciate the contributors of the [open_asr_leaderboard repository](https://github.com/huggingface/open_asr_leaderboard) for creating reproducible evaluation scripts.
|
| 356 |
+
"""
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def get_evaluation_model_text(asr_results_df: Any | None = None) -> str:
|
| 360 |
+
summary = get_benchmark_summary(asr_results_df)
|
| 361 |
+
language_phrase = (
|
| 362 |
+
f"all {_format_count(summary['languages'])} currently loaded benchmark {_plural('language', summary['languages'])}"
|
| 363 |
+
if _is_loaded_frame(asr_results_df) and summary["languages"] > 0
|
| 364 |
+
else "all supported benchmark languages"
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
return f"""
|
| 368 |
+
### Submit a Model for Evaluation
|
| 369 |
+
|
| 370 |
+
Add a new ASR model to PazaBench. We'll evaluate it across {language_phrase}.
|
| 371 |
+
|
| 372 |
+
**Requirements:**
|
| 373 |
+
- Model must be **publicly available** on [Hugging Face Hub](https://huggingface.co/models)
|
| 374 |
+
- Must support speech-to-text / ASR tasks
|
| 375 |
+
- Should be compatible with `transformers` AutoModel or provide clear loading instructions
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
EVALUATION_LANGUAGE_TEXT = """
|
| 379 |
+
### Request Language Evaluation
|
| 380 |
+
|
| 381 |
+
Submit a language dataset from any region for evaluation on PazaBench. We'll benchmark it using all supported ASR models. Provide the dataset source in the form below.
|
| 382 |
+
|
| 383 |
+
**Requirements:**
|
| 384 |
+
- Dataset must be publicly accessible on Hugging Face Hub or via a public URL
|
| 385 |
+
- Must contain audio samples with text transcriptions
|
| 386 |
+
- Audio should be 16kHz mono WAV format (will be resampled if needed)
|
| 387 |
+
"""
|
| 388 |
+
|
| 389 |
+
CHANGELOG_HEADER = """
|
| 390 |
+
### Change Log
|
| 391 |
+
|
| 392 |
+
What changed in each PazaBench release, with key coverage metrics per version. The full model, dataset and language lists live in the **ASR Metrics** and **Benchmark Inputs** tabs.
|
| 393 |
+
"""
|
| 394 |
+
|
| 395 |
+
# Per-version release cards (HTML template lives in src/data/changelog_cards.html)
|
| 396 |
+
CHANGELOG_CARDS_HTML = _load_text("changelog_cards.html")
|
| 397 |
+
|
| 398 |
+
INTRODUCTION_TEXT = get_introduction_text()
|
| 399 |
+
LLM_BENCHMARKS_TEXT = get_llm_benchmarks_text()
|
| 400 |
+
LLM_BENCHMARKS_DATASETS_TEXT = get_llm_benchmarks_datasets_text()
|
| 401 |
+
EVALUATION_MODEL_TEXT = get_evaluation_model_text()
|
| 402 |
+
|
| 403 |
+
CITATION_BUTTON_LABEL = "Copy the following snippet to cite these results"
|
| 404 |
+
CITATION_BUTTON_TEXT = r"""
|
| 405 |
+
@misc{pazabench2026,
|
| 406 |
+
title={PazaBench: A Benchmark for Automatic Speech Recognition on Low Resource Languages},
|
| 407 |
+
author={Microsoft Research Africa, Nairobi},
|
| 408 |
+
year={2026},
|
| 409 |
+
howpublished={\url{https://www.microsoft.com/en-us/research/project/project-gecko/}},
|
| 410 |
+
note={Alpha version. Part of Project Gecko - Equitable Generative AI for the Global Majority}
|
| 411 |
+
}
|
| 412 |
+
"""
|
src/assets/PazaBench-Banner-Mobile.png
ADDED
|
Git LFS Details
|
src/assets/PazaBench-Banner2.png
ADDED
|
Git LFS Details
|
src/constants.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Constants and mappings for PazaBench.
|
| 3 |
+
|
| 4 |
+
This module contains all mapping dictionaries and configuration constants
|
| 5 |
+
that are shared across the application.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
_DATA_DIR = Path(__file__).parent / "data"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _load_json(name: str):
|
| 15 |
+
"""Load a JSON data file bundled under ``src/data/``."""
|
| 16 |
+
with open(_DATA_DIR / name, encoding="utf-8") as data_file:
|
| 17 |
+
return json.load(data_file)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# =============================================================================
|
| 21 |
+
# File Paths
|
| 22 |
+
# =============================================================================
|
| 23 |
+
|
| 24 |
+
RESULTS_CSV_PATH = Path("results_summary.csv")
|
| 25 |
+
RESULTS_CSV_FILENAME = "results_summary.csv"
|
| 26 |
+
|
| 27 |
+
MODEL_FAMILY_ALIASES = {
|
| 28 |
+
"hubert": "facebook_hubert",
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def canonicalize_model_family(model_family: str) -> str:
|
| 33 |
+
"""Map legacy or duplicate family identifiers to a single canonical value."""
|
| 34 |
+
return MODEL_FAMILY_ALIASES.get(model_family, model_family)
|
| 35 |
+
|
| 36 |
+
# =============================================================================
|
| 37 |
+
# Filter Configuration
|
| 38 |
+
# =============================================================================
|
| 39 |
+
|
| 40 |
+
FILTER_COLUMN_ORDER = ["model", "language", "dataset_group"]
|
| 41 |
+
FILTER_PARAM_MAP = {
|
| 42 |
+
"model": "models",
|
| 43 |
+
"language": "languages",
|
| 44 |
+
"dataset_group": "dataset_groups",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
# =============================================================================
|
| 48 |
+
# Display Configuration
|
| 49 |
+
# =============================================================================
|
| 50 |
+
|
| 51 |
+
ASR_DISPLAY_COLUMNS = [
|
| 52 |
+
"model_family",
|
| 53 |
+
"model",
|
| 54 |
+
"dataset_group",
|
| 55 |
+
"split",
|
| 56 |
+
"language",
|
| 57 |
+
"region",
|
| 58 |
+
"cer",
|
| 59 |
+
"wer",
|
| 60 |
+
"rtfx",
|
| 61 |
+
"duration_sec",
|
| 62 |
+
"inference_time_sec",
|
| 63 |
+
"num_samples",
|
| 64 |
+
]
|
| 65 |
+
ASR_NUMERIC_COLUMNS = ["wer", "cer", "rtfx", "duration_sec", "inference_time_sec", "num_samples"]
|
| 66 |
+
ASR_TEXT_COLUMNS = ["model_family", "model", "dataset_group", "split", "language", "region"]
|
| 67 |
+
|
| 68 |
+
# =============================================================================
|
| 69 |
+
# Metric Configuration
|
| 70 |
+
# =============================================================================
|
| 71 |
+
|
| 72 |
+
METRIC_CONFIGS = {
|
| 73 |
+
"cer": {"label": "CER", "better": "lower", "fmt": "{:.2f}"},
|
| 74 |
+
"wer": {"label": "WER", "better": "lower", "fmt": "{:.2f}"},
|
| 75 |
+
"rtfx": {"label": "RTFx", "better": "higher", "fmt": "{:.2f}"},
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
VIEW_MODE_COLUMNS = {
|
| 79 |
+
"Model families": "model_family",
|
| 80 |
+
"Individual models": "model",
|
| 81 |
+
}
|
| 82 |
+
DEFAULT_VIEW_MODE = "Model families"
|
| 83 |
+
|
| 84 |
+
# =============================================================================
|
| 85 |
+
# Language Normalization
|
| 86 |
+
# =============================================================================
|
| 87 |
+
|
| 88 |
+
LANGUAGE_NAME_MAPPING = _load_json("language_name_mapping.json")
|
| 89 |
+
|
| 90 |
+
# =============================================================================
|
| 91 |
+
# Geographic Mappings
|
| 92 |
+
# =============================================================================
|
| 93 |
+
|
| 94 |
+
# Language to country mapping for Africa map (using ISO 3166-1 alpha-3 codes)
|
| 95 |
+
LANGUAGE_COUNTRY_MAP = _load_json("language_country_map.json")
|
| 96 |
+
|
| 97 |
+
# Country code to name mapping
|
| 98 |
+
COUNTRY_NAMES = _load_json("country_names.json")
|
| 99 |
+
|
| 100 |
+
# All African countries (ISO 3166-1 alpha-3 -> name), used to show "No coverage"
|
| 101 |
+
# on the map for countries without benchmark data.
|
| 102 |
+
AFRICAN_COUNTRIES = _load_json("african_countries.json")
|
| 103 |
+
|
| 104 |
+
# Language to countries mapping (full country names)
|
| 105 |
+
# Used for language metadata and region lookups
|
| 106 |
+
LANGUAGE_TO_COUNTRIES_MAP: dict[str, list[str]] = _load_json("language_to_countries_map.json")
|
| 107 |
+
|
| 108 |
+
# Country to African region mapping (geographical)
|
| 109 |
+
COUNTRY_TO_REGION_MAP: dict[str, str] = _load_json("country_to_region_map.json")
|
| 110 |
+
|
| 111 |
+
# =============================================================================
|
| 112 |
+
# Visualization Interpretation Text
|
| 113 |
+
# =============================================================================
|
| 114 |
+
|
| 115 |
+
INTERPRETATIONS = {
|
| 116 |
+
'speed_accuracy': """
|
| 117 |
+
- Each bubble represents a specific model; **bubble size = model parameter count**
|
| 118 |
+
- **X-axis (WER)**: Left is better (more accurate)
|
| 119 |
+
- **Y-axis (RTFx)**: Up is better (faster processing)
|
| 120 |
+
- **Top-left quadrant (⭐)**: Ideal zone - fast AND accurate models
|
| 121 |
+
- Gray dashed lines show median values for reference
|
| 122 |
+
- Uses median values to reduce impact of outliers
|
| 123 |
+
- Hover over bubbles to see exact parameter counts (e.g., 1.5B, 300M)
|
| 124 |
+
""",
|
| 125 |
+
|
| 126 |
+
'leaderboard': """
|
| 127 |
+
- **No language selected**: Shows model families (aggregated across all languages)
|
| 128 |
+
- **Language(s) selected**: Shows top 15 individual models for those languages
|
| 129 |
+
- The horizontal bars show the median Word Error Rate (WER)
|
| 130 |
+
- Lower WER values (left side) indicate better accuracy
|
| 131 |
+
- Error bars represent the standard deviation, showing variability
|
| 132 |
+
- Bar colors correspond to each model family's assigned color
|
| 133 |
+
- Hover over bars to see additional metrics like RTFx (speed) and total samples evaluated
|
| 134 |
+
- Uses median instead of mean to reduce impact of outliers
|
| 135 |
+
""",
|
| 136 |
+
|
| 137 |
+
'cer_leaderboard': """
|
| 138 |
+
- **No language selected**: Shows model families (aggregated across all languages)
|
| 139 |
+
- **Language(s) selected**: Shows top 15 individual models for those languages
|
| 140 |
+
- The horizontal bars show the median Character Error Rate (CER)
|
| 141 |
+
- Lower CER values (left side) indicate better accuracy
|
| 142 |
+
- CER is especially important for agglutinative and low-resource languages
|
| 143 |
+
- Error bars represent the standard deviation, showing variability
|
| 144 |
+
- Bar colors correspond to each model family's assigned color
|
| 145 |
+
- Hover over bars to see additional metrics like WER, RTFx (speed) and total samples evaluated
|
| 146 |
+
- Uses median instead of mean to reduce impact of outliers
|
| 147 |
+
""",
|
| 148 |
+
|
| 149 |
+
'correlation': """
|
| 150 |
+
- Each point represents one evaluation result
|
| 151 |
+
- Strong positive correlation means CER and WER move together
|
| 152 |
+
- Models with high character errors typically also have high word errors
|
| 153 |
+
- The trend line shows the overall relationship
|
| 154 |
+
- **Below the line**: Models make more accurate character-level predictions (phonetically closer errors)
|
| 155 |
+
- **Above the line**: Models make more severe character-level errors per word mistake
|
| 156 |
+
- Can be filtered by language to analyze specific language patterns
|
| 157 |
+
""",
|
| 158 |
+
|
| 159 |
+
'consistency': """
|
| 160 |
+
- Coefficient of Variation (CV) = (Standard Deviation / Median) × 100%
|
| 161 |
+
- **Lower CV** = more consistent performance across different languages
|
| 162 |
+
- **Higher CV** = performance varies widely depending on the language
|
| 163 |
+
- Bar colors correspond to each model family's assigned color
|
| 164 |
+
- Important for production deployment - you want consistent models
|
| 165 |
+
- Outliers have been removed using IQR method for more robust analysis
|
| 166 |
+
- Uses median instead of mean for more robust central tendency measure
|
| 167 |
+
"""
|
| 168 |
+
}
|
src/data/african_countries.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"DZA": "Algeria",
|
| 3 |
+
"AGO": "Angola",
|
| 4 |
+
"BEN": "Benin",
|
| 5 |
+
"BWA": "Botswana",
|
| 6 |
+
"BFA": "Burkina Faso",
|
| 7 |
+
"BDI": "Burundi",
|
| 8 |
+
"CPV": "Cabo Verde",
|
| 9 |
+
"CMR": "Cameroon",
|
| 10 |
+
"CAF": "Central African Republic",
|
| 11 |
+
"TCD": "Chad",
|
| 12 |
+
"COM": "Comoros",
|
| 13 |
+
"COG": "Republic of Congo",
|
| 14 |
+
"COD": "DR Congo",
|
| 15 |
+
"CIV": "Côte d'Ivoire",
|
| 16 |
+
"DJI": "Djibouti",
|
| 17 |
+
"EGY": "Egypt",
|
| 18 |
+
"GNQ": "Equatorial Guinea",
|
| 19 |
+
"ERI": "Eritrea",
|
| 20 |
+
"SWZ": "Eswatini",
|
| 21 |
+
"ETH": "Ethiopia",
|
| 22 |
+
"GAB": "Gabon",
|
| 23 |
+
"GMB": "Gambia",
|
| 24 |
+
"GHA": "Ghana",
|
| 25 |
+
"GIN": "Guinea",
|
| 26 |
+
"GNB": "Guinea-Bissau",
|
| 27 |
+
"KEN": "Kenya",
|
| 28 |
+
"LSO": "Lesotho",
|
| 29 |
+
"LBR": "Liberia",
|
| 30 |
+
"LBY": "Libya",
|
| 31 |
+
"MDG": "Madagascar",
|
| 32 |
+
"MWI": "Malawi",
|
| 33 |
+
"MLI": "Mali",
|
| 34 |
+
"MRT": "Mauritania",
|
| 35 |
+
"MUS": "Mauritius",
|
| 36 |
+
"MAR": "Morocco",
|
| 37 |
+
"MOZ": "Mozambique",
|
| 38 |
+
"NAM": "Namibia",
|
| 39 |
+
"NER": "Niger",
|
| 40 |
+
"NGA": "Nigeria",
|
| 41 |
+
"RWA": "Rwanda",
|
| 42 |
+
"STP": "São Tomé and Príncipe",
|
| 43 |
+
"SEN": "Senegal",
|
| 44 |
+
"SYC": "Seychelles",
|
| 45 |
+
"SLE": "Sierra Leone",
|
| 46 |
+
"SOM": "Somalia",
|
| 47 |
+
"ZAF": "South Africa",
|
| 48 |
+
"SSD": "South Sudan",
|
| 49 |
+
"SDN": "Sudan",
|
| 50 |
+
"TZA": "Tanzania",
|
| 51 |
+
"TGO": "Togo",
|
| 52 |
+
"TUN": "Tunisia",
|
| 53 |
+
"UGA": "Uganda",
|
| 54 |
+
"ZMB": "Zambia",
|
| 55 |
+
"ZWE": "Zimbabwe"
|
| 56 |
+
}
|
src/data/changelog_cards.html
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<div class="changelog-cards" style="display: flex; flex-wrap: wrap; gap: 20px; align-items: stretch;">
|
| 3 |
+
<div style="flex: 1 1 340px; background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%); border-radius: 12px; padding: 20px; border: 1px solid #e0e7ef;">
|
| 4 |
+
<h4 style="margin: 0 0 2px 0; color: #0f172a; font-size: 1.1em; font-weight: 600;">🗓️ v2 · Release</h4>
|
| 5 |
+
<div style="color: #666; font-size: 0.82rem; margin: 0 0 14px 0; border-bottom: 2px solid #8CD0FF; padding-bottom: 8px;">June 30, 2026</div>
|
| 6 |
+
<div style="display: grid; gap: 8px;">
|
| 7 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 8 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🌍 Languages</span>
|
| 9 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">61</span>
|
| 10 |
+
</div>
|
| 11 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 12 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📁 Dataset groups</span>
|
| 13 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">11</span>
|
| 14 |
+
</div>
|
| 15 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 16 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📍 Countries</span>
|
| 17 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">38</span>
|
| 18 |
+
</div>
|
| 19 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 20 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🎙️ Models</span>
|
| 21 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">53</span>
|
| 22 |
+
</div>
|
| 23 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 24 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🧩 Model families</span>
|
| 25 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">16</span>
|
| 26 |
+
</div>
|
| 27 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 28 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🧪 Evaluation splits</span>
|
| 29 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">93</span>
|
| 30 |
+
</div>
|
| 31 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 32 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📊 Test samples</span>
|
| 33 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">417,460</span>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
<div style="margin-top: 14px; font-size: 0.85rem; line-height: 1.5;">
|
| 37 |
+
<strong style="color: #0f172a;">What's new</strong>
|
| 38 |
+
<ul style="margin: 6px 0 0 0; padding-left: 18px; color: #444;">
|
| 39 |
+
<li>Added <strong>SeamlessM4T</strong> — a new model and model family</li>
|
| 40 |
+
<li>Four new dataset groups: <a href="https://huggingface.co/datasets/alaleye/fon">Alaleye</a>, <a href="https://huggingface.co/datasets/sartifyllc/Sukuma-Voices">Sartify LLC</a>, <a href="https://huggingface.co/datasets/CLEAR-Global/TWB-Voice-1.0">TWB</a>, <a href="https://huggingface.co/datasets/google/WaxalNLP">WAXAL</a></li>
|
| 41 |
+
<li>New filter for model language support (multilingual vs. English-only)</li>
|
| 42 |
+
<li>Changelog</li>
|
| 43 |
+
</ul>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
<div style="flex: 1 1 340px; background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%); border-radius: 12px; padding: 20px; border: 1px solid #e0e7ef;">
|
| 47 |
+
<h4 style="margin: 0 0 2px 0; color: #0f172a; font-size: 1.1em; font-weight: 600;">🗓️ v1 · Initial release</h4>
|
| 48 |
+
<div style="color: #666; font-size: 0.82rem; margin: 0 0 14px 0; border-bottom: 2px solid #8CD0FF; padding-bottom: 8px;">February 5, 2026</div>
|
| 49 |
+
<div style="display: grid; gap: 8px;">
|
| 50 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 51 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🌍 Languages</span>
|
| 52 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">39</span>
|
| 53 |
+
</div>
|
| 54 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 55 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📁 Dataset groups</span>
|
| 56 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">7</span>
|
| 57 |
+
</div>
|
| 58 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 59 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📍 Countries</span>
|
| 60 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">34</span>
|
| 61 |
+
</div>
|
| 62 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 63 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🎙️ Models</span>
|
| 64 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">52</span>
|
| 65 |
+
</div>
|
| 66 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 67 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🧩 Model families</span>
|
| 68 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">15</span>
|
| 69 |
+
</div>
|
| 70 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 71 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🧪 Evaluation splits</span>
|
| 72 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">60</span>
|
| 73 |
+
</div>
|
| 74 |
+
<div style="background: white; padding: 9px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 75 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📊 Test samples</span>
|
| 76 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">209,540</span>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
<div style="margin-top: 14px; font-size: 0.85rem; line-height: 1.5;">
|
| 80 |
+
<strong style="color: #0f172a;">Highlights</strong>
|
| 81 |
+
<ul style="margin: 6px 0 0 0; padding-left: 18px; color: #444;">
|
| 82 |
+
<li>Initial public launch of PazaBench</li>
|
| 83 |
+
<li>Scored on <strong>CER</strong>, <strong>WER</strong> and <strong>RTFx</strong></li>
|
| 84 |
+
<li>Whisper duration-truncation to curb hallucination</li>
|
| 85 |
+
</ul>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
src/data/country_names.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"ZAF": "South Africa",
|
| 3 |
+
"ETH": "Ethiopia",
|
| 4 |
+
"EGY": "Egypt",
|
| 5 |
+
"MAR": "Morocco",
|
| 6 |
+
"DZA": "Algeria",
|
| 7 |
+
"TUN": "Tunisia",
|
| 8 |
+
"LBY": "Libya",
|
| 9 |
+
"SDN": "Sudan",
|
| 10 |
+
"CMR": "Cameroon",
|
| 11 |
+
"MOZ": "Mozambique",
|
| 12 |
+
"KEN": "Kenya",
|
| 13 |
+
"BFA": "Burkina Faso",
|
| 14 |
+
"CIV": "Côte d'Ivoire",
|
| 15 |
+
"SEN": "Senegal",
|
| 16 |
+
"MLI": "Mali",
|
| 17 |
+
"NGA": "Nigeria",
|
| 18 |
+
"GIN": "Guinea",
|
| 19 |
+
"NER": "Niger",
|
| 20 |
+
"UGA": "Uganda",
|
| 21 |
+
"RWA": "Rwanda",
|
| 22 |
+
"COD": "DR Congo",
|
| 23 |
+
"COG": "Congo",
|
| 24 |
+
"TZA": "Tanzania",
|
| 25 |
+
"MWI": "Malawi",
|
| 26 |
+
"ZMB": "Zambia",
|
| 27 |
+
"LSO": "Lesotho",
|
| 28 |
+
"BWA": "Botswana",
|
| 29 |
+
"ZWE": "Zimbabwe",
|
| 30 |
+
"SOM": "Somalia",
|
| 31 |
+
"ERI": "Eritrea",
|
| 32 |
+
"GHA": "Ghana",
|
| 33 |
+
"AGO": "Angola",
|
| 34 |
+
"GMB": "Gambia",
|
| 35 |
+
"BEN": "Benin",
|
| 36 |
+
"TCD": "Chad",
|
| 37 |
+
"TGO": "Togo",
|
| 38 |
+
"MDG": "Madagascar",
|
| 39 |
+
"SWZ": "Eswatini"
|
| 40 |
+
}
|
src/data/country_to_region_map.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Kenya": "East Africa",
|
| 3 |
+
"Tanzania": "East Africa",
|
| 4 |
+
"Uganda": "East Africa",
|
| 5 |
+
"Rwanda": "East Africa",
|
| 6 |
+
"Burundi": "East Africa",
|
| 7 |
+
"Ethiopia": "East Africa",
|
| 8 |
+
"Eritrea": "East Africa",
|
| 9 |
+
"Somalia": "East Africa",
|
| 10 |
+
"Djibouti": "East Africa",
|
| 11 |
+
"South Africa": "Southern Africa",
|
| 12 |
+
"Namibia": "Southern Africa",
|
| 13 |
+
"Botswana": "Southern Africa",
|
| 14 |
+
"Zimbabwe": "Southern Africa",
|
| 15 |
+
"Zambia": "Southern Africa",
|
| 16 |
+
"Malawi": "Southern Africa",
|
| 17 |
+
"Mozambique": "Southern Africa",
|
| 18 |
+
"Angola": "Southern Africa",
|
| 19 |
+
"Lesotho": "Southern Africa",
|
| 20 |
+
"Eswatini": "Southern Africa",
|
| 21 |
+
"Nigeria": "West Africa",
|
| 22 |
+
"Ghana": "West Africa",
|
| 23 |
+
"Senegal": "West Africa",
|
| 24 |
+
"Gambia": "West Africa",
|
| 25 |
+
"Guinea": "West Africa",
|
| 26 |
+
"Mali": "West Africa",
|
| 27 |
+
"Burkina Faso": "West Africa",
|
| 28 |
+
"Côte d'Ivoire": "West Africa",
|
| 29 |
+
"Niger": "West Africa",
|
| 30 |
+
"Cameroon": "West Africa",
|
| 31 |
+
"DR Congo": "Central Africa",
|
| 32 |
+
"Republic of Congo": "Central Africa",
|
| 33 |
+
"Central African Republic": "Central Africa",
|
| 34 |
+
"Gabon": "Central Africa",
|
| 35 |
+
"Egypt": "North Africa",
|
| 36 |
+
"Libya": "North Africa",
|
| 37 |
+
"Tunisia": "North Africa",
|
| 38 |
+
"Algeria": "North Africa",
|
| 39 |
+
"Morocco": "North Africa",
|
| 40 |
+
"Sudan": "North Africa",
|
| 41 |
+
"Benin": "West Africa",
|
| 42 |
+
"Togo": "West Africa",
|
| 43 |
+
"Chad": "Central Africa",
|
| 44 |
+
"Madagascar": "East Africa"
|
| 45 |
+
}
|
src/data/dataset_group_metadata.json
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"ALFFA": {
|
| 3 |
+
"description": "Read and broadcast speech for African ASR benchmarks",
|
| 4 |
+
"url": "https://openslr.org/25/",
|
| 5 |
+
"license": "MIT",
|
| 6 |
+
"languages": [
|
| 7 |
+
"Amharic",
|
| 8 |
+
"Swahili",
|
| 9 |
+
"Wolof"
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
"African Next Voices Kenya": {
|
| 13 |
+
"description": "Conversational speech collected in Kenya",
|
| 14 |
+
"url": "https://huggingface.co/datasets/MCAA1-MSU/anv_data_ke",
|
| 15 |
+
"license": "CC BY 4.0",
|
| 16 |
+
"languages": [
|
| 17 |
+
"Dholuo",
|
| 18 |
+
"Kalenjin",
|
| 19 |
+
"Kikuyu",
|
| 20 |
+
"Maasai",
|
| 21 |
+
"Somali"
|
| 22 |
+
]
|
| 23 |
+
},
|
| 24 |
+
"African Next Voices South Africa": {
|
| 25 |
+
"description": "Conversational speech collected in South Africa",
|
| 26 |
+
"url": "https://huggingface.co/datasets/dsfsi-anv/za-african-next-voices",
|
| 27 |
+
"license": "CC BY 4.0",
|
| 28 |
+
"languages": [
|
| 29 |
+
"Sesotho",
|
| 30 |
+
"Setswana",
|
| 31 |
+
"Xhosa",
|
| 32 |
+
"Xitsonga",
|
| 33 |
+
"Tshivenda",
|
| 34 |
+
"Zulu"
|
| 35 |
+
]
|
| 36 |
+
},
|
| 37 |
+
"Google FLEURS": {
|
| 38 |
+
"description": "Read speech from the FLEURS multilingual benchmark",
|
| 39 |
+
"url": "https://huggingface.co/datasets/google/fleurs",
|
| 40 |
+
"license": "CC BY 4.0",
|
| 41 |
+
"languages": [
|
| 42 |
+
"Afrikaans",
|
| 43 |
+
"Amharic",
|
| 44 |
+
"Fula",
|
| 45 |
+
"Ganda",
|
| 46 |
+
"Hausa",
|
| 47 |
+
"Igbo",
|
| 48 |
+
"Kamba",
|
| 49 |
+
"Lingala",
|
| 50 |
+
"Dholuo",
|
| 51 |
+
"Northern Sotho",
|
| 52 |
+
"Nyanja",
|
| 53 |
+
"Oromo",
|
| 54 |
+
"Shona",
|
| 55 |
+
"Somali",
|
| 56 |
+
"Swahili",
|
| 57 |
+
"Umbundu",
|
| 58 |
+
"Wolof",
|
| 59 |
+
"Xhosa",
|
| 60 |
+
"Yoruba",
|
| 61 |
+
"Zulu"
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
"DigiGreen Kikuyu ASR": {
|
| 65 |
+
"description": "Agricultural speech from Kikuyu-speaking farmers",
|
| 66 |
+
"url": "https://huggingface.co/datasets/DigiGreen/KikuyuASR_trainingdataset",
|
| 67 |
+
"license": "Apache 2.0",
|
| 68 |
+
"languages": [
|
| 69 |
+
"Kikuyu"
|
| 70 |
+
]
|
| 71 |
+
},
|
| 72 |
+
"WAXAL": {
|
| 73 |
+
"description": "Community speech corpus for African languages",
|
| 74 |
+
"url": "https://huggingface.co/datasets/google/WaxalNLP",
|
| 75 |
+
"license": "Mixed (CC BY 4.0 / CC BY-SA 4.0)",
|
| 76 |
+
"languages": [
|
| 77 |
+
"Acholi",
|
| 78 |
+
"Akan",
|
| 79 |
+
"Amharic",
|
| 80 |
+
"Dagbani",
|
| 81 |
+
"Dagaare",
|
| 82 |
+
"Ewe",
|
| 83 |
+
"Fula",
|
| 84 |
+
"Ikposo",
|
| 85 |
+
"Lingala",
|
| 86 |
+
"Luganda",
|
| 87 |
+
"Maasai",
|
| 88 |
+
"Malagasy",
|
| 89 |
+
"Nyankole",
|
| 90 |
+
"Oromo",
|
| 91 |
+
"Sidamo",
|
| 92 |
+
"Shona",
|
| 93 |
+
"Soga",
|
| 94 |
+
"Tigrinya",
|
| 95 |
+
"Wolaytta"
|
| 96 |
+
]
|
| 97 |
+
},
|
| 98 |
+
"Alaleye": {
|
| 99 |
+
"description": "Read speech in Fongbe",
|
| 100 |
+
"url": "https://huggingface.co/datasets/alaleye/fon",
|
| 101 |
+
"license": "CC BY 4.0",
|
| 102 |
+
"languages": [
|
| 103 |
+
"Fongbe"
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
"Sartify LLC": {
|
| 107 |
+
"description": "Sukuma speech corpus",
|
| 108 |
+
"url": "https://huggingface.co/datasets/sartifyllc/Sukuma-Voices",
|
| 109 |
+
"license": "CC BY 4.0",
|
| 110 |
+
"languages": [
|
| 111 |
+
"Sukuma"
|
| 112 |
+
]
|
| 113 |
+
},
|
| 114 |
+
"TWB": {
|
| 115 |
+
"description": "Read speech curated by CLEAR Global",
|
| 116 |
+
"url": "https://huggingface.co/datasets/CLEAR-Global/TWB-Voice-1.0",
|
| 117 |
+
"license": "CC BY-NC 4.0 (gated access)",
|
| 118 |
+
"languages": [
|
| 119 |
+
"Hausa",
|
| 120 |
+
"Kanuri",
|
| 121 |
+
"Chadian Arabic",
|
| 122 |
+
"Swahili"
|
| 123 |
+
]
|
| 124 |
+
},
|
| 125 |
+
"Mozilla Common Voice 23.0": {
|
| 126 |
+
"description": "Crowdsourced speech from Common Voice 23.0",
|
| 127 |
+
"url": "https://commonvoice.mozilla.org/",
|
| 128 |
+
"license": "CC0 1.0",
|
| 129 |
+
"languages": [
|
| 130 |
+
"Adamawa Fulfulde",
|
| 131 |
+
"Afrikaans",
|
| 132 |
+
"Amharic",
|
| 133 |
+
"Arabic",
|
| 134 |
+
"Baatonum",
|
| 135 |
+
"Bafut",
|
| 136 |
+
"Bankon",
|
| 137 |
+
"Basaa",
|
| 138 |
+
"Borgu Fulfulde",
|
| 139 |
+
"Dholuo",
|
| 140 |
+
"Dioula",
|
| 141 |
+
"Ekoti",
|
| 142 |
+
"Hausa",
|
| 143 |
+
"Igbo",
|
| 144 |
+
"Kabyle",
|
| 145 |
+
"Kalenjin",
|
| 146 |
+
"Kidaw'ida",
|
| 147 |
+
"Kinyarwanda",
|
| 148 |
+
"Luganda",
|
| 149 |
+
"Mbo",
|
| 150 |
+
"Northern Sotho",
|
| 151 |
+
"Nyungwe",
|
| 152 |
+
"Setswana",
|
| 153 |
+
"South Ndebele",
|
| 154 |
+
"Swahili",
|
| 155 |
+
"Swati",
|
| 156 |
+
"Tamazight",
|
| 157 |
+
"Tigre",
|
| 158 |
+
"Tigrinya",
|
| 159 |
+
"Tsonga",
|
| 160 |
+
"Twi",
|
| 161 |
+
"Venda",
|
| 162 |
+
"Xhosa",
|
| 163 |
+
"Yoruba",
|
| 164 |
+
"Zulu"
|
| 165 |
+
]
|
| 166 |
+
},
|
| 167 |
+
"Naija Voices": {
|
| 168 |
+
"description": "Conversational Nigerian speech",
|
| 169 |
+
"url": "https://huggingface.co/datasets/naijavoices/naijavoices-dataset",
|
| 170 |
+
"license": "CC BY-NC-SA 4.0",
|
| 171 |
+
"languages": [
|
| 172 |
+
"Hausa",
|
| 173 |
+
"Igbo",
|
| 174 |
+
"Yoruba"
|
| 175 |
+
]
|
| 176 |
+
}
|
| 177 |
+
}
|
src/data/language_country_map.json
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Afrikaans": [
|
| 3 |
+
"ZAF"
|
| 4 |
+
],
|
| 5 |
+
"Amharic": [
|
| 6 |
+
"ETH"
|
| 7 |
+
],
|
| 8 |
+
"Arabic": [
|
| 9 |
+
"EGY",
|
| 10 |
+
"MAR",
|
| 11 |
+
"DZA",
|
| 12 |
+
"TUN",
|
| 13 |
+
"LBY",
|
| 14 |
+
"SDN"
|
| 15 |
+
],
|
| 16 |
+
"Basaa": [
|
| 17 |
+
"CMR"
|
| 18 |
+
],
|
| 19 |
+
"Dholuo": [
|
| 20 |
+
"KEN"
|
| 21 |
+
],
|
| 22 |
+
"Dioula": [
|
| 23 |
+
"BFA",
|
| 24 |
+
"CIV"
|
| 25 |
+
],
|
| 26 |
+
"Acholi": [
|
| 27 |
+
"UGA"
|
| 28 |
+
],
|
| 29 |
+
"Akan": [
|
| 30 |
+
"GHA"
|
| 31 |
+
],
|
| 32 |
+
"Adamawa Fulfulde": [
|
| 33 |
+
"NGA",
|
| 34 |
+
"CMR"
|
| 35 |
+
],
|
| 36 |
+
"Baatonum": [
|
| 37 |
+
"BEN"
|
| 38 |
+
],
|
| 39 |
+
"Bafut": [
|
| 40 |
+
"CMR"
|
| 41 |
+
],
|
| 42 |
+
"Bankon": [
|
| 43 |
+
"CMR"
|
| 44 |
+
],
|
| 45 |
+
"Ekoti": [
|
| 46 |
+
"MOZ"
|
| 47 |
+
],
|
| 48 |
+
"Dagbani": [
|
| 49 |
+
"GHA"
|
| 50 |
+
],
|
| 51 |
+
"Dagaare": [
|
| 52 |
+
"GHA"
|
| 53 |
+
],
|
| 54 |
+
"Fula": [
|
| 55 |
+
"SEN",
|
| 56 |
+
"MLI",
|
| 57 |
+
"NGA",
|
| 58 |
+
"GIN",
|
| 59 |
+
"CMR",
|
| 60 |
+
"NER"
|
| 61 |
+
],
|
| 62 |
+
"Borgu Fulfulde": [
|
| 63 |
+
"BEN",
|
| 64 |
+
"NGA"
|
| 65 |
+
],
|
| 66 |
+
"Chadian Arabic": [
|
| 67 |
+
"TCD"
|
| 68 |
+
],
|
| 69 |
+
"Luganda": [
|
| 70 |
+
"UGA"
|
| 71 |
+
],
|
| 72 |
+
"Hausa": [
|
| 73 |
+
"NGA",
|
| 74 |
+
"NER"
|
| 75 |
+
],
|
| 76 |
+
"Igbo": [
|
| 77 |
+
"NGA"
|
| 78 |
+
],
|
| 79 |
+
"Kabyle": [
|
| 80 |
+
"DZA"
|
| 81 |
+
],
|
| 82 |
+
"Kalenjin": [
|
| 83 |
+
"KEN"
|
| 84 |
+
],
|
| 85 |
+
"Kamba": [
|
| 86 |
+
"KEN"
|
| 87 |
+
],
|
| 88 |
+
"Kidaw'ida": [
|
| 89 |
+
"KEN"
|
| 90 |
+
],
|
| 91 |
+
"Kikuyu": [
|
| 92 |
+
"KEN"
|
| 93 |
+
],
|
| 94 |
+
"Kinyarwanda": [
|
| 95 |
+
"RWA"
|
| 96 |
+
],
|
| 97 |
+
"Kanuri": [
|
| 98 |
+
"NGA",
|
| 99 |
+
"NER",
|
| 100 |
+
"TCD"
|
| 101 |
+
],
|
| 102 |
+
"Ikposo": [
|
| 103 |
+
"TGO",
|
| 104 |
+
"GHA"
|
| 105 |
+
],
|
| 106 |
+
"Lingala": [
|
| 107 |
+
"COD",
|
| 108 |
+
"COG"
|
| 109 |
+
],
|
| 110 |
+
"Maasai": [
|
| 111 |
+
"KEN"
|
| 112 |
+
],
|
| 113 |
+
"Malagasy": [
|
| 114 |
+
"MDG"
|
| 115 |
+
],
|
| 116 |
+
"Mbo": [
|
| 117 |
+
"CMR"
|
| 118 |
+
],
|
| 119 |
+
"Northern Sotho": [
|
| 120 |
+
"ZAF"
|
| 121 |
+
],
|
| 122 |
+
"Nyanja": [
|
| 123 |
+
"MWI",
|
| 124 |
+
"ZMB"
|
| 125 |
+
],
|
| 126 |
+
"Nyankole": [
|
| 127 |
+
"UGA"
|
| 128 |
+
],
|
| 129 |
+
"Nyungwe": [
|
| 130 |
+
"MOZ"
|
| 131 |
+
],
|
| 132 |
+
"Oromo": [
|
| 133 |
+
"ETH"
|
| 134 |
+
],
|
| 135 |
+
"Soga": [
|
| 136 |
+
"UGA"
|
| 137 |
+
],
|
| 138 |
+
"Sesotho": [
|
| 139 |
+
"ZAF",
|
| 140 |
+
"LSO"
|
| 141 |
+
],
|
| 142 |
+
"Setswana": [
|
| 143 |
+
"ZAF",
|
| 144 |
+
"BWA"
|
| 145 |
+
],
|
| 146 |
+
"South Ndebele": [
|
| 147 |
+
"ZAF"
|
| 148 |
+
],
|
| 149 |
+
"Shona": [
|
| 150 |
+
"ZWE"
|
| 151 |
+
],
|
| 152 |
+
"Somali": [
|
| 153 |
+
"KEN",
|
| 154 |
+
"SOM"
|
| 155 |
+
],
|
| 156 |
+
"Swati": [
|
| 157 |
+
"SWZ",
|
| 158 |
+
"ZAF"
|
| 159 |
+
],
|
| 160 |
+
"Swahili": [
|
| 161 |
+
"KEN",
|
| 162 |
+
"TZA",
|
| 163 |
+
"COD"
|
| 164 |
+
],
|
| 165 |
+
"Tamazight": [
|
| 166 |
+
"MAR",
|
| 167 |
+
"DZA"
|
| 168 |
+
],
|
| 169 |
+
"Tigre": [
|
| 170 |
+
"ERI"
|
| 171 |
+
],
|
| 172 |
+
"Tigrinya": [
|
| 173 |
+
"ERI",
|
| 174 |
+
"ETH"
|
| 175 |
+
],
|
| 176 |
+
"Tsonga": [
|
| 177 |
+
"ZAF",
|
| 178 |
+
"MOZ"
|
| 179 |
+
],
|
| 180 |
+
"Venda": [
|
| 181 |
+
"ZAF"
|
| 182 |
+
],
|
| 183 |
+
"Tshivenda": [
|
| 184 |
+
"ZAF",
|
| 185 |
+
"ZWE"
|
| 186 |
+
],
|
| 187 |
+
"Twi": [
|
| 188 |
+
"GHA"
|
| 189 |
+
],
|
| 190 |
+
"Umbundu": [
|
| 191 |
+
"AGO"
|
| 192 |
+
],
|
| 193 |
+
"Wolaytta": [
|
| 194 |
+
"ETH"
|
| 195 |
+
],
|
| 196 |
+
"Wolof": [
|
| 197 |
+
"SEN",
|
| 198 |
+
"GMB"
|
| 199 |
+
],
|
| 200 |
+
"Xhosa": [
|
| 201 |
+
"ZAF"
|
| 202 |
+
],
|
| 203 |
+
"Xitsonga": [
|
| 204 |
+
"ZAF",
|
| 205 |
+
"MOZ"
|
| 206 |
+
],
|
| 207 |
+
"Yoruba": [
|
| 208 |
+
"NGA",
|
| 209 |
+
"BEN"
|
| 210 |
+
],
|
| 211 |
+
"Zulu": [
|
| 212 |
+
"ZAF"
|
| 213 |
+
]
|
| 214 |
+
}
|
src/data/language_name_mapping.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Ganda": "Luganda",
|
| 3 |
+
"Luganda": "Luganda",
|
| 4 |
+
"Dholuo": "Dholuo"
|
| 5 |
+
}
|
src/data/language_to_countries_map.json
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Swahili": [
|
| 3 |
+
"Kenya",
|
| 4 |
+
"Tanzania",
|
| 5 |
+
"Uganda",
|
| 6 |
+
"DR Congo"
|
| 7 |
+
],
|
| 8 |
+
"Dholuo": [
|
| 9 |
+
"Kenya"
|
| 10 |
+
],
|
| 11 |
+
"Dholuo (Luo)": [
|
| 12 |
+
"Kenya"
|
| 13 |
+
],
|
| 14 |
+
"Acholi": [
|
| 15 |
+
"Uganda"
|
| 16 |
+
],
|
| 17 |
+
"Akan": [
|
| 18 |
+
"Ghana"
|
| 19 |
+
],
|
| 20 |
+
"Adamawa Fulfulde": [
|
| 21 |
+
"Nigeria",
|
| 22 |
+
"Cameroon"
|
| 23 |
+
],
|
| 24 |
+
"Baatonum": [
|
| 25 |
+
"Benin"
|
| 26 |
+
],
|
| 27 |
+
"Bafut": [
|
| 28 |
+
"Cameroon"
|
| 29 |
+
],
|
| 30 |
+
"Bankon": [
|
| 31 |
+
"Cameroon"
|
| 32 |
+
],
|
| 33 |
+
"Kalenjin": [
|
| 34 |
+
"Kenya"
|
| 35 |
+
],
|
| 36 |
+
"Kikuyu": [
|
| 37 |
+
"Kenya"
|
| 38 |
+
],
|
| 39 |
+
"Kamba": [
|
| 40 |
+
"Kenya"
|
| 41 |
+
],
|
| 42 |
+
"Maasai": [
|
| 43 |
+
"Kenya",
|
| 44 |
+
"Tanzania"
|
| 45 |
+
],
|
| 46 |
+
"Kidaw'ida": [
|
| 47 |
+
"Kenya",
|
| 48 |
+
"Tanzania"
|
| 49 |
+
],
|
| 50 |
+
"Luganda": [
|
| 51 |
+
"Uganda"
|
| 52 |
+
],
|
| 53 |
+
"Kinyarwanda": [
|
| 54 |
+
"Rwanda"
|
| 55 |
+
],
|
| 56 |
+
"Dagbani": [
|
| 57 |
+
"Ghana"
|
| 58 |
+
],
|
| 59 |
+
"Dagaare": [
|
| 60 |
+
"Ghana"
|
| 61 |
+
],
|
| 62 |
+
"Somali": [
|
| 63 |
+
"Somalia",
|
| 64 |
+
"Kenya",
|
| 65 |
+
"Ethiopia"
|
| 66 |
+
],
|
| 67 |
+
"Amharic": [
|
| 68 |
+
"Ethiopia"
|
| 69 |
+
],
|
| 70 |
+
"Tigrinya": [
|
| 71 |
+
"Eritrea",
|
| 72 |
+
"Ethiopia"
|
| 73 |
+
],
|
| 74 |
+
"Tigre": [
|
| 75 |
+
"Eritrea"
|
| 76 |
+
],
|
| 77 |
+
"Oromo": [
|
| 78 |
+
"Ethiopia"
|
| 79 |
+
],
|
| 80 |
+
"Afrikaans": [
|
| 81 |
+
"South Africa",
|
| 82 |
+
"Namibia"
|
| 83 |
+
],
|
| 84 |
+
"Zulu": [
|
| 85 |
+
"South Africa"
|
| 86 |
+
],
|
| 87 |
+
"Xhosa": [
|
| 88 |
+
"South Africa"
|
| 89 |
+
],
|
| 90 |
+
"Setswana": [
|
| 91 |
+
"Botswana",
|
| 92 |
+
"South Africa"
|
| 93 |
+
],
|
| 94 |
+
"Sesotho": [
|
| 95 |
+
"South Africa",
|
| 96 |
+
"Lesotho"
|
| 97 |
+
],
|
| 98 |
+
"Northern Sotho": [
|
| 99 |
+
"South Africa"
|
| 100 |
+
],
|
| 101 |
+
"Xitsonga": [
|
| 102 |
+
"South Africa",
|
| 103 |
+
"Mozambique"
|
| 104 |
+
],
|
| 105 |
+
"Tshivenda": [
|
| 106 |
+
"South Africa"
|
| 107 |
+
],
|
| 108 |
+
"South Ndebele": [
|
| 109 |
+
"South Africa"
|
| 110 |
+
],
|
| 111 |
+
"Shona": [
|
| 112 |
+
"Zimbabwe"
|
| 113 |
+
],
|
| 114 |
+
"Nyanja": [
|
| 115 |
+
"Malawi",
|
| 116 |
+
"Zambia"
|
| 117 |
+
],
|
| 118 |
+
"Nyungwe": [
|
| 119 |
+
"Mozambique"
|
| 120 |
+
],
|
| 121 |
+
"Ekoti": [
|
| 122 |
+
"Mozambique"
|
| 123 |
+
],
|
| 124 |
+
"Yoruba": [
|
| 125 |
+
"Nigeria"
|
| 126 |
+
],
|
| 127 |
+
"Igbo": [
|
| 128 |
+
"Nigeria"
|
| 129 |
+
],
|
| 130 |
+
"Hausa": [
|
| 131 |
+
"Nigeria",
|
| 132 |
+
"Niger"
|
| 133 |
+
],
|
| 134 |
+
"Wolof": [
|
| 135 |
+
"Senegal",
|
| 136 |
+
"Gambia"
|
| 137 |
+
],
|
| 138 |
+
"Fula": [
|
| 139 |
+
"Senegal",
|
| 140 |
+
"Guinea",
|
| 141 |
+
"Mali",
|
| 142 |
+
"Nigeria"
|
| 143 |
+
],
|
| 144 |
+
"Borgu Fulfulde": [
|
| 145 |
+
"Benin",
|
| 146 |
+
"Nigeria"
|
| 147 |
+
],
|
| 148 |
+
"Chadian Arabic": [
|
| 149 |
+
"Chad"
|
| 150 |
+
],
|
| 151 |
+
"Twi": [
|
| 152 |
+
"Ghana"
|
| 153 |
+
],
|
| 154 |
+
"Dioula": [
|
| 155 |
+
"Burkina Faso",
|
| 156 |
+
"Côte d'Ivoire",
|
| 157 |
+
"Mali"
|
| 158 |
+
],
|
| 159 |
+
"Kanuri": [
|
| 160 |
+
"Nigeria",
|
| 161 |
+
"Niger",
|
| 162 |
+
"Chad"
|
| 163 |
+
],
|
| 164 |
+
"Ikposo": [
|
| 165 |
+
"Togo",
|
| 166 |
+
"Ghana"
|
| 167 |
+
],
|
| 168 |
+
"Basaa": [
|
| 169 |
+
"Cameroon"
|
| 170 |
+
],
|
| 171 |
+
"Ewe": [
|
| 172 |
+
"Ghana",
|
| 173 |
+
"Togo"
|
| 174 |
+
],
|
| 175 |
+
"Fongbe": [
|
| 176 |
+
"Benin"
|
| 177 |
+
],
|
| 178 |
+
"Mbo": [
|
| 179 |
+
"Cameroon"
|
| 180 |
+
],
|
| 181 |
+
"Lingala": [
|
| 182 |
+
"DR Congo",
|
| 183 |
+
"Republic of Congo"
|
| 184 |
+
],
|
| 185 |
+
"Umbundu": [
|
| 186 |
+
"Angola"
|
| 187 |
+
],
|
| 188 |
+
"Malagasy": [
|
| 189 |
+
"Madagascar"
|
| 190 |
+
],
|
| 191 |
+
"Arabic": [
|
| 192 |
+
"Egypt",
|
| 193 |
+
"Libya",
|
| 194 |
+
"Tunisia",
|
| 195 |
+
"Algeria",
|
| 196 |
+
"Morocco",
|
| 197 |
+
"Sudan"
|
| 198 |
+
],
|
| 199 |
+
"Kabyle": [
|
| 200 |
+
"Algeria"
|
| 201 |
+
],
|
| 202 |
+
"Tamazight": [
|
| 203 |
+
"Morocco",
|
| 204 |
+
"Algeria"
|
| 205 |
+
],
|
| 206 |
+
"Wolaytta": [
|
| 207 |
+
"Ethiopia"
|
| 208 |
+
],
|
| 209 |
+
"Soga": [
|
| 210 |
+
"Uganda"
|
| 211 |
+
]
|
| 212 |
+
}
|
src/data/model_family_metadata.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Phi-4": {
|
| 3 |
+
"display_name": "Microsoft Phi-4",
|
| 4 |
+
"description": "Multimodal speech-language baseline evaluated for speech-to-text."
|
| 5 |
+
},
|
| 6 |
+
"facebook-data2vec": {
|
| 7 |
+
"display_name": "Facebook Data2Vec",
|
| 8 |
+
"description": "Self-supervised speech representations fine-tuned for ASR."
|
| 9 |
+
},
|
| 10 |
+
"facebook-mms": {
|
| 11 |
+
"display_name": "Facebook MMS",
|
| 12 |
+
"description": "Massively Multilingual Speech CTC models."
|
| 13 |
+
},
|
| 14 |
+
"facebook-wav2vec2": {
|
| 15 |
+
"display_name": "Facebook Wav2Vec2",
|
| 16 |
+
"description": "Wav2Vec2 ASR baselines trained on English speech."
|
| 17 |
+
},
|
| 18 |
+
"facebook-wav2vec2-conformer": {
|
| 19 |
+
"display_name": "Facebook Wav2Vec2 Conformer",
|
| 20 |
+
"description": "Conformer variants of Wav2Vec2 for ASR."
|
| 21 |
+
},
|
| 22 |
+
"facebook_hubert": {
|
| 23 |
+
"display_name": "Facebook HuBERT",
|
| 24 |
+
"description": "HuBERT speech representation models fine-tuned for ASR."
|
| 25 |
+
},
|
| 26 |
+
"granite-speech": {
|
| 27 |
+
"display_name": "IBM Granite Speech",
|
| 28 |
+
"description": "Speech-language models with Granite LLM backbones."
|
| 29 |
+
},
|
| 30 |
+
"kyutai": {
|
| 31 |
+
"display_name": "Kyutai STT",
|
| 32 |
+
"description": "Streaming speech-to-text model."
|
| 33 |
+
},
|
| 34 |
+
"lite_asr": {
|
| 35 |
+
"display_name": "Lite ASR / EfficientSpeech",
|
| 36 |
+
"description": "Efficient Whisper-derived ASR variants tuned for speed or accuracy."
|
| 37 |
+
},
|
| 38 |
+
"moonshine": {
|
| 39 |
+
"display_name": "Moonshine",
|
| 40 |
+
"description": "Compact encoder-decoder ASR models optimized for small-footprint use."
|
| 41 |
+
},
|
| 42 |
+
"nvidia-nemo": {
|
| 43 |
+
"display_name": "NVIDIA NeMo ASR",
|
| 44 |
+
"description": "NeMo encoder-decoder and transducer ASR models."
|
| 45 |
+
},
|
| 46 |
+
"omnilingual": {
|
| 47 |
+
"display_name": "Facebook Omnilingual ASR",
|
| 48 |
+
"description": "Omnilingual CTC and LLM ASR variants."
|
| 49 |
+
},
|
| 50 |
+
"openai-whisper": {
|
| 51 |
+
"display_name": "OpenAI Whisper",
|
| 52 |
+
"description": "Whisper encoder-decoder models across multiple model sizes."
|
| 53 |
+
},
|
| 54 |
+
"paza": {
|
| 55 |
+
"display_name": "Paza by Microsoft Research Africa, Nairobi",
|
| 56 |
+
"description": "Paza ASR and speech-language models adapted for African-language evaluation."
|
| 57 |
+
},
|
| 58 |
+
"qwen2-audio": {
|
| 59 |
+
"display_name": "Qwen2 Audio",
|
| 60 |
+
"description": "Qwen2 Audio speech-language models."
|
| 61 |
+
},
|
| 62 |
+
"seamlessM4T": {
|
| 63 |
+
"display_name": "SeamlessM4T",
|
| 64 |
+
"description": "Meta SeamlessM4T multilingual speech-to-text models."
|
| 65 |
+
}
|
| 66 |
+
}
|
src/data_processing.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data loading, filtering, aggregation and metric computation for PazaBench.
|
| 3 |
+
|
| 4 |
+
This module handles all data processing logic:
|
| 5 |
+
- Loading ASR results from CSV (with HuggingFace Hub fallback)
|
| 6 |
+
- Filtering dataframes by model, language, dataset, region
|
| 7 |
+
- Aggregating results by language with proper RTFx computation
|
| 8 |
+
- Building metric pivot tables and DataFrames
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from functools import lru_cache
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import pandas as pd
|
| 15 |
+
from huggingface_hub import hf_hub_download
|
| 16 |
+
|
| 17 |
+
from src.constants import (
|
| 18 |
+
ASR_DISPLAY_COLUMNS,
|
| 19 |
+
ASR_NUMERIC_COLUMNS,
|
| 20 |
+
ASR_TEXT_COLUMNS,
|
| 21 |
+
canonicalize_model_family,
|
| 22 |
+
DEFAULT_VIEW_MODE,
|
| 23 |
+
FILTER_COLUMN_ORDER,
|
| 24 |
+
LANGUAGE_NAME_MAPPING,
|
| 25 |
+
METRIC_CONFIGS,
|
| 26 |
+
RESULTS_CSV_FILENAME,
|
| 27 |
+
RESULTS_CSV_PATH,
|
| 28 |
+
VIEW_MODE_COLUMNS,
|
| 29 |
+
)
|
| 30 |
+
from src.display.styling import format_metric_value
|
| 31 |
+
from src.envs import HF_ENABLED, RESULTS_REPO, TOKEN
|
| 32 |
+
from src.language_metadata import get_language_regions
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
REQUIRED_RESULTS_COLUMNS = [
|
| 36 |
+
"model_family",
|
| 37 |
+
"model",
|
| 38 |
+
"dataset_group",
|
| 39 |
+
"split",
|
| 40 |
+
"language",
|
| 41 |
+
"wer",
|
| 42 |
+
"cer",
|
| 43 |
+
"num_samples",
|
| 44 |
+
]
|
| 45 |
+
def _validate_results_schema(frame: pd.DataFrame) -> None:
|
| 46 |
+
"""Validate required columns are present without blocking app startup."""
|
| 47 |
+
missing = [column for column in REQUIRED_RESULTS_COLUMNS if column not in frame.columns]
|
| 48 |
+
if missing:
|
| 49 |
+
raise ValueError(
|
| 50 |
+
"ASR results CSV is missing required columns: "
|
| 51 |
+
+ ", ".join(sorted(missing))
|
| 52 |
+
+ ". Re-run the aggregator and ensure new fields are mapped correctly."
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Normalize empty strings in text columns to NA (later filled as "Unknown").
|
| 56 |
+
for column in ASR_TEXT_COLUMNS:
|
| 57 |
+
if column in frame.columns:
|
| 58 |
+
frame[column] = frame[column].replace(r"^\s*$", pd.NA, regex=True)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# =============================================================================
|
| 62 |
+
# Data Loading
|
| 63 |
+
# =============================================================================
|
| 64 |
+
|
| 65 |
+
@lru_cache(maxsize=1)
|
| 66 |
+
def _cached_asr_results(csv_path: str) -> pd.DataFrame:
|
| 67 |
+
"""Load ASR results from HuggingFace Hub or fall back to local file."""
|
| 68 |
+
path = Path(csv_path)
|
| 69 |
+
|
| 70 |
+
if HF_ENABLED and RESULTS_REPO:
|
| 71 |
+
try:
|
| 72 |
+
print(f"Downloading {RESULTS_CSV_FILENAME} from {RESULTS_REPO}...")
|
| 73 |
+
downloaded_path = hf_hub_download(
|
| 74 |
+
repo_id=RESULTS_REPO,
|
| 75 |
+
filename=RESULTS_CSV_FILENAME,
|
| 76 |
+
repo_type="dataset",
|
| 77 |
+
token=TOKEN,
|
| 78 |
+
)
|
| 79 |
+
path = Path(downloaded_path)
|
| 80 |
+
print(f"Successfully downloaded results from {RESULTS_REPO}")
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"Could not download from HuggingFace Hub: {e}")
|
| 83 |
+
print(f"Falling back to local file at {csv_path}")
|
| 84 |
+
path = Path(csv_path)
|
| 85 |
+
|
| 86 |
+
if not path.exists():
|
| 87 |
+
raise FileNotFoundError(
|
| 88 |
+
f"ASR results CSV not found at {path}. "
|
| 89 |
+
"Please generate it via src.aggregate_results before launching the app."
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
frame = pd.read_csv(path)
|
| 93 |
+
|
| 94 |
+
_validate_results_schema(frame)
|
| 95 |
+
|
| 96 |
+
# Convert numeric columns
|
| 97 |
+
for column in ASR_NUMERIC_COLUMNS:
|
| 98 |
+
if column in frame.columns:
|
| 99 |
+
frame[column] = pd.to_numeric(frame[column], errors="coerce")
|
| 100 |
+
|
| 101 |
+
# Fill missing text columns
|
| 102 |
+
for column in ASR_TEXT_COLUMNS:
|
| 103 |
+
if column in frame.columns:
|
| 104 |
+
frame[column] = frame[column].fillna("Unknown")
|
| 105 |
+
|
| 106 |
+
if "model_family" in frame.columns:
|
| 107 |
+
frame["model_family"] = frame["model_family"].map(canonicalize_model_family)
|
| 108 |
+
|
| 109 |
+
# Normalize language names
|
| 110 |
+
if "language" in frame.columns:
|
| 111 |
+
frame["language"] = frame["language"].replace(LANGUAGE_NAME_MAPPING)
|
| 112 |
+
|
| 113 |
+
# Filter out rows with very few samples
|
| 114 |
+
MIN_SAMPLES_THRESHOLD = 10
|
| 115 |
+
if "num_samples" in frame.columns:
|
| 116 |
+
frame = frame[frame["num_samples"] >= MIN_SAMPLES_THRESHOLD]
|
| 117 |
+
|
| 118 |
+
# Add region metadata column
|
| 119 |
+
if "language" in frame.columns:
|
| 120 |
+
frame["african_region"] = frame["language"].apply(
|
| 121 |
+
lambda x: ", ".join(get_language_regions(x))
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
return frame
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def load_asr_results(csv_path: Path = RESULTS_CSV_PATH) -> pd.DataFrame:
|
| 128 |
+
"""Load ASR results DataFrame."""
|
| 129 |
+
return _cached_asr_results(str(csv_path))
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# =============================================================================
|
| 133 |
+
# Filtering
|
| 134 |
+
# =============================================================================
|
| 135 |
+
|
| 136 |
+
def _sorted_column_values(frame: pd.DataFrame, column: str) -> list[str]:
|
| 137 |
+
"""Get sorted unique values from a column, with 'Unknown' at the end."""
|
| 138 |
+
if column not in frame.columns or frame.empty:
|
| 139 |
+
return []
|
| 140 |
+
values = sorted({value for value in frame[column].dropna().unique() if value != "Unknown"})
|
| 141 |
+
if (frame[column] == "Unknown").any():
|
| 142 |
+
values.append("Unknown")
|
| 143 |
+
return values
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def get_filter_options(frame: pd.DataFrame) -> dict[str, list[str]]:
|
| 147 |
+
"""Get all available filter options from the DataFrame."""
|
| 148 |
+
from src.about import get_dataset_group_label
|
| 149 |
+
from src.language_metadata import get_all_regions
|
| 150 |
+
|
| 151 |
+
options = {column: _sorted_column_values(frame, column) for column in FILTER_COLUMN_ORDER}
|
| 152 |
+
options["dataset_group_labels"] = [
|
| 153 |
+
get_dataset_group_label(dg, frame) for dg in options.get("dataset_group", [])
|
| 154 |
+
]
|
| 155 |
+
options["african_region"] = get_all_regions()
|
| 156 |
+
options["language_support"] = _sorted_column_values(frame, "language_support")
|
| 157 |
+
return options
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def get_languages_for_filters(
|
| 161 |
+
frame: pd.DataFrame,
|
| 162 |
+
african_regions: list[str] | None = None,
|
| 163 |
+
) -> list[str]:
|
| 164 |
+
"""Get languages that match the given region filter."""
|
| 165 |
+
from src.language_metadata import get_languages_by_region
|
| 166 |
+
|
| 167 |
+
if not african_regions:
|
| 168 |
+
return _sorted_column_values(frame, "language")
|
| 169 |
+
|
| 170 |
+
region_languages: set[str] = set()
|
| 171 |
+
for region in african_regions:
|
| 172 |
+
region_languages.update(get_languages_by_region(region))
|
| 173 |
+
|
| 174 |
+
available_languages = set(_sorted_column_values(frame, "language"))
|
| 175 |
+
return sorted(region_languages & available_languages)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def filter_asr_dataframe(
|
| 179 |
+
frame: pd.DataFrame,
|
| 180 |
+
*,
|
| 181 |
+
models: list[str] | None = None,
|
| 182 |
+
languages: list[str] | None = None,
|
| 183 |
+
dataset_groups: list[str] | None = None,
|
| 184 |
+
language_support: list[str] | None = None,
|
| 185 |
+
african_regions: list[str] | None = None,
|
| 186 |
+
) -> pd.DataFrame:
|
| 187 |
+
"""Filter the ASR results DataFrame by the given criteria."""
|
| 188 |
+
from src.language_metadata import get_languages_by_region
|
| 189 |
+
|
| 190 |
+
filtered = frame.copy()
|
| 191 |
+
|
| 192 |
+
if models:
|
| 193 |
+
filtered = filtered[filtered["model"].isin(models)]
|
| 194 |
+
if languages:
|
| 195 |
+
filtered = filtered[filtered["language"].isin(languages)]
|
| 196 |
+
if dataset_groups:
|
| 197 |
+
filtered = filtered[filtered["dataset_group"].isin(dataset_groups)]
|
| 198 |
+
if language_support and "language_support" in filtered.columns:
|
| 199 |
+
filtered = filtered[filtered["language_support"].isin(language_support)]
|
| 200 |
+
|
| 201 |
+
# Apply region filter by getting languages for selected regions
|
| 202 |
+
if african_regions:
|
| 203 |
+
region_languages: set[str] = set()
|
| 204 |
+
for region in african_regions:
|
| 205 |
+
region_languages.update(get_languages_by_region(region))
|
| 206 |
+
if region_languages and "language" in filtered.columns:
|
| 207 |
+
filtered = filtered[filtered["language"].isin(region_languages)]
|
| 208 |
+
|
| 209 |
+
return filtered
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def prepare_display_dataframe(
|
| 213 |
+
frame: pd.DataFrame,
|
| 214 |
+
max_rows: int,
|
| 215 |
+
include_split: bool = True
|
| 216 |
+
) -> pd.DataFrame:
|
| 217 |
+
"""Prepare a DataFrame for display with proper formatting."""
|
| 218 |
+
columns = [col for col in ASR_DISPLAY_COLUMNS if col in frame.columns]
|
| 219 |
+
if not include_split and "split" in columns:
|
| 220 |
+
columns = [col for col in columns if col != "split"]
|
| 221 |
+
|
| 222 |
+
display = frame.loc[:, columns].copy()
|
| 223 |
+
|
| 224 |
+
for column in ["wer", "cer", "rtfx"]:
|
| 225 |
+
if column in display.columns:
|
| 226 |
+
display[column] = display[column].round(3)
|
| 227 |
+
for column in ["duration_sec", "inference_time_sec"]:
|
| 228 |
+
if column in display.columns:
|
| 229 |
+
display[column] = display[column].round(2)
|
| 230 |
+
|
| 231 |
+
display = display.head(max_rows)
|
| 232 |
+
display.insert(0, "", range(1, len(display) + 1))
|
| 233 |
+
return display
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# =============================================================================
|
| 237 |
+
# Aggregation
|
| 238 |
+
# =============================================================================
|
| 239 |
+
|
| 240 |
+
def _join_unique_values(values: pd.Series) -> str | None:
|
| 241 |
+
"""Join unique non-empty values from a Series."""
|
| 242 |
+
if values is None:
|
| 243 |
+
return None
|
| 244 |
+
unique_values = [str(v) for v in values.dropna().unique() if str(v).strip()]
|
| 245 |
+
return ", ".join(unique_values) if unique_values else None
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def _weighted_average(series: pd.Series, weights: pd.Series) -> float | None:
|
| 249 |
+
"""Compute weighted average of a series."""
|
| 250 |
+
numeric = pd.to_numeric(series, errors="coerce").dropna()
|
| 251 |
+
if numeric.empty:
|
| 252 |
+
return None
|
| 253 |
+
aligned_weights = weights.loc[numeric.index].fillna(0).astype(float)
|
| 254 |
+
total_weight = aligned_weights.sum()
|
| 255 |
+
if total_weight <= 0:
|
| 256 |
+
return float(round(numeric.mean(), 4))
|
| 257 |
+
return float(round((numeric * aligned_weights).sum() / total_weight, 4))
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _compute_rtfx_from_totals(
|
| 261 |
+
duration_series: pd.Series,
|
| 262 |
+
inference_time_series: pd.Series
|
| 263 |
+
) -> float | None:
|
| 264 |
+
"""Compute RTFx as Total Audio Duration / Total Transcription Time."""
|
| 265 |
+
duration_numeric = pd.to_numeric(duration_series, errors="coerce").dropna()
|
| 266 |
+
inference_numeric = pd.to_numeric(inference_time_series, errors="coerce").dropna()
|
| 267 |
+
|
| 268 |
+
common_index = duration_numeric.index.intersection(inference_numeric.index)
|
| 269 |
+
if common_index.empty:
|
| 270 |
+
return None
|
| 271 |
+
|
| 272 |
+
total_duration = duration_numeric.loc[common_index].sum()
|
| 273 |
+
total_inference = inference_numeric.loc[common_index].sum()
|
| 274 |
+
|
| 275 |
+
if total_inference <= 0:
|
| 276 |
+
return None
|
| 277 |
+
|
| 278 |
+
return float(round(total_duration / total_inference, 4))
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def aggregate_by_language(frame: pd.DataFrame) -> pd.DataFrame:
|
| 282 |
+
"""Aggregate results by model and language with proper metric computation."""
|
| 283 |
+
if frame.empty or "language" not in frame.columns:
|
| 284 |
+
return frame
|
| 285 |
+
|
| 286 |
+
group_keys = [col for col in ["model_family", "model", "language"] if col in frame.columns]
|
| 287 |
+
if not group_keys:
|
| 288 |
+
return frame
|
| 289 |
+
|
| 290 |
+
aggregated_rows: list[dict[str, object]] = []
|
| 291 |
+
for _, group in frame.groupby(group_keys, dropna=False):
|
| 292 |
+
weight_series = group["num_samples"] if "num_samples" in group.columns else pd.Series([1] * len(group))
|
| 293 |
+
weight_series = pd.to_numeric(weight_series, errors="coerce").fillna(0).astype(float)
|
| 294 |
+
|
| 295 |
+
# Compute RTFx: sum(duration) / sum(inference_time)
|
| 296 |
+
rtfx_value = None
|
| 297 |
+
if "duration_sec" in group.columns and "inference_time_sec" in group.columns:
|
| 298 |
+
rtfx_value = _compute_rtfx_from_totals(group["duration_sec"], group["inference_time_sec"])
|
| 299 |
+
|
| 300 |
+
# Sum durations and inference times
|
| 301 |
+
total_duration = None
|
| 302 |
+
total_inference = None
|
| 303 |
+
if "duration_sec" in group.columns:
|
| 304 |
+
duration_numeric = pd.to_numeric(group["duration_sec"], errors="coerce").dropna()
|
| 305 |
+
if not duration_numeric.empty:
|
| 306 |
+
total_duration = float(round(duration_numeric.sum(), 4))
|
| 307 |
+
if "inference_time_sec" in group.columns:
|
| 308 |
+
inference_numeric = pd.to_numeric(group["inference_time_sec"], errors="coerce").dropna()
|
| 309 |
+
if not inference_numeric.empty:
|
| 310 |
+
total_inference = float(round(inference_numeric.sum(), 4))
|
| 311 |
+
|
| 312 |
+
aggregated_rows.append({
|
| 313 |
+
"model_family": group.get("model_family", pd.Series([None])).iloc[0] if "model_family" in group else None,
|
| 314 |
+
"model": group.get("model", pd.Series([None])).iloc[0] if "model" in group else None,
|
| 315 |
+
"dataset_group": _join_unique_values(group["dataset_group"]) if "dataset_group" in group.columns else None,
|
| 316 |
+
"language": group.get("language", pd.Series([None])).iloc[0] if "language" in group else None,
|
| 317 |
+
"region": _join_unique_values(group["region"]) if "region" in group.columns else None,
|
| 318 |
+
"wer": _weighted_average(group["wer"], weight_series) if "wer" in group.columns else None,
|
| 319 |
+
"cer": _weighted_average(group["cer"], weight_series) if "cer" in group.columns else None,
|
| 320 |
+
"rtfx": rtfx_value,
|
| 321 |
+
"duration_sec": total_duration,
|
| 322 |
+
"inference_time_sec": total_inference,
|
| 323 |
+
"num_samples": int(weight_series.sum()) if weight_series.sum() > 0 else len(group),
|
| 324 |
+
})
|
| 325 |
+
|
| 326 |
+
return pd.DataFrame(aggregated_rows)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
# =============================================================================
|
| 330 |
+
# Metric Tables
|
| 331 |
+
# =============================================================================
|
| 332 |
+
|
| 333 |
+
def _pivot_metric_table(aggregated: pd.DataFrame, metric: str, view_mode: str) -> pd.DataFrame:
|
| 334 |
+
"""Create a pivot table for a specific metric."""
|
| 335 |
+
if aggregated.empty or metric not in aggregated.columns:
|
| 336 |
+
return pd.DataFrame()
|
| 337 |
+
|
| 338 |
+
column_key = VIEW_MODE_COLUMNS.get(view_mode, VIEW_MODE_COLUMNS[DEFAULT_VIEW_MODE])
|
| 339 |
+
if column_key not in aggregated.columns:
|
| 340 |
+
fallback = "model" if "model" in aggregated.columns else None
|
| 341 |
+
if fallback is None:
|
| 342 |
+
return pd.DataFrame()
|
| 343 |
+
column_key = fallback
|
| 344 |
+
|
| 345 |
+
pivot = (
|
| 346 |
+
aggregated
|
| 347 |
+
.pivot_table(index="language", columns=column_key, values=metric, aggfunc="median")
|
| 348 |
+
.sort_index()
|
| 349 |
+
)
|
| 350 |
+
pivot = pivot.dropna(how="all")
|
| 351 |
+
if pivot.empty:
|
| 352 |
+
return pivot
|
| 353 |
+
|
| 354 |
+
# Sort columns by performance
|
| 355 |
+
column_scores = pivot.mean(skipna=True)
|
| 356 |
+
ascending = METRIC_CONFIGS.get(metric, {}).get("better", "lower") == "lower"
|
| 357 |
+
ordered_columns = column_scores.sort_values(ascending=ascending).index.tolist()
|
| 358 |
+
|
| 359 |
+
# Ensure all columns are included
|
| 360 |
+
missing = [col for col in pivot.columns if col not in ordered_columns]
|
| 361 |
+
ordered_columns.extend(missing)
|
| 362 |
+
|
| 363 |
+
return pivot[ordered_columns]
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def _build_metric_table_html(pivot: pd.DataFrame, metric: str) -> str:
|
| 367 |
+
"""Build HTML table for a metric pivot table."""
|
| 368 |
+
config = METRIC_CONFIGS[metric]
|
| 369 |
+
if pivot.empty:
|
| 370 |
+
return f"<p class='metric-table-empty'>No {config['label']} data available for the current filters.</p>"
|
| 371 |
+
|
| 372 |
+
header_cells = "".join(f"<th>{col}</th>" for col in pivot.columns)
|
| 373 |
+
rows_html: list[str] = []
|
| 374 |
+
|
| 375 |
+
for i, (language, row) in enumerate(pivot.iterrows(), 1):
|
| 376 |
+
cell_html = []
|
| 377 |
+
for column in pivot.columns:
|
| 378 |
+
value = row[column]
|
| 379 |
+
display = format_metric_value(value, config["fmt"])
|
| 380 |
+
cell_html.append(f"<td>{display}</td>")
|
| 381 |
+
|
| 382 |
+
onclick = (
|
| 383 |
+
"(function(el){var tr=el.parentElement;"
|
| 384 |
+
"var isHighlighted=tr.classList.contains('row-highlighted');"
|
| 385 |
+
"tr.parentElement.querySelectorAll('tr').forEach(function(r){"
|
| 386 |
+
"r.classList.remove('row-highlighted')});"
|
| 387 |
+
"if(!isHighlighted){tr.classList.add('row-highlighted')}})(this)"
|
| 388 |
+
)
|
| 389 |
+
rows_html.append(
|
| 390 |
+
f"<tr><td>{i}</td><th onclick=\"{onclick}\">{language}</th>{''.join(cell_html)}</tr>"
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
caption = f"<caption>Columns sorted by overall {config['label']} performance</caption>"
|
| 394 |
+
table_html = (
|
| 395 |
+
"<div class='metric-table-wrapper'>"
|
| 396 |
+
f"<table class='metric-table'>{caption}<thead><tr><th>#</th><th>Language</th>{header_cells}</tr></thead>"
|
| 397 |
+
f"<tbody>{''.join(rows_html)}</tbody></table></div>"
|
| 398 |
+
)
|
| 399 |
+
return table_html
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def _build_metric_dataframe(pivot: pd.DataFrame, metric: str) -> pd.DataFrame:
|
| 403 |
+
"""Build a sortable DataFrame from the pivot table."""
|
| 404 |
+
if pivot.empty:
|
| 405 |
+
return pd.DataFrame()
|
| 406 |
+
|
| 407 |
+
df = pivot.reset_index()
|
| 408 |
+
df = df.rename(columns={"language": "Language"})
|
| 409 |
+
|
| 410 |
+
# Round numeric columns
|
| 411 |
+
for col in df.columns:
|
| 412 |
+
if col != "Language" and df[col].dtype in ['float64', 'float32']:
|
| 413 |
+
df[col] = df[col].round(2)
|
| 414 |
+
|
| 415 |
+
df.insert(0, "", range(1, len(df) + 1))
|
| 416 |
+
return df
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def compute_metric_tables(
|
| 420 |
+
models: list[str] | None,
|
| 421 |
+
languages: list[str] | None,
|
| 422 |
+
dataset_groups: list[str] | None,
|
| 423 |
+
view_mode: str,
|
| 424 |
+
african_regions: list[str] | None = None,
|
| 425 |
+
language_support: list[str] | None = None,
|
| 426 |
+
asr_results_df: pd.DataFrame | None = None,
|
| 427 |
+
) -> dict[str, str]:
|
| 428 |
+
"""Compute HTML metric tables for all metrics."""
|
| 429 |
+
if asr_results_df is None or asr_results_df.empty:
|
| 430 |
+
return {metric: "<p class='metric-table-empty'>No ASR results loaded.</p>" for metric in METRIC_CONFIGS}
|
| 431 |
+
|
| 432 |
+
filtered = filter_asr_dataframe(
|
| 433 |
+
asr_results_df,
|
| 434 |
+
models=models,
|
| 435 |
+
languages=languages,
|
| 436 |
+
dataset_groups=dataset_groups,
|
| 437 |
+
language_support=language_support,
|
| 438 |
+
african_regions=african_regions,
|
| 439 |
+
)
|
| 440 |
+
aggregated = aggregate_by_language(filtered)
|
| 441 |
+
|
| 442 |
+
tables: dict[str, str] = {}
|
| 443 |
+
for metric in METRIC_CONFIGS:
|
| 444 |
+
pivot = _pivot_metric_table(aggregated, metric, view_mode)
|
| 445 |
+
tables[metric] = _build_metric_table_html(pivot, metric)
|
| 446 |
+
return tables
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def compute_metric_dataframes(
|
| 450 |
+
models: list[str] | None,
|
| 451 |
+
languages: list[str] | None,
|
| 452 |
+
dataset_groups: list[str] | None,
|
| 453 |
+
view_mode: str,
|
| 454 |
+
african_regions: list[str] | None = None,
|
| 455 |
+
language_support: list[str] | None = None,
|
| 456 |
+
asr_results_df: pd.DataFrame | None = None,
|
| 457 |
+
) -> dict[str, pd.DataFrame]:
|
| 458 |
+
"""Compute metric DataFrames for interactive sorting."""
|
| 459 |
+
if asr_results_df is None or asr_results_df.empty:
|
| 460 |
+
return {metric: pd.DataFrame() for metric in METRIC_CONFIGS}
|
| 461 |
+
|
| 462 |
+
filtered = filter_asr_dataframe(
|
| 463 |
+
asr_results_df,
|
| 464 |
+
models=models,
|
| 465 |
+
languages=languages,
|
| 466 |
+
dataset_groups=dataset_groups,
|
| 467 |
+
language_support=language_support,
|
| 468 |
+
african_regions=african_regions,
|
| 469 |
+
)
|
| 470 |
+
aggregated = aggregate_by_language(filtered)
|
| 471 |
+
|
| 472 |
+
dataframes: dict[str, pd.DataFrame] = {}
|
| 473 |
+
for metric in METRIC_CONFIGS:
|
| 474 |
+
pivot = _pivot_metric_table(aggregated, metric, view_mode)
|
| 475 |
+
dataframes[metric] = _build_metric_dataframe(pivot, metric)
|
| 476 |
+
return dataframes
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
# =============================================================================
|
| 480 |
+
# Helper Functions
|
| 481 |
+
# =============================================================================
|
| 482 |
+
|
| 483 |
+
def strip_dataset_label(label: str) -> str:
|
| 484 |
+
"""Strip descriptor from dataset label.
|
| 485 |
+
E.g., 'ALFFA (read speech & broadcast news, 3 languages)' -> 'ALFFA'
|
| 486 |
+
"""
|
| 487 |
+
if " (" in label:
|
| 488 |
+
return label.split(" (")[0]
|
| 489 |
+
return label
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def strip_dataset_labels(labels: list[str] | None) -> list[str]:
|
| 493 |
+
"""Strip descriptors from a list of dataset labels."""
|
| 494 |
+
if not labels:
|
| 495 |
+
return []
|
| 496 |
+
return [strip_dataset_label(label) for label in labels]
|
src/display/css_html_js.py
ADDED
|
@@ -0,0 +1,2205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
custom_css = """
|
| 2 |
+
|
| 3 |
+
/* ===== FORCE LIGHT MODE ===== */
|
| 4 |
+
/* Override Gradio's dark mode completely - this app is light mode only */
|
| 5 |
+
:root, html, body, .gradio-container, .dark, body.dark, html.dark {
|
| 6 |
+
--body-background-fill: white !important;
|
| 7 |
+
--background-fill-primary: white !important;
|
| 8 |
+
--background-fill-secondary: #f9fafb !important;
|
| 9 |
+
--body-text-color: #1f2937 !important;
|
| 10 |
+
--body-text-color-subdued: #6b7280 !important;
|
| 11 |
+
--block-background-fill: white !important;
|
| 12 |
+
--block-border-color: #e5e7eb !important;
|
| 13 |
+
--block-label-background-fill: #f3f4f6 !important;
|
| 14 |
+
--block-label-text-color: #374151 !important;
|
| 15 |
+
--block-title-text-color: #111827 !important;
|
| 16 |
+
--input-background-fill: white !important;
|
| 17 |
+
--input-border-color: #d1d5db !important;
|
| 18 |
+
--table-even-background-fill: #f9fafb !important;
|
| 19 |
+
--table-odd-background-fill: white !important;
|
| 20 |
+
--table-row-focus: #e0f2fe !important;
|
| 21 |
+
--table-border-color: #e5e7eb !important;
|
| 22 |
+
--border-color-primary: #e5e7eb !important;
|
| 23 |
+
--border-color-accent: #8CD0FF !important;
|
| 24 |
+
--color-accent: #8CD0FF !important;
|
| 25 |
+
--neutral-50: #f9fafb !important;
|
| 26 |
+
--neutral-100: #f3f4f6 !important;
|
| 27 |
+
--neutral-200: #e5e7eb !important;
|
| 28 |
+
--neutral-300: #d1d5db !important;
|
| 29 |
+
--neutral-400: #9ca3af !important;
|
| 30 |
+
--neutral-500: #6b7280 !important;
|
| 31 |
+
--neutral-600: #4b5563 !important;
|
| 32 |
+
--neutral-700: #374151 !important;
|
| 33 |
+
--neutral-800: #1f2937 !important;
|
| 34 |
+
--neutral-900: #111827 !important;
|
| 35 |
+
--neutral-950: #030712 !important;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* Apply light mode background to main containers only, exclude Plotly */
|
| 39 |
+
html, body, .gradio-container {
|
| 40 |
+
background: white !important;
|
| 41 |
+
color: #1f2937 !important;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* Force light mode on Gradio table components (not Plotly) */
|
| 45 |
+
.dark .svelte-table, body.dark .svelte-table,
|
| 46 |
+
.dark [data-testid="dataframe"], body.dark [data-testid="dataframe"],
|
| 47 |
+
.table-wrap:not(.js-plotly-plot *),
|
| 48 |
+
.dataframe:not(.js-plotly-plot *) {
|
| 49 |
+
background: white !important;
|
| 50 |
+
color: #1f2937 !important;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* Target Gradio tables specifically, exclude Plotly SVG tables */
|
| 54 |
+
.gradio-container table:not(.js-plotly-plot table),
|
| 55 |
+
.dark .gradio-container table:not(.js-plotly-plot table) {
|
| 56 |
+
background: white !important;
|
| 57 |
+
color: #1f2937 !important;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.gradio-container th:not(.js-plotly-plot th),
|
| 61 |
+
.gradio-container td:not(.js-plotly-plot td),
|
| 62 |
+
.dark .gradio-container th:not(.js-plotly-plot th),
|
| 63 |
+
.dark .gradio-container td:not(.js-plotly-plot td) {
|
| 64 |
+
background-color: inherit !important;
|
| 65 |
+
color: #1f2937 !important;
|
| 66 |
+
border-color: #e5e7eb !important;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.gradio-container thead:not(.js-plotly-plot thead),
|
| 70 |
+
.dark .gradio-container thead:not(.js-plotly-plot thead) {
|
| 71 |
+
background: #f8fafc !important;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.gradio-container tbody tr:nth-child(even):not(.js-plotly-plot tr),
|
| 75 |
+
.dark .gradio-container tbody tr:nth-child(even):not(.js-plotly-plot tr) {
|
| 76 |
+
background: #f9fafb !important;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.gradio-container tbody tr:nth-child(odd):not(.js-plotly-plot tr),
|
| 80 |
+
.dark .gradio-container tbody tr:nth-child(odd):not(.js-plotly-plot tr) {
|
| 81 |
+
background: white !important;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* Override prefers-color-scheme dark for main elements */
|
| 85 |
+
@media (prefers-color-scheme: dark) {
|
| 86 |
+
:root, html, body, .gradio-container {
|
| 87 |
+
--body-background-fill: white !important;
|
| 88 |
+
--background-fill-primary: white !important;
|
| 89 |
+
--body-text-color: #1f2937 !important;
|
| 90 |
+
background: white !important;
|
| 91 |
+
color: #1f2937 !important;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* Ensure Plotly charts render correctly - DO NOT override their styles */
|
| 96 |
+
.js-plotly-plot, .js-plotly-plot *,
|
| 97 |
+
.plotly, .plotly *,
|
| 98 |
+
.js-plotly-plot svg, .js-plotly-plot svg * {
|
| 99 |
+
background: unset !important;
|
| 100 |
+
background-color: unset !important;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* Reset Plotly container backgrounds */
|
| 104 |
+
.js-plotly-plot .plotly .main-svg,
|
| 105 |
+
.js-plotly-plot .plotly .bg {
|
| 106 |
+
fill: white !important;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* Microsoft-style font family - using system fonts with proper bold weights */
|
| 110 |
+
/* Note: Segoe UI from cdnfonts may not have all weights, rely on system fallbacks */
|
| 111 |
+
|
| 112 |
+
/* Apply font globally */
|
| 113 |
+
.gradio-container,
|
| 114 |
+
.gradio-container *,
|
| 115 |
+
body {
|
| 116 |
+
font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/* Bold text weights */
|
| 120 |
+
b, strong,
|
| 121 |
+
.gradio-container b,
|
| 122 |
+
.gradio-container strong,
|
| 123 |
+
.prose b,
|
| 124 |
+
.prose strong,
|
| 125 |
+
.md b,
|
| 126 |
+
.md strong,
|
| 127 |
+
.markdown-code b,
|
| 128 |
+
.markdown-code strong,
|
| 129 |
+
[class*="markdown"] b,
|
| 130 |
+
[class*="markdown"] strong,
|
| 131 |
+
p b, p strong,
|
| 132 |
+
span b, span strong,
|
| 133 |
+
div b, div strong {
|
| 134 |
+
font-weight: 600 !important;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
/* Banner image styling */
|
| 138 |
+
#banner-image {
|
| 139 |
+
width: 100%;
|
| 140 |
+
margin-bottom: 1rem;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
#banner-image img {
|
| 144 |
+
width: 100%;
|
| 145 |
+
height: auto;
|
| 146 |
+
object-fit: contain;
|
| 147 |
+
border-radius: 8px;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/* Custom metric table variables (light mode only) */
|
| 151 |
+
:root {
|
| 152 |
+
--metric-table-bg: #ffffff;
|
| 153 |
+
--metric-table-border: #d3d3d3;
|
| 154 |
+
--metric-table-header-bg: #f8fafc;
|
| 155 |
+
--metric-table-header-color: #0f172a;
|
| 156 |
+
--metric-table-row-header-bg: #f2f5f9;
|
| 157 |
+
--metric-table-caption: #666666;
|
| 158 |
+
--metric-table-empty-color: #777777;
|
| 159 |
+
--metric-table-row-highlight: #DDF1FF;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* ===== GLOBAL TEXT SELECTION FIX ===== */
|
| 163 |
+
/* Fix text selection highlight for better contrast in dark mode */
|
| 164 |
+
::selection {
|
| 165 |
+
background-color: rgba(140, 208, 255, 0.4) !important;
|
| 166 |
+
color: inherit !important;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
::-moz-selection {
|
| 170 |
+
background-color: rgba(140, 208, 255, 0.4) !important;
|
| 171 |
+
color: inherit !important;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* PazaBench brand color for buttons */
|
| 175 |
+
button.primary {
|
| 176 |
+
background-color: #8CD0FF !important;
|
| 177 |
+
border-color: #8CD0FF !important;
|
| 178 |
+
color: #0f172a !important;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
button.primary:hover {
|
| 182 |
+
background-color: #6bc0f5 !important;
|
| 183 |
+
border-color: #6bc0f5 !important;
|
| 184 |
+
color: #0f172a !important;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/* WCAG 4.1.2 Fix: Make buttons inside aria-hidden containers non-interactive */
|
| 188 |
+
/* Neutralize focusable buttons in aria-hidden containers */
|
| 189 |
+
.visually-hidden[aria-hidden="true"] button,
|
| 190 |
+
[aria-hidden="true"] .tab-container button,
|
| 191 |
+
.tab-container.visually-hidden[aria-hidden="true"] button,
|
| 192 |
+
div[aria-hidden="true"] button {
|
| 193 |
+
pointer-events: none !important;
|
| 194 |
+
visibility: hidden !important;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/* Link color + markdown table styling */
|
| 198 |
+
a {
|
| 199 |
+
text-decoration: none;
|
| 200 |
+
color: #464feb;
|
| 201 |
+
}
|
| 202 |
+
/* Change Log cards: remove link horizontal padding */
|
| 203 |
+
.changelog-cards a {
|
| 204 |
+
padding-left: 0;
|
| 205 |
+
padding-right: 0;
|
| 206 |
+
}
|
| 207 |
+
.markdown-text table th,
|
| 208 |
+
.markdown-text table td {
|
| 209 |
+
border: 1px solid #e6e6e6;
|
| 210 |
+
}
|
| 211 |
+
.markdown-text table th {
|
| 212 |
+
background-color: #f5f5f5;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* Scroll the tall Sample Distribution chart inside a fixed-height container. */
|
| 216 |
+
#language-coverage-plot {
|
| 217 |
+
max-height: 640px !important;
|
| 218 |
+
overflow-y: auto !important;
|
| 219 |
+
overflow-x: hidden !important;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Hide the Plotly logo ("Produced with Plotly") from every chart's modebar. */
|
| 223 |
+
.js-plotly-plot .modebar-btn--logo,
|
| 224 |
+
.js-plotly-plot a[data-title="Produced with Plotly"] {
|
| 225 |
+
display: none !important;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* Typography hierarchy */
|
| 229 |
+
.markdown-text {
|
| 230 |
+
font-size: 16px !important;
|
| 231 |
+
line-height: 1.6 !important;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* ===== FILTER & CUSTOMIZE ACCORDION STYLING ===== */
|
| 235 |
+
/* Match accordion text size to 15px */
|
| 236 |
+
#filter-customize-accordion .label-wrap span,
|
| 237 |
+
#filter-customize-accordion .label-wrap .open-button,
|
| 238 |
+
#filter-customize-accordion > div > button span {
|
| 239 |
+
font-size: 15px !important;
|
| 240 |
+
font-weight: 400 !important; /* Regular, not bold */
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/* ===== EXPLAINER TEXT STYLING ===== */
|
| 244 |
+
/* Reduce font size for 'Compare models at a glance' to 15px */
|
| 245 |
+
.explainer-text,
|
| 246 |
+
.explainer-text h3,
|
| 247 |
+
.explainer-text p {
|
| 248 |
+
font-size: 15px !important;
|
| 249 |
+
line-height: 1.5 !important;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/* ===== LEADERBOARD TABLE ROW NUMBER COLUMN ===== */
|
| 253 |
+
/* Make row number column (first column with empty header) narrow and regular weight */
|
| 254 |
+
#wer-leaderboard-table th:first-child,
|
| 255 |
+
#cer-leaderboard-table th:first-child,
|
| 256 |
+
#rtfx-leaderboard-table th:first-child {
|
| 257 |
+
width: 40px !important;
|
| 258 |
+
min-width: 40px !important;
|
| 259 |
+
max-width: 60px !important;
|
| 260 |
+
font-weight: 400 !important;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
#wer-leaderboard-table td:first-child,
|
| 264 |
+
#cer-leaderboard-table td:first-child,
|
| 265 |
+
#rtfx-leaderboard-table td:first-child {
|
| 266 |
+
width: 40px !important;
|
| 267 |
+
min-width: 40px !important;
|
| 268 |
+
max-width: 60px !important;
|
| 269 |
+
font-weight: 400 !important; /* Regular, not bold */
|
| 270 |
+
text-align: center;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* ===== LANGUAGE COLUMN BACKGROUND COLOR ===== */
|
| 274 |
+
/* Make language column (second column) have a light blue background for differentiation */
|
| 275 |
+
#wer-leaderboard-table th:nth-child(2),
|
| 276 |
+
#cer-leaderboard-table th:nth-child(2),
|
| 277 |
+
#rtfx-leaderboard-table th:nth-child(2) {
|
| 278 |
+
background-color: #EEF8FF !important;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
#wer-leaderboard-table td:nth-child(2),
|
| 282 |
+
#cer-leaderboard-table td:nth-child(2),
|
| 283 |
+
#rtfx-leaderboard-table td:nth-child(2) {
|
| 284 |
+
background-color: #EEF8FF !important;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/* Dark mode language column - slightly muted blue */
|
| 288 |
+
body.dark #wer-leaderboard-table th:nth-child(2),
|
| 289 |
+
body.dark #cer-leaderboard-table th:nth-child(2),
|
| 290 |
+
body.dark #rtfx-leaderboard-table th:nth-child(2),
|
| 291 |
+
body.dark #wer-leaderboard-table td:nth-child(2),
|
| 292 |
+
body.dark #cer-leaderboard-table td:nth-child(2),
|
| 293 |
+
body.dark #rtfx-leaderboard-table td:nth-child(2),
|
| 294 |
+
[data-theme="dark"] #wer-leaderboard-table th:nth-child(2),
|
| 295 |
+
[data-theme="dark"] #cer-leaderboard-table th:nth-child(2),
|
| 296 |
+
[data-theme="dark"] #rtfx-leaderboard-table th:nth-child(2),
|
| 297 |
+
[data-theme="dark"] #wer-leaderboard-table td:nth-child(2),
|
| 298 |
+
[data-theme="dark"] #cer-leaderboard-table td:nth-child(2),
|
| 299 |
+
[data-theme="dark"] #rtfx-leaderboard-table td:nth-child(2) {
|
| 300 |
+
background-color: rgba(140, 208, 255, 0.3) !important;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* ===== HIDE GRADIO DATAFRAME CONTEXT MENU ===== */
|
| 304 |
+
/* Hide the right-click context menu (filter option) as it has no function */
|
| 305 |
+
.cell-menu,
|
| 306 |
+
.context-menu,
|
| 307 |
+
[data-testid="dataframe"] .cell-menu,
|
| 308 |
+
[data-testid="dataframe"] .context-menu,
|
| 309 |
+
.table-wrap .cell-menu,
|
| 310 |
+
.table-wrap .context-menu,
|
| 311 |
+
.svelte-virtual-table-viewport .cell-menu,
|
| 312 |
+
.svelte-virtual-table-viewport .context-menu,
|
| 313 |
+
#wer-leaderboard-table .cell-menu,
|
| 314 |
+
#cer-leaderboard-table .cell-menu,
|
| 315 |
+
#rtfx-leaderboard-table .cell-menu,
|
| 316 |
+
#wer-leaderboard-table .context-menu,
|
| 317 |
+
#cer-leaderboard-table .context-menu,
|
| 318 |
+
#rtfx-leaderboard-table .context-menu {
|
| 319 |
+
display: none !important;
|
| 320 |
+
visibility: hidden !important;
|
| 321 |
+
pointer-events: none !important;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
/* Metric direction notes styling */
|
| 325 |
+
.metric-note {
|
| 326 |
+
font-size: 14px !important;
|
| 327 |
+
color: #666 !important;
|
| 328 |
+
margin-bottom: 8px !important;
|
| 329 |
+
padding: 8px 12px !important;
|
| 330 |
+
background-color: #f8f9fa !important;
|
| 331 |
+
border-left: 3px solid #8CD0FF !important;
|
| 332 |
+
border-radius: 4px !important;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
/* Remove nested styling from markdown content inside metric-note */
|
| 336 |
+
.metric-note p,
|
| 337 |
+
.metric-note .prose p,
|
| 338 |
+
.metric-note .prose,
|
| 339 |
+
.metric-note > div,
|
| 340 |
+
.metric-note .markdown-code,
|
| 341 |
+
.metric-note .md {
|
| 342 |
+
margin: 0 !important;
|
| 343 |
+
padding: 0 !important;
|
| 344 |
+
background: transparent !important;
|
| 345 |
+
border: none !important;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
#models-to-add-text {
|
| 349 |
+
font-size: 18px !important;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
#citation-button span {
|
| 353 |
+
font-size: 16px !important;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
#citation-button textarea {
|
| 357 |
+
font-size: 16px !important;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
#citation-button > label > button {
|
| 361 |
+
margin: 6px;
|
| 362 |
+
transform: scale(1.3);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
#leaderboard-table {
|
| 366 |
+
margin-top: 15px
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
#leaderboard-table-lite {
|
| 370 |
+
margin-top: 15px
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/* Sortable Dataframe Leaderboard Styles */
|
| 374 |
+
#wer-leaderboard-table,
|
| 375 |
+
#cer-leaderboard-table,
|
| 376 |
+
#rtfx-leaderboard-table {
|
| 377 |
+
margin-top: 15px;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
#wer-leaderboard-table table,
|
| 381 |
+
#cer-leaderboard-table table,
|
| 382 |
+
#rtfx-leaderboard-table table {
|
| 383 |
+
font-size: 14px;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
/* ===== COLUMN SIZING ===== */
|
| 387 |
+
/* Headers styling */
|
| 388 |
+
#wer-leaderboard-table th,
|
| 389 |
+
#cer-leaderboard-table th,
|
| 390 |
+
#rtfx-leaderboard-table th {
|
| 391 |
+
position: relative;
|
| 392 |
+
overflow: hidden;
|
| 393 |
+
text-overflow: ellipsis;
|
| 394 |
+
white-space: nowrap;
|
| 395 |
+
min-width: 70px; /* Ensure short model names like "paza" are not truncated */
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
/* Resize handle on right edge of headers */
|
| 399 |
+
#wer-leaderboard-table th .col-resize-handle,
|
| 400 |
+
#cer-leaderboard-table th .col-resize-handle,
|
| 401 |
+
#rtfx-leaderboard-table th .col-resize-handle {
|
| 402 |
+
position: absolute;
|
| 403 |
+
right: 0;
|
| 404 |
+
top: 0;
|
| 405 |
+
bottom: 0;
|
| 406 |
+
width: 5px;
|
| 407 |
+
cursor: col-resize;
|
| 408 |
+
background: transparent;
|
| 409 |
+
z-index: 10;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
#wer-leaderboard-table th .col-resize-handle:hover,
|
| 413 |
+
#cer-leaderboard-table th .col-resize-handle:hover,
|
| 414 |
+
#rtfx-leaderboard-table th .col-resize-handle:hover,
|
| 415 |
+
#wer-leaderboard-table th .col-resize-handle.resizing,
|
| 416 |
+
#cer-leaderboard-table th .col-resize-handle.resizing,
|
| 417 |
+
#rtfx-leaderboard-table th .col-resize-handle.resizing {
|
| 418 |
+
background: #8CD0FF;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/* Cell content handling */
|
| 422 |
+
#wer-leaderboard-table td,
|
| 423 |
+
#cer-leaderboard-table td,
|
| 424 |
+
#rtfx-leaderboard-table td {
|
| 425 |
+
overflow: hidden;
|
| 426 |
+
text-overflow: ellipsis;
|
| 427 |
+
white-space: nowrap;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
/* Show full content on hover via tooltip */
|
| 431 |
+
#wer-leaderboard-table td[title],
|
| 432 |
+
#cer-leaderboard-table td[title],
|
| 433 |
+
#rtfx-leaderboard-table td[title],
|
| 434 |
+
#wer-leaderboard-table th[title],
|
| 435 |
+
#cer-leaderboard-table th[title],
|
| 436 |
+
#rtfx-leaderboard-table th[title] {
|
| 437 |
+
cursor: help;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
/* Make column headers look clickable for sorting */
|
| 441 |
+
#wer-leaderboard-table th,
|
| 442 |
+
#cer-leaderboard-table th,
|
| 443 |
+
#rtfx-leaderboard-table th {
|
| 444 |
+
cursor: pointer;
|
| 445 |
+
user-select: none;
|
| 446 |
+
transition: background-color 0.2s ease;
|
| 447 |
+
font-weight: 600;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
/* Force header font size to 15px - must target all descendants due to Gradio 6 Svelte component nesting */
|
| 451 |
+
#wer-leaderboard-table th,
|
| 452 |
+
#wer-leaderboard-table th *,
|
| 453 |
+
#cer-leaderboard-table th,
|
| 454 |
+
#cer-leaderboard-table th *,
|
| 455 |
+
#rtfx-leaderboard-table th,
|
| 456 |
+
#rtfx-leaderboard-table th * {
|
| 457 |
+
font-size: 15px !important;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
#wer-leaderboard-table th:hover,
|
| 461 |
+
#cer-leaderboard-table th:hover,
|
| 462 |
+
#rtfx-leaderboard-table th:hover {
|
| 463 |
+
background-color: transparent !important;
|
| 464 |
+
text-decoration: underline;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
/* ===== LEADERBOARD TABLE HEADER SELECTION FIX ===== */
|
| 468 |
+
/* Remove focus/selection highlight from leaderboard table headers */
|
| 469 |
+
/* Keep only sort arrows visible, remove the box-shadow ring */
|
| 470 |
+
#wer-leaderboard-table th.focus,
|
| 471 |
+
#cer-leaderboard-table th.focus,
|
| 472 |
+
#rtfx-leaderboard-table th.focus,
|
| 473 |
+
#wer-leaderboard-table th:focus,
|
| 474 |
+
#cer-leaderboard-table th:focus,
|
| 475 |
+
#rtfx-leaderboard-table th:focus,
|
| 476 |
+
#wer-leaderboard-table th.sorted,
|
| 477 |
+
#cer-leaderboard-table th.sorted,
|
| 478 |
+
#rtfx-leaderboard-table th.sorted {
|
| 479 |
+
--ring-color: transparent !important;
|
| 480 |
+
box-shadow: none !important;
|
| 481 |
+
outline: none !important;
|
| 482 |
+
background-color: transparent !important;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
/* Dark mode specific for leaderboard tables */
|
| 486 |
+
body.dark #wer-leaderboard-table th.focus,
|
| 487 |
+
body.dark #cer-leaderboard-table th.focus,
|
| 488 |
+
body.dark #rtfx-leaderboard-table th.focus,
|
| 489 |
+
body.dark #wer-leaderboard-table th:focus,
|
| 490 |
+
body.dark #cer-leaderboard-table th:focus,
|
| 491 |
+
body.dark #rtfx-leaderboard-table th:focus,
|
| 492 |
+
body.dark #wer-leaderboard-table th.sorted,
|
| 493 |
+
body.dark #cer-leaderboard-table th.sorted,
|
| 494 |
+
body.dark #rtfx-leaderboard-table th.sorted,
|
| 495 |
+
[data-theme="dark"] #wer-leaderboard-table th.focus,
|
| 496 |
+
[data-theme="dark"] #cer-leaderboard-table th.focus,
|
| 497 |
+
[data-theme="dark"] #rtfx-leaderboard-table th.focus,
|
| 498 |
+
[data-theme="dark"] #wer-leaderboard-table th:focus,
|
| 499 |
+
[data-theme="dark"] #cer-leaderboard-table th:focus,
|
| 500 |
+
[data-theme="dark"] #rtfx-leaderboard-table th:focus,
|
| 501 |
+
[data-theme="dark"] #wer-leaderboard-table th.sorted,
|
| 502 |
+
[data-theme="dark"] #cer-leaderboard-table th.sorted,
|
| 503 |
+
[data-theme="dark"] #rtfx-leaderboard-table th.sorted {
|
| 504 |
+
--ring-color: transparent !important;
|
| 505 |
+
box-shadow: none !important;
|
| 506 |
+
outline: none !important;
|
| 507 |
+
background-color: transparent !important;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/* Remove selection highlight from internal header elements */
|
| 511 |
+
#wer-leaderboard-table th .cell-wrap,
|
| 512 |
+
#cer-leaderboard-table th .cell-wrap,
|
| 513 |
+
#rtfx-leaderboard-table th .cell-wrap,
|
| 514 |
+
#wer-leaderboard-table th .header-button,
|
| 515 |
+
#cer-leaderboard-table th .header-button,
|
| 516 |
+
#rtfx-leaderboard-table th .header-button {
|
| 517 |
+
background-color: transparent !important;
|
| 518 |
+
box-shadow: none !important;
|
| 519 |
+
outline: none !important;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
body.dark #wer-leaderboard-table th .cell-wrap,
|
| 523 |
+
body.dark #cer-leaderboard-table th .cell-wrap,
|
| 524 |
+
body.dark #rtfx-leaderboard-table th .cell-wrap,
|
| 525 |
+
body.dark #wer-leaderboard-table th .header-button,
|
| 526 |
+
body.dark #cer-leaderboard-table th .header-button,
|
| 527 |
+
body.dark #rtfx-leaderboard-table th .header-button,
|
| 528 |
+
[data-theme="dark"] #wer-leaderboard-table th .cell-wrap,
|
| 529 |
+
[data-theme="dark"] #cer-leaderboard-table th .cell-wrap,
|
| 530 |
+
[data-theme="dark"] #rtfx-leaderboard-table th .cell-wrap,
|
| 531 |
+
[data-theme="dark"] #wer-leaderboard-table th .header-button,
|
| 532 |
+
[data-theme="dark"] #cer-leaderboard-table th .header-button,
|
| 533 |
+
[data-theme="dark"] #rtfx-leaderboard-table th .header-button {
|
| 534 |
+
background-color: transparent !important;
|
| 535 |
+
box-shadow: none !important;
|
| 536 |
+
outline: none !important;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
/* Language column styling (first column after index) */
|
| 540 |
+
#wer-leaderboard-table td:first-child,
|
| 541 |
+
#cer-leaderboard-table td:first-child,
|
| 542 |
+
#rtfx-leaderboard-table td:first-child {
|
| 543 |
+
font-weight: 600;
|
| 544 |
+
position: sticky;
|
| 545 |
+
left: 0;
|
| 546 |
+
background-color: var(--metric-table-row-header-bg);
|
| 547 |
+
z-index: 1;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
/* Make # and Language columns look clickable for row-level sorting */
|
| 551 |
+
#wer-leaderboard-table td:nth-child(1),
|
| 552 |
+
#wer-leaderboard-table td:nth-child(2),
|
| 553 |
+
#cer-leaderboard-table td:nth-child(1),
|
| 554 |
+
#cer-leaderboard-table td:nth-child(2),
|
| 555 |
+
#rtfx-leaderboard-table td:nth-child(1),
|
| 556 |
+
#rtfx-leaderboard-table td:nth-child(2) {
|
| 557 |
+
cursor: pointer;
|
| 558 |
+
transition: background-color 0.2s ease, color 0.2s ease;
|
| 559 |
+
position: relative;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
/* Row sort button styling */
|
| 563 |
+
.row-sort-btn {
|
| 564 |
+
display: inline-block;
|
| 565 |
+
margin-left: 4px;
|
| 566 |
+
font-size: 10px;
|
| 567 |
+
opacity: 0.4;
|
| 568 |
+
cursor: pointer;
|
| 569 |
+
vertical-align: middle;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
#wer-leaderboard-table td:nth-child(2):hover .row-sort-btn,
|
| 573 |
+
#cer-leaderboard-table td:nth-child(2):hover .row-sort-btn,
|
| 574 |
+
#rtfx-leaderboard-table td:nth-child(2):hover .row-sort-btn {
|
| 575 |
+
opacity: 0.8;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
#wer-leaderboard-table td:nth-child(1):hover,
|
| 579 |
+
#wer-leaderboard-table td:nth-child(2):hover,
|
| 580 |
+
#cer-leaderboard-table td:nth-child(1):hover,
|
| 581 |
+
#cer-leaderboard-table td:nth-child(2):hover,
|
| 582 |
+
#rtfx-leaderboard-table td:nth-child(1):hover,
|
| 583 |
+
#rtfx-leaderboard-table td:nth-child(2):hover {
|
| 584 |
+
background-color: rgba(140, 208, 255, 0.2) !important;
|
| 585 |
+
text-decoration: underline;
|
| 586 |
+
color: #0066cc !important;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
#search-bar-table-box > div:first-child {
|
| 590 |
+
background: none;
|
| 591 |
+
border: none;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
#search-bar {
|
| 595 |
+
padding: 0px;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
/* Limit the width of the first AutoEvalColumn so that names don't expand too much */
|
| 599 |
+
#leaderboard-table td:nth-child(2),
|
| 600 |
+
#leaderboard-table th:nth-child(2) {
|
| 601 |
+
max-width: 400px;
|
| 602 |
+
overflow: auto;
|
| 603 |
+
white-space: nowrap;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.tab-buttons button {
|
| 607 |
+
font-size: 20px;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
/* PazaBench brand color for active tabs */
|
| 611 |
+
.tab-buttons button.selected,
|
| 612 |
+
.tabs button.selected,
|
| 613 |
+
button.tab-nav.selected,
|
| 614 |
+
[role="tab"][aria-selected="true"],
|
| 615 |
+
.tab-nav.selected {
|
| 616 |
+
background-color: #DDF1FF !important;
|
| 617 |
+
border-color: #8CD0FF !important;
|
| 618 |
+
color: #0f172a !important;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
/* Subtle hover state for tabs - light mode */
|
| 622 |
+
.tab-buttons button:hover,
|
| 623 |
+
.tabs button:hover,
|
| 624 |
+
button.tab-nav:hover,
|
| 625 |
+
[role="tab"]:hover {
|
| 626 |
+
background-color: #DDF1FF !important;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
/* ===== DARK MODE TAB HOVER FIX ===== */
|
| 630 |
+
/* Make tab hover transparent in dark mode to prevent text obscuring */
|
| 631 |
+
body.dark .tab-buttons button:hover,
|
| 632 |
+
body.dark .tabs button:hover,
|
| 633 |
+
body.dark button.tab-nav:hover,
|
| 634 |
+
body.dark [role="tab"]:hover,
|
| 635 |
+
[data-theme="dark"] .tab-buttons button:hover,
|
| 636 |
+
[data-theme="dark"] .tabs button:hover,
|
| 637 |
+
[data-theme="dark"] button.tab-nav:hover,
|
| 638 |
+
[data-theme="dark"] [role="tab"]:hover,
|
| 639 |
+
.dark .tab-buttons button:hover,
|
| 640 |
+
.dark .tabs button:hover,
|
| 641 |
+
.dark button.tab-nav:hover,
|
| 642 |
+
.dark [role="tab"]:hover {
|
| 643 |
+
background-color: transparent !important;
|
| 644 |
+
text-decoration: underline;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
/* Dark mode active tab - ensure text is visible */
|
| 648 |
+
body.dark .tab-buttons button.selected,
|
| 649 |
+
body.dark .tabs button.selected,
|
| 650 |
+
body.dark button.tab-nav.selected,
|
| 651 |
+
body.dark [role="tab"][aria-selected="true"],
|
| 652 |
+
[data-theme="dark"] .tab-buttons button.selected,
|
| 653 |
+
[data-theme="dark"] .tabs button.selected,
|
| 654 |
+
[data-theme="dark"] button.tab-nav.selected,
|
| 655 |
+
[data-theme="dark"] [role="tab"][aria-selected="true"],
|
| 656 |
+
.dark .tab-buttons button.selected,
|
| 657 |
+
.dark .tabs button.selected,
|
| 658 |
+
.dark button.tab-nav.selected,
|
| 659 |
+
.dark [role="tab"][aria-selected="true"] {
|
| 660 |
+
background-color: rgba(140, 208, 255, 0.2) !important;
|
| 661 |
+
border-color: #8CD0FF !important;
|
| 662 |
+
color: #e5e7eb !important;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
/* Prevent text selection on tabs */
|
| 666 |
+
.tab-buttons button,
|
| 667 |
+
.tabs button,
|
| 668 |
+
button.tab-nav,
|
| 669 |
+
[role="tab"] {
|
| 670 |
+
-webkit-user-select: none !important;
|
| 671 |
+
-moz-user-select: none !important;
|
| 672 |
+
-ms-user-select: none !important;
|
| 673 |
+
user-select: none !important;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
/* Override Gradio's default orange accent colors with PazaBench blue */
|
| 677 |
+
/* Tab underline/border colors */
|
| 678 |
+
.tabs .tab-nav::after,
|
| 679 |
+
.tab-buttons button::after,
|
| 680 |
+
button.tab-nav::after,
|
| 681 |
+
[role="tab"]::after {
|
| 682 |
+
background-color: #8CD0FF !important;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.tabs .tab-nav.selected::after,
|
| 686 |
+
.tab-buttons button.selected::after,
|
| 687 |
+
button.tab-nav.selected::after,
|
| 688 |
+
[role="tab"][aria-selected="true"]::after {
|
| 689 |
+
background-color: #8CD0FF !important;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
/* Gradio dataframe selected cell borders */
|
| 693 |
+
.table-wrap td.focus,
|
| 694 |
+
.table-wrap th.focus,
|
| 695 |
+
table td:focus,
|
| 696 |
+
table th:focus,
|
| 697 |
+
.dataframe td.selected,
|
| 698 |
+
.dataframe th.selected,
|
| 699 |
+
.svelte-table td.focus,
|
| 700 |
+
.svelte-table th.focus,
|
| 701 |
+
[data-testid="dataframe"] td.selected,
|
| 702 |
+
[data-testid="dataframe"] th.selected {
|
| 703 |
+
outline-color: #8CD0FF !important;
|
| 704 |
+
border-color: #8CD0FF !important;
|
| 705 |
+
box-shadow: 0 0 0 2px #8CD0FF !important;
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
/* Override orange focus rings and borders globally */
|
| 709 |
+
*:focus {
|
| 710 |
+
outline-color: #8CD0FF !important;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
input:focus,
|
| 714 |
+
textarea:focus,
|
| 715 |
+
select:focus,
|
| 716 |
+
button:focus {
|
| 717 |
+
border-color: #8CD0FF !important;
|
| 718 |
+
box-shadow: 0 0 0 2px rgba(140, 208, 255, 0.3) !important;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
/* Gradio specific accent color overrides */
|
| 722 |
+
.gradio-container {
|
| 723 |
+
--color-accent: #8CD0FF !important;
|
| 724 |
+
--color-accent-soft: #DDF1FF !important;
|
| 725 |
+
/* Radio and Checkbox - only style the indicator dot, not the label background */
|
| 726 |
+
--checkbox-label-background-fill-selected: transparent !important;
|
| 727 |
+
--checkbox-background-color-selected: #8CD0FF !important;
|
| 728 |
+
--checkbox-border-color-selected: #8CD0FF !important;
|
| 729 |
+
--slider-color: #8CD0FF !important;
|
| 730 |
+
/* Primary button colors */
|
| 731 |
+
--primary-50: #DDF1FF !important;
|
| 732 |
+
--primary-100: #c5e7ff !important;
|
| 733 |
+
--primary-200: #aeddff !important;
|
| 734 |
+
--primary-300: #8CD0FF !important;
|
| 735 |
+
--primary-400: #6bc0f5 !important;
|
| 736 |
+
--primary-500: #4ab0eb !important;
|
| 737 |
+
--primary-600: #29a0e1 !important;
|
| 738 |
+
--primary-700: #0890d7 !important;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
/* Dark mode accent colors */
|
| 742 |
+
body.dark .gradio-container,
|
| 743 |
+
[data-theme="dark"] .gradio-container {
|
| 744 |
+
--color-accent: #8CD0FF !important;
|
| 745 |
+
--color-accent-soft: #1e3a5f !important;
|
| 746 |
+
/* Radio and Checkbox - only style the indicator dot, not the label background */
|
| 747 |
+
--checkbox-label-background-fill-selected: transparent !important;
|
| 748 |
+
--checkbox-background-color-selected: #8CD0FF !important;
|
| 749 |
+
--checkbox-border-color-selected: #8CD0FF !important;
|
| 750 |
+
--slider-color: #8CD0FF !important;
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
/* ===== LOADING/PROGRESS BAR FIX FOR DARK MODE ===== */
|
| 754 |
+
/* Make loading indicator more subtle/transparent */
|
| 755 |
+
.progress-bar,
|
| 756 |
+
.eta-bar,
|
| 757 |
+
[class*="progress"],
|
| 758 |
+
[class*="loader"],
|
| 759 |
+
[class*="loading"],
|
| 760 |
+
.wrap.svelte-j1gjts,
|
| 761 |
+
.wrap.svelte-j1gjts .progress-bar {
|
| 762 |
+
background-color: transparent !important;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
/* Gradio's internal progress/loading indicators */
|
| 766 |
+
body.dark .progress-bar,
|
| 767 |
+
body.dark .eta-bar,
|
| 768 |
+
body.dark [class*="progress"],
|
| 769 |
+
[data-theme="dark"] .progress-bar,
|
| 770 |
+
[data-theme="dark"] .eta-bar,
|
| 771 |
+
[data-theme="dark"] [class*="progress"],
|
| 772 |
+
.dark .progress-bar,
|
| 773 |
+
.dark .eta-bar,
|
| 774 |
+
.dark [class*="progress"] {
|
| 775 |
+
background-color: transparent !important;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
/* ===== ACCORDION HOVER FIX FOR DARK MODE ===== */
|
| 779 |
+
/* Fix accordion text highlight/selection obscuring text in dark mode */
|
| 780 |
+
/* Target accordion header button hover states - using actual Gradio class names */
|
| 781 |
+
.label-wrap:hover,
|
| 782 |
+
button.label-wrap:hover {
|
| 783 |
+
background-color: transparent !important;
|
| 784 |
+
-webkit-user-select: none !important;
|
| 785 |
+
-moz-user-select: none !important;
|
| 786 |
+
-ms-user-select: none !important;
|
| 787 |
+
user-select: none !important;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
/* Dark mode specific accordion hover fix */
|
| 791 |
+
body.dark .label-wrap:hover,
|
| 792 |
+
body.dark button.label-wrap:hover,
|
| 793 |
+
[data-theme="dark"] .label-wrap:hover,
|
| 794 |
+
[data-theme="dark"] button.label-wrap:hover,
|
| 795 |
+
.dark .label-wrap:hover,
|
| 796 |
+
.dark button.label-wrap:hover {
|
| 797 |
+
background-color: transparent !important;
|
| 798 |
+
-webkit-user-select: none !important;
|
| 799 |
+
user-select: none !important;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
/* Prevent text selection highlight on accordion labels */
|
| 803 |
+
.label-wrap::selection,
|
| 804 |
+
.label-wrap *::selection,
|
| 805 |
+
button.label-wrap::selection,
|
| 806 |
+
button.label-wrap *::selection {
|
| 807 |
+
background-color: transparent !important;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
body.dark .label-wrap::selection,
|
| 811 |
+
body.dark .label-wrap *::selection,
|
| 812 |
+
[data-theme="dark"] .label-wrap::selection,
|
| 813 |
+
[data-theme="dark"] .label-wrap *::selection,
|
| 814 |
+
.dark .label-wrap::selection,
|
| 815 |
+
.dark .label-wrap *::selection {
|
| 816 |
+
background-color: transparent !important;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
/* ===== DATAFRAME/TABLE HEADER HOVER FIX FOR DARK MODE ===== */
|
| 820 |
+
/* Fix table header hover states obscuring text */
|
| 821 |
+
body.dark table th:hover,
|
| 822 |
+
body.dark [data-testid="dataframe"] th:hover,
|
| 823 |
+
body.dark .table-wrap th:hover,
|
| 824 |
+
body.dark .svelte-table th:hover,
|
| 825 |
+
[data-theme="dark"] table th:hover,
|
| 826 |
+
[data-theme="dark"] [data-testid="dataframe"] th:hover,
|
| 827 |
+
[data-theme="dark"] .table-wrap th:hover,
|
| 828 |
+
[data-theme="dark"] .svelte-table th:hover,
|
| 829 |
+
.dark table th:hover,
|
| 830 |
+
.dark [data-testid="dataframe"] th:hover,
|
| 831 |
+
.dark .table-wrap th:hover,
|
| 832 |
+
.dark .svelte-table th:hover {
|
| 833 |
+
background-color: transparent !important;
|
| 834 |
+
-webkit-user-select: none !important;
|
| 835 |
+
user-select: none !important;
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
/* More comprehensive table header fix - catch Gradio's generated classes */
|
| 839 |
+
body.dark th,
|
| 840 |
+
body.dark thead th,
|
| 841 |
+
body.dark .header,
|
| 842 |
+
body.dark [class*="header"],
|
| 843 |
+
body.dark th[role="columnheader"],
|
| 844 |
+
[data-theme="dark"] th,
|
| 845 |
+
[data-theme="dark"] thead th,
|
| 846 |
+
[data-theme="dark"] .header,
|
| 847 |
+
[data-theme="dark"] [class*="header"],
|
| 848 |
+
[data-theme="dark"] th[role="columnheader"],
|
| 849 |
+
.dark th,
|
| 850 |
+
.dark thead th,
|
| 851 |
+
.dark .header,
|
| 852 |
+
.dark [class*="header"],
|
| 853 |
+
.dark th[role="columnheader"] {
|
| 854 |
+
-webkit-user-select: none !important;
|
| 855 |
+
-moz-user-select: none !important;
|
| 856 |
+
user-select: none !important;
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
body.dark th:hover,
|
| 860 |
+
body.dark thead th:hover,
|
| 861 |
+
body.dark th[role="columnheader"]:hover,
|
| 862 |
+
[data-theme="dark"] th:hover,
|
| 863 |
+
[data-theme="dark"] thead th:hover,
|
| 864 |
+
[data-theme="dark"] th[role="columnheader"]:hover,
|
| 865 |
+
.dark th:hover,
|
| 866 |
+
.dark thead th:hover,
|
| 867 |
+
.dark th[role="columnheader"]:hover {
|
| 868 |
+
background-color: transparent !important;
|
| 869 |
+
text-decoration: underline;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
/* Prevent text selection on table headers */
|
| 873 |
+
th::selection,
|
| 874 |
+
th *::selection,
|
| 875 |
+
thead::selection,
|
| 876 |
+
thead *::selection {
|
| 877 |
+
background-color: transparent !important;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
/* ===== GRADIO DATAFRAME HEADER SELECTION/FOCUS FIX ===== */
|
| 881 |
+
/* Remove the blue highlight ring from selected/focused column headers in dark mode */
|
| 882 |
+
/* Gradio uses .focus class and --ring-color variable for header selection state */
|
| 883 |
+
body.dark th.focus,
|
| 884 |
+
body.dark thead th.focus,
|
| 885 |
+
body.dark .table-wrap th.focus,
|
| 886 |
+
body.dark [data-testid="dataframe"] th.focus,
|
| 887 |
+
[data-theme="dark"] th.focus,
|
| 888 |
+
[data-theme="dark"] thead th.focus,
|
| 889 |
+
[data-theme="dark"] .table-wrap th.focus,
|
| 890 |
+
[data-theme="dark"] [data-testid="dataframe"] th.focus,
|
| 891 |
+
.dark th.focus,
|
| 892 |
+
.dark thead th.focus,
|
| 893 |
+
.dark .table-wrap th.focus,
|
| 894 |
+
.dark [data-testid="dataframe"] th.focus {
|
| 895 |
+
--ring-color: transparent !important;
|
| 896 |
+
box-shadow: none !important;
|
| 897 |
+
outline: none !important;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
/* Also handle selected header state */
|
| 901 |
+
body.dark th.selected,
|
| 902 |
+
body.dark thead th.selected,
|
| 903 |
+
body.dark th[aria-selected="true"],
|
| 904 |
+
[data-theme="dark"] th.selected,
|
| 905 |
+
[data-theme="dark"] thead th.selected,
|
| 906 |
+
[data-theme="dark"] th[aria-selected="true"],
|
| 907 |
+
.dark th.selected,
|
| 908 |
+
.dark thead th.selected,
|
| 909 |
+
.dark th[aria-selected="true"] {
|
| 910 |
+
--ring-color: transparent !important;
|
| 911 |
+
box-shadow: none !important;
|
| 912 |
+
outline: none !important;
|
| 913 |
+
background-color: transparent !important;
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
/* Header focus state */
|
| 917 |
+
th.focus,
|
| 918 |
+
thead th.focus,
|
| 919 |
+
.table-wrap th.focus,
|
| 920 |
+
[data-testid="dataframe"] th.focus {
|
| 921 |
+
--ring-color: transparent !important;
|
| 922 |
+
box-shadow: none !important;
|
| 923 |
+
outline: none !important;
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
th.selected,
|
| 927 |
+
thead th.selected,
|
| 928 |
+
th[aria-selected="true"] {
|
| 929 |
+
--ring-color: transparent !important;
|
| 930 |
+
box-shadow: none !important;
|
| 931 |
+
outline: none !important;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
/* ===== KEEP SORT ARROWS VISIBLE ===== */
|
| 935 |
+
/* Ensure sort arrows (SortArrowUp/SortArrowDown) remain visible */
|
| 936 |
+
/* Sort indicators container in Gradio dataframe header */
|
| 937 |
+
.sort-indicators,
|
| 938 |
+
.table-wrap .sort-indicators,
|
| 939 |
+
[data-testid="dataframe"] .sort-indicators,
|
| 940 |
+
th .sort-indicators {
|
| 941 |
+
display: flex !important;
|
| 942 |
+
visibility: visible !important;
|
| 943 |
+
opacity: 1 !important;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
/* Sort arrow icons */
|
| 947 |
+
.sort-arrow,
|
| 948 |
+
.sort-indicators .sort-arrow,
|
| 949 |
+
th .sort-arrow {
|
| 950 |
+
display: flex !important;
|
| 951 |
+
visibility: visible !important;
|
| 952 |
+
opacity: 1 !important;
|
| 953 |
+
color: var(--body-text-color) !important;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
/* Sort priority badge (shows order when multi-column sorting) */
|
| 957 |
+
.sort-priority,
|
| 958 |
+
.sort-indicators .sort-priority,
|
| 959 |
+
th .sort-priority {
|
| 960 |
+
display: flex !important;
|
| 961 |
+
visibility: visible !important;
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
/* Ensure sorted column headers show their state via arrows only, not highlight */
|
| 965 |
+
th.sorted,
|
| 966 |
+
th[aria-sort="ascending"],
|
| 967 |
+
th[aria-sort="descending"] {
|
| 968 |
+
background-color: transparent !important;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
body.dark th.sorted,
|
| 972 |
+
body.dark th[aria-sort="ascending"],
|
| 973 |
+
body.dark th[aria-sort="descending"],
|
| 974 |
+
[data-theme="dark"] th.sorted,
|
| 975 |
+
[data-theme="dark"] th[aria-sort="ascending"],
|
| 976 |
+
[data-theme="dark"] th[aria-sort="descending"],
|
| 977 |
+
.dark th.sorted,
|
| 978 |
+
.dark th[aria-sort="ascending"],
|
| 979 |
+
.dark th[aria-sort="descending"] {
|
| 980 |
+
background-color: transparent !important;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
/* ===== GRADIO DATAFRAME INTERNAL HEADER BUTTON FIX ===== */
|
| 984 |
+
/* Gradio's TableHeader uses .cell-wrap with a button.header-button inside */
|
| 985 |
+
/* Remove any selection/focus styling from these internal elements */
|
| 986 |
+
th .cell-wrap,
|
| 987 |
+
thead th .cell-wrap,
|
| 988 |
+
.table-wrap th .cell-wrap,
|
| 989 |
+
[data-testid="dataframe"] th .cell-wrap {
|
| 990 |
+
background-color: transparent !important;
|
| 991 |
+
box-shadow: none !important;
|
| 992 |
+
outline: none !important;
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
th .header-button,
|
| 996 |
+
thead th .header-button,
|
| 997 |
+
th .cell-wrap .header-button,
|
| 998 |
+
th button.header-button {
|
| 999 |
+
background-color: transparent !important;
|
| 1000 |
+
box-shadow: none !important;
|
| 1001 |
+
outline: none !important;
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
/* Dark mode specific for internal header elements */
|
| 1005 |
+
body.dark th .cell-wrap,
|
| 1006 |
+
body.dark th .header-button,
|
| 1007 |
+
body.dark th .cell-wrap .header-button,
|
| 1008 |
+
[data-theme="dark"] th .cell-wrap,
|
| 1009 |
+
[data-theme="dark"] th .header-button,
|
| 1010 |
+
[data-theme="dark"] th .cell-wrap .header-button,
|
| 1011 |
+
.dark th .cell-wrap,
|
| 1012 |
+
.dark th .header-button,
|
| 1013 |
+
.dark th .cell-wrap .header-button {
|
| 1014 |
+
background-color: transparent !important;
|
| 1015 |
+
box-shadow: none !important;
|
| 1016 |
+
outline: none !important;
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
/* Remove highlight on header button focus/active states */
|
| 1020 |
+
th .header-button:focus,
|
| 1021 |
+
th .header-button:active,
|
| 1022 |
+
th .header-button:hover,
|
| 1023 |
+
th .cell-wrap:focus,
|
| 1024 |
+
th .cell-wrap:active {
|
| 1025 |
+
background-color: transparent !important;
|
| 1026 |
+
box-shadow: none !important;
|
| 1027 |
+
outline: none !important;
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
body.dark th .header-button:focus,
|
| 1031 |
+
body.dark th .header-button:active,
|
| 1032 |
+
body.dark th .header-button:hover,
|
| 1033 |
+
body.dark th .cell-wrap:focus,
|
| 1034 |
+
body.dark th .cell-wrap:active,
|
| 1035 |
+
[data-theme="dark"] th .header-button:focus,
|
| 1036 |
+
[data-theme="dark"] th .header-button:active,
|
| 1037 |
+
[data-theme="dark"] th .header-button:hover,
|
| 1038 |
+
[data-theme="dark"] th .cell-wrap:focus,
|
| 1039 |
+
[data-theme="dark"] th .cell-wrap:active,
|
| 1040 |
+
.dark th .header-button:focus,
|
| 1041 |
+
.dark th .header-button:active,
|
| 1042 |
+
.dark th .header-button:hover,
|
| 1043 |
+
.dark th .cell-wrap:focus,
|
| 1044 |
+
.dark th .cell-wrap:active {
|
| 1045 |
+
background-color: transparent !important;
|
| 1046 |
+
box-shadow: none !important;
|
| 1047 |
+
outline: none !important;
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
/* Fix table cell selection/focus in dark mode - make background subtle */
|
| 1051 |
+
body.dark table th:focus,
|
| 1052 |
+
body.dark table td:focus,
|
| 1053 |
+
body.dark [data-testid="dataframe"] th:focus,
|
| 1054 |
+
body.dark [data-testid="dataframe"] td:focus,
|
| 1055 |
+
[data-theme="dark"] table th:focus,
|
| 1056 |
+
[data-theme="dark"] table td:focus,
|
| 1057 |
+
[data-theme="dark"] [data-testid="dataframe"] th:focus,
|
| 1058 |
+
[data-theme="dark"] [data-testid="dataframe"] td:focus,
|
| 1059 |
+
.dark table th:focus,
|
| 1060 |
+
.dark table td:focus,
|
| 1061 |
+
.dark [data-testid="dataframe"] th:focus,
|
| 1062 |
+
.dark [data-testid="dataframe"] td:focus {
|
| 1063 |
+
background-color: rgba(140, 208, 255, 0.15) !important;
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
/* Prevent text selection on table headers */
|
| 1067 |
+
table th::selection,
|
| 1068 |
+
table th *::selection,
|
| 1069 |
+
[data-testid="dataframe"] th::selection,
|
| 1070 |
+
[data-testid="dataframe"] th *::selection {
|
| 1071 |
+
background-color: transparent !important;
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
/* ===== GLOBAL DARK MODE TEXT SELECTION FIX ===== */
|
| 1075 |
+
/* Fix text selection color in dark mode globally for better contrast */
|
| 1076 |
+
body.dark ::selection,
|
| 1077 |
+
[data-theme="dark"] ::selection,
|
| 1078 |
+
.dark ::selection {
|
| 1079 |
+
background-color: rgba(140, 208, 255, 0.4) !important;
|
| 1080 |
+
color: inherit !important;
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
/* Override Radio button selected indicator (the inner dot) - Gradio 5 specific */
|
| 1084 |
+
input[type="radio"]:checked {
|
| 1085 |
+
accent-color: #8CD0FF !important;
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
/* Radio group - remove background fill, keep only dot styled */
|
| 1089 |
+
.gradio-radio label.selected,
|
| 1090 |
+
[data-testid*="radio-label"].selected {
|
| 1091 |
+
background-color: transparent !important;
|
| 1092 |
+
border-color: transparent !important;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
/* Checkbox checked styling */
|
| 1096 |
+
input[type="checkbox"]:checked {
|
| 1097 |
+
accent-color: #8CD0FF !important;
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
#scale-logo {
|
| 1101 |
+
border-style: none !important;
|
| 1102 |
+
box-shadow: none;
|
| 1103 |
+
display: block;
|
| 1104 |
+
margin-left: auto;
|
| 1105 |
+
margin-right: auto;
|
| 1106 |
+
max-width: 600px;
|
| 1107 |
+
}
|
| 1108 |
+
|
| 1109 |
+
#scale-logo .download {
|
| 1110 |
+
display: none;
|
| 1111 |
+
}
|
| 1112 |
+
#filter_type{
|
| 1113 |
+
border: 0;
|
| 1114 |
+
padding-left: 0;
|
| 1115 |
+
padding-top: 0;
|
| 1116 |
+
}
|
| 1117 |
+
#filter_type label {
|
| 1118 |
+
display: flex;
|
| 1119 |
+
}
|
| 1120 |
+
#filter_type label > span{
|
| 1121 |
+
margin-top: var(--spacing-lg);
|
| 1122 |
+
margin-right: 0.5em;
|
| 1123 |
+
}
|
| 1124 |
+
#filter_type label > .wrap{
|
| 1125 |
+
width: 103px;
|
| 1126 |
+
}
|
| 1127 |
+
#filter_type label > .wrap .wrap-inner{
|
| 1128 |
+
padding: 2px;
|
| 1129 |
+
}
|
| 1130 |
+
#filter_type label > .wrap .wrap-inner input{
|
| 1131 |
+
width: 1px
|
| 1132 |
+
}
|
| 1133 |
+
#filter-columns-type{
|
| 1134 |
+
border:0;
|
| 1135 |
+
padding:0.5;
|
| 1136 |
+
}
|
| 1137 |
+
#filter-columns-size{
|
| 1138 |
+
border:0;
|
| 1139 |
+
padding:0.5;
|
| 1140 |
+
}
|
| 1141 |
+
#box-filter > .form{
|
| 1142 |
+
border: 0
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
.metric-tabs button {
|
| 1146 |
+
font-size: 18px;
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
.metric-table-wrapper {
|
| 1150 |
+
overflow-x: auto;
|
| 1151 |
+
margin-top: 1rem;
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
/* ===== MARKDOWN TABLE COLUMN WIDTHS ===== */
|
| 1155 |
+
/* Ensure Datasets and Evaluated Models tables have proper column widths */
|
| 1156 |
+
/* Target markdown-rendered tables in Benchmark Inputs tab */
|
| 1157 |
+
.markdown-text table th:nth-child(4),
|
| 1158 |
+
.markdown-text table td:nth-child(4),
|
| 1159 |
+
.prose table th:nth-child(4),
|
| 1160 |
+
.prose table td:nth-child(4) {
|
| 1161 |
+
min-width: 80px !important;
|
| 1162 |
+
white-space: nowrap;
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
.markdown-text table th:nth-child(5),
|
| 1166 |
+
.markdown-text table td:nth-child(5),
|
| 1167 |
+
.prose table th:nth-child(5),
|
| 1168 |
+
.prose table td:nth-child(5) {
|
| 1169 |
+
min-width: 100px !important;
|
| 1170 |
+
white-space: nowrap;
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
/* Chart Gallery Styles */
|
| 1174 |
+
.chart-gallery {
|
| 1175 |
+
display: grid;
|
| 1176 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 1177 |
+
gap: 1rem;
|
| 1178 |
+
margin: 1rem 0;
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
/* Plotly Chart Responsiveness */
|
| 1182 |
+
.js-plotly-plot, .plotly {
|
| 1183 |
+
width: 100% !important;
|
| 1184 |
+
}
|
| 1185 |
+
|
| 1186 |
+
.js-plotly-plot .plotly .main-svg {
|
| 1187 |
+
width: 100% !important;
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
/* Better mobile display for Plotly charts */
|
| 1191 |
+
@media (max-width: 768px) {
|
| 1192 |
+
.js-plotly-plot .plotly .legend {
|
| 1193 |
+
font-size: 9px !important;
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
.js-plotly-plot .plotly .gtitle {
|
| 1197 |
+
font-size: 14px !important;
|
| 1198 |
+
}
|
| 1199 |
+
|
| 1200 |
+
.js-plotly-plot .plotly .xtitle,
|
| 1201 |
+
.js-plotly-plot .plotly .ytitle {
|
| 1202 |
+
font-size: 11px !important;
|
| 1203 |
+
}
|
| 1204 |
+
|
| 1205 |
+
/* Stack the map and info/chart column full-width on mobile. */
|
| 1206 |
+
#language-coverage-row {
|
| 1207 |
+
flex-direction: column !important;
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
#language-coverage-row > * {
|
| 1211 |
+
width: 100% !important;
|
| 1212 |
+
min-width: 0 !important;
|
| 1213 |
+
}
|
| 1214 |
+
|
| 1215 |
+
/* Wide content tables scroll horizontally on mobile. */
|
| 1216 |
+
.markdown-text table {
|
| 1217 |
+
display: block;
|
| 1218 |
+
overflow-x: auto;
|
| 1219 |
+
-webkit-overflow-scrolling: touch;
|
| 1220 |
+
}
|
| 1221 |
+
.markdown-text table th,
|
| 1222 |
+
.markdown-text table td {
|
| 1223 |
+
white-space: nowrap;
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
/* Visualisation charts keep a usable min-width and scroll horizontally on mobile. */
|
| 1227 |
+
.viz-scroll-plot {
|
| 1228 |
+
overflow-x: auto !important;
|
| 1229 |
+
-webkit-overflow-scrolling: touch;
|
| 1230 |
+
}
|
| 1231 |
+
.viz-scroll-plot .js-plotly-plot {
|
| 1232 |
+
min-width: 700px !important;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
/* Leaderboard metric tables keep their min-width but scroll inside their wrapper. */
|
| 1236 |
+
.metric-table-wrapper,
|
| 1237 |
+
.metric-table-container,
|
| 1238 |
+
.table-wrap {
|
| 1239 |
+
overflow-x: auto !important;
|
| 1240 |
+
-webkit-overflow-scrolling: touch;
|
| 1241 |
+
}
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
.chart-card {
|
| 1245 |
+
background: var(--metric-table-bg, #ffffff);
|
| 1246 |
+
border: 1px solid var(--metric-table-border, #d3d3d3);
|
| 1247 |
+
border-radius: 8px;
|
| 1248 |
+
padding: 1rem;
|
| 1249 |
+
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
| 1250 |
+
}
|
| 1251 |
+
|
| 1252 |
+
.chart-card:hover {
|
| 1253 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 1254 |
+
transform: translateY(-2px);
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.chart-card h4 {
|
| 1258 |
+
margin: 0 0 0.5rem 0;
|
| 1259 |
+
color: var(--metric-table-header-color, #0f172a);
|
| 1260 |
+
font-size: 1.1rem;
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
.chart-card p {
|
| 1264 |
+
margin: 0;
|
| 1265 |
+
color: var(--metric-table-caption, #666666);
|
| 1266 |
+
font-size: 0.9rem;
|
| 1267 |
+
line-height: 1.4;
|
| 1268 |
+
}
|
| 1269 |
+
|
| 1270 |
+
.metric-table {
|
| 1271 |
+
border-collapse: collapse;
|
| 1272 |
+
width: 100%;
|
| 1273 |
+
min-width: 600px;
|
| 1274 |
+
text-align: center;
|
| 1275 |
+
font-size: 16px;
|
| 1276 |
+
background-color: var(--metric-table-bg);
|
| 1277 |
+
}
|
| 1278 |
+
|
| 1279 |
+
.metric-table caption {
|
| 1280 |
+
caption-side: bottom;
|
| 1281 |
+
padding-top: 0.5rem;
|
| 1282 |
+
color: var(--metric-table-caption);
|
| 1283 |
+
font-size: 0.9rem;
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
.metric-table th,
|
| 1287 |
+
.metric-table td {
|
| 1288 |
+
border: 1px solid var(--metric-table-border);
|
| 1289 |
+
padding: 0.5rem;
|
| 1290 |
+
}
|
| 1291 |
+
|
| 1292 |
+
.metric-table thead th {
|
| 1293 |
+
background-color: var(--metric-table-header-bg);
|
| 1294 |
+
color: var(--metric-table-header-color);
|
| 1295 |
+
position: sticky;
|
| 1296 |
+
top: 0;
|
| 1297 |
+
z-index: 1;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
.metric-table tbody th {
|
| 1301 |
+
text-align: left;
|
| 1302 |
+
background-color: var(--metric-table-row-header-bg);
|
| 1303 |
+
color: var(--metric-table-header-color);
|
| 1304 |
+
position: sticky;
|
| 1305 |
+
left: 0;
|
| 1306 |
+
z-index: 2;
|
| 1307 |
+
cursor: pointer;
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
.metric-table tbody th:hover {
|
| 1311 |
+
text-decoration: underline;
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
.metric-table tbody tr.row-highlighted {
|
| 1315 |
+
background-color: var(--metric-table-row-highlight) !important;
|
| 1316 |
+
}
|
| 1317 |
+
|
| 1318 |
+
.metric-table tbody tr.row-highlighted td,
|
| 1319 |
+
.metric-table tbody tr.row-highlighted th {
|
| 1320 |
+
background-color: var(--metric-table-row-highlight) !important;
|
| 1321 |
+
}
|
| 1322 |
+
|
| 1323 |
+
/* Leaderboard Gradio dataframe row highlighting */
|
| 1324 |
+
#wer-leaderboard-table tbody tr.leaderboard-row-highlighted,
|
| 1325 |
+
#cer-leaderboard-table tbody tr.leaderboard-row-highlighted,
|
| 1326 |
+
#rtfx-leaderboard-table tbody tr.leaderboard-row-highlighted {
|
| 1327 |
+
background-color: var(--metric-table-row-highlight, rgba(140, 208, 255, 0.3)) !important;
|
| 1328 |
+
}
|
| 1329 |
+
|
| 1330 |
+
#wer-leaderboard-table tbody tr.leaderboard-row-highlighted td,
|
| 1331 |
+
#cer-leaderboard-table tbody tr.leaderboard-row-highlighted td,
|
| 1332 |
+
#rtfx-leaderboard-table tbody tr.leaderboard-row-highlighted td {
|
| 1333 |
+
background-color: var(--metric-table-row-highlight, rgba(140, 208, 255, 0.3)) !important;
|
| 1334 |
+
}
|
| 1335 |
+
|
| 1336 |
+
/* Dark mode leaderboard row highlighting */
|
| 1337 |
+
body.dark #wer-leaderboard-table tbody tr.leaderboard-row-highlighted,
|
| 1338 |
+
body.dark #cer-leaderboard-table tbody tr.leaderboard-row-highlighted,
|
| 1339 |
+
body.dark #rtfx-leaderboard-table tbody tr.leaderboard-row-highlighted,
|
| 1340 |
+
[data-theme="dark"] #wer-leaderboard-table tbody tr.leaderboard-row-highlighted,
|
| 1341 |
+
[data-theme="dark"] #cer-leaderboard-table tbody tr.leaderboard-row-highlighted,
|
| 1342 |
+
[data-theme="dark"] #rtfx-leaderboard-table tbody tr.leaderboard-row-highlighted {
|
| 1343 |
+
background-color: rgba(140, 208, 255, 0.25) !important;
|
| 1344 |
+
}
|
| 1345 |
+
|
| 1346 |
+
body.dark #wer-leaderboard-table tbody tr.leaderboard-row-highlighted td,
|
| 1347 |
+
body.dark #cer-leaderboard-table tbody tr.leaderboard-row-highlighted td,
|
| 1348 |
+
body.dark #rtfx-leaderboard-table tbody tr.leaderboard-row-highlighted td,
|
| 1349 |
+
[data-theme="dark"] #wer-leaderboard-table tbody tr.leaderboard-row-highlighted td,
|
| 1350 |
+
[data-theme="dark"] #cer-leaderboard-table tbody tr.leaderboard-row-highlighted td,
|
| 1351 |
+
[data-theme="dark"] #rtfx-leaderboard-table tbody tr.leaderboard-row-highlighted td {
|
| 1352 |
+
background-color: rgba(140, 208, 255, 0.25) !important;
|
| 1353 |
+
}
|
| 1354 |
+
|
| 1355 |
+
/* Make all leaderboard cells clickable for row highlighting */
|
| 1356 |
+
#wer-leaderboard-table tbody td,
|
| 1357 |
+
#cer-leaderboard-table tbody td,
|
| 1358 |
+
#rtfx-leaderboard-table tbody td {
|
| 1359 |
+
cursor: pointer;
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
.metric-table-empty {
|
| 1363 |
+
padding: 1rem;
|
| 1364 |
+
text-align: center;
|
| 1365 |
+
color: var(--metric-table-empty-color);
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
/* Footer Styles */
|
| 1369 |
+
.paza-footer {
|
| 1370 |
+
margin-top: 2rem;
|
| 1371 |
+
padding: 1.5rem 1rem;
|
| 1372 |
+
border-top: 1px solid var(--metric-table-border, #d3d3d3);
|
| 1373 |
+
text-align: center;
|
| 1374 |
+
font-size: 0.9rem;
|
| 1375 |
+
color: var(--metric-table-caption, #666666);
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
.paza-footer a {
|
| 1379 |
+
color: #0078d4;
|
| 1380 |
+
text-decoration: none;
|
| 1381 |
+
transition: color 0.2s ease;
|
| 1382 |
+
}
|
| 1383 |
+
|
| 1384 |
+
.paza-footer a:hover {
|
| 1385 |
+
color: #106ebe;
|
| 1386 |
+
text-decoration: underline;
|
| 1387 |
+
}
|
| 1388 |
+
|
| 1389 |
+
/* Dark mode footer link colors */
|
| 1390 |
+
body.dark .paza-footer a,
|
| 1391 |
+
[data-theme="dark"] .paza-footer a,
|
| 1392 |
+
body[class*="dark"] .paza-footer a,
|
| 1393 |
+
[data-color-mode="dark"] .paza-footer a {
|
| 1394 |
+
color: #60a5fa;
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
+
body.dark .paza-footer a:hover,
|
| 1398 |
+
[data-theme="dark"] .paza-footer a:hover,
|
| 1399 |
+
body[class*="dark"] .paza-footer a:hover,
|
| 1400 |
+
[data-color-mode="dark"] .paza-footer a:hover {
|
| 1401 |
+
color: #93c5fd;
|
| 1402 |
+
}
|
| 1403 |
+
|
| 1404 |
+
.paza-footer-links {
|
| 1405 |
+
display: flex;
|
| 1406 |
+
flex-wrap: wrap;
|
| 1407 |
+
justify-content: center;
|
| 1408 |
+
align-items: center;
|
| 1409 |
+
gap: 1rem 1.5rem;
|
| 1410 |
+
}
|
| 1411 |
+
|
| 1412 |
+
.paza-footer-links .copyright {
|
| 1413 |
+
font-size: 0.85rem;
|
| 1414 |
+
}
|
| 1415 |
+
"""
|
| 1416 |
+
|
| 1417 |
+
get_window_url_params = """
|
| 1418 |
+
function(url_params) {
|
| 1419 |
+
const params = new URLSearchParams(window.location.search);
|
| 1420 |
+
url_params = Object.fromEntries(params);
|
| 1421 |
+
return url_params;
|
| 1422 |
+
}
|
| 1423 |
+
"""
|
| 1424 |
+
|
| 1425 |
+
# WCAG 4.1.2 Fix: JavaScript to remove focusability from aria-hidden elements.
|
| 1426 |
+
# Also includes dark mode flash prevention script.
|
| 1427 |
+
fix_aria_hidden_focus_js = """
|
| 1428 |
+
<script>
|
| 1429 |
+
// Dark mode flash prevention - run immediately before page renders
|
| 1430 |
+
(function() {
|
| 1431 |
+
// Check for HuggingFace Spaces dark mode preference or system preference
|
| 1432 |
+
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| 1433 |
+
const isHFDark = document.documentElement.classList.contains('dark') ||
|
| 1434 |
+
document.body?.classList.contains('dark') ||
|
| 1435 |
+
document.documentElement.getAttribute('data-theme') === 'dark' ||
|
| 1436 |
+
document.documentElement.getAttribute('data-color-mode') === 'dark';
|
| 1437 |
+
|
| 1438 |
+
if (isDarkMode || isHFDark) {
|
| 1439 |
+
// Apply dark class immediately to prevent flash
|
| 1440 |
+
document.documentElement.classList.add('dark');
|
| 1441 |
+
document.documentElement.setAttribute('data-theme', 'dark');
|
| 1442 |
+
if (document.body) {
|
| 1443 |
+
document.body.classList.add('dark');
|
| 1444 |
+
} else {
|
| 1445 |
+
// If body not ready, wait for it
|
| 1446 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1447 |
+
document.body.classList.add('dark');
|
| 1448 |
+
});
|
| 1449 |
+
}
|
| 1450 |
+
}
|
| 1451 |
+
|
| 1452 |
+
// Also listen for theme changes from HF Spaces
|
| 1453 |
+
const observer = new MutationObserver(function(mutations) {
|
| 1454 |
+
mutations.forEach(function(mutation) {
|
| 1455 |
+
if (mutation.attributeName === 'class' ||
|
| 1456 |
+
mutation.attributeName === 'data-theme' ||
|
| 1457 |
+
mutation.attributeName === 'data-color-mode') {
|
| 1458 |
+
const html = document.documentElement;
|
| 1459 |
+
const body = document.body;
|
| 1460 |
+
if (html.classList.contains('dark') ||
|
| 1461 |
+
html.getAttribute('data-theme') === 'dark' ||
|
| 1462 |
+
html.getAttribute('data-color-mode') === 'dark') {
|
| 1463 |
+
if (body && !body.classList.contains('dark')) {
|
| 1464 |
+
body.classList.add('dark');
|
| 1465 |
+
}
|
| 1466 |
+
}
|
| 1467 |
+
}
|
| 1468 |
+
});
|
| 1469 |
+
});
|
| 1470 |
+
|
| 1471 |
+
observer.observe(document.documentElement, {
|
| 1472 |
+
attributes: true,
|
| 1473 |
+
attributeFilter: ['class', 'data-theme', 'data-color-mode']
|
| 1474 |
+
});
|
| 1475 |
+
})();
|
| 1476 |
+
</script>
|
| 1477 |
+
<script>
|
| 1478 |
+
(function() {
|
| 1479 |
+
function fixAriaHiddenFocus() {
|
| 1480 |
+
// Find all aria-hidden containers and make their focusable children unfocusable
|
| 1481 |
+
const hiddenContainers = document.querySelectorAll('[aria-hidden="true"]');
|
| 1482 |
+
hiddenContainers.forEach(container => {
|
| 1483 |
+
const focusableElements = container.querySelectorAll(
|
| 1484 |
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
| 1485 |
+
);
|
| 1486 |
+
focusableElements.forEach(el => {
|
| 1487 |
+
el.setAttribute('tabindex', '-1');
|
| 1488 |
+
el.setAttribute('aria-disabled', 'true');
|
| 1489 |
+
el.setAttribute('data-a11y-fixed', 'true');
|
| 1490 |
+
// Also disable the button to prevent any form of focus
|
| 1491 |
+
if (el.tagName === 'BUTTON') {
|
| 1492 |
+
el.disabled = true;
|
| 1493 |
+
}
|
| 1494 |
+
});
|
| 1495 |
+
});
|
| 1496 |
+
}
|
| 1497 |
+
|
| 1498 |
+
// Run immediately
|
| 1499 |
+
fixAriaHiddenFocus();
|
| 1500 |
+
|
| 1501 |
+
// Run on DOMContentLoaded
|
| 1502 |
+
document.addEventListener('DOMContentLoaded', fixAriaHiddenFocus);
|
| 1503 |
+
|
| 1504 |
+
// Run after a short delay to catch late-loading Gradio components
|
| 1505 |
+
setTimeout(fixAriaHiddenFocus, 100);
|
| 1506 |
+
setTimeout(fixAriaHiddenFocus, 500);
|
| 1507 |
+
setTimeout(fixAriaHiddenFocus, 1000);
|
| 1508 |
+
setTimeout(fixAriaHiddenFocus, 2000);
|
| 1509 |
+
|
| 1510 |
+
// Use MutationObserver to handle dynamically added content
|
| 1511 |
+
const observer = new MutationObserver((mutations) => {
|
| 1512 |
+
fixAriaHiddenFocus();
|
| 1513 |
+
});
|
| 1514 |
+
|
| 1515 |
+
// Start observing when body is available
|
| 1516 |
+
function startObserver() {
|
| 1517 |
+
if (document.body) {
|
| 1518 |
+
observer.observe(document.body, {
|
| 1519 |
+
childList: true,
|
| 1520 |
+
subtree: true,
|
| 1521 |
+
attributes: true,
|
| 1522 |
+
attributeFilter: ['aria-hidden']
|
| 1523 |
+
});
|
| 1524 |
+
fixAriaHiddenFocus();
|
| 1525 |
+
} else {
|
| 1526 |
+
setTimeout(startObserver, 10);
|
| 1527 |
+
}
|
| 1528 |
+
}
|
| 1529 |
+
startObserver();
|
| 1530 |
+
})();
|
| 1531 |
+
|
| 1532 |
+
// Row highlighting for metric tables when clicking on language
|
| 1533 |
+
(function() {
|
| 1534 |
+
function setupRowHighlighting() {
|
| 1535 |
+
const tables = document.querySelectorAll('.metric-table');
|
| 1536 |
+
tables.forEach(table => {
|
| 1537 |
+
if (table.dataset.highlightSetup) return;
|
| 1538 |
+
table.dataset.highlightSetup = 'true';
|
| 1539 |
+
|
| 1540 |
+
const rows = table.querySelectorAll('tbody tr');
|
| 1541 |
+
rows.forEach(row => {
|
| 1542 |
+
const languageCell = row.querySelector('th');
|
| 1543 |
+
if (languageCell) {
|
| 1544 |
+
languageCell.addEventListener('click', function() {
|
| 1545 |
+
// Toggle highlight on the clicked row
|
| 1546 |
+
const isHighlighted = row.classList.contains('row-highlighted');
|
| 1547 |
+
// Remove highlight from all rows in this table
|
| 1548 |
+
rows.forEach(r => r.classList.remove('row-highlighted'));
|
| 1549 |
+
// If row wasn't highlighted, highlight it now
|
| 1550 |
+
if (!isHighlighted) {
|
| 1551 |
+
row.classList.add('row-highlighted');
|
| 1552 |
+
}
|
| 1553 |
+
});
|
| 1554 |
+
}
|
| 1555 |
+
});
|
| 1556 |
+
});
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
// Run on load and observe for dynamically added tables
|
| 1560 |
+
document.addEventListener('DOMContentLoaded', setupRowHighlighting);
|
| 1561 |
+
setTimeout(setupRowHighlighting, 100);
|
| 1562 |
+
setTimeout(setupRowHighlighting, 500);
|
| 1563 |
+
setTimeout(setupRowHighlighting, 1000);
|
| 1564 |
+
setTimeout(setupRowHighlighting, 2000);
|
| 1565 |
+
|
| 1566 |
+
const rowObserver = new MutationObserver((mutations) => {
|
| 1567 |
+
setupRowHighlighting();
|
| 1568 |
+
});
|
| 1569 |
+
|
| 1570 |
+
function startRowObserver() {
|
| 1571 |
+
if (document.body) {
|
| 1572 |
+
rowObserver.observe(document.body, {
|
| 1573 |
+
childList: true,
|
| 1574 |
+
subtree: true
|
| 1575 |
+
});
|
| 1576 |
+
setupRowHighlighting();
|
| 1577 |
+
} else {
|
| 1578 |
+
setTimeout(startRowObserver, 10);
|
| 1579 |
+
}
|
| 1580 |
+
}
|
| 1581 |
+
startRowObserver();
|
| 1582 |
+
})();
|
| 1583 |
+
|
| 1584 |
+
// Row highlighting and row-based sorting for Gradio leaderboard dataframes (WER, CER, RTFx tables)
|
| 1585 |
+
(function() {
|
| 1586 |
+
// Track sort state per table per row
|
| 1587 |
+
const rowSortState = {};
|
| 1588 |
+
// Track which containers have event listeners attached
|
| 1589 |
+
const setupContainers = new WeakSet();
|
| 1590 |
+
|
| 1591 |
+
function setupLeaderboardRowSorting() {
|
| 1592 |
+
const leaderboardIds = ['#wer-leaderboard-table', '#cer-leaderboard-table', '#rtfx-leaderboard-table'];
|
| 1593 |
+
|
| 1594 |
+
leaderboardIds.forEach(tableId => {
|
| 1595 |
+
const container = document.querySelector(tableId);
|
| 1596 |
+
if (!container) return;
|
| 1597 |
+
|
| 1598 |
+
// Initialize sort state for this table
|
| 1599 |
+
if (!rowSortState[tableId]) {
|
| 1600 |
+
rowSortState[tableId] = { rowIndex: null, ascending: true };
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
// Skip if this specific container element already has event listener
|
| 1604 |
+
if (setupContainers.has(container)) return;
|
| 1605 |
+
setupContainers.add(container);
|
| 1606 |
+
|
| 1607 |
+
// Use event delegation on the container - Gradio uses divs, not tables
|
| 1608 |
+
container.addEventListener('click', function(e) {
|
| 1609 |
+
// Don't intercept clicks on sort buttons in header
|
| 1610 |
+
if (e.target.closest('button') || e.target.closest('.sort-button')) {
|
| 1611 |
+
return;
|
| 1612 |
+
}
|
| 1613 |
+
|
| 1614 |
+
// Ignore clicks in the aria-hidden sizing table
|
| 1615 |
+
const ariaHiddenTable = e.target.closest('table[aria-hidden="true"]');
|
| 1616 |
+
if (ariaHiddenTable) {
|
| 1617 |
+
return;
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
// Find the clicked cell - Gradio uses div.cell-wrap or td
|
| 1621 |
+
let clickedCell = e.target.closest('td');
|
| 1622 |
+
if (!clickedCell) {
|
| 1623 |
+
// Maybe clicked on span.text inside cell-wrap
|
| 1624 |
+
const cellWrap = e.target.closest('.cell-wrap');
|
| 1625 |
+
if (cellWrap) {
|
| 1626 |
+
clickedCell = cellWrap.closest('td');
|
| 1627 |
+
}
|
| 1628 |
+
}
|
| 1629 |
+
if (!clickedCell) {
|
| 1630 |
+
return;
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
const clickedRow = clickedCell.closest('tr');
|
| 1634 |
+
if (!clickedRow) {
|
| 1635 |
+
return;
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
// Check if this is a body row (not header) - use the specific class
|
| 1639 |
+
const tbody = clickedRow.closest('tbody.tbody') || clickedRow.closest('tbody');
|
| 1640 |
+
if (!tbody) {
|
| 1641 |
+
return;
|
| 1642 |
+
}
|
| 1643 |
+
|
| 1644 |
+
// Make sure this tbody is inside a svelte-virtual-table-viewport (not the hidden sizing table)
|
| 1645 |
+
const virtualViewport = tbody.closest('svelte-virtual-table-viewport');
|
| 1646 |
+
if (!virtualViewport) {
|
| 1647 |
+
return;
|
| 1648 |
+
}
|
| 1649 |
+
|
| 1650 |
+
// Get all cells in the row
|
| 1651 |
+
const allCells = Array.from(clickedRow.querySelectorAll('td'));
|
| 1652 |
+
const cellIndex = allCells.indexOf(clickedCell);
|
| 1653 |
+
|
| 1654 |
+
// If clicking on # column (index 0) or Language column (index 1), sort the row
|
| 1655 |
+
if (cellIndex === 0 || cellIndex === 1) {
|
| 1656 |
+
e.preventDefault();
|
| 1657 |
+
e.stopPropagation();
|
| 1658 |
+
|
| 1659 |
+
// Find the virtual table (the one with actual data, not aria-hidden one)
|
| 1660 |
+
const virtualTableViewport = container.querySelector('svelte-virtual-table-viewport');
|
| 1661 |
+
const table = virtualTableViewport ?
|
| 1662 |
+
virtualTableViewport.querySelector('table.table') :
|
| 1663 |
+
container.querySelector('table:not([aria-hidden="true"])');
|
| 1664 |
+
const thead = table ? table.querySelector('thead.thead') || table.querySelector('thead') : null;
|
| 1665 |
+
const virtualTbody = table ? table.querySelector('tbody.tbody') || table.querySelector('tbody') : null;
|
| 1666 |
+
|
| 1667 |
+
if (table && thead && virtualTbody) {
|
| 1668 |
+
sortRowColumns(tableId, table, thead, virtualTbody, clickedRow);
|
| 1669 |
+
}
|
| 1670 |
+
return;
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
+
// For other cells, just toggle highlight
|
| 1674 |
+
const allRows = tbody.querySelectorAll('tr');
|
| 1675 |
+
const isHighlighted = clickedRow.classList.contains('leaderboard-row-highlighted');
|
| 1676 |
+
|
| 1677 |
+
// Remove highlight from all rows
|
| 1678 |
+
allRows.forEach(row => row.classList.remove('leaderboard-row-highlighted'));
|
| 1679 |
+
|
| 1680 |
+
// If row wasn't highlighted, highlight it now
|
| 1681 |
+
if (!isHighlighted) {
|
| 1682 |
+
clickedRow.classList.add('leaderboard-row-highlighted');
|
| 1683 |
+
}
|
| 1684 |
+
}, true); // Use capture phase
|
| 1685 |
+
|
| 1686 |
+
// Add tooltips to # and Language columns to explain row sorting
|
| 1687 |
+
addRowSortTooltips(container);
|
| 1688 |
+
});
|
| 1689 |
+
}
|
| 1690 |
+
|
| 1691 |
+
function addRowSortTooltips(container) {
|
| 1692 |
+
// Find the virtual table tbody (not the aria-hidden one)
|
| 1693 |
+
const virtualTableViewport = container.querySelector('svelte-virtual-table-viewport');
|
| 1694 |
+
const table = virtualTableViewport ?
|
| 1695 |
+
virtualTableViewport.querySelector('table.table') :
|
| 1696 |
+
container.querySelector('table:not([aria-hidden="true"])');
|
| 1697 |
+
const tbody = table ? table.querySelector('tbody.tbody') || table.querySelector('tbody') : null;
|
| 1698 |
+
if (!tbody) return;
|
| 1699 |
+
|
| 1700 |
+
const rows = tbody.querySelectorAll('tr');
|
| 1701 |
+
rows.forEach(row => {
|
| 1702 |
+
const cells = row.querySelectorAll('td');
|
| 1703 |
+
if (cells.length >= 2) {
|
| 1704 |
+
// # column (index 0)
|
| 1705 |
+
if (cells[0] && !cells[0].dataset.tooltipSet) {
|
| 1706 |
+
cells[0].title = 'Click to sort columns by this row\'s values';
|
| 1707 |
+
cells[0].dataset.tooltipSet = 'true';
|
| 1708 |
+
}
|
| 1709 |
+
// Language column (index 1)
|
| 1710 |
+
if (cells[1] && !cells[1].dataset.tooltipSet) {
|
| 1711 |
+
const langName = cells[1].textContent.trim();
|
| 1712 |
+
cells[1].title = `Click to sort model columns by ${langName}'s scores`;
|
| 1713 |
+
cells[1].dataset.tooltipSet = 'true';
|
| 1714 |
+
}
|
| 1715 |
+
}
|
| 1716 |
+
});
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
function sortRowColumns(tableId, table, thead, tbody, clickedRow) {
|
| 1720 |
+
// Get all header cells
|
| 1721 |
+
const headerRow = thead.querySelector('tr');
|
| 1722 |
+
if (!headerRow) {
|
| 1723 |
+
return;
|
| 1724 |
+
}
|
| 1725 |
+
|
| 1726 |
+
const headerCells = Array.from(headerRow.querySelectorAll('th'));
|
| 1727 |
+
const rowCells = Array.from(clickedRow.querySelectorAll('td'));
|
| 1728 |
+
|
| 1729 |
+
// First 2 columns are # and Language - don't include in sort
|
| 1730 |
+
const fixedCols = 2;
|
| 1731 |
+
|
| 1732 |
+
// Extract model columns (starting from index 2)
|
| 1733 |
+
const modelHeaders = headerCells.slice(fixedCols);
|
| 1734 |
+
const modelCells = rowCells.slice(fixedCols);
|
| 1735 |
+
|
| 1736 |
+
if (modelHeaders.length === 0 || modelCells.length === 0) {
|
| 1737 |
+
return;
|
| 1738 |
+
}
|
| 1739 |
+
|
| 1740 |
+
// Get row index to track sort state
|
| 1741 |
+
const allBodyRows = Array.from(tbody.querySelectorAll('tr'));
|
| 1742 |
+
const rowIndex = allBodyRows.indexOf(clickedRow);
|
| 1743 |
+
|
| 1744 |
+
// Determine sort direction - toggle if same row clicked again
|
| 1745 |
+
let ascending;
|
| 1746 |
+
if (rowSortState[tableId].rowIndex === rowIndex) {
|
| 1747 |
+
ascending = !rowSortState[tableId].ascending;
|
| 1748 |
+
} else {
|
| 1749 |
+
// Determine default direction based on table type
|
| 1750 |
+
// WER/CER: lower is better, so ascending (best first)
|
| 1751 |
+
// RTFx: higher is better, so descending (best first)
|
| 1752 |
+
ascending = !tableId.includes('rtfx');
|
| 1753 |
+
}
|
| 1754 |
+
rowSortState[tableId] = { rowIndex, ascending };
|
| 1755 |
+
|
| 1756 |
+
// Create array of {header, cell, value} for sorting
|
| 1757 |
+
const columnsData = modelHeaders.map((header, i) => {
|
| 1758 |
+
const cell = modelCells[i];
|
| 1759 |
+
// Get text content - handle Gradio's nested structure
|
| 1760 |
+
let cellText = '';
|
| 1761 |
+
if (cell) {
|
| 1762 |
+
// Try different selectors for Gradio's structure
|
| 1763 |
+
const textEl = cell.querySelector('.cell-wrap .text') ||
|
| 1764 |
+
cell.querySelector('.cell-wrap') ||
|
| 1765 |
+
cell.querySelector('span') ||
|
| 1766 |
+
cell;
|
| 1767 |
+
cellText = textEl.textContent.trim();
|
| 1768 |
+
}
|
| 1769 |
+
// Parse value, treating '--' or empty as Infinity (worst)
|
| 1770 |
+
let value = parseFloat(cellText);
|
| 1771 |
+
if (isNaN(value) || cellText === '--' || cellText === '') {
|
| 1772 |
+
value = ascending ? Infinity : -Infinity;
|
| 1773 |
+
}
|
| 1774 |
+
return { header, cell, value, originalIndex: i };
|
| 1775 |
+
});
|
| 1776 |
+
|
| 1777 |
+
// Sort columns by value
|
| 1778 |
+
columnsData.sort((a, b) => {
|
| 1779 |
+
if (ascending) {
|
| 1780 |
+
return a.value - b.value;
|
| 1781 |
+
} else {
|
| 1782 |
+
return b.value - a.value;
|
| 1783 |
+
}
|
| 1784 |
+
});
|
| 1785 |
+
|
| 1786 |
+
// Store new column order indices
|
| 1787 |
+
const newOrder = columnsData.map(col => col.originalIndex);
|
| 1788 |
+
|
| 1789 |
+
// Reorder header cells (after fixed columns)
|
| 1790 |
+
const newHeaderOrder = [];
|
| 1791 |
+
for (let i = 0; i < fixedCols; i++) {
|
| 1792 |
+
newHeaderOrder.push(headerCells[i]);
|
| 1793 |
+
}
|
| 1794 |
+
newOrder.forEach(idx => {
|
| 1795 |
+
newHeaderOrder.push(headerCells[fixedCols + idx]);
|
| 1796 |
+
});
|
| 1797 |
+
|
| 1798 |
+
// Clear and rebuild header row
|
| 1799 |
+
while (headerRow.firstChild) {
|
| 1800 |
+
headerRow.removeChild(headerRow.firstChild);
|
| 1801 |
+
}
|
| 1802 |
+
newHeaderOrder.forEach(cell => {
|
| 1803 |
+
if (cell) headerRow.appendChild(cell);
|
| 1804 |
+
});
|
| 1805 |
+
|
| 1806 |
+
// Reorder cells in all body rows
|
| 1807 |
+
allBodyRows.forEach((row, rIdx) => {
|
| 1808 |
+
const cells = Array.from(row.querySelectorAll('td'));
|
| 1809 |
+
const newCellOrder = [];
|
| 1810 |
+
for (let i = 0; i < fixedCols; i++) {
|
| 1811 |
+
newCellOrder.push(cells[i]);
|
| 1812 |
+
}
|
| 1813 |
+
newOrder.forEach(idx => {
|
| 1814 |
+
newCellOrder.push(cells[fixedCols + idx]);
|
| 1815 |
+
});
|
| 1816 |
+
|
| 1817 |
+
// Clear and rebuild row
|
| 1818 |
+
while (row.firstChild) {
|
| 1819 |
+
row.removeChild(row.firstChild);
|
| 1820 |
+
}
|
| 1821 |
+
newCellOrder.forEach(cell => {
|
| 1822 |
+
if (cell) row.appendChild(cell);
|
| 1823 |
+
});
|
| 1824 |
+
});
|
| 1825 |
+
|
| 1826 |
+
// Highlight the clicked row to show it was used for sorting
|
| 1827 |
+
allBodyRows.forEach(row => row.classList.remove('leaderboard-row-highlighted'));
|
| 1828 |
+
// Find the row at the same index after reordering
|
| 1829 |
+
const newRows = tbody.querySelectorAll('tr');
|
| 1830 |
+
if (newRows[rowIndex]) {
|
| 1831 |
+
newRows[rowIndex].classList.add('leaderboard-row-highlighted');
|
| 1832 |
+
}
|
| 1833 |
+
}
|
| 1834 |
+
|
| 1835 |
+
// Run on load and observe for dynamically added tables
|
| 1836 |
+
document.addEventListener('DOMContentLoaded', setupLeaderboardRowSorting);
|
| 1837 |
+
setTimeout(setupLeaderboardRowSorting, 500);
|
| 1838 |
+
setTimeout(setupLeaderboardRowSorting, 1000);
|
| 1839 |
+
setTimeout(setupLeaderboardRowSorting, 2000);
|
| 1840 |
+
setTimeout(setupLeaderboardRowSorting, 3000);
|
| 1841 |
+
setTimeout(setupLeaderboardRowSorting, 5000);
|
| 1842 |
+
|
| 1843 |
+
const leaderboardObserver = new MutationObserver((mutations) => {
|
| 1844 |
+
// Check if table content was updated - need to reset sort state
|
| 1845 |
+
for (const m of mutations) {
|
| 1846 |
+
if (m.type === 'childList') {
|
| 1847 |
+
// Check if this affects one of our leaderboard tables
|
| 1848 |
+
const targetContainer = m.target.closest('#wer-leaderboard-table, #cer-leaderboard-table, #rtfx-leaderboard-table');
|
| 1849 |
+
if (targetContainer) {
|
| 1850 |
+
// Reset sort state for this table when its content changes
|
| 1851 |
+
const tableId = '#' + targetContainer.id;
|
| 1852 |
+
if (rowSortState[tableId]) {
|
| 1853 |
+
rowSortState[tableId] = { rowIndex: null, ascending: true };
|
| 1854 |
+
}
|
| 1855 |
+
// Refresh tooltips for the updated content
|
| 1856 |
+
addRowSortTooltips(targetContainer);
|
| 1857 |
+
}
|
| 1858 |
+
}
|
| 1859 |
+
}
|
| 1860 |
+
|
| 1861 |
+
// Always try to set up - the WeakSet will prevent duplicate listeners
|
| 1862 |
+
setupLeaderboardRowSorting();
|
| 1863 |
+
|
| 1864 |
+
// Also add tooltips to any newly added tables
|
| 1865 |
+
const containers = document.querySelectorAll('#wer-leaderboard-table, #cer-leaderboard-table, #rtfx-leaderboard-table');
|
| 1866 |
+
containers.forEach(addRowSortTooltips);
|
| 1867 |
+
});
|
| 1868 |
+
|
| 1869 |
+
function startLeaderboardObserver() {
|
| 1870 |
+
if (document.body) {
|
| 1871 |
+
leaderboardObserver.observe(document.body, {
|
| 1872 |
+
childList: true,
|
| 1873 |
+
subtree: true
|
| 1874 |
+
});
|
| 1875 |
+
setupLeaderboardRowSorting();
|
| 1876 |
+
} else {
|
| 1877 |
+
setTimeout(startLeaderboardObserver, 10);
|
| 1878 |
+
}
|
| 1879 |
+
}
|
| 1880 |
+
startLeaderboardObserver();
|
| 1881 |
+
})();
|
| 1882 |
+
|
| 1883 |
+
// Column resizing for leaderboard dataframes (WER, CER, RTFx tables)
|
| 1884 |
+
(function() {
|
| 1885 |
+
function setupColumnResize() {
|
| 1886 |
+
const leaderboardIds = ['#wer-leaderboard-table', '#cer-leaderboard-table', '#rtfx-leaderboard-table'];
|
| 1887 |
+
|
| 1888 |
+
leaderboardIds.forEach(tableId => {
|
| 1889 |
+
const container = document.querySelector(tableId);
|
| 1890 |
+
if (!container) return;
|
| 1891 |
+
|
| 1892 |
+
const table = container.querySelector('table');
|
| 1893 |
+
if (!table || table.dataset.resizeSetup) return;
|
| 1894 |
+
table.dataset.resizeSetup = 'true';
|
| 1895 |
+
|
| 1896 |
+
const headers = table.querySelectorAll('thead th');
|
| 1897 |
+
|
| 1898 |
+
headers.forEach((th, index) => {
|
| 1899 |
+
// Skip if resize handle already exists
|
| 1900 |
+
if (th.querySelector('.col-resize-handle')) return;
|
| 1901 |
+
|
| 1902 |
+
// Add title attribute for tooltip showing full header text
|
| 1903 |
+
const headerText = th.textContent.trim();
|
| 1904 |
+
if (headerText && !th.getAttribute('title')) {
|
| 1905 |
+
th.setAttribute('title', headerText);
|
| 1906 |
+
}
|
| 1907 |
+
|
| 1908 |
+
// Create resize handle
|
| 1909 |
+
const resizeHandle = document.createElement('div');
|
| 1910 |
+
resizeHandle.className = 'col-resize-handle';
|
| 1911 |
+
th.appendChild(resizeHandle);
|
| 1912 |
+
|
| 1913 |
+
let startX, startWidth, isResizing = false;
|
| 1914 |
+
|
| 1915 |
+
resizeHandle.addEventListener('mousedown', function(e) {
|
| 1916 |
+
e.preventDefault();
|
| 1917 |
+
e.stopPropagation();
|
| 1918 |
+
isResizing = true;
|
| 1919 |
+
startX = e.pageX;
|
| 1920 |
+
startWidth = th.offsetWidth;
|
| 1921 |
+
resizeHandle.classList.add('resizing');
|
| 1922 |
+
document.body.style.cursor = 'col-resize';
|
| 1923 |
+
document.body.style.userSelect = 'none';
|
| 1924 |
+
});
|
| 1925 |
+
|
| 1926 |
+
document.addEventListener('mousemove', function(e) {
|
| 1927 |
+
if (!isResizing) return;
|
| 1928 |
+
const diff = e.pageX - startX;
|
| 1929 |
+
const newWidth = Math.max(60, Math.min(300, startWidth + diff));
|
| 1930 |
+
th.style.width = newWidth + 'px';
|
| 1931 |
+
th.style.minWidth = newWidth + 'px';
|
| 1932 |
+
th.style.maxWidth = newWidth + 'px';
|
| 1933 |
+
|
| 1934 |
+
// Also resize corresponding cells in tbody
|
| 1935 |
+
const tbody = table.querySelector('tbody');
|
| 1936 |
+
if (tbody) {
|
| 1937 |
+
const rows = tbody.querySelectorAll('tr');
|
| 1938 |
+
rows.forEach(row => {
|
| 1939 |
+
const cell = row.children[index];
|
| 1940 |
+
if (cell) {
|
| 1941 |
+
cell.style.width = newWidth + 'px';
|
| 1942 |
+
cell.style.minWidth = newWidth + 'px';
|
| 1943 |
+
cell.style.maxWidth = newWidth + 'px';
|
| 1944 |
+
}
|
| 1945 |
+
});
|
| 1946 |
+
}
|
| 1947 |
+
});
|
| 1948 |
+
|
| 1949 |
+
document.addEventListener('mouseup', function() {
|
| 1950 |
+
if (isResizing) {
|
| 1951 |
+
isResizing = false;
|
| 1952 |
+
resizeHandle.classList.remove('resizing');
|
| 1953 |
+
document.body.style.cursor = '';
|
| 1954 |
+
document.body.style.userSelect = '';
|
| 1955 |
+
}
|
| 1956 |
+
});
|
| 1957 |
+
});
|
| 1958 |
+
|
| 1959 |
+
// Add title attributes to all cells for tooltip on truncated content
|
| 1960 |
+
const cells = table.querySelectorAll('tbody td');
|
| 1961 |
+
cells.forEach(cell => {
|
| 1962 |
+
const cellText = cell.textContent.trim();
|
| 1963 |
+
if (cellText && !cell.getAttribute('title')) {
|
| 1964 |
+
cell.setAttribute('title', cellText);
|
| 1965 |
+
}
|
| 1966 |
+
});
|
| 1967 |
+
});
|
| 1968 |
+
}
|
| 1969 |
+
|
| 1970 |
+
// Run on load and observe for dynamically added tables
|
| 1971 |
+
document.addEventListener('DOMContentLoaded', setupColumnResize);
|
| 1972 |
+
setTimeout(setupColumnResize, 100);
|
| 1973 |
+
setTimeout(setupColumnResize, 500);
|
| 1974 |
+
setTimeout(setupColumnResize, 1000);
|
| 1975 |
+
setTimeout(setupColumnResize, 2000);
|
| 1976 |
+
setTimeout(setupColumnResize, 3000);
|
| 1977 |
+
|
| 1978 |
+
const resizeObserver = new MutationObserver((mutations) => {
|
| 1979 |
+
setupColumnResize();
|
| 1980 |
+
});
|
| 1981 |
+
|
| 1982 |
+
function startResizeObserver() {
|
| 1983 |
+
if (document.body) {
|
| 1984 |
+
resizeObserver.observe(document.body, {
|
| 1985 |
+
childList: true,
|
| 1986 |
+
subtree: true
|
| 1987 |
+
});
|
| 1988 |
+
setupColumnResize();
|
| 1989 |
+
} else {
|
| 1990 |
+
setTimeout(startResizeObserver, 10);
|
| 1991 |
+
}
|
| 1992 |
+
}
|
| 1993 |
+
startResizeObserver();
|
| 1994 |
+
})();
|
| 1995 |
+
|
| 1996 |
+
// Disable right-click context menu on dataframes (filter option has no function)
|
| 1997 |
+
(function() {
|
| 1998 |
+
function disableDataframeContextMenu() {
|
| 1999 |
+
const leaderboardIds = ['#wer-leaderboard-table', '#cer-leaderboard-table', '#rtfx-leaderboard-table'];
|
| 2000 |
+
|
| 2001 |
+
leaderboardIds.forEach(tableId => {
|
| 2002 |
+
const container = document.querySelector(tableId);
|
| 2003 |
+
if (!container || container.dataset.contextMenuDisabled) return;
|
| 2004 |
+
container.dataset.contextMenuDisabled = 'true';
|
| 2005 |
+
|
| 2006 |
+
// Prevent default context menu on right-click
|
| 2007 |
+
container.addEventListener('contextmenu', function(e) {
|
| 2008 |
+
e.preventDefault();
|
| 2009 |
+
e.stopPropagation();
|
| 2010 |
+
return false;
|
| 2011 |
+
}, true);
|
| 2012 |
+
});
|
| 2013 |
+
|
| 2014 |
+
// Also target any dataframe tables globally
|
| 2015 |
+
const dataframes = document.querySelectorAll('[data-testid="dataframe"], .table-wrap, .svelte-virtual-table-viewport');
|
| 2016 |
+
dataframes.forEach(df => {
|
| 2017 |
+
if (df.dataset.contextMenuDisabled) return;
|
| 2018 |
+
df.dataset.contextMenuDisabled = 'true';
|
| 2019 |
+
|
| 2020 |
+
df.addEventListener('contextmenu', function(e) {
|
| 2021 |
+
e.preventDefault();
|
| 2022 |
+
e.stopPropagation();
|
| 2023 |
+
return false;
|
| 2024 |
+
}, true);
|
| 2025 |
+
});
|
| 2026 |
+
}
|
| 2027 |
+
|
| 2028 |
+
// Run on load and observe for dynamically added tables
|
| 2029 |
+
document.addEventListener('DOMContentLoaded', disableDataframeContextMenu);
|
| 2030 |
+
setTimeout(disableDataframeContextMenu, 100);
|
| 2031 |
+
setTimeout(disableDataframeContextMenu, 500);
|
| 2032 |
+
setTimeout(disableDataframeContextMenu, 1000);
|
| 2033 |
+
setTimeout(disableDataframeContextMenu, 2000);
|
| 2034 |
+
|
| 2035 |
+
const contextMenuObserver = new MutationObserver((mutations) => {
|
| 2036 |
+
disableDataframeContextMenu();
|
| 2037 |
+
});
|
| 2038 |
+
|
| 2039 |
+
function startContextMenuObserver() {
|
| 2040 |
+
if (document.body) {
|
| 2041 |
+
contextMenuObserver.observe(document.body, {
|
| 2042 |
+
childList: true,
|
| 2043 |
+
subtree: true
|
| 2044 |
+
});
|
| 2045 |
+
disableDataframeContextMenu();
|
| 2046 |
+
} else {
|
| 2047 |
+
setTimeout(startContextMenuObserver, 10);
|
| 2048 |
+
}
|
| 2049 |
+
}
|
| 2050 |
+
startContextMenuObserver();
|
| 2051 |
+
})();
|
| 2052 |
+
</script>
|
| 2053 |
+
"""
|
| 2054 |
+
|
| 2055 |
+
|
| 2056 |
+
light_mode_head = """
|
| 2057 |
+
<script>
|
| 2058 |
+
// Force light mode immediately to prevent flash of dark mode
|
| 2059 |
+
(function() {
|
| 2060 |
+
// Remove dark class if present
|
| 2061 |
+
document.documentElement.classList.remove('dark');
|
| 2062 |
+
document.body && document.body.classList.remove('dark');
|
| 2063 |
+
|
| 2064 |
+
// Set theme parameter in URL if not already light (only once on initial load)
|
| 2065 |
+
var url = new URL(window.location);
|
| 2066 |
+
if (url.searchParams.get('__theme') !== 'light') {
|
| 2067 |
+
url.searchParams.set('__theme', 'light');
|
| 2068 |
+
// Preserve the hash fragment when updating URL
|
| 2069 |
+
var newUrl = url.pathname + url.search + url.hash;
|
| 2070 |
+
history.replaceState(null, '', newUrl);
|
| 2071 |
+
}
|
| 2072 |
+
|
| 2073 |
+
// Also watch for dark class being added and remove it
|
| 2074 |
+
var observer = new MutationObserver(function(mutations) {
|
| 2075 |
+
mutations.forEach(function(mutation) {
|
| 2076 |
+
if (mutation.attributeName === 'class') {
|
| 2077 |
+
var target = mutation.target;
|
| 2078 |
+
if (target.classList.contains('dark')) {
|
| 2079 |
+
target.classList.remove('dark');
|
| 2080 |
+
}
|
| 2081 |
+
}
|
| 2082 |
+
});
|
| 2083 |
+
});
|
| 2084 |
+
|
| 2085 |
+
// Start observing document element
|
| 2086 |
+
observer.observe(document.documentElement, { attributes: true });
|
| 2087 |
+
|
| 2088 |
+
// Also observe body when it's ready
|
| 2089 |
+
if (document.body) {
|
| 2090 |
+
observer.observe(document.body, { attributes: true });
|
| 2091 |
+
} else {
|
| 2092 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 2093 |
+
document.body.classList.remove('dark');
|
| 2094 |
+
observer.observe(document.body, { attributes: true });
|
| 2095 |
+
});
|
| 2096 |
+
}
|
| 2097 |
+
})();
|
| 2098 |
+
</script>
|
| 2099 |
+
<style>
|
| 2100 |
+
/* Force light mode by overriding Gradio's CSS variables */
|
| 2101 |
+
:root, html, body, .gradio-container, .dark, body.dark {
|
| 2102 |
+
--body-background-fill: white !important;
|
| 2103 |
+
--background-fill-primary: white !important;
|
| 2104 |
+
--background-fill-secondary: #f9fafb !important;
|
| 2105 |
+
--body-text-color: #1f2937 !important;
|
| 2106 |
+
--body-text-color-subdued: #6b7280 !important;
|
| 2107 |
+
--block-background-fill: white !important;
|
| 2108 |
+
--block-border-color: #e5e7eb !important;
|
| 2109 |
+
--block-label-background-fill: #f3f4f6 !important;
|
| 2110 |
+
--block-label-text-color: #374151 !important;
|
| 2111 |
+
--block-title-text-color: #111827 !important;
|
| 2112 |
+
--input-background-fill: white !important;
|
| 2113 |
+
--input-border-color: #d1d5db !important;
|
| 2114 |
+
--table-even-background-fill: #f9fafb !important;
|
| 2115 |
+
--table-odd-background-fill: white !important;
|
| 2116 |
+
--table-row-focus: #e0f2fe !important;
|
| 2117 |
+
--border-color-primary: #e5e7eb !important;
|
| 2118 |
+
--neutral-50: #f9fafb !important;
|
| 2119 |
+
--neutral-100: #f3f4f6 !important;
|
| 2120 |
+
--neutral-200: #e5e7eb !important;
|
| 2121 |
+
--neutral-700: #374151 !important;
|
| 2122 |
+
--neutral-800: #1f2937 !important;
|
| 2123 |
+
--neutral-900: #111827 !important;
|
| 2124 |
+
--neutral-950: #030712 !important;
|
| 2125 |
+
}
|
| 2126 |
+
|
| 2127 |
+
/* Apply light background to main containers only */
|
| 2128 |
+
html, body, .gradio-container {
|
| 2129 |
+
background: white !important;
|
| 2130 |
+
color: #1f2937 !important;
|
| 2131 |
+
}
|
| 2132 |
+
|
| 2133 |
+
/* Target Gradio tables specifically, exclude Plotly */
|
| 2134 |
+
.gradio-container table:not(.js-plotly-plot table),
|
| 2135 |
+
.dark .gradio-container table:not(.js-plotly-plot table) {
|
| 2136 |
+
background: white !important;
|
| 2137 |
+
color: #1f2937 !important;
|
| 2138 |
+
}
|
| 2139 |
+
|
| 2140 |
+
.gradio-container th:not(.js-plotly-plot th),
|
| 2141 |
+
.gradio-container td:not(.js-plotly-plot td) {
|
| 2142 |
+
background: inherit !important;
|
| 2143 |
+
color: #1f2937 !important;
|
| 2144 |
+
border-color: #e5e7eb !important;
|
| 2145 |
+
}
|
| 2146 |
+
|
| 2147 |
+
/* Override system dark mode preference */
|
| 2148 |
+
@media (prefers-color-scheme: dark) {
|
| 2149 |
+
:root, html, body, .gradio-container {
|
| 2150 |
+
--body-background-fill: white !important;
|
| 2151 |
+
--background-fill-primary: white !important;
|
| 2152 |
+
--background-fill-secondary: #f9fafb !important;
|
| 2153 |
+
--body-text-color: #1f2937 !important;
|
| 2154 |
+
--block-background-fill: white !important;
|
| 2155 |
+
background: white !important;
|
| 2156 |
+
color: #1f2937 !important;
|
| 2157 |
+
}
|
| 2158 |
+
}
|
| 2159 |
+
|
| 2160 |
+
/* Plotly charts */
|
| 2161 |
+
.js-plotly-plot, .js-plotly-plot *,
|
| 2162 |
+
.plotly, .plotly * {
|
| 2163 |
+
background: unset !important;
|
| 2164 |
+
background-color: unset !important;
|
| 2165 |
+
}
|
| 2166 |
+
</style>
|
| 2167 |
+
"""
|
| 2168 |
+
|
| 2169 |
+
# =============================================================================
|
| 2170 |
+
# HTML Templates
|
| 2171 |
+
# =============================================================================
|
| 2172 |
+
|
| 2173 |
+
chart_gallery_html = """
|
| 2174 |
+
<div class="chart-gallery">
|
| 2175 |
+
<div class="chart-card">
|
| 2176 |
+
<h4>🏆 CER Performance Leaderboard</h4>
|
| 2177 |
+
<p>Ranks models by Character Error Rate. Especially useful for agglutinative and low-resource languages.</p>
|
| 2178 |
+
</div>
|
| 2179 |
+
<div class="chart-card">
|
| 2180 |
+
<h4>🏆 WER Performance Leaderboard</h4>
|
| 2181 |
+
<p>Ranks model families or top individual models by Word Error Rate. Filter by language to see top performers.</p>
|
| 2182 |
+
</div>
|
| 2183 |
+
<div class="chart-card">
|
| 2184 |
+
<h4>🚀 Speed vs Accuracy Tradeoffs</h4>
|
| 2185 |
+
<p>Scatter plot comparing model speed (RTFx) against accuracy (WER). Find the sweet spot between fast and accurate models.</p>
|
| 2186 |
+
</div>
|
| 2187 |
+
<div class="chart-card">
|
| 2188 |
+
<h4>🔍 CER vs WER Correlation</h4>
|
| 2189 |
+
<p>Scatter plot showing relationship between character and word error rates. Understand error patterns across models.</p>
|
| 2190 |
+
</div>
|
| 2191 |
+
</div>
|
| 2192 |
+
"""
|
| 2193 |
+
|
| 2194 |
+
section_divider_html = "<hr style='margin: 2rem 0;'>"
|
| 2195 |
+
|
| 2196 |
+
footer_html = """
|
| 2197 |
+
<footer class="paza-footer">
|
| 2198 |
+
<div class="paza-footer-links">
|
| 2199 |
+
<a href="https://go.microsoft.com/fwlink/?LinkId=521839" target="_blank" rel="noopener noreferrer">Privacy & Cookies</a>
|
| 2200 |
+
<a href="https://www.microsoft.com/en-us/research/lab/microsoft-research-lab-africa-nairobi/" target="_blank" rel="noopener noreferrer">Microsoft Research Africa, Nairobi</a>
|
| 2201 |
+
<a href="https://www.microsoft.com/en-us/research/project/project-gecko/" target="_blank" rel="noopener noreferrer">Project Gecko</a>
|
| 2202 |
+
<a href="https://www.microsoft.com/en-us/research/" target="_blank" rel="noopener noreferrer" class="copyright">© Microsoft Research 2026</a>
|
| 2203 |
+
</div>
|
| 2204 |
+
</footer>
|
| 2205 |
+
"""
|
src/display/formatting.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def model_hyperlink(link, model_name):
|
| 2 |
+
return f'<a target="_blank" href="{link}" style="color: var(--link-text-color); text-decoration: underline;text-decoration-style: dotted;">{model_name}</a>'
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def make_clickable_model(model_name):
|
| 6 |
+
link = f"https://huggingface.co/{model_name}"
|
| 7 |
+
return model_hyperlink(link, model_name)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def styled_error(error):
|
| 11 |
+
# Remove trailing full stop if present for cleaner display
|
| 12 |
+
error_text = error.rstrip('.')
|
| 13 |
+
return f"<p style='color: red; font-size: 14px; text-align: center;'>{error_text}</p>"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def styled_warning(warn):
|
| 17 |
+
return f"<p style='color: orange; font-size: 20px; text-align: center;'>{warn}</p>"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def styled_message(message):
|
| 21 |
+
return f"<p style='color: green; font-size: 20px; text-align: center;'>{message}</p>"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def has_no_nan_values(df, columns):
|
| 25 |
+
return df[columns].notna().all(axis=1)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def has_nan_values(df, columns):
|
| 29 |
+
return df[columns].isna().any(axis=1)
|
src/display/model_family_colors.csv
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model Families,Model ID,Color
|
| 2 |
+
Granite,ibm-granite/granite-speech-3.3-2b,#288A56
|
| 3 |
+
Granite,ibm-granite/granite-speech-3.3-8b,#42B870
|
| 4 |
+
granite-speech,ibm-granite/granite-speech-3.3-2b,#288A56
|
| 5 |
+
Kyutai,kyutai/stt-2.6b-en,#43E5CA
|
| 6 |
+
LiteASR,efficient-speech/lite-whisper-large-v3-acc,#944500
|
| 7 |
+
LiteASR,efficient-speech/lite-whisper-large-v3,#CA6407
|
| 8 |
+
LiteASR,efficient-speech/lite-whisper-large-v3-fast,#FE8401
|
| 9 |
+
LiteASR,efficient-speech/lite-whisper-large-v3-turbo-acc,#FFA43D
|
| 10 |
+
LiteASR,efficient-speech/lite-whisper-large-v3-turbo,#FFC470
|
| 11 |
+
LiteASR,efficient-speech/lite-whisper-large-v3-turbo-fast,#FFE4BE
|
| 12 |
+
lite_asr,efficient-speech/lite-whisper-large-v3,#CA6407
|
| 13 |
+
Moonshine,usefulsensors/moonshine-base,#CC23D1
|
| 14 |
+
Moonshine,usefulsensors/moonshine-tiny,#E656EB
|
| 15 |
+
NeMo,nvidia/canary-1b-v2,#379539
|
| 16 |
+
NeMo,nvidia/canary-qwen-2.5b,#62BE55
|
| 17 |
+
NeMo,nvidia/parakeet-tdt-0.6b-v3,#76D45E
|
| 18 |
+
nvidia-nemo,nvidia/canary-1b-v2,#379539
|
| 19 |
+
nvidia-nemo,nvidia/canary-qwen-2.5b,#62BE55
|
| 20 |
+
nvidia-nemo,nvidia/parakeet-tdt-0.6b-v3,#76D45E
|
| 21 |
+
Omnilingual,omniASR-CTC-300M,#9FF2EA
|
| 22 |
+
Omnilingual,omniASR-CTC-1B,#43E5CA
|
| 23 |
+
Omnilingual,omniASR-CTC-3B,#1EC8B0
|
| 24 |
+
Omnilingual,omniASR-CTC-7B,#22918B
|
| 25 |
+
Omnilingual,omniASR-LLM-300M,#136C6C
|
| 26 |
+
Omnilingual,omniASR-LLM-1B,#01263F
|
| 27 |
+
Omnilingual,omniASR-LLM-3B,#024169
|
| 28 |
+
Omnilingual,omniASR-LLM-7B,#09638E
|
| 29 |
+
Omnilingual,omniASR-LLM-7B-ZS,#1384B1
|
| 30 |
+
Phi-4,microsoft/Phi-4-multimodal-instruct,#64DE89
|
| 31 |
+
Qwen2-Audio,Qwen/Qwen2-Audio-7B-Instruct,#4A43CB
|
| 32 |
+
Qwen2-Audio,Qwen/Qwen2-Audio-7B,#6864F6
|
| 33 |
+
SpeechBrain,speechbrain/asr-transformer-transformerlm-librispeech,#FAB500
|
| 34 |
+
SpeechBrain,speechbrain/asr-crdnn-rnnlm-librispeech,#FFCD0F
|
| 35 |
+
Wav2Vec2,facebook/wav2vec2-large-960h,#003580
|
| 36 |
+
Wav2Vec2,facebook/wav2vec2-large-960h-lv60-self,#0057AA
|
| 37 |
+
Wav2Vec2,facebook/wav2vec2-large-robust-ft-libri-960h,#0078D4
|
| 38 |
+
Wav2Vec2,facebook/wav2vec2-conformer-rel-pos-large-960h-ft,#0FAFFF
|
| 39 |
+
Wav2Vec2,facebook/wav2vec2-conformer-rope-large-960h-ft,#3BD5FF
|
| 40 |
+
facebook-wav2vec2-conformer,facebook/wav2vec2-conformer,#0FAFFF
|
| 41 |
+
Whisper,openai/whisper-tiny,#420C5F
|
| 42 |
+
Whisper,openai/whisper-tiny.en,#55107A
|
| 43 |
+
Whisper,openai/whisper-base,#691A90
|
| 44 |
+
Whisper,openai/whisper-base.en,#7F21A9
|
| 45 |
+
Whisper,openai/whisper-small,#A931D8
|
| 46 |
+
Whisper,openai/whisper-small.en,#CA59F7
|
| 47 |
+
Whisper,openai/whisper-medium,#DC8AFD
|
| 48 |
+
Whisper,openai/whisper-medium.en,#EBC7FA
|
| 49 |
+
Whisper,openai/whisper-large,#CEB0FF
|
| 50 |
+
Whisper,openai/whisper-large-v2,#AC80FF
|
| 51 |
+
Whisper,openai/whisper-large-v3,#8B52F4
|
| 52 |
+
Whisper,openai/whisper-large-v3-turbo,#6D37CD
|
| 53 |
+
openai-whisper,openai/whisper-large-v3,#8B52F4
|
| 54 |
+
Distil-Whisper,distil-whisper/distil-medium.en,#CA2134
|
| 55 |
+
Distil-Whisper,distil-whisper/distil-large-v2,#E82C41
|
| 56 |
+
Distil-Whisper,distil-whisper/distil-large-v3,#FB5B6C
|
| 57 |
+
CrisperWhisper,nyrahealth/CrisperWhisper,#EC318F
|
| 58 |
+
Facebook-MMS,facebook/mms-1b-all,#09638E
|
| 59 |
+
Facebook-MMS,facebook/mms-1b-fl102,#024169
|
| 60 |
+
facebook-mms,facebook/mms-1b-all,#09638E
|
| 61 |
+
paza,paza/microsoft-paza-mms-1b-all,#8CD0FF
|
| 62 |
+
paza,paza/microsoft-paza-whisper-large-v3-turbo,#BABAFF
|
| 63 |
+
paza,paza/microsoft-paza-Phi-4-multimodal-instruct,#CEB0FF
|
| 64 |
+
Facebook-Wav2Vec2,facebook/wav2vec2-base-960h,#0A1852
|
| 65 |
+
facebook-wav2vec2,facebook/wav2vec2-base-960h,#0A1852
|
| 66 |
+
Facebook-Data2Vec,facebook/data2vec-audio-base-960h,#0D1F69
|
| 67 |
+
Facebook-Data2Vec,facebook/data2vec-audio-large-960h,#163697
|
| 68 |
+
facebook-data2vec,facebook/data2vec-audio-base-960h,#0D1F69
|
| 69 |
+
Facebook-HuBERT,facebook/hubert-large-ls960-ft,#2052CB
|
| 70 |
+
Facebook-HuBERT,facebook/hubert-xlarge-ls960-ft,#367AF2
|
| 71 |
+
facebook_hubert,facebook/hubert-large-ls960-ft,#2052CB
|
| 72 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-CTC-300M,#58AAFE
|
| 73 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-CTC-1B,#B3E0FF
|
| 74 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-CTC-3B,#D1ECFF
|
| 75 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-CTC-7B,#95D3FF
|
| 76 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-LLM-300M,#0C74A1
|
| 77 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-LLM-1B,#119FC5
|
| 78 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-LLM-3B,#16BBDA
|
| 79 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-LLM-7B,#36DFF1
|
| 80 |
+
Facebook-Omnilingual-ASR,facebook/omniASR-LLM-7B-ZS,#9FF0F9
|
| 81 |
+
lite_asr_trunc,efficient/speech-lite-whisper-large-v3,#B35900
|
| 82 |
+
lite_asr_trunc,efficient/speech-lite-whisper-large-v3-acc,#CC6600
|
| 83 |
+
lite_asr_trunc,efficient/speech-lite-whisper-large-v3-fast,#E67300
|
| 84 |
+
lite_asr_trunc,efficient/speech-lite-whisper-large-v3-turbo,#FF8000
|
| 85 |
+
lite_asr_trunc,efficient/speech-lite-whisper-large-v3-turbo-acc,#FF9933
|
| 86 |
+
lite_asr_trunc,efficient/speech-lite-whisper-large-v3-turbo-fast,#FFB366
|
| 87 |
+
openai-whisper_trunc,openai/whisper-tiny.en,#5D1480
|
| 88 |
+
openai-whisper_trunc,openai/whisper-base.en,#7A1CA8
|
| 89 |
+
openai-whisper_trunc,openai/whisper-small.en,#9827D2
|
| 90 |
+
openai-whisper_trunc,openai/whisper-medium.en,#B44AE8
|
| 91 |
+
openai-whisper_trunc,openai/whisper-large,#C96FF3
|
| 92 |
+
openai-whisper_trunc,openai/whisper-large-v2,#DA94FA
|
| 93 |
+
openai-whisper_trunc,openai/whisper-large-v3,#E8B5FF
|
| 94 |
+
openai-whisper_trunc,distil/whisper-distil-medium.en,#D12545
|
| 95 |
+
openai-whisper_trunc,distil/whisper-distil-large-v2,#E83050
|
| 96 |
+
openai-whisper_trunc,distil/whisper-distil-large-v3,#F5566E
|
src/display/styling.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Styling and color utilities for the leaderboard display."""
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def format_metric_value(value: float | None, fmt: str) -> str:
|
| 7 |
+
"""Format a metric value for display."""
|
| 8 |
+
if value is None or pd.isna(value):
|
| 9 |
+
return "--"
|
| 10 |
+
try:
|
| 11 |
+
return fmt.format(float(value))
|
| 12 |
+
except Exception:
|
| 13 |
+
return f"{value}"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def hex_to_rgb(code: str) -> tuple[int, int, int]:
|
| 17 |
+
"""Convert hex color code to RGB tuple."""
|
| 18 |
+
code = code.lstrip("#")
|
| 19 |
+
return tuple(int(code[i:i + 2], 16) for i in (0, 2, 4))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def rgb_to_hex(rgb: tuple[float, float, float]) -> str:
|
| 23 |
+
"""Convert RGB tuple to hex color code."""
|
| 24 |
+
return "#" + "".join(f"{max(0, min(255, int(round(channel)))):02x}" for channel in rgb)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def interpolate_color(start_hex: str, end_hex: str, ratio: float) -> str:
|
| 28 |
+
"""Interpolate between two colors based on ratio."""
|
| 29 |
+
ratio = max(0.0, min(1.0, ratio))
|
| 30 |
+
start = hex_to_rgb(start_hex)
|
| 31 |
+
end = hex_to_rgb(end_hex)
|
| 32 |
+
blended = tuple(start[i] + (end[i] - start[i]) * ratio for i in range(3))
|
| 33 |
+
return rgb_to_hex(blended)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def cell_color(value: float | None, minimum: float | None, maximum: float | None, better: str) -> str:
|
| 37 |
+
"""Get cell background color based on value and metric direction."""
|
| 38 |
+
if value is None or pd.isna(value) or minimum is None or maximum is None:
|
| 39 |
+
return "#f3f4f6"
|
| 40 |
+
if abs(maximum - minimum) < 1e-9:
|
| 41 |
+
ratio = 0.5
|
| 42 |
+
else:
|
| 43 |
+
ratio = (float(value) - minimum) / (maximum - minimum)
|
| 44 |
+
if better == "higher":
|
| 45 |
+
ratio = 1.0 - ratio
|
| 46 |
+
return interpolate_color("#d9f7be", "#ffccc7", ratio)
|
src/display/utils.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from enum import Enum
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def fields(raw_class):
|
| 7 |
+
return [v for k, v in raw_class.__dict__.items() if k[:2] != "__" and k[-2:] != "__"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# User-facing column definitions.
|
| 11 |
+
@dataclass
|
| 12 |
+
class ColumnContent:
|
| 13 |
+
name: str
|
| 14 |
+
type: str
|
| 15 |
+
displayed_by_default: bool
|
| 16 |
+
hidden: bool = False
|
| 17 |
+
never_hidden: bool = False
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
## For the queue columns in the submission tab
|
| 21 |
+
class EvalQueueColumn: # Queue column
|
| 22 |
+
model = ColumnContent("model", "markdown", True)
|
| 23 |
+
revision = ColumnContent("revision", "str", True)
|
| 24 |
+
private = ColumnContent("private", "bool", True)
|
| 25 |
+
precision = ColumnContent("precision", "str", True)
|
| 26 |
+
weight_type = ColumnContent("weight_type", "str", True)
|
| 27 |
+
status = ColumnContent("status", "str", True)
|
| 28 |
+
|
| 29 |
+
## All the model information that we might need
|
| 30 |
+
@dataclass
|
| 31 |
+
class ModelDetails:
|
| 32 |
+
name: str
|
| 33 |
+
display_name: str = ""
|
| 34 |
+
symbol: str = "" # emoji
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class ModelType(Enum):
|
| 38 |
+
PT = ModelDetails(name="pretrained", symbol="🟢")
|
| 39 |
+
FT = ModelDetails(name="fine-tuned", symbol="🔶")
|
| 40 |
+
IFT = ModelDetails(name="instruction-tuned", symbol="⭕")
|
| 41 |
+
RL = ModelDetails(name="RL-tuned", symbol="🟦")
|
| 42 |
+
Unknown = ModelDetails(name="", symbol="?")
|
| 43 |
+
|
| 44 |
+
def to_str(self, separator=" "):
|
| 45 |
+
return f"{self.value.symbol}{separator}{self.value.name}"
|
| 46 |
+
|
| 47 |
+
@staticmethod
|
| 48 |
+
def from_str(type):
|
| 49 |
+
if "fine-tuned" in type or "🔶" in type:
|
| 50 |
+
return ModelType.FT
|
| 51 |
+
if "pretrained" in type or "🟢" in type:
|
| 52 |
+
return ModelType.PT
|
| 53 |
+
if "RL-tuned" in type or "🟦" in type:
|
| 54 |
+
return ModelType.RL
|
| 55 |
+
if "instruction-tuned" in type or "⭕" in type:
|
| 56 |
+
return ModelType.IFT
|
| 57 |
+
return ModelType.Unknown
|
| 58 |
+
|
| 59 |
+
class WeightType(Enum):
|
| 60 |
+
Adapter = ModelDetails("Adapter")
|
| 61 |
+
Original = ModelDetails("Original")
|
| 62 |
+
Delta = ModelDetails("Delta")
|
| 63 |
+
|
| 64 |
+
class Precision(Enum):
|
| 65 |
+
float16 = ModelDetails("float16")
|
| 66 |
+
bfloat16 = ModelDetails("bfloat16")
|
| 67 |
+
Unknown = ModelDetails("?")
|
| 68 |
+
|
| 69 |
+
@staticmethod
|
| 70 |
+
def from_str(precision):
|
| 71 |
+
if precision in ["torch.float16", "float16"]:
|
| 72 |
+
return Precision.float16
|
| 73 |
+
if precision in ["torch.bfloat16", "bfloat16"]:
|
| 74 |
+
return Precision.bfloat16
|
| 75 |
+
return Precision.Unknown
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
EVAL_COLS = [c.name for c in fields(EvalQueueColumn)]
|
| 79 |
+
EVAL_TYPES = [c.type for c in fields(EvalQueueColumn)]
|
| 80 |
+
|
src/envs.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from huggingface_hub import HfApi
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
TOKEN = os.environ.get("HF_TOKEN")
|
| 9 |
+
OWNER = os.environ.get("OWNER")
|
| 10 |
+
|
| 11 |
+
REPO_ID = os.environ.get("REPO_ID")
|
| 12 |
+
QUEUE_REPO = os.environ.get("SUBMISSIONS_REPO")
|
| 13 |
+
RESULTS_REPO = os.environ.get("RESULTS_REPO")
|
| 14 |
+
|
| 15 |
+
# If you setup a cache later, just change HF_HOME
|
| 16 |
+
CACHE_PATH=os.getenv("HF_HOME", ".")
|
| 17 |
+
|
| 18 |
+
# Local caches
|
| 19 |
+
EVAL_REQUESTS_PATH = os.path.join(CACHE_PATH, "eval-queue")
|
| 20 |
+
EVAL_RESULTS_PATH = os.path.join(CACHE_PATH, "eval-results")
|
| 21 |
+
EVAL_REQUESTS_PATH_BACKEND = os.path.join(CACHE_PATH, "eval-queue-bk")
|
| 22 |
+
EVAL_RESULTS_PATH_BACKEND = os.path.join(CACHE_PATH, "eval-results-bk")
|
| 23 |
+
|
| 24 |
+
# Only create API if token is available
|
| 25 |
+
API = HfApi(token=TOKEN) if TOKEN else None
|
| 26 |
+
|
| 27 |
+
# Flag to enable/disable HuggingFace Hub features
|
| 28 |
+
HF_ENABLED = TOKEN is not None and os.environ.get("DISABLE_HF_HUB", "").lower() != "true"
|
| 29 |
+
|
| 30 |
+
# Telemetry: Set TELEMETRY_ENABLED=true to enable Azure Application Insights
|
| 31 |
+
TELEMETRY_ENABLED = os.environ.get("TELEMETRY_ENABLED", "false").lower() == "true"
|
| 32 |
+
|
src/language_metadata.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
|
| 3 |
+
from src.constants import COUNTRY_TO_REGION_MAP, LANGUAGE_TO_COUNTRIES_MAP
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def get_language_countries(language: str) -> List[str]:
|
| 7 |
+
"""Get the list of countries where a language is spoken."""
|
| 8 |
+
return LANGUAGE_TO_COUNTRIES_MAP.get(language, ["Unknown"])
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_language_regions(language: str) -> List[str]:
|
| 12 |
+
"""Get the list of African regions where a language is spoken."""
|
| 13 |
+
countries = get_language_countries(language)
|
| 14 |
+
regions = set()
|
| 15 |
+
for country in countries:
|
| 16 |
+
region = COUNTRY_TO_REGION_MAP.get(country)
|
| 17 |
+
if region:
|
| 18 |
+
regions.add(region)
|
| 19 |
+
return list(regions) if regions else ["Unknown"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_all_regions() -> List[str]:
|
| 23 |
+
"""Get all unique African regions."""
|
| 24 |
+
regions = set(COUNTRY_TO_REGION_MAP.values())
|
| 25 |
+
return sorted(regions)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_languages_by_region(region: str) -> List[str]:
|
| 29 |
+
"""Get all languages spoken in a specific African region."""
|
| 30 |
+
countries_in_region = [c for c, r in COUNTRY_TO_REGION_MAP.items() if r == region]
|
| 31 |
+
languages = set()
|
| 32 |
+
for lang, countries in LANGUAGE_TO_COUNTRIES_MAP.items():
|
| 33 |
+
if any(c in countries_in_region for c in countries):
|
| 34 |
+
languages.add(lang)
|
| 35 |
+
return list(languages)
|
src/leaderboard/read_evals.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import glob
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
|
| 6 |
+
import dateutil
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from src.display.formatting import make_clickable_model
|
| 10 |
+
from src.display.utils import AutoEvalColumn, ModelType, Tasks, Precision, WeightType
|
| 11 |
+
from src.submission.check_validity import is_model_on_hub
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class EvalResult:
|
| 16 |
+
"""Represents one full evaluation. Built from a combination of the result and request file for a given run.
|
| 17 |
+
"""
|
| 18 |
+
eval_name: str # org_model_precision (uid)
|
| 19 |
+
full_model: str # org/model (path on hub)
|
| 20 |
+
org: str
|
| 21 |
+
model: str
|
| 22 |
+
revision: str # commit hash, "" if main
|
| 23 |
+
results: dict
|
| 24 |
+
precision: Precision = Precision.Unknown
|
| 25 |
+
model_type: ModelType = ModelType.Unknown # Pretrained, fine tuned, ...
|
| 26 |
+
weight_type: WeightType = WeightType.Original # Original or Adapter
|
| 27 |
+
architecture: str = "Unknown"
|
| 28 |
+
license: str = "?"
|
| 29 |
+
likes: int = 0
|
| 30 |
+
num_params: int = 0
|
| 31 |
+
date: str = "" # submission date of request file
|
| 32 |
+
still_on_hub: bool = False
|
| 33 |
+
|
| 34 |
+
@classmethod
|
| 35 |
+
def init_from_json_file(self, json_filepath):
|
| 36 |
+
"""Inits the result from the specific model result file"""
|
| 37 |
+
with open(json_filepath) as fp:
|
| 38 |
+
data = json.load(fp)
|
| 39 |
+
|
| 40 |
+
config = data.get("config")
|
| 41 |
+
|
| 42 |
+
# Precision
|
| 43 |
+
precision = Precision.from_str(config.get("model_dtype"))
|
| 44 |
+
|
| 45 |
+
# Get model and org
|
| 46 |
+
org_and_model = config.get("model_name", config.get("model_args", None))
|
| 47 |
+
org_and_model = org_and_model.split("/", 1)
|
| 48 |
+
|
| 49 |
+
if len(org_and_model) == 1:
|
| 50 |
+
org = None
|
| 51 |
+
model = org_and_model[0]
|
| 52 |
+
result_key = f"{model}_{precision.value.name}"
|
| 53 |
+
else:
|
| 54 |
+
org = org_and_model[0]
|
| 55 |
+
model = org_and_model[1]
|
| 56 |
+
result_key = f"{org}_{model}_{precision.value.name}"
|
| 57 |
+
full_model = "/".join(org_and_model)
|
| 58 |
+
|
| 59 |
+
still_on_hub, _, model_config = is_model_on_hub(
|
| 60 |
+
full_model, config.get("model_sha", "main"), trust_remote_code=True, test_tokenizer=False
|
| 61 |
+
)
|
| 62 |
+
architecture = "?"
|
| 63 |
+
if model_config is not None:
|
| 64 |
+
architectures = getattr(model_config, "architectures", None)
|
| 65 |
+
if architectures:
|
| 66 |
+
architecture = ";".join(architectures)
|
| 67 |
+
|
| 68 |
+
# Extract results available in this file (some results are split in several files)
|
| 69 |
+
results = {}
|
| 70 |
+
for task in Tasks:
|
| 71 |
+
task = task.value
|
| 72 |
+
|
| 73 |
+
# We average all scores of a given metric (not all metrics are present in all files)
|
| 74 |
+
accs = np.array([v.get(task.metric, None) for k, v in data["results"].items() if task.benchmark == k])
|
| 75 |
+
if accs.size == 0 or any([acc is None for acc in accs]):
|
| 76 |
+
continue
|
| 77 |
+
|
| 78 |
+
mean_acc = np.mean(accs)
|
| 79 |
+
results[task.benchmark] = mean_acc
|
| 80 |
+
|
| 81 |
+
return self(
|
| 82 |
+
eval_name=result_key,
|
| 83 |
+
full_model=full_model,
|
| 84 |
+
org=org,
|
| 85 |
+
model=model,
|
| 86 |
+
results=results,
|
| 87 |
+
precision=precision,
|
| 88 |
+
revision= config.get("model_sha", ""),
|
| 89 |
+
still_on_hub=still_on_hub,
|
| 90 |
+
architecture=architecture
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
def update_with_request_file(self, requests_path):
|
| 94 |
+
"""Finds the relevant request file for the current model and updates info with it"""
|
| 95 |
+
request_file = get_request_file_for_model(requests_path, self.full_model, self.precision.value.name)
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
with open(request_file, "r") as f:
|
| 99 |
+
request = json.load(f)
|
| 100 |
+
self.model_type = ModelType.from_str(request.get("model_type", ""))
|
| 101 |
+
self.weight_type = WeightType[request.get("weight_type", "Original")]
|
| 102 |
+
self.license = request.get("license", "?")
|
| 103 |
+
self.likes = request.get("likes", 0)
|
| 104 |
+
self.num_params = request.get("params", 0)
|
| 105 |
+
self.date = request.get("submitted_time", "")
|
| 106 |
+
except Exception:
|
| 107 |
+
print(f"Could not find request file for {self.org}/{self.model} with precision {self.precision.value.name}")
|
| 108 |
+
|
| 109 |
+
def to_dict(self):
|
| 110 |
+
"""Converts the Eval Result to a dict compatible with our dataframe display"""
|
| 111 |
+
average = sum([v for v in self.results.values() if v is not None]) / len(Tasks)
|
| 112 |
+
data_dict = {
|
| 113 |
+
"eval_name": self.eval_name, # not a column, just a save name,
|
| 114 |
+
AutoEvalColumn.precision.name: self.precision.value.name,
|
| 115 |
+
AutoEvalColumn.model_type.name: self.model_type.value.name,
|
| 116 |
+
AutoEvalColumn.model_type_symbol.name: self.model_type.value.symbol,
|
| 117 |
+
AutoEvalColumn.weight_type.name: self.weight_type.value.name,
|
| 118 |
+
AutoEvalColumn.architecture.name: self.architecture,
|
| 119 |
+
AutoEvalColumn.model.name: make_clickable_model(self.full_model),
|
| 120 |
+
AutoEvalColumn.revision.name: self.revision,
|
| 121 |
+
AutoEvalColumn.average.name: average,
|
| 122 |
+
AutoEvalColumn.license.name: self.license,
|
| 123 |
+
AutoEvalColumn.likes.name: self.likes,
|
| 124 |
+
AutoEvalColumn.params.name: self.num_params,
|
| 125 |
+
AutoEvalColumn.still_on_hub.name: self.still_on_hub,
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
for task in Tasks:
|
| 129 |
+
data_dict[task.value.col_name] = self.results[task.value.benchmark]
|
| 130 |
+
|
| 131 |
+
return data_dict
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def get_request_file_for_model(requests_path, model_name, precision):
|
| 135 |
+
"""Selects the correct request file for a given model. Only keeps runs tagged as FINISHED"""
|
| 136 |
+
request_files = os.path.join(
|
| 137 |
+
requests_path,
|
| 138 |
+
f"{model_name}_eval_request_*.json",
|
| 139 |
+
)
|
| 140 |
+
request_files = glob.glob(request_files)
|
| 141 |
+
|
| 142 |
+
# Select correct request file (precision)
|
| 143 |
+
request_file = ""
|
| 144 |
+
request_files = sorted(request_files, reverse=True)
|
| 145 |
+
for tmp_request_file in request_files:
|
| 146 |
+
with open(tmp_request_file, "r") as f:
|
| 147 |
+
req_content = json.load(f)
|
| 148 |
+
if (
|
| 149 |
+
req_content["status"] in ["FINISHED"]
|
| 150 |
+
and req_content["precision"] == precision.split(".")[-1]
|
| 151 |
+
):
|
| 152 |
+
request_file = tmp_request_file
|
| 153 |
+
return request_file
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def get_raw_eval_results(results_path: str, requests_path: str) -> list[EvalResult]:
|
| 157 |
+
"""From the path of the results folder root, extract all needed info for results"""
|
| 158 |
+
model_result_filepaths = []
|
| 159 |
+
|
| 160 |
+
for root, _, files in os.walk(results_path):
|
| 161 |
+
# We should only have json files in model results
|
| 162 |
+
if len(files) == 0 or any([not f.endswith(".json") for f in files]):
|
| 163 |
+
continue
|
| 164 |
+
|
| 165 |
+
# Sort the files by date
|
| 166 |
+
try:
|
| 167 |
+
files.sort(key=lambda x: x.removesuffix(".json").removeprefix("results_")[:-7])
|
| 168 |
+
except dateutil.parser._parser.ParserError:
|
| 169 |
+
files = [files[-1]]
|
| 170 |
+
|
| 171 |
+
for file in files:
|
| 172 |
+
model_result_filepaths.append(os.path.join(root, file))
|
| 173 |
+
|
| 174 |
+
eval_results = {}
|
| 175 |
+
for model_result_filepath in model_result_filepaths:
|
| 176 |
+
# Creation of result
|
| 177 |
+
eval_result = EvalResult.init_from_json_file(model_result_filepath)
|
| 178 |
+
eval_result.update_with_request_file(requests_path)
|
| 179 |
+
|
| 180 |
+
# Store results of same eval together
|
| 181 |
+
eval_name = eval_result.eval_name
|
| 182 |
+
if eval_name in eval_results.keys():
|
| 183 |
+
eval_results[eval_name].results.update({k: v for k, v in eval_result.results.items() if v is not None})
|
| 184 |
+
else:
|
| 185 |
+
eval_results[eval_name] = eval_result
|
| 186 |
+
|
| 187 |
+
results = []
|
| 188 |
+
for v in eval_results.values():
|
| 189 |
+
try:
|
| 190 |
+
v.to_dict() # we test if the dict version is complete
|
| 191 |
+
results.append(v)
|
| 192 |
+
except KeyError: # not all eval values present
|
| 193 |
+
continue
|
| 194 |
+
|
| 195 |
+
return results
|
src/model_counts.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Model Parameter Counts Dictionary for PazaBench Leaderboard
|
| 2 |
+
# To be used as bubble sizes in the speech vs accuracy tradeoff chart
|
| 3 |
+
# Last updated with verified HuggingFace model card data
|
| 4 |
+
|
| 5 |
+
MODEL_PARAMETER_COUNTS = {
|
| 6 |
+
# Facebook Data2Vec family (based on wav2vec2 architecture)
|
| 7 |
+
"facebook/data2vec-audio-base-960h": 94_400_000, # 94.4M
|
| 8 |
+
"facebook/data2vec-audio-large-960h": 315_000_000, # ~315M (large architecture)
|
| 9 |
+
|
| 10 |
+
# Facebook MMS (Massively Multilingual Speech) family
|
| 11 |
+
"facebook/mms-1b-all": 1_000_000_000, # 1B params
|
| 12 |
+
"facebook/mms-1b-fl102": 1_000_000_000, # 1B params
|
| 13 |
+
|
| 14 |
+
# Facebook Wav2Vec2 family
|
| 15 |
+
"facebook/wav2vec2-base-960h": 94_400_000, # 94.4M
|
| 16 |
+
"facebook/wav2vec2-large-960h": 315_000_000, # ~315M (large)
|
| 17 |
+
"facebook/wav2vec2-large-960h-lv60-self": 315_000_000, # ~315M (large)
|
| 18 |
+
"facebook/wav2vec2-large-robust-ft-libri-960h": 315_000_000, # ~315M
|
| 19 |
+
|
| 20 |
+
# Facebook Wav2Vec2 Conformer family
|
| 21 |
+
"facebook/wav2vec2-conformer-rel-pos-large-960h-ft": 600_000_000, # 600M
|
| 22 |
+
"facebook/wav2vec2-conformer-rope-large-960h-ft": 600_000_000, # 600M
|
| 23 |
+
|
| 24 |
+
# Facebook HuBERT family
|
| 25 |
+
"facebook/hubert-large-ls960-ft": 315_000_000, # ~315M (large)
|
| 26 |
+
"facebook/hubert-xlarge-ls960-ft": 1_000_000_000, # 1B params
|
| 27 |
+
|
| 28 |
+
# IBM Granite Speech family (verified from HF model cards)
|
| 29 |
+
# Note: Total params include speech encoder + LLM backbone
|
| 30 |
+
"ibm/granite-granite-speech-3.3-2b": 3_000_000_000, # 3B params
|
| 31 |
+
"ibm/granite-granite-speech-3.3-8b": 9_000_000_000, # 9B params
|
| 32 |
+
|
| 33 |
+
# Kyutai family
|
| 34 |
+
"kyutai/stt-2.6b-en": 2_600_000_000, # 2.6B params
|
| 35 |
+
|
| 36 |
+
# Lite ASR / EfficientSpeech family (Whisper-based compressions)
|
| 37 |
+
# https://huggingface.co/efficient-speech
|
| 38 |
+
"efficient/speech-lite-whisper-large-v3": 1_000_000_000, # ~1B (large-v3 compression)
|
| 39 |
+
"efficient/speech-lite-whisper-large-v3-acc": 1_000_000_000, # ~1B (accuracy-optimized)
|
| 40 |
+
"efficient/speech-lite-whisper-large-v3-fast": 1_000_000_000, # ~1B (speed-optimized)
|
| 41 |
+
"efficient/speech-lite-whisper-large-v3-turbo": 600_000_000, # ~0.6B (turbo compression)
|
| 42 |
+
"efficient/speech-lite-whisper-large-v3-turbo-acc": 600_000_000, # ~0.6B (turbo accuracy)
|
| 43 |
+
"efficient/speech-lite-whisper-large-v3-turbo-fast": 600_000_000, # ~0.6B (turbo fast)
|
| 44 |
+
|
| 45 |
+
# Moonshine family (Useful Sensors)
|
| 46 |
+
"usefulsensors/moonshine-tiny": 27_100_000, # 27.1M params
|
| 47 |
+
"usefulsensors/moonshine-base": 61_500_000, # 61.5M params
|
| 48 |
+
|
| 49 |
+
# OpenAI Whisper family
|
| 50 |
+
"openai/whisper-tiny.en": 37_800_000, # 37.8M
|
| 51 |
+
"openai/whisper-base.en": 72_600_000, # 72.6M
|
| 52 |
+
"openai/whisper-small.en": 244_000_000, # 244M
|
| 53 |
+
"openai/whisper-medium.en": 769_000_000, # 769M
|
| 54 |
+
"openai/whisper-large": 1_550_000_000, # 1550M
|
| 55 |
+
"openai/whisper-large-v2": 1_550_000_000, # 1550M
|
| 56 |
+
"openai/whisper-large-v3": 1_550_000_000, # 1550M
|
| 57 |
+
"openai/whisper-large-v3-turbo": 809_000_000, # 809M
|
| 58 |
+
|
| 59 |
+
# Distil-Whisper family
|
| 60 |
+
"distil/whisper-distil-large-v2": 756_000_000, # 756M
|
| 61 |
+
"distil/whisper-distil-large-v3": 756_000_000, # 756M
|
| 62 |
+
"distil/whisper-distil-medium.en": 394_000_000, # 394M
|
| 63 |
+
|
| 64 |
+
# Paza family (actual model names from results_summary.csv)
|
| 65 |
+
"paza/microsoft-paza-Phi-4-multimodal-instruct": 5_600_000_000, # 5.6B
|
| 66 |
+
"paza/microsoft-paza-mms-1b-all": 1_000_000_000, # 1B
|
| 67 |
+
"paza/microsoft-paza-whisper-large-v3-turbo": 809_000_000, # 809M
|
| 68 |
+
|
| 69 |
+
# Qwen2 Audio family
|
| 70 |
+
"Qwen/Qwen2-Audio-7B": 8_000_000_000, # 8B params
|
| 71 |
+
"Qwen/Qwen2-Audio-7B-Instruct": 8_000_000_000, # 8B params
|
| 72 |
+
|
| 73 |
+
# OmniASR family - CTC models
|
| 74 |
+
"facebook/omniASR-CTC-300M": 325_494_996, # 325M
|
| 75 |
+
"facebook/omniASR-CTC-1B": 975_065_300, # 975M
|
| 76 |
+
"facebook/omniASR-CTC-3B": 3_080_423_636, # 3.08B
|
| 77 |
+
"facebook/omniASR-CTC-7B": 6_504_786_132, # 6.5B
|
| 78 |
+
|
| 79 |
+
# OmniASR family - LLM models (encoder-decoder)
|
| 80 |
+
"facebook/omniASR-LLM-300M": 1_627_603_584, # 1.6B
|
| 81 |
+
"facebook/omniASR-LLM-1B": 2_275_710_592, # 2.3B
|
| 82 |
+
"facebook/omniASR-LLM-3B": 4_376_679_040, # 4.4B
|
| 83 |
+
"facebook/omniASR-LLM-7B": 7_801_041_536, # 7.8B
|
| 84 |
+
|
| 85 |
+
# OmniASR family - Zero-shot model
|
| 86 |
+
"facebook/omniASR-LLM-7B-ZS": 7_810_900_608, # 7.8B
|
| 87 |
+
|
| 88 |
+
# Microsoft Phi-4 family
|
| 89 |
+
"microsoft/Phi-4-multimodal-instruct": 5_600_000_000, # 5.6B params
|
| 90 |
+
|
| 91 |
+
# NVIDIA NeMo ASR family
|
| 92 |
+
"nvidia/canary-1b": 1_000_000_000, # ~1B (FastConformer encoder-decoder)
|
| 93 |
+
"nvidia/canary-1b-v2": 1_000_000_000, # ~1B params
|
| 94 |
+
"nvidia/canary-qwen-2.5b": 2_500_000_000, # 2.5B params
|
| 95 |
+
"nvidia/parakeet-tdt-0.6b-v2": 600_000_000, # 600M
|
| 96 |
+
"nvidia/parakeet-tdt-0.6b-v3": 600_000_000, # 600M params
|
| 97 |
+
|
| 98 |
+
# Seamless M4T family
|
| 99 |
+
"facebook/seamless-m4t-v2-large": 2_300_000_000, # 2.3B params
|
| 100 |
+
}
|
src/populate.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
from src.display.formatting import make_clickable_model
|
| 7 |
+
from src.display.utils import EvalQueueColumn
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def get_evaluation_queue_df(save_path: str, cols: list) -> list[pd.DataFrame]:
|
| 11 |
+
"""Creates the different dataframes for the evaluation queues requestes"""
|
| 12 |
+
if not os.path.exists(save_path):
|
| 13 |
+
return pd.DataFrame(columns=cols), pd.DataFrame(columns=cols), pd.DataFrame(columns=cols)
|
| 14 |
+
|
| 15 |
+
entries = [entry for entry in os.listdir(save_path) if not entry.startswith(".")]
|
| 16 |
+
all_evals = []
|
| 17 |
+
|
| 18 |
+
for entry in entries:
|
| 19 |
+
if ".json" in entry:
|
| 20 |
+
file_path = os.path.join(save_path, entry)
|
| 21 |
+
with open(file_path) as fp:
|
| 22 |
+
data = json.load(fp)
|
| 23 |
+
|
| 24 |
+
data[EvalQueueColumn.model.name] = make_clickable_model(data["model"])
|
| 25 |
+
data[EvalQueueColumn.revision.name] = data.get("revision", "main")
|
| 26 |
+
|
| 27 |
+
all_evals.append(data)
|
| 28 |
+
elif ".md" not in entry:
|
| 29 |
+
# this is a folder
|
| 30 |
+
sub_entries = [e for e in os.listdir(f"{save_path}/{entry}") if os.path.isfile(e) and not e.startswith(".")]
|
| 31 |
+
for sub_entry in sub_entries:
|
| 32 |
+
file_path = os.path.join(save_path, entry, sub_entry)
|
| 33 |
+
with open(file_path) as fp:
|
| 34 |
+
data = json.load(fp)
|
| 35 |
+
|
| 36 |
+
data[EvalQueueColumn.model.name] = make_clickable_model(data["model"])
|
| 37 |
+
data[EvalQueueColumn.revision.name] = data.get("revision", "main")
|
| 38 |
+
all_evals.append(data)
|
| 39 |
+
|
| 40 |
+
pending_list = [e for e in all_evals if e["status"] in ["PENDING", "RERUN"]]
|
| 41 |
+
running_list = [e for e in all_evals if e["status"] == "RUNNING"]
|
| 42 |
+
finished_list = [e for e in all_evals if e["status"].startswith("FINISHED") or e["status"] == "PENDING_NEW_EVAL"]
|
| 43 |
+
df_pending = pd.DataFrame.from_records(pending_list, columns=cols)
|
| 44 |
+
df_running = pd.DataFrame.from_records(running_list, columns=cols)
|
| 45 |
+
df_finished = pd.DataFrame.from_records(finished_list, columns=cols)
|
| 46 |
+
return df_finished[cols], df_running[cols], df_pending[cols]
|
src/submission/check_validity.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
|
| 5 |
+
import huggingface_hub
|
| 6 |
+
from huggingface_hub import ModelCard
|
| 7 |
+
from huggingface_hub.hf_api import ModelInfo
|
| 8 |
+
from transformers import AutoConfig
|
| 9 |
+
from transformers.models.auto.tokenization_auto import AutoTokenizer
|
| 10 |
+
|
| 11 |
+
def check_model_card(repo_id: str) -> tuple[bool, str]:
|
| 12 |
+
"""Checks if the model card and license exist and have been filled"""
|
| 13 |
+
try:
|
| 14 |
+
card = ModelCard.load(repo_id)
|
| 15 |
+
except huggingface_hub.utils.EntryNotFoundError:
|
| 16 |
+
return False, "Please add a model card to your model to explain how you trained/fine-tuned it."
|
| 17 |
+
|
| 18 |
+
# Enforce license metadata
|
| 19 |
+
if card.data.license is None:
|
| 20 |
+
if not ("license_name" in card.data and "license_link" in card.data):
|
| 21 |
+
return False, (
|
| 22 |
+
"License not found. Please add a license to your model card using the `license` metadata or a"
|
| 23 |
+
" `license_name`/`license_link` pair."
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Enforce card content
|
| 27 |
+
if len(card.text) < 200:
|
| 28 |
+
return False, "Please add a description to your model card, it is too short."
|
| 29 |
+
|
| 30 |
+
return True, ""
|
| 31 |
+
|
| 32 |
+
def is_model_on_hub(model_name: str, revision: str, token: str = None, trust_remote_code=False, test_tokenizer=False) -> tuple[bool, str]:
|
| 33 |
+
"""Checks if the model model_name is on the hub, and whether it (and its tokenizer) can be loaded with AutoClasses."""
|
| 34 |
+
try:
|
| 35 |
+
config = AutoConfig.from_pretrained(model_name, revision=revision, trust_remote_code=trust_remote_code, token=token)
|
| 36 |
+
if test_tokenizer:
|
| 37 |
+
try:
|
| 38 |
+
AutoTokenizer.from_pretrained(model_name, revision=revision, trust_remote_code=trust_remote_code, token=token)
|
| 39 |
+
except ValueError as e:
|
| 40 |
+
return (
|
| 41 |
+
False,
|
| 42 |
+
f"uses a tokenizer which is not in a transformers release: {e}",
|
| 43 |
+
None
|
| 44 |
+
)
|
| 45 |
+
except Exception:
|
| 46 |
+
return (False, "'s tokenizer cannot be loaded. Is your tokenizer class in a stable transformers release, and correctly configured?", None)
|
| 47 |
+
return True, None, config
|
| 48 |
+
|
| 49 |
+
except ValueError:
|
| 50 |
+
return (
|
| 51 |
+
False,
|
| 52 |
+
"needs to be launched with `trust_remote_code=True`. For safety reason, we do not allow these models to be automatically submitted to the leaderboard.",
|
| 53 |
+
None
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
except Exception:
|
| 57 |
+
return False, "was not found on hub!", None
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def get_model_size(model_info: ModelInfo, precision: str):
|
| 61 |
+
"""Gets the model size from the configuration, or the model name if the configuration does not contain the information."""
|
| 62 |
+
try:
|
| 63 |
+
model_size = round(model_info.safetensors["total"] / 1e9, 3)
|
| 64 |
+
except (AttributeError, TypeError):
|
| 65 |
+
return 0 # Unknown model sizes are indicated as 0, see NUMERIC_INTERVALS in app.py
|
| 66 |
+
|
| 67 |
+
size_factor = 8 if (precision == "GPTQ" or "gptq" in model_info.modelId.lower()) else 1
|
| 68 |
+
model_size = size_factor * model_size
|
| 69 |
+
return model_size
|
| 70 |
+
|
| 71 |
+
def get_model_arch(model_info: ModelInfo):
|
| 72 |
+
"""Gets the model architecture from the configuration"""
|
| 73 |
+
return model_info.config.get("architectures", "Unknown")
|
| 74 |
+
|
| 75 |
+
def already_submitted_models(requested_models_dir: str) -> set[str]:
|
| 76 |
+
"""Gather a list of already submitted models to avoid duplicates"""
|
| 77 |
+
depth = 1
|
| 78 |
+
file_names = []
|
| 79 |
+
users_to_submission_dates = defaultdict(list)
|
| 80 |
+
|
| 81 |
+
for root, _, files in os.walk(requested_models_dir):
|
| 82 |
+
current_depth = root.count(os.sep) - requested_models_dir.count(os.sep)
|
| 83 |
+
if current_depth == depth:
|
| 84 |
+
for file in files:
|
| 85 |
+
if not file.endswith(".json"):
|
| 86 |
+
continue
|
| 87 |
+
with open(os.path.join(root, file), "r") as f:
|
| 88 |
+
info = json.load(f)
|
| 89 |
+
file_names.append(f"{info['model']}_{info['revision']}_{info['precision']}")
|
| 90 |
+
|
| 91 |
+
# Select organisation
|
| 92 |
+
if info["model"].count("/") == 0 or "submitted_time" not in info:
|
| 93 |
+
continue
|
| 94 |
+
organisation, _ = info["model"].split("/")
|
| 95 |
+
users_to_submission_dates[organisation].append(info["submitted_time"])
|
| 96 |
+
|
| 97 |
+
return set(file_names), users_to_submission_dates
|
src/submission/submit.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
from datetime import datetime, timezone
|
| 5 |
+
from urllib.parse import urlparse
|
| 6 |
+
|
| 7 |
+
from huggingface_hub import dataset_info
|
| 8 |
+
from huggingface_hub.utils import HfHubHTTPError, RepositoryNotFoundError
|
| 9 |
+
|
| 10 |
+
from src.display.formatting import styled_error, styled_message, styled_warning
|
| 11 |
+
from src.envs import API, EVAL_REQUESTS_PATH, TOKEN, QUEUE_REPO
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def is_dataset_on_hub(dataset_name: str, token: str = None) -> tuple[bool, str | None]:
|
| 15 |
+
"""Check if a dataset exists on the Hugging Face Hub and is publicly available."""
|
| 16 |
+
try:
|
| 17 |
+
info = dataset_info(dataset_name, token=token)
|
| 18 |
+
if info.private:
|
| 19 |
+
return False, "is private. Please make the dataset publicly available on Hugging Face Hub."
|
| 20 |
+
return True, None
|
| 21 |
+
except RepositoryNotFoundError:
|
| 22 |
+
return False, "was not found on the Hub!"
|
| 23 |
+
except HfHubHTTPError as e:
|
| 24 |
+
return False, f"could not be accessed: {str(e)}"
|
| 25 |
+
except Exception as e:
|
| 26 |
+
return False, f"error checking dataset: {str(e)}"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def is_valid_url(url: str) -> tuple[bool, str | None]:
|
| 30 |
+
"""Validate URL using urllib.parse with strict scheme enforcement."""
|
| 31 |
+
if not url or not url.strip():
|
| 32 |
+
return False, "URL cannot be empty."
|
| 33 |
+
|
| 34 |
+
url = url.strip()
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
parsed = urlparse(url)
|
| 38 |
+
|
| 39 |
+
# Strict scheme validation - only http/https allowed
|
| 40 |
+
if parsed.scheme not in ('http', 'https'):
|
| 41 |
+
return False, "URL must start with http:// or https://"
|
| 42 |
+
|
| 43 |
+
# Must have a valid network location (domain)
|
| 44 |
+
if not parsed.netloc:
|
| 45 |
+
return False, "Invalid URL domain. Please provide a complete URL."
|
| 46 |
+
|
| 47 |
+
# Extract hostname (remove port if present)
|
| 48 |
+
hostname = parsed.hostname
|
| 49 |
+
if not hostname or '.' not in hostname:
|
| 50 |
+
return False, "Invalid URL domain. Please provide a complete URL."
|
| 51 |
+
|
| 52 |
+
# Validate hostname format (alphanumeric, dots, hyphens only)
|
| 53 |
+
# This blocks javascript:, data:, vbscript: and other injection schemes
|
| 54 |
+
hostname_parts = hostname.split('.')
|
| 55 |
+
for part in hostname_parts:
|
| 56 |
+
if not part or not all(c.isalnum() or c == '-' for c in part):
|
| 57 |
+
return False, "Invalid domain name in URL."
|
| 58 |
+
if part.startswith('-') or part.endswith('-'):
|
| 59 |
+
return False, "Invalid domain name in URL."
|
| 60 |
+
|
| 61 |
+
return True, None
|
| 62 |
+
except Exception:
|
| 63 |
+
return False, "Invalid URL format."
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def add_language_eval_request(
|
| 67 |
+
location: str,
|
| 68 |
+
dataset_name: str,
|
| 69 |
+
dataset_url: str,
|
| 70 |
+
dataset_config: str,
|
| 71 |
+
dataset_split: str,
|
| 72 |
+
audio_column: str,
|
| 73 |
+
text_column: str,
|
| 74 |
+
license: str,
|
| 75 |
+
):
|
| 76 |
+
"""Submit a request to evaluate a new language/dataset on all models."""
|
| 77 |
+
# Validate required fields based on location
|
| 78 |
+
if not license or not license.strip():
|
| 79 |
+
return styled_error("Please provide a license for the dataset.")
|
| 80 |
+
|
| 81 |
+
if location == "HuggingFace":
|
| 82 |
+
# Validate HuggingFace dataset
|
| 83 |
+
if not dataset_name or not dataset_name.strip():
|
| 84 |
+
return styled_error("Please provide a dataset name.")
|
| 85 |
+
|
| 86 |
+
dataset_name = dataset_name.strip()
|
| 87 |
+
if "/" not in dataset_name:
|
| 88 |
+
return styled_error("Dataset name must be in the format 'owner/dataset-name' (e.g., 'mozilla-foundation/common_voice_13_0').")
|
| 89 |
+
|
| 90 |
+
# Check if dataset exists on Hub
|
| 91 |
+
dataset_on_hub, error = is_dataset_on_hub(dataset_name, token=TOKEN)
|
| 92 |
+
if not dataset_on_hub:
|
| 93 |
+
return styled_error(f'Dataset "{dataset_name}" {error}')
|
| 94 |
+
|
| 95 |
+
dataset_identifier = dataset_name
|
| 96 |
+
dataset_source = "huggingface"
|
| 97 |
+
else:
|
| 98 |
+
# Validate external URL
|
| 99 |
+
if not dataset_url or not dataset_url.strip():
|
| 100 |
+
return styled_error("Please provide a dataset URL.")
|
| 101 |
+
|
| 102 |
+
valid_url, error = is_valid_url(dataset_url)
|
| 103 |
+
if not valid_url:
|
| 104 |
+
return styled_error(error)
|
| 105 |
+
|
| 106 |
+
dataset_url = dataset_url.strip()
|
| 107 |
+
dataset_identifier = dataset_url
|
| 108 |
+
dataset_source = "external"
|
| 109 |
+
|
| 110 |
+
current_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 111 |
+
|
| 112 |
+
# Use defaults for optional fields
|
| 113 |
+
config = dataset_config.strip() if dataset_config and dataset_config.strip() else "default"
|
| 114 |
+
split = dataset_split.strip() if dataset_split and dataset_split.strip() else "test"
|
| 115 |
+
audio_col = audio_column.strip() if audio_column and audio_column.strip() else "audio"
|
| 116 |
+
text_col = text_column.strip() if text_column and text_column.strip() else "text"
|
| 117 |
+
|
| 118 |
+
# Create safe identifier for filename
|
| 119 |
+
if dataset_source == "huggingface":
|
| 120 |
+
safe_dataset = dataset_identifier.replace("/", "_").replace(" ", "_")
|
| 121 |
+
else:
|
| 122 |
+
# For external URLs, create a safe identifier from the URL
|
| 123 |
+
safe_dataset = re.sub(r'[^a-zA-Z0-9]', '_', dataset_identifier)[:100]
|
| 124 |
+
safe_config = config.replace(" ", "_").lower()
|
| 125 |
+
|
| 126 |
+
OUT_DIR = f"{EVAL_REQUESTS_PATH}/language_requests"
|
| 127 |
+
os.makedirs(OUT_DIR, exist_ok=True)
|
| 128 |
+
|
| 129 |
+
# Check if similar request already exists
|
| 130 |
+
existing_files = os.listdir(OUT_DIR) if os.path.exists(OUT_DIR) else []
|
| 131 |
+
for existing_file in existing_files:
|
| 132 |
+
if existing_file.startswith(f"lang_eval_{safe_dataset}_{safe_config}_"):
|
| 133 |
+
return styled_warning("A similar evaluation request for this dataset configuration already exists.")
|
| 134 |
+
|
| 135 |
+
# Create language evaluation request entry
|
| 136 |
+
eval_entry = {
|
| 137 |
+
"type": "language_evaluation",
|
| 138 |
+
"source": dataset_source,
|
| 139 |
+
"dataset": dataset_identifier,
|
| 140 |
+
"config": config,
|
| 141 |
+
"split": split,
|
| 142 |
+
"audio_column": audio_col,
|
| 143 |
+
"text_column": text_col,
|
| 144 |
+
"license": license.strip() if license else "",
|
| 145 |
+
"status": "PENDING",
|
| 146 |
+
"submitted_time": current_time,
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
# Create unique filename
|
| 150 |
+
filename = f"lang_eval_{safe_dataset}_{safe_config}_{current_time.replace(':', '-')}.json"
|
| 151 |
+
|
| 152 |
+
print(f"Creating language eval request: {filename}")
|
| 153 |
+
out_path = f"{OUT_DIR}/{filename}"
|
| 154 |
+
|
| 155 |
+
with open(out_path, "w") as f:
|
| 156 |
+
f.write(json.dumps(eval_entry, indent=2))
|
| 157 |
+
|
| 158 |
+
# Upload to Hub if API is available
|
| 159 |
+
if API:
|
| 160 |
+
try:
|
| 161 |
+
print("Uploading language eval request")
|
| 162 |
+
API.upload_file(
|
| 163 |
+
path_or_fileobj=out_path,
|
| 164 |
+
path_in_repo=f"language_requests/{filename}",
|
| 165 |
+
repo_id=QUEUE_REPO,
|
| 166 |
+
repo_type="dataset",
|
| 167 |
+
commit_message=f"Add language evaluation request for {dataset_identifier} ({config})",
|
| 168 |
+
)
|
| 169 |
+
os.remove(out_path)
|
| 170 |
+
except Exception as e:
|
| 171 |
+
print(f"Could not upload to Hub: {e}")
|
| 172 |
+
# Keep local file if upload fails
|
| 173 |
+
|
| 174 |
+
source_label = "Hugging Face" if dataset_source == "huggingface" else "External URL"
|
| 175 |
+
return styled_message(
|
| 176 |
+
f"✅ Your language evaluation request has been submitted!\n\n"
|
| 177 |
+
f"**Source:** {source_label}\n"
|
| 178 |
+
f"**Dataset:** {dataset_identifier}\n"
|
| 179 |
+
f"**Config:** {config}\n\n"
|
| 180 |
+
f"We will review your request and run evaluations on all supported models."
|
| 181 |
+
)
|
src/telemetry.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
import uuid
|
| 4 |
+
import threading
|
| 5 |
+
from contextlib import contextmanager
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from functools import wraps
|
| 8 |
+
from typing import Any, Callable
|
| 9 |
+
|
| 10 |
+
# Telemetry configuration from environment variables
|
| 11 |
+
TELEMETRY_ENABLED = os.environ.get("TELEMETRY_ENABLED", "false").lower() == "true"
|
| 12 |
+
APPLICATIONINSIGHTS_CONNECTION_STRING = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING", "")
|
| 13 |
+
|
| 14 |
+
# Debug mode - prints telemetry events to console/docker logs
|
| 15 |
+
TELEMETRY_DEBUG = os.environ.get("TELEMETRY_DEBUG", "false").lower() == "true"
|
| 16 |
+
|
| 17 |
+
# Cloud role name for Application Insights (required when telemetry is enabled)
|
| 18 |
+
CLOUD_ROLE_NAME = os.environ.get("OTEL_SERVICE_NAME")
|
| 19 |
+
|
| 20 |
+
# Module-level state
|
| 21 |
+
_telemetry_initialized = False
|
| 22 |
+
_tracer = None
|
| 23 |
+
_meter = None
|
| 24 |
+
_logger = None
|
| 25 |
+
|
| 26 |
+
# Metric instruments (initialized lazily)
|
| 27 |
+
_counters: dict[str, Any] = {}
|
| 28 |
+
_histograms: dict[str, Any] = {}
|
| 29 |
+
|
| 30 |
+
# =============================================================================
|
| 31 |
+
# Session Context Management
|
| 32 |
+
# =============================================================================
|
| 33 |
+
|
| 34 |
+
# Thread-local storage for session context
|
| 35 |
+
_session_context = threading.local()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _generate_event_id() -> str:
|
| 39 |
+
|
| 40 |
+
return str(uuid.uuid4())[:8]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def set_session_context(session_hash: str | None) -> str:
|
| 44 |
+
|
| 45 |
+
if session_hash:
|
| 46 |
+
_session_context.session_id = session_hash[:12] # Truncate for readability
|
| 47 |
+
else:
|
| 48 |
+
# Generate a temporary session ID if none provided
|
| 49 |
+
if not hasattr(_session_context, 'session_id') or not _session_context.session_id:
|
| 50 |
+
_session_context.session_id = f"anon_{_generate_event_id()}"
|
| 51 |
+
return _session_context.session_id
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_session_id() -> str:
|
| 55 |
+
|
| 56 |
+
if not hasattr(_session_context, 'session_id') or not _session_context.session_id:
|
| 57 |
+
return set_session_context(None)
|
| 58 |
+
return _session_context.session_id
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def clear_session_context() -> None:
|
| 62 |
+
|
| 63 |
+
if hasattr(_session_context, 'session_id'):
|
| 64 |
+
_session_context.session_id = None
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _debug_log(message: str) -> None:
|
| 68 |
+
|
| 69 |
+
if TELEMETRY_DEBUG:
|
| 70 |
+
print(f"[TELEMETRY] {message}", flush=True)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _initialize_telemetry() -> bool:
|
| 74 |
+
|
| 75 |
+
global _telemetry_initialized, _tracer, _meter, _logger
|
| 76 |
+
|
| 77 |
+
if _telemetry_initialized:
|
| 78 |
+
return True
|
| 79 |
+
|
| 80 |
+
if not TELEMETRY_ENABLED:
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
if not APPLICATIONINSIGHTS_CONNECTION_STRING:
|
| 84 |
+
logging.warning("APPLICATIONINSIGHTS_CONNECTION_STRING not set. Telemetry disabled.")
|
| 85 |
+
return False
|
| 86 |
+
|
| 87 |
+
if not CLOUD_ROLE_NAME:
|
| 88 |
+
logging.warning("OTEL_SERVICE_NAME not set. Telemetry disabled.")
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
from azure.monitor.opentelemetry import configure_azure_monitor
|
| 93 |
+
from opentelemetry import trace, metrics
|
| 94 |
+
|
| 95 |
+
# Set cloud role name via environment variable (OpenTelemetry standard)
|
| 96 |
+
os.environ.setdefault("OTEL_SERVICE_NAME", CLOUD_ROLE_NAME)
|
| 97 |
+
|
| 98 |
+
# Configure Azure Monitor OpenTelemetry
|
| 99 |
+
configure_azure_monitor(
|
| 100 |
+
connection_string=APPLICATIONINSIGHTS_CONNECTION_STRING,
|
| 101 |
+
logger_name="pazabench",
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Get tracer and meter
|
| 105 |
+
_tracer = trace.get_tracer("pazabench.telemetry")
|
| 106 |
+
_meter = metrics.get_meter("pazabench.telemetry")
|
| 107 |
+
|
| 108 |
+
# Set up logger for custom events
|
| 109 |
+
_logger = logging.getLogger("pazabench")
|
| 110 |
+
_logger.setLevel(logging.INFO)
|
| 111 |
+
|
| 112 |
+
_telemetry_initialized = True
|
| 113 |
+
logging.info(f"Telemetry initialized. Cloud Role Name: {CLOUD_ROLE_NAME}")
|
| 114 |
+
return True
|
| 115 |
+
|
| 116 |
+
except ImportError as e:
|
| 117 |
+
logging.warning(f"Azure Monitor OpenTelemetry packages not installed: {e}")
|
| 118 |
+
return False
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logging.warning(f"Failed to initialize telemetry: {e}")
|
| 121 |
+
return False
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _ensure_initialized(func: Callable) -> Callable:
|
| 125 |
+
|
| 126 |
+
@wraps(func)
|
| 127 |
+
def wrapper(*args, **kwargs):
|
| 128 |
+
if not _telemetry_initialized:
|
| 129 |
+
_initialize_telemetry()
|
| 130 |
+
if not _telemetry_initialized:
|
| 131 |
+
return None
|
| 132 |
+
return func(*args, **kwargs)
|
| 133 |
+
return wrapper
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# =============================================================================
|
| 137 |
+
# Custom Events - Track user interactions and behavior
|
| 138 |
+
# =============================================================================
|
| 139 |
+
|
| 140 |
+
@_ensure_initialized
|
| 141 |
+
def track_event(
|
| 142 |
+
name: str,
|
| 143 |
+
properties: dict[str, Any] | None = None,
|
| 144 |
+
parent_event_id: str | None = None
|
| 145 |
+
) -> str | None:
|
| 146 |
+
|
| 147 |
+
# Generate unique event ID for this event
|
| 148 |
+
event_id = _generate_event_id()
|
| 149 |
+
session_id = get_session_id()
|
| 150 |
+
|
| 151 |
+
# Build properties with automatic session and event tracking
|
| 152 |
+
all_properties: dict[str, Any] = {
|
| 153 |
+
"event_id": event_id,
|
| 154 |
+
"session_id": session_id,
|
| 155 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 156 |
+
}
|
| 157 |
+
if parent_event_id:
|
| 158 |
+
all_properties["parent_event_id"] = parent_event_id
|
| 159 |
+
if properties:
|
| 160 |
+
all_properties.update(properties)
|
| 161 |
+
|
| 162 |
+
# Debug logging to console/docker logs
|
| 163 |
+
props_str = ", ".join(f"{k}={v}" for k, v in all_properties.items())
|
| 164 |
+
_debug_log(f"EVENT: {name} | {props_str}")
|
| 165 |
+
|
| 166 |
+
if not _logger:
|
| 167 |
+
return event_id
|
| 168 |
+
|
| 169 |
+
extra = {"microsoft.custom_event.name": name}
|
| 170 |
+
extra.update(all_properties)
|
| 171 |
+
|
| 172 |
+
_logger.info(f"Event: {name}", extra=extra)
|
| 173 |
+
return event_id
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
@_ensure_initialized
|
| 177 |
+
def track_page_view(page_name: str, properties: dict[str, Any] | None = None) -> None:
|
| 178 |
+
|
| 179 |
+
event_properties = {"page_name": page_name}
|
| 180 |
+
if properties:
|
| 181 |
+
event_properties.update(properties)
|
| 182 |
+
track_event("page_view", event_properties)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@_ensure_initialized
|
| 186 |
+
def track_filter_change(
|
| 187 |
+
filter_type: str,
|
| 188 |
+
values: list[str] | None,
|
| 189 |
+
tab: str | None = None
|
| 190 |
+
) -> None:
|
| 191 |
+
|
| 192 |
+
properties = {
|
| 193 |
+
"filter_type": filter_type,
|
| 194 |
+
"values_count": len(values) if values else 0,
|
| 195 |
+
"values": ",".join(values[:10]) if values else "", # Limit to first 10 for size
|
| 196 |
+
}
|
| 197 |
+
if tab:
|
| 198 |
+
properties["tab"] = tab
|
| 199 |
+
track_event("filter_change", properties)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
@_ensure_initialized
|
| 203 |
+
def track_language_interest(
|
| 204 |
+
language: str,
|
| 205 |
+
context: str,
|
| 206 |
+
properties: dict[str, Any] | None = None
|
| 207 |
+
) -> None:
|
| 208 |
+
|
| 209 |
+
event_properties = {"language": language, "context": context}
|
| 210 |
+
if properties:
|
| 211 |
+
event_properties.update(properties)
|
| 212 |
+
track_event("language_interest", event_properties)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@_ensure_initialized
|
| 216 |
+
def track_model_interest(
|
| 217 |
+
model: str,
|
| 218 |
+
context: str,
|
| 219 |
+
properties: dict[str, Any] | None = None
|
| 220 |
+
) -> None:
|
| 221 |
+
|
| 222 |
+
event_properties = {"model": model, "context": context}
|
| 223 |
+
if properties:
|
| 224 |
+
event_properties.update(properties)
|
| 225 |
+
track_event("model_interest", event_properties)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@_ensure_initialized
|
| 229 |
+
def track_visualization_view(
|
| 230 |
+
chart_name: str,
|
| 231 |
+
properties: dict[str, Any] | None = None,
|
| 232 |
+
languages: list[str] | None = None,
|
| 233 |
+
models: list[str] | None = None
|
| 234 |
+
) -> str | None:
|
| 235 |
+
|
| 236 |
+
event_properties: dict[str, Any] = {"chart_name": chart_name}
|
| 237 |
+
|
| 238 |
+
# Include languages in the main event (no separate events needed)
|
| 239 |
+
if languages:
|
| 240 |
+
event_properties["languages"] = ",".join(languages[:10]) # Limit to 10
|
| 241 |
+
event_properties["languages_count"] = str(len(languages))
|
| 242 |
+
|
| 243 |
+
# Include models if provided
|
| 244 |
+
if models:
|
| 245 |
+
event_properties["models"] = ",".join(models[:10]) # Limit to 10
|
| 246 |
+
event_properties["models_count"] = str(len(models))
|
| 247 |
+
|
| 248 |
+
if properties:
|
| 249 |
+
event_properties.update(properties)
|
| 250 |
+
|
| 251 |
+
return track_event("visualization_view", event_properties)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
@_ensure_initialized
|
| 255 |
+
def track_submission_attempt(
|
| 256 |
+
dataset_name: str,
|
| 257 |
+
success: bool,
|
| 258 |
+
error_message: str | None = None
|
| 259 |
+
) -> None:
|
| 260 |
+
|
| 261 |
+
properties = {
|
| 262 |
+
"dataset_name": dataset_name,
|
| 263 |
+
"success": str(success),
|
| 264 |
+
}
|
| 265 |
+
if error_message:
|
| 266 |
+
properties["error_message"] = error_message[:500] # Limit size
|
| 267 |
+
track_event("submission_attempt", properties)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
@_ensure_initialized
|
| 271 |
+
def track_accordion_toggle(accordion_name: str, is_open: bool) -> None:
|
| 272 |
+
|
| 273 |
+
track_event("accordion_toggle", {
|
| 274 |
+
"accordion_name": accordion_name,
|
| 275 |
+
"action": "open" if is_open else "close"
|
| 276 |
+
})
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
@_ensure_initialized
|
| 280 |
+
def track_metric_tab_view(metric: str) -> None:
|
| 281 |
+
|
| 282 |
+
track_event("metric_tab_view", {"metric": metric})
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
@_ensure_initialized
|
| 286 |
+
def track_view_mode_change(view_mode: str) -> None:
|
| 287 |
+
|
| 288 |
+
track_event("view_mode_change", {"view_mode": view_mode})
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
@_ensure_initialized
|
| 292 |
+
def track_region_filter(regions: list[str] | None) -> None:
|
| 293 |
+
|
| 294 |
+
track_filter_change("african_region", regions, tab="asr_metrics")
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
# =============================================================================
|
| 298 |
+
# Metrics - Track numeric measurements
|
| 299 |
+
# =============================================================================
|
| 300 |
+
|
| 301 |
+
def _get_counter(name: str, description: str = "") -> Any:
|
| 302 |
+
|
| 303 |
+
if not _meter:
|
| 304 |
+
return None
|
| 305 |
+
if name not in _counters:
|
| 306 |
+
_counters[name] = _meter.create_counter(
|
| 307 |
+
name,
|
| 308 |
+
description=description,
|
| 309 |
+
unit="1"
|
| 310 |
+
)
|
| 311 |
+
return _counters[name]
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def _get_histogram(name: str, description: str = "", unit: str = "ms") -> Any:
|
| 315 |
+
|
| 316 |
+
if not _meter:
|
| 317 |
+
return None
|
| 318 |
+
if name not in _histograms:
|
| 319 |
+
_histograms[name] = _meter.create_histogram(
|
| 320 |
+
name,
|
| 321 |
+
description=description,
|
| 322 |
+
unit=unit
|
| 323 |
+
)
|
| 324 |
+
return _histograms[name]
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
@_ensure_initialized
|
| 328 |
+
def track_metric(
|
| 329 |
+
name: str,
|
| 330 |
+
value: float,
|
| 331 |
+
attributes: dict[str, str] | None = None,
|
| 332 |
+
metric_type: str = "histogram"
|
| 333 |
+
) -> None:
|
| 334 |
+
|
| 335 |
+
attrs = attributes or {}
|
| 336 |
+
|
| 337 |
+
if metric_type == "counter":
|
| 338 |
+
counter = _get_counter(name, f"Counter for {name}")
|
| 339 |
+
if counter:
|
| 340 |
+
counter.add(int(value), attrs)
|
| 341 |
+
else:
|
| 342 |
+
histogram = _get_histogram(name, f"Histogram for {name}")
|
| 343 |
+
if histogram:
|
| 344 |
+
histogram.record(value, attrs)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@_ensure_initialized
|
| 348 |
+
def increment_counter(name: str, attributes: dict[str, str] | None = None) -> None:
|
| 349 |
+
|
| 350 |
+
track_metric(name, 1, attributes, metric_type="counter")
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
# =============================================================================
|
| 354 |
+
# Error Tracking - Track exceptions and errors
|
| 355 |
+
# =============================================================================
|
| 356 |
+
|
| 357 |
+
@_ensure_initialized
|
| 358 |
+
def track_error(
|
| 359 |
+
exception: Exception | None = None,
|
| 360 |
+
properties: dict[str, Any] | None = None,
|
| 361 |
+
context: str | None = None
|
| 362 |
+
) -> None:
|
| 363 |
+
|
| 364 |
+
# Debug logging
|
| 365 |
+
exc_info = f"{type(exception).__name__}: {exception}" if exception else "No exception"
|
| 366 |
+
_debug_log(f"ERROR: context={context} | {exc_info}")
|
| 367 |
+
|
| 368 |
+
if not _tracer:
|
| 369 |
+
return
|
| 370 |
+
|
| 371 |
+
from opentelemetry.trace import SpanKind
|
| 372 |
+
|
| 373 |
+
error_properties = properties or {}
|
| 374 |
+
if context:
|
| 375 |
+
error_properties["context"] = context
|
| 376 |
+
|
| 377 |
+
with _tracer.start_as_current_span(
|
| 378 |
+
f"error_{context or 'unknown'}",
|
| 379 |
+
kind=SpanKind.INTERNAL
|
| 380 |
+
) as span:
|
| 381 |
+
if exception:
|
| 382 |
+
span.record_exception(exception)
|
| 383 |
+
error_properties["exception_type"] = type(exception).__name__
|
| 384 |
+
error_properties["exception_message"] = str(exception)[:500]
|
| 385 |
+
|
| 386 |
+
for key, value in error_properties.items():
|
| 387 |
+
span.set_attribute(f"error.{key}", str(value))
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
# =============================================================================
|
| 391 |
+
# Tracing - Track operations with spans
|
| 392 |
+
# =============================================================================
|
| 393 |
+
|
| 394 |
+
@contextmanager
|
| 395 |
+
def start_span(name: str, attributes: dict[str, str] | None = None):
|
| 396 |
+
|
| 397 |
+
if not _tracer or not _telemetry_initialized:
|
| 398 |
+
yield None
|
| 399 |
+
return
|
| 400 |
+
|
| 401 |
+
with _tracer.start_as_current_span(name) as span:
|
| 402 |
+
if attributes:
|
| 403 |
+
for key, value in attributes.items():
|
| 404 |
+
span.set_attribute(key, str(value))
|
| 405 |
+
yield span
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# =============================================================================
|
| 409 |
+
# Utility Functions
|
| 410 |
+
# =============================================================================
|
| 411 |
+
|
| 412 |
+
def is_telemetry_enabled() -> bool:
|
| 413 |
+
|
| 414 |
+
return _telemetry_initialized
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def get_telemetry_status() -> dict[str, Any]:
|
| 418 |
+
|
| 419 |
+
return {
|
| 420 |
+
"enabled": TELEMETRY_ENABLED,
|
| 421 |
+
"initialized": _telemetry_initialized,
|
| 422 |
+
"connection_string_set": bool(APPLICATIONINSIGHTS_CONNECTION_STRING),
|
| 423 |
+
"cloud_role_name": CLOUD_ROLE_NAME,
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
# =============================================================================
|
| 428 |
+
# Convenience wrapper for tracking in Gradio callbacks
|
| 429 |
+
# =============================================================================
|
| 430 |
+
|
| 431 |
+
def with_telemetry(event_name: str, **event_props):
|
| 432 |
+
|
| 433 |
+
def decorator(func: Callable) -> Callable:
|
| 434 |
+
@wraps(func)
|
| 435 |
+
def wrapper(*args, **kwargs):
|
| 436 |
+
track_event(event_name, event_props)
|
| 437 |
+
return func(*args, **kwargs)
|
| 438 |
+
return wrapper
|
| 439 |
+
return decorator
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def init_session_from_request(request) -> str:
|
| 443 |
+
|
| 444 |
+
session_hash = None
|
| 445 |
+
if request:
|
| 446 |
+
# Try to get session_hash from gr.Request object
|
| 447 |
+
session_hash = getattr(request, 'session_hash', None)
|
| 448 |
+
if not session_hash:
|
| 449 |
+
# Fallback: try to get from request headers or query params
|
| 450 |
+
session_hash = getattr(request, 'username', None)
|
| 451 |
+
return set_session_context(session_hash)
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def track_page_load(page_name: str, request=None) -> str | None:
|
| 455 |
+
|
| 456 |
+
if request:
|
| 457 |
+
init_session_from_request(request)
|
| 458 |
+
return track_page_view(page_name)
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
# Initialize telemetry on module load if enabled
|
| 462 |
+
if TELEMETRY_ENABLED:
|
| 463 |
+
_initialize_telemetry()
|
src/visualizations.py
ADDED
|
@@ -0,0 +1,1193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PazaBench Visualization Functions for Gradio Integration
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import plotly.graph_objects as go
|
| 9 |
+
|
| 10 |
+
from src.constants import (
|
| 11 |
+
AFRICAN_COUNTRIES,
|
| 12 |
+
COUNTRY_NAMES,
|
| 13 |
+
LANGUAGE_COUNTRY_MAP
|
| 14 |
+
)
|
| 15 |
+
from src.model_counts import MODEL_PARAMETER_COUNTS
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Load model family colors from CSV
|
| 19 |
+
def _load_model_family_colors() -> tuple[dict[str, str], dict[str, str]]:
|
| 20 |
+
"""
|
| 21 |
+
Load color mappings from the model_family_colors.csv file.
|
| 22 |
+
Returns:
|
| 23 |
+
- model_family_colors: dict mapping model_family -> color (first color for that family)
|
| 24 |
+
- model_id_colors: dict mapping model_id -> color
|
| 25 |
+
"""
|
| 26 |
+
csv_path = os.path.join(os.path.dirname(__file__), 'display', 'model_family_colors.csv')
|
| 27 |
+
model_family_colors = {}
|
| 28 |
+
model_id_colors = {}
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
df = pd.read_csv(csv_path)
|
| 32 |
+
for _, row in df.iterrows():
|
| 33 |
+
family = row['Model Families']
|
| 34 |
+
model_id = row['Model ID']
|
| 35 |
+
color = row['Color']
|
| 36 |
+
|
| 37 |
+
# Store first color encountered for each family (using normalized key)
|
| 38 |
+
normalized_family = _normalize_family_name(family)
|
| 39 |
+
if normalized_family not in model_family_colors:
|
| 40 |
+
model_family_colors[normalized_family] = color
|
| 41 |
+
|
| 42 |
+
# Store color for each model ID
|
| 43 |
+
model_id_colors[model_id] = color
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"Warning: Could not load model family colors: {e}")
|
| 46 |
+
|
| 47 |
+
return model_family_colors, model_id_colors
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _normalize_family_name(family: str) -> str:
|
| 51 |
+
"""Normalize family name for consistent lookup (lowercase, remove underscores/dashes)."""
|
| 52 |
+
return family.lower().replace('_', '').replace('-', '').replace(' ', '')
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
MODEL_FAMILY_COLORS, MODEL_ID_COLORS = _load_model_family_colors()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _get_color_for_family(family: str) -> str:
|
| 59 |
+
"""Get color for a model family, with fallback."""
|
| 60 |
+
normalized = _normalize_family_name(family)
|
| 61 |
+
return MODEL_FAMILY_COLORS.get(normalized, '#888888')
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _get_color_for_model(model_id: str) -> str:
|
| 65 |
+
"""Get color for a specific model ID, with fallback to family color."""
|
| 66 |
+
if model_id in MODEL_ID_COLORS:
|
| 67 |
+
return MODEL_ID_COLORS[model_id]
|
| 68 |
+
# Try to find family and use family color
|
| 69 |
+
for family, color in MODEL_FAMILY_COLORS.items():
|
| 70 |
+
if family.lower() in model_id.lower():
|
| 71 |
+
return color
|
| 72 |
+
return '#888888'
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _remove_wer_outliers(df: pd.DataFrame, multiplier: float = 1.5) -> pd.DataFrame:
|
| 76 |
+
"""
|
| 77 |
+
Remove WER outliers using IQR method for cleaner visualizations.
|
| 78 |
+
Only removes HIGH outliers (poor performers), keeps LOW outliers (best performers).
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
df: DataFrame with 'wer' column
|
| 82 |
+
multiplier: IQR multiplier (default 1.5 for standard outlier detection)
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
DataFrame with high outliers removed
|
| 86 |
+
"""
|
| 87 |
+
if df.empty or 'wer' not in df.columns:
|
| 88 |
+
return df
|
| 89 |
+
Q1 = df['wer'].quantile(0.25)
|
| 90 |
+
Q3 = df['wer'].quantile(0.75)
|
| 91 |
+
IQR = Q3 - Q1
|
| 92 |
+
# Only remove HIGH outliers (poor performers), keep LOW outliers (best performers)
|
| 93 |
+
# Lower WER is better, so we don't want to remove low-WER entries
|
| 94 |
+
upper_bound = Q3 + multiplier * IQR
|
| 95 |
+
return df[df['wer'] <= upper_bound]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _get_language_sample_counts(asr_df: pd.DataFrame | None = None) -> dict[str, int]:
|
| 99 |
+
"""Derive per-language sample counts from ASR results; returns {} when data is unavailable."""
|
| 100 |
+
if asr_df is None or asr_df.empty:
|
| 101 |
+
print("No ASR data available, using default language sample counts.")
|
| 102 |
+
return {}
|
| 103 |
+
if 'language' not in asr_df.columns or 'num_samples' not in asr_df.columns:
|
| 104 |
+
print("Required columns not found in ASR data.")
|
| 105 |
+
return {}
|
| 106 |
+
|
| 107 |
+
dedupe_keys = [column for column in ['language', 'dataset_group', 'split'] if column in asr_df.columns]
|
| 108 |
+
if not dedupe_keys:
|
| 109 |
+
dedupe_keys = ['language']
|
| 110 |
+
|
| 111 |
+
sample_frame = asr_df[[column for column in set(dedupe_keys + ['num_samples'])]].copy()
|
| 112 |
+
sample_frame['num_samples'] = pd.to_numeric(sample_frame['num_samples'], errors='coerce').fillna(0)
|
| 113 |
+
sample_frame = sample_frame.dropna(subset=['language'])
|
| 114 |
+
sample_frame = sample_frame[sample_frame['language'] != 'Unknown']
|
| 115 |
+
|
| 116 |
+
if sample_frame.empty:
|
| 117 |
+
print("No valid language data found, using default language sample counts.")
|
| 118 |
+
return {}
|
| 119 |
+
|
| 120 |
+
# Dedupe across repeated model rows by taking the max split count before summing by language.
|
| 121 |
+
unique_counts = sample_frame.groupby(dedupe_keys, dropna=False)['num_samples'].max().reset_index()
|
| 122 |
+
by_language = unique_counts.groupby('language')['num_samples'].sum()
|
| 123 |
+
|
| 124 |
+
counts = {
|
| 125 |
+
str(language): int(value)
|
| 126 |
+
for language, value in by_language.items()
|
| 127 |
+
if pd.notna(value) and float(value) > 0
|
| 128 |
+
}
|
| 129 |
+
return counts
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def create_language_coverage_chart(
|
| 133 |
+
selected_languages: list[str] | None = None,
|
| 134 |
+
asr_df: pd.DataFrame | None = None,
|
| 135 |
+
) -> go.Figure:
|
| 136 |
+
"""
|
| 137 |
+
Create a horizontal bar chart showing sample counts for each language in PazaBench.
|
| 138 |
+
Languages are sorted by sample count (descending).
|
| 139 |
+
Selected languages are highlighted with a different color.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
selected_languages: List of languages to highlight (None = no highlighting)
|
| 143 |
+
"""
|
| 144 |
+
# Create dataframe from language sample counts
|
| 145 |
+
language_counts = _get_language_sample_counts(asr_df)
|
| 146 |
+
df = pd.DataFrame([
|
| 147 |
+
{"Language": lang, "Sample Count": count}
|
| 148 |
+
for lang, count in language_counts.items()
|
| 149 |
+
])
|
| 150 |
+
|
| 151 |
+
# Sort by sample count descending
|
| 152 |
+
df = df.sort_values("Sample Count", ascending=True) # ascending=True for horizontal bar (bottom to top)
|
| 153 |
+
|
| 154 |
+
# Determine colors - highlight selected languages
|
| 155 |
+
if selected_languages and len(selected_languages) > 0:
|
| 156 |
+
colors = [
|
| 157 |
+
"#0078D4" if lang in selected_languages else "#D0E8FF"
|
| 158 |
+
for lang in df["Language"]
|
| 159 |
+
]
|
| 160 |
+
# Add border to selected bars
|
| 161 |
+
line_widths = [2 if lang in selected_languages else 0 for lang in df["Language"]]
|
| 162 |
+
line_colors = ["#005A9E" if lang in selected_languages else "rgba(0,0,0,0)" for lang in df["Language"]]
|
| 163 |
+
# Show the sample size on the selected bars.
|
| 164 |
+
bar_text = [
|
| 165 |
+
f"{count:,}" if lang in selected_languages else ""
|
| 166 |
+
for lang, count in zip(df["Language"], df["Sample Count"])
|
| 167 |
+
]
|
| 168 |
+
else:
|
| 169 |
+
colors = "#8CD0FF" # Solid blue color matching theme
|
| 170 |
+
line_widths = 0
|
| 171 |
+
line_colors = "rgba(0,0,0,0)"
|
| 172 |
+
bar_text = None
|
| 173 |
+
|
| 174 |
+
fig = go.Figure(go.Bar(
|
| 175 |
+
y=df["Language"],
|
| 176 |
+
x=df["Sample Count"],
|
| 177 |
+
orientation='h',
|
| 178 |
+
marker=dict(
|
| 179 |
+
color=colors,
|
| 180 |
+
line=dict(width=line_widths, color=line_colors) if selected_languages else None,
|
| 181 |
+
),
|
| 182 |
+
text=bar_text,
|
| 183 |
+
textposition='outside',
|
| 184 |
+
textfont=dict(size=10),
|
| 185 |
+
cliponaxis=False,
|
| 186 |
+
hovertemplate='<b>%{y}</b><br>Samples: %{x:,}<extra></extra>',
|
| 187 |
+
))
|
| 188 |
+
|
| 189 |
+
# Build title
|
| 190 |
+
if selected_languages and len(selected_languages) > 0:
|
| 191 |
+
title_text = f"Language Coverage in PazaBench ({len(selected_languages)} selected)"
|
| 192 |
+
else:
|
| 193 |
+
title_text = "Language Coverage in PazaBench"
|
| 194 |
+
|
| 195 |
+
# One row per language (all labels visible); the container is scrollable in the UI.
|
| 196 |
+
chart_height = max(600, 26 * len(df) + 140)
|
| 197 |
+
|
| 198 |
+
fig.update_layout(
|
| 199 |
+
title=dict(
|
| 200 |
+
text=title_text,
|
| 201 |
+
font=dict(size=16),
|
| 202 |
+
x=0.5
|
| 203 |
+
),
|
| 204 |
+
xaxis_title="Number of Samples",
|
| 205 |
+
yaxis=dict(
|
| 206 |
+
title="",
|
| 207 |
+
tickmode="linear",
|
| 208 |
+
dtick=1,
|
| 209 |
+
tickfont=dict(size=11),
|
| 210 |
+
automargin=True,
|
| 211 |
+
),
|
| 212 |
+
height=chart_height,
|
| 213 |
+
autosize=True,
|
| 214 |
+
margin=dict(l=120, r=70, t=60, b=40),
|
| 215 |
+
template='plotly_white',
|
| 216 |
+
showlegend=False,
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
return fig
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def _wrap_languages(languages: list[str], per_line: int = 3) -> str:
|
| 223 |
+
"""Wrap a language list across multiple hover lines so it fits inside the map."""
|
| 224 |
+
if not languages:
|
| 225 |
+
return "—"
|
| 226 |
+
return "<br>".join(
|
| 227 |
+
", ".join(languages[i:i + per_line]) for i in range(0, len(languages), per_line)
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def create_language_location_map(languages: str | list[str] | None = None) -> go.Figure:
|
| 232 |
+
"""
|
| 233 |
+
Create an interactive choropleth map of Africa. Countries with benchmark data are
|
| 234 |
+
highlighted (solid blue) with the languages spoken there wrapped in the tooltip;
|
| 235 |
+
countries without data show "No coverage". When language(s) are selected, the
|
| 236 |
+
countries where they are spoken are highlighted and the remaining data countries
|
| 237 |
+
are shown in a lighter shade.
|
| 238 |
+
|
| 239 |
+
Args:
|
| 240 |
+
languages: The language(s) to highlight on the map (single string or list)
|
| 241 |
+
"""
|
| 242 |
+
fig = go.Figure()
|
| 243 |
+
|
| 244 |
+
if isinstance(languages, str):
|
| 245 |
+
languages = [languages]
|
| 246 |
+
|
| 247 |
+
# Which languages are spoken in each country that has benchmark data.
|
| 248 |
+
country_language_map = {}
|
| 249 |
+
for lang, codes in LANGUAGE_COUNTRY_MAP.items():
|
| 250 |
+
for code in codes:
|
| 251 |
+
country_language_map.setdefault(code, []).append(lang)
|
| 252 |
+
data_codes = set(country_language_map)
|
| 253 |
+
|
| 254 |
+
selected = bool(languages)
|
| 255 |
+
if selected:
|
| 256 |
+
highlight_codes = {code for lang in languages for code in LANGUAGE_COUNTRY_MAP.get(lang, [])}
|
| 257 |
+
else:
|
| 258 |
+
highlight_codes = set(data_codes)
|
| 259 |
+
|
| 260 |
+
# Trace 1 — African countries with no benchmark data: light grey, "No coverage".
|
| 261 |
+
no_coverage = [code for code in AFRICAN_COUNTRIES if code not in data_codes]
|
| 262 |
+
if no_coverage:
|
| 263 |
+
fig.add_trace(go.Choropleth(
|
| 264 |
+
locations=no_coverage,
|
| 265 |
+
z=[0] * len(no_coverage),
|
| 266 |
+
text=[AFRICAN_COUNTRIES.get(code, code) for code in no_coverage],
|
| 267 |
+
hovertemplate="<b>%{text}</b><br>No coverage<extra></extra>",
|
| 268 |
+
colorscale=[[0, "#EDF1F5"], [1, "#EDF1F5"]],
|
| 269 |
+
showscale=False,
|
| 270 |
+
marker_line_color="white",
|
| 271 |
+
marker_line_width=0.5,
|
| 272 |
+
))
|
| 273 |
+
|
| 274 |
+
# Trace 2 — data countries not currently highlighted (selected view only):
|
| 275 |
+
# lighter blue, still surface their languages so users know they have coverage.
|
| 276 |
+
if selected:
|
| 277 |
+
other = sorted(data_codes - highlight_codes)
|
| 278 |
+
if other:
|
| 279 |
+
fig.add_trace(go.Choropleth(
|
| 280 |
+
locations=other,
|
| 281 |
+
z=[0] * len(other),
|
| 282 |
+
text=[COUNTRY_NAMES.get(code, code) for code in other],
|
| 283 |
+
customdata=[_wrap_languages(sorted(country_language_map[code])) for code in other],
|
| 284 |
+
hovertemplate="<b>%{text}</b><br>Languages: %{customdata}<extra></extra>",
|
| 285 |
+
colorscale=[[0, "#DCEEFB"], [1, "#DCEEFB"]],
|
| 286 |
+
showscale=False,
|
| 287 |
+
marker_line_color="white",
|
| 288 |
+
marker_line_width=0.5,
|
| 289 |
+
))
|
| 290 |
+
|
| 291 |
+
# Trace 3 — highlighted countries: solid blue with a wrapped language tooltip.
|
| 292 |
+
highlight = sorted(highlight_codes)
|
| 293 |
+
if highlight:
|
| 294 |
+
customdata = []
|
| 295 |
+
for code in highlight:
|
| 296 |
+
if selected:
|
| 297 |
+
langs = [lang for lang in languages if code in LANGUAGE_COUNTRY_MAP.get(lang, [])]
|
| 298 |
+
else:
|
| 299 |
+
langs = sorted(country_language_map.get(code, []))
|
| 300 |
+
customdata.append(_wrap_languages(langs))
|
| 301 |
+
fig.add_trace(go.Choropleth(
|
| 302 |
+
locations=highlight,
|
| 303 |
+
z=[1] * len(highlight),
|
| 304 |
+
text=[COUNTRY_NAMES.get(code, code) for code in highlight],
|
| 305 |
+
customdata=customdata,
|
| 306 |
+
hovertemplate="<b>%{text}</b><br>Languages: %{customdata}<extra></extra>",
|
| 307 |
+
colorscale=[[0, "#8CD0FF"], [1, "#8CD0FF"]],
|
| 308 |
+
showscale=False,
|
| 309 |
+
marker_line_color="white",
|
| 310 |
+
marker_line_width=0.5,
|
| 311 |
+
))
|
| 312 |
+
|
| 313 |
+
if selected and highlight:
|
| 314 |
+
if len(languages) == 1:
|
| 315 |
+
title_text = f"Where {languages[0]} is spoken"
|
| 316 |
+
else:
|
| 317 |
+
title_text = f"Where {len(languages)} selected languages are spoken"
|
| 318 |
+
else:
|
| 319 |
+
title_text = "Select a language to explore"
|
| 320 |
+
|
| 321 |
+
fig.update_geos(
|
| 322 |
+
visible=True,
|
| 323 |
+
resolution=50,
|
| 324 |
+
scope="africa",
|
| 325 |
+
showcountries=True,
|
| 326 |
+
countrycolor="lightgray",
|
| 327 |
+
showcoastlines=True,
|
| 328 |
+
coastlinecolor="gray",
|
| 329 |
+
showland=True,
|
| 330 |
+
landcolor="#f5f5f5",
|
| 331 |
+
showocean=True,
|
| 332 |
+
oceancolor="#e3f2fd",
|
| 333 |
+
showlakes=True,
|
| 334 |
+
lakecolor="#e3f2fd",
|
| 335 |
+
projection_type="natural earth",
|
| 336 |
+
center=dict(lat=5, lon=20),
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
fig.update_layout(
|
| 340 |
+
title=dict(
|
| 341 |
+
text=title_text,
|
| 342 |
+
font=dict(size=16),
|
| 343 |
+
x=0.5
|
| 344 |
+
),
|
| 345 |
+
height=600,
|
| 346 |
+
autosize=True,
|
| 347 |
+
margin=dict(l=5, r=5, t=50, b=5),
|
| 348 |
+
geo=dict(
|
| 349 |
+
bgcolor="rgba(0,0,0,0)",
|
| 350 |
+
)
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
# Use SVG renderer for better resolution
|
| 354 |
+
fig.update_layout(
|
| 355 |
+
template="plotly_white",
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
return fig
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def get_language_sample_info(languages: str | list[str] | None = None, asr_df: pd.DataFrame | None = None) -> str:
|
| 362 |
+
"""
|
| 363 |
+
Get the sample count information for specific language(s) as styled HTML.
|
| 364 |
+
Returns HTML for display in Gradio.
|
| 365 |
+
|
| 366 |
+
Args:
|
| 367 |
+
languages: The language(s) to get information for (single string or list)
|
| 368 |
+
asr_df: DataFrame with ASR results to extract dataset groups
|
| 369 |
+
"""
|
| 370 |
+
# Normalize to list
|
| 371 |
+
if isinstance(languages, str):
|
| 372 |
+
languages = [languages]
|
| 373 |
+
|
| 374 |
+
language_counts = _get_language_sample_counts(asr_df)
|
| 375 |
+
|
| 376 |
+
if languages and len(languages) > 0:
|
| 377 |
+
# Filter to valid languages
|
| 378 |
+
valid_languages = [lang for lang in languages if lang in language_counts]
|
| 379 |
+
|
| 380 |
+
if valid_languages:
|
| 381 |
+
# Aggregate data across all selected languages
|
| 382 |
+
total_samples = sum(language_counts.get(lang, 0) for lang in valid_languages)
|
| 383 |
+
all_countries = set()
|
| 384 |
+
for lang in valid_languages:
|
| 385 |
+
all_countries.update(LANGUAGE_COUNTRY_MAP.get(lang, []))
|
| 386 |
+
country_names = sorted([COUNTRY_NAMES.get(code, code) for code in all_countries])
|
| 387 |
+
|
| 388 |
+
# Get dataset groups from ASR data if available
|
| 389 |
+
dataset_groups = set()
|
| 390 |
+
if asr_df is not None and not asr_df.empty and 'language' in asr_df.columns:
|
| 391 |
+
for lang in valid_languages:
|
| 392 |
+
lang_data = asr_df[asr_df['language'] == lang]
|
| 393 |
+
if not lang_data.empty and 'dataset_group' in lang_data.columns:
|
| 394 |
+
dataset_groups.update(lang_data['dataset_group'].unique().tolist())
|
| 395 |
+
dataset_groups = sorted(dataset_groups)
|
| 396 |
+
|
| 397 |
+
# Build title based on number of languages
|
| 398 |
+
if len(valid_languages) == 1:
|
| 399 |
+
title = f"🌍 {valid_languages[0]}"
|
| 400 |
+
else:
|
| 401 |
+
title = f"🌍 {', '.join(sorted(valid_languages))}"
|
| 402 |
+
|
| 403 |
+
html = f"""
|
| 404 |
+
<div style="background: linear-gradient(135deg, #DDF1FF 0%, #ffffff 100%); border-radius: 12px; padding: 20px; border: 1px solid #8CD0FF;">
|
| 405 |
+
<h4 style="margin: 0 0 16px 0; color: #0f172a; font-size: 1.1em; font-weight: 600; border-bottom: 2px solid #8CD0FF; padding-bottom: 8px;">
|
| 406 |
+
{title}
|
| 407 |
+
</h4>
|
| 408 |
+
|
| 409 |
+
<div style="display: grid; gap: 12px;">
|
| 410 |
+
<div style="background: white; padding: 12px 16px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
|
| 411 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📊 Total Samples</span>
|
| 412 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">{total_samples:,}</span>
|
| 413 |
+
</div>
|
| 414 |
+
|
| 415 |
+
<div style="background: white; padding: 12px 16px; border-radius: 8px;">
|
| 416 |
+
<div style="color: #666; font-weight: 500; margin-bottom: 6px; font-size: 0.9rem;">📍 Countries ({len(country_names)})</div>
|
| 417 |
+
<div style="color: #0f172a; font-weight: 500; font-size: 0.9rem;">{', '.join(country_names) if country_names else 'N/A'}</div>
|
| 418 |
+
</div>
|
| 419 |
+
|
| 420 |
+
<div style="background: white; padding: 12px 16px; border-radius: 8px;">
|
| 421 |
+
<div style="color: #666; font-weight: 500; margin-bottom: 6px; font-size: 0.9rem;">📁 Dataset Sources ({len(dataset_groups)})</div>
|
| 422 |
+
<div style="color: #0f172a; font-size: 0.9rem;">
|
| 423 |
+
{(', '.join(dataset_groups) if dataset_groups else '<em>No dataset info available</em>')}
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
</div>
|
| 427 |
+
</div>
|
| 428 |
+
"""
|
| 429 |
+
return html
|
| 430 |
+
|
| 431 |
+
# Default view - show sample overview summary
|
| 432 |
+
total_languages = len(language_counts)
|
| 433 |
+
total_samples = sum(language_counts.values())
|
| 434 |
+
total_countries = len(set(code for codes in LANGUAGE_COUNTRY_MAP.values() for code in codes))
|
| 435 |
+
|
| 436 |
+
html = f"""
|
| 437 |
+
<div style="background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%); border-radius: 12px; padding: 20px; border: 1px solid #e0e7ef;">
|
| 438 |
+
<h4 style="margin: 0 0 16px 0; color: #0f172a; font-size: 1.1em; font-weight: 600; border-bottom: 2px solid #8CD0FF; padding-bottom: 8px;">
|
| 439 |
+
📊 Sample Overview
|
| 440 |
+
</h4>
|
| 441 |
+
|
| 442 |
+
<div style="display: grid; gap: 10px; margin-bottom: 16px;">
|
| 443 |
+
<div style="background: white; padding: 10px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 444 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">🌍 Languages</span>
|
| 445 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">{total_languages}</span>
|
| 446 |
+
</div>
|
| 447 |
+
<div style="background: white; padding: 10px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 448 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📊 Total Samples</span>
|
| 449 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">{total_samples:,}</span>
|
| 450 |
+
</div>
|
| 451 |
+
<div style="background: white; padding: 10px 14px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb;">
|
| 452 |
+
<span style="color: #666; font-weight: 500; font-size: 0.9rem;">📍 Countries</span>
|
| 453 |
+
<span style="color: #0f172a; font-weight: 700; font-size: 0.9rem;">{total_countries}</span>
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
|
| 457 |
+
<p style="margin: 14px 0 0 0; font-size: 0.85rem; color: #666; font-style: italic; text-align: center;">
|
| 458 |
+
👈 Select a language on the left to explore its details
|
| 459 |
+
</p>
|
| 460 |
+
</div>
|
| 461 |
+
"""
|
| 462 |
+
|
| 463 |
+
return html
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def get_language_sample_info_df(language: str | None = None, asr_df: pd.DataFrame | None = None) -> pd.DataFrame:
|
| 467 |
+
"""Return language sample info as a DataFrame."""
|
| 468 |
+
language_counts = _get_language_sample_counts(asr_df)
|
| 469 |
+
if language and language in language_counts:
|
| 470 |
+
sample_count = language_counts[language]
|
| 471 |
+
countries = LANGUAGE_COUNTRY_MAP.get(language, [])
|
| 472 |
+
country_names = [COUNTRY_NAMES.get(code, code) for code in countries]
|
| 473 |
+
|
| 474 |
+
df = pd.DataFrame({
|
| 475 |
+
"Metric": ["Language", "Total Samples", "Countries"],
|
| 476 |
+
"Value": [
|
| 477 |
+
language,
|
| 478 |
+
f"{sample_count:,}",
|
| 479 |
+
", ".join(country_names) if country_names else "N/A"
|
| 480 |
+
]
|
| 481 |
+
})
|
| 482 |
+
else:
|
| 483 |
+
df = pd.DataFrame({
|
| 484 |
+
"Metric": ["Language", "Total Samples", "Countries"],
|
| 485 |
+
"Value": ["Select a language", "-", "-"]
|
| 486 |
+
})
|
| 487 |
+
|
| 488 |
+
return df
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def get_all_languages(asr_df: pd.DataFrame | None = None) -> list[str]:
|
| 492 |
+
"""Get a sorted list of all languages in PazaBench."""
|
| 493 |
+
return sorted(_get_language_sample_counts(asr_df).keys())
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def create_africa_language_map() -> go.Figure:
|
| 497 |
+
"""
|
| 498 |
+
Create an interactive choropleth map of Africa showing language coverage.
|
| 499 |
+
Hover over countries to see the languages spoken there.
|
| 500 |
+
"""
|
| 501 |
+
# Build country data with languages
|
| 502 |
+
country_data = {}
|
| 503 |
+
for language, countries in LANGUAGE_COUNTRY_MAP.items():
|
| 504 |
+
for country_code in countries:
|
| 505 |
+
if country_code not in country_data:
|
| 506 |
+
country_data[country_code] = {
|
| 507 |
+
"languages": [],
|
| 508 |
+
"count": 0,
|
| 509 |
+
"country_name": COUNTRY_NAMES.get(country_code, country_code)
|
| 510 |
+
}
|
| 511 |
+
country_data[country_code]["languages"].append(language)
|
| 512 |
+
country_data[country_code]["count"] += 1
|
| 513 |
+
|
| 514 |
+
# Create dataframe for plotly
|
| 515 |
+
df_map = pd.DataFrame([
|
| 516 |
+
{
|
| 517 |
+
"country_code": code,
|
| 518 |
+
"country_name": data["country_name"],
|
| 519 |
+
"language_count": data["count"],
|
| 520 |
+
"languages": ", ".join(sorted(data["languages"]))
|
| 521 |
+
}
|
| 522 |
+
for code, data in country_data.items()
|
| 523 |
+
])
|
| 524 |
+
|
| 525 |
+
fig = go.Figure(go.Choropleth(
|
| 526 |
+
locations=df_map["country_code"],
|
| 527 |
+
z=df_map["language_count"],
|
| 528 |
+
text=df_map["country_name"],
|
| 529 |
+
customdata=df_map[["languages", "language_count"]],
|
| 530 |
+
hovertemplate="<b>%{text}</b><br>" +
|
| 531 |
+
"Languages: %{customdata[1]}<br>" +
|
| 532 |
+
"<i>%{customdata[0]}</i><extra></extra>",
|
| 533 |
+
colorscale=[
|
| 534 |
+
[0, "#DDF1FF"],
|
| 535 |
+
[0.25, "#B5E0FF"],
|
| 536 |
+
[0.5, "#8CD0FF"],
|
| 537 |
+
[0.75, "#6BC0F5"],
|
| 538 |
+
[1, "#4AAFEB"]
|
| 539 |
+
],
|
| 540 |
+
showscale=True,
|
| 541 |
+
colorbar=dict(
|
| 542 |
+
title="Languages",
|
| 543 |
+
tickmode="linear",
|
| 544 |
+
tick0=1,
|
| 545 |
+
dtick=1
|
| 546 |
+
),
|
| 547 |
+
marker_line_color="white",
|
| 548 |
+
marker_line_width=0.5,
|
| 549 |
+
))
|
| 550 |
+
|
| 551 |
+
fig.update_geos(
|
| 552 |
+
visible=True,
|
| 553 |
+
resolution=50,
|
| 554 |
+
scope="africa",
|
| 555 |
+
showcountries=True,
|
| 556 |
+
countrycolor="lightgray",
|
| 557 |
+
showcoastlines=True,
|
| 558 |
+
coastlinecolor="gray",
|
| 559 |
+
showland=True,
|
| 560 |
+
landcolor="#f5f5f5",
|
| 561 |
+
showocean=True,
|
| 562 |
+
oceancolor="#e3f2fd",
|
| 563 |
+
showlakes=True,
|
| 564 |
+
lakecolor="#e3f2fd",
|
| 565 |
+
projection_type="natural earth",
|
| 566 |
+
center=dict(lat=5, lon=20),
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
fig.update_layout(
|
| 570 |
+
title=dict(
|
| 571 |
+
text="African Languages in PazaBench",
|
| 572 |
+
font=dict(size=18),
|
| 573 |
+
x=0.5
|
| 574 |
+
),
|
| 575 |
+
height=600,
|
| 576 |
+
autosize=True,
|
| 577 |
+
margin=dict(l=5, r=5, t=50, b=5),
|
| 578 |
+
geo=dict(
|
| 579 |
+
bgcolor="rgba(0,0,0,0)",
|
| 580 |
+
),
|
| 581 |
+
template="plotly_white",
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
return fig
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
def create_model_leaderboard(df: pd.DataFrame, languages: list[str] | None = None, top_n_models: int = 15) -> go.Figure:
|
| 588 |
+
"""
|
| 589 |
+
Visualization 1: Model Family / Individual Model Performance Leaderboard
|
| 590 |
+
- When no language filter: Shows model families (aggregated)
|
| 591 |
+
- When language(s) selected: Shows top N individual models
|
| 592 |
+
|
| 593 |
+
Outliers are removed for cleaner visualization.
|
| 594 |
+
|
| 595 |
+
Args:
|
| 596 |
+
df: DataFrame with evaluation results
|
| 597 |
+
languages: List of languages to filter by (None = all languages)
|
| 598 |
+
top_n_models: Number of top individual models to show when languages are filtered (default: 15)
|
| 599 |
+
"""
|
| 600 |
+
# Apply language filter if provided
|
| 601 |
+
filtered_df = df.copy()
|
| 602 |
+
if languages:
|
| 603 |
+
filtered_df = filtered_df[filtered_df['language'].isin(languages)]
|
| 604 |
+
|
| 605 |
+
# Remove WER outliers for cleaner visualization
|
| 606 |
+
filtered_df = _remove_wer_outliers(filtered_df)
|
| 607 |
+
|
| 608 |
+
# Determine mode: individual models if languages selected, otherwise model families
|
| 609 |
+
show_individual_models = languages is not None and len(languages) > 0
|
| 610 |
+
|
| 611 |
+
if show_individual_models:
|
| 612 |
+
# Individual model mode: show top N models by median WER
|
| 613 |
+
model_perf = filtered_df.groupby(['model_family', 'model']).agg({
|
| 614 |
+
'wer': ['median', 'std', 'count'],
|
| 615 |
+
'cer': 'median',
|
| 616 |
+
'rtfx': 'median'
|
| 617 |
+
}).reset_index()
|
| 618 |
+
|
| 619 |
+
# Get unique sample counts per model
|
| 620 |
+
unique_samples = filtered_df.groupby(['model_family', 'model', 'language', 'dataset_group'])['num_samples'].first().reset_index()
|
| 621 |
+
model_samples = unique_samples.groupby(['model_family', 'model'])['num_samples'].sum().reset_index()
|
| 622 |
+
model_samples.columns = ['model_family', 'model', 'total_samples']
|
| 623 |
+
|
| 624 |
+
model_perf.columns = ['model_family', 'model', 'wer_median', 'wer_std', 'count', 'cer_median', 'rtfx_median']
|
| 625 |
+
model_perf = model_perf.merge(model_samples, on=['model_family', 'model'], how='left')
|
| 626 |
+
model_perf = model_perf.sort_values('wer_median').head(top_n_models)
|
| 627 |
+
|
| 628 |
+
# Create short model name for display
|
| 629 |
+
model_perf['model_short'] = model_perf['model'].apply(
|
| 630 |
+
lambda x: x.split('/')[-1] if '/' in x else x
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
+
# Get colors for each model based on family
|
| 634 |
+
model_perf['color'] = model_perf['model_family'].apply(_get_color_for_family)
|
| 635 |
+
|
| 636 |
+
fig = go.Figure()
|
| 637 |
+
|
| 638 |
+
fig.add_trace(go.Bar(
|
| 639 |
+
y=model_perf['model_short'],
|
| 640 |
+
x=model_perf['wer_median'],
|
| 641 |
+
orientation='h',
|
| 642 |
+
marker=dict(color=model_perf['color']),
|
| 643 |
+
text=model_perf['wer_median'].round(2),
|
| 644 |
+
textposition='outside',
|
| 645 |
+
hovertemplate=(
|
| 646 |
+
'<b>%{customdata[0]}</b><br>' +
|
| 647 |
+
'<i>Family: %{customdata[1]}</i><br><br>' +
|
| 648 |
+
'<b>Median WER:</b> %{x:.3f}<br>' +
|
| 649 |
+
'<b>RTFx:</b> %{customdata[2]:.1f}<br>' +
|
| 650 |
+
'<b>Evaluations:</b> %{customdata[3]}<br>' +
|
| 651 |
+
'<b>Samples:</b> %{customdata[4]:,}<extra></extra>'
|
| 652 |
+
),
|
| 653 |
+
customdata=model_perf[['model', 'model_family', 'rtfx_median', 'count', 'total_samples']]
|
| 654 |
+
))
|
| 655 |
+
|
| 656 |
+
# Build title with language info (languages is guaranteed to be non-empty here)
|
| 657 |
+
lang_list = languages if languages else []
|
| 658 |
+
lang_str = ", ".join(lang_list[:3]) + ("..." if len(lang_list) > 3 else "")
|
| 659 |
+
title_text = f"Top {min(top_n_models, len(model_perf))} Models for {lang_str}"
|
| 660 |
+
|
| 661 |
+
else:
|
| 662 |
+
# Model family mode
|
| 663 |
+
model_perf = filtered_df.groupby('model_family').agg({
|
| 664 |
+
'wer': ['median', 'std', 'count'],
|
| 665 |
+
'cer': 'median',
|
| 666 |
+
'rtfx': 'median'
|
| 667 |
+
}).reset_index()
|
| 668 |
+
|
| 669 |
+
# Get unique sample counts per model family (avoid double-counting across models)
|
| 670 |
+
unique_samples = filtered_df.groupby(['model_family', 'language', 'dataset_group'])['num_samples'].first().reset_index()
|
| 671 |
+
family_samples = unique_samples.groupby('model_family')['num_samples'].sum().reset_index()
|
| 672 |
+
family_samples.columns = ['model_family', 'total_samples']
|
| 673 |
+
|
| 674 |
+
model_perf.columns = ['model_family', 'wer_median', 'wer_std', 'count', 'cer_median', 'rtfx_median']
|
| 675 |
+
model_perf = model_perf.merge(family_samples, on='model_family', how='left')
|
| 676 |
+
model_perf = model_perf.sort_values('wer_median')
|
| 677 |
+
|
| 678 |
+
# Get colors for each model family
|
| 679 |
+
model_perf['color'] = model_perf['model_family'].apply(_get_color_for_family)
|
| 680 |
+
|
| 681 |
+
fig = go.Figure()
|
| 682 |
+
|
| 683 |
+
fig.add_trace(go.Bar(
|
| 684 |
+
y=model_perf['model_family'],
|
| 685 |
+
x=model_perf['wer_median'],
|
| 686 |
+
orientation='h',
|
| 687 |
+
error_x=dict(type='data', array=model_perf['wer_std']),
|
| 688 |
+
marker=dict(color=model_perf['color']),
|
| 689 |
+
text=model_perf['wer_median'].round(2),
|
| 690 |
+
textposition='outside',
|
| 691 |
+
hovertemplate=(
|
| 692 |
+
'<b>%{y}</b><br><br>' +
|
| 693 |
+
'<b>Median WER:</b> %{x:.3f}<br>' +
|
| 694 |
+
'<b>Std Dev:</b> %{customdata[0]:.3f}<br>' +
|
| 695 |
+
'<b>RTFx:</b> %{customdata[1]:.1f}<br>' +
|
| 696 |
+
'<b>Evaluations:</b> %{customdata[2]}<br>' +
|
| 697 |
+
'<b>Samples:</b> %{customdata[3]:,}<extra></extra>'
|
| 698 |
+
),
|
| 699 |
+
customdata=model_perf[['wer_std', 'rtfx_median', 'count', 'total_samples']]
|
| 700 |
+
))
|
| 701 |
+
|
| 702 |
+
title_text = "Model Family Performance Leaderboard"
|
| 703 |
+
|
| 704 |
+
# Calculate dynamic height based on number of items
|
| 705 |
+
num_items = len(model_perf)
|
| 706 |
+
height = max(400, min(700, 100 + num_items * 35))
|
| 707 |
+
|
| 708 |
+
fig.update_layout(
|
| 709 |
+
title=title_text,
|
| 710 |
+
xaxis_title="Word Error Rate (WER)",
|
| 711 |
+
yaxis_title="",
|
| 712 |
+
height=height,
|
| 713 |
+
autosize=True,
|
| 714 |
+
showlegend=False,
|
| 715 |
+
template='plotly_white',
|
| 716 |
+
margin=dict(l=200, r=30, t=60, b=60)
|
| 717 |
+
)
|
| 718 |
+
|
| 719 |
+
return fig
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
def create_cer_leaderboard(df: pd.DataFrame, languages: list[str] | None = None, top_n_models: int = 15) -> go.Figure:
|
| 723 |
+
"""
|
| 724 |
+
Visualization: CER Model Family / Individual Model Performance Leaderboard
|
| 725 |
+
- When no language filter: Shows model families (aggregated)
|
| 726 |
+
- When language(s) selected: Shows top N individual models
|
| 727 |
+
|
| 728 |
+
Outliers are removed for cleaner visualization.
|
| 729 |
+
|
| 730 |
+
Args:
|
| 731 |
+
df: DataFrame with evaluation results
|
| 732 |
+
languages: List of languages to filter by (None = all languages)
|
| 733 |
+
top_n_models: Number of top individual models to show when languages are filtered (default: 15)
|
| 734 |
+
"""
|
| 735 |
+
# Apply language filter if provided
|
| 736 |
+
filtered_df = df.copy()
|
| 737 |
+
if languages:
|
| 738 |
+
filtered_df = filtered_df[filtered_df['language'].isin(languages)]
|
| 739 |
+
|
| 740 |
+
# Remove CER outliers for cleaner visualization (similar to WER outlier removal)
|
| 741 |
+
if not filtered_df.empty and 'cer' in filtered_df.columns:
|
| 742 |
+
Q1 = filtered_df['cer'].quantile(0.25)
|
| 743 |
+
Q3 = filtered_df['cer'].quantile(0.75)
|
| 744 |
+
IQR = Q3 - Q1
|
| 745 |
+
upper_bound = Q3 + 1.5 * IQR
|
| 746 |
+
filtered_df = filtered_df[filtered_df['cer'] <= upper_bound]
|
| 747 |
+
|
| 748 |
+
# Determine mode: individual models if languages selected, otherwise model families
|
| 749 |
+
show_individual_models = languages is not None and len(languages) > 0
|
| 750 |
+
|
| 751 |
+
if show_individual_models:
|
| 752 |
+
# Individual model mode: show top N models by median CER
|
| 753 |
+
model_perf = filtered_df.groupby(['model_family', 'model']).agg({
|
| 754 |
+
'cer': ['median', 'std', 'count'],
|
| 755 |
+
'wer': 'median',
|
| 756 |
+
'rtfx': 'median'
|
| 757 |
+
}).reset_index()
|
| 758 |
+
|
| 759 |
+
# Get unique sample counts per model
|
| 760 |
+
unique_samples = filtered_df.groupby(['model_family', 'model', 'language', 'dataset_group'])['num_samples'].first().reset_index()
|
| 761 |
+
model_samples = unique_samples.groupby(['model_family', 'model'])['num_samples'].sum().reset_index()
|
| 762 |
+
model_samples.columns = ['model_family', 'model', 'total_samples']
|
| 763 |
+
|
| 764 |
+
model_perf.columns = ['model_family', 'model', 'cer_median', 'cer_std', 'count', 'wer_median', 'rtfx_median']
|
| 765 |
+
model_perf = model_perf.merge(model_samples, on=['model_family', 'model'], how='left')
|
| 766 |
+
model_perf = model_perf.sort_values('cer_median').head(top_n_models)
|
| 767 |
+
|
| 768 |
+
# Create short model name for display
|
| 769 |
+
model_perf['model_short'] = model_perf['model'].apply(
|
| 770 |
+
lambda x: x.split('/')[-1] if '/' in x else x
|
| 771 |
+
)
|
| 772 |
+
|
| 773 |
+
# Get colors for each model based on family
|
| 774 |
+
model_perf['color'] = model_perf['model_family'].apply(_get_color_for_family)
|
| 775 |
+
|
| 776 |
+
fig = go.Figure()
|
| 777 |
+
|
| 778 |
+
fig.add_trace(go.Bar(
|
| 779 |
+
y=model_perf['model_short'],
|
| 780 |
+
x=model_perf['cer_median'],
|
| 781 |
+
orientation='h',
|
| 782 |
+
marker=dict(color=model_perf['color']),
|
| 783 |
+
text=model_perf['cer_median'].round(2),
|
| 784 |
+
textposition='outside',
|
| 785 |
+
hovertemplate=(
|
| 786 |
+
'<b>%{customdata[0]}</b><br>' +
|
| 787 |
+
'<i>Family: %{customdata[1]}</i><br><br>' +
|
| 788 |
+
'<b>Median CER:</b> %{x:.3f}<br>' +
|
| 789 |
+
'<b>WER:</b> %{customdata[2]:.3f}<br>' +
|
| 790 |
+
'<b>RTFx:</b> %{customdata[3]:.1f}<br>' +
|
| 791 |
+
'<b>Evaluations:</b> %{customdata[4]}<br>' +
|
| 792 |
+
'<b>Samples:</b> %{customdata[5]:,}<extra></extra>'
|
| 793 |
+
),
|
| 794 |
+
customdata=model_perf[['model', 'model_family', 'wer_median', 'rtfx_median', 'count', 'total_samples']]
|
| 795 |
+
))
|
| 796 |
+
|
| 797 |
+
# Build title with language info (languages is guaranteed to be non-empty here)
|
| 798 |
+
lang_list = languages if languages else []
|
| 799 |
+
lang_str = ", ".join(lang_list[:3]) + ("..." if len(lang_list) > 3 else "")
|
| 800 |
+
title_text = f"Top {min(top_n_models, len(model_perf))} Models by CER for {lang_str}"
|
| 801 |
+
|
| 802 |
+
else:
|
| 803 |
+
# Model family mode
|
| 804 |
+
model_perf = filtered_df.groupby('model_family').agg({
|
| 805 |
+
'cer': ['median', 'std', 'count'],
|
| 806 |
+
'wer': 'median',
|
| 807 |
+
'rtfx': 'median'
|
| 808 |
+
}).reset_index()
|
| 809 |
+
|
| 810 |
+
# Get unique sample counts per model family (avoid double-counting across models)
|
| 811 |
+
unique_samples = filtered_df.groupby(['model_family', 'language', 'dataset_group'])['num_samples'].first().reset_index()
|
| 812 |
+
family_samples = unique_samples.groupby('model_family')['num_samples'].sum().reset_index()
|
| 813 |
+
family_samples.columns = ['model_family', 'total_samples']
|
| 814 |
+
|
| 815 |
+
model_perf.columns = ['model_family', 'cer_median', 'cer_std', 'count', 'wer_median', 'rtfx_median']
|
| 816 |
+
model_perf = model_perf.merge(family_samples, on='model_family', how='left')
|
| 817 |
+
model_perf = model_perf.sort_values('cer_median')
|
| 818 |
+
|
| 819 |
+
# Get colors for each model family
|
| 820 |
+
model_perf['color'] = model_perf['model_family'].apply(_get_color_for_family)
|
| 821 |
+
|
| 822 |
+
fig = go.Figure()
|
| 823 |
+
|
| 824 |
+
fig.add_trace(go.Bar(
|
| 825 |
+
y=model_perf['model_family'],
|
| 826 |
+
x=model_perf['cer_median'],
|
| 827 |
+
orientation='h',
|
| 828 |
+
error_x=dict(type='data', array=model_perf['cer_std']),
|
| 829 |
+
marker=dict(color=model_perf['color']),
|
| 830 |
+
text=model_perf['cer_median'].round(2),
|
| 831 |
+
textposition='outside',
|
| 832 |
+
hovertemplate=(
|
| 833 |
+
'<b>%{y}</b><br><br>' +
|
| 834 |
+
'<b>Median CER:</b> %{x:.3f}<br>' +
|
| 835 |
+
'<b>Std Dev:</b> %{customdata[0]:.3f}<br>' +
|
| 836 |
+
'<b>WER:</b> %{customdata[1]:.3f}<br>' +
|
| 837 |
+
'<b>RTFx:</b> %{customdata[2]:.1f}<br>' +
|
| 838 |
+
'<b>Evaluations:</b> %{customdata[3]}<br>' +
|
| 839 |
+
'<b>Samples:</b> %{customdata[4]:,}<extra></extra>'
|
| 840 |
+
),
|
| 841 |
+
customdata=model_perf[['cer_std', 'wer_median', 'rtfx_median', 'count', 'total_samples']]
|
| 842 |
+
))
|
| 843 |
+
|
| 844 |
+
title_text = "Model Family Performance by CER"
|
| 845 |
+
|
| 846 |
+
# Calculate dynamic height based on number of items
|
| 847 |
+
num_items = len(model_perf)
|
| 848 |
+
height = max(400, min(700, 100 + num_items * 35))
|
| 849 |
+
|
| 850 |
+
fig.update_layout(
|
| 851 |
+
title=title_text,
|
| 852 |
+
xaxis_title="Character Error Rate (CER)",
|
| 853 |
+
yaxis_title="",
|
| 854 |
+
height=height,
|
| 855 |
+
autosize=True,
|
| 856 |
+
showlegend=False,
|
| 857 |
+
template='plotly_white',
|
| 858 |
+
margin=dict(l=200, r=30, t=60, b=60)
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
return fig
|
| 862 |
+
|
| 863 |
+
|
| 864 |
+
def create_speed_accuracy_scatter(df: pd.DataFrame, view_mode: str = "model_family", languages: list[str] | None = None) -> go.Figure:
|
| 865 |
+
"""
|
| 866 |
+
Visualization 2: Speed vs Accuracy Tradeoff
|
| 867 |
+
Scatter plot showing the relationship between WER and RTFx with quadrants.
|
| 868 |
+
Outliers are removed for cleaner visualization.
|
| 869 |
+
|
| 870 |
+
Args:
|
| 871 |
+
df: DataFrame with evaluation results
|
| 872 |
+
view_mode: Either "model_family" (bubbles same size per family, color by family) or
|
| 873 |
+
"individual_model" (bubble size = model params, color = model family)
|
| 874 |
+
languages: List of languages to filter by (None = all languages)
|
| 875 |
+
"""
|
| 876 |
+
# Apply language filter if provided
|
| 877 |
+
if languages:
|
| 878 |
+
df = df[df['language'].isin(languages)]
|
| 879 |
+
|
| 880 |
+
# Remove WER outliers for cleaner visualization
|
| 881 |
+
df = _remove_wer_outliers(df)
|
| 882 |
+
if view_mode == "individual_model":
|
| 883 |
+
# Aggregate by individual model
|
| 884 |
+
model_agg = df.groupby(['model_family', 'model']).agg({
|
| 885 |
+
'wer': 'median',
|
| 886 |
+
'rtfx': 'median',
|
| 887 |
+
'cer': 'median',
|
| 888 |
+
}).reset_index()
|
| 889 |
+
|
| 890 |
+
# Get unique sample counts per model
|
| 891 |
+
unique_samples = df.groupby(['model_family', 'model', 'language', 'dataset_group'])['num_samples'].first().reset_index()
|
| 892 |
+
model_samples = unique_samples.groupby(['model_family', 'model'])['num_samples'].sum().reset_index()
|
| 893 |
+
model_samples.columns = ['model_family', 'model', 'num_samples']
|
| 894 |
+
model_agg = model_agg.merge(model_samples, on=['model_family', 'model'], how='left')
|
| 895 |
+
|
| 896 |
+
# Get parameter count for each individual model
|
| 897 |
+
model_agg['params'] = model_agg['model'].apply(
|
| 898 |
+
lambda m: MODEL_PARAMETER_COUNTS.get(m, 500_000_000) # Default 500M
|
| 899 |
+
)
|
| 900 |
+
model_agg['params_billions'] = model_agg['params'] / 1_000_000_000
|
| 901 |
+
model_agg['params_display'] = model_agg['params'].apply(
|
| 902 |
+
lambda x: f"{x/1_000_000_000:.1f}B" if x >= 1_000_000_000 else f"{x/1_000_000:.0f}M"
|
| 903 |
+
)
|
| 904 |
+
|
| 905 |
+
# Create short model name for display
|
| 906 |
+
model_agg['model_short'] = model_agg['model'].apply(
|
| 907 |
+
lambda x: x.split('/')[-1] if '/' in x else x
|
| 908 |
+
)
|
| 909 |
+
|
| 910 |
+
fig = go.Figure()
|
| 911 |
+
|
| 912 |
+
# Add scatter traces for each model family
|
| 913 |
+
for family in model_agg['model_family'].unique():
|
| 914 |
+
family_data = model_agg[model_agg['model_family'] == family]
|
| 915 |
+
family_color = _get_color_for_family(family)
|
| 916 |
+
fig.add_trace(go.Scatter(
|
| 917 |
+
x=family_data['wer'],
|
| 918 |
+
y=family_data['rtfx'],
|
| 919 |
+
mode='markers',
|
| 920 |
+
name=family,
|
| 921 |
+
marker=dict(
|
| 922 |
+
size=family_data['params'] / family_data['params'].max() * 50 + 10,
|
| 923 |
+
sizemode='diameter',
|
| 924 |
+
sizemin=8,
|
| 925 |
+
color=family_color
|
| 926 |
+
),
|
| 927 |
+
customdata=family_data[['model_short', 'cer', 'num_samples', 'params_display']].values,
|
| 928 |
+
hovertemplate=(
|
| 929 |
+
'<b>%{customdata[0]}</b><br><br>' +
|
| 930 |
+
'<b>WER:</b> %{x:.3f}<br>' +
|
| 931 |
+
'<b>RTFx:</b> %{y:.1f}<br>' +
|
| 932 |
+
'<b>CER:</b> %{customdata[1]:.3f}<br>' +
|
| 933 |
+
'<b>Samples:</b> %{customdata[2]:,}<br>' +
|
| 934 |
+
'<b>Parameters:</b> %{customdata[3]}<extra></extra>'
|
| 935 |
+
)
|
| 936 |
+
))
|
| 937 |
+
|
| 938 |
+
title_text = "Speed vs Accuracy Tradeoff by Individual Model"
|
| 939 |
+
else:
|
| 940 |
+
# Aggregate by model family
|
| 941 |
+
model_agg = df.groupby('model_family').agg({
|
| 942 |
+
'wer': 'median',
|
| 943 |
+
'rtfx': 'median',
|
| 944 |
+
'cer': 'median',
|
| 945 |
+
'model': 'first' # Get a representative model name for parameter lookup
|
| 946 |
+
}).reset_index()
|
| 947 |
+
|
| 948 |
+
# Get unique sample counts per model family
|
| 949 |
+
unique_samples = df.groupby(['model_family', 'language', 'dataset_group'])['num_samples'].first().reset_index()
|
| 950 |
+
family_samples = unique_samples.groupby('model_family')['num_samples'].sum().reset_index()
|
| 951 |
+
family_samples.columns = ['model_family', 'num_samples']
|
| 952 |
+
model_agg = model_agg.merge(family_samples, on='model_family', how='left')
|
| 953 |
+
|
| 954 |
+
# For model family view, use a uniform size (no bubble size variation)
|
| 955 |
+
# Use a constant for params to make bubbles the same size
|
| 956 |
+
model_agg['params'] = 1_000_000_000 # Use constant 1B for uniform bubble size
|
| 957 |
+
model_agg['params_display'] = 'N/A' # Not applicable in family view
|
| 958 |
+
|
| 959 |
+
fig = go.Figure()
|
| 960 |
+
|
| 961 |
+
# Add scatter traces for each model family
|
| 962 |
+
for family in model_agg['model_family'].unique():
|
| 963 |
+
family_data = model_agg[model_agg['model_family'] == family]
|
| 964 |
+
family_color = _get_color_for_family(family)
|
| 965 |
+
fig.add_trace(go.Scatter(
|
| 966 |
+
x=family_data['wer'],
|
| 967 |
+
y=family_data['rtfx'],
|
| 968 |
+
mode='markers',
|
| 969 |
+
name=family,
|
| 970 |
+
marker=dict(
|
| 971 |
+
size=20,
|
| 972 |
+
sizemode='diameter',
|
| 973 |
+
color=family_color
|
| 974 |
+
),
|
| 975 |
+
customdata=family_data[['cer', 'num_samples']].values,
|
| 976 |
+
hovertemplate=(
|
| 977 |
+
'<b>' + family + '</b><br><br>' +
|
| 978 |
+
'<b>WER:</b> %{x:.3f}<br>' +
|
| 979 |
+
'<b>RTFx:</b> %{y:.1f}<br>' +
|
| 980 |
+
'<b>CER:</b> %{customdata[0]:.3f}<br>' +
|
| 981 |
+
'<b>Samples:</b> %{customdata[1]:,}<extra></extra>'
|
| 982 |
+
)
|
| 983 |
+
))
|
| 984 |
+
|
| 985 |
+
title_text = "Speed vs Accuracy Tradeoff by Model Family"
|
| 986 |
+
|
| 987 |
+
# Add quadrant lines at median
|
| 988 |
+
median_wer = model_agg['wer'].median()
|
| 989 |
+
median_rtfx = model_agg['rtfx'].median()
|
| 990 |
+
|
| 991 |
+
fig.add_hline(y=median_rtfx, line_dash="dash", line_color="gray", annotation_text="Median RTFx", annotation_position="right")
|
| 992 |
+
fig.add_vline(x=median_wer, line_dash="dash", line_color="gray", annotation_text="Median WER", annotation_position="top")
|
| 993 |
+
|
| 994 |
+
# Add quadrant label - centered in the "Fast & Accurate" quadrant (low WER, high RTFx)
|
| 995 |
+
# The ideal quadrant is: x from min to median_wer, y from median_rtfx to max
|
| 996 |
+
quadrant_center_x = (model_agg['wer'].min() + median_wer) / 2
|
| 997 |
+
quadrant_center_y = (median_rtfx + model_agg['rtfx'].max()) / 2
|
| 998 |
+
|
| 999 |
+
fig.add_annotation(
|
| 1000 |
+
x=quadrant_center_x,
|
| 1001 |
+
y=quadrant_center_y,
|
| 1002 |
+
text="Fast & Accurate ⭐",
|
| 1003 |
+
showarrow=False,
|
| 1004 |
+
font=dict(size=12, color="green", family="Arial Black")
|
| 1005 |
+
)
|
| 1006 |
+
|
| 1007 |
+
fig.update_layout(
|
| 1008 |
+
title=title_text,
|
| 1009 |
+
xaxis_title="WER",
|
| 1010 |
+
yaxis_title="RTFx",
|
| 1011 |
+
yaxis=dict(rangemode='tozero'), # Ensure y-axis starts at 0 (RTFx can't be negative)
|
| 1012 |
+
height=550,
|
| 1013 |
+
autosize=True,
|
| 1014 |
+
template='plotly_white',
|
| 1015 |
+
legend=dict(
|
| 1016 |
+
orientation="h",
|
| 1017 |
+
yanchor="top",
|
| 1018 |
+
y=-0.15,
|
| 1019 |
+
xanchor="center",
|
| 1020 |
+
x=0.5,
|
| 1021 |
+
font=dict(size=10)
|
| 1022 |
+
),
|
| 1023 |
+
margin=dict(l=60, r=20, t=50, b=120)
|
| 1024 |
+
)
|
| 1025 |
+
|
| 1026 |
+
return fig
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
def create_wer_cer_correlation(df: pd.DataFrame, languages: list[str] | None = None, top_n_models: int | None = None) -> go.Figure:
|
| 1030 |
+
"""
|
| 1031 |
+
Visualization 7: WER vs CER Correlation
|
| 1032 |
+
Scatter plot showing the relationship between word and character error rates.
|
| 1033 |
+
Defaults to Swahili if no language is specified.
|
| 1034 |
+
Outliers are removed for cleaner visualization.
|
| 1035 |
+
|
| 1036 |
+
Args:
|
| 1037 |
+
df: DataFrame with evaluation results
|
| 1038 |
+
languages: List of languages to filter by (defaults to ["Swahili"] if None)
|
| 1039 |
+
top_n_models: If specified, only show top N models by WER (0 = show all)
|
| 1040 |
+
"""
|
| 1041 |
+
# Default to Swahili if no language filter provided
|
| 1042 |
+
if not languages:
|
| 1043 |
+
languages = ["Swahili"]
|
| 1044 |
+
|
| 1045 |
+
# Apply language filter
|
| 1046 |
+
filtered_df = df.copy()
|
| 1047 |
+
filtered_df = filtered_df[filtered_df['language'].isin(languages)]
|
| 1048 |
+
|
| 1049 |
+
# Remove WER outliers for cleaner visualization
|
| 1050 |
+
filtered_df = _remove_wer_outliers(filtered_df)
|
| 1051 |
+
|
| 1052 |
+
# Apply top N models filter if specified
|
| 1053 |
+
if top_n_models and top_n_models > 0:
|
| 1054 |
+
model_wer = filtered_df.groupby('model')['wer'].median().sort_values()
|
| 1055 |
+
top_models = model_wer.head(top_n_models).index.tolist()
|
| 1056 |
+
filtered_df = filtered_df[filtered_df['model'].isin(top_models)]
|
| 1057 |
+
|
| 1058 |
+
fig = go.Figure()
|
| 1059 |
+
|
| 1060 |
+
# Add scatter traces for each model family
|
| 1061 |
+
for family in filtered_df['model_family'].unique():
|
| 1062 |
+
family_data = filtered_df[filtered_df['model_family'] == family]
|
| 1063 |
+
family_color = _get_color_for_family(family)
|
| 1064 |
+
# Normalize size for better visualization
|
| 1065 |
+
max_samples = filtered_df['num_samples'].max() if not filtered_df.empty else 1
|
| 1066 |
+
sizes = (family_data['num_samples'] / max_samples * 25 + 5).values if not family_data.empty else [10]
|
| 1067 |
+
|
| 1068 |
+
fig.add_trace(go.Scatter(
|
| 1069 |
+
x=family_data['wer'],
|
| 1070 |
+
y=family_data['cer'],
|
| 1071 |
+
mode='markers',
|
| 1072 |
+
name=family,
|
| 1073 |
+
marker=dict(
|
| 1074 |
+
size=sizes,
|
| 1075 |
+
sizemode='diameter',
|
| 1076 |
+
sizemin=5,
|
| 1077 |
+
opacity=0.6,
|
| 1078 |
+
color=family_color
|
| 1079 |
+
),
|
| 1080 |
+
customdata=family_data[['language', 'model', 'dataset_group', 'num_samples']].values,
|
| 1081 |
+
hovertemplate=(
|
| 1082 |
+
'<b>%{customdata[0]}</b><br><br>' +
|
| 1083 |
+
'<b>Model:</b> %{customdata[1]}<br>' +
|
| 1084 |
+
'<b>Dataset:</b> %{customdata[2]}<br>' +
|
| 1085 |
+
'<b>WER:</b> %{x:.3f}<br>' +
|
| 1086 |
+
'<b>CER:</b> %{y:.3f}<br>' +
|
| 1087 |
+
'<b>Samples:</b> %{customdata[3]:,}<extra></extra>'
|
| 1088 |
+
)
|
| 1089 |
+
))
|
| 1090 |
+
|
| 1091 |
+
# Add trendline
|
| 1092 |
+
if len(filtered_df) > 1:
|
| 1093 |
+
import numpy as np
|
| 1094 |
+
z = np.polyfit(filtered_df['wer'], filtered_df['cer'], 1)
|
| 1095 |
+
p = np.poly1d(z)
|
| 1096 |
+
x_range = np.linspace(filtered_df['wer'].min(), filtered_df['wer'].max(), 100)
|
| 1097 |
+
fig.add_trace(go.Scatter(
|
| 1098 |
+
x=x_range,
|
| 1099 |
+
y=p(x_range),
|
| 1100 |
+
mode='lines',
|
| 1101 |
+
name='Trend',
|
| 1102 |
+
line=dict(color='gray', dash='dash'),
|
| 1103 |
+
hoverinfo='skip'
|
| 1104 |
+
))
|
| 1105 |
+
|
| 1106 |
+
# Calculate correlation
|
| 1107 |
+
correlation = filtered_df[['wer', 'cer']].corr().iloc[0, 1] if len(filtered_df) > 1 else 0
|
| 1108 |
+
|
| 1109 |
+
# Build title with language info
|
| 1110 |
+
lang_str = ", ".join(languages[:3]) + ("..." if len(languages) > 3 else "")
|
| 1111 |
+
title_text = f"WER vs CER Correlation for {lang_str} (r={correlation:.2f})"
|
| 1112 |
+
|
| 1113 |
+
fig.update_layout(
|
| 1114 |
+
title=title_text,
|
| 1115 |
+
xaxis_title="WER",
|
| 1116 |
+
yaxis_title="CER",
|
| 1117 |
+
height=550,
|
| 1118 |
+
autosize=True,
|
| 1119 |
+
template='plotly_white',
|
| 1120 |
+
legend=dict(
|
| 1121 |
+
orientation="h",
|
| 1122 |
+
yanchor="top",
|
| 1123 |
+
y=-0.15,
|
| 1124 |
+
xanchor="center",
|
| 1125 |
+
x=0.5,
|
| 1126 |
+
font=dict(size=10)
|
| 1127 |
+
),
|
| 1128 |
+
margin=dict(l=60, r=20, t=50, b=120),
|
| 1129 |
+
# Zoom X-axis to useful range (0-1.5)
|
| 1130 |
+
xaxis=dict(range=[0, 1.5]),
|
| 1131 |
+
yaxis=dict(range=[0, 1.5])
|
| 1132 |
+
)
|
| 1133 |
+
|
| 1134 |
+
return fig
|
| 1135 |
+
|
| 1136 |
+
|
| 1137 |
+
def create_model_consistency(df: pd.DataFrame) -> go.Figure:
|
| 1138 |
+
"""
|
| 1139 |
+
Visualization 9: Model Consistency Analysis
|
| 1140 |
+
Shows coefficient of variation (CV) to measure consistency across languages.
|
| 1141 |
+
Removes high outliers only using IQR method (keeps best performers).
|
| 1142 |
+
"""
|
| 1143 |
+
# Remove only HIGH outliers using IQR method on WER (keep best performers)
|
| 1144 |
+
Q1 = df['wer'].quantile(0.25)
|
| 1145 |
+
Q3 = df['wer'].quantile(0.75)
|
| 1146 |
+
IQR = Q3 - Q1
|
| 1147 |
+
upper_bound = Q3 + 1.5 * IQR
|
| 1148 |
+
df_no_outliers = df[df['wer'] <= upper_bound]
|
| 1149 |
+
|
| 1150 |
+
model_variance = df_no_outliers.groupby('model_family').agg({
|
| 1151 |
+
'wer': ['median', 'std', 'count']
|
| 1152 |
+
}).reset_index()
|
| 1153 |
+
|
| 1154 |
+
model_variance.columns = ['model_family', 'wer_median', 'wer_std', 'count']
|
| 1155 |
+
model_variance['cv'] = (model_variance['wer_std'] / model_variance['wer_median'] * 100)
|
| 1156 |
+
model_variance = model_variance.sort_values('cv')
|
| 1157 |
+
|
| 1158 |
+
# Get colors for each model family
|
| 1159 |
+
model_variance['color'] = model_variance['model_family'].apply(_get_color_for_family)
|
| 1160 |
+
|
| 1161 |
+
fig = go.Figure()
|
| 1162 |
+
|
| 1163 |
+
fig.add_trace(go.Bar(
|
| 1164 |
+
y=model_variance['model_family'],
|
| 1165 |
+
x=model_variance['cv'],
|
| 1166 |
+
orientation='h',
|
| 1167 |
+
marker=dict(
|
| 1168 |
+
color=model_variance['color']
|
| 1169 |
+
),
|
| 1170 |
+
text=model_variance['cv'].round(1),
|
| 1171 |
+
textposition='outside',
|
| 1172 |
+
hovertemplate=(
|
| 1173 |
+
'<b>%{y}</b><br><br>' +
|
| 1174 |
+
'<b>Coefficient of Variation:</b> %{x:.1f}%<br>' +
|
| 1175 |
+
'<b>Median WER:</b> %{customdata[0]:.3f}<br>' +
|
| 1176 |
+
'<b>Std Dev:</b> %{customdata[1]:.3f}<br>' +
|
| 1177 |
+
'<b>Evaluations:</b> %{customdata[2]}<extra></extra>'
|
| 1178 |
+
),
|
| 1179 |
+
customdata=model_variance[['wer_median', 'wer_std', 'count']]
|
| 1180 |
+
))
|
| 1181 |
+
|
| 1182 |
+
fig.update_layout(
|
| 1183 |
+
title="Model Consistency Ranking (Outliers Removed)",
|
| 1184 |
+
xaxis_title="Coefficient of Variation (%)",
|
| 1185 |
+
yaxis_title="Model Family",
|
| 1186 |
+
height=550,
|
| 1187 |
+
template='plotly_white',
|
| 1188 |
+
showlegend=False,
|
| 1189 |
+
margin=dict(l=200, r=100, t=80, b=80)
|
| 1190 |
+
)
|
| 1191 |
+
|
| 1192 |
+
return fig
|
| 1193 |
+
|