ardaatahan commited on
Commit
79fc12a
·
0 Parent(s):

initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OS generated files
2
+ .DS_Store
3
+ Thumbs.db
4
+
5
+ # Environment files
6
+ *.env
7
+ .env
8
+
9
+ # Python virtual environment
10
+ venv/
11
+ env/
12
+ *.pyc
13
+ __pycache__/
14
+
15
+ # Hugging Face related
16
+ .huggingface
17
+
18
+ # Project specific
19
+ argmaxinc/
20
+ table_data.json
21
+
22
+ # Jupyter Notebook
23
+ .ipynb_checkpoints
24
+
25
+ # PyCharm
26
+ .idea/
27
+
28
+ # VS Code
29
+ .vscode/
30
+
31
+ # Gradio temporary files
32
+ gradio_cached_examples/
33
+
34
+ # Logs
35
+ *.log
36
+
37
+ # Dependency directories
38
+ node_modules/
39
+
40
+ # Distribution / packaging
41
+ dist/
42
+ build/
43
+ *.egg-info/
44
+
45
+ # Temporary files
46
+ *.tmp
47
+ *.bak
48
+ *.swp
49
+
50
+ # Dataset files (if you don't want to track them)
51
+ *.jsonl
52
+
53
+ # Model files (if you don't want to track them)
54
+ *.pth
55
+ *.h5
56
+ *.ckpt
57
+
58
+ .gradio/
.pre-commit-config.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pycqa/isort
3
+ rev: 5.13.2
4
+ hooks:
5
+ - id: isort
6
+ args: ["--profile", "black"]
7
+
8
+ - repo: https://github.com/psf/black
9
+ rev: 23.3.0
10
+ hooks:
11
+ - id: black
12
+ name: black
13
+ language: python
14
+
15
+ - repo: https://github.com/pre-commit/pre-commit-hooks
16
+ rev: v4.5.0
17
+ hooks:
18
+ - id: end-of-file-fixer
Makefile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .PHONY: format use-local-data
2
+
3
+ format:
4
+ @pre-commit run --all-files
5
+
6
+ use-local-data:
7
+ @python performance_generate.py
README.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: WhisperKit Android Benchmarks
3
+ emoji: 🏆
4
+ colorFrom: green
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ app_file: main.py
8
+ license: mit
9
+ ---
10
+
11
+ ## Prerequisites
12
+
13
+ Ensure you have the following software installed:
14
+
15
+ - Python 3.10 or higher
16
+ - pip (Python package installer)
17
+
18
+ ## Installation
19
+
20
+ 1. **Clone the repository**:
21
+
22
+ ```sh
23
+ git clone https://github.com/argmaxinc/model-performance-dashboard.git
24
+ cd model-performance-dashboard
25
+ ```
26
+
27
+ 2. **Create a virtual environment**:
28
+
29
+ ```sh
30
+ python -m venv venv
31
+ source venv/bin/activate
32
+ ```
33
+
34
+ 3. **Install required packages**:
35
+ ```sh
36
+ pip install -r requirements.txt
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ 1. **Run the application**:
42
+
43
+ ```sh
44
+ gradio main.py
45
+ ```
46
+
47
+ 2. **Access the application**:
48
+ After running main.py, a local server will start, and you will see an interface URL in the terminal. Open the URL in your web browser to interact with Argmax Android Benchmark dashboard.
49
+
50
+ ## Data Generation
51
+
52
+ 1. **Performance Data Update (performance_generate.py)**:
53
+ - Downloads benchmark data from [WhisperKit Evals Dataset](https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset).
54
+ - Processes the data to extract performance metrics for various models, devices, and operating systems.
55
+ - Calculates metrics such as speed, tokens per second for long and short-form data.
56
+ - Saves the results in `performance_data.json` and `support_data.csv`.
57
+
58
+ ## Data Update
59
+
60
+ To update the dashboard with latest data from our HuggingFace datasets, run:
61
+
62
+ ```sh
63
+ make use-huggingface-data
64
+ ```
65
+
66
+ Alternatively, you can use our on-device testing code [TODO:INSERT_LINK_TO_OS_TEST_CODE] on your device to update the dashboard with your own data. After generating the Xcode data, place the resulting `.json` files in the `whisperkit-evals/xcresults/benchmark_data` directory, then run:
67
+
68
+ ```sh
69
+ make use-local-data
70
+ ```
constants.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from textwrap import dedent
2
+
3
+ BANNER_TEXT = """
4
+ <div style="text-align: center;">
5
+ <h1><a href='https://github.com/argmaxinc/WhisperKitAndroid'>WhisperKit Android Benchmarks</a></h1>
6
+ </div>
7
+ """
8
+
9
+
10
+ INTRO_LABEL = """We present comprehensive benchmarks for WhisperKit Android, our on-device ASR solution for Android devices, compared against a reference implementation. These benchmarks aim to help developers and enterprises make informed decisions when choosing optimized or compressed variants of machine learning models for production use. Show more."""
11
+
12
+
13
+ INTRO_TEXT = """
14
+ <h3 style="display: flex;
15
+ justify-content: center;
16
+ align-items: center;
17
+ "></h2>
18
+ \n📈 Key Metrics:
19
+ Word Error Rate (WER) (⬇️): The percentage of words incorrectly transcribed. Lower is better.
20
+ Quality of Inference (QoI) (⬆️): Percentage of examples where WhisperKit Android performs no worse than the reference model. Higher is better.
21
+ Tokens per Second (⬆️): The number of output tokens generated per second. Higher is better.
22
+ Speed (⬆️): Input audio seconds transcribed per second. Higher is better.
23
+ 🎯 WhisperKi Android is evaluated across different datasets, with a focus on per-example no-regressions (QoI) and overall accuracy (WER).
24
+ \n💻 Our benchmarks include:
25
+ Reference: <a href='https://platform.openai.com/docs/guides/speech-to-text'>WhisperOpenAIAPI</a> (OpenAI's Whisper API)
26
+ On-device: <a href='https://github.com/argmaxinc/WhisperKitAndroid'>WhisperKit Android</a> (various versions and optimizations)
27
+ ℹ️ Reference Implementation:
28
+ <a href='https://platform.openai.com/docs/guides/speech-to-text'>WhisperOpenAIAPI</a> sets the reference standard. We assume it uses the equivalent of openai/whisper-large-v2 in float16 precision, along with additional undisclosed optimizations from OpenAI. As of 02/29/24, it costs $0.36 per hour of audio and has a 25MB file size limit per request.
29
+ \n🔍 We use two primary datasets:
30
+ <a href='https://huggingface.co/datasets/argmaxinc/librispeech'>LibriSpeech</a>: ~5 hours of short English audio clips
31
+ <a href='https://huggingface.co/datasets/argmaxinc/earnings22'>Earnings22</a>: ~120 hours of English audio from earnings calls
32
+ 🔄 Results are periodically updated using our automated evaluation pipeline on Apple Silicon Macs.
33
+ \n🛠️ Developers can use <a href='https://github.com/argmaxinc/WhisperKitAndroid'>WhisperKit Android</a> to reproduce these results or run evaluations on their own custom datasets.
34
+ 🔗 Links:
35
+ - <a href='https://github.com/argmaxinc/WhisperKit Android'>WhisperKit Android</a>
36
+ - <a href='https://github.com/argmaxinc/whisperkittools'>whisperkittools</a>
37
+ - <a href='https://huggingface.co/datasets/argmaxinc/librispeech'>LibriSpeech</a>
38
+ - <a href='https://huggingface.co/datasets/argmaxinc/earnings22'>Earnings22</a>
39
+ - <a href='https://platform.openai.com/docs/guides/speech-to-text'>WhisperOpenAIAPI</a>
40
+ """
41
+
42
+
43
+ METHODOLOGY_TEXT = dedent(
44
+ """
45
+ # Methodology
46
+ ## Overview
47
+ WhisperKit Android Benchmarks is the one-stop shop for on-device performance and quality testing of WhisperKit Android models across supported devices, OS versions and audio datasets.
48
+ ## Metrics
49
+ - **Speed factor** (⬆️): Computed as the ratio of input audio length to end-to-end WhisperKit Android latency for transcribing that audio. A speed factor of N means N seconds of input audio was transcribed in 1 second.
50
+ - **Tok/s (Tokens per second)** (⬆️): Total number of text decoder forward passes divided by the end-to-end processing time.
51
+ - This metric varies with input data given that the pace of speech changes the text decoder % of overall latency. This metric should not be confused with the reciprocal of the text decoder latency which is constant across input files.
52
+ - **WER (Word Error Rate)** (⬇️): The ratio of words incorrectly transcribed when comparing the model's output to reference transcriptions, with lower values indicating better accuracy.
53
+ - **QoI (Quality of Inference)** (⬆️): The ratio of examples where WhisperKit Android performs no worse than the reference model.
54
+ - This metric does not capture improvements to the reference. It only measures potential regressions.
55
+
56
+ ## Data
57
+ - **Short-form**: 10 minutes of English audiobook clips with 30s/clip comprising a subset of the [librispeech test set](https://huggingface.co/datasets/argmaxinc/librispeech). Proxy for average streaming performance.
58
+ - **Long-form**: 10 minutes of earnings call recordings in English. Built from the [earnings22 test set](https://huggingface.co/datasets/argmaxinc/earnings22-12hours). Proxy for average from-file performance.
59
+ - Full datasets are used for English Quality tests and random 10-minute subsets are used for Performance tests.
60
+ ## Performance Measurement
61
+ 1. On-device testing is conducted with [WhisperKit Android Tests](https://github.com/argmaxinc/WhisperKitAndroid) on Android devices, across different Android versions.
62
+ 2. Performance is recorded on 10-minute datasets described above for short- and long-form
63
+ 3. Quality metrics are recorded on 10-minute datasets using an Apple M2 Pro CPU on a Linux host to allow for fast processing of many configurations and providing a consistent, high-performance baseline for all evaluations displayed in the English Quality tab.
64
+ 4. Results are aggregated and presented in the dashboard, allowing for easy comparison and analysis.
65
+ ## Dashboard Features
66
+ - Performance: Interactive filtering by model, device, OS, and performance metrics
67
+ - Timeline: Visualizations of performance trends
68
+ - English Quality: English transcription quality on short- and long-form audio
69
+ - Device Support: Matrix of supported device, OS and model version combinations. Unsupported combinations are marked with :warning:.
70
+ - This methodology ensures a comprehensive and fair evaluation of speech recognition models supported by WhisperKit Android across a wide range of scenarios and use cases.
71
+ """
72
+ )
73
+
74
+ PERFORMANCE_TEXT = dedent(
75
+ """
76
+ ## Metrics
77
+ - **Speed factor** (⬆️): Computed as the ratio of input audio length to end-to-end WhisperKit Android latency for transcribing that audio. A speed factor of N means N seconds of input audio was transcribed in 1 second.
78
+ - **Tok/s (Tokens per second)** (⬆️): Total number of text decoder forward passes divided by the end-to-end processing time.
79
+ ## Data
80
+ - **Short-form**: 5 hours of English audiobook clips with 30s/clip comprising the [librispeech test set](https://huggingface.co/datasets/argmaxinc/librispeech).
81
+ - **Long-form**: 12 hours of earnings call recordings with ~1hr/clip in English with various accents. Built by randomly selecting 10% of the [earnings22 test set](https://huggingface.co/datasets/argmaxinc/earnings22-12hours).
82
+ """
83
+ )
84
+
85
+ QUALITY_TEXT = dedent(
86
+ """
87
+ ## Metrics
88
+ - **WER (Word Error Rate)** (⬇️): The ratio of words incorrectly transcribed when comparing the model's output to reference transcriptions, with lower values indicating better accuracy.
89
+ - **QoI (Quality of Inference)** (⬆️): The ratio of examples where WhisperKit Android performs no worse than the reference model.
90
+ - This metric does not capture improvements to the reference. It only measures potential regressions.
91
+ """
92
+ )
93
+
94
+ COL_NAMES = {
95
+ "model.model_version": "Model",
96
+ "device.product_name": "Device",
97
+ "device.os": "OS",
98
+ "average_wer": "Average WER",
99
+ "qoi": "QoI",
100
+ "speed": "Speed",
101
+ "tokens_per_second": "Tok / s",
102
+ "model": "Model",
103
+ "device": "Device",
104
+ "os": "OS",
105
+ "english_wer": "English WER",
106
+ "multilingual_wer": "Multilingual WER",
107
+ }
108
+
109
+
110
+ CITATION_BUTTON_LABEL = "Copy the following snippet to cite these results"
111
+
112
+
113
+ CITATION_BUTTON_TEXT = r"""@misc{whisperkit-android-argmax,
114
+ title = {WhisperKit Android},
115
+ author = {Argmax, Inc.},
116
+ year = {2024},
117
+ URL = {https://github.com/argmaxinc/WhisperKitAndroid}
118
+ }"""
119
+
120
+
121
+ HEADER = """<div align="center">
122
+ <div position: relative>
123
+ <img
124
+ src="data:image/png;base64,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"
125
+ style="display:block;width:7%;height:auto;"
126
+ />
127
+ </div>
128
+ </div>"""
129
+
130
+
131
+ EARNINGS22_URL = (
132
+ "https://huggingface.co/datasets/argmaxinc/earnings22-debug/resolve/main/{0}"
133
+ )
134
+ LIBRISPEECH_URL = (
135
+ "https://huggingface.co/datasets/argmaxinc/librispeech-debug/resolve/main/{0}"
136
+ )
137
+
138
+ AUDIO_URL = (
139
+ "https://huggingface.co/datasets/argmaxinc/whisperkit-test-data/resolve/main/"
140
+ )
141
+
142
+ WHISPER_OPEN_AI_LINK = "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/tree/main/WhisperKit/{}/{}"
143
+
144
+ BASE_WHISPERKIT_BENCHMARK_URL = "https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data"
dashboard_data/device_map.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "24291524202091": "Zebra TC73 (SM7325)",
3
+ "b56a7242": "Xiaomi 12 Pro (SM8450)",
4
+ "kjx4ojmvy9izizfu": "Redmi 13 (MT6769)",
5
+ "R5CR2085YMZ": "Samsung Galaxy S21 (SM8350)",
6
+ "R5CW12SACFF": "Samsung Galaxy XCover6 Pro (SM7325)",
7
+ "R5CW82D0AEX": "Samsung Galaxy S23 Ultra (SM8550)",
8
+ "R5CX41ZYDJN": "Samsung Galaxy S24 Ultra (SM8650)",
9
+ "R8YX604R0CB": "Samsung Galaxy A04e (MT6765)",
10
+ "R9YT60YH77J": "Samsung Galaxy Tab A8 (T618)",
11
+ "ZY22JRLRR8": "Motorola Moto E13 (T606)"
12
+ }
dashboard_data/performance_data.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"model": "openai/whisper-base", "device": "Samsung Galaxy S23 Ultra (SM8550)", "os": "Android 14", "timestamp": "2025-01-16-17-22-40-PM", "speed": 4.89, "tokens_per_second": 29.21, "dataset_speed": {"librispeech-10mins": 3.65, "earnings22-10mins": 7.07}, "dataset_tokens_per_second": {"librispeech-10mins": 30.38, "earnings22-10mins": 27.86}, "average_wer": 14.95, "qoi": 0.38, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
2
+ {"model": "openai/whisper-base", "device": "Samsung Galaxy S24 Ultra (SM8650)", "os": "Android 14", "timestamp": "2025-01-16-14-40-52-PM", "speed": 5.83, "tokens_per_second": 36.51, "dataset_speed": {"earnings22-10mins": 9.0, "librispeech-10mins": 4.2}, "dataset_tokens_per_second": {"earnings22-10mins": 36.31, "librispeech-10mins": 36.67}, "average_wer": 14.95, "qoi": 0.38, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
3
+ {"model": "openai/whisper-small", "device": "Samsung Galaxy S24 Ultra (SM8650)", "os": "Android 14", "timestamp": "2025-01-16-15-06-31-PM", "speed": 4.74, "tokens_per_second": 38.47, "dataset_speed": {"earnings22-10mins": 8.61, "librispeech-10mins": 3.17}, "dataset_tokens_per_second": {"earnings22-10mins": 38.65, "librispeech-10mins": 38.33}, "average_wer": 12.29, "qoi": 0.49, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
4
+ {"model": "openai/whisper-tiny", "device": "Samsung Galaxy S23 Ultra (SM8550)", "os": "Android 14", "timestamp": "2025-01-16-17-34-12-PM", "speed": 14.67, "tokens_per_second": 257.86, "dataset_speed": {"librispeech-10mins": 8.88, "earnings22-10mins": 36.01}, "dataset_tokens_per_second": {"librispeech-10mins": 286.3, "earnings22-10mins": 229.95}, "average_wer": 21.68, "qoi": 0.3, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
5
+ {"model": "openai/whisper-small", "device": "Samsung Galaxy XCover6 Pro (SM7325)", "os": "Android 14", "timestamp": "2025-01-16-18-46-41-PM", "speed": 0.51, "tokens_per_second": 4.0, "dataset_speed": {"earnings22-10mins": 0.87, "librispeech-10mins": 0.35}, "dataset_tokens_per_second": {"earnings22-10mins": 4.2, "librispeech-10mins": 3.86}, "average_wer": 7.8, "qoi": 0.51, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
6
+ {"model": "openai/whisper-small", "device": "Redmi 13 (MT6769)", "os": "Android 15", "timestamp": "2025-01-16-15-06-31-PM", "speed": 0.27, "tokens_per_second": 1.88, "dataset_speed": {"earnings22-10mins": 0.44, "librispeech-10mins": 0.19}, "dataset_tokens_per_second": {"earnings22-10mins": 1.94, "librispeech-10mins": 1.83}, "average_wer": 7.8, "qoi": 0.51, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
7
+ {"model": "openai/whisper-base", "device": "Redmi 13 (MT6769)", "os": "Android 15", "timestamp": "2025-01-16-14-40-52-PM", "speed": 0.73, "tokens_per_second": 4.31, "dataset_speed": {"earnings22-10mins": 1.21, "librispeech-10mins": 0.51}, "dataset_tokens_per_second": {"earnings22-10mins": 4.99, "librispeech-10mins": 3.88}, "average_wer": 10.74, "qoi": 0.43, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
8
+ {"model": "openai/whisper-small", "device": "Xiaomi 12 Pro (SM8450)", "os": "Android 14", "timestamp": "2025-01-16-16-32-12-PM", "speed": 3.51, "tokens_per_second": 26.98, "dataset_speed": {"librispeech-10mins": 2.36, "earnings22-10mins": 6.28}, "dataset_tokens_per_second": {"librispeech-10mins": 26.15, "earnings22-10mins": 28.15}, "average_wer": 13.15, "qoi": 0.48, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
9
+ {"model": "openai/whisper-tiny", "device": "Samsung Galaxy S24 Ultra (SM8650)", "os": "Android 14", "timestamp": "2025-01-16-14-26-27-PM", "speed": 15.63, "tokens_per_second": 303.32, "dataset_speed": {"earnings22-10mins": 47.28, "librispeech-10mins": 9.01}, "dataset_tokens_per_second": {"earnings22-10mins": 269.96, "librispeech-10mins": 334.2}, "average_wer": 21.99, "qoi": 0.3, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
10
+ {"model": "openai/whisper-base", "device": "Xiaomi 12 Pro (SM8450)", "os": "Android 14", "timestamp": "2025-01-16-17-22-40-PM", "speed": 3.47, "tokens_per_second": 55.93, "dataset_speed": {"librispeech-10mins": 2.24, "earnings22-10mins": 6.9}, "dataset_tokens_per_second": {"librispeech-10mins": 56.37, "earnings22-10mins": 55.46}, "average_wer": 26.48, "qoi": 0.37, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
11
+ {"model": "openai/whisper-tiny", "device": "Redmi 13 (MT6769)", "os": "Android 15", "timestamp": "2025-01-16-14-26-27-PM", "speed": 1.55, "tokens_per_second": 9.19, "dataset_speed": {"earnings22-10mins": 2.38, "librispeech-10mins": 1.12}, "dataset_tokens_per_second": {"earnings22-10mins": 9.9, "librispeech-10mins": 8.67}, "average_wer": 14.39, "qoi": 0.36, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
12
+ {"model": "openai/whisper-tiny", "device": "Xiaomi 12 Pro (SM8450)", "os": "Android 14", "timestamp": "2025-01-16-17-34-12-PM", "speed": 13.48, "tokens_per_second": 213.32, "dataset_speed": {"librispeech-10mins": 8.24, "earnings22-10mins": 31.96}, "dataset_tokens_per_second": {"librispeech-10mins": 235.5, "earnings22-10mins": 191.21}, "average_wer": 21.54, "qoi": 0.3, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
13
+ {"model": "openai/whisper-base", "device": "Samsung Galaxy XCover6 Pro (SM7325)", "os": "Android 14", "timestamp": "2025-01-16-18-07-06-PM", "speed": 1.45, "tokens_per_second": 8.88, "dataset_speed": {"earnings22-10mins": 2.48, "librispeech-10mins": 1.0}, "dataset_tokens_per_second": {"earnings22-10mins": 10.94, "librispeech-10mins": 7.7}, "average_wer": 10.74, "qoi": 0.43, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
14
+ {"model": "openai/whisper-tiny", "device": "Samsung Galaxy XCover6 Pro (SM7325)", "os": "Android 14", "timestamp": "2025-01-16-17-45-00-PM", "speed": 2.51, "tokens_per_second": 14.75, "dataset_speed": {"earnings22-10mins": 3.85, "librispeech-10mins": 1.81}, "dataset_tokens_per_second": {"earnings22-10mins": 15.32, "librispeech-10mins": 14.29}, "average_wer": 14.39, "qoi": 0.36, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
15
+ {"model": "openai/whisper-small", "device": "Samsung Galaxy S23 Ultra (SM8550)", "os": "Android 14", "timestamp": "2025-01-16-15-06-31-PM", "speed": 3.97, "tokens_per_second": 31.98, "dataset_speed": {"earnings22-10mins": 6.93, "librispeech-10mins": 2.7}, "dataset_tokens_per_second": {"earnings22-10mins": 30.89, "librispeech-10mins": 32.88}, "average_wer": 12.25, "qoi": 0.49, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
16
+ {"model": "openai/whisper-base", "device": "Motorola Moto E13 (T606)", "os": "Android 13", "timestamp": "2025-01-16-14-40-52-PM", "speed": 0.73, "tokens_per_second": 4.1, "dataset_speed": {"earnings22-10mins": 1.0, "librispeech-10mins": 0.56}, "dataset_tokens_per_second": {"earnings22-10mins": 3.87, "librispeech-10mins": 4.3}, "average_wer": 10.74, "qoi": 0.43, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
17
+ {"model": "openai/whisper-tiny", "device": "Motorola Moto E13 (T606)", "os": "Android 13", "timestamp": "2025-01-16-14-26-27-PM", "speed": 1.38, "tokens_per_second": 7.4, "dataset_speed": {"earnings22-10mins": 1.8, "librispeech-10mins": 1.1}, "dataset_tokens_per_second": {"earnings22-10mins": 6.92, "librispeech-10mins": 7.86}, "average_wer": 14.39, "qoi": 0.36, "commit_hash": "6962d0d", "commit_timestamp": "2024-10-25T012729"}
dashboard_data/quality_data.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {"model": "openai/whisper-tiny", "timestamp": "linux_openai_whisper-tiny_librispeech-10mins_2025-01-13-15-19-17-PM", "average_wer": 14.47, "dataset_wer": {"earnings22-10mins": 16.13, "librispeech-10mins": 12.82}, "qoi": 0.36}
2
+ {"model": "openai/whisper-small", "timestamp": "linux_openai_whisper-small_librispeech-10mins_2025-01-13-15-48-15-PM", "average_wer": 7.8, "dataset_wer": {"earnings22-10mins": 7.85, "librispeech-10mins": 7.76}, "qoi": 0.51}
3
+ {"model": "openai/whisper-base", "timestamp": "linux_openai_whisper-base_librispeech-10mins_2025-01-13-15-31-01-PM", "average_wer": 10.77, "dataset_wer": {"earnings22-10mins": 11.48, "librispeech-10mins": 10.06}, "qoi": 0.43}
dashboard_data/support_data.csv ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ,Model,"""Motorola Moto E13 (T606)""","""Redmi 13 (MT6769)""","""Samsung Galaxy S23 Ultra (SM8550)""","""Samsung Galaxy S24 Ultra (SM8650)""","""Samsung Galaxy XCover6 Pro (SM7325)""","""Xiaomi 12 Pro (SM8450)"""
2
+ openai/whisper-base,openai/whisper-base,✅ Android 13,✅ Android 15,✅ Android 14,✅ Android 14,✅ Android 14,✅ Android 14
3
+ openai/whisper-small,openai/whisper-small,Not Supported,✅ Android 15,✅ Android 14,✅ Android 14,✅ Android 14,✅ Android 14
4
+ openai/whisper-tiny,openai/whisper-tiny,✅ Android 13,✅ Android 15,✅ Android 14,✅ Android 14,✅ Android 14,✅ Android 14
main.py ADDED
@@ -0,0 +1,1139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main module for the WhisperKit Evaluation Dashboard.
3
+ This module sets up and runs the Gradio interface for the WhisperKit Evaluation Dashboard,
4
+ allowing users to explore and compare speech recognition model performance across different
5
+ devices, operating systems, and datasets.
6
+ """
7
+
8
+ import os
9
+ from math import ceil, floor
10
+ import re
11
+
12
+ import gradio as gr
13
+ import pandas as pd
14
+ from argmax_gradio_components import RangeSlider
15
+ from dotenv import load_dotenv
16
+ from huggingface_hub import login
17
+
18
+ # Import custom constants and utility functions
19
+ from constants import (
20
+ BANNER_TEXT,
21
+ CITATION_BUTTON_LABEL,
22
+ CITATION_BUTTON_TEXT,
23
+ COL_NAMES,
24
+ HEADER,
25
+ METHODOLOGY_TEXT,
26
+ PERFORMANCE_TEXT,
27
+ QUALITY_TEXT,
28
+ )
29
+ from utils import (
30
+ add_datasets_to_performance_columns,
31
+ add_datasets_to_quality_columns,
32
+ create_initial_performance_column_dict,
33
+ create_initial_quality_column_dict,
34
+ css,
35
+ fields,
36
+ get_os_name_and_version,
37
+ make_dataset_wer_clickable_link,
38
+ make_model_name_clickable_link,
39
+ plot_metric,
40
+ read_json_line_by_line,
41
+ )
42
+
43
+ # Load environment variables
44
+ load_dotenv()
45
+
46
+ # Get the Hugging Face token from the environment variable
47
+ HF_TOKEN = os.getenv("HF_TOKEN")
48
+
49
+ # Use the token for login
50
+ login(token=HF_TOKEN, add_to_git_credential=True)
51
+
52
+ # Define repository and directory information
53
+ repo_id = "argmaxinc/whisperkit-evals-dataset"
54
+ directory = "xcresults/benchmark_results"
55
+ local_dir = ""
56
+
57
+ # Load benchmark data from JSON files
58
+ PERFORMANCE_DATA = read_json_line_by_line("dashboard_data/performance_data.json")
59
+ QUALITY_DATA = read_json_line_by_line("dashboard_data/quality_data.json")
60
+
61
+ # Convert JSON data to pandas DataFrames
62
+ quality_df = pd.json_normalize(QUALITY_DATA)
63
+ benchmark_df = pd.json_normalize(PERFORMANCE_DATA)
64
+
65
+ # Process timestamp data
66
+ def safe_parse_datetime(x):
67
+ try:
68
+ return pd.to_datetime(x, format="%Y-%m-%d-%H-%M-%S-%p")
69
+ except (ValueError, TypeError):
70
+ return pd.NaT # Return Not-a-Time for invalid dates
71
+
72
+ benchmark_df["timestamp"] = benchmark_df["timestamp"].apply(safe_parse_datetime).dt.tz_localize(None)
73
+ quality_df["timestamp"] = quality_df["timestamp"].apply(safe_parse_datetime).dt.tz_localize(None)
74
+
75
+ # First create a temporary column for model length
76
+ sorted_quality_df = (
77
+ quality_df.assign(model_len=quality_df["model"].str.len())
78
+ .sort_values(
79
+ by=["model_len", "model", "timestamp"],
80
+ ascending=[True, True, False],
81
+ )
82
+ .drop(columns=["model_len"])
83
+ .drop_duplicates(subset=["model"], keep="first")
84
+ .reset_index(drop=True)
85
+ )
86
+
87
+ sorted_performance_df = (
88
+ benchmark_df.assign(model_len=benchmark_df["model"].str.len())
89
+ .sort_values(
90
+ by=["model_len", "model", "device", "os", "timestamp"],
91
+ ascending=[True, True, True, True, False],
92
+ )
93
+ .drop(columns=["model_len"])
94
+ .drop_duplicates(subset=["model", "device", "os"], keep="first")
95
+ .reset_index(drop=True)
96
+ )
97
+
98
+ # Identify dataset-specific columns
99
+ dataset_wer_columns = [
100
+ col for col in sorted_quality_df.columns if col.startswith("dataset_wer.")
101
+ ]
102
+ dataset_speed_columns = [
103
+ col for col in sorted_performance_df.columns if col.startswith("dataset_speed.")
104
+ ]
105
+ dataset_toks_columns = [
106
+ col
107
+ for col in sorted_performance_df.columns
108
+ if col.startswith("dataset_tokens_per_second.")
109
+ ]
110
+
111
+ # Extract dataset names
112
+ QUALITY_DATASETS = [col.split(".")[-1] for col in dataset_wer_columns]
113
+ PERFORMANCE_DATASETS = [col.split(".")[-1] for col in dataset_speed_columns]
114
+
115
+ # Prepare DataFrames for display
116
+ model_df = sorted_quality_df[
117
+ ["model", "average_wer", "qoi", "timestamp"] + dataset_wer_columns
118
+ ]
119
+ performance_df = sorted_performance_df[
120
+ [
121
+ "model",
122
+ "device",
123
+ "os",
124
+ "average_wer",
125
+ "qoi",
126
+ "speed",
127
+ "tokens_per_second",
128
+ "timestamp",
129
+ ]
130
+ + dataset_speed_columns
131
+ + dataset_toks_columns
132
+ ].copy()
133
+
134
+ # Rename columns for clarity
135
+ performance_df = performance_df.rename(
136
+ lambda x: COL_NAMES[x] if x in COL_NAMES else x, axis="columns"
137
+ )
138
+ model_df = model_df.rename(
139
+ lambda x: COL_NAMES[x] if x in COL_NAMES else x, axis="columns"
140
+ )
141
+
142
+ # Process dataset-specific columns
143
+ for col in dataset_wer_columns:
144
+ dataset_name = col.split(".")[-1]
145
+ model_df = model_df.rename(columns={col: dataset_name})
146
+ # model_df[dataset_name] = model_df.apply(
147
+ # lambda x: make_dataset_wer_clickable_link(x, dataset_name), axis=1
148
+ # )
149
+
150
+ for col in dataset_speed_columns:
151
+ dataset_name = col.split(".")[-1]
152
+ performance_df = performance_df.rename(
153
+ columns={
154
+ col: f"{'Short-Form' if dataset_name == 'librispeech-10mins' else 'Long-Form'} Speed"
155
+ }
156
+ )
157
+
158
+ for col in dataset_toks_columns:
159
+ dataset_name = col.split(".")[-1]
160
+ performance_df = performance_df.rename(
161
+ columns={
162
+ col: f"{'Short-Form' if dataset_name == 'librispeech-10mins' else 'Long-Form'} Tok/s"
163
+ }
164
+ )
165
+
166
+ # Process model names for display
167
+ model_df["model_raw"] = model_df["Model"].copy()
168
+ performance_df["model_raw"] = performance_df["Model"].copy()
169
+ model_df["Model"] = model_df["Model"].apply(lambda x: make_model_name_clickable_link(x))
170
+ performance_df["Model"] = performance_df["Model"].apply(
171
+ lambda x: make_model_name_clickable_link(x)
172
+ )
173
+ performance_df["Average WER"] = performance_df["Average WER"].apply(
174
+ lambda x: x if x < 90 else f"""<p style='color: yellow'>{x}</p>"""
175
+ )
176
+
177
+ # Extract unique devices and OS versions
178
+ PERFORMANCE_DEVICES = performance_df["Device"].unique().tolist()
179
+ PERFORMANCE_OS = performance_df["OS"].apply(get_os_name_and_version).unique().tolist()
180
+ PERFORMANCE_OS.sort()
181
+
182
+ # Create initial column dictionaries and update with dataset information
183
+ initial_performance_column_dict = create_initial_performance_column_dict()
184
+ initial_quality_column_dict = create_initial_quality_column_dict()
185
+
186
+ performance_column_info = add_datasets_to_performance_columns(
187
+ initial_performance_column_dict, PERFORMANCE_DATASETS
188
+ )
189
+ quality_column_info = add_datasets_to_quality_columns(
190
+ initial_quality_column_dict, QUALITY_DATASETS
191
+ )
192
+
193
+ # Unpack the returned dictionaries
194
+ updated_performance_column_dict = performance_column_info["column_dict"]
195
+ updated_quality_column_dict = quality_column_info["column_dict"]
196
+
197
+ PerformanceAutoEvalColumn = performance_column_info["AutoEvalColumn"]
198
+ QualityAutoEvalColumn = quality_column_info["AutoEvalColumn"]
199
+
200
+ # Define column sets for different views
201
+ PERFORMANCE_COLS = performance_column_info["COLS"]
202
+ QUALITY_COLS = quality_column_info["COLS"]
203
+ PERFORMANCE_TYPES = performance_column_info["TYPES"]
204
+ QUALITY_TYPES = quality_column_info["TYPES"]
205
+ PERFORMANCE_ALWAYS_HERE_COLS = performance_column_info["ALWAYS_HERE_COLS"]
206
+ QUALITY_ALWAYS_HERE_COLS = quality_column_info["ALWAYS_HERE_COLS"]
207
+ PERFORMANCE_TOGGLE_COLS = performance_column_info["TOGGLE_COLS"]
208
+ QUALITY_TOGGLE_COLS = quality_column_info["TOGGLE_COLS"]
209
+ PERFORMANCE_SELECTED_COLS = performance_column_info["SELECTED_COLS"]
210
+ QUALITY_SELECTED_COLS = quality_column_info["SELECTED_COLS"]
211
+
212
+
213
+ def performance_filter(
214
+ df,
215
+ columns,
216
+ model_query,
217
+ exclude_models,
218
+ devices,
219
+ os,
220
+ short_speed_slider,
221
+ long_speed_slider,
222
+ short_toks_slider,
223
+ long_toks_slider,
224
+ ):
225
+ """
226
+ Filters the performance DataFrame based on specified criteria.
227
+ :param df: The DataFrame to be filtered.
228
+ :param columns: The columns to be included in the filtered DataFrame.
229
+ :param model_query: The query string to filter the 'Model' column.
230
+ :param exclude_models: Models to exclude from the results.
231
+ :param devices: The devices to filter the 'Device' column.
232
+ :param os: The list of operating systems to filter the 'OS' column.
233
+ :param short_speed_slider: The range of values to filter the 'Short-Form Speed' column.
234
+ :param long_speed_slider: The range of values to filter the 'Long-Form Speed' column.
235
+ :param short_toks_slider: The range of values to filter the 'Short-Form Tok/s' column.
236
+ :param long_toks_slider: The range of values to filter the 'Long-Form Tok/s' column.
237
+ :return: The filtered DataFrame.
238
+ """
239
+
240
+ # Select columns based on input and always-present columns
241
+ filtered_df = df[
242
+ PERFORMANCE_ALWAYS_HERE_COLS
243
+ + [c for c in PERFORMANCE_COLS if c in df.columns and c in columns]
244
+ ]
245
+
246
+ # Filter models based on query
247
+ if model_query:
248
+ filtered_df = filtered_df[
249
+ filtered_df["Model"].str.contains(
250
+ "|".join(q.strip() for q in model_query.split(";")), case=False
251
+ )
252
+ ]
253
+
254
+ # Exclude specified models
255
+ if exclude_models:
256
+ exclude_list = [m.strip() for m in exclude_models.split(";")]
257
+ filtered_df = filtered_df[
258
+ ~filtered_df["Model"].str.contains("|".join(exclude_list), case=False)
259
+ ]
260
+
261
+ # Filter by devices
262
+ if devices:
263
+ filtered_df = filtered_df[filtered_df["Device"].isin(devices)]
264
+ else:
265
+ filtered_df = pd.DataFrame(columns=filtered_df.columns)
266
+
267
+ # Filter by operating systems
268
+ filtered_df = (
269
+ filtered_df[
270
+ (
271
+ filtered_df["OS"].str.contains(
272
+ "|".join(q.strip() for q in os), case=False
273
+ )
274
+ )
275
+ ]
276
+ if os
277
+ else pd.DataFrame(columns=filtered_df.columns)
278
+ )
279
+
280
+ # Apply short-form and long-form speed and tokens per second filters
281
+ min_short_speed, max_short_speed = short_speed_slider
282
+ min_long_speed, max_long_speed = long_speed_slider
283
+ min_short_toks, max_short_toks = short_toks_slider
284
+ min_long_toks, max_long_toks = long_toks_slider
285
+
286
+ if "Short-Form Speed" in filtered_df.columns:
287
+ filtered_df = filtered_df[
288
+ ((filtered_df["Short-Form Speed"] >= min_short_speed)
289
+ & (filtered_df["Short-Form Speed"] <= max_short_speed))
290
+ | filtered_df["Short-Form Speed"].isna()
291
+ ]
292
+ if "Long-Form Speed" in filtered_df.columns:
293
+ filtered_df = filtered_df[
294
+ ((filtered_df["Long-Form Speed"] >= min_long_speed)
295
+ & (filtered_df["Long-Form Speed"] <= max_long_speed))
296
+ | filtered_df["Long-Form Speed"].isna()
297
+ ]
298
+ if "Short-Form Tok/s" in filtered_df.columns:
299
+ filtered_df = filtered_df[
300
+ ((filtered_df["Short-Form Tok/s"] >= min_short_toks)
301
+ & (filtered_df["Short-Form Tok/s"] <= max_short_toks))
302
+ | filtered_df["Short-Form Tok/s"].isna()
303
+ ]
304
+ if "Long-Form Tok/s" in filtered_df.columns:
305
+ filtered_df = filtered_df[
306
+ ((filtered_df["Long-Form Tok/s"] >= min_long_toks)
307
+ & (filtered_df["Long-Form Tok/s"] <= max_long_toks))
308
+ | filtered_df["Long-Form Tok/s"].isna()
309
+ ]
310
+
311
+ return filtered_df
312
+
313
+
314
+ def quality_filter(df, columns, model_query, wer_slider, qoi_slider, exclude_models):
315
+ """
316
+ Filters the quality DataFrame based on specified criteria.
317
+ :param df: The DataFrame to be filtered.
318
+ :param columns: The columns to be included in the filtered DataFrame.
319
+ :param model_query: The query string to filter the 'Model' column.
320
+ :param wer_slider: The range of values to filter the 'Average WER' column.
321
+ :param qoi_slider: The range of values to filter the 'QoI' column.
322
+ :param exclude_models: Models to exclude from the results.
323
+ :return: The filtered DataFrame.
324
+ """
325
+ # Select columns based on input and always-present columns
326
+ filtered_df = df[
327
+ QUALITY_ALWAYS_HERE_COLS
328
+ + [c for c in QUALITY_COLS if c in df.columns and c in columns]
329
+ ]
330
+
331
+ # Filter models based on query
332
+ if model_query:
333
+ filtered_df = filtered_df[
334
+ filtered_df["Model"].str.contains(
335
+ "|".join(q.strip() for q in model_query.split(";")), case=False
336
+ )
337
+ ]
338
+
339
+ # Exclude specified models
340
+ if exclude_models:
341
+ exclude_list = [m.strip() for m in exclude_models.split(";")]
342
+ filtered_df = filtered_df[
343
+ ~filtered_df["Model"].str.contains("|".join(exclude_list), case=False)
344
+ ]
345
+
346
+ # Apply WER and QoI filters
347
+ min_wer_slider, max_wer_slider = wer_slider
348
+ min_qoi_slider, max_qoi_slider = qoi_slider
349
+ if "Average WER" in filtered_df.columns:
350
+ filtered_df = filtered_df[
351
+ (filtered_df["Average WER"] >= min_wer_slider)
352
+ & (filtered_df["Average WER"] <= max_wer_slider)
353
+ ]
354
+ if "QoI" in filtered_df.columns:
355
+ filtered_df = filtered_df[
356
+ (filtered_df["QoI"] >= min_qoi_slider)
357
+ & (filtered_df["QoI"] <= max_qoi_slider)
358
+ ]
359
+
360
+ return filtered_df
361
+
362
+ diff_tab = gr.TabItem("Difference Checker", elem_id="diff_checker", id=2)
363
+ text_diff_elems = []
364
+
365
+ tabs = gr.Tabs(elem_id="tab-elems")
366
+
367
+ font = [
368
+ "Zwizz Regular", # Local font
369
+ "IBM Plex Mono", # Monospace font
370
+ "ui-sans-serif",
371
+ "system-ui",
372
+ "sans-serif",
373
+ ]
374
+
375
+ # Define the Gradio interface
376
+ with gr.Blocks(css=css, theme=gr.themes.Base(font=font)) as demo:
377
+ # Add header and banner to the interface
378
+ gr.HTML(HEADER)
379
+ gr.HTML(BANNER_TEXT, elem_classes="markdown-text")
380
+
381
+ # Create tabs for different sections of the dashboard
382
+ with tabs.render():
383
+ # Performance Tab
384
+ with gr.TabItem("Performance", elem_id="benchmark", id=0):
385
+ with gr.Row():
386
+ with gr.Column(scale=1):
387
+ with gr.Row():
388
+ with gr.Column(scale=6, elem_classes="filter_models_column"):
389
+ filter_performance_models = gr.Textbox(
390
+ placeholder="🔍 Filter Model (separate multiple queries with ';')",
391
+ label="Filter Models",
392
+ )
393
+ with gr.Column(scale=4, elem_classes="exclude_models_column"):
394
+ exclude_performance_models = gr.Textbox(
395
+ placeholder="🔍 Exclude Model",
396
+ label="Exclude Model",
397
+ )
398
+ with gr.Row():
399
+ with gr.Accordion("See All Columns", open=False):
400
+ with gr.Row():
401
+ with gr.Column(scale=9, elem_id="performance_columns"):
402
+ performance_shown_columns = gr.CheckboxGroup(
403
+ choices=PERFORMANCE_TOGGLE_COLS,
404
+ value=PERFORMANCE_SELECTED_COLS,
405
+ label="Toggle Columns",
406
+ elem_id="column-select",
407
+ interactive=True,
408
+ )
409
+ with gr.Column(
410
+ scale=1,
411
+ min_width=200,
412
+ elem_id="performance_select_columns",
413
+ ):
414
+ with gr.Row():
415
+ select_all_button = gr.Button(
416
+ "Select All",
417
+ elem_id="select-all-button",
418
+ interactive=True,
419
+ )
420
+ deselect_all_button = gr.Button(
421
+ "Deselect All",
422
+ elem_id="deselect-all-button",
423
+ interactive=True,
424
+ )
425
+
426
+ def select_all_columns():
427
+ return PERFORMANCE_TOGGLE_COLS
428
+
429
+ def deselect_all_columns():
430
+ return []
431
+
432
+ select_all_button.click(
433
+ select_all_columns,
434
+ inputs=[],
435
+ outputs=performance_shown_columns,
436
+ )
437
+ deselect_all_button.click(
438
+ deselect_all_columns,
439
+ inputs=[],
440
+ outputs=performance_shown_columns,
441
+ )
442
+
443
+ with gr.Row():
444
+ with gr.Accordion("Filter Devices", open=False):
445
+ with gr.Row():
446
+ with gr.Column(
447
+ scale=9, elem_id="filter_devices_column"
448
+ ):
449
+ performance_shown_devices = gr.CheckboxGroup(
450
+ choices=PERFORMANCE_DEVICES,
451
+ value=PERFORMANCE_DEVICES,
452
+ label="Filter Devices",
453
+ interactive=True,
454
+ )
455
+ with gr.Column(
456
+ scale=1,
457
+ min_width=200,
458
+ elem_id="filter_select_devices",
459
+ ):
460
+ with gr.Row():
461
+ select_all_devices_button = gr.Button(
462
+ "Select All",
463
+ elem_id="select-all-devices-button",
464
+ interactive=True,
465
+ )
466
+ deselect_all_devices_button = gr.Button(
467
+ "Deselect All",
468
+ elem_id="deselect-all-devices-button",
469
+ interactive=True,
470
+ )
471
+
472
+ def select_all_devices():
473
+ return PERFORMANCE_DEVICES
474
+
475
+ def deselect_all_devices():
476
+ return []
477
+
478
+ select_all_devices_button.click(
479
+ select_all_devices,
480
+ inputs=[],
481
+ outputs=performance_shown_devices,
482
+ )
483
+ deselect_all_devices_button.click(
484
+ deselect_all_devices,
485
+ inputs=[],
486
+ outputs=performance_shown_devices,
487
+ )
488
+ with gr.Row():
489
+ performance_shown_os = gr.CheckboxGroup(
490
+ choices=PERFORMANCE_OS,
491
+ value=PERFORMANCE_OS,
492
+ label="Filter OS",
493
+ interactive=True,
494
+ )
495
+ with gr.Column(scale=1):
496
+ with gr.Accordion("See Performance Filters"):
497
+ with gr.Row():
498
+ with gr.Row():
499
+ min_short_speed, max_short_speed = floor(
500
+ min(performance_df["Short-Form Speed"])
501
+ ), ceil(max(performance_df["Short-Form Speed"]))
502
+ short_speed_slider = RangeSlider(
503
+ value=[min_short_speed, max_short_speed],
504
+ minimum=min_short_speed,
505
+ maximum=max_short_speed,
506
+ step=0.001,
507
+ label="Short-Form Speed",
508
+ )
509
+ with gr.Row():
510
+ min_long_speed, max_long_speed = floor(
511
+ performance_df["Long-Form Speed"].dropna().min()
512
+ ), ceil(performance_df["Long-Form Speed"].dropna().max())
513
+ long_speed_slider = RangeSlider(
514
+ value=[min_long_speed, max_long_speed],
515
+ minimum=min_long_speed,
516
+ maximum=max_long_speed,
517
+ step=0.001,
518
+ label="Long-Form Speed",
519
+ )
520
+ with gr.Row():
521
+ with gr.Row():
522
+ min_short_toks, max_short_toks = floor(
523
+ min(performance_df["Short-Form Tok/s"])
524
+ ), ceil(max(performance_df["Short-Form Tok/s"]))
525
+ short_toks_slider = RangeSlider(
526
+ value=[min_short_toks, max_short_toks],
527
+ minimum=min_short_toks,
528
+ maximum=max_short_toks,
529
+ step=0.001,
530
+ label="Short-Form Tok/s",
531
+ )
532
+ with gr.Row():
533
+ min_long_toks, max_long_toks = floor(
534
+ performance_df["Long-Form Tok/s"].dropna().min()
535
+ ), ceil(performance_df["Long-Form Tok/s"].dropna().max())
536
+ long_toks_slider = RangeSlider(
537
+ value=[min_long_toks, max_long_toks],
538
+ minimum=min_long_toks,
539
+ maximum=max_long_toks,
540
+ step=0.001,
541
+ label="Long-Form Tok/s",
542
+ )
543
+ with gr.Row():
544
+ gr.Markdown(PERFORMANCE_TEXT, elem_classes="markdown-text")
545
+ with gr.Row():
546
+ leaderboard_df = gr.components.Dataframe(
547
+ value=performance_df[
548
+ PERFORMANCE_ALWAYS_HERE_COLS + performance_shown_columns.value
549
+ ],
550
+ headers=[
551
+ PERFORMANCE_ALWAYS_HERE_COLS + performance_shown_columns.value
552
+ ],
553
+ datatype=[
554
+ c.type
555
+ for c in fields(PerformanceAutoEvalColumn)
556
+ if c.name in PERFORMANCE_COLS
557
+ ],
558
+ elem_id="leaderboard-table",
559
+ elem_classes="large-table",
560
+ interactive=False,
561
+ )
562
+
563
+ # Copy of the leaderboard dataframe to apply filters to
564
+ hidden_leaderboard_df = gr.components.Dataframe(
565
+ value=performance_df,
566
+ headers=PERFORMANCE_COLS,
567
+ datatype=[
568
+ c.type
569
+ for c in fields(PerformanceAutoEvalColumn)
570
+ if c.name in PERFORMANCE_COLS
571
+ ],
572
+ visible=False,
573
+ )
574
+
575
+ # Inputs for the dataframe filter function
576
+ performance_filter_inputs = [
577
+ hidden_leaderboard_df,
578
+ performance_shown_columns,
579
+ filter_performance_models,
580
+ exclude_performance_models,
581
+ performance_shown_devices,
582
+ performance_shown_os,
583
+ short_speed_slider,
584
+ long_speed_slider,
585
+ short_toks_slider,
586
+ long_toks_slider,
587
+ ]
588
+
589
+ filter_output = leaderboard_df
590
+ filter_performance_models.change(
591
+ performance_filter, performance_filter_inputs, filter_output
592
+ )
593
+ exclude_performance_models.change(
594
+ performance_filter, performance_filter_inputs, filter_output
595
+ )
596
+ performance_shown_columns.change(
597
+ performance_filter, performance_filter_inputs, filter_output
598
+ )
599
+ performance_shown_devices.change(
600
+ performance_filter, performance_filter_inputs, filter_output
601
+ )
602
+ performance_shown_os.change(
603
+ performance_filter, performance_filter_inputs, filter_output
604
+ )
605
+ short_speed_slider.change(
606
+ performance_filter, performance_filter_inputs, filter_output
607
+ )
608
+ long_speed_slider.change(
609
+ performance_filter, performance_filter_inputs, filter_output
610
+ )
611
+ short_toks_slider.change(
612
+ performance_filter, performance_filter_inputs, filter_output
613
+ )
614
+ long_toks_slider.change(
615
+ performance_filter, performance_filter_inputs, filter_output
616
+ )
617
+
618
+ with gr.TabItem("English Quality", elem_id="timeline", id=1):
619
+ with gr.Row():
620
+ with gr.Column(scale=1):
621
+ with gr.Row():
622
+ with gr.Column(scale=6, elem_classes="filter_models_column"):
623
+ filter_quality_models = gr.Textbox(
624
+ placeholder="🔍 Filter Model (separate multiple queries with ';')",
625
+ label="Filter Models",
626
+ )
627
+ with gr.Column(scale=4, elem_classes="exclude_models_column"):
628
+ exclude_quality_models = gr.Textbox(
629
+ placeholder="🔍 Exclude Model",
630
+ label="Exclude Model",
631
+ )
632
+ with gr.Row():
633
+ with gr.Accordion("See All Columns", open=False):
634
+ quality_shown_columns = gr.CheckboxGroup(
635
+ choices=QUALITY_TOGGLE_COLS,
636
+ value=QUALITY_SELECTED_COLS,
637
+ label="Toggle Columns",
638
+ elem_id="column-select",
639
+ interactive=True,
640
+ )
641
+ with gr.Column(scale=1):
642
+ with gr.Accordion("See Quality Filters"):
643
+ with gr.Row():
644
+ with gr.Row():
645
+ quality_min_avg_wer, quality_max_avg_wer = (
646
+ floor(min(model_df["Average WER"])),
647
+ ceil(max(model_df["Average WER"])) + 1,
648
+ )
649
+ wer_slider = RangeSlider(
650
+ value=[quality_min_avg_wer, quality_max_avg_wer],
651
+ minimum=quality_min_avg_wer,
652
+ maximum=quality_max_avg_wer,
653
+ label="Average WER",
654
+ )
655
+ with gr.Row():
656
+ quality_min_qoi, quality_max_qoi = floor(
657
+ min(model_df["QoI"])
658
+ ), ceil(max(model_df["QoI"] + 1))
659
+ qoi_slider = RangeSlider(
660
+ value=[quality_min_qoi, quality_max_qoi],
661
+ minimum=quality_min_qoi,
662
+ maximum=quality_max_qoi,
663
+ label="QoI",
664
+ )
665
+ with gr.Row():
666
+ gr.Markdown(QUALITY_TEXT)
667
+ with gr.Row():
668
+ quality_leaderboard_df = gr.components.Dataframe(
669
+ value=model_df[
670
+ QUALITY_ALWAYS_HERE_COLS + quality_shown_columns.value
671
+ ],
672
+ headers=[QUALITY_ALWAYS_HERE_COLS + quality_shown_columns.value],
673
+ datatype=[
674
+ c.type
675
+ for c in fields(QualityAutoEvalColumn)
676
+ if c.name in QUALITY_COLS
677
+ ],
678
+ elem_id="leaderboard-table",
679
+ elem_classes="large-table",
680
+ interactive=False,
681
+ )
682
+
683
+ # Copy of the leaderboard dataframe to apply filters to
684
+ hidden_quality_leaderboard_df = gr.components.Dataframe(
685
+ value=model_df,
686
+ headers=QUALITY_COLS,
687
+ datatype=[
688
+ c.type
689
+ for c in fields(QualityAutoEvalColumn)
690
+ if c.name in QUALITY_COLS
691
+ ],
692
+ visible=False,
693
+ )
694
+
695
+ # Inputs for the dataframe filter function
696
+ filter_inputs = [
697
+ hidden_quality_leaderboard_df,
698
+ quality_shown_columns,
699
+ filter_quality_models,
700
+ wer_slider,
701
+ qoi_slider,
702
+ exclude_quality_models,
703
+ ]
704
+ filter_output = quality_leaderboard_df
705
+ filter_quality_models.change(
706
+ quality_filter, filter_inputs, filter_output
707
+ )
708
+ exclude_quality_models.change(
709
+ quality_filter, filter_inputs, filter_output
710
+ )
711
+ quality_shown_columns.change(
712
+ quality_filter, filter_inputs, filter_output
713
+ )
714
+ wer_slider.change(quality_filter, filter_inputs, filter_output)
715
+ qoi_slider.change(quality_filter, filter_inputs, filter_output)
716
+
717
+ # Timeline Tab
718
+ with gr.TabItem("Timeline", elem_id="timeline", id=4):
719
+ # Create subtabs for different metrics
720
+ with gr.Tabs():
721
+ with gr.TabItem("QoI", id=0):
722
+ with gr.Row():
723
+ with gr.Column(scale=6):
724
+ filter_qoi = gr.Textbox(
725
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
726
+ label="Filter",
727
+ )
728
+ with gr.Column(scale=4):
729
+ exclude_qoi = gr.Textbox(
730
+ placeholder="🔍 Exclude Model-Device-OS",
731
+ label="Exclude",
732
+ )
733
+ with gr.Row():
734
+ with gr.Column():
735
+ qoi_plot = gr.Plot(container=True)
736
+ demo.load(
737
+ lambda x, y, z: plot_metric(
738
+ x,
739
+ "qoi",
740
+ "QoI",
741
+ "QoI Over Time for Model-Device-OS Combinations",
742
+ y,
743
+ z,
744
+ ),
745
+ [
746
+ gr.Dataframe(benchmark_df, visible=False),
747
+ filter_qoi,
748
+ exclude_qoi,
749
+ ],
750
+ qoi_plot,
751
+ )
752
+ filter_qoi.change(
753
+ lambda x, y, z: plot_metric(
754
+ x,
755
+ "qoi",
756
+ "QoI",
757
+ "QoI Over Time for Model-Device-OS Combinations",
758
+ y,
759
+ z,
760
+ ),
761
+ [
762
+ gr.Dataframe(benchmark_df, visible=False),
763
+ filter_qoi,
764
+ exclude_qoi,
765
+ ],
766
+ qoi_plot,
767
+ )
768
+ exclude_qoi.change(
769
+ lambda x, y, z: plot_metric(
770
+ x,
771
+ "qoi",
772
+ "QoI",
773
+ "QoI Over Time for Model-Device-OS Combinations",
774
+ y,
775
+ z,
776
+ ),
777
+ [
778
+ gr.Dataframe(benchmark_df, visible=False),
779
+ filter_qoi,
780
+ exclude_qoi,
781
+ ],
782
+ qoi_plot,
783
+ )
784
+
785
+ with gr.TabItem("Average WER", id=1):
786
+ with gr.Row():
787
+ with gr.Column(scale=6):
788
+ filter_average_wer = gr.Textbox(
789
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
790
+ label="Filter",
791
+ )
792
+ with gr.Column(scale=4):
793
+ exclude_average_wer = gr.Textbox(
794
+ placeholder="🔍 Exclude Model-Device-OS",
795
+ label="Exclude",
796
+ )
797
+ with gr.Row():
798
+ with gr.Column():
799
+ average_wer_plot = gr.Plot(container=True)
800
+ demo.load(
801
+ lambda x, y, z: plot_metric(
802
+ x,
803
+ "average_wer",
804
+ "Average WER",
805
+ "Average WER Over Time for Model-Device-OS Combinations",
806
+ y,
807
+ z,
808
+ ),
809
+ [
810
+ gr.Dataframe(benchmark_df, visible=False),
811
+ filter_average_wer,
812
+ exclude_average_wer,
813
+ ],
814
+ average_wer_plot,
815
+ )
816
+ filter_average_wer.change(
817
+ lambda x, y, z: plot_metric(
818
+ x,
819
+ "average_wer",
820
+ "Average WER",
821
+ "Average WER Over Time for Model-Device-OS Combinations",
822
+ y,
823
+ z,
824
+ ),
825
+ [
826
+ gr.Dataframe(benchmark_df, visible=False),
827
+ filter_average_wer,
828
+ exclude_average_wer,
829
+ ],
830
+ average_wer_plot,
831
+ )
832
+ exclude_average_wer.change(
833
+ lambda x, y, z: plot_metric(
834
+ x,
835
+ "average_wer",
836
+ "Average WER",
837
+ "Average WER Over Time for Model-Device-OS Combinations",
838
+ y,
839
+ z,
840
+ ),
841
+ [
842
+ gr.Dataframe(benchmark_df, visible=False),
843
+ filter_average_wer,
844
+ exclude_average_wer,
845
+ ],
846
+ average_wer_plot,
847
+ )
848
+
849
+ with gr.TabItem("Speed", id=2):
850
+ with gr.Row():
851
+ with gr.Column(scale=6):
852
+ filter_speed = gr.Textbox(
853
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
854
+ label="Filter",
855
+ )
856
+ with gr.Column(scale=4):
857
+ exclude_speed = gr.Textbox(
858
+ placeholder="🔍 Exclude Model-Device-OS",
859
+ label="Exclude",
860
+ )
861
+ with gr.Row():
862
+ with gr.Column():
863
+ speed_plot = gr.Plot(container=True)
864
+ demo.load(
865
+ lambda x, y, z: plot_metric(
866
+ x,
867
+ "speed",
868
+ "Speed",
869
+ "Speed Over Time for Model-Device-OS Combinations",
870
+ y,
871
+ z,
872
+ ),
873
+ [
874
+ gr.Dataframe(benchmark_df, visible=False),
875
+ filter_speed,
876
+ exclude_speed,
877
+ ],
878
+ speed_plot,
879
+ )
880
+ filter_speed.change(
881
+ lambda x, y, z: plot_metric(
882
+ x,
883
+ "speed",
884
+ "Speed",
885
+ "Speed Over Time for Model-Device-OS Combinations",
886
+ y,
887
+ z,
888
+ ),
889
+ [
890
+ gr.Dataframe(benchmark_df, visible=False),
891
+ filter_speed,
892
+ exclude_speed,
893
+ ],
894
+ speed_plot,
895
+ )
896
+ exclude_speed.change(
897
+ lambda x, y, z: plot_metric(
898
+ x,
899
+ "speed",
900
+ "Speed",
901
+ "Speed Over Time for Model-Device-OS Combinations",
902
+ y,
903
+ z,
904
+ ),
905
+ [
906
+ gr.Dataframe(benchmark_df, visible=False),
907
+ filter_speed,
908
+ exclude_speed,
909
+ ],
910
+ speed_plot,
911
+ )
912
+
913
+ with gr.TabItem("Tok/s", id=3):
914
+ with gr.Row():
915
+ with gr.Column(scale=6):
916
+ filter_toks = gr.Textbox(
917
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
918
+ label="Filter",
919
+ )
920
+ with gr.Column(scale=4):
921
+ exclude_toks = gr.Textbox(
922
+ placeholder="🔍 Exclude Model-Device-OS",
923
+ label="Exclude",
924
+ )
925
+ with gr.Row():
926
+ with gr.Column():
927
+ toks_plot = gr.Plot(container=True)
928
+ demo.load(
929
+ lambda x, y, z: plot_metric(
930
+ x,
931
+ "tokens_per_second",
932
+ "Tok/s",
933
+ "Tok/s Over Time for Model-Device-OS Combinations",
934
+ y,
935
+ z,
936
+ ),
937
+ [
938
+ gr.Dataframe(benchmark_df, visible=False),
939
+ filter_toks,
940
+ exclude_toks,
941
+ ],
942
+ toks_plot,
943
+ )
944
+ filter_toks.change(
945
+ lambda x, y, z: plot_metric(
946
+ x,
947
+ "tokens_per_second",
948
+ "Tok/s",
949
+ "Tok/s Over Time for Model-Device-OS Combinations",
950
+ y,
951
+ z,
952
+ ),
953
+ [
954
+ gr.Dataframe(benchmark_df, visible=False),
955
+ filter_toks,
956
+ exclude_toks,
957
+ ],
958
+ toks_plot,
959
+ )
960
+ exclude_toks.change(
961
+ lambda x, y, z: plot_metric(
962
+ x,
963
+ "tokens_per_second",
964
+ "Tok/s",
965
+ "Tok/s Over Time for Model-Device-OS Combinations",
966
+ y,
967
+ z,
968
+ ),
969
+ [
970
+ gr.Dataframe(benchmark_df, visible=False),
971
+ filter_toks,
972
+ exclude_toks,
973
+ ],
974
+ toks_plot,
975
+ )
976
+
977
+ # Device Support Tab
978
+ with gr.TabItem("Device Support", elem_id="device_support", id=6):
979
+ # Load device support data from CSV
980
+ support_data = pd.read_csv("dashboard_data/support_data.csv")
981
+ support_data.set_index(support_data.columns[0], inplace=True)
982
+ support_data["Model"] = support_data["Model"].apply(
983
+ lambda x: x.replace("_", "/")
984
+ )
985
+ support_data["Model"] = support_data["Model"].apply(
986
+ lambda x: make_model_name_clickable_link(x)
987
+ )
988
+ support_data = (
989
+ support_data.assign(model_len=support_data["Model"].str.len())
990
+ .sort_values(
991
+ by=["model_len"],
992
+ ascending=[True],
993
+ )
994
+ .drop(columns=["model_len"])
995
+ )
996
+
997
+ with gr.Row():
998
+ with gr.Column(scale=1):
999
+ with gr.Row():
1000
+ with gr.Column(scale=6, elem_id="filter_models_column"):
1001
+ filter_support_models = gr.Textbox(
1002
+ placeholder="🔍 Filter Model (separate multiple queries with ';')",
1003
+ label="Filter Models",
1004
+ )
1005
+ with gr.Column(scale=4, elem_classes="exclude_models_column"):
1006
+ exclude_support_models = gr.Textbox(
1007
+ placeholder="🔍 Exclude Model",
1008
+ label="Exclude Model",
1009
+ )
1010
+ with gr.Row():
1011
+ with gr.Accordion("See All Columns", open=False):
1012
+ with gr.Row():
1013
+ with gr.Column(scale=9):
1014
+ support_shown_columns = gr.CheckboxGroup(
1015
+ choices=support_data.columns.tolist()[
1016
+ 1:
1017
+ ], # Exclude 'Model' column
1018
+ value=support_data.columns.tolist()[1:],
1019
+ label="Toggle Columns",
1020
+ elem_id="support-column-select",
1021
+ interactive=True,
1022
+ )
1023
+ with gr.Column(scale=1, min_width=200):
1024
+ with gr.Row():
1025
+ select_all_support_button = gr.Button(
1026
+ "Select All",
1027
+ elem_id="select-all-support-button",
1028
+ interactive=True,
1029
+ )
1030
+ deselect_all_support_button = gr.Button(
1031
+ "Deselect All",
1032
+ elem_id="deselect-all-support-button",
1033
+ interactive=True,
1034
+ )
1035
+ with gr.Column():
1036
+ gr.Markdown(
1037
+ """
1038
+ ### Legend
1039
+ - ✅ Supported: The model is supported and tested on this device.
1040
+ - ⚠️ Failed: Some tests failed on this device.
1041
+ """
1042
+ )
1043
+
1044
+ # Display device support data in a table
1045
+ device_support_table = gr.Dataframe(
1046
+ value=support_data,
1047
+ headers=support_data.columns.tolist(),
1048
+ datatype=["html" for _ in support_data.columns],
1049
+ elem_id="device-support-table",
1050
+ elem_classes="large-table",
1051
+ interactive=False,
1052
+ )
1053
+
1054
+ # Hidden dataframe to store the original data
1055
+ hidden_support_df = gr.Dataframe(value=support_data, visible=False)
1056
+
1057
+ def filter_support_data(df, columns, model_query, exclude_models):
1058
+ filtered_df = df.copy()
1059
+
1060
+ # Filter models based on query
1061
+ if model_query:
1062
+ filtered_df = filtered_df[
1063
+ filtered_df["Model"].str.contains(
1064
+ "|".join(q.strip() for q in model_query.split(";")),
1065
+ case=False,
1066
+ regex=True,
1067
+ )
1068
+ ]
1069
+
1070
+ # Exclude specified models
1071
+ if exclude_models:
1072
+ exclude_list = [
1073
+ re.escape(m.strip()) for m in exclude_models.split(";")
1074
+ ]
1075
+ filtered_df = filtered_df[
1076
+ ~filtered_df["Model"].str.contains(
1077
+ "|".join(exclude_list), case=False, regex=True
1078
+ )
1079
+ ]
1080
+
1081
+ # Select columns
1082
+ selected_columns = ["Model"] + [
1083
+ col for col in columns if col in df.columns
1084
+ ]
1085
+ filtered_df = filtered_df[selected_columns]
1086
+
1087
+ return filtered_df
1088
+
1089
+ def select_all_support_columns():
1090
+ return support_data.columns.tolist()[1:] # Exclude 'Model' column
1091
+
1092
+ def deselect_all_support_columns():
1093
+ return []
1094
+
1095
+ # Connect the filter function to the input components
1096
+ filter_inputs = [
1097
+ hidden_support_df,
1098
+ support_shown_columns,
1099
+ filter_support_models,
1100
+ exclude_support_models,
1101
+ ]
1102
+ filter_support_models.change(
1103
+ filter_support_data, filter_inputs, device_support_table
1104
+ )
1105
+ exclude_support_models.change(
1106
+ filter_support_data, filter_inputs, device_support_table
1107
+ )
1108
+ support_shown_columns.change(
1109
+ filter_support_data, filter_inputs, device_support_table
1110
+ )
1111
+
1112
+ # Connect select all and deselect all buttons
1113
+ select_all_support_button.click(
1114
+ select_all_support_columns,
1115
+ inputs=[],
1116
+ outputs=support_shown_columns,
1117
+ )
1118
+ deselect_all_support_button.click(
1119
+ deselect_all_support_columns,
1120
+ inputs=[],
1121
+ outputs=support_shown_columns,
1122
+ )
1123
+
1124
+ # Methodology Tab
1125
+ with gr.TabItem("Methodology", elem_id="methodology", id=7):
1126
+ gr.Markdown(METHODOLOGY_TEXT, elem_id="methodology-text")
1127
+
1128
+ # Citation section
1129
+ with gr.Accordion("📙 Citation", open=False):
1130
+ citation_button = gr.Textbox(
1131
+ value=CITATION_BUTTON_TEXT,
1132
+ label=CITATION_BUTTON_LABEL,
1133
+ lines=7,
1134
+ elem_id="citation-button",
1135
+ show_copy_button=True,
1136
+ )
1137
+
1138
+ # Launch the Gradio interface
1139
+ demo.launch(debug=True, share=True, ssr_mode=False)
performance_generate.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import shutil
4
+ import sys
5
+ from collections import defaultdict
6
+ from statistics import mean
7
+
8
+ import pandas as pd
9
+ import requests
10
+
11
+ from constants import BASE_WHISPERKIT_BENCHMARK_URL
12
+ from text_normalizer import text_normalizer
13
+ from utils import compute_average_wer, download_dataset
14
+
15
+
16
+ def fetch_evaluation_data(url):
17
+ """
18
+ Fetches evaluation data from the given URL.
19
+ :param url: The URL to fetch the evaluation data from.
20
+ :returns: The evaluation data as a dictionary.
21
+ :rauses: sys.exit if the request fails
22
+ """
23
+ response = requests.get(url)
24
+ if response.status_code == 200:
25
+ return json.loads(response.text)
26
+ else:
27
+ sys.exit(f"Failed to fetch WhisperKit evals: {response.text}")
28
+
29
+
30
+ def process_benchmark_file(file_path, dataset_dfs, device_map, results):
31
+ """
32
+ Processes a single benchmark file and updates the results dictionary.
33
+ :param file_path: Path to the benchmark JSON file.
34
+ :param dataset_dfs: Dictionary of DataFrames containing dataset information.
35
+ :param results: Dictionary to store the processed results.
36
+ This function reads a benchmark JSON file, extracts relevant information,
37
+ and updates the results dictionary with various metrics including WER,
38
+ speed, tokens per second, and quality of inference (QoI).
39
+ """
40
+ with open(file_path, "r") as file:
41
+ test_results = json.load(file)
42
+
43
+ if len(test_results) == 0:
44
+ return
45
+
46
+ commit_hash_timestamp = file_path.split("/")[-2]
47
+ commit_timestamp, commit_hash = commit_hash_timestamp.split("_")
48
+
49
+ first_test_result = test_results[0]
50
+ if first_test_result is None:
51
+ return
52
+
53
+ filename = file_path.split("/")[-1].strip(".json")
54
+ device, company, model, dataset_dir, timestamp = filename.split("_")
55
+ model = f"{company}_{model}"
56
+
57
+ if device not in device_map:
58
+ return
59
+
60
+ device = device_map[device]
61
+ os_info = first_test_result["staticAttributes"]["os"]
62
+
63
+ key = (model, device, os_info, commit_timestamp)
64
+ dataset_name = dataset_dir
65
+ for test_result in test_results:
66
+ if test_result is None:
67
+ continue
68
+
69
+ test_info = test_result["testInfo"]
70
+ audio_file_name = test_info["audioFile"]
71
+
72
+ dataset_df = dataset_dfs[dataset_name]
73
+
74
+ wer_entry = {
75
+ "prediction": text_normalizer(test_info["prediction"]),
76
+ "reference": text_normalizer(test_info["reference"]),
77
+ }
78
+ results[key]["timestamp"] = timestamp
79
+ results[key]["average_wer"].append(wer_entry)
80
+
81
+ input_audio_seconds = test_info["timings"]["inputAudioSeconds"]
82
+ full_pipeline = test_info["timings"]["fullPipeline"] / 1000
83
+ time_elapsed = test_result["latencyStats"]["measurements"]["timeElapsed"]
84
+ total_decoding_loops = test_info["timings"]["totalDecodingLoops"]
85
+
86
+ results[key]["dataset_speed"][dataset_name][
87
+ "inputAudioSeconds"
88
+ ] += input_audio_seconds
89
+ results[key]["dataset_speed"][dataset_name]["fullPipeline"] += full_pipeline
90
+
91
+ results[key]["speed"]["inputAudioSeconds"] += input_audio_seconds
92
+ results[key]["speed"]["fullPipeline"] += full_pipeline
93
+
94
+ results[key]["commit_hash"] = commit_hash
95
+ results[key]["commit_timestamp"] = commit_timestamp
96
+
97
+ results[key]["dataset_tokens_per_second"][dataset_name][
98
+ "totalDecodingLoops"
99
+ ] += total_decoding_loops
100
+ results[key]["dataset_tokens_per_second"][dataset_name][
101
+ "timeElapsed"
102
+ ] += time_elapsed
103
+ results[key]["tokens_per_second"]["totalDecodingLoops"] += total_decoding_loops
104
+ results[key]["tokens_per_second"]["timeElapsed"] += time_elapsed
105
+
106
+ audio = audio_file_name.split(".")[0]
107
+ audio = audio.split("-")[0]
108
+
109
+ dataset_row = dataset_df.loc[dataset_df["file"].str.contains(audio)].iloc[0]
110
+ reference_wer = dataset_row["wer"]
111
+ prediction_wer = test_info["wer"]
112
+
113
+ results[key]["qoi"].append(1 if prediction_wer <= reference_wer * 110 else 0)
114
+
115
+
116
+ def calculate_and_save_performance_results(
117
+ performance_results, performance_output_path
118
+ ):
119
+ """
120
+ Calculates final performance metrics and saves them to a JSON file.
121
+ :param performance_results: Dictionary containing raw performance data.
122
+ :param performance_output_path: Path to save the processed performance results.
123
+ This function processes the raw performance data, calculates average metrics,
124
+ and writes the final results to a JSON file, with each entry representing
125
+ a unique combination of model, device, and OS.
126
+ """
127
+ not_supported = []
128
+ with open(performance_output_path, "w") as performance_file:
129
+ for key, data in performance_results.items():
130
+ model, device, os_info, timestamp = key
131
+ speed = round(
132
+ data["speed"]["inputAudioSeconds"] / data["speed"]["fullPipeline"], 2
133
+ )
134
+
135
+ # if speed < 1.0:
136
+ # not_supported.append((model, device, os_info))
137
+ # continue
138
+
139
+ performance_entry = {
140
+ "model": model.replace("_", "/"),
141
+ "device": device,
142
+ "os": os_info.replace("_", " "),
143
+ "timestamp": data["timestamp"],
144
+ "speed": speed,
145
+ "tokens_per_second": round(
146
+ data["tokens_per_second"]["totalDecodingLoops"]
147
+ / data["tokens_per_second"]["timeElapsed"],
148
+ 2,
149
+ ),
150
+ "dataset_speed": {
151
+ dataset: round(
152
+ speed_info["inputAudioSeconds"] / speed_info["fullPipeline"], 2
153
+ )
154
+ for dataset, speed_info in data["dataset_speed"].items()
155
+ },
156
+ "dataset_tokens_per_second": {
157
+ dataset: round(
158
+ tps_info["totalDecodingLoops"] / tps_info["timeElapsed"], 2
159
+ )
160
+ for dataset, tps_info in data["dataset_tokens_per_second"].items()
161
+ },
162
+ "average_wer": compute_average_wer(data["average_wer"]),
163
+ "qoi": round(mean(data["qoi"]), 2),
164
+ "commit_hash": data["commit_hash"],
165
+ "commit_timestamp": data["commit_timestamp"],
166
+ }
167
+
168
+ json.dump(performance_entry, performance_file)
169
+ performance_file.write("\n")
170
+
171
+ return not_supported
172
+
173
+
174
+ def generate_support_matrix(performance_data_path="dashboard_data/performance_data.json", output_file="dashboard_data/support_data.csv"):
175
+ """
176
+ Generate a support matrix CSV showing model compatibility across devices and OS versions.
177
+ ✅: All tests passed
178
+ ⚠️: Some tests failed
179
+ """
180
+ support_matrix = defaultdict(lambda: defaultdict(lambda: {
181
+ "os_versions": set(),
182
+ "dataset_count": 0
183
+ }))
184
+
185
+ models = set()
186
+ devices = set()
187
+
188
+ # Process performance data
189
+ with open(performance_data_path, 'r') as f:
190
+ for line in f:
191
+ entry = json.loads(line)
192
+ model = entry["model"]
193
+ device = entry["device"]
194
+ os_info = entry["os"]
195
+
196
+ models.add(model)
197
+ devices.add(device)
198
+
199
+ support_matrix[model][device]["os_versions"].add(os_info)
200
+ if "dataset_speed" in entry:
201
+ support_matrix[model][device]["dataset_count"] = len(entry["dataset_speed"])
202
+
203
+ # Create DataFrame with correct headers
204
+ df = pd.DataFrame(columns=['', 'Model'] + [f'"{device}"' for device in sorted(devices)])
205
+
206
+ # Add each model with its data
207
+ for model in sorted(models):
208
+ row_data = {'': model, 'Model': model}
209
+
210
+ for device in sorted(devices):
211
+ info = support_matrix[model].get(device, {"dataset_count": 0, "os_versions": set()})
212
+ os_versions = ', '.join(sorted(info["os_versions"]))
213
+
214
+ if info["dataset_count"] == 0:
215
+ row_data[f'"{device}"'] = "Not Supported"
216
+ elif info["dataset_count"] >= 2:
217
+ row_data[f'"{device}"'] = f"✅ {os_versions}"
218
+ else:
219
+ row_data[f'"{device}"'] = f"⚠️ {os_versions}"
220
+
221
+ df = pd.concat([df, pd.DataFrame([row_data])], ignore_index=True)
222
+
223
+ # Save to CSV
224
+ df.to_csv(output_file, index=False)
225
+
226
+
227
+ def main():
228
+ """
229
+ Main function to orchestrate the performance data generation process.
230
+ This function performs the following steps:
231
+ 1. Downloads benchmark data if requested.
232
+ 2. Fetches evaluation data for various datasets.
233
+ 3. Processes benchmark files and summary files.
234
+ 4. Calculates and saves performance and support results.
235
+ """
236
+ source_xcresult_repo = "argmaxinc/whisperkit-evals-dataset"
237
+ source_xcresult_subfolder = "benchmark_data/"
238
+ source_xcresult_directory = f"{source_xcresult_repo}/{source_xcresult_subfolder}"
239
+ if len(sys.argv) > 1 and sys.argv[1] == "download":
240
+ try:
241
+ shutil.rmtree(source_xcresult_repo)
242
+ except:
243
+ print("Nothing to remove.")
244
+ download_dataset(
245
+ source_xcresult_repo, source_xcresult_repo, source_xcresult_subfolder
246
+ )
247
+
248
+ datasets = {
249
+ "Earnings-22": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/earnings22/2024-03-04_13%3A39%3A42_GMT-0800.json",
250
+ "LibriSpeech": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/librispeech/2024-02-28_18%3A45%3A02_GMT-0800.json?download=true",
251
+ "earnings22-10mins": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/earnings22/2024-03-04_13%3A39%3A42_GMT-0800.json",
252
+ "librispeech-10mins": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/librispeech/2024-02-28_18%3A45%3A02_GMT-0800.json?download=true",
253
+ "earnings22-12hours": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/earnings22/2024-03-04_13%3A39%3A42_GMT-0800.json",
254
+ "librispeech": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/librispeech/2024-02-28_18%3A45%3A02_GMT-0800.json?download=true",
255
+ }
256
+
257
+ dataset_dfs = {}
258
+ for dataset_name, url in datasets.items():
259
+ evals = fetch_evaluation_data(url)
260
+ dataset_dfs[dataset_name] = pd.json_normalize(evals["results"])
261
+
262
+ performance_results = defaultdict(
263
+ lambda: {
264
+ "average_wer": [],
265
+ "qoi": [],
266
+ "speed": {"inputAudioSeconds": 0, "fullPipeline": 0},
267
+ "tokens_per_second": {"totalDecodingLoops": 0, "timeElapsed": 0},
268
+ "dataset_speed": defaultdict(
269
+ lambda: {"inputAudioSeconds": 0, "fullPipeline": 0}
270
+ ),
271
+ "dataset_tokens_per_second": defaultdict(
272
+ lambda: {"totalDecodingLoops": 0, "timeElapsed": 0}
273
+ ),
274
+ "timestamp": None,
275
+ "commit_hash": None,
276
+ "commit_timestamp": None,
277
+ "test_timestamp": None,
278
+ }
279
+ )
280
+
281
+ with open("dashboard_data/device_map.json", "r") as f:
282
+ device_map = json.load(f)
283
+
284
+ for subdir, _, files in os.walk(source_xcresult_directory):
285
+ for filename in files:
286
+ file_path = os.path.join(subdir, filename)
287
+ if not filename.endswith(".json"):
288
+ continue
289
+ else:
290
+ process_benchmark_file(file_path, dataset_dfs, device_map, performance_results)
291
+
292
+ calculate_and_save_performance_results(
293
+ performance_results, "dashboard_data/performance_data.json"
294
+ )
295
+
296
+ generate_support_matrix()
297
+
298
+
299
+ if __name__ == "__main__":
300
+ main()
quality_generate.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import shutil
4
+ import sys
5
+ from collections import defaultdict
6
+ from statistics import mean
7
+
8
+ import pandas as pd
9
+ import requests
10
+
11
+ from text_normalizer import text_normalizer
12
+ from utils import compute_average_wer, download_dataset
13
+
14
+
15
+ def fetch_evaluation_data(url):
16
+ """
17
+ Fetches evaluation data from the given URL.
18
+ :param url: The URL to fetch the evaluation data from.
19
+ :returns: The evaluation data as a dictionary.
20
+ :rauses: sys.exit if the request fails
21
+ """
22
+ response = requests.get(url)
23
+ if response.status_code == 200:
24
+ return json.loads(response.text)
25
+ else:
26
+ sys.exit(f"Failed to fetch WhisperKit evals: {response.text}")
27
+
28
+
29
+ def get_device_name(device):
30
+ """
31
+ Gets the device name from the device map if it exists.
32
+ :param device: String representing the device name.
33
+ :returns: The device name from the device map if it exists, otherwise the input device name.
34
+ """
35
+ with open("dashboard_data/device_map.json", "r") as f:
36
+ device_map = json.load(f)
37
+ return device_map.get(device, device).replace(" ", "_")
38
+
39
+
40
+ def process_quality_file(file_path, dataset_dfs, quality_results):
41
+ """
42
+ Processes a single quality file and updates the quality_results dictionary.
43
+
44
+ :param file_path: Path to the quality JSON file.
45
+ :param dataset_dfs: Dictionary of DataFrames containing dataset information.
46
+ :param quality_results: Dictionary to store the processed quality results.
47
+
48
+ This function reads a quality JSON file, extracts relevant information,
49
+ and updates the quality_results dictionary with various metrics including WER
50
+ and Quality of Inference (QoI) for different datasets.
51
+ """
52
+ with open(file_path, "r") as file:
53
+ test_results = json.load(file)
54
+
55
+ if len(test_results) == 0:
56
+ return
57
+
58
+ model = file_path.split("/")[-3].replace("_", "/")
59
+ device = "Linux"
60
+ timestamp = file_path.split("/")[-1].split(".")[0]
61
+ key = model
62
+ dataset_name = file_path.split("/")[-2]
63
+
64
+ for test_result in test_results:
65
+ audio_file_name = test_result["testInfo"]["audioFile"]
66
+
67
+ dataset_key = "Earnings-22" if "earnings22" in dataset_name else "LibriSpeech"
68
+ dataset_df = dataset_dfs[dataset_key]
69
+
70
+ wer_entry = {
71
+ "prediction": text_normalizer(test_result["testInfo"]["prediction"]),
72
+ "reference": text_normalizer(test_result["testInfo"]["reference"]),
73
+ }
74
+ quality_results[key]["timestamp"] = timestamp
75
+ quality_results[key]["dataset_wer"][dataset_name].append(wer_entry)
76
+
77
+ audio = audio_file_name.split("-")[0]
78
+ dataset_row = dataset_df.loc[dataset_df["file"].str.contains(audio)].iloc[0]
79
+ reference_wer = dataset_row["wer"]
80
+ prediction_wer = test_result["testInfo"]["wer"]
81
+
82
+ quality_results[key]["qoi"].append(1 if prediction_wer <= reference_wer * 110 else 0)
83
+
84
+
85
+ def calculate_and_save_quality_results(quality_results, quality_output_path):
86
+ """
87
+ Calculates final quality metrics and saves them to a JSON file.
88
+
89
+ :param quality_results: Dictionary containing raw quality data.
90
+ :param quality_output_path: Path to save the processed quality results.
91
+
92
+ This function processes the raw quality data, calculates average metrics,
93
+ and writes the final results to a JSON file, with each entry representing
94
+ a unique model's quality metrics across different datasets, including
95
+ Word Error Rate (WER) and Quality of Inference (QoI).
96
+ """
97
+ with open(quality_output_path, "w") as quality_file:
98
+ for key, data in quality_results.items():
99
+ model = key
100
+
101
+ dataset_wers = {
102
+ dataset: compute_average_wer(wer)
103
+ for dataset, wer in data["dataset_wer"].items()
104
+ }
105
+ average_wer = (
106
+ sum(dataset_wers.values()) / len(dataset_wers)
107
+ if len(dataset_wers) != 0
108
+ else 0
109
+ )
110
+
111
+ quality_entry = {
112
+ "model": model.replace("_", "/"),
113
+ "timestamp": data["timestamp"],
114
+ "average_wer": round(average_wer, 2),
115
+ "dataset_wer": dataset_wers,
116
+ "qoi": round(mean(data["qoi"]), 2),
117
+ }
118
+
119
+ json.dump(quality_entry, quality_file)
120
+ quality_file.write("\n")
121
+
122
+
123
+ def main():
124
+ """
125
+ Main function to orchestrate the quality data generation process.
126
+
127
+ This function performs the following steps:
128
+ 1. Downloads quality data if requested.
129
+ 2. Fetches evaluation data for various datasets.
130
+ 3. Processes quality files for specific datasets.
131
+ 4. Calculates and saves quality results, including WER and QoI metrics.
132
+ """
133
+ if len(sys.argv) > 1 and sys.argv[1] == "download":
134
+ try:
135
+ shutil.rmtree("english")
136
+ except:
137
+ print("Nothing to remove.")
138
+ download_dataset("argmaxinc/whisperkit-evals", "english", "WhisperKit")
139
+
140
+ datasets = {
141
+ "Earnings-22": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/earnings22/2024-03-04_13%3A39%3A42_GMT-0800.json",
142
+ "LibriSpeech": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/librispeech/2024-02-28_18%3A45%3A02_GMT-0800.json?download=true",
143
+ "earnings22-10mins": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/earnings22/2024-03-04_13%3A39%3A42_GMT-0800.json",
144
+ "librispeech-10mins": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/librispeech/2024-02-28_18%3A45%3A02_GMT-0800.json?download=true",
145
+ "earnings22-12hours": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/earnings22/2024-03-04_13%3A39%3A42_GMT-0800.json",
146
+ "librispeech": "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/resolve/main/WhisperOpenAIAPI/openai_whisper-large-v2/librispeech/2024-02-28_18%3A45%3A02_GMT-0800.json?download=true",
147
+ }
148
+
149
+ dataset_dfs = {}
150
+ for dataset_name, url in datasets.items():
151
+ evals = fetch_evaluation_data(url)
152
+ dataset_dfs[dataset_name] = pd.json_normalize(evals["results"])
153
+
154
+ source_quality_directory = "argmaxinc/english/WhisperKit/"
155
+
156
+ quality_results = defaultdict(
157
+ lambda: {
158
+ "average_wer": [],
159
+ "dataset_wer": defaultdict(list),
160
+ "qoi": [],
161
+ "timestamp": None,
162
+ }
163
+ )
164
+
165
+ for subdir, _, files in os.walk(source_quality_directory):
166
+ dataset = subdir.split("/")[-1]
167
+ if dataset not in ["earnings22-10mins", "librispeech-10mins"]:
168
+ continue
169
+
170
+ for filename in files:
171
+ if not filename.endswith(".json"):
172
+ continue
173
+
174
+ file_path = os.path.join(subdir, filename)
175
+ process_quality_file(file_path, dataset_dfs, quality_results)
176
+
177
+ calculate_and_save_quality_results(
178
+ quality_results, "dashboard_data/quality_data.json"
179
+ )
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()
requirements.txt ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles
2
+ aiohttp
3
+ aiosignal
4
+ altair
5
+ annotated-types
6
+ anyio
7
+ argmax_gradio_components
8
+ async-timeout
9
+ attrs
10
+ backports.tarfile
11
+ build
12
+ certifi
13
+ cffi
14
+ cfgv
15
+ charset-normalizer
16
+ click
17
+ contourpy
18
+ cycler
19
+ datasets
20
+ dill
21
+ distlib
22
+ dnspython
23
+ docutils
24
+ email_validator
25
+ exceptiongroup
26
+ fastapi
27
+ fastapi-cli
28
+ ffmpy
29
+ filelock
30
+ fonttools
31
+ frozenlist
32
+ fsspec
33
+ gradio==5.0.1
34
+ h11
35
+ httpcore
36
+ httptools
37
+ httpx
38
+ huggingface-hub
39
+ identify
40
+ idna
41
+ importlib_metadata
42
+ importlib_resources
43
+ jaraco.classes
44
+ jaraco.context
45
+ jaraco.functools
46
+ Jinja2
47
+ jsonschema
48
+ jsonschema-specifications
49
+ keyring
50
+ kiwisolver
51
+ markdown-it-py
52
+ MarkupSafe
53
+ matplotlib
54
+ mdurl
55
+ more-itertools
56
+ multidict
57
+ multiprocess
58
+ nh3
59
+ nodeenv
60
+ numpy
61
+ orjson
62
+ packaging
63
+ pandas
64
+ pillow
65
+ pkginfo
66
+ platformdirs
67
+ plotly
68
+ pre-commit
69
+ pyarrow
70
+ pyarrow-hotfix
71
+ pycparser
72
+ pydantic
73
+ pydantic_core
74
+ pydub
75
+ Pygments
76
+ pyparsing
77
+ pyproject_hooks
78
+ python-dateutil
79
+ python-dotenv
80
+ python-multipart
81
+ pytz
82
+ PyYAML
83
+ readme_renderer
84
+ referencing
85
+ requests
86
+ requests-toolbelt
87
+ rfc3986
88
+ rich
89
+ rpds-py
90
+ ruff
91
+ scipy
92
+ scikit-learn
93
+ semantic-version
94
+ shellingham
95
+ six
96
+ sniffio
97
+ soundfile
98
+ starlette
99
+ tenacity
100
+ text_normalizer
101
+ tomli
102
+ tomlkit
103
+ toolz
104
+ tqdm
105
+ twine
106
+ typer
107
+ typing_extensions
108
+ tzdata
109
+ ujson
110
+ urllib3
111
+ uvicorn
112
+ uvloop
113
+ virtualenv
114
+ watchfiles
115
+ websockets
116
+ xxhash
117
+ yarl
118
+ zipp
119
+ iso639-lang
120
+ evaluate
121
+ jiwer
122
+ regex
text_normalizer.py ADDED
@@ -0,0 +1,2374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2022 The OpenAI team and The HuggingFace Team. All rights reserved.
2
+ # Most of the code is copy pasted from the original whisper repository
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import re
17
+ import unicodedata
18
+ from fractions import Fraction
19
+ from typing import Iterator, List, Match, Optional, Union
20
+
21
+ import regex
22
+
23
+ abbr = {
24
+ "accessorise": "accessorize",
25
+ "accessorised": "accessorized",
26
+ "accessorises": "accessorizes",
27
+ "accessorising": "accessorizing",
28
+ "acclimatisation": "acclimatization",
29
+ "acclimatise": "acclimatize",
30
+ "acclimatised": "acclimatized",
31
+ "acclimatises": "acclimatizes",
32
+ "acclimatising": "acclimatizing",
33
+ "accoutrements": "accouterments",
34
+ "aeon": "eon",
35
+ "aeons": "eons",
36
+ "aerogramme": "aerogram",
37
+ "aerogrammes": "aerograms",
38
+ "aeroplane": "airplane",
39
+ "aeroplanes": "airplanes",
40
+ "aesthete": "esthete",
41
+ "aesthetes": "esthetes",
42
+ "aesthetic": "esthetic",
43
+ "aesthetically": "esthetically",
44
+ "aesthetics": "esthetics",
45
+ "aetiology": "etiology",
46
+ "ageing": "aging",
47
+ "aggrandisement": "aggrandizement",
48
+ "agonise": "agonize",
49
+ "agonised": "agonized",
50
+ "agonises": "agonizes",
51
+ "agonising": "agonizing",
52
+ "agonisingly": "agonizingly",
53
+ "almanack": "almanac",
54
+ "almanacks": "almanacs",
55
+ "aluminium": "aluminum",
56
+ "amortisable": "amortizable",
57
+ "amortisation": "amortization",
58
+ "amortisations": "amortizations",
59
+ "amortise": "amortize",
60
+ "amortised": "amortized",
61
+ "amortises": "amortizes",
62
+ "amortising": "amortizing",
63
+ "amphitheatre": "amphitheater",
64
+ "amphitheatres": "amphitheaters",
65
+ "anaemia": "anemia",
66
+ "anaemic": "anemic",
67
+ "anaesthesia": "anesthesia",
68
+ "anaesthetic": "anesthetic",
69
+ "anaesthetics": "anesthetics",
70
+ "anaesthetise": "anesthetize",
71
+ "anaesthetised": "anesthetized",
72
+ "anaesthetises": "anesthetizes",
73
+ "anaesthetising": "anesthetizing",
74
+ "anaesthetist": "anesthetist",
75
+ "anaesthetists": "anesthetists",
76
+ "anaesthetize": "anesthetize",
77
+ "anaesthetized": "anesthetized",
78
+ "anaesthetizes": "anesthetizes",
79
+ "anaesthetizing": "anesthetizing",
80
+ "analogue": "analog",
81
+ "analogues": "analogs",
82
+ "analyse": "analyze",
83
+ "analysed": "analyzed",
84
+ "analyses": "analyzes",
85
+ "analysing": "analyzing",
86
+ "anglicise": "anglicize",
87
+ "anglicised": "anglicized",
88
+ "anglicises": "anglicizes",
89
+ "anglicising": "anglicizing",
90
+ "annualised": "annualized",
91
+ "antagonise": "antagonize",
92
+ "antagonised": "antagonized",
93
+ "antagonises": "antagonizes",
94
+ "antagonising": "antagonizing",
95
+ "apologise": "apologize",
96
+ "apologised": "apologized",
97
+ "apologises": "apologizes",
98
+ "apologising": "apologizing",
99
+ "appal": "appall",
100
+ "appals": "appalls",
101
+ "appetiser": "appetizer",
102
+ "appetisers": "appetizers",
103
+ "appetising": "appetizing",
104
+ "appetisingly": "appetizingly",
105
+ "arbour": "arbor",
106
+ "arbours": "arbors",
107
+ "archaeologically": "archeologically",
108
+ "archaeologist": "archeologist",
109
+ "archaeologists": "archeologists",
110
+ "archaeology": "archeology</span>",
111
+ "archeological": "archaeological",
112
+ "ardour": "ardor",
113
+ "armour": "armor",
114
+ "armoured": "armored",
115
+ "armourer": "armorer",
116
+ "armourers": "armorers",
117
+ "armouries": "armories",
118
+ "armoury": "armory",
119
+ "artefact": "artifact",
120
+ "artefacts": "artifacts",
121
+ "authorise": "authorize",
122
+ "authorised": "authorized",
123
+ "authorises": "authorizes",
124
+ "authorising": "authorizing",
125
+ "axe": "ax",
126
+ "backpedalled": "backpedaled",
127
+ "backpedalling": "backpedaling",
128
+ "bannister": "banister",
129
+ "bannisters": "banisters",
130
+ "baptise": "baptize",
131
+ "baptised": "baptized",
132
+ "baptises": "baptizes",
133
+ "baptising": "baptizing",
134
+ "bastardise": "bastardize",
135
+ "bastardised": "bastardized",
136
+ "bastardises": "bastardizes",
137
+ "bastardising": "bastardizing",
138
+ "battleax": "battleaxe",
139
+ "baulk": "balk",
140
+ "baulked": "balked",
141
+ "baulking": "balking",
142
+ "baulks": "balks",
143
+ "bedevilled": "bedeviled",
144
+ "bedevilling": "bedeviling",
145
+ "behaviour": "behavior",
146
+ "behavioural": "behavioral",
147
+ "behaviourism": "behaviorism",
148
+ "behaviourist": "behaviorist",
149
+ "behaviourists": "behaviorists",
150
+ "behaviours": "behaviors",
151
+ "behove": "behoove",
152
+ "behoved": "behooved",
153
+ "behoves": "behooves",
154
+ "bejewelled": "bejeweled",
155
+ "belabour": "belabor",
156
+ "belaboured": "belabored",
157
+ "belabouring": "belaboring",
158
+ "belabours": "belabors",
159
+ "bevelled": "beveled",
160
+ "bevvies": "bevies",
161
+ "bevvy": "bevy",
162
+ "biassed": "biased",
163
+ "biassing": "biasing",
164
+ "bingeing": "binging",
165
+ "bougainvillaea": "bougainvillea",
166
+ "bougainvillaeas": "bougainvilleas",
167
+ "bowdlerise": "bowdlerize",
168
+ "bowdlerised": "bowdlerized",
169
+ "bowdlerises": "bowdlerizes",
170
+ "bowdlerising": "bowdlerizing",
171
+ "breathalyse": "breathalyze",
172
+ "breathalysed": "breathalyzed",
173
+ "breathalyser": "breathalyzer",
174
+ "breathalysers": "breathalyzers",
175
+ "breathalyses": "breathalyzes",
176
+ "breathalysing": "breathalyzing",
177
+ "brutalise": "brutalize",
178
+ "brutalised": "brutalized",
179
+ "brutalises": "brutalizes",
180
+ "brutalising": "brutalizing",
181
+ "busses": "buses",
182
+ "bussing": "busing",
183
+ "caesarean": "cesarean",
184
+ "caesareans": "cesareans",
185
+ "calibre": "caliber",
186
+ "calibres": "calibers",
187
+ "calliper": "caliper",
188
+ "callipers": "calipers",
189
+ "callisthenics": "calisthenics",
190
+ "canalise": "canalize",
191
+ "canalised": "canalized",
192
+ "canalises": "canalizes",
193
+ "canalising": "canalizing",
194
+ "cancelation": "cancellation",
195
+ "cancelations": "cancellations",
196
+ "cancelled": "canceled",
197
+ "cancelling": "canceling",
198
+ "candour": "candor",
199
+ "cannibalise": "cannibalize",
200
+ "cannibalised": "cannibalized",
201
+ "cannibalises": "cannibalizes",
202
+ "cannibalising": "cannibalizing",
203
+ "canonise": "canonize",
204
+ "canonised": "canonized",
205
+ "canonises": "canonizes",
206
+ "canonising": "canonizing",
207
+ "capitalise": "capitalize",
208
+ "capitalised": "capitalized",
209
+ "capitalises": "capitalizes",
210
+ "capitalising": "capitalizing",
211
+ "caramelise": "caramelize",
212
+ "caramelised": "caramelized",
213
+ "caramelises": "caramelizes",
214
+ "caramelising": "caramelizing",
215
+ "carbonise": "carbonize",
216
+ "carbonised": "carbonized",
217
+ "carbonises": "carbonizes",
218
+ "carbonising": "carbonizing",
219
+ "carolled": "caroled",
220
+ "carolling": "caroling",
221
+ "catalogue": "catalog",
222
+ "catalogued": "cataloged",
223
+ "catalogues": "catalogs",
224
+ "cataloguing": "cataloging",
225
+ "catalyse": "catalyze",
226
+ "catalysed": "catalyzed",
227
+ "catalyses": "catalyzes",
228
+ "catalysing": "catalyzing",
229
+ "categorise": "categorize",
230
+ "categorised": "categorized",
231
+ "categorises": "categorizes",
232
+ "categorising": "categorizing",
233
+ "cauterise": "cauterize",
234
+ "cauterised": "cauterized",
235
+ "cauterises": "cauterizes",
236
+ "cauterising": "cauterizing",
237
+ "cavilled": "caviled",
238
+ "cavilling": "caviling",
239
+ "centigramme": "centigram",
240
+ "centigrammes": "centigrams",
241
+ "centilitre": "centiliter",
242
+ "centilitres": "centiliters",
243
+ "centimetre": "centimeter",
244
+ "centimetres": "centimeters",
245
+ "centralise": "centralize",
246
+ "centralised": "centralized",
247
+ "centralises": "centralizes",
248
+ "centralising": "centralizing",
249
+ "centre": "center",
250
+ "centred": "centered",
251
+ "centrefold": "centerfold",
252
+ "centrefolds": "centerfolds",
253
+ "centrepiece": "centerpiece",
254
+ "centrepieces": "centerpieces",
255
+ "centres": "centers",
256
+ "channelled": "channeled",
257
+ "channelling": "channeling",
258
+ "characterise": "characterize",
259
+ "characterised": "characterized",
260
+ "characterises": "characterizes",
261
+ "characterising": "characterizing",
262
+ "cheque": "check",
263
+ "chequebook": "checkbook",
264
+ "chequebooks": "checkbooks",
265
+ "chequered": "checkered",
266
+ "cheques": "checks",
267
+ "chilli": "chili",
268
+ "chimaera": "chimera",
269
+ "chimaeras": "chimeras",
270
+ "chiselled": "chiseled",
271
+ "chiselling": "chiseling",
272
+ "circularise": "circularize",
273
+ "circularised": "circularized",
274
+ "circularises": "circularizes",
275
+ "circularising": "circularizing",
276
+ "civilise": "civilize",
277
+ "civilised": "civilized",
278
+ "civilises": "civilizes",
279
+ "civilising": "civilizing",
280
+ "clamour": "clamor",
281
+ "clamoured": "clamored",
282
+ "clamouring": "clamoring",
283
+ "clamours": "clamors",
284
+ "clangour": "clangor",
285
+ "clarinettist": "clarinetist",
286
+ "clarinettists": "clarinetists",
287
+ "collectivise": "collectivize",
288
+ "collectivised": "collectivized",
289
+ "collectivises": "collectivizes",
290
+ "collectivising": "collectivizing",
291
+ "colonisation": "colonization",
292
+ "colonise": "colonize",
293
+ "colonised": "colonized",
294
+ "coloniser": "colonizer",
295
+ "colonisers": "colonizers",
296
+ "colonises": "colonizes",
297
+ "colonising": "colonizing",
298
+ "colour": "color",
299
+ "colourant": "colorant",
300
+ "colourants": "colorants",
301
+ "coloured": "colored",
302
+ "coloureds": "coloreds",
303
+ "colourful": "colorful",
304
+ "colourfully": "colorfully",
305
+ "colouring": "coloring",
306
+ "colourize": "colorize",
307
+ "colourized": "colorized",
308
+ "colourizes": "colorizes",
309
+ "colourizing": "colorizing",
310
+ "colourless": "colorless",
311
+ "colours": "colors",
312
+ "commercialise": "commercialize",
313
+ "commercialised": "commercialized",
314
+ "commercialises": "commercializes",
315
+ "commercialising": "commercializing",
316
+ "compartmentalise": "compartmentalize",
317
+ "compartmentalised": "compartmentalized",
318
+ "compartmentalises": "compartmentalizes",
319
+ "compartmentalising": "compartmentalizing",
320
+ "computerise": "computerize",
321
+ "computerised": "computerized",
322
+ "computerises": "computerizes",
323
+ "computerising": "computerizing",
324
+ "conceptualise": "conceptualize",
325
+ "conceptualised": "conceptualized",
326
+ "conceptualises": "conceptualizes",
327
+ "conceptualising": "conceptualizing",
328
+ "connexion": "connection",
329
+ "connexions": "connections",
330
+ "contextualise": "contextualize",
331
+ "contextualised": "contextualized",
332
+ "contextualises": "contextualizes",
333
+ "contextualising": "contextualizing",
334
+ "cosier": "cozier",
335
+ "cosies": "cozies",
336
+ "cosiest": "coziest",
337
+ "cosily": "cozily",
338
+ "cosiness": "coziness",
339
+ "cosy": "cozy",
340
+ "councillor": "councilor",
341
+ "councillors": "councilors",
342
+ "counselled": "counseled",
343
+ "counselling": "counseling",
344
+ "counsellor": "counselor",
345
+ "counsellors": "counselors",
346
+ "crenelated": "crenellated",
347
+ "criminalise": "criminalize",
348
+ "criminalised": "criminalized",
349
+ "criminalises": "criminalizes",
350
+ "criminalising": "criminalizing",
351
+ "criticise": "criticize",
352
+ "criticised": "criticized",
353
+ "criticises": "criticizes",
354
+ "criticising": "criticizing",
355
+ "crueller": "crueler",
356
+ "cruellest": "cruelest",
357
+ "crystallisation": "crystallization",
358
+ "crystallise": "crystallize",
359
+ "crystallised": "crystallized",
360
+ "crystallises": "crystallizes",
361
+ "crystallising": "crystallizing",
362
+ "cudgelled": "cudgeled",
363
+ "cudgelling": "cudgeling",
364
+ "customise": "customize",
365
+ "customised": "customized",
366
+ "customises": "customizes",
367
+ "customising": "customizing",
368
+ "cypher": "cipher",
369
+ "cyphers": "ciphers",
370
+ "decentralisation": "decentralization",
371
+ "decentralise": "decentralize",
372
+ "decentralised": "decentralized",
373
+ "decentralises": "decentralizes",
374
+ "decentralising": "decentralizing",
375
+ "decriminalisation": "decriminalization",
376
+ "decriminalise": "decriminalize",
377
+ "decriminalised": "decriminalized",
378
+ "decriminalises": "decriminalizes",
379
+ "decriminalising": "decriminalizing",
380
+ "defence": "defense",
381
+ "defenceless": "defenseless",
382
+ "defences": "defenses",
383
+ "dehumanisation": "dehumanization",
384
+ "dehumanise": "dehumanize",
385
+ "dehumanised": "dehumanized",
386
+ "dehumanises": "dehumanizes",
387
+ "dehumanising": "dehumanizing",
388
+ "demeanour": "demeanor",
389
+ "demilitarisation": "demilitarization",
390
+ "demilitarise": "demilitarize",
391
+ "demilitarised": "demilitarized",
392
+ "demilitarises": "demilitarizes",
393
+ "demilitarising": "demilitarizing",
394
+ "demobilisation": "demobilization",
395
+ "demobilise": "demobilize",
396
+ "demobilised": "demobilized",
397
+ "demobilises": "demobilizes",
398
+ "demobilising": "demobilizing",
399
+ "democratisation": "democratization",
400
+ "democratise": "democratize",
401
+ "democratised": "democratized",
402
+ "democratises": "democratizes",
403
+ "democratising": "democratizing",
404
+ "demonise": "demonize",
405
+ "demonised": "demonized",
406
+ "demonises": "demonizes",
407
+ "demonising": "demonizing",
408
+ "demoralisation": "demoralization",
409
+ "demoralise": "demoralize",
410
+ "demoralised": "demoralized",
411
+ "demoralises": "demoralizes",
412
+ "demoralising": "demoralizing",
413
+ "denationalisation": "denationalization",
414
+ "denationalise": "denationalize",
415
+ "denationalised": "denationalized",
416
+ "denationalises": "denationalizes",
417
+ "denationalising": "denationalizing",
418
+ "deodorise": "deodorize",
419
+ "deodorised": "deodorized",
420
+ "deodorises": "deodorizes",
421
+ "deodorising": "deodorizing",
422
+ "depersonalise": "depersonalize",
423
+ "depersonalised": "depersonalized",
424
+ "depersonalises": "depersonalizes",
425
+ "depersonalising": "depersonalizing",
426
+ "deputise": "deputize",
427
+ "deputised": "deputized",
428
+ "deputises": "deputizes",
429
+ "deputising": "deputizing",
430
+ "desensitisation": "desensitization",
431
+ "desensitise": "desensitize",
432
+ "desensitised": "desensitized",
433
+ "desensitises": "desensitizes",
434
+ "desensitising": "desensitizing",
435
+ "destabilisation": "destabilization",
436
+ "destabilise": "destabilize",
437
+ "destabilised": "destabilized",
438
+ "destabilises": "destabilizes",
439
+ "destabilising": "destabilizing",
440
+ "dialled": "dialed",
441
+ "dialling": "dialing",
442
+ "dialogue": "dialog",
443
+ "dialogues": "dialogs",
444
+ "diarrhoea": "diarrhea",
445
+ "digitise": "digitize",
446
+ "digitised": "digitized",
447
+ "digitises": "digitizes",
448
+ "digitising": "digitizing",
449
+ "disc": "disk",
450
+ "discolour": "discolor",
451
+ "discoloured": "discolored",
452
+ "discolouring": "discoloring",
453
+ "discolours": "discolors",
454
+ "discs": "disks",
455
+ "disembowelled": "disemboweled",
456
+ "disembowelling": "disemboweling",
457
+ "disfavour": "disfavor",
458
+ "dishevelled": "disheveled",
459
+ "dishonour": "dishonor",
460
+ "dishonourable": "dishonorable",
461
+ "dishonourably": "dishonorably",
462
+ "dishonoured": "dishonored",
463
+ "dishonouring": "dishonoring",
464
+ "dishonours": "dishonors",
465
+ "disorganisation": "disorganization",
466
+ "disorganised": "disorganized",
467
+ "distil": "distill",
468
+ "distils": "distills",
469
+ "dramatisation": "dramatization",
470
+ "dramatisations": "dramatizations",
471
+ "dramatise": "dramatize",
472
+ "dramatised": "dramatized",
473
+ "dramatises": "dramatizes",
474
+ "dramatising": "dramatizing",
475
+ "draught": "draft",
476
+ "draughtboard": "draftboard",
477
+ "draughtboards": "draftboards",
478
+ "draughtier": "draftier",
479
+ "draughtiest": "draftiest",
480
+ "draughts": "drafts",
481
+ "draughtsman": "draftsman",
482
+ "draughtsmanship": "draftsmanship",
483
+ "draughtsmen": "draftsmen",
484
+ "draughtswoman": "draftswoman",
485
+ "draughtswomen": "draftswomen",
486
+ "draughty": "drafty",
487
+ "drivelled": "driveled",
488
+ "drivelling": "driveling",
489
+ "duelled": "dueled",
490
+ "duelling": "dueling",
491
+ "economise": "economize",
492
+ "economised": "economized",
493
+ "economises": "economizes",
494
+ "economising": "economizing",
495
+ "editorialise": "editorialize",
496
+ "editorialised": "editorialized",
497
+ "editorialises": "editorializes",
498
+ "editorialising": "editorializing",
499
+ "edoema": "edema",
500
+ "empathise": "empathize",
501
+ "empathised": "empathized",
502
+ "empathises": "empathizes",
503
+ "empathising": "empathizing",
504
+ "emphasise": "emphasize",
505
+ "emphasised": "emphasized",
506
+ "emphasises": "emphasizes",
507
+ "emphasising": "emphasizing",
508
+ "enamelled": "enameled",
509
+ "enamelling": "enameling",
510
+ "enamoured": "enamored",
511
+ "encyclopaedia": "encyclopedia",
512
+ "encyclopaedias": "encyclopedias",
513
+ "encyclopaedic": "encyclopedic",
514
+ "endeavour": "endeavor",
515
+ "endeavoured": "endeavored",
516
+ "endeavouring": "endeavoring",
517
+ "endeavours": "endeavors",
518
+ "energise": "energize",
519
+ "energised": "energized",
520
+ "energises": "energizes",
521
+ "energising": "energizing",
522
+ "enrol": "enroll",
523
+ "enrols": "enrolls",
524
+ "enthral": "enthrall",
525
+ "enthrals": "enthralls",
526
+ "epaulette": "epaulet",
527
+ "epaulettes": "epaulets",
528
+ "epicentre": "epicenter",
529
+ "epicentres": "epicenters",
530
+ "epilogue": "epilog",
531
+ "epilogues": "epilogs",
532
+ "epitomise": "epitomize",
533
+ "epitomised": "epitomized",
534
+ "epitomises": "epitomizes",
535
+ "epitomising": "epitomizing",
536
+ "equalisation": "equalization",
537
+ "equalise": "equalize",
538
+ "equalised": "equalized",
539
+ "equaliser": "equalizer",
540
+ "equalisers": "equalizers",
541
+ "equalises": "equalizes",
542
+ "equalising": "equalizing",
543
+ "eulogise": "eulogize",
544
+ "eulogised": "eulogized",
545
+ "eulogises": "eulogizes",
546
+ "eulogising": "eulogizing",
547
+ "evangelise": "evangelize",
548
+ "evangelised": "evangelized",
549
+ "evangelises": "evangelizes",
550
+ "evangelising": "evangelizing",
551
+ "exorcise": "exorcize",
552
+ "exorcised": "exorcized",
553
+ "exorcises": "exorcizes",
554
+ "exorcising": "exorcizing",
555
+ "extemporisation": "extemporization",
556
+ "extemporise": "extemporize",
557
+ "extemporised": "extemporized",
558
+ "extemporises": "extemporizes",
559
+ "extemporising": "extemporizing",
560
+ "externalisation": "externalization",
561
+ "externalisations": "externalizations",
562
+ "externalise": "externalize",
563
+ "externalised": "externalized",
564
+ "externalises": "externalizes",
565
+ "externalising": "externalizing",
566
+ "factorise": "factorize",
567
+ "factorised": "factorized",
568
+ "factorises": "factorizes",
569
+ "factorising": "factorizing",
570
+ "faecal": "fecal",
571
+ "faeces": "feces",
572
+ "familiarisation": "familiarization",
573
+ "familiarise": "familiarize",
574
+ "familiarised": "familiarized",
575
+ "familiarises": "familiarizes",
576
+ "familiarising": "familiarizing",
577
+ "fantasise": "fantasize",
578
+ "fantasised": "fantasized",
579
+ "fantasises": "fantasizes",
580
+ "fantasising": "fantasizing",
581
+ "favour": "favor",
582
+ "favourable": "favorable",
583
+ "favourably": "favorably",
584
+ "favoured": "favored",
585
+ "favouring": "favoring",
586
+ "favourite": "favorite",
587
+ "favourites": "favorites",
588
+ "favouritism": "favoritism",
589
+ "favours": "favors",
590
+ "feminise": "feminize",
591
+ "feminised": "feminized",
592
+ "feminises": "feminizes",
593
+ "feminising": "feminizing",
594
+ "fertilisation": "fertilization",
595
+ "fertilise": "fertilize",
596
+ "fertilised": "fertilized",
597
+ "fertiliser": "fertilizer",
598
+ "fertilisers": "fertilizers",
599
+ "fertilises": "fertilizes",
600
+ "fertilising": "fertilizing",
601
+ "fervour": "fervor",
602
+ "fibre": "fiber",
603
+ "fibreglass": "fiberglass",
604
+ "fibres": "fibers",
605
+ "fictionalisation": "fictionalization",
606
+ "fictionalisations": "fictionalizations",
607
+ "fictionalise": "fictionalize",
608
+ "fictionalised": "fictionalized",
609
+ "fictionalises": "fictionalizes",
610
+ "fictionalising": "fictionalizing",
611
+ "fillet": "filet",
612
+ "filleted": "fileted",
613
+ "filleting": "fileting",
614
+ "fillets": "filets",
615
+ "finalisation": "finalization",
616
+ "finalise": "finalize",
617
+ "finalised": "finalized",
618
+ "finalises": "finalizes",
619
+ "finalising": "finalizing",
620
+ "flautist": "flutist",
621
+ "flautists": "flutists",
622
+ "flavour": "flavor",
623
+ "flavoured": "flavored",
624
+ "flavouring": "flavoring",
625
+ "flavourings": "flavorings",
626
+ "flavourless": "flavorless",
627
+ "flavours": "flavors",
628
+ "flavoursome": "flavorsome",
629
+ "flyer / flier": "flier / flyer",
630
+ "foetal": "fetal",
631
+ "foetid": "fetid",
632
+ "foetus": "fetus",
633
+ "foetuses": "fetuses",
634
+ "formalisation": "formalization",
635
+ "formalise": "formalize",
636
+ "formalised": "formalized",
637
+ "formalises": "formalizes",
638
+ "formalising": "formalizing",
639
+ "fossilisation": "fossilization",
640
+ "fossilise": "fossilize",
641
+ "fossilised": "fossilized",
642
+ "fossilises": "fossilizes",
643
+ "fossilising": "fossilizing",
644
+ "fraternisation": "fraternization",
645
+ "fraternise": "fraternize",
646
+ "fraternised": "fraternized",
647
+ "fraternises": "fraternizes",
648
+ "fraternising": "fraternizing",
649
+ "fulfil": "fulfill",
650
+ "fulfilment": "fulfillment",
651
+ "fulfils": "fulfills",
652
+ "funnelled": "funneled",
653
+ "funnelling": "funneling",
654
+ "gage": "gauge",
655
+ "gaged": "gauged",
656
+ "gages": "gauges",
657
+ "gaging": "gauging",
658
+ "galvanise": "galvanize",
659
+ "galvanised": "galvanized",
660
+ "galvanises": "galvanizes",
661
+ "galvanising": "galvanizing",
662
+ "gambolled": "gamboled",
663
+ "gambolling": "gamboling",
664
+ "gaol": "jail",
665
+ "gaolbird": "jailbird",
666
+ "gaolbirds": "jailbirds",
667
+ "gaolbreak": "jailbreak",
668
+ "gaolbreaks": "jailbreaks",
669
+ "gaoled": "jailed",
670
+ "gaoler": "jailer",
671
+ "gaolers": "jailers",
672
+ "gaoling": "jailing",
673
+ "gaols": "jails",
674
+ "gasses": "gases",
675
+ "generalisation": "generalization",
676
+ "generalisations": "generalizations",
677
+ "generalise": "generalize",
678
+ "generalised": "generalized",
679
+ "generalises": "generalizes",
680
+ "generalising": "generalizing",
681
+ "ghettoise": "ghettoize",
682
+ "ghettoised": "ghettoized",
683
+ "ghettoises": "ghettoizes",
684
+ "ghettoising": "ghettoizing",
685
+ "gipsies": "gypsies",
686
+ "glamor": "glamour",
687
+ "glamorise": "glamorize",
688
+ "glamorised": "glamorized",
689
+ "glamorises": "glamorizes",
690
+ "glamorising": "glamorizing",
691
+ "globalisation": "globalization",
692
+ "globalise": "globalize",
693
+ "globalised": "globalized",
694
+ "globalises": "globalizes",
695
+ "globalising": "globalizing",
696
+ "glueing": "gluing",
697
+ "goitre": "goiter",
698
+ "goitres": "goiters",
699
+ "gonorrhoea": "gonorrhea",
700
+ "gramme": "gram",
701
+ "grammes": "grams",
702
+ "gravelled": "graveled",
703
+ "grey": "gray",
704
+ "greyed": "grayed",
705
+ "greying": "graying",
706
+ "greyish": "grayish",
707
+ "greyness": "grayness",
708
+ "greys": "grays",
709
+ "grovelled": "groveled",
710
+ "grovelling": "groveling",
711
+ "groyne": "groin",
712
+ "groynes": "groins",
713
+ "gruelling": "grueling",
714
+ "gruellingly": "gruelingly",
715
+ "gryphon": "griffin",
716
+ "gryphons": "griffins",
717
+ "gynaecological": "gynecological",
718
+ "gynaecologist": "gynecologist",
719
+ "gynaecologists": "gynecologists",
720
+ "gynaecology": "gynecology",
721
+ "haematological": "hematological",
722
+ "haematologist": "hematologist",
723
+ "haematologists": "hematologists",
724
+ "haematology": "hematology",
725
+ "haemoglobin": "hemoglobin",
726
+ "haemophilia": "hemophilia",
727
+ "haemophiliac": "hemophiliac",
728
+ "haemophiliacs": "hemophiliacs",
729
+ "haemorrhage": "hemorrhage",
730
+ "haemorrhaged": "hemorrhaged",
731
+ "haemorrhages": "hemorrhages",
732
+ "haemorrhaging": "hemorrhaging",
733
+ "haemorrhoids": "hemorrhoids",
734
+ "harbour": "harbor",
735
+ "harboured": "harbored",
736
+ "harbouring": "harboring",
737
+ "harbours": "harbors",
738
+ "harmonisation": "harmonization",
739
+ "harmonise": "harmonize",
740
+ "harmonised": "harmonized",
741
+ "harmonises": "harmonizes",
742
+ "harmonising": "harmonizing",
743
+ "homoeopath": "homeopath",
744
+ "homoeopathic": "homeopathic",
745
+ "homoeopaths": "homeopaths",
746
+ "homoeopathy": "homeopathy",
747
+ "homogenise": "homogenize",
748
+ "homogenised": "homogenized",
749
+ "homogenises": "homogenizes",
750
+ "homogenising": "homogenizing",
751
+ "honour": "honor",
752
+ "honourable": "honorable",
753
+ "honourably": "honorably",
754
+ "honoured": "honored",
755
+ "honouring": "honoring",
756
+ "honours": "honors",
757
+ "hospitalisation": "hospitalization",
758
+ "hospitalise": "hospitalize",
759
+ "hospitalised": "hospitalized",
760
+ "hospitalises": "hospitalizes",
761
+ "hospitalising": "hospitalizing",
762
+ "humanise": "humanize",
763
+ "humanised": "humanized",
764
+ "humanises": "humanizes",
765
+ "humanising": "humanizing",
766
+ "humour": "humor",
767
+ "humoured": "humored",
768
+ "humouring": "humoring",
769
+ "humourless": "humorless",
770
+ "humours": "humors",
771
+ "hybridise": "hybridize",
772
+ "hybridised": "hybridized",
773
+ "hybridises": "hybridizes",
774
+ "hybridising": "hybridizing",
775
+ "hypnotise": "hypnotize",
776
+ "hypnotised": "hypnotized",
777
+ "hypnotises": "hypnotizes",
778
+ "hypnotising": "hypnotizing",
779
+ "hypothesise": "hypothesize",
780
+ "hypothesised": "hypothesized",
781
+ "hypothesises": "hypothesizes",
782
+ "hypothesising": "hypothesizing",
783
+ "idealisation": "idealization",
784
+ "idealise": "idealize",
785
+ "idealised": "idealized",
786
+ "idealises": "idealizes",
787
+ "idealising": "idealizing",
788
+ "idolise": "idolize",
789
+ "idolised": "idolized",
790
+ "idolises": "idolizes",
791
+ "idolising": "idolizing",
792
+ "immobilisation": "immobilization",
793
+ "immobilise": "immobilize",
794
+ "immobilised": "immobilized",
795
+ "immobiliser": "immobilizer",
796
+ "immobilisers": "immobilizers",
797
+ "immobilises": "immobilizes",
798
+ "immobilising": "immobilizing",
799
+ "immortalise": "immortalize",
800
+ "immortalised": "immortalized",
801
+ "immortalises": "immortalizes",
802
+ "immortalising": "immortalizing",
803
+ "immunisation": "immunization",
804
+ "immunise": "immunize",
805
+ "immunised": "immunized",
806
+ "immunises": "immunizes",
807
+ "immunising": "immunizing",
808
+ "impanelled": "impaneled",
809
+ "impanelling": "impaneling",
810
+ "imperilled": "imperiled",
811
+ "imperilling": "imperiling",
812
+ "individualise": "individualize",
813
+ "individualised": "individualized",
814
+ "individualises": "individualizes",
815
+ "individualising": "individualizing",
816
+ "industrialise": "industrialize",
817
+ "industrialised": "industrialized",
818
+ "industrialises": "industrializes",
819
+ "industrialising": "industrializing",
820
+ "inflexion": "inflection",
821
+ "inflexions": "inflections",
822
+ "initialise": "initialize",
823
+ "initialised": "initialized",
824
+ "initialises": "initializes",
825
+ "initialising": "initializing",
826
+ "initialled": "initialed",
827
+ "initialling": "initialing",
828
+ "instal": "install",
829
+ "instalment": "installment",
830
+ "instalments": "installments",
831
+ "instals": "installs",
832
+ "instil": "instill",
833
+ "instils": "instills",
834
+ "institutionalisation": "institutionalization",
835
+ "institutionalise": "institutionalize",
836
+ "institutionalised": "institutionalized",
837
+ "institutionalises": "institutionalizes",
838
+ "institutionalising": "institutionalizing",
839
+ "intellectualise": "intellectualize",
840
+ "intellectualised": "intellectualized",
841
+ "intellectualises": "intellectualizes",
842
+ "intellectualising": "intellectualizing",
843
+ "internalisation": "internalization",
844
+ "internalise": "internalize",
845
+ "internalised": "internalized",
846
+ "internalises": "internalizes",
847
+ "internalising": "internalizing",
848
+ "internationalisation": "internationalization",
849
+ "internationalise": "internationalize",
850
+ "internationalised": "internationalized",
851
+ "internationalises": "internationalizes",
852
+ "internationalising": "internationalizing",
853
+ "ionisation": "ionization",
854
+ "ionise": "ionize",
855
+ "ionised": "ionized",
856
+ "ioniser": "ionizer",
857
+ "ionisers": "ionizers",
858
+ "ionises": "ionizes",
859
+ "ionising": "ionizing",
860
+ "italicise": "italicize",
861
+ "italicised": "italicized",
862
+ "italicises": "italicizes",
863
+ "italicising": "italicizing",
864
+ "itemise": "itemize",
865
+ "itemised": "itemized",
866
+ "itemises": "itemizes",
867
+ "itemising": "itemizing",
868
+ "jeopardise": "jeopardize",
869
+ "jeopardised": "jeopardized",
870
+ "jeopardises": "jeopardizes",
871
+ "jeopardising": "jeopardizing",
872
+ "jewelled": "jeweled",
873
+ "jeweller": "jeweler",
874
+ "jewellers": "jewelers",
875
+ "jewellery": "jewelry",
876
+ "judgement": "judgment",
877
+ "kilogramme": "kilogram",
878
+ "kilogrammes": "kilograms",
879
+ "kilometre": "kilometer",
880
+ "kilometres": "kilometers",
881
+ "labelled": "labeled",
882
+ "labelling": "labeling",
883
+ "labour": "labor",
884
+ "laboured": "labored",
885
+ "labourer": "laborer",
886
+ "labourers": "laborers",
887
+ "labouring": "laboring",
888
+ "labours": "labors",
889
+ "lacklustre": "lackluster",
890
+ "legalisation": "legalization",
891
+ "legalise": "legalize",
892
+ "legalised": "legalized",
893
+ "legalises": "legalizes",
894
+ "legalising": "legalizing",
895
+ "legitimise": "legitimize",
896
+ "legitimised": "legitimized",
897
+ "legitimises": "legitimizes",
898
+ "legitimising": "legitimizing",
899
+ "leukaemia": "leukemia",
900
+ "levelled": "leveled",
901
+ "leveller": "leveler",
902
+ "levellers": "levelers",
903
+ "levelling": "leveling",
904
+ "libelled": "libeled",
905
+ "libelling": "libeling",
906
+ "libellous": "libelous",
907
+ "liberalisation": "liberalization",
908
+ "liberalise": "liberalize",
909
+ "liberalised": "liberalized",
910
+ "liberalises": "liberalizes",
911
+ "liberalising": "liberalizing",
912
+ "licence": "license",
913
+ "licenced": "licensed",
914
+ "licences": "licenses",
915
+ "licencing": "licensing",
916
+ "likeable": "likable",
917
+ "lionisation": "lionization",
918
+ "lionise": "lionize",
919
+ "lionised": "lionized",
920
+ "lionises": "lionizes",
921
+ "lionising": "lionizing",
922
+ "liquidise": "liquidize",
923
+ "liquidised": "liquidized",
924
+ "liquidiser": "liquidizer",
925
+ "liquidisers": "liquidizers",
926
+ "liquidises": "liquidizes",
927
+ "liquidising": "liquidizing",
928
+ "litre": "liter",
929
+ "litres": "liters",
930
+ "localise": "localize",
931
+ "localised": "localized",
932
+ "localises": "localizes",
933
+ "localising": "localizing",
934
+ "louvre": "louver",
935
+ "louvred": "louvered",
936
+ "louvres": "louvers",
937
+ "lustre": "luster",
938
+ "magnetise": "magnetize",
939
+ "magnetised": "magnetized",
940
+ "magnetises": "magnetizes",
941
+ "magnetising": "magnetizing",
942
+ "manoeuvrability": "maneuverability",
943
+ "manoeuvrable": "maneuverable",
944
+ "manoeuvre": "maneuver",
945
+ "manoeuvred": "maneuvered",
946
+ "manoeuvres": "maneuvers",
947
+ "manoeuvring": "maneuvering",
948
+ "manoeuvrings": "maneuverings",
949
+ "marginalisation": "marginalization",
950
+ "marginalise": "marginalize",
951
+ "marginalised": "marginalized",
952
+ "marginalises": "marginalizes",
953
+ "marginalising": "marginalizing",
954
+ "marshalled": "marshaled",
955
+ "marshalling": "marshaling",
956
+ "marvelled": "marveled",
957
+ "marvelling": "marveling",
958
+ "marvellous": "marvelous",
959
+ "marvellously": "marvelously",
960
+ "materialisation": "materialization",
961
+ "materialise": "materialize",
962
+ "materialised": "materialized",
963
+ "materialises": "materializes",
964
+ "materialising": "materializing",
965
+ "maximisation": "maximization",
966
+ "maximise": "maximize",
967
+ "maximised": "maximized",
968
+ "maximises": "maximizes",
969
+ "maximising": "maximizing",
970
+ "meagre": "meager",
971
+ "mechanisation": "mechanization",
972
+ "mechanise": "mechanize",
973
+ "mechanised": "mechanized",
974
+ "mechanises": "mechanizes",
975
+ "mechanising": "mechanizing",
976
+ "mediaeval": "medieval",
977
+ "memorialise": "memorialize",
978
+ "memorialised": "memorialized",
979
+ "memorialises": "memorializes",
980
+ "memorialising": "memorializing",
981
+ "memorise": "memorize",
982
+ "memorised": "memorized",
983
+ "memorises": "memorizes",
984
+ "memorising": "memorizing",
985
+ "mesmerise": "mesmerize",
986
+ "mesmerised": "mesmerized",
987
+ "mesmerises": "mesmerizes",
988
+ "mesmerising": "mesmerizing",
989
+ "metabolise": "metabolize",
990
+ "metabolised": "metabolized",
991
+ "metabolises": "metabolizes",
992
+ "metabolising": "metabolizing",
993
+ "metre": "meter",
994
+ "metres": "meters",
995
+ "mhm": "hmm",
996
+ "micrometre": "micrometer",
997
+ "micrometres": "micrometers",
998
+ "militarise": "militarize",
999
+ "militarised": "militarized",
1000
+ "militarises": "militarizes",
1001
+ "militarising": "militarizing",
1002
+ "milligramme": "milligram",
1003
+ "milligrammes": "milligrams",
1004
+ "millilitre": "milliliter",
1005
+ "millilitres": "milliliters",
1006
+ "millimetre": "millimeter",
1007
+ "millimetres": "millimeters",
1008
+ "miniaturisation": "miniaturization",
1009
+ "miniaturise": "miniaturize",
1010
+ "miniaturised": "miniaturized",
1011
+ "miniaturises": "miniaturizes",
1012
+ "miniaturising": "miniaturizing",
1013
+ "minibusses": "minibuses",
1014
+ "minimise": "minimize",
1015
+ "minimised": "minimized",
1016
+ "minimises": "minimizes",
1017
+ "minimising": "minimizing",
1018
+ "misbehaviour": "misbehavior",
1019
+ "misdemeanour": "misdemeanor",
1020
+ "misdemeanours": "misdemeanors",
1021
+ "misspelt": "misspelled",
1022
+ "mitre": "miter",
1023
+ "mitres": "miters",
1024
+ "mm": "hmm",
1025
+ "mmm": "hmm",
1026
+ "mobilisation": "mobilization",
1027
+ "mobilise": "mobilize",
1028
+ "mobilised": "mobilized",
1029
+ "mobilises": "mobilizes",
1030
+ "mobilising": "mobilizing",
1031
+ "modelled": "modeled",
1032
+ "modeller": "modeler",
1033
+ "modellers": "modelers",
1034
+ "modelling": "modeling",
1035
+ "modernise": "modernize",
1036
+ "modernised": "modernized",
1037
+ "modernises": "modernizes",
1038
+ "modernising": "modernizing",
1039
+ "moisturise": "moisturize",
1040
+ "moisturised": "moisturized",
1041
+ "moisturiser": "moisturizer",
1042
+ "moisturisers": "moisturizers",
1043
+ "moisturises": "moisturizes",
1044
+ "moisturising": "moisturizing",
1045
+ "monologue": "monolog",
1046
+ "monologues": "monologs",
1047
+ "monopolisation": "monopolization",
1048
+ "monopolise": "monopolize",
1049
+ "monopolised": "monopolized",
1050
+ "monopolises": "monopolizes",
1051
+ "monopolising": "monopolizing",
1052
+ "moralise": "moralize",
1053
+ "moralised": "moralized",
1054
+ "moralises": "moralizes",
1055
+ "moralising": "moralizing",
1056
+ "motorised": "motorized",
1057
+ "mould": "mold",
1058
+ "moulded": "molded",
1059
+ "moulder": "molder",
1060
+ "mouldered": "moldered",
1061
+ "mouldering": "moldering",
1062
+ "moulders": "molders",
1063
+ "mouldier": "moldier",
1064
+ "mouldiest": "moldiest",
1065
+ "moulding": "molding",
1066
+ "mouldings": "moldings",
1067
+ "moulds": "molds",
1068
+ "mouldy": "moldy",
1069
+ "moult": "molt",
1070
+ "moulted": "molted",
1071
+ "moulting": "molting",
1072
+ "moults": "molts",
1073
+ "moustache": "mustache",
1074
+ "moustached": "mustached",
1075
+ "moustaches": "mustaches",
1076
+ "moustachioed": "mustachioed",
1077
+ "multicoloured": "multicolored",
1078
+ "nationalisation": "nationalization",
1079
+ "nationalisations": "nationalizations",
1080
+ "nationalise": "nationalize",
1081
+ "nationalised": "nationalized",
1082
+ "nationalises": "nationalizes",
1083
+ "nationalising": "nationalizing",
1084
+ "naturalisation": "naturalization",
1085
+ "naturalise": "naturalize",
1086
+ "naturalised": "naturalized",
1087
+ "naturalises": "naturalizes",
1088
+ "naturalising": "naturalizing",
1089
+ "neighbour": "neighbor",
1090
+ "neighbourhood": "neighborhood",
1091
+ "neighbourhoods": "neighborhoods",
1092
+ "neighbouring": "neighboring",
1093
+ "neighbourliness": "neighborliness",
1094
+ "neighbourly": "neighborly",
1095
+ "neighbours": "neighbors",
1096
+ "neutralisation": "neutralization",
1097
+ "neutralise": "neutralize",
1098
+ "neutralised": "neutralized",
1099
+ "neutralises": "neutralizes",
1100
+ "neutralising": "neutralizing",
1101
+ "normalisation": "normalization",
1102
+ "normalise": "normalize",
1103
+ "normalised": "normalized",
1104
+ "normalises": "normalizes",
1105
+ "normalising": "normalizing",
1106
+ "odour": "odor",
1107
+ "odourless": "odorless",
1108
+ "odours": "odors",
1109
+ "oesophagus": "esophagus",
1110
+ "oesophaguses": "esophaguses",
1111
+ "oestrogen": "estrogen",
1112
+ "offence": "offense",
1113
+ "offences": "offenses",
1114
+ "omelette": "omelet",
1115
+ "omelettes": "omelets",
1116
+ "optimise": "optimize",
1117
+ "optimised": "optimized",
1118
+ "optimises": "optimizes",
1119
+ "optimising": "optimizing",
1120
+ "organisation": "organization",
1121
+ "organisational": "organizational",
1122
+ "organisations": "organizations",
1123
+ "organise": "organize",
1124
+ "organised": "organized",
1125
+ "organiser": "organizer",
1126
+ "organisers": "organizers",
1127
+ "organises": "organizes",
1128
+ "organising": "organizing",
1129
+ "orthopaedic": "orthopedic",
1130
+ "orthopaedics": "orthopedics",
1131
+ "ostracise": "ostracize",
1132
+ "ostracised": "ostracized",
1133
+ "ostracises": "ostracizes",
1134
+ "ostracising": "ostracizing",
1135
+ "outmanoeuvre": "outmaneuver",
1136
+ "outmanoeuvred": "outmaneuvered",
1137
+ "outmanoeuvres": "outmaneuvers",
1138
+ "outmanoeuvring": "outmaneuvering",
1139
+ "overemphasise": "overemphasize",
1140
+ "overemphasised": "overemphasized",
1141
+ "overemphasises": "overemphasizes",
1142
+ "overemphasising": "overemphasizing",
1143
+ "oxidisation": "oxidization",
1144
+ "oxidise": "oxidize",
1145
+ "oxidised": "oxidized",
1146
+ "oxidises": "oxidizes",
1147
+ "oxidising": "oxidizing",
1148
+ "paederast": "pederast",
1149
+ "paederasts": "pederasts",
1150
+ "paediatric": "pediatric",
1151
+ "paediatrician": "pediatrician",
1152
+ "paediatricians": "pediatricians",
1153
+ "paediatrics": "pediatrics",
1154
+ "paedophile": "pedophile",
1155
+ "paedophiles": "pedophiles",
1156
+ "paedophilia": "pedophilia",
1157
+ "palaeolithic": "paleolithic",
1158
+ "palaeontologist": "paleontologist",
1159
+ "palaeontologists": "paleontologists",
1160
+ "palaeontology": "paleontology",
1161
+ "panelled": "paneled",
1162
+ "panelling": "paneling",
1163
+ "panellist": "panelist",
1164
+ "panellists": "panelists",
1165
+ "paralyse": "paralyze",
1166
+ "paralysed": "paralyzed",
1167
+ "paralyses": "paralyzes",
1168
+ "paralysing": "paralyzing",
1169
+ "parcelled": "parceled",
1170
+ "parcelling": "parceling",
1171
+ "parlour": "parlor",
1172
+ "parlours": "parlors",
1173
+ "particularise": "particularize",
1174
+ "particularised": "particularized",
1175
+ "particularises": "particularizes",
1176
+ "particularising": "particularizing",
1177
+ "passivisation": "passivization",
1178
+ "passivise": "passivize",
1179
+ "passivised": "passivized",
1180
+ "passivises": "passivizes",
1181
+ "passivising": "passivizing",
1182
+ "pasteurisation": "pasteurization",
1183
+ "pasteurise": "pasteurize",
1184
+ "pasteurised": "pasteurized",
1185
+ "pasteurises": "pasteurizes",
1186
+ "pasteurising": "pasteurizing",
1187
+ "patronise": "patronize",
1188
+ "patronised": "patronized",
1189
+ "patronises": "patronizes",
1190
+ "patronising": "patronizing",
1191
+ "patronisingly": "patronizingly",
1192
+ "pedalled": "pedaled",
1193
+ "pedalling": "pedaling",
1194
+ "pedestrianisation": "pedestrianization",
1195
+ "pedestrianise": "pedestrianize",
1196
+ "pedestrianised": "pedestrianized",
1197
+ "pedestrianises": "pedestrianizes",
1198
+ "pedestrianising": "pedestrianizing",
1199
+ "penalise": "penalize",
1200
+ "penalised": "penalized",
1201
+ "penalises": "penalizes",
1202
+ "penalising": "penalizing",
1203
+ "pencilled": "penciled",
1204
+ "pencilling": "penciling",
1205
+ "personalise": "personalize",
1206
+ "personalised": "personalized",
1207
+ "personalises": "personalizes",
1208
+ "personalising": "personalizing",
1209
+ "pharmacopoeia": "pharmacopeia",
1210
+ "pharmacopoeias": "pharmacopeias",
1211
+ "philosophise": "philosophize",
1212
+ "philosophised": "philosophized",
1213
+ "philosophises": "philosophizes",
1214
+ "philosophising": "philosophizing",
1215
+ "philtre": "filter",
1216
+ "philtres": "filters",
1217
+ "phoney": "phony",
1218
+ "plagiarise": "plagiarize",
1219
+ "plagiarised": "plagiarized",
1220
+ "plagiarises": "plagiarizes",
1221
+ "plagiarising": "plagiarizing",
1222
+ "plough": "plow",
1223
+ "ploughed": "plowed",
1224
+ "ploughing": "plowing",
1225
+ "ploughman": "plowman",
1226
+ "ploughmen": "plowmen",
1227
+ "ploughs": "plows",
1228
+ "ploughshare": "plowshare",
1229
+ "ploughshares": "plowshares",
1230
+ "polarisation": "polarization",
1231
+ "polarise": "polarize",
1232
+ "polarised": "polarized",
1233
+ "polarises": "polarizes",
1234
+ "polarising": "polarizing",
1235
+ "politicisation": "politicization",
1236
+ "politicise": "politicize",
1237
+ "politicised": "politicized",
1238
+ "politicises": "politicizes",
1239
+ "politicising": "politicizing",
1240
+ "popularisation": "popularization",
1241
+ "popularise": "popularize",
1242
+ "popularised": "popularized",
1243
+ "popularises": "popularizes",
1244
+ "popularising": "popularizing",
1245
+ "pouffe": "pouf",
1246
+ "pouffes": "poufs",
1247
+ "practise": "practice",
1248
+ "practised": "practiced",
1249
+ "practises": "practices",
1250
+ "practising": "practicing",
1251
+ "praesidium": "presidium",
1252
+ "praesidiums": "presidiums",
1253
+ "pressurisation": "pressurization",
1254
+ "pressurise": "pressurize",
1255
+ "pressurised": "pressurized",
1256
+ "pressurises": "pressurizes",
1257
+ "pressurising": "pressurizing",
1258
+ "pretence": "pretense",
1259
+ "pretences": "pretenses",
1260
+ "primaeval": "primeval",
1261
+ "prioritisation": "prioritization",
1262
+ "prioritise": "prioritize",
1263
+ "prioritised": "prioritized",
1264
+ "prioritises": "prioritizes",
1265
+ "prioritising": "prioritizing",
1266
+ "privatisation": "privatization",
1267
+ "privatisations": "privatizations",
1268
+ "privatise": "privatize",
1269
+ "privatised": "privatized",
1270
+ "privatises": "privatizes",
1271
+ "privatising": "privatizing",
1272
+ "professionalisation": "professionalization",
1273
+ "professionalise": "professionalize",
1274
+ "professionalised": "professionalized",
1275
+ "professionalises": "professionalizes",
1276
+ "professionalising": "professionalizing",
1277
+ "programme": "program",
1278
+ "programmes": "programs",
1279
+ "prologue": "prolog",
1280
+ "prologues": "prologs",
1281
+ "propagandise": "propagandize",
1282
+ "propagandised": "propagandized",
1283
+ "propagandises": "propagandizes",
1284
+ "propagandising": "propagandizing",
1285
+ "proselytise": "proselytize",
1286
+ "proselytised": "proselytized",
1287
+ "proselytiser": "proselytizer",
1288
+ "proselytisers": "proselytizers",
1289
+ "proselytises": "proselytizes",
1290
+ "proselytising": "proselytizing",
1291
+ "psychoanalyse": "psychoanalyze",
1292
+ "psychoanalysed": "psychoanalyzed",
1293
+ "psychoanalyses": "psychoanalyzes",
1294
+ "psychoanalysing": "psychoanalyzing",
1295
+ "publicise": "publicize",
1296
+ "publicised": "publicized",
1297
+ "publicises": "publicizes",
1298
+ "publicising": "publicizing",
1299
+ "pulverisation": "pulverization",
1300
+ "pulverise": "pulverize",
1301
+ "pulverised": "pulverized",
1302
+ "pulverises": "pulverizes",
1303
+ "pulverising": "pulverizing",
1304
+ "pummelled": "pummel",
1305
+ "pummelling": "pummeled",
1306
+ "pyjama": "pajama",
1307
+ "pyjamas": "pajamas",
1308
+ "pzazz": "pizzazz",
1309
+ "quarrelled": "quarreled",
1310
+ "quarrelling": "quarreling",
1311
+ "radicalise": "radicalize",
1312
+ "radicalised": "radicalized",
1313
+ "radicalises": "radicalizes",
1314
+ "radicalising": "radicalizing",
1315
+ "rancour": "rancor",
1316
+ "randomise": "randomize",
1317
+ "randomised": "randomized",
1318
+ "randomises": "randomizes",
1319
+ "randomising": "randomizing",
1320
+ "rationalisation": "rationalization",
1321
+ "rationalisations": "rationalizations",
1322
+ "rationalise": "rationalize",
1323
+ "rationalised": "rationalized",
1324
+ "rationalises": "rationalizes",
1325
+ "rationalising": "rationalizing",
1326
+ "ravelled": "raveled",
1327
+ "ravelling": "raveling",
1328
+ "realisable": "realizable",
1329
+ "realisation": "realization",
1330
+ "realisations": "realizations",
1331
+ "realise": "realize",
1332
+ "realised": "realized",
1333
+ "realises": "realizes",
1334
+ "realising": "realizing",
1335
+ "recognisable": "recognizable",
1336
+ "recognisably": "recognizably",
1337
+ "recognisance": "recognizance",
1338
+ "recognise": "recognize",
1339
+ "recognised": "recognized",
1340
+ "recognises": "recognizes",
1341
+ "recognising": "recognizing",
1342
+ "reconnoitre": "reconnoiter",
1343
+ "reconnoitred": "reconnoitered",
1344
+ "reconnoitres": "reconnoiters",
1345
+ "reconnoitring": "reconnoitering",
1346
+ "refuelled": "refueled",
1347
+ "refuelling": "refueling",
1348
+ "regularisation": "regularization",
1349
+ "regularise": "regularize",
1350
+ "regularised": "regularized",
1351
+ "regularises": "regularizes",
1352
+ "regularising": "regularizing",
1353
+ "remodelled": "remodeled",
1354
+ "remodelling": "remodeling",
1355
+ "remould": "remold",
1356
+ "remoulded": "remolded",
1357
+ "remoulding": "remolding",
1358
+ "remoulds": "remolds",
1359
+ "reorganisation": "reorganization",
1360
+ "reorganisations": "reorganizations",
1361
+ "reorganise": "reorganize",
1362
+ "reorganised": "reorganized",
1363
+ "reorganises": "reorganizes",
1364
+ "reorganising": "reorganizing",
1365
+ "revelled": "reveled",
1366
+ "reveller": "reveler",
1367
+ "revellers": "revelers",
1368
+ "revelling": "reveling",
1369
+ "revitalise": "revitalize",
1370
+ "revitalised": "revitalized",
1371
+ "revitalises": "revitalizes",
1372
+ "revitalising": "revitalizing",
1373
+ "revolutionise": "revolutionize",
1374
+ "revolutionised": "revolutionized",
1375
+ "revolutionises": "revolutionizes",
1376
+ "revolutionising": "revolutionizing",
1377
+ "rhapsodise": "rhapsodize",
1378
+ "rhapsodised": "rhapsodized",
1379
+ "rhapsodises": "rhapsodizes",
1380
+ "rhapsodising": "rhapsodizing",
1381
+ "rigour": "rigor",
1382
+ "rigours": "rigors",
1383
+ "ritualised": "ritualized",
1384
+ "rivalled": "rivaled",
1385
+ "rivalling": "rivaling",
1386
+ "romanticise": "romanticize",
1387
+ "romanticised": "romanticized",
1388
+ "romanticises": "romanticizes",
1389
+ "romanticising": "romanticizing",
1390
+ "rumour": "rumor",
1391
+ "rumoured": "rumored",
1392
+ "rumours": "rumors",
1393
+ "sabre": "saber",
1394
+ "sabres": "sabers",
1395
+ "saltpetre": "saltpeter",
1396
+ "sanitise": "sanitize",
1397
+ "sanitised": "sanitized",
1398
+ "sanitises": "sanitizes",
1399
+ "sanitising": "sanitizing",
1400
+ "satirise": "satirize",
1401
+ "satirised": "satirized",
1402
+ "satirises": "satirizes",
1403
+ "satirising": "satirizing",
1404
+ "saviour": "savior",
1405
+ "saviours": "saviors",
1406
+ "savour": "savor",
1407
+ "savoured": "savored",
1408
+ "savouries": "savories",
1409
+ "savouring": "savoring",
1410
+ "savours": "savors",
1411
+ "savoury": "savory",
1412
+ "scandalise": "scandalize",
1413
+ "scandalised": "scandalized",
1414
+ "scandalises": "scandalizes",
1415
+ "scandalising": "scandalizing",
1416
+ "sceptic": "skeptic",
1417
+ "sceptical": "skeptical",
1418
+ "sceptically": "skeptically",
1419
+ "scepticism": "skepticism",
1420
+ "sceptics": "skeptics",
1421
+ "sceptre": "scepter",
1422
+ "sceptres": "scepters",
1423
+ "scrutinise": "scrutinize",
1424
+ "scrutinised": "scrutinized",
1425
+ "scrutinises": "scrutinizes",
1426
+ "scrutinising": "scrutinizing",
1427
+ "secularisation": "secularization",
1428
+ "secularise": "secularize",
1429
+ "secularised": "secularized",
1430
+ "secularises": "secularizes",
1431
+ "secularising": "secularizing",
1432
+ "sensationalise": "sensationalize",
1433
+ "sensationalised": "sensationalized",
1434
+ "sensationalises": "sensationalizes",
1435
+ "sensationalising": "sensationalizing",
1436
+ "sensitise": "sensitize",
1437
+ "sensitised": "sensitized",
1438
+ "sensitises": "sensitizes",
1439
+ "sensitising": "sensitizing",
1440
+ "sentimentalise": "sentimentalize",
1441
+ "sentimentalised": "sentimentalized",
1442
+ "sentimentalises": "sentimentalizes",
1443
+ "sentimentalising": "sentimentalizing",
1444
+ "sepulchre": "sepulcher",
1445
+ "sepulchres": "sepulchers",
1446
+ "serialisation": "serialization",
1447
+ "serialisations": "serializations",
1448
+ "serialise": "serialize",
1449
+ "serialised": "serialized",
1450
+ "serialises": "serializes",
1451
+ "serialising": "serializing",
1452
+ "sermonise": "sermonize",
1453
+ "sermonised": "sermonized",
1454
+ "sermonises": "sermonizes",
1455
+ "sermonising": "sermonizing",
1456
+ "sheikh": "sheik",
1457
+ "shovelled": "shoveled",
1458
+ "shovelling": "shoveling",
1459
+ "shrivelled": "shriveled",
1460
+ "shrivelling": "shriveling",
1461
+ "signalise": "signalize",
1462
+ "signalised": "signalized",
1463
+ "signalises": "signalizes",
1464
+ "signalising": "signalizing",
1465
+ "signalled": "signaled",
1466
+ "signalling": "signaling",
1467
+ "smoulder": "smolder",
1468
+ "smouldered": "smoldered",
1469
+ "smouldering": "smoldering",
1470
+ "smoulders": "smolders",
1471
+ "snivelled": "sniveled",
1472
+ "snivelling": "sniveling",
1473
+ "snorkelled": "snorkeled",
1474
+ "snorkelling": "snorkeling",
1475
+ "snowplough": "snowplow",
1476
+ "snowploughs": "snowplow",
1477
+ "socialisation": "socialization",
1478
+ "socialise": "socialize",
1479
+ "socialised": "socialized",
1480
+ "socialises": "socializes",
1481
+ "socialising": "socializing",
1482
+ "sodomise": "sodomize",
1483
+ "sodomised": "sodomized",
1484
+ "sodomises": "sodomizes",
1485
+ "sodomising": "sodomizing",
1486
+ "solemnise": "solemnize",
1487
+ "solemnised": "solemnized",
1488
+ "solemnises": "solemnizes",
1489
+ "solemnising": "solemnizing",
1490
+ "sombre": "somber",
1491
+ "specialisation": "specialization",
1492
+ "specialisations": "specializations",
1493
+ "specialise": "specialize",
1494
+ "specialised": "specialized",
1495
+ "specialises": "specializes",
1496
+ "specialising": "specializing",
1497
+ "spectre": "specter",
1498
+ "spectres": "specters",
1499
+ "spiralled": "spiraled",
1500
+ "spiralling": "spiraling",
1501
+ "splendour": "splendor",
1502
+ "splendours": "splendors",
1503
+ "squirrelled": "squirreled",
1504
+ "squirrelling": "squirreling",
1505
+ "stabilisation": "stabilization",
1506
+ "stabilise": "stabilize",
1507
+ "stabilised": "stabilized",
1508
+ "stabiliser": "stabilizer",
1509
+ "stabilisers": "stabilizers",
1510
+ "stabilises": "stabilizes",
1511
+ "stabilising": "stabilizing",
1512
+ "standardisation": "standardization",
1513
+ "standardise": "standardize",
1514
+ "standardised": "standardized",
1515
+ "standardises": "standardizes",
1516
+ "standardising": "standardizing",
1517
+ "stencilled": "stenciled",
1518
+ "stencilling": "stenciling",
1519
+ "sterilisation": "sterilization",
1520
+ "sterilisations": "sterilizations",
1521
+ "sterilise": "sterilize",
1522
+ "sterilised": "sterilized",
1523
+ "steriliser": "sterilizer",
1524
+ "sterilisers": "sterilizers",
1525
+ "sterilises": "sterilizes",
1526
+ "sterilising": "sterilizing",
1527
+ "stigmatisation": "stigmatization",
1528
+ "stigmatise": "stigmatize",
1529
+ "stigmatised": "stigmatized",
1530
+ "stigmatises": "stigmatizes",
1531
+ "stigmatising": "stigmatizing",
1532
+ "storey": "story",
1533
+ "storeys": "stories",
1534
+ "subsidisation": "subsidization",
1535
+ "subsidise": "subsidize",
1536
+ "subsidised": "subsidized",
1537
+ "subsidiser": "subsidizer",
1538
+ "subsidisers": "subsidizers",
1539
+ "subsidises": "subsidizes",
1540
+ "subsidising": "subsidizing",
1541
+ "succour": "succor",
1542
+ "succoured": "succored",
1543
+ "succouring": "succoring",
1544
+ "succours": "succors",
1545
+ "sulphate": "sulfate",
1546
+ "sulphates": "sulfates",
1547
+ "sulphide": "sulfide",
1548
+ "sulphides": "sulfides",
1549
+ "sulphur": "sulfur",
1550
+ "sulphurous": "sulfurous",
1551
+ "summarise": "summarize",
1552
+ "summarised": "summarized",
1553
+ "summarises": "summarizes",
1554
+ "summarising": "summarizing",
1555
+ "swivelled": "swiveled",
1556
+ "swivelling": "swiveling",
1557
+ "symbolise": "symbolize",
1558
+ "symbolised": "symbolized",
1559
+ "symbolises": "symbolizes",
1560
+ "symbolising": "symbolizing",
1561
+ "sympathise": "sympathize",
1562
+ "sympathised": "sympathized",
1563
+ "sympathiser": "sympathizer",
1564
+ "sympathisers": "sympathizers",
1565
+ "sympathises": "sympathizes",
1566
+ "sympathising": "sympathizing",
1567
+ "synchronisation": "synchronization",
1568
+ "synchronise": "synchronize",
1569
+ "synchronised": "synchronized",
1570
+ "synchronises": "synchronizes",
1571
+ "synchronising": "synchronizing",
1572
+ "synthesise": "synthesize",
1573
+ "synthesised": "synthesized",
1574
+ "synthesiser": "synthesizer",
1575
+ "synthesisers": "synthesizers",
1576
+ "synthesises": "synthesizes",
1577
+ "synthesising": "synthesizing",
1578
+ "syphon": "siphon",
1579
+ "syphoned": "siphoned",
1580
+ "syphoning": "siphoning",
1581
+ "syphons": "siphons",
1582
+ "systematisation": "systematization",
1583
+ "systematise": "systematize",
1584
+ "systematised": "systematized",
1585
+ "systematises": "systematizes",
1586
+ "systematising": "systematizing",
1587
+ "tantalise": "tantalize",
1588
+ "tantalised": "tantalized",
1589
+ "tantalises": "tantalizes",
1590
+ "tantalising": "tantalizing",
1591
+ "tantalisingly": "tantalizingly",
1592
+ "tasselled": "tasseled",
1593
+ "technicolour": "technicolor",
1594
+ "temporise": "temporize",
1595
+ "temporised": "temporized",
1596
+ "temporises": "temporizes",
1597
+ "temporising": "temporizing",
1598
+ "tenderise": "tenderize",
1599
+ "tenderised": "tenderized",
1600
+ "tenderises": "tenderizes",
1601
+ "tenderising": "tenderizing",
1602
+ "terrorise": "terrorize",
1603
+ "terrorised": "terrorized",
1604
+ "terrorises": "terrorizes",
1605
+ "terrorising": "terrorizing",
1606
+ "theatre": "theater",
1607
+ "theatregoer": "theatergoer",
1608
+ "theatregoers": "theatergoers",
1609
+ "theatres": "theaters",
1610
+ "theorise": "theorize",
1611
+ "theorised": "theorized",
1612
+ "theorises": "theorizes",
1613
+ "theorising": "theorizing",
1614
+ "tonne": "ton",
1615
+ "tonnes": "tons",
1616
+ "towelled": "toweled",
1617
+ "towelling": "toweling",
1618
+ "toxaemia": "toxemia",
1619
+ "tranquillise": "tranquilize",
1620
+ "tranquillised": "tranquilized",
1621
+ "tranquilliser": "tranquilizer",
1622
+ "tranquillisers": "tranquilizers",
1623
+ "tranquillises": "tranquilizes",
1624
+ "tranquillising": "tranquilizing",
1625
+ "tranquillity": "tranquility",
1626
+ "tranquillize": "tranquilize",
1627
+ "tranquillized": "tranquilized",
1628
+ "tranquillizer": "tranquilizer",
1629
+ "tranquillizers": "tranquilizers",
1630
+ "tranquillizes": "tranquilizes",
1631
+ "tranquillizing": "tranquilizing",
1632
+ "tranquilly": "tranquility",
1633
+ "transistorised": "transistorized",
1634
+ "traumatise": "traumatize",
1635
+ "traumatised": "traumatized",
1636
+ "traumatises": "traumatizes",
1637
+ "traumatising": "traumatizing",
1638
+ "travelled": "traveled",
1639
+ "traveller": "traveler",
1640
+ "travellers": "travelers",
1641
+ "travelling": "traveling",
1642
+ "travelog": "travelogue",
1643
+ "travelogs": "travelogues",
1644
+ "trialled": "trialed",
1645
+ "trialling": "trialing",
1646
+ "tricolour": "tricolor",
1647
+ "tricolours": "tricolors",
1648
+ "trivialise": "trivialize",
1649
+ "trivialised": "trivialized",
1650
+ "trivialises": "trivializes",
1651
+ "trivialising": "trivializing",
1652
+ "tumour": "tumor",
1653
+ "tumours": "tumors",
1654
+ "tunnelled": "tunneled",
1655
+ "tunnelling": "tunneling",
1656
+ "tyrannise": "tyrannize",
1657
+ "tyrannised": "tyrannized",
1658
+ "tyrannises": "tyrannizes",
1659
+ "tyrannising": "tyrannizing",
1660
+ "tyre": "tire",
1661
+ "tyres": "tires",
1662
+ "unauthorised": "unauthorized",
1663
+ "uncivilised": "uncivilized",
1664
+ "underutilised": "underutilized",
1665
+ "unequalled": "unequaled",
1666
+ "unfavourable": "unfavorable",
1667
+ "unfavourably": "unfavorably",
1668
+ "unionisation": "unionization",
1669
+ "unionise": "unionize",
1670
+ "unionised": "unionized",
1671
+ "unionises": "unionizes",
1672
+ "unionising": "unionizing",
1673
+ "unorganised": "unorganized",
1674
+ "unravelled": "unraveled",
1675
+ "unravelling": "unraveling",
1676
+ "unrecognisable": "unrecognizable",
1677
+ "unrecognised": "unrecognized",
1678
+ "unrivalled": "unrivaled",
1679
+ "unsavoury": "unsavory",
1680
+ "untrammelled": "untrammeled",
1681
+ "urbanisation": "urbanization",
1682
+ "urbanise": "urbanize",
1683
+ "urbanised": "urbanized",
1684
+ "urbanises": "urbanizes",
1685
+ "urbanising": "urbanizing",
1686
+ "utilisable": "utilizable",
1687
+ "utilisation": "utilization",
1688
+ "utilise": "utilize",
1689
+ "utilised": "utilized",
1690
+ "utilises": "utilizes",
1691
+ "utilising": "utilizing",
1692
+ "valour": "valor",
1693
+ "vandalise": "vandalize",
1694
+ "vandalised": "vandalized",
1695
+ "vandalises": "vandalizes",
1696
+ "vandalising": "vandalizing",
1697
+ "vaporisation": "vaporization",
1698
+ "vaporise": "vaporize",
1699
+ "vaporised": "vaporized",
1700
+ "vaporises": "vaporizes",
1701
+ "vaporising": "vaporizing",
1702
+ "vapour": "vapor",
1703
+ "vapours": "vapors",
1704
+ "verbalise": "verbalize",
1705
+ "verbalised": "verbalized",
1706
+ "verbalises": "verbalizes",
1707
+ "verbalising": "verbalizing",
1708
+ "victimisation": "victimization",
1709
+ "victimise": "victimize",
1710
+ "victimised": "victimized",
1711
+ "victimises": "victimizes",
1712
+ "victimising": "victimizing",
1713
+ "videodisc": "videodisk",
1714
+ "videodiscs": "videodisks",
1715
+ "vigour": "vigor",
1716
+ "visualisation": "visualization",
1717
+ "visualisations": "visualizations",
1718
+ "visualise": "visualize",
1719
+ "visualised": "visualized",
1720
+ "visualises": "visualizes",
1721
+ "visualising": "visualizing",
1722
+ "vocalisation": "vocalization",
1723
+ "vocalisations": "vocalizations",
1724
+ "vocalise": "vocalize",
1725
+ "vocalised": "vocalized",
1726
+ "vocalises": "vocalizes",
1727
+ "vocalising": "vocalizing",
1728
+ "vulcanised": "vulcanized",
1729
+ "vulgarisation": "vulgarization",
1730
+ "vulgarise": "vulgarize",
1731
+ "vulgarised": "vulgarized",
1732
+ "vulgarises": "vulgarizes",
1733
+ "vulgarising": "vulgarizing",
1734
+ "waggon": "wagon",
1735
+ "waggons": "wagons",
1736
+ "watercolour": "watercolor",
1737
+ "watercolours": "watercolors",
1738
+ "weaselled": "weaseled",
1739
+ "weaselling": "weaseling",
1740
+ "westernisation": "westernization",
1741
+ "westernise": "westernize",
1742
+ "westernised": "westernized",
1743
+ "westernises": "westernizes",
1744
+ "westernising": "westernizing",
1745
+ "womanise": "womanize",
1746
+ "womanised": "womanized",
1747
+ "womaniser": "womanizer",
1748
+ "womanisers": "womanizers",
1749
+ "womanises": "womanizes",
1750
+ "womanising": "womanizing",
1751
+ "woollen": "woolen",
1752
+ "woollens": "woolens",
1753
+ "woollies": "woolies",
1754
+ "woolly": "wooly",
1755
+ "worshipped": "worshiped",
1756
+ "worshipper": "worshiper",
1757
+ "worshipping": "worshiping",
1758
+ "yodelled": "yodeled",
1759
+ "yodelling": "yodeling",
1760
+ "yoghourt": "yogurt",
1761
+ "yoghourts": "yogurts",
1762
+ "yoghurt": "yogurt",
1763
+ "yoghurts": "yogurts",
1764
+ }
1765
+
1766
+ # non-ASCII letters that are not separated by "NFKD" normalization
1767
+ ADDITIONAL_DIACRITICS = {
1768
+ "œ": "oe",
1769
+ "Œ": "OE",
1770
+ "ø": "o",
1771
+ "Ø": "O",
1772
+ "æ": "ae",
1773
+ "Æ": "AE",
1774
+ "ß": "ss",
1775
+ "ẞ": "SS",
1776
+ "đ": "d",
1777
+ "Đ": "D",
1778
+ "ð": "d",
1779
+ "Ð": "D",
1780
+ "þ": "th",
1781
+ "Þ": "th",
1782
+ "ł": "l",
1783
+ "Ł": "L",
1784
+ }
1785
+
1786
+
1787
+ def remove_symbols_and_diacritics(s: str, keep=""):
1788
+ """
1789
+ Replace any other markers, symbols, and punctuations with a space, and drop any diacritics
1790
+ (category 'Mn' and some manual mappings)
1791
+ """
1792
+
1793
+ def replace_character(char):
1794
+ if char in keep:
1795
+ return char
1796
+ elif char in ADDITIONAL_DIACRITICS:
1797
+ return ADDITIONAL_DIACRITICS[char]
1798
+
1799
+ elif unicodedata.category(char) == "Mn":
1800
+ return ""
1801
+
1802
+ elif unicodedata.category(char)[0] in "MSP":
1803
+ return " "
1804
+
1805
+ return char
1806
+
1807
+ return "".join(replace_character(c) for c in unicodedata.normalize("NFKD", s))
1808
+
1809
+
1810
+ def remove_symbols(s: str):
1811
+ """
1812
+ Replace any other markers, symbols, punctuations with a space, keeping diacritics
1813
+ """
1814
+ return "".join(
1815
+ " " if unicodedata.category(c)[0] in "MSP" else c
1816
+ for c in unicodedata.normalize("NFKC", s)
1817
+ )
1818
+
1819
+
1820
+ class BasicTextNormalizer:
1821
+ def __init__(self, remove_diacritics: bool = False, split_letters: bool = False):
1822
+ self.clean = (
1823
+ remove_symbols_and_diacritics if remove_diacritics else remove_symbols
1824
+ )
1825
+ self.split_letters = split_letters
1826
+
1827
+ def __call__(self, s: str):
1828
+ s = s.lower()
1829
+ s = re.sub(r"[<\[][^>\]]*[>\]]", "", s) # remove words between brackets
1830
+ s = re.sub(r"\(([^)]+?)\)", "", s) # remove words between parenthesis
1831
+ s = self.clean(s).lower()
1832
+
1833
+ if self.split_letters:
1834
+ s = " ".join(regex.findall(r"\X", s, regex.U))
1835
+
1836
+ s = re.sub(
1837
+ r"\s+", " ", s
1838
+ ) # replace any successive whitespace characters with a space
1839
+
1840
+ return s
1841
+
1842
+
1843
+ class EnglishNumberNormalizer:
1844
+ """
1845
+ Convert any spelled-out numbers into arabic numbers, while handling:
1846
+
1847
+ - remove any commas
1848
+ - keep the suffixes such as: `1960s`, `274th`, `32nd`, etc.
1849
+ - spell out currency symbols after the number. e.g. `$20 million` -> `20000000 dollars`
1850
+ - spell out `one` and `ones`
1851
+ - interpret successive single-digit numbers as nominal: `one oh one` -> `101`
1852
+ """
1853
+
1854
+ def __init__(self):
1855
+ super().__init__()
1856
+
1857
+ self.zeros = {"o", "oh", "zero"}
1858
+ # fmt: off
1859
+ self.ones = {
1860
+ name: i
1861
+ for i, name in enumerate(
1862
+ [
1863
+ "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
1864
+ "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen",
1865
+ "eighteen", "nineteen"],
1866
+ start=1,
1867
+ )
1868
+ }
1869
+ # fmt: on
1870
+ self.ones_plural = {
1871
+ "sixes" if name == "six" else name + "s": (value, "s")
1872
+ for name, value in self.ones.items()
1873
+ }
1874
+ self.ones_ordinal = {
1875
+ "zeroth": (0, "th"),
1876
+ "first": (1, "st"),
1877
+ "second": (2, "nd"),
1878
+ "third": (3, "rd"),
1879
+ "fifth": (5, "th"),
1880
+ "twelfth": (12, "th"),
1881
+ **{
1882
+ name + ("h" if name.endswith("t") else "th"): (value, "th")
1883
+ for name, value in self.ones.items()
1884
+ if value > 3 and value != 5 and value != 12
1885
+ },
1886
+ }
1887
+ self.ones_suffixed = {**self.ones_plural, **self.ones_ordinal}
1888
+
1889
+ self.tens = {
1890
+ "twenty": 20,
1891
+ "thirty": 30,
1892
+ "forty": 40,
1893
+ "fifty": 50,
1894
+ "sixty": 60,
1895
+ "seventy": 70,
1896
+ "eighty": 80,
1897
+ "ninety": 90,
1898
+ }
1899
+ self.tens_plural = {
1900
+ name.replace("y", "ies"): (value, "s") for name, value in self.tens.items()
1901
+ }
1902
+ self.tens_ordinal = {
1903
+ name.replace("y", "ieth"): (value, "th")
1904
+ for name, value in self.tens.items()
1905
+ }
1906
+ self.tens_suffixed = {**self.tens_plural, **self.tens_ordinal}
1907
+
1908
+ self.multipliers = {
1909
+ "hundred": 100,
1910
+ "thousand": 1_000,
1911
+ "million": 1_000_000,
1912
+ "billion": 1_000_000_000,
1913
+ "trillion": 1_000_000_000_000,
1914
+ "quadrillion": 1_000_000_000_000_000,
1915
+ "quintillion": 1_000_000_000_000_000_000,
1916
+ "sextillion": 1_000_000_000_000_000_000_000,
1917
+ "septillion": 1_000_000_000_000_000_000_000_000,
1918
+ "octillion": 1_000_000_000_000_000_000_000_000_000,
1919
+ "nonillion": 1_000_000_000_000_000_000_000_000_000_000,
1920
+ "decillion": 1_000_000_000_000_000_000_000_000_000_000_000,
1921
+ }
1922
+ self.multipliers_plural = {
1923
+ name + "s": (value, "s") for name, value in self.multipliers.items()
1924
+ }
1925
+ self.multipliers_ordinal = {
1926
+ name + "th": (value, "th") for name, value in self.multipliers.items()
1927
+ }
1928
+ self.multipliers_suffixed = {
1929
+ **self.multipliers_plural,
1930
+ **self.multipliers_ordinal,
1931
+ }
1932
+ self.decimals = {*self.ones, *self.tens, *self.zeros}
1933
+
1934
+ self.preceding_prefixers = {
1935
+ "minus": "-",
1936
+ "negative": "-",
1937
+ "plus": "+",
1938
+ "positive": "+",
1939
+ }
1940
+ self.following_prefixers = {
1941
+ "pound": "£",
1942
+ "pounds": "£",
1943
+ "euro": "€",
1944
+ "euros": "€",
1945
+ "dollar": "$",
1946
+ "dollars": "$",
1947
+ "cent": "¢",
1948
+ "cents": "¢",
1949
+ }
1950
+ self.prefixes = set(
1951
+ list(self.preceding_prefixers.values())
1952
+ + list(self.following_prefixers.values())
1953
+ )
1954
+ self.suffixers = {
1955
+ "per": {"cent": "%"},
1956
+ "percent": "%",
1957
+ }
1958
+ self.specials = {"and", "double", "triple", "point"}
1959
+
1960
+ self.words = {
1961
+ key
1962
+ for mapping in [
1963
+ self.zeros,
1964
+ self.ones,
1965
+ self.ones_suffixed,
1966
+ self.tens,
1967
+ self.tens_suffixed,
1968
+ self.multipliers,
1969
+ self.multipliers_suffixed,
1970
+ self.preceding_prefixers,
1971
+ self.following_prefixers,
1972
+ self.suffixers,
1973
+ self.specials,
1974
+ ]
1975
+ for key in mapping
1976
+ }
1977
+ self.literal_words = {"one", "ones"}
1978
+
1979
+ def process_words(self, words: List[str]) -> Iterator[str]:
1980
+ prefix: Optional[str] = None
1981
+ value: Optional[Union[str, int]] = None
1982
+ skip = False
1983
+
1984
+ def to_fraction(s: str):
1985
+ try:
1986
+ return Fraction(s)
1987
+ except ValueError:
1988
+ return None
1989
+
1990
+ def output(result: Union[str, int]):
1991
+ nonlocal prefix, value
1992
+ result = str(result)
1993
+ if prefix is not None:
1994
+ result = prefix + result
1995
+ value = None
1996
+ prefix = None
1997
+ return result
1998
+
1999
+ if len(words) == 0:
2000
+ return
2001
+
2002
+ for i, current in enumerate(words):
2003
+ prev = words[i - 1] if i != 0 else None
2004
+ next = words[i + 1] if i != len(words) - 1 else None
2005
+ if skip:
2006
+ skip = False
2007
+ continue
2008
+
2009
+ next_is_numeric = next is not None and re.match(r"^\d+(\.\d+)?$", next)
2010
+ has_prefix = current[0] in self.prefixes
2011
+ current_without_prefix = current[1:] if has_prefix else current
2012
+ if re.match(r"^\d+(\.\d+)?$", current_without_prefix):
2013
+ # arabic numbers (potentially with signs and fractions)
2014
+ f = to_fraction(current_without_prefix)
2015
+ if f is None:
2016
+ raise ValueError("Converting the fraction failed")
2017
+
2018
+ if value is not None:
2019
+ if isinstance(value, str) and value.endswith("."):
2020
+ # concatenate decimals / ip address components
2021
+ value = str(value) + str(current)
2022
+ continue
2023
+ else:
2024
+ yield output(value)
2025
+
2026
+ prefix = current[0] if has_prefix else prefix
2027
+ if f.denominator == 1:
2028
+ value = f.numerator # store integers as int
2029
+ else:
2030
+ value = current_without_prefix
2031
+ elif current not in self.words:
2032
+ # non-numeric words
2033
+ if value is not None:
2034
+ yield output(value)
2035
+ yield output(current)
2036
+ elif current in self.zeros:
2037
+ value = str(value or "") + "0"
2038
+ elif current in self.ones:
2039
+ ones = self.ones[current]
2040
+
2041
+ if value is None:
2042
+ value = ones
2043
+ elif isinstance(value, str) or prev in self.ones:
2044
+ if (
2045
+ prev in self.tens and ones < 10
2046
+ ): # replace the last zero with the digit
2047
+ value = value[:-1] + str(ones)
2048
+ else:
2049
+ value = str(value) + str(ones)
2050
+ elif ones < 10:
2051
+ if value % 10 == 0:
2052
+ value += ones
2053
+ else:
2054
+ value = str(value) + str(ones)
2055
+ else: # eleven to nineteen
2056
+ if value % 100 == 0:
2057
+ value += ones
2058
+ else:
2059
+ value = str(value) + str(ones)
2060
+ elif current in self.ones_suffixed:
2061
+ # ordinal or cardinal; yield the number right away
2062
+ ones, suffix = self.ones_suffixed[current]
2063
+ if value is None:
2064
+ yield output(str(ones) + suffix)
2065
+ elif isinstance(value, str) or prev in self.ones:
2066
+ if prev in self.tens and ones < 10:
2067
+ yield output(value[:-1] + str(ones) + suffix)
2068
+ else:
2069
+ yield output(str(value) + str(ones) + suffix)
2070
+ elif ones < 10:
2071
+ if value % 10 == 0:
2072
+ yield output(str(value + ones) + suffix)
2073
+ else:
2074
+ yield output(str(value) + str(ones) + suffix)
2075
+ else: # eleven to nineteen
2076
+ if value % 100 == 0:
2077
+ yield output(str(value + ones) + suffix)
2078
+ else:
2079
+ yield output(str(value) + str(ones) + suffix)
2080
+ value = None
2081
+ elif current in self.tens:
2082
+ tens = self.tens[current]
2083
+ if value is None:
2084
+ value = tens
2085
+ elif isinstance(value, str):
2086
+ value = str(value) + str(tens)
2087
+ else:
2088
+ if value % 100 == 0:
2089
+ value += tens
2090
+ else:
2091
+ value = str(value) + str(tens)
2092
+ elif current in self.tens_suffixed:
2093
+ # ordinal or cardinal; yield the number right away
2094
+ tens, suffix = self.tens_suffixed[current]
2095
+ if value is None:
2096
+ yield output(str(tens) + suffix)
2097
+ elif isinstance(value, str):
2098
+ yield output(str(value) + str(tens) + suffix)
2099
+ else:
2100
+ if value % 100 == 0:
2101
+ yield output(str(value + tens) + suffix)
2102
+ else:
2103
+ yield output(str(value) + str(tens) + suffix)
2104
+ elif current in self.multipliers:
2105
+ multiplier = self.multipliers[current]
2106
+ if value is None:
2107
+ value = multiplier
2108
+ elif isinstance(value, str) or value == 0:
2109
+ f = to_fraction(value)
2110
+ p = f * multiplier if f is not None else None
2111
+ if f is not None and p.denominator == 1:
2112
+ value = p.numerator
2113
+ else:
2114
+ yield output(value)
2115
+ value = multiplier
2116
+ else:
2117
+ before = value // 1000 * 1000
2118
+ residual = value % 1000
2119
+ value = before + residual * multiplier
2120
+ elif current in self.multipliers_suffixed:
2121
+ multiplier, suffix = self.multipliers_suffixed[current]
2122
+ if value is None:
2123
+ yield output(str(multiplier) + suffix)
2124
+ elif isinstance(value, str):
2125
+ f = to_fraction(value)
2126
+ p = f * multiplier if f is not None else None
2127
+ if f is not None and p.denominator == 1:
2128
+ yield output(str(p.numerator) + suffix)
2129
+ else:
2130
+ yield output(value)
2131
+ yield output(str(multiplier) + suffix)
2132
+ else: # int
2133
+ before = value // 1000 * 1000
2134
+ residual = value % 1000
2135
+ value = before + residual * multiplier
2136
+ yield output(str(value) + suffix)
2137
+ value = None
2138
+ elif current in self.preceding_prefixers:
2139
+ # apply prefix (positive, minus, etc.) if it precedes a number
2140
+ if value is not None:
2141
+ yield output(value)
2142
+
2143
+ if next in self.words or next_is_numeric:
2144
+ prefix = self.preceding_prefixers[current]
2145
+ else:
2146
+ yield output(current)
2147
+ elif current in self.following_prefixers:
2148
+ # apply prefix (dollars, cents, etc.) only after a number
2149
+ if value is not None:
2150
+ prefix = self.following_prefixers[current]
2151
+ yield output(value)
2152
+ else:
2153
+ yield output(current)
2154
+ elif current in self.suffixers:
2155
+ # apply suffix symbols (percent -> '%')
2156
+ if value is not None:
2157
+ suffix = self.suffixers[current]
2158
+ if isinstance(suffix, dict):
2159
+ if next in suffix:
2160
+ yield output(str(value) + suffix[next])
2161
+ skip = True
2162
+ else:
2163
+ yield output(value)
2164
+ yield output(current)
2165
+ else:
2166
+ yield output(str(value) + suffix)
2167
+ else:
2168
+ yield output(current)
2169
+ elif current in self.specials:
2170
+ if next not in self.words and not next_is_numeric:
2171
+ # apply special handling only if the next word can be numeric
2172
+ if value is not None:
2173
+ yield output(value)
2174
+ yield output(current)
2175
+ elif current == "and":
2176
+ # ignore "and" after hundreds, thousands, etc.
2177
+ if prev not in self.multipliers:
2178
+ if value is not None:
2179
+ yield output(value)
2180
+ yield output(current)
2181
+ elif current == "double" or current == "triple":
2182
+ if next in self.ones or next in self.zeros:
2183
+ repeats = 2 if current == "double" else 3
2184
+ ones = self.ones.get(next, 0)
2185
+ value = str(value or "") + str(ones) * repeats
2186
+ skip = True
2187
+ else:
2188
+ if value is not None:
2189
+ yield output(value)
2190
+ yield output(current)
2191
+ elif current == "point":
2192
+ if next in self.decimals or next_is_numeric:
2193
+ value = str(value or "") + "."
2194
+ else:
2195
+ # should all have been covered at this point
2196
+ raise ValueError(f"Unexpected token: {current}")
2197
+ else:
2198
+ # all should have been covered at this point
2199
+ raise ValueError(f"Unexpected token: {current}")
2200
+
2201
+ if value is not None:
2202
+ yield output(value)
2203
+
2204
+ def preprocess(self, s: str):
2205
+ # replace "<number> and a half" with "<number> point five"
2206
+ results = []
2207
+
2208
+ segments = re.split(r"\band\s+a\s+half\b", s)
2209
+ for i, segment in enumerate(segments):
2210
+ if len(segment.strip()) == 0:
2211
+ continue
2212
+ if i == len(segments) - 1:
2213
+ results.append(segment)
2214
+ else:
2215
+ results.append(segment)
2216
+ last_word = segment.rsplit(maxsplit=2)[-1]
2217
+ if last_word in self.decimals or last_word in self.multipliers:
2218
+ results.append("point five")
2219
+ else:
2220
+ results.append("and a half")
2221
+
2222
+ s = " ".join(results)
2223
+
2224
+ # put a space at number/letter boundary
2225
+ s = re.sub(r"([a-z])([0-9])", r"\1 \2", s)
2226
+ s = re.sub(r"([0-9])([a-z])", r"\1 \2", s)
2227
+
2228
+ # but remove spaces which could be a suffix
2229
+ s = re.sub(r"([0-9])\s+(st|nd|rd|th|s)\b", r"\1\2", s)
2230
+
2231
+ return s
2232
+
2233
+ def postprocess(self, s: str):
2234
+ def combine_cents(m: Match):
2235
+ try:
2236
+ currency = m.group(1)
2237
+ integer = m.group(2)
2238
+ cents = int(m.group(3))
2239
+ return f"{currency}{integer}.{cents:02d}"
2240
+ except ValueError:
2241
+ return m.string
2242
+
2243
+ def extract_cents(m: Match):
2244
+ try:
2245
+ return f"¢{int(m.group(1))}"
2246
+ except ValueError:
2247
+ return m.string
2248
+
2249
+ # apply currency postprocessing; "$2 and ¢7" -> "$2.07"
2250
+ s = re.sub(r"([€£$])([0-9]+) (?:and )?¢([0-9]{1,2})\b", combine_cents, s)
2251
+ s = re.sub(r"[€£$]0.([0-9]{1,2})\b", extract_cents, s)
2252
+
2253
+ # write "one(s)" instead of "1(s)", just for the readability
2254
+ s = re.sub(r"\b1(s?)\b", r"one\1", s)
2255
+
2256
+ return s
2257
+
2258
+ def __call__(self, s: str):
2259
+ s = self.preprocess(s)
2260
+ s = " ".join(word for word in self.process_words(s.split()) if word is not None)
2261
+ s = self.postprocess(s)
2262
+
2263
+ return s
2264
+
2265
+
2266
+ class EnglishSpellingNormalizer:
2267
+ """
2268
+ Applies British-American spelling mappings as listed in [1].
2269
+
2270
+ [1] https://www.tysto.com/uk-us-spelling-list.html
2271
+ """
2272
+
2273
+ def __init__(self, english_spelling_mapping):
2274
+ self.mapping = english_spelling_mapping
2275
+
2276
+ def __call__(self, s: str):
2277
+ return " ".join(self.mapping.get(word, word) for word in s.split())
2278
+
2279
+
2280
+ class EnglishTextNormalizer:
2281
+ def __init__(self, english_spelling_mapping=abbr):
2282
+ self.ignore_patterns = r"\b(hmm|mm|mhm|mmm|uh|um)\b"
2283
+ self.replacers = {
2284
+ # common contractions
2285
+ r"\bwon't\b": "will not",
2286
+ r"\bcan't\b": "can not",
2287
+ r"\blet's\b": "let us",
2288
+ r"\bain't\b": "aint",
2289
+ r"\by'all\b": "you all",
2290
+ r"\bwanna\b": "want to",
2291
+ r"\bgotta\b": "got to",
2292
+ r"\bgonna\b": "going to",
2293
+ r"\bi'ma\b": "i am going to",
2294
+ r"\bimma\b": "i am going to",
2295
+ r"\bwoulda\b": "would have",
2296
+ r"\bcoulda\b": "could have",
2297
+ r"\bshoulda\b": "should have",
2298
+ r"\bma'am\b": "madam",
2299
+ # contractions in titles/prefixes
2300
+ r"\bmr\b": "mister ",
2301
+ r"\bmrs\b": "missus ",
2302
+ r"\bst\b": "saint ",
2303
+ r"\bdr\b": "doctor ",
2304
+ r"\bprof\b": "professor ",
2305
+ r"\bcapt\b": "captain ",
2306
+ r"\bgov\b": "governor ",
2307
+ r"\bald\b": "alderman ",
2308
+ r"\bgen\b": "general ",
2309
+ r"\bsen\b": "senator ",
2310
+ r"\brep\b": "representative ",
2311
+ r"\bpres\b": "president ",
2312
+ r"\brev\b": "reverend ",
2313
+ r"\bhon\b": "honorable ",
2314
+ r"\basst\b": "assistant ",
2315
+ r"\bassoc\b": "associate ",
2316
+ r"\blt\b": "lieutenant ",
2317
+ r"\bcol\b": "colonel ",
2318
+ r"\bjr\b": "junior ",
2319
+ r"\bsr\b": "senior ",
2320
+ r"\besq\b": "esquire ",
2321
+ # prefect tenses, ideally it should be any past participles, but it's harder..
2322
+ r"'d been\b": " had been",
2323
+ r"'s been\b": " has been",
2324
+ r"'d gone\b": " had gone",
2325
+ r"'s gone\b": " has gone",
2326
+ r"'d done\b": " had done", # "'s done" is ambiguous
2327
+ r"'s got\b": " has got",
2328
+ # general contractions
2329
+ r"n't\b": " not",
2330
+ r"'re\b": " are",
2331
+ r"'s\b": " is",
2332
+ r"'d\b": " would",
2333
+ r"'ll\b": " will",
2334
+ r"'t\b": " not",
2335
+ r"'ve\b": " have",
2336
+ r"'m\b": " am",
2337
+ }
2338
+ self.standardize_numbers = EnglishNumberNormalizer()
2339
+ self.standardize_spellings = EnglishSpellingNormalizer(english_spelling_mapping)
2340
+
2341
+ def __call__(self, s: str):
2342
+ s = s.lower()
2343
+
2344
+ s = re.sub(r"[<\[][^>\]]*[>\]]", "", s) # remove words between brackets
2345
+ s = re.sub(r"\(([^)]+?)\)", "", s) # remove words between parenthesis
2346
+ s = re.sub(self.ignore_patterns, "", s)
2347
+ s = re.sub(
2348
+ r"\s+'", "'", s
2349
+ ) # standardize when there's a space before an apostrophe
2350
+
2351
+ for pattern, replacement in self.replacers.items():
2352
+ s = re.sub(pattern, replacement, s)
2353
+
2354
+ s = re.sub(r"(\d),(\d)", r"\1\2", s) # remove commas between digits
2355
+ s = re.sub(r"\.([^0-9]|$)", r" \1", s) # remove periods not followed by numbers
2356
+ s = remove_symbols_and_diacritics(
2357
+ s, keep=".%$¢€£"
2358
+ ) # keep some symbols for numerics
2359
+
2360
+ s = self.standardize_numbers(s)
2361
+ s = self.standardize_spellings(s)
2362
+
2363
+ # now remove prefix/suffix symbols that are not preceded/followed by numbers
2364
+ s = re.sub(r"[.$¢€£]([^0-9])", r" \1", s)
2365
+ s = re.sub(r"([^0-9])%", r"\1 ", s)
2366
+
2367
+ s = re.sub(
2368
+ r"\s+", " ", s
2369
+ ) # replace any successive whitespace characters with a space
2370
+
2371
+ return s
2372
+
2373
+
2374
+ text_normalizer = EnglishTextNormalizer()
utils.py ADDED
@@ -0,0 +1,906 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import colorsys
2
+ import json
3
+ import os
4
+ import random
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from dataclasses import dataclass, make_dataclass
7
+ from datetime import datetime
8
+ from io import BytesIO
9
+
10
+ import aiohttp
11
+ import evaluate
12
+ import numpy as np
13
+ import pandas as pd
14
+ import plotly.graph_objects as go
15
+ from huggingface_hub import hf_hub_download, list_repo_files
16
+ from pydub import AudioSegment
17
+
18
+ from constants import WHISPER_OPEN_AI_LINK
19
+
20
+ # Load the Word Error Rate (WER) metric from the evaluate library
21
+ wer_metric = evaluate.load("wer")
22
+
23
+
24
+ def compute_average_wer(results):
25
+ """
26
+ Compute the average Word Error Rate (WER) for a list of transcription results.
27
+ :param results: List of dictionaries, each containing 'reference' and 'prediction' keys
28
+ :return: Average WER as a percentage, rounded to 2 decimal places
29
+ This function calculates the WER for each reference-prediction pair and returns
30
+ the average. If no predictions are provided, it returns 100% WER.
31
+ """
32
+ references = [result["reference"] for result in results]
33
+ predictions = [result["prediction"] for result in results]
34
+ if len(predictions) == 0:
35
+ return 1
36
+ return round(
37
+ wer_metric.compute(references=references, predictions=predictions) * 100.0,
38
+ 2,
39
+ )
40
+
41
+
42
+ def read_json_line_by_line(file_path):
43
+ """
44
+ Read a JSON file line by line, parsing each line as a separate JSON object.
45
+ :param file_path: Path to the JSON file
46
+ :return: List of parsed JSON objects
47
+ This function is useful for reading large JSON files that contain one JSON object
48
+ per line. It handles JSON parsing errors gracefully, skipping invalid lines.
49
+ """
50
+ data = []
51
+ with open(file_path, "r") as f:
52
+ for line in f:
53
+ try:
54
+ item = json.loads(line.strip())
55
+ data.append(item)
56
+ except json.JSONDecodeError:
57
+ print(f"Skipping invalid JSON in {file_path}: {line}")
58
+ return data
59
+
60
+
61
+ def group_wer(group):
62
+ """
63
+ Calculate the Word Error Rate (WER) for a group of transcriptions.
64
+ :param group: DataFrame group containing 'normalized_reference' and 'normalized_prediction' columns
65
+ :return: Average WER for the group
66
+ This function is typically used with DataFrame groupby operations to calculate
67
+ WER for specific groups of transcriptions.
68
+ """
69
+ return compute_average_wer(
70
+ group[["normalized_reference", "normalized_prediction"]]
71
+ .rename(
72
+ columns={
73
+ "normalized_reference": "reference",
74
+ "normalized_prediction": "prediction",
75
+ }
76
+ )
77
+ .to_dict("records")
78
+ )
79
+
80
+
81
+ def load_multilingual_results(csv_file):
82
+ """
83
+ Load multilingual results from a CSV file into a pandas DataFrame.
84
+ :param csv_file: Path to the CSV file containing multilingual results
85
+ :return: DataFrame with the loaded results, or None if the file is not found
86
+ This function attempts to load a CSV file using pandas, handling potential
87
+ FileNotFoundError exceptions.
88
+ """
89
+ try:
90
+ df = pd.json_normalize(csv_file)
91
+ return df
92
+ except FileNotFoundError:
93
+ return None
94
+
95
+
96
+ def download_dataset(repo_id, local_dir, remote_dir, path_includes=""):
97
+ """
98
+ Download benchmark result files from a specified Hugging Face repository to a local directory.
99
+ :param repo_id: ID of the Hugging Face repository
100
+ :param local_dir: Local directory where downloaded files will be saved
101
+ :param remote_dir: Remote directory within the repository to download from
102
+ This function uses the Hugging Face Hub API to list and download files from a
103
+ specific directory in a repository. It forces the download to ensure up-to-date files.
104
+ """
105
+ files = list_repo_files(repo_id, repo_type="dataset")
106
+ directory_files = [
107
+ file for file in files if file.startswith(remote_dir) and path_includes in file
108
+ ]
109
+ with ThreadPoolExecutor() as executor:
110
+ executor.map(
111
+ lambda file: hf_hub_download(
112
+ repo_id=repo_id,
113
+ repo_type="dataset",
114
+ filename=file,
115
+ local_dir=local_dir,
116
+ force_download=True,
117
+ ),
118
+ directory_files,
119
+ )
120
+
121
+
122
+ def process_file(file_path):
123
+ """
124
+ Process a file containing JSON objects delimited by new lines.
125
+ :param file_path: Path to the file to be processed
126
+ :return: List of dictionaries, each representing a parsed JSON object
127
+ This function reads the file line by line, parsing each line as a JSON object.
128
+ It handles potential JSON decoding errors, printing error messages for invalid lines.
129
+ """
130
+ data = []
131
+ with open(file_path, "r") as file:
132
+ for line in file:
133
+ line = line.strip()
134
+ if not line:
135
+ continue
136
+ try:
137
+ json_obj = json.loads(line)
138
+ data.append(json_obj)
139
+ except json.JSONDecodeError as e:
140
+ print(f"Error decoding JSON in line: {line}")
141
+ print(f"Error message: {str(e)}")
142
+ return data
143
+
144
+
145
+ def dir_to_json(root_dir, output_file):
146
+ """
147
+ Convert a directory of benchmark result files to a single JSON file.
148
+ :param root_dir: Root directory containing the benchmark result files
149
+ :param output_file: Output file where the JSON data will be saved
150
+ This function walks through the directory structure, processes each file,
151
+ and writes the combined data to a single JSON file. It extracts metadata
152
+ from the file path and includes it in the JSON output.
153
+ """
154
+ with open(output_file, "w") as outfile:
155
+ for subdir, _, files in os.walk(root_dir):
156
+ for file in files:
157
+ file_path = os.path.join(subdir, file)
158
+ # ignore .DS_Store and summary files
159
+ if file_path.endswith(".DS_Store") or "summary" in file_path:
160
+ continue
161
+ parts = file_path.split(os.sep)
162
+ model_version = parts[2]
163
+ device_name = parts[3].replace("_", " ")
164
+ os_type_version = parts[4]
165
+ dataset_name = parts[5]
166
+ timestamp_commit = parts[6].replace(".json", "")
167
+ timestamp, commit_hash, commit_timestamp = timestamp_commit.split("_")
168
+
169
+ data_list = process_file(file_path)
170
+ for data in data_list:
171
+ original_entry = {
172
+ "model": model_version.replace("_", "/"),
173
+ "device": device_name,
174
+ "os": os_type_version.replace("_", " "),
175
+ "wer": data["wer"],
176
+ "dataset_name": dataset_name,
177
+ "reference_transcription": data["reference_transcription"],
178
+ "prediction_transcription": data["prediction_transcription"],
179
+ "difference_transcription": data["difference_transcription"],
180
+ "audio_file_url": data["audio_file_url"],
181
+ "timestamp": timestamp.replace("-", ":").replace(":", "-", 2),
182
+ "commit_hash": commit_hash,
183
+ "commit_timestamp": commit_timestamp,
184
+ }
185
+
186
+ outfile.write(json.dumps(original_entry) + "\n")
187
+
188
+
189
+ async def download_audio_to_ndarray(url):
190
+ """
191
+ Downloads an audio file from a URL and converts it to a NumPy array.
192
+ :param url: The URL of the audio file to download
193
+ :return: A tuple containing the sample rate and audio data as a NumPy array
194
+ This asynchronous function uses aiohttp to download the audio file,
195
+ converts it to an AudioSegment, and then to a NumPy array. It handles
196
+ both mono and stereo audio files.
197
+ """
198
+ async with aiohttp.ClientSession() as session:
199
+ async with session.get(url) as response:
200
+ if response.status == 200:
201
+ audio_bytes = BytesIO(await response.read())
202
+ audio = AudioSegment.from_file(audio_bytes, format="mp3")
203
+ audio_data = np.array(audio.get_array_of_samples())
204
+ if audio.channels == 2:
205
+ audio_data = audio_data.reshape((-1, 2))
206
+ return audio.frame_rate, audio_data
207
+ else:
208
+ return None, None
209
+
210
+
211
+ async def play_audio(url):
212
+ """
213
+ Wrapper function for Gradio to play audio from a URL.
214
+ :param url: The URL of the audio file to play
215
+ :return: A tuple of sample rate and audio data, or an error message
216
+ This function uses download_audio_to_ndarray to get the audio data
217
+ and returns it in a format suitable for Gradio's audio player.
218
+ """
219
+ sample_rate, audio_data = await download_audio_to_ndarray(url)
220
+ if audio_data is None:
221
+ return "Error downloading the file"
222
+ else:
223
+ return sample_rate, audio_data
224
+
225
+
226
+ def get_filter_cond(df, model, device, os, dataset, timestamp=None):
227
+ """
228
+ Creates a filter condition for a DataFrame based on specified parameters.
229
+ :param df: DataFrame containing the transcription data
230
+ :param model: String representing the model name
231
+ :param device: String representing the device name
232
+ :param os: String representing the OS name
233
+ :param dataset: String representing the dataset name
234
+ :param timestamp: Optional timestamp for filtering (default: None)
235
+ :return: A boolean mask for filtering the DataFrame
236
+ This function constructs a complex boolean condition for filtering
237
+ the DataFrame based on the provided parameters.
238
+ """
239
+ filter_cond = (
240
+ (df["model"] == model)
241
+ & (df["device"] == device)
242
+ & (df["os"] == os)
243
+ & (df["dataset_name"] == dataset)
244
+ )
245
+ return filter_cond & (df["timestamp"] == timestamp) if timestamp else filter_cond
246
+
247
+
248
+ def get_filtered_transcript(df, model, device, os, dataset, timestamp):
249
+ """
250
+ Retrieves filtered transcription data from a DataFrame.
251
+ :param df: DataFrame containing the transcription data
252
+ :param model: String representing the model name
253
+ :param device: String representing the device name
254
+ :param os: String representing the OS name
255
+ :param dataset: String representing the dataset name
256
+ :param timestamp: String representing the timestamp
257
+ :return: A filtered DataFrame with transcription data
258
+ This function applies a filter to the input DataFrame and returns
259
+ relevant columns for transcription analysis.
260
+ """
261
+ filter_cond = get_filter_cond(df, model, device, os, dataset, timestamp)
262
+ df = df[filter_cond][
263
+ [
264
+ "reference_transcription",
265
+ "prediction_transcription",
266
+ "difference_transcription",
267
+ "audio_file_url",
268
+ ]
269
+ ]
270
+ return df
271
+
272
+
273
+ def get_filtered_timestamps(df, model, device, os, dataset):
274
+ """
275
+ Retrieves unique timestamps for a specific model, device, OS, and dataset combination.
276
+ :param df: DataFrame containing the transcription data
277
+ :param model: String representing the model name
278
+ :param device: String representing the device name
279
+ :param os: String representing the OS name
280
+ :param dataset: String representing the dataset name
281
+ :return: A filtered DataFrame containing unique timestamps
282
+ This function is useful for getting a list of available timestamps
283
+ for a specific configuration, which can be used for further analysis or UI elements.
284
+ """
285
+ filter_cond = get_filter_cond(df, model, device, os, dataset)
286
+ df = df[filter_cond][["timestamp"]].drop_duplicates()
287
+ return df
288
+
289
+
290
+ def make_model_name_clickable_link(model):
291
+ """
292
+ Creates an HTML link to the Hugging Face model page.
293
+ :param model: String representing the model name
294
+ :return: An HTML string containing a clickable link to the model page
295
+ This function generates a formatted HTML link that can be used in
296
+ web interfaces to provide direct access to the model's page on Hugging Face.
297
+ """
298
+ return f"""<a style="color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;" href="https://huggingface.co/argmaxinc/whisperkit-coreml/tree/main/{model.replace('/', '_')}" target="_blank">{model}</a>"""
299
+
300
+
301
+ def make_dataset_wer_clickable_link(row, dataset):
302
+ """
303
+ Creates a clickable link for the WER value of a dataset.
304
+ :param row: Row containing the dataset WER value
305
+ :param dataset: String representing the dataset name
306
+ :return: An HTML string containing a clickable link to the dataset's WER details
307
+ This function generates a formatted HTML link that can be used in
308
+ web interfaces to provide access to detailed WER information for a specific dataset.
309
+ """
310
+ dataset_column = f"{dataset}"
311
+ href = WHISPER_OPEN_AI_LINK.format(
312
+ row["Model"].replace("/", "_"),
313
+ dataset,
314
+ )
315
+ return f'<a style="color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;" href="{href}">{row[dataset_column]}</a>'
316
+
317
+
318
+ def make_timestamp_clickable_link(model, dataset, timestamp):
319
+ """
320
+ Creates a clickable link for a timestamp.
321
+ :param model: String representing the model name
322
+ :param dataset: String representing the dataset name
323
+ :param timestamp: Timestamp to be displayed and used in the link
324
+ :return: An HTML string containing a clickable div for the timestamp
325
+ This function generates a formatted HTML div that can be used as a clickable
326
+ element in web interfaces, typically for displaying and interacting with specific timestamps.
327
+ """
328
+ elem_id = (
329
+ f"{dataset}-{model}-{timestamp}".replace(" ", "_")
330
+ .replace('"', "")
331
+ .replace("'", "")
332
+ .replace(",", "")
333
+ )
334
+ onclick = f"onclick=\"document.getElementById('{elem_id}').click();\""
335
+ return f'<div style="color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;" {onclick} href="#">{timestamp}</div>'
336
+
337
+
338
+ def make_multilingual_model_clickable_link(model):
339
+ """
340
+ Creates a clickable link for a multilingual model name.
341
+ :param model: String representing the model name
342
+ :return: An HTML string containing a clickable div for the model name
343
+ This function generates a formatted HTML div that can be used as a clickable
344
+ element in web interfaces, typically for displaying and interacting with multilingual model names.
345
+ """
346
+ elem_id = (
347
+ f"{model}".replace(" ", "_").replace('"', "").replace("'", "").replace(",", "")
348
+ )
349
+ onclick = f"onclick=\"document.getElementById('{elem_id}').click();console.log('hello');\""
350
+ return f'<div style="color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;" {onclick} href="#">{model}</div>'
351
+
352
+
353
+ def plot_metric(
354
+ df, y_axis_col, y_axis_title, fig_title, filter_input=None, exclude_input=None
355
+ ):
356
+ """
357
+ Plots a metric for each model-device-OS group in a DataFrame.
358
+ :param df: DataFrame containing the benchmark data
359
+ :param y_axis_col: DataFrame column to use as the y-axis
360
+ :param y_axis_title: Display name for the y-axis
361
+ :param fig_title: Display title for the figure
362
+ :param filter_input: Optional string to filter the model-device-OS combinations
363
+ :param exclude_input: Optional string to exclude model-device-OS combinations
364
+ :return: A Plotly figure object
365
+ """
366
+ grouped = df.groupby(["model", "device", "os"])
367
+ sorted_groups = [group.sort_values("commit_timestamp") for _, group in grouped]
368
+
369
+ if filter_input:
370
+ filters = [f.strip().lower() for f in filter_input.split(";")]
371
+ sorted_groups = [
372
+ group
373
+ for group in sorted_groups
374
+ if any(
375
+ f
376
+ in f"{group['model'].iloc[0]}-{group['device'].iloc[0]}-{group['os'].iloc[0]}".lower()
377
+ for f in filters
378
+ )
379
+ ]
380
+
381
+ if exclude_input:
382
+ excludes = [e.strip().lower() for e in exclude_input.split(";")]
383
+ sorted_groups = [
384
+ group
385
+ for group in sorted_groups
386
+ if not any(
387
+ e
388
+ in f"{group['model'].iloc[0]}-{group['device'].iloc[0]}-{group['os'].iloc[0]}".lower()
389
+ for e in excludes
390
+ )
391
+ ]
392
+
393
+ base_colors = ["#4542f4", "#0e0c06", "#ccf0a7", "#ff7f4e", "#ffd15a"]
394
+ num_colors = len(sorted_groups)
395
+ random_colors = generate_random_colors(base_colors, num_colors)
396
+ fig = go.Figure()
397
+ for i, group in enumerate(sorted_groups):
398
+ model_device_os = (
399
+ f"{group['model'].iloc[0]}-{group['device'].iloc[0]}-{group['os'].iloc[0]}"
400
+ )
401
+ fig.add_trace(
402
+ go.Scatter(
403
+ x=group["commit_timestamp"].apply(
404
+ lambda x: datetime.strptime(x, "%Y-%m-%dT%H%M%S").strftime(
405
+ "%Y-%m-%d %H:%M:%S"
406
+ )
407
+ ),
408
+ y=group[y_axis_col],
409
+ mode="lines+markers",
410
+ name=model_device_os,
411
+ line=dict(color=random_colors[i % len(random_colors)]),
412
+ marker=dict(color=random_colors[i % len(random_colors)]),
413
+ hovertemplate=(
414
+ f"<b>{model_device_os}</b><br>"
415
+ "Timestamp: %{x}<br>"
416
+ f"{y_axis_title}: %{{y:.2f}}<br>"
417
+ "<extra></extra>"
418
+ ),
419
+ )
420
+ )
421
+ fig.update_layout(
422
+ title=fig_title,
423
+ xaxis_title="Commit Timestamp",
424
+ yaxis_title=y_axis_title,
425
+ legend_title="Model-Device-OS",
426
+ width=1100,
427
+ height=600,
428
+ plot_bgcolor="rgb(250,249,244)",
429
+ )
430
+ return fig
431
+
432
+
433
+ def fields(raw_class):
434
+ """
435
+ Returns the fields of a dataclass.
436
+ :param raw_class: The dataclass to inspect
437
+ :return: List of fields in the dataclass
438
+ This utility function extracts and returns all the fields defined in a dataclass,
439
+ excluding special methods and attributes.
440
+ """
441
+ return [
442
+ v for k, v in raw_class.__dict__.items() if k[:2] != "__" and k[-2:] != "__"
443
+ ]
444
+
445
+
446
+ def get_os_name_and_version(os_string):
447
+ """
448
+ Extracts the OS name and major version from a string.
449
+ :param os_string: String representing the OS name and version
450
+ :return: Formatted string with OS name and major version
451
+ This function splits the input string into OS name and version,
452
+ then returns a formatted string with just the major version number.
453
+ """
454
+ os_name, os_version = os_string.split()
455
+ os_version = os_version.split(".")[0]
456
+ return f"{os_name} {os_version}"
457
+
458
+
459
+ def create_initial_quality_column_dict():
460
+ """
461
+ Creates the initial column dictionary for the quality table.
462
+ :return: A list of column dictionaries
463
+ This function defines the basic structure of the quality table,
464
+ including columns for model, average WER, and QoI (Quality of Implementation).
465
+ """
466
+ return [
467
+ [
468
+ "model",
469
+ ColumnContent,
470
+ ColumnContent("Model", "html", True, never_hidden=True),
471
+ ],
472
+ ["average_wer", ColumnContent, ColumnContent("Average WER", "html", True)],
473
+ ["qoi", ColumnContent, ColumnContent("QoI", "html", True)],
474
+ ]
475
+
476
+
477
+ def calculate_parity(m2_ultra_wer, row):
478
+ """
479
+ Calculates the WER parity between M2 Ultra and the current model.
480
+ :param m2_ultra_wer: DataFrame containing WER values for M2 Ultra
481
+ :param row: Current row being processed
482
+ :return: WER difference between M2 Ultra and current model, or None if not applicable
483
+ This function computes the percentage difference in WER between the M2 Ultra model
484
+ and the current model, providing a measure of relative performance.
485
+ """
486
+ if row["Model"] in m2_ultra_wer.index:
487
+ return round(m2_ultra_wer[row["Model"]] - row["Average WER"], 2)
488
+ return None
489
+
490
+
491
+ def create_initial_performance_column_dict():
492
+ """
493
+ Creates the initial column dictionary for the performance table.
494
+ :return: A list of column dictionaries
495
+ This function defines the basic structure of the performance table,
496
+ including columns for model, device, OS, average WER, QoI, speed, and tokens per second.
497
+ """
498
+ return [
499
+ [
500
+ "model",
501
+ ColumnContent,
502
+ ColumnContent("Model", "html", True, never_hidden=True),
503
+ ],
504
+ [
505
+ "device",
506
+ ColumnContent,
507
+ ColumnContent("Device", "html", True, never_hidden=True),
508
+ ],
509
+ ["os", ColumnContent, ColumnContent("OS", "html", True, never_hidden=True)],
510
+ ["average_wer", ColumnContent, ColumnContent("Average WER", "html", True)],
511
+ ["qoi", ColumnContent, ColumnContent("QoI", "html", False)],
512
+ ["speed", ColumnContent, ColumnContent("Speed", "html", False)],
513
+ ["toks", ColumnContent, ColumnContent("Tok / s", "html", False)],
514
+ ]
515
+
516
+
517
+ def add_datasets_to_quality_columns(column_dict, datasets):
518
+ """
519
+ Adds dataset-specific columns to the quality table column dictionary.
520
+ :param column_dict: The initial column dictionary
521
+ :param datasets: List of dataset names to add
522
+ :return: A dictionary containing the updated column dictionary and related metadata
523
+ This function extends the quality table structure with columns for each dataset,
524
+ and creates a dataclass to represent the table structure. It also generates
525
+ metadata about the columns for use in the UI.
526
+ """
527
+ updated_column_dict = column_dict.copy()
528
+
529
+ for dataset in datasets:
530
+ field_name = dataset.replace("-", "")
531
+ updated_column_dict.append(
532
+ [field_name, ColumnContent, ColumnContent(dataset, "html", True)]
533
+ )
534
+
535
+ AutoEvalColumn = make_dataclass("AutoEvalColumn", updated_column_dict, frozen=True)
536
+
537
+ COLS = [c.name for c in fields(AutoEvalColumn) if not c.hidden]
538
+ TYPES = [c.type for c in fields(AutoEvalColumn) if not c.hidden]
539
+ ALWAYS_HERE_COLS = [c.name for c in fields(AutoEvalColumn) if c.never_hidden]
540
+ TOGGLE_COLS = [c.name for c in fields(AutoEvalColumn) if not c.never_hidden]
541
+ SELECTED_COLS = [
542
+ c.name
543
+ for c in fields(AutoEvalColumn)
544
+ if not c.never_hidden and c.displayed_by_default
545
+ ]
546
+
547
+ return {
548
+ "column_dict": updated_column_dict,
549
+ "AutoEvalColumn": AutoEvalColumn,
550
+ "COLS": COLS,
551
+ "TYPES": TYPES,
552
+ "ALWAYS_HERE_COLS": ALWAYS_HERE_COLS,
553
+ "TOGGLE_COLS": TOGGLE_COLS,
554
+ "SELECTED_COLS": SELECTED_COLS,
555
+ }
556
+
557
+
558
+ def add_datasets_to_performance_columns(column_dict, datasets):
559
+ """
560
+ Adds dataset-specific columns to the performance table column dictionary.
561
+ :param column_dict: The initial column dictionary
562
+ :param datasets: List of dataset names to add
563
+ :return: A dictionary containing the updated column dictionary and related metadata
564
+ This function extends the performance table structure with columns for each dataset,
565
+ adding both speed and tokens per second metrics. It also creates a dataclass to
566
+ represent the table structure and generates metadata about the columns for use in the UI.
567
+ """
568
+ updated_column_dict = column_dict.copy()
569
+
570
+ for dataset in datasets:
571
+ field_name = dataset.replace("-", "")
572
+ updated_column_dict.append(
573
+ [
574
+ f"{field_name}_speed",
575
+ ColumnContent,
576
+ ColumnContent(
577
+ f"{'Short-Form' if dataset == 'librispeech-10mins' else 'Long-Form'} Speed",
578
+ "html",
579
+ True,
580
+ ),
581
+ ]
582
+ )
583
+ updated_column_dict.append(
584
+ [
585
+ f"{field_name}_toks",
586
+ ColumnContent,
587
+ ColumnContent(
588
+ f"{'Short-Form' if dataset == 'librispeech-10mins' else 'Long-Form'} Tok/s",
589
+ "html",
590
+ True,
591
+ ),
592
+ ]
593
+ )
594
+
595
+ AutoEvalColumn = make_dataclass("AutoEvalColumn", updated_column_dict, frozen=True)
596
+
597
+ COLS = [c.name for c in fields(AutoEvalColumn) if not c.hidden]
598
+ TYPES = [c.type for c in fields(AutoEvalColumn) if not c.hidden]
599
+ ALWAYS_HERE_COLS = [c.name for c in fields(AutoEvalColumn) if c.never_hidden]
600
+ TOGGLE_COLS = [c.name for c in fields(AutoEvalColumn) if not c.never_hidden]
601
+ SELECTED_COLS = [
602
+ c.name
603
+ for c in fields(AutoEvalColumn)
604
+ if not c.never_hidden and c.displayed_by_default
605
+ ]
606
+
607
+ return {
608
+ "column_dict": updated_column_dict,
609
+ "AutoEvalColumn": AutoEvalColumn,
610
+ "COLS": COLS,
611
+ "TYPES": TYPES,
612
+ "ALWAYS_HERE_COLS": ALWAYS_HERE_COLS,
613
+ "TOGGLE_COLS": TOGGLE_COLS,
614
+ "SELECTED_COLS": SELECTED_COLS,
615
+ }
616
+
617
+
618
+ def create_confusion_matrix_plot(matrix, labels, is_forced):
619
+ """
620
+ Creates a confusion matrix plot for language detection.
621
+ :param matrix: 2D numpy array representing the confusion matrix
622
+ :param labels: List of language labels
623
+ :param is_forced: Boolean indicating whether language hint was used
624
+ :return: A Plotly figure object representing the confusion matrix
625
+ This function generates a heatmap visualization of the confusion matrix
626
+ for language detection, with customized layout and hover information.
627
+ """
628
+ fig = go.Figure(
629
+ data=go.Heatmap(
630
+ z=matrix,
631
+ x=labels,
632
+ y=labels,
633
+ colorscale=[
634
+ [0, "rgb(250,249,244)"],
635
+ [0.5, "rgb(69,66,244)"],
636
+ [1.0, "rgb(14,12,6)"],
637
+ ],
638
+ hoverongaps=False,
639
+ hovertemplate="True: %{y}<br>Predicted: %{x}<br>Value: %{z}<extra></extra>",
640
+ )
641
+ )
642
+ fig.update_layout(
643
+ title=f'Language Detection Confusion Matrix with {"Language Hint" if is_forced else "Language Prediction by Model"}',
644
+ xaxis_title="Predicted Language",
645
+ yaxis_title="True Language",
646
+ xaxis=dict(tickangle=-45),
647
+ width=600,
648
+ height=600,
649
+ margin=dict(l=50, r=50, t=50, b=50),
650
+ )
651
+ return fig
652
+
653
+
654
+ def hex_to_rgb(hex_color):
655
+ """
656
+ Converts a hexadecimal color code to RGB values.
657
+ :param hex_color: String representing a color in hexadecimal format
658
+ :return: Tuple of three integers representing RGB values
659
+ This function takes a hex color code and returns the corresponding
660
+ RGB values as a tuple of integers.
661
+ """
662
+ hex_color = hex_color.lstrip("#")
663
+ return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
664
+
665
+
666
+ def rgb_to_hex(rgb):
667
+ """
668
+ Converts RGB values to a hexadecimal color code.
669
+ :param rgb: Tuple of three integers representing RGB values
670
+ :return: String representing the color in hexadecimal format
671
+ This function takes RGB values as a tuple and returns the corresponding
672
+ hex color code as a string.
673
+ """
674
+ return "#{:02x}{:02x}{:02x}".format(*rgb)
675
+
676
+
677
+ def interpolate_colors(color1, color2, factor):
678
+ """
679
+ Interpolates between two colors in HSV space.
680
+ :param color1: First color in hexadecimal format
681
+ :param color2: Second color in hexadecimal format
682
+ :param factor: Float between 0 and 1, representing the interpolation factor
683
+ :return: Interpolated color in hexadecimal format
684
+ This function performs color interpolation in HSV color space, which can
685
+ produce more visually pleasing results than simple RGB interpolation.
686
+ """
687
+ rgb1 = hex_to_rgb(color1)
688
+ rgb2 = hex_to_rgb(color2)
689
+
690
+ hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
691
+ hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
692
+
693
+ h = (hsv1[0] + factor * (hsv2[0] - hsv1[0])) % 1.0
694
+ s = hsv1[1] + factor * (hsv2[1] - hsv1[1])
695
+ v = hsv1[2] + factor * (hsv2[2] - hsv1[2])
696
+
697
+ rgb = colorsys.hsv_to_rgb(h, s, v)
698
+ return rgb_to_hex(tuple(int(x * 255) for x in rgb))
699
+
700
+
701
+ def color_distance(color1, color2):
702
+ """
703
+ Calculates the Euclidean distance between two colors in RGB space.
704
+ :param color1: First color in hexadecimal format
705
+ :param color2: Second color in hexadecimal format
706
+ :return: Float representing the distance between the two colors
707
+ This function computes the Euclidean distance between two colors in RGB space,
708
+ which can be used as a measure of color similarity.
709
+ """
710
+ rgb1 = hex_to_rgb(color1)
711
+ rgb2 = hex_to_rgb(color2)
712
+ return sum((a - b) ** 2 for a, b in zip(rgb1, rgb2)) ** 0.5
713
+
714
+
715
+ def generate_random_colors(base_colors, num_colors, min_distance=30):
716
+ """
717
+ Generates a list of random colors based on a set of base colors.
718
+ :param base_colors: List of base colors in hexadecimal format
719
+ :param num_colors: Number of colors to generate
720
+ :param min_distance: Minimum distance between generated colors (default: 30)
721
+ :return: List of generated colors in hexadecimal format
722
+ This function creates a list of random colors by interpolating between
723
+ the provided base colors. It attempts to maintain a minimum distance
724
+ between colors to ensure visual distinctiveness.
725
+ """
726
+ generated_colors = []
727
+ attempts = 0
728
+ max_attempts = 1000
729
+
730
+ while len(generated_colors) < num_colors and attempts < max_attempts:
731
+ color1, color2 = random.sample(base_colors, 2)
732
+ factor = random.random()
733
+ new_color = interpolate_colors(color1, color2, factor)
734
+
735
+ if all(color_distance(new_color, c) >= min_distance for c in generated_colors):
736
+ generated_colors.append(new_color)
737
+ attempts = 0
738
+ else:
739
+ attempts += 1
740
+
741
+ if attempts > 100:
742
+ if random.random() < 0.1:
743
+ generated_colors.append(new_color)
744
+ attempts = 0
745
+
746
+ return generated_colors
747
+
748
+
749
+ @dataclass
750
+ class Task:
751
+ """
752
+ Dataclass representing a benchmark task.
753
+ :param benchmark: String representing the benchmark name
754
+ :param metric: String representing the metric used for evaluation
755
+ :param col_name: String representing the column name in the results DataFrame
756
+ """
757
+
758
+ benchmark: str
759
+ metric: str
760
+ col_name: str
761
+
762
+
763
+ @dataclass(frozen=True)
764
+ class ColumnContent:
765
+ """
766
+ Dataclass representing a column in the results table.
767
+ :param name: String representing the column name
768
+ :param type: String representing the data type of the column
769
+ :param displayed_by_default: Boolean indicating if the column should be displayed by default
770
+ :param hidden: Boolean indicating if the column should be hidden (default: False)
771
+ :param never_hidden: Boolean indicating if the column should never be hidden (default: False)
772
+ :param dummy: Boolean indicating if this is a dummy column (default: False)
773
+ """
774
+
775
+ name: str
776
+ type: str
777
+ displayed_by_default: bool
778
+ hidden: bool = False
779
+ never_hidden: bool = False
780
+ dummy: bool = False
781
+
782
+
783
+ css = """
784
+ @font-face {
785
+ font-family: 'Zwizz Regular';
786
+ font-style: normal;
787
+ font-weight: normal;
788
+ src: local('Zwizz Regular'), url('static/Zwizz-Regular.woff') format('woff');
789
+ }
790
+ @font-face {
791
+ font-family: 'Zwizz Medium';
792
+ font-style: normal;
793
+ font-weight: normal;
794
+ src: local('Zwizz Medium'), url('static/Zwizz-Medium.woff') format('woff');
795
+ }
796
+ @font-face {
797
+ font-family: 'Zwizz SemiBold';
798
+ font-style: normal;
799
+ font-weight: normal;
800
+ src: local('Zwizz SemiBold'), url('static/Zwizz-SemiBold.woff') format('woff');
801
+ }
802
+
803
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap');
804
+ @import url('https://fonts.googleapis.com/css2?family=Sora:[email protected]&display=swap');
805
+ /* Typography Scale */
806
+ h1, .h1 {
807
+ font-family: 'Sora', sans-serif;
808
+ font-weight: 300;
809
+ font-size: 2em;
810
+ letter-spacing: -0.05em;
811
+ }
812
+ h2, .h2 {
813
+ font-family: 'Sora', sans-serif;
814
+ font-weight: 400;
815
+ letter-spacing: -0.05em;
816
+ }
817
+ h3, h4, h5, .h3, .h4, .h5 {
818
+ font-family: 'Sora', sans-serif;
819
+ font-weight: 400;
820
+ letter-spacing: -0.05em;
821
+ }
822
+ h6, .h6, pre, code, .monospace {
823
+ font-family: 'IBM Plex Mono', monospace;
824
+ font-weight: 400;
825
+ letter-spacing: 0.01em;
826
+ }
827
+ /* Add strong tag styling */
828
+ strong, b {
829
+ font-family: 'Zwizz SemiBold', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
830
+ letter-spacing: -0.02em;
831
+ }
832
+ /* Global Zwizz styles */
833
+ :root {
834
+ --zwizz-spacing: -0.02em;
835
+ }
836
+ /* All Gradio elements should have Zwizz spacing */
837
+ .gradio-container * {
838
+ letter-spacing: var(--zwizz-spacing);
839
+ line-height: 1.7;
840
+ }
841
+ /* UI Elements */
842
+ .tab-buttons button, #models-to-add-text, .gradio-button {
843
+ font-family: 'Sora', sans-serif;
844
+ font-weight: 400;
845
+ letter-spacing: -0.05em;
846
+ }
847
+ /* Specific Table Styling */
848
+ table, .table, th, td {
849
+ font-family: 'IBM Plex Mono', 'Noto Color Emoji', sans-serif, monospace !important;
850
+ font-weight: 400;
851
+ letter-spacing: 0.01em;
852
+ }
853
+ /* Technical/Code Elements */
854
+ .code-block, .technical-text {
855
+ font-family: 'IBM Plex Mono', monospace;
856
+ font-weight: 400;
857
+ letter-spacing: 0.01em;
858
+ }
859
+ /* Additional Elements */
860
+ #methodology-text p, #methodology-text li, .markdown-text {
861
+ font-family: 'Zwizz Regular', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
862
+ font-size: 16px !important;
863
+ letter-spacing: var(--zwizz-spacing);
864
+ line-height: 1.7;
865
+ }
866
+ /* Font weight utilities */
867
+ .zwizz-medium {
868
+ font-family: 'Zwizz Medium', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
869
+ }
870
+ .zwizz-semibold {
871
+ font-family: 'Zwizz SemiBold', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
872
+ }
873
+ /* Maintaining Original Layout Rules */
874
+ .gradio-container {
875
+ max-width: 95% !important;
876
+ }
877
+ /* Table Layouts */
878
+ .large-table,
879
+ .large-table .table-wrap,
880
+ #multilingual-model-table .table-wrap,
881
+ #lookup-table .table-wrap {
882
+ height: 35em !important;
883
+ overflow-y: scroll !important;
884
+ }
885
+ /* SVG Container Rules */
886
+ .svg-container,
887
+ .main-svg {
888
+ width: 100% !important;
889
+ }
890
+ .large-table, .large-table .table-wrap, #multilingual-model-table .table-wrap, #lookup-table .table-wrap {
891
+ height: 35em !important;
892
+ overflow-y: scroll !important;
893
+ }
894
+ .left-side-table .table-wrap {
895
+ height: 15em !important;
896
+ overflow-y: scroll !important;
897
+ }
898
+ #average-wer-table .table-wrap {
899
+ height: 8em !important;
900
+ overflow-y: scroll !important;
901
+ }
902
+ #general-wer-table .table-wrap {
903
+ height: 35em !important;
904
+ overflow-y: scroll !important;
905
+ }
906
+ """