ardaatahan commited on
Commit
1543414
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,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: format use-huggingface-data use-local-data
2
+
3
+ format:
4
+ @pre-commit run --all-files
5
+
6
+ use-huggingface-data:
7
+ @python multilingual_generate.py download
8
+ @python performance_generate.py download
9
+ @python quality_generate.py
10
+
11
+ use-local-data:
12
+ @python performance_generate.py
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: WhisperKit 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 Benchmark dashboard.
49
+
50
+ ## Data Generation
51
+
52
+ The data generation process involves three main scripts: performance_generate.py, multilingual_generate.py, and quality_generate.py. Each script is responsible for updating a specific aspect of the benchmark data.
53
+
54
+ 1. **Performance Data Update (performance_generate.py)**:
55
+
56
+ - Downloads benchmark data from [WhisperKit Evals Dataset](https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset).
57
+ - Processes the data to extract performance metrics for various models, devices, and operating systems.
58
+ - Calculates metrics such as speed, tokens per second for long and short-form data.
59
+ - Saves the results in `performance_data.json` and `support_data.csv`.
60
+
61
+ 2. **Multilingual Data Update (multilingual_generate.py)**:
62
+
63
+ - Downloads multilingual evaluation data from [WhisperKit Multilingual Evals Dataset](https://huggingface.co/datasets/argmaxinc/whisperkit-evals-multilingual).
64
+ - Processes the data to generate confusion matrices for language detection.
65
+ - Calculates metrics for both forced and unforced language detection scenarios.
66
+ - Saves the results in `multilingual_confusion_matrices.json` and `multilingual_results.csv`.
67
+
68
+ 3. **Quality Data Update (quality_generate.py)**:
69
+ - Downloads quality evaluation data from [WhisperKit Evals](https://huggingface.co/datasets/argmaxinc/whisperkit-evals).
70
+ - Processes the data to calculate Word Error Rate (WER) and Quality of Inference (QoI) metrics for each dataset.
71
+ - Saves the results in `quality_data.json`.
72
+
73
+ ## Data Update
74
+
75
+ To update the dashboard with latest data from our HuggingFace datasets, run:
76
+
77
+ ```sh
78
+ make use-huggingface-data
79
+ ```
80
+
81
+ 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:
82
+
83
+ ```sh
84
+ make use-local-data
85
+ ```
constants.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from textwrap import dedent
2
+
3
+ from iso639 import Lang
4
+
5
+ BANNER_TEXT = """
6
+ <div style="text-align: center;">
7
+ <h1><a href='https://github.com/argmaxinc/WhisperKit'>WhisperKit Benchmarks</a></h1>
8
+ </div>
9
+ """
10
+
11
+
12
+ INTRO_LABEL = """We present comprehensive benchmarks for WhisperKit, our on-device ASR solution, 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."""
13
+
14
+
15
+ INTRO_TEXT = """
16
+ <h3 style="display: flex;
17
+ justify-content: center;
18
+ align-items: center;
19
+ "></h2>
20
+ \n📈 Key Metrics:
21
+ Word Error Rate (WER) (⬇️): The percentage of words incorrectly transcribed. Lower is better.
22
+ Quality of Inference (QoI) (⬆️): Percentage of examples where WhisperKit performs no worse than the reference model. Higher is better.
23
+ Tokens per Second (⬆️): The number of output tokens generated per second. Higher is better.
24
+ Speed (⬆️): Input audio seconds transcribed per second. Higher is better.
25
+
26
+ 🎯 WhisperKit is evaluated across different datasets, with a focus on per-example no-regressions (QoI) and overall accuracy (WER).
27
+ \n💻 Our benchmarks include:
28
+ Reference: <a href='https://platform.openai.com/docs/guides/speech-to-text'>WhisperOpenAIAPI</a> (OpenAI's Whisper API)
29
+ On-device: <a href='https://github.com/argmaxinc/WhisperKit'>WhisperKit</a> (various versions and optimizations)
30
+
31
+ ℹ️ Reference Implementation:
32
+ <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.
33
+ \n🔍 We use two primary datasets:
34
+ <a href='https://huggingface.co/datasets/argmaxinc/librispeech'>LibriSpeech</a>: ~5 hours of short English audio clips
35
+ <a href='https://huggingface.co/datasets/argmaxinc/earnings22'>Earnings22</a>: ~120 hours of English audio from earnings calls
36
+
37
+ 🌐 Multilingual Benchmarks:
38
+ These benchmarks aim to demonstrate WhisperKit's capabilities across diverse languages, helping developers assess its suitability for multilingual applications.
39
+ \nDataset:
40
+ <a href='https://huggingface.co/datasets/argmaxinc/whisperkit-evals-multilingual'>Common Voice 17.0</a>: Short-form audio files (<30s/clip) for a maximum of 400 samples per language from Common Voice 17.0. Test set covers a wide range of languages to test model's versatility.
41
+
42
+ \nMetrics:
43
+ Average WER: Provides an overall measure of model performance across all languages.
44
+ Language-specific WER: Allows for detailed analysis of model performance for each supported language.
45
+ Language Detection Accuracy: Measured using a confusion matrix, showing the model's ability to identify the correct language.
46
+ Results are shown for both forced (correct language given as input) and unforced (model detects language) scenarios.
47
+
48
+ 🔄 Results are periodically updated using our automated evaluation pipeline on Apple Silicon Macs.
49
+ \n🛠️ Developers can use <a href='https://github.com/argmaxinc/WhisperKit'>WhisperKit</a> to reproduce these results or run evaluations on their own custom datasets.
50
+
51
+ 🔗 Links:
52
+ - <a href='https://github.com/argmaxinc/WhisperKit'>WhisperKit</a>
53
+ - <a href='https://github.com/argmaxinc/whisperkittools'>whisperkittools</a>
54
+ - <a href='https://huggingface.co/datasets/argmaxinc/librispeech'>LibriSpeech</a>
55
+ - <a href='https://huggingface.co/datasets/argmaxinc/earnings22'>Earnings22</a>
56
+ - <a href='https://huggingface.co/datasets/argmaxinc/whisperkit-evals-multilingual'>Common Voice 17.0</a>
57
+ - <a href='https://platform.openai.com/docs/guides/speech-to-text'>WhisperOpenAIAPI</a>
58
+ """
59
+
60
+
61
+ METHODOLOGY_TEXT = dedent(
62
+ """
63
+ # Methodology
64
+
65
+ ## Overview
66
+ WhisperKit Benchmarks is the one-stop shop for on-device performance and quality testing of WhisperKit models across supported devices, OS versions and audio datasets.
67
+
68
+ ## Metrics
69
+
70
+ - **Speed factor** (⬆️): Computed as the ratio of input audio length to end-to-end WhisperKit latency for transcribing that audio. A speed factor of N means N seconds of input audio was transcribed in 1 second.
71
+ - **Tok/s (Tokens per second)** (⬆️): Total number of text decoder forward passes divided by the end-to-end processing time.
72
+ - 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.
73
+ - **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.
74
+ - **QoI (Quality of Inference)** (⬆️): The ratio of examples where WhisperKit performs no worse than the reference model.
75
+ - This metric does not capture improvements to the reference. It only measures potential regressions.
76
+ - **Parity %**: The percentage difference between a model's Average WER on a given device and its Average WER on the Apple M2 Ultra, where a negative value indicates worse performance compared to the M2 Ultra.
77
+ - **Multilingual results**: Separated into "language hinted" and "language predicted" categories to evaluate performance with and without prior knowledge of the input language.
78
+
79
+ ## Data
80
+
81
+ - **Short-form**: 5 hours of English audiobook clips with 30s/clip comprising the [librispeech test set](https://huggingface.co/datasets/argmaxinc/librispeech). Proxy for average streaming performance.
82
+ - **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). Proxy for average from-file performance.
83
+ - Full datasets are used for English Quality tests and random 10-minute subsets are used for Performance tests.
84
+ - **Multilingual**: Max 400 samples per language with <30s/clip from [Common Voice 17.0 Test Set](https://huggingface.co/datasets/argmaxinc/common_voice_17_0-argmax_subset-400). Common Voice covers 77 of the 99 languages supported by Whisper.
85
+
86
+ ## Performance Measurement
87
+
88
+ 1. On-device testing is conducted with [WhisperKit Regression Test Automations](https://github.com/argmaxinc/WhisperKit/blob/main/BENCHMARKS.md) on iPhones, iPads, and Macs, across different iOS and macOS versions.
89
+ 2. Performance is recorded on 10-minute datasets described above for short- and long-form
90
+ 3. Quality metrics are recorded on full datasets on Apple M2 Ultra Mac Studios to allow for fast processing of many configurations and providing a consistent, high-performance baseline for all evaluations displayed in the English Quality tab.
91
+ 4. Quality is also sanity-checked on 10-minute datasets in order to catch potential correctness regressions across different device and OS combinations despite running the same version of WhisperKit.
92
+ 5. Results are aggregated and presented in the dashboard, allowing for easy comparison and analysis.
93
+
94
+ ## Dashboard Features
95
+
96
+ - Performance: Interactive filtering by model, device, OS, and performance metrics
97
+ - Timeline: Visualizations of performance trends
98
+ - English Quality: English transcription quality on short- and long-form audio
99
+ - Multilingual Quality: Multilingual (77) transcription quality on short-form audio with and without language prediction
100
+ - Device Support: Matrix of supported device, OS and model version combinations. Unsupported combinations are marked with :warning:.
101
+ - This methodology ensures a comprehensive and fair evaluation of speech recognition models supported by WhisperKit across a wide range of scenarios and use cases.
102
+ """
103
+ )
104
+
105
+ PERFORMANCE_TEXT = dedent(
106
+ """
107
+ ## Metrics
108
+ - **Speed factor** (⬆️): Computed as the ratio of input audio length to end-to-end WhisperKit latency for transcribing that audio. A speed factor of N means N seconds of input audio was transcribed in 1 second.
109
+ - **Tok/s (Tokens per second)** (⬆️): Total number of text decoder forward passes divided by the end-to-end processing time.
110
+ - **Parity %**: The percentage difference between a model's Average WER on a given device and its Average WER on the Apple M2 Ultra, where a negative value indicates worse performance compared to the M2 Ultra.
111
+
112
+ ## Data
113
+
114
+ - **Short-form**: 5 hours of English audiobook clips with 30s/clip comprising the [librispeech test set](https://huggingface.co/datasets/argmaxinc/librispeech).
115
+ - **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).
116
+ """
117
+ )
118
+
119
+ QUALITY_TEXT = dedent(
120
+ """
121
+ ## Metrics
122
+ - **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.
123
+ - **QoI (Quality of Inference)** (⬆️): The ratio of examples where WhisperKit performs no worse than the reference model.
124
+ - This metric does not capture improvements to the reference. It only measures potential regressions.
125
+ """
126
+ )
127
+
128
+ COL_NAMES = {
129
+ "model.model_version": "Model",
130
+ "device.product_name": "Device",
131
+ "device.os": "OS",
132
+ "average_wer": "Average WER",
133
+ "qoi": "QoI",
134
+ "speed": "Speed",
135
+ "tokens_per_second": "Tok / s",
136
+ "model": "Model",
137
+ "device": "Device",
138
+ "os": "OS",
139
+ "parity": "Parity %",
140
+ }
141
+
142
+
143
+ CITATION_BUTTON_LABEL = "Copy the following snippet to cite these results"
144
+
145
+
146
+ CITATION_BUTTON_TEXT = r"""@misc{whisperkit-argmax,
147
+ title = {WhisperKit},
148
+ author = {Argmax, Inc.},
149
+ year = {2024},
150
+ URL = {https://github.com/argmaxinc/WhisperKit}
151
+ }"""
152
+
153
+
154
+ HEADER = """<div align="center">
155
+ <div position: relative>
156
+ <img
157
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbgAAAG5CAYAAAD8liEWAAAdN0lEQVR4Ae3db3IbRbfH8XNGMhUeyzzKCtBdQXxXECW2c133Dc4KYlaQZAUhKwheQewVoLyhXNixhxVgVoBYASKWIWVp+txuxeEBbhxsSzM9an0/VRQQ/liZGc2vT/8VAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABadSsk27g9NANSYDVSz3MxeHRy1dgXRba6fdp1lj8Vs1b9AOzKXbCCqJ2Ky1/ikke/vf9qXihFwAP6s719G92K8jOCDbfP3TjEqXvpQ6Epa+qru6XevP+tJhTIBgP/oFOfFD+FFK6jUJNzOi+MEwy3omGXfrPvKVCpEwAH4u/bkRYtKXVzzjiRMnX6ztfVLWypCwAH4kM6DtbMtQSXW1t6Ea92R9LV/f7O0LRUh4AB8mHPbgkpkkn0hC8KpVPZ7JeAAfJCp3hFUZVUWhVX3eyXgAFymI6iGLVDA+W5KqQgBBwBIEgEHAEgSAQcASBIBBwBIEgEHAEgSAQcASBIBBwBIEgEHAEgSAQcASBIBBwBIEgEHAEgSAQcASBIBBwBIEgEHAEgSAQcASBIBB+BSW1u/VHZ2FzBrBByASw2HzUU6iDOKzfXTrqAUBByAS2VOuoJSFaaPBKUg4ABcykQfb27+3hGUYnJtjUZEWQg4AB/TLkbFS0EpLq5tR1CKpgDAx/gKY2Nt+ENjqfFwf//TvmBqoXKbhBvVW6lKDzgV6QtQMybW9k8nMwSvymS1OC9+8kG3q+JeNbOs70aNgdzQrfatQa+nN/7vY9jsTt9V6xqjVdHsbnE+3ub5K58KsOA2usNVa/rAc7qqKl/Qqq6KDUT1xF/vvYOj1q7U0Mb94bZ/Sz4Ss1UCaXb8/a4kewg44G9CS32cjbdV9JmgKv3GJ417dekCvehC/CZUroKZI+CAyELQuaw4NiYBVGXgQ+6/Y4fcJNzOi2PhvpemqoBjFiVwif38037mGvcYR65MLWZsTio3wi0JBBzwESHkXGZfCqrhxz/XI+7sMdlVhG7JZBBwwD84PFzJxdyeoBJqsiWRsKtIWgg44AqsobuCSqjoXYmnI0gGAQdcQajiGIurhplEm47vq8eOIBkEHHBV5r4XAHODgAOuyGl2IgDmBgEHXJGp6wuAuUHAAVe0VCxRwQFzhIADACSJgAOuKCz6FgBzg4ADACSJgAMAJImAAwAkiYADACSJgAMAJImAAwAkiYADACSJgAMAJImAAwAkiYADUDcdicQk3ll0mD0CDkDtrK+fdqViGxvDVSHgkkLAAaidzElXKmYmjwRJIeAA1I6JPt7a+qWyampz8/eOOtkSJIWAA1BH7bPTpW+kIsWoeCkRx/5QjqYAQB2ZdDfWhj80lhoP9/fLOaooVG6TcLPqu0RRPgIOf7G1Ze23g7d/dA3dat8a9Ho6ENSSivTDn00l3KPr3qe22rtJFVbX6sVktTgvfvJBt6viXmXF0sm05/Jtdn/vSHPccaJfFOfjbX8Vazux5P39Dfw97sv11P/+lkylZBv3hyaYczYQ1V5DbW//cCWXBVaH5zm89JzITsst7/by2TQ+wku/yIqu/38/W9SXYV2UcX+3utY+y8626nJ/D45apWdPQMDhelR2l1dGT3u92wtZ1UV/ntV2lovWV7N68X3I+v3Tr1T0maB6C3J/qwo4Jpngeky2/eD/cZUz3HDBv/wOXq88KfPlFxwerXzlxHYEVdvl/s4WAYfr8+Miv51+8lJQmdBtFVr2UpHCjb+S64/p4YbC/W24xnOpyKLcXwION2JmWzF2m1hUvl/0edkt+z/L89sDpYqrjL+/+bSTZ65jUe4vAYcbU5ex80NFlpycSMVcJrmgEpbZnlQsc81dSRwBhylYV1CJb/NW5QE3Ho8r/5mLqtWq/lpXWTHGQsAB9deXCEI3lqASsWYla6RnqyoEHAAgSQQcACBJBBwAIEkEHAAgSQQcgI9hognmFgEH4FJKwFWBa1wSAg4A4iLgSkLAYRpsuAygtgg4TIOAA1BbBByAS93gFGmgNgg4TCWcBC1Iljr3s6BcNCJKQ8BhKpkUdFMmzFSZAIG5RcBhKqNMVgXJMtG+oGT2o6AUBBymo0bAJUwbnAlXNpUsF5SCgMN0TL8QJOvgYHIOHd2UJcqKjHP3SkLAYVqd9fXTrqBMHYmq+tOmF4WavYp58KhFf7bKRcBhapmTrqBU/9sdxusKdrorKEVmzScSycbGMPnhBQIOUzPRx5ubLBco0zizLYnkIG+dOLEdwWyp7USt3kweSeIIOMxCuzgvjre2fmHJQElCIyLm9X19tPJElAknM6Nu7+D1SrTqLTRI1Um0RlNVCDjMSufszdIPVHKlaZ+dLn0jER28bt2jkpueiT0/eP3ZtkQ0Ho1fSOLjbwEBh1nq+Erup4214UuCrgQmXX9tj2Ne21DJNVzjv8QcE0+uxQb+j51w7Q6PVr6SSLa2rB2eITVNvnoLVEr24P7wJ0HtlTSbqh+2IVKVE3H2aybWl0z7merAjRqDW+1bg15vvnbK2Lg/NIkvXNe8oba3f7iSS0STGbROV1WsY5J9rmp/dKOqffyZmrcZfCof31LLdLKcYnDxN/7fdb9OFspndnIY+T5t+vvkXHbXxPluUY0+lHBw1Co9e4JKfgjmx1bX2mdy1rGmtcPsSFO9GyoHiSEEo8hJHV7k79Uk4P7GBipXayi8fwmHRoc5+bHxSSPf34830WGjO1y1TLq+K+lx/QLPBv4z7YxdczePOBkkVF1vh8PV8aQhoXd8y6Hjf7ntGxBXCqo6NiQIONRG2FC50NFXolm8WVcqu8sro6e93u2oFV89A25KvhpcyuTpt+8WdUcRnrFx5l74SrAeXWfq9paLlSe9PF4PQ6i6CtNnYmG3oPhV1ywRcKid8BJyWXEcrUXoqw4fcvdihlySAXdBM/n6u8PWU4loY+3NrlgWdfq6ifX8ONlDiSRUbL+9OXvmH7RosyzLVlXAMckEVxbW7GSucU9ibd1ksvrbcOmZoBS+y/LJxtrwh5jLEUZFEV7q0RowYZyt6ZrRQj6E29np2XHK4VYlAg7XEkLOMovWug0vYWZolihyIyLPb/vxxHhLEXyw5DEXX4fKLdwDwUwQcLi2yYywiIt+i9GY1m2JQiMi5v6i1tCeROIbb9GWP4QxNyq32SLgcCOm9lwiUdG7glJpmNwQyUHEyS6t1jjazy4iXvNUEXC4kYt1PVHGSszS34EhOpNuzLG4f1pzVpZYE5gm3e7GpuWzRsDhxtTcK4mDPS8rcPZmaSF2u6iD0WjEuFsJCDjcmFMOakwap7VXpmHKtS4BAYcbM3V9QbLU0RVcFadyRzBzBBxuLMuyviBZlum/BZXQxHYqqQsCDjfWGDXmaqNkXNPiTebheU4MAYcbi7kgFigBAZcYAg4A4qOLsgQEHABEdtWjb3A9BByA2rk4tw6YCgEH4DIxq4rqA07j7J6C8hBwAC4TLeBMjAoOUyPgANRO5uxXqZhG+JkoFwEHoHaibAOnwtZziSHgANRPZhECLt7eqiZsi1YGAg5A7YzHk3PZKh2H+9fKeS5ICgEH4FKxzoTL89uDKrsM1exVrLPgUB4CDsClhsNmtGNcqjw13jXka4lkY2PIUTklIeAAXEpNoh16Gk6Nd2I7Uja1nYsT6qMwk0eCUhBwAC5n+ihWN2Xw+mjlia/kelIWlfzg9coTiWRz8/eOuniNiNQRcAA+pn12uvSNRHT4euWhSQndlb5yO3jduicRjUfjF8IMytIQcJiKCtsbJc+ku7E2PA7VhkRyeLTyVcM1/kvM7U33zIUdUmwn/L9iVm5bW9YO11RNqd5K1JQFFB6ut4O3f3S7cK4Z8A98yBXnxU/+pbzbUNuTcbNf9ffm4udth7/e6A5XffN81X+wVZPsc1V7/31+/+d3MyJN/X/jfvV/cSIuOznIV6KtdZu8d4bDVeeyu2dvhj5cOcW7bCoLYnP9tFu47JGKddNbVKm7B0fLX0oED+4Pf4pxPQ+OWlGe3Y37QxN8hA1U9I/p9vZ+A2MfNJrZwDn5uZnZya1W66TX07malr/Z9RVsNuq6LLvjfz8d323a+fsxN/7X2gTXP6vq+5t8BRe6VYpR8bJw0p08fgKgPNq2P2/SbO8bP/675961qAuncvbmTHw1mPtf3vMvu12psUnj2PRZYUV3Mqrjwq++e5P8//fJwtQMcyHpgJuE23lxLAziAvVjodE5Gd973FhqPNzfr9dQQehS/P307IVvHG8L5lLSk0xC5SaEG1BvJquhIRpzEsuHnJ2eHTsj3OZZsgHnx0q2L1qIAOqvc9EgrQU/tvwiBK9grqVbwSm7AwBzxTdI1/14l0QWKkk/thZtCQFmJ92Ao/UFzB01fSaR+Uoy+mfAbCQZcN3uZGshpuoC88Y3TGNuDXbxGVh8nYgkA+6W3CLcgPnUPh8sdSSSzXddpLw/EpFkwLEzSXU4iRizNsriDS+MnTK0kRD2osQ86gvSpRYtZFSsI0gGAQegXiz7t0SiZnRPJoSAA1Arau62RGJZ9rkgGQQcgFqxTKNVcEhLkgE32fUbwFxSZjFiRqjgANSKGQGH2Ugz4JrjjiBlHUG6NN4sWZuc+I1UUMEBwIXM2a+CZCQZcIVjLQuA6zOdr1PG8XFJBpyKdgTAfDLtSyQqQsAlJM0uSqOCA+aVivtZIvFjcH1BMpIMOBZrAnNMsxOJpRHxZ2PmEq3gOAsOmFf/WjnPJZLRaNQXuimTkVzAcdwFMMdU8l7vdrSAyXP/s1Wo4hKRXMAVpo8EwFwytecSWR0+A2YjqYCbVG8m2wJgHu0eHq7kEtnkM6jbE8y9ZAJuc/P3TuH0pQCYP75bcNktP5WaGBXFE7oq518SARcqt+K8OBa2cALmj6+Wlovle728Pousw1jcqBjdE6OSm2dNmWOTYPNjboWjWxKYLzYQ1ZMw3nV4+FkuNTSZcCKyvb5+uisue6xiW4K5UvuA29qy9tvB23a2VLTHznWcZR0VveMftq4Pto7MmEq8jV5vyr8k2D8PFbOB/x7+Y8Vl7zdOvtidxMR+lMxOWuPWSZ0qto+5GBcMf4gPu66ZtXXyHgobSmT/Fv3LxhL+n/11Fre/Bu9+n5MNKJQZ3hUqPeAe3B/+ZFN0HZ69OZt0pBZF+LssBJBMviYyhdC3br716L9spq5fFEsnbbk1mJcvHBCHDfz3bmfsmrt5/mlfIvrfjeGqK9yqy7I75nxovAuZP8LlfbBOQlit75z83PTBuj/lJJZpJ8FsdIervkv2iWp21xhSKd1cd1Fez7svZ8u1vibIgGuajJOtPIn53QkTydy4eGzOtkdFCDLf8nXhn5i8b/H+0fC1d+FhF/8sNIwLp7Jx/zR0jfYaanv7EWZsHuStMHFlOxzKXDSKl/6zdQWlWYyA8xVbo2g+3I/c6gTmkQ+J3uHrz7YlIt8T9KI4L568+zuVm/PVnsm2H7vf3lgb7jaWGs/396t/L1y8i+75z3BMyJUn/fPgwvTjYvke4QZcXxiTbrpm1On7IQR8EfZEZi0E3XlxHCpDiWRUjB4KW4OVJumAC1/ORtF4SJckcDM+WPKYjcNQuZVc4XSKURFt/WyYqaliO4JSJB1w/sv5nMoNuDnLLNo6sLAMqJTK7e98gK6/28M2CmtoT1CKlANucHDU2hUAN9ZqjaPt5uHHyZ5JRbTCn/V3BwctdkwpSbIBp2bfC4CpxNrZfzIuVuXkC5PVra1foq1Rm8f1t/Mg3QqOfeSAuTUajao+07E9HDY5RzIx6VZwwsQSYF41TCsPm4bTaAHHou9yJBtwpu+2BgIwf5zKHamY++uWW0hAsgHn1FHBlSzsxiBACTTCno3qqKJSk2zAfTKmixKY0kJ9hyzTfwuSkm4FJw0CDpjOYn2HjAouNelv1QVgHi3UsTIsEygHAQegdv5+phpwEwQcALzTkUj+OBQVM0XAAfgg5aVbGRPrC2Yu2YDLpKCLA5iCcYxLZTJnvwpmLtmAGze0IyhXc9wRAFMzZVlTGdLdycSMCq5kI65x4uxHQUWMvXNLkPBmy8bGqSXLTLuCZKlkuaAajYyAK0HCk0y08r3sFo5Gu8Z051QgK3jpVuXiTDie6xlLN+Ain++UusrP6/oTVRbFlk3NXu3nn/Ylkli768d9Z8Q7PT1VKS8TaP/2pln+cfcLqhgV0U5ANuOsv7JZU7+SSDY2htGGF2KeCddwza8FM5X0OjgTfUwVN3sX1du2RKKqrwTlUdu56DKL8+ML25JI1CTazw4VsxPbEcxM6gu922enS8eE3OyEcCvOi2OJJOzZ993r5Z6gHCr5weuVaD0f4fnyDdNHEovpo0kDLpLXR/7a+3sgmIn0dzLxY3Eh5GI+tKnYXD/tXoRbRyII4Za5xj1BOULl9roV9fqOR+MXEvd067bvfn8pEYV7QCU3G4uxVZcPufBifnD/7BlBd30h2DbWhseF02jhFlq1IdxiTnxIkw18l2/PMrsXs3Lb2rL2/6wNX6pptC7CP5h0H6ydfRO7kgv3xFez9FZMQaVkD+4PfzKp2TlL/mWpZt/7FutJM8v6nywv93s9dhIIwovm/Oys4wq36rLsjjjZinP/bOAH2/ri75Nl0js8XMmlBjbuD01q6p+OXLH3s09N+yruZ5dJ3hq3Tnp5vGc/nArvMvfIxPlw1boNJfTVP3tNlb1vI45JbnWtfdo47TZMV51kd1TfbbCgHzm/rnbv3L85OGqVnj1BJT9kGt3uf1pRzea4E3YoCTfaVO/Ofpq6b83KYgadSfjSzPYFE164vgWa+//3j/6FOhCXnYwuDqLN57QSq0fA2cB/iB3f/5LHDv5Q3Y+drmaZfG7OPz9qHf/L7ascd1PGM1em65zZ9r4xoSonYu77bGnpZH8/3jO/7u+TFratmt2tQ/gRcFcQWn+Fjr4SzeINSuNDdn33yl5dqq5Zih5wvvdhuVh+GLXq8l13blw8Nmfb8xRQ0ansNpYaz2MGXXhnjjP3QiXeTNWAgLuGje5w1XclfFP3sjx1oYXrMvsyxWB7L2bA+Yqnd3i08lAi8kMOL/wFYH3pFPy76uvvDltPJaKNtTe7YvEKg6oCLolJJgd56+Ridh3jaJGEcPuXW/7vlMMtpnB9m64Z+aU4PCbcpmdOnoRrKRGNiiLcx+Tfl8nMogyz63y3WNTW7aJ6P30/ZrdZ6nywPI85gzRUbrG2ZktSmKm5PnwhkeT57YEuwFKEpJYJTKoHFklWzonsMH2/XEsu3vZkYSIJldvshUouTP6QSKyR/hKE5NbBmdpzQZUGrc9Gu4JSfZvHm6ZemEbbdzR1GvHaxtyOrSrJBdzFGBBdZRUJ6wl7vdtc73JFnTFJ12SJ/LWNuZXgdZY+zKMkdzLxVVwuqISq5oKyRQu44rzoCkp19mYp/u4tiUoy4DKTHwWVKDLj6Jqk2aqgXMo1LkuaFZxYX1CJ5rjZF6Qr3qntC0Md63fLkmTAOWUMriq32m+51sA0Mv1cUIokA25JWY9VFSaYpO1jG/piNuwK+3biZtI8LoduMwBYeItxHhwA1FdHUAoCDtOgexJAbSUZcGwbVRkCDpiBmIu9U0YFBwCRvR3cIuBKQMABuBRnLGKeEXAAgCQRcAAQGRsmlIOAA4DI2DChHAQcUH9MQABuINmAS/2co1pQrnFFCLjExVomkPokomQDzinHuJTOCLiqbHZ/70jFNtdPu4JKDIfNyo/M2dgYJn9MT7pdlKbfC0plme0JKmFZUfmhmIXpI0El1KT6Q08LIeDm1diNdummLJHKyeHhSi6ohBN5XGU31uamrxhNuoJq+MbE5JpX65kkLtmAy/PbA3PyUNhOauZCw6FRNB4KqtT5bbhU2QtpPBq/EBZ5V6ldjIpvqmrEPLg/XIj7m/QsyoO8deKbvveo5GYnXMvMNe6x32f1fIPtyYP14YsyX4JbW9b+n7XhSzWtvsts0Zmsnp0uHZdZyYX7G8LNRJ7IAlBZEBv3h9v+d/tIzHy/szIr7VpsIKon/gu4d3DU2pUF5p8jk/j6/kPsfNKQ/NuD1kwmU4VJLC5zj0zcE74f0fX9uypfymRnFvc3hNr52VlnXOgXdbm//j1SSfYsTMABs1CTgFswFoYZ9iyT3njc7OdT9h50fZg3m+OOFratmt2t+1T59z1QKU3pJ+CAGiLgqmVivZZrfdnLtZSx9FC5jjP3QsXokq1QVQHXFACooRBuh0crpU5muhhLfrix9mZXLGNZRGLYqgtA7YRuuaZrPpWKjIoiTLpgxnViCDgAteMDbqfKmbphWZHvptwRJIWAA1A7DSe5VCxzzV1BUgg4ALXzbT6b5Q/XcVEx0k2ZEAIOQN30JRIl4JJCwAEAkkTAAQCSRMABAJJEwAEAkkTAAQCSRMABAJJEwAEAkkTAAQCSRMABAJJEwAFXFM4OEwBzg4ADrqhYKtoCYG4QcMAVNcwIOGCOEHDAFY2drgqAuUHAAVekKncEwNwg4ICrMukKgLlBwAFXsLk5mUHZEZTOV8rRzmQz5Ty4lBBwwBUUo+KZoBJmUvlp3n/64d8LkkHAAf9gUr2ZbAsqoaqvJBLLpCdIBgEHfEQIt+K8OBZUQkX6371ejhYyh4cruf8QuSAJBBxwiT+FW0dQuhBumWvck8gaRePL8FkEc4+AA/5mc/20++D+2bPifPyDEG7V8FVTCLf9/NO+RBY+Q/gshNz8UwEW1NaWtd8O3rZdY7TqLOtkqnfNXNd/Ldix5EpsoKJTzTo00dwytzfpGqyhtbU3W2qNR6rWUZMbPxcmYRccnqv3Do5alWRP6T/kwf3hTwLUjFGZ3YAN/HXb8f0+eWvcOunlypT6a1r3vQNa2LZqdneRn8FkAm7j/tAEwHxTt7dcrDwh1GYjnEwxztwLFduSBVRVwDUFAD7Cd6/1Dl9/ti2YmYuxxocba292xbJHglIwyQTApcJEi6ZrPhWUYlQUT/yfqIpLQsABuJQfX8jrMLMxVXl+e+C7KXcEpSDgAFzKMtsTlMplLCwvCwEH4FKt1jjevpALYjzmGpeFgANwqV7vNuNDJQvdlIJSEHAAgCQRcACAJBFwAIAkEXAAgCQRcACAJBFwAIAkEXAAgCQRcACAJBFwAIAkEXAAgCQRcACAJBFwAIAkEXAAgCQRcACAJBFwABAfR+aUgIAD8EGqwkGcFdHFCrjKfq8EHIAPc/azoBrmvpdFUWHDiYAD8EGm2hNUwjV0VxaFyZ5UhIAD8P/4LrP+wVFrV1CJw8OV3F/0XBIXnquGa+RSEQIOwF+El1DmGvcElWoUjS/DtZeUqT7dzz/tS0UIOAD/4cdHQrhV+RLCO+Gah2ufYsiF35Nldu+718uVdnurlGzj/tAEQG2Fl48TPZHM7Uy6yhCdf29um+gXqtbxY1arMpds4Cu2EzN51XLLu71cWQoBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoN7+D8MSQdHK7cEnAAAAAElFTkSuQmCC"
158
+ style="display:block;width:7%;height:auto;"
159
+ />
160
+ </div>
161
+ </div>"""
162
+
163
+
164
+ EARNINGS22_URL = (
165
+ "https://huggingface.co/datasets/argmaxinc/earnings22-debug/resolve/main/{0}"
166
+ )
167
+ LIBRISPEECH_URL = (
168
+ "https://huggingface.co/datasets/argmaxinc/librispeech-debug/resolve/main/{0}"
169
+ )
170
+
171
+ AUDIO_URL = (
172
+ "https://huggingface.co/datasets/argmaxinc/whisperkit-test-data/resolve/main/"
173
+ )
174
+
175
+ WHISPER_OPEN_AI_LINK = "https://huggingface.co/datasets/argmaxinc/whisperkit-evals/tree/main/WhisperKit/{}/{}"
176
+
177
+ BASE_WHISPERKIT_BENCHMARK_URL = "https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data"
178
+
179
+ AVAILABLE_LANGUAGES = [
180
+ "af",
181
+ "am",
182
+ "ar",
183
+ "as",
184
+ "az",
185
+ "ba",
186
+ "be",
187
+ "bg",
188
+ "bn",
189
+ "br",
190
+ "ca",
191
+ "cs",
192
+ "cy",
193
+ "da",
194
+ "de",
195
+ "el",
196
+ "en",
197
+ "es",
198
+ "et",
199
+ "eu",
200
+ "fa",
201
+ "fi",
202
+ "fr",
203
+ "gl",
204
+ "ha",
205
+ "he",
206
+ "hi",
207
+ "hu",
208
+ "hy",
209
+ "id",
210
+ "it",
211
+ "ja",
212
+ "ka",
213
+ "kk",
214
+ "ko",
215
+ "lo",
216
+ "lt",
217
+ "lv",
218
+ "mk",
219
+ "ml",
220
+ "mn",
221
+ "mr",
222
+ "mt",
223
+ "ne",
224
+ "nl",
225
+ "nn",
226
+ "oc",
227
+ "pa",
228
+ "pl",
229
+ "ps",
230
+ "pt",
231
+ "ro",
232
+ "ru",
233
+ "sk",
234
+ "sl",
235
+ "sq",
236
+ "sr",
237
+ "sv",
238
+ "sw",
239
+ "ta",
240
+ "te",
241
+ "th",
242
+ "tk",
243
+ "tr",
244
+ "tt",
245
+ "uk",
246
+ "ur",
247
+ "uz",
248
+ "vi",
249
+ "yi",
250
+ "yo",
251
+ "yue",
252
+ "zh",
253
+ ]
254
+ LANGUAGE_MAP = {lang: Lang(lang).name for lang in AVAILABLE_LANGUAGES}
dashboard_data/config.json ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "whisperkit-coreml",
3
+ "version": "0.2",
4
+ "device_support": [
5
+ {
6
+ "identifiers": ["iPhone11", "iPhone12", "Watch7", "Watch8"],
7
+ "models": {
8
+ "default": "openai_whisper-tiny",
9
+ "supported": [
10
+ "openai_whisper-tiny",
11
+ "openai_whisper-tiny.en",
12
+ "openai_whisper-base",
13
+ "openai_whisper-base.en"
14
+ ]
15
+ }
16
+ },
17
+ {
18
+ "identifiers": ["iPhone13", "iPad13,18", "iPad13,1"],
19
+ "models": {
20
+ "default": "openai_whisper-base",
21
+ "supported": [
22
+ "openai_whisper-tiny",
23
+ "openai_whisper-tiny.en",
24
+ "openai_whisper-base",
25
+ "openai_whisper-base.en",
26
+ "openai_whisper-small",
27
+ "openai_whisper-small.en"
28
+ ]
29
+ }
30
+ },
31
+ {
32
+ "identifiers": [
33
+ "iPhone14",
34
+ "iPhone15",
35
+ "iPhone16",
36
+ "iPhone17",
37
+ "iPad14,1",
38
+ "iPad14,2"
39
+ ],
40
+ "models": {
41
+ "default": "openai_whisper-base",
42
+ "supported": [
43
+ "openai_whisper-tiny",
44
+ "openai_whisper-tiny.en",
45
+ "openai_whisper-base",
46
+ "openai_whisper-base.en",
47
+ "openai_whisper-small",
48
+ "openai_whisper-small.en",
49
+ "openai_whisper-large-v2_949MB",
50
+ "openai_whisper-large-v2_turbo_955MB",
51
+ "openai_whisper-large-v3_947MB",
52
+ "openai_whisper-large-v3_turbo_954MB",
53
+ "distil-whisper_distil-large-v3_594MB",
54
+ "distil-whisper_distil-large-v3_turbo_600MB",
55
+ "openai_whisper-large-v3-v20240930_626MB",
56
+ "openai_whisper-large-v3-v20240930_turbo_632MB"
57
+ ]
58
+ }
59
+ },
60
+ {
61
+ "identifiers": [
62
+ "Mac13",
63
+ "iMac21",
64
+ "MacBookAir10,1",
65
+ "MacBookPro17",
66
+ "MacBookPro18",
67
+ "Macmini9",
68
+ "iPad13,16",
69
+ "iPad13,4",
70
+ "iPad13,8"
71
+ ],
72
+ "models": {
73
+ "default": "openai_whisper-large-v3-v20240930",
74
+ "supported": [
75
+ "openai_whisper-tiny",
76
+ "openai_whisper-tiny.en",
77
+ "openai_whisper-base",
78
+ "openai_whisper-base.en",
79
+ "openai_whisper-small",
80
+ "openai_whisper-small.en",
81
+ "openai_whisper-large-v2",
82
+ "openai_whisper-large-v2_949MB",
83
+ "openai_whisper-large-v3",
84
+ "openai_whisper-large-v3_947MB",
85
+ "distil-whisper_distil-large-v3",
86
+ "distil-whisper_distil-large-v3_594MB",
87
+ "openai_whisper-large-v3-v20240930",
88
+ "openai_whisper-large-v3-v20240930_626MB"
89
+ ]
90
+ }
91
+ },
92
+ {
93
+ "identifiers": [
94
+ "Mac14",
95
+ "Mac15",
96
+ "Mac16",
97
+ "iPad14,3",
98
+ "iPad14,4",
99
+ "iPad14,5",
100
+ "iPad14,6",
101
+ "iPad14,8",
102
+ "iPad14,9",
103
+ "iPad14,10",
104
+ "iPad14,11",
105
+ "iPad16"
106
+ ],
107
+ "models": {
108
+ "default": "openai_whisper-large-v3-v20240930",
109
+ "supported": [
110
+ "openai_whisper-tiny",
111
+ "openai_whisper-tiny.en",
112
+ "openai_whisper-base",
113
+ "openai_whisper-base.en",
114
+ "openai_whisper-small",
115
+ "openai_whisper-small.en",
116
+ "openai_whisper-large-v2",
117
+ "openai_whisper-large-v2_949MB",
118
+ "openai_whisper-large-v2_turbo",
119
+ "openai_whisper-large-v2_turbo_955MB",
120
+ "openai_whisper-large-v3",
121
+ "openai_whisper-large-v3_947MB",
122
+ "openai_whisper-large-v3_turbo",
123
+ "openai_whisper-large-v3_turbo_954MB",
124
+ "distil-whisper_distil-large-v3",
125
+ "distil-whisper_distil-large-v3_594MB",
126
+ "distil-whisper_distil-large-v3_turbo",
127
+ "distil-whisper_distil-large-v3_turbo_600MB",
128
+ "openai_whisper-large-v3-v20240930",
129
+ "openai_whisper-large-v3-v20240930_turbo",
130
+ "openai_whisper-large-v3-v20240930_626MB",
131
+ "openai_whisper-large-v3-v20240930_turbo_632MB"
132
+ ]
133
+ }
134
+ }
135
+ ]
136
+ }
dashboard_data/device_map.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Mac14,12": "Apple M2 Pro",
3
+ "Mac14,14": "Apple M2 Ultra",
4
+ "Mac15,3": "Apple M3",
5
+ "Mac15,9": "Apple M3 Max",
6
+ "iPad14,8": "iPad Air 11-inch (M2)",
7
+ "iPad16,1": "iPad mini (A17 Pro)",
8
+ "iPad16,3": "iPad Pro 11-inch (M4)",
9
+ "iPhone12,1": "iPhone 11",
10
+ "iPhone14,2": "iPhone 13 Pro",
11
+ "iPhone14,5": "iPhone 13",
12
+ "iPhone14,7": "iPhone 14",
13
+ "iPhone17,1": "iPhone 16 Pro"
14
+ }
dashboard_data/diff_checker_data.json ADDED
File without changes
dashboard_data/multilingual_confusion_matrices.json ADDED
The diff for this file is too large to render. See raw diff
 
dashboard_data/multilingual_results.csv ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Model,Forced Tokens,Average WER,WER_sl,WER_sk,WER_ur,WER_sw,WER_uz,WER_pl,WER_vi,WER_sq,WER_sv,WER_he,WER_mt,WER_hy,WER_am,WER_nn,WER_be,WER_da,WER_mr,WER_kk,WER_mn,WER_ja,WER_el,WER_lv,WER_oc,WER_it,WER_ca,WER_cs,WER_te,WER_ru,WER_tk,WER_ro,WER_yo,WER_yue,WER_yi,WER_pt,WER_ps,WER_zh,WER_uk,WER_sr,WER_pa,WER_ml,WER_mk,WER_ba,WER_ha,WER_ar,WER_gl,WER_hu,WER_nl,WER_bg,WER_bn,WER_ne,WER_af,WER_hi,WER_ka,WER_de,WER_as,WER_az,WER_br,WER_ko,WER_fi,WER_id,WER_fr,WER_es,WER_et,WER_en,WER_fa,WER_lt,WER_cy,WER_eu,WER_lo,WER_tt,WER_ta,WER_th,WER_tr
2
+ openai_whisper-large-v3-v20240930,False,51.57,36.9,29.71,46.48,64.04,110.02,14.74,14.89,69.25,18.09,29.11,86.41,74.32,145.83,50.03,79.08,19.43,67.19,43.57,116.51,26.33,21.75,32.92,73.51,14.39,20.36,14.41,140.14,15.81,112.64,15.2,95.06,51.16,103.7,16.37,111.73,27.24,24.08,62.2,104.28,121.81,48.07,102.63,104.87,40.62,18.12,16.39,11.46,21.95,98.71,86.28,37.8,43.31,137.87,14.01,103.2,38.1,100.68,20.79,16.62,12.28,16.31,5.94,32.21,12.54,60.73,35.6,57.45,42.35,103.14,98.21,44.83,31.0,31.24
3
+ openai_whisper-large-v3-v20240930,True,46.09,27.13,24.61,25.59,61.29,98.84,12.12,16.92,65.69,12.97,26.85,84.04,73.95,128.9,39.97,61.51,17.63,48.26,41.87,97.08,21.97,17.73,30.78,71.01,12.83,18.25,12.85,75.43,13.28,104.35,11.41,89.71,64.28,100.0,14.93,95.78,25.34,19.14,54.07,120.4,112.94,34.52,100.0,96.64,31.45,15.0,15.3,8.91,20.42,79.7,63.89,36.54,26.14,132.26,12.26,105.14,33.33,95.96,20.75,15.42,11.11,15.51,6.1,31.51,12.13,55.96,32.84,54.92,40.65,114.11,98.39,41.54,23.3,24.29
4
+ openai_whisper-tiny,False,105.22,121.79,133.13,113.57,119.78,118.34,103.44,99.27,119.73,82.19,122.66,112.51,132.53,120.31,103.18,115.1,99.88,101.13,125.86,114.12,82.61,130.16,112.75,100.2,89.71,82.85,125.93,113.15,109.31,117.09,118.71,109.16,88.8,120.37,81.94,115.21,79.77,115.73,114.84,103.05,105.04,117.77,116.19,109.58,159.43,78.61,129.6,76.43,122.17,100.32,104.44,118.43,102.01,140.29,79.95,100.86,146.83,110.82,110.13,93.9,124.4,67.17,68.41,113.76,33.14,122.06,133.54,112.59,132.9,106.52,123.61,100.96,110.41,125.91
5
+ openai_whisper-tiny,True,86.1,81.42,92.88,70.33,112.75,122.16,56.82,50.52,99.17,64.45,72.15,103.81,133.47,140.93,102.1,98.9,79.55,102.76,179.77,128.57,53.32,66.33,89.27,93.19,60.12,59.02,81.79,133.22,58.43,124.62,66.43,111.99,90.36,102.78,65.71,105.43,65.2,69.07,80.42,104.57,133.29,83.84,110.69,97.86,97.63,54.0,85.5,54.06,83.5,106.27,103.34,93.39,102.17,140.86,49.53,112.36,90.67,102.29,62.34,72.56,54.08,59.57,34.99,101.75,33.4,130.66,101.05,93.62,97.05,113.15,107.44,80.94,42.42,66.07
6
+ openai_whisper-small,False,96.89,116.31,109.59,106.84,110.05,117.73,69.24,74.63,110.83,52.47,97.58,109.76,138.13,118.51,93.33,113.81,66.04,101.28,127.1,115.44,89.93,120.81,101.47,88.06,67.04,51.78,117.66,120.07,92.0,116.14,86.09,106.41,48.07,105.56,37.94,105.77,117.15,101.7,108.27,101.54,102.57,115.41,118.11,104.62,151.92,65.17,66.19,60.49,118.61,100.46,102.89,99.37,100.76,141.61,49.17,100.37,121.83,107.77,137.18,68.85,104.92,54.88,47.03,96.83,18.2,119.04,121.42,111.48,116.31,118.1,120.3,100.63,115.33,108.8
7
+ openai_whisper-small,True,69.14,49.09,51.74,40.93,96.1,115.21,23.74,25.43,89.94,23.97,43.29,96.2,120.55,130.06,164.5,78.06,37.18,89.86,82.95,262.79,30.25,31.49,62.47,114.14,25.02,30.35,37.7,311.76,26.09,174.55,26.99,161.95,48.61,100.0,35.7,94.66,42.22,40.02,60.5,160.3,115.92,50.81,118.57,97.46,47.01,30.45,44.66,19.94,49.16,129.67,107.33,71.02,45.58,,23.87,131.95,62.3,,34.7,30.07,23.81,27.11,11.94,72.39,17.35,97.5,75.61,67.42,77.08,102.07,103.03,42.18,21.52,33.32
8
+ openai_whisper-large-v3,False,54.77,41.01,32.74,44.39,66.07,110.74,17.82,14.19,64.45,13.59,36.31,96.11,70.79,134.23,52.71,85.41,16.63,60.48,58.79,122.59,33.66,28.76,27.08,78.1,13.55,17.32,20.67,123.88,16.13,107.71,10.99,110.75,53.95,105.56,14.51,103.6,42.68,32.28,64.23,101.2,102.62,68.19,100.46,99.97,36.03,23.57,13.44,12.17,30.2,98.44,101.1,40.79,75.87,149.84,14.75,100.54,35.32,106.08,20.94,15.9,11.86,15.51,6.36,30.06,12.7,62.68,32.37,51.06,45.38,104.29,100.73,81.72,38.07,26.73
9
+ openai_whisper-large-v3,True,34.23,18.87,18.44,21.24,58.02,90.52,10.13,12.32,53.97,9.81,23.79,78.78,54.56,,29.37,45.53,13.89,42.37,48.61,87.75,20.38,12.35,21.06,65.39,11.11,14.69,12.04,61.25,13.0,99.39,5.39,97.25,14.27,101.85,13.75,88.95,25.41,15.59,41.4,57.1,107.34,20.59,99.25,91.39,23.08,13.06,12.44,7.03,17.37,,52.77,36.38,20.33,,9.89,,21.43,86.38,20.37,10.32,9.47,13.67,4.93,28.43,12.21,45.43,27.63,35.05,40.65,102.76,90.45,28.97,6.11,17.88
10
+ openai_whisper-large-v3-v20240930_626MB,False,52.29,39.68,29.99,49.08,66.59,107.43,15.31,15.95,71.18,17.19,32.01,88.37,79.06,135.02,51.08,80.09,20.74,71.26,47.37,105.47,25.78,22.21,34.77,74.12,15.26,20.99,15.98,139.45,16.29,106.18,16.59,95.23,51.42,101.85,16.46,107.76,29.67,27.49,64.5,103.61,115.9,47.55,100.79,103.61,38.22,19.62,17.52,11.63,24.46,98.93,85.04,39.69,47.4,133.75,14.69,104.02,38.49,101.45,22.74,16.62,12.28,17.04,6.02,34.3,13.39,62.2,38.5,60.13,45.51,103.6,98.12,48.42,35.09,31.2
11
+ openai_whisper-large-v3-v20240930_626MB,True,47.64,30.62,25.67,26.93,62.36,97.82,13.11,17.36,67.47,12.72,29.16,84.89,77.31,111.23,39.77,63.57,18.94,50.51,45.76,97.71,22.33,18.71,31.72,72.64,13.16,19.39,14.49,84.78,14.37,102.51,12.24,93.42,66.1,100.0,14.85,95.84,27.18,21.48,56.17,134.5,123.72,36.74,98.12,95.73,32.09,15.48,17.05,9.05,22.25,81.89,63.49,40.0,27.3,128.28,13.21,102.9,34.52,96.28,22.01,15.17,12.66,15.68,5.97,33.98,13.03,56.96,35.97,57.0,43.62,121.47,99.17,42.74,23.09,24.11
12
+ openai_whisper-large-v3-v20240930_547MB,False,61.3,56.47,43.16,61.91,88.9,109.85,24.17,22.93,88.74,26.09,45.97,96.7,107.38,134.76,57.25,85.1,27.18,70.85,71.7,109.68,30.21,32.19,50.95,80.91,20.97,30.91,27.66,137.02,20.13,112.37,27.76,99.08,65.2,116.67,21.24,103.29,38.97,40.09,73.1,103.36,116.51,67.78,109.58,103.61,53.89,27.16,30.42,17.39,39.13,102.58,86.68,51.97,58.32,132.81,19.31,103.2,59.92,103.96,26.14,23.24,18.05,23.09,8.1,47.03,16.56,84.54,55.51,75.58,63.11,111.89,103.54,61.24,58.17,42.66
13
+ openai_whisper-large-v3-v20240930_547MB,True,54.61,40.12,35.54,35.4,78.38,102.05,19.6,25.97,81.39,19.25,38.72,89.86,109.32,146.41,46.71,69.67,25.3,60.25,66.07,101.55,25.79,26.23,45.55,75.4,18.77,27.16,23.73,106.23,18.63,108.87,18.26,97.33,74.97,101.85,18.91,95.65,34.74,30.15,63.01,,120.76,47.96,104.64,100.75,41.63,20.54,27.11,14.18,33.31,96.72,74.91,48.5,38.49,129.01,17.62,101.41,47.22,99.32,26.25,20.31,17.25,22.0,7.84,45.12,16.12,77.07,49.49,71.63,57.09,114.95,101.79,53.56,39.58,33.54
14
+ openai_whisper-large-v2,False,94.09,119.27,112.44,106.95,110.77,122.16,75.3,61.28,112.19,43.62,91.08,112.01,137.54,118.3,90.82,118.11,25.33,100.79,152.88,115.67,79.34,113.99,69.07,91.12,50.45,40.5,112.0,113.84,99.36,123.07,96.59,110.1,52.67,100.0,47.38,106.7,125.66,95.49,,101.13,102.34,118.31,124.76,105.15,143.76,63.7,44.82,48.04,119.44,100.18,102.64,101.1,100.38,154.52,65.08,100.59,85.71,105.79,97.02,48.57,92.39,31.95,46.74,99.3,13.74,116.06,137.91,74.42,107.11,111.27,131.83,100.18,119.35,113.86
15
+ openai_whisper-large-v2,True,47.14,25.76,25.84,25.24,67.14,100.99,12.51,17.69,65.57,12.16,24.01,83.34,62.4,176.79,47.12,49.05,16.72,48.12,58.01,136.6,22.6,15.04,28.69,72.69,14.34,16.2,17.14,165.74,15.11,115.56,7.86,95.93,53.06,105.56,15.23,99.75,36.59,20.95,43.09,105.46,114.5,25.32,107.07,115.23,26.39,16.27,16.72,8.93,21.52,103.9,62.0,47.24,25.92,150.19,11.7,107.19,29.37,106.33,24.84,13.13,12.2,16.21,6.93,35.96,12.7,53.38,38.94,32.85,49.09,103.76,105.51,28.08,8.76,19.55
16
+ openai_whisper-base,False,104.18,125.5,143.16,112.16,113.79,122.43,99.57,99.07,123.02,75.03,98.01,114.12,138.03,114.66,99.56,122.48,81.21,101.78,137.07,115.04,91.47,131.68,117.3,96.63,78.16,69.85,128.78,132.18,103.59,125.21,114.33,106.87,72.69,125.93,59.81,114.59,74.81,113.57,119.31,103.18,105.38,123.9,123.21,109.11,160.77,70.55,110.22,80.49,122.28,100.7,104.89,108.98,101.32,143.37,61.29,100.7,134.52,111.44,136.38,102.17,126.88,58.74,58.79,115.46,25.71,122.69,149.93,110.1,126.44,116.79,132.15,101.37,112.61,122.57
17
+ openai_whisper-base,True,79.92,72.07,76.57,59.1,106.54,171.77,43.44,40.15,100.62,45.53,61.22,102.6,208.4,165.98,83.98,92.45,61.96,103.31,99.05,201.0,42.73,55.22,81.5,82.95,46.45,48.59,67.24,117.99,44.21,151.1,54.19,105.42,70.49,111.11,48.98,98.51,53.88,58.31,76.9,100.38,119.75,74.21,134.5,116.11,72.17,47.63,71.07,37.01,73.54,100.79,101.9,87.4,102.24,117.93,38.09,109.8,84.13,106.76,48.87,56.32,43.04,45.09,24.55,91.31,25.11,104.21,91.07,87.41,98.64,106.13,108.77,60.25,32.91,51.87
dashboard_data/performance_data.json ADDED
The diff for this file is too large to render. See raw diff
 
dashboard_data/quality_data.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"model": "openai/whisper-large-v3/947MB", "timestamp": "2024-10-18_16:59:10_GMT-0700", "average_wer": 9.74, "dataset_wer": {"librispeech": 2.41, "earnings22-12hours": 17.08}, "qoi": 0.94}
2
+ {"model": "openai/whisper-large-v2/turbo/955MB", "timestamp": "2024-10-18_16:52:35_GMT-0700", "average_wer": 7.27, "dataset_wer": {"librispeech": 2.4, "earnings22-12hours": 12.14}, "qoi": 0.94}
3
+ {"model": "openai/whisper-tiny.en", "timestamp": "2024-10-19_15:40:06_GMT-0700", "average_wer": 12.23, "dataset_wer": {"librispeech": 5.61, "earnings22-12hours": 18.86}, "qoi": 0.63}
4
+ {"model": "distil-whisper/distil-large-v3/594MB", "timestamp": "2024-10-20_13:02:33_GMT-0700", "average_wer": 8.96, "dataset_wer": {"librispeech": 2.87, "earnings22-12hours": 15.06}, "qoi": 0.86}
5
+ {"model": "openai/whisper-large-v2/949MB", "timestamp": "2024-10-18_19:51:30_GMT-0400", "average_wer": 7.88, "dataset_wer": {"librispeech": 2.38, "earnings22-12hours": 13.39}, "qoi": 0.94}
6
+ {"model": "openai/whisper-large-v3/turbo/954MB", "timestamp": "2024-10-20_13:49:26_GMT-0700", "average_wer": 22.75, "dataset_wer": {"librispeech": 2.51, "earnings22-12hours": 43.0}, "qoi": 0.93}
7
+ {"model": "distil-whisper/distil-large-v3", "timestamp": "2024-10-20_20:32:22_GMT-0700", "average_wer": 7.2, "dataset_wer": {"librispeech": 2.38, "earnings22-12hours": 12.02}, "qoi": 0.9}
8
+ {"model": "openai/whisper-large-v3-v20240930", "timestamp": "2024-10-18_18:35:46_GMT-0700", "average_wer": 6.74, "dataset_wer": {"librispeech": 1.93, "earnings22-12hours": 11.55}, "qoi": 0.94}
9
+ {"model": "openai/whisper-tiny", "timestamp": "2024-10-20_20:19:04_GMT-0700", "average_wer": 14.21, "dataset_wer": {"librispeech": 7.46, "earnings22-12hours": 20.97}, "qoi": 0.52}
10
+ {"model": "openai/whisper-large-v3-v20240930/turbo/632MB", "timestamp": "2024-10-18_20:10:30_GMT-0700", "average_wer": 6.86, "dataset_wer": {"librispeech": 1.95, "earnings22-12hours": 11.77}, "qoi": 0.93}
11
+ {"model": "openai/whisper-large-v2/turbo", "timestamp": "2024-10-18_14:58:38_GMT-0700", "average_wer": 7.25, "dataset_wer": {"librispeech": 2.4, "earnings22-12hours": 12.1}, "qoi": 0.96}
12
+ {"model": "openai/whisper-small", "timestamp": "2024-10-18_12:40:03_GMT-0700", "average_wer": 8.11, "dataset_wer": {"librispeech": 3.21, "earnings22-12hours": 13.0}, "qoi": 0.83}
13
+ {"model": "openai/whisper-large-v3-v20240930/turbo", "timestamp": "2024-10-18_19:37:26_GMT-0700", "average_wer": 6.72, "dataset_wer": {"librispeech": 1.92, "earnings22-12hours": 11.52}, "qoi": 0.94}
14
+ {"model": "openai/whisper-large-v3", "timestamp": "2024-10-18_18:01:14_GMT-0400", "average_wer": 6.85, "dataset_wer": {"librispeech": 2.02, "earnings22-12hours": 11.69}, "qoi": 0.95}
15
+ {"model": "openai/whisper-large-v3-v20240930/626MB", "timestamp": "2024-10-18_19:21:06_GMT-0700", "average_wer": 7.15, "dataset_wer": {"librispeech": 1.96, "earnings22-12hours": 12.35}, "qoi": 0.93}
16
+ {"model": "openai/whisper-base.en", "timestamp": "2024-10-20_12:31:44_GMT-0700", "average_wer": 9.59, "dataset_wer": {"librispeech": 3.98, "earnings22-12hours": 15.2}, "qoi": 0.75}
17
+ {"model": "openai/whisper-large-v3-v20240930/547MB", "timestamp": "2024-10-18_21:59:11_GMT-0400", "average_wer": 16.82, "dataset_wer": {"librispeech": 2.16, "earnings22-12hours": 31.49}, "qoi": 0.92}
18
+ {"model": "distil-whisper/distil-large-v3/turbo/600MB", "timestamp": "2024-10-18_17:50:17_GMT-0700", "average_wer": 8.33, "dataset_wer": {"librispeech": 2.8, "earnings22-12hours": 13.87}, "qoi": 0.86}
19
+ {"model": "openai/whisper-large-v2", "timestamp": "2024-10-18_17:07:15_GMT-0400", "average_wer": 7.32, "dataset_wer": {"librispeech": 2.36, "earnings22-12hours": 12.28}, "qoi": 0.97}
20
+ {"model": "openai/whisper-small.en", "timestamp": "2024-10-18_15:39:48_GMT-0400", "average_wer": 7.85, "dataset_wer": {"librispeech": 2.88, "earnings22-12hours": 12.82}, "qoi": 0.86}
21
+ {"model": "distil-whisper/distil-large-v3/turbo", "timestamp": "2024-10-20_12:45:20_GMT-0700", "average_wer": 7.2, "dataset_wer": {"librispeech": 2.35, "earnings22-12hours": 12.05}, "qoi": 0.9}
22
+ {"model": "openai/whisper-base", "timestamp": "2024-10-18_20:25:50_GMT-0700", "average_wer": 10.67, "dataset_wer": {"librispeech": 4.94, "earnings22-12hours": 16.4}, "qoi": 0.67}
23
+ {"model": "openai/whisper-large-v3/turbo", "timestamp": "2024-10-20_16:58:25_GMT-0400", "average_wer": 6.86, "dataset_wer": {"librispeech": 1.97, "earnings22-12hours": 11.74}, "qoi": 0.95}
dashboard_data/support_data.csv ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ,Model,Apple M2 Pro,Apple M2 Ultra,Apple M3,Apple M3 Max,iPad Air 11-inch (M2),iPad mini (A17 Pro),iPad Pro 11-inch (M4),iPhone 11,iPhone 13 Pro,iPhone 13,iPhone 14,iPhone 16 Pro
2
+ distil-whisper_distil-large-v3,distil-whisper_distil-large-v3,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
3
+ distil-whisper_distil-large-v3_594MB,distil-whisper_distil-large-v3_594MB,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
4
+ distil-whisper_distil-large-v3_turbo,distil-whisper_distil-large-v3_turbo,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad14%2C8_summary_2024-10-25T032747.json>iPadOS 17.6.1</a>,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad16%2C1_summary_2024-10-25T054749.json>iPadOS 18.0.1</a>,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad16%2C3_summary_2024-10-25T032747.json>iPadOS 18.1</a>,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
5
+ distil-whisper_distil-large-v3_turbo_600MB,distil-whisper_distil-large-v3_turbo_600MB,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/Mac14%2C12_summary_2024-10-25T031359.json>macOS 15.0.1</a>,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
6
+ openai_whisper-base,openai_whisper-base,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/Mac14%2C12_summary_2024-10-25T031359.json>macOS 15.0.1</a>,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,✅ iOS 17.6.1,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
7
+ openai_whisper-base.en,openai_whisper-base.en,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,✅ iOS 17.6.1,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
8
+ openai_whisper-large-v2,openai_whisper-large-v2,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/Mac14%2C12_summary_2024-10-25T031359.json>macOS 15.0.1</a>,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
9
+ openai_whisper-large-v2_949MB,openai_whisper-large-v2_949MB,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/Mac14%2C12_summary_2024-10-25T031359.json>macOS 15.0.1</a>,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPhone14%2C5_summary_2024-10-25T032747.json>iOS 17.3</a>,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
10
+ openai_whisper-large-v2_turbo,openai_whisper-large-v2_turbo,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad14%2C8_summary_2024-10-25T032747.json>iPadOS 17.6.1</a>,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad16%2C1_summary_2024-10-25T054749.json>iPadOS 18.0.1</a>,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
11
+ openai_whisper-large-v2_turbo_955MB,openai_whisper-large-v2_turbo_955MB,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
12
+ openai_whisper-large-v3,openai_whisper-large-v3,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
13
+ openai_whisper-large-v3-v20240930,openai_whisper-large-v3-v20240930,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad14%2C8_summary_2024-10-25T032747.json>iPadOS 17.6.1</a>,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad16%2C1_summary_2024-10-25T054749.json>iPadOS 18.0.1</a>,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad16%2C3_summary_2024-10-25T032747.json>iPadOS 18.1</a>,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
14
+ openai_whisper-large-v3-v20240930_626MB,openai_whisper-large-v3-v20240930_626MB,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
15
+ openai_whisper-large-v3-v20240930_turbo,openai_whisper-large-v3-v20240930_turbo,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,Not Supported,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad16%2C1_summary_2024-10-25T054749.json>iPadOS 18.0.1</a>,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPad16%2C3_summary_2024-10-25T032747.json>iPadOS 18.1</a>,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
16
+ openai_whisper-large-v3-v20240930_turbo_632MB,openai_whisper-large-v3-v20240930_turbo_632MB,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPhone14%2C5_summary_2024-10-25T032747.json>iOS 17.3</a>,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPhone14%2C7_summary_2024-10-25T032747.json>iOS 17.3</a>,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
17
+ openai_whisper-large-v3_947MB,openai_whisper-large-v3_947MB,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/iPhone14%2C5_summary_2024-10-25T032747.json>iOS 17.3</a>,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
18
+ openai_whisper-large-v3_turbo,openai_whisper-large-v3_turbo,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported,Not Supported
19
+ openai_whisper-large-v3_turbo_954MB,openai_whisper-large-v3_turbo_954MB,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
20
+ openai_whisper-small,openai_whisper-small,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
21
+ openai_whisper-small.en,openai_whisper-small.en,⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href=https://huggingface.co/datasets/argmaxinc/whisperkit-evals-dataset/blob/main/benchmark_data/2024-10-25T012729_6962d0d/Mac14%2C12_summary_2024-10-25T031359.json>macOS 15.0.1</a>,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
22
+ openai_whisper-tiny,openai_whisper-tiny,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,Not Supported,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
23
+ openai_whisper-tiny.en,openai_whisper-tiny.en,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.0.1,✅ macOS 15.1,✅ iPadOS 17.6.1,✅ iPadOS 18.0.1,✅ iPadOS 18.1,✅ iOS 17.6.1,✅ iOS 18.0,✅ iOS 17.3,✅ iOS 17.3,✅ iOS 18.0.1<p>✅ iOS 18.0</p>
main.py ADDED
@@ -0,0 +1,1302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 json
9
+ import os
10
+ import re
11
+ from math import ceil, floor
12
+
13
+ import gradio as gr
14
+ import pandas as pd
15
+ from argmax_gradio_components import RangeSlider
16
+ from dotenv import load_dotenv
17
+ from huggingface_hub import login
18
+
19
+ # Import custom constants and utility functions
20
+ from constants import (
21
+ BANNER_TEXT,
22
+ CITATION_BUTTON_LABEL,
23
+ CITATION_BUTTON_TEXT,
24
+ COL_NAMES,
25
+ HEADER,
26
+ LANGUAGE_MAP,
27
+ METHODOLOGY_TEXT,
28
+ PERFORMANCE_TEXT,
29
+ QUALITY_TEXT,
30
+ )
31
+ from utils import (
32
+ add_datasets_to_performance_columns,
33
+ add_datasets_to_quality_columns,
34
+ calculate_parity,
35
+ create_confusion_matrix_plot,
36
+ create_initial_performance_column_dict,
37
+ create_initial_quality_column_dict,
38
+ css,
39
+ fields,
40
+ get_os_name_and_version,
41
+ make_dataset_wer_clickable_link,
42
+ make_model_name_clickable_link,
43
+ make_multilingual_model_clickable_link,
44
+ plot_metric,
45
+ read_json_line_by_line,
46
+ )
47
+
48
+ # Load environment variables
49
+ load_dotenv()
50
+
51
+ # Get the Hugging Face token from the environment variable
52
+ HF_TOKEN = os.getenv("HF_TOKEN")
53
+
54
+ # Use the token for login
55
+ login(token=HF_TOKEN, add_to_git_credential=True)
56
+
57
+ # Define repository and directory information
58
+ repo_id = "argmaxinc/whisperkit-evals-dataset"
59
+ directory = "xcresults/benchmark_results"
60
+ local_dir = ""
61
+
62
+ # Load benchmark data from JSON files
63
+ PERFORMANCE_DATA = read_json_line_by_line("dashboard_data/performance_data.json")
64
+ QUALITY_DATA = read_json_line_by_line("dashboard_data/quality_data.json")
65
+
66
+ # Convert JSON data to pandas DataFrames
67
+ quality_df = pd.json_normalize(QUALITY_DATA)
68
+ benchmark_df = pd.json_normalize(PERFORMANCE_DATA)
69
+
70
+ # Process timestamp data
71
+ benchmark_df["timestamp"] = pd.to_datetime(benchmark_df["timestamp"]).dt.tz_localize(
72
+ None
73
+ )
74
+ benchmark_df["timestamp"] = pd.to_datetime(benchmark_df["timestamp"]).dt.tz_localize(
75
+ None
76
+ )
77
+
78
+ # First create a temporary column for model length
79
+ sorted_quality_df = (
80
+ quality_df.assign(model_len=quality_df["model"].str.len())
81
+ .sort_values(
82
+ by=["model_len", "model", "timestamp"],
83
+ ascending=[True, True, False],
84
+ )
85
+ .drop(columns=["model_len"])
86
+ .drop_duplicates(subset=["model"], keep="first")
87
+ .reset_index(drop=True)
88
+ )
89
+
90
+ sorted_performance_df = (
91
+ benchmark_df.assign(model_len=benchmark_df["model"].str.len())
92
+ .sort_values(
93
+ by=["model_len", "model", "device", "os", "timestamp"],
94
+ ascending=[True, True, True, True, False],
95
+ )
96
+ .drop(columns=["model_len"])
97
+ .drop_duplicates(subset=["model", "device", "os"], keep="first")
98
+ .reset_index(drop=True)
99
+ )
100
+
101
+ # Identify dataset-specific columns
102
+ dataset_wer_columns = [
103
+ col for col in sorted_quality_df.columns if col.startswith("dataset_wer.")
104
+ ]
105
+ dataset_speed_columns = [
106
+ col for col in sorted_performance_df.columns if col.startswith("dataset_speed.")
107
+ ]
108
+ dataset_toks_columns = [
109
+ col
110
+ for col in sorted_performance_df.columns
111
+ if col.startswith("dataset_tokens_per_second.")
112
+ ]
113
+
114
+ # Extract dataset names
115
+ QUALITY_DATASETS = [col.split(".")[-1] for col in dataset_wer_columns]
116
+ PERFORMANCE_DATASETS = [col.split(".")[-1] for col in dataset_speed_columns]
117
+
118
+ # Prepare DataFrames for display
119
+ model_df = sorted_quality_df[
120
+ ["model", "average_wer", "qoi", "timestamp"] + dataset_wer_columns
121
+ ]
122
+ performance_df = sorted_performance_df[
123
+ [
124
+ "model",
125
+ "device",
126
+ "os",
127
+ "average_wer",
128
+ "qoi",
129
+ "speed",
130
+ "tokens_per_second",
131
+ "timestamp",
132
+ ]
133
+ + dataset_speed_columns
134
+ + dataset_toks_columns
135
+ ].copy()
136
+
137
+ # Rename columns for clarity
138
+ performance_df = performance_df.rename(
139
+ lambda x: COL_NAMES[x] if x in COL_NAMES else x, axis="columns"
140
+ )
141
+ model_df = model_df.rename(
142
+ lambda x: COL_NAMES[x] if x in COL_NAMES else x, axis="columns"
143
+ )
144
+
145
+ # Process dataset-specific columns
146
+ for col in dataset_wer_columns:
147
+ dataset_name = col.split(".")[-1]
148
+ model_df = model_df.rename(columns={col: dataset_name})
149
+ model_df[dataset_name] = model_df.apply(
150
+ lambda x: make_dataset_wer_clickable_link(x, dataset_name), axis=1
151
+ )
152
+
153
+ for col in dataset_speed_columns:
154
+ dataset_name = col.split(".")[-1]
155
+ performance_df = performance_df.rename(
156
+ columns={
157
+ col: f"{'Short-Form' if dataset_name == 'librispeech-10mins' else 'Long-Form'} Speed"
158
+ }
159
+ )
160
+
161
+ for col in dataset_toks_columns:
162
+ dataset_name = col.split(".")[-1]
163
+ performance_df = performance_df.rename(
164
+ columns={
165
+ col: f"{'Short-Form' if dataset_name == 'librispeech-10mins' else 'Long-Form'} Tok/s"
166
+ }
167
+ )
168
+
169
+ # Calculate parity with M2 Ultra
170
+ m2_ultra_wer = (
171
+ performance_df[performance_df["Device"] == "Apple M2 Ultra"]
172
+ .groupby("Model")["Average WER"]
173
+ .first()
174
+ )
175
+ performance_df["Parity %"] = performance_df.apply(
176
+ lambda row: calculate_parity(m2_ultra_wer, row), axis=1
177
+ )
178
+
179
+ # Process model names for display
180
+ model_df["model_raw"] = model_df["Model"].copy()
181
+ performance_df["model_raw"] = performance_df["Model"].copy()
182
+ model_df["Model"] = model_df["Model"].apply(lambda x: make_model_name_clickable_link(x))
183
+ performance_df["Model"] = performance_df["Model"].apply(
184
+ lambda x: make_model_name_clickable_link(x)
185
+ )
186
+
187
+ # Extract unique devices and OS versions
188
+ PERFORMANCE_DEVICES = performance_df["Device"].unique().tolist()
189
+ PERFORMANCE_OS = performance_df["OS"].apply(get_os_name_and_version).unique().tolist()
190
+ PERFORMANCE_OS.sort()
191
+
192
+ # Create initial column dictionaries and update with dataset information
193
+ initial_performance_column_dict = create_initial_performance_column_dict()
194
+ initial_quality_column_dict = create_initial_quality_column_dict()
195
+
196
+ performance_column_info = add_datasets_to_performance_columns(
197
+ initial_performance_column_dict, PERFORMANCE_DATASETS
198
+ )
199
+ quality_column_info = add_datasets_to_quality_columns(
200
+ initial_quality_column_dict, QUALITY_DATASETS
201
+ )
202
+
203
+ # Unpack the returned dictionaries
204
+ updated_performance_column_dict = performance_column_info["column_dict"]
205
+ updated_quality_column_dict = quality_column_info["column_dict"]
206
+
207
+ PerformanceAutoEvalColumn = performance_column_info["AutoEvalColumn"]
208
+ QualityAutoEvalColumn = quality_column_info["AutoEvalColumn"]
209
+
210
+ # Define column sets for different views
211
+ PERFORMANCE_COLS = performance_column_info["COLS"]
212
+ QUALITY_COLS = quality_column_info["COLS"]
213
+ PERFORMANCE_TYPES = performance_column_info["TYPES"]
214
+ QUALITY_TYPES = quality_column_info["TYPES"]
215
+ PERFORMANCE_ALWAYS_HERE_COLS = performance_column_info["ALWAYS_HERE_COLS"]
216
+ QUALITY_ALWAYS_HERE_COLS = quality_column_info["ALWAYS_HERE_COLS"]
217
+ PERFORMANCE_TOGGLE_COLS = performance_column_info["TOGGLE_COLS"]
218
+ QUALITY_TOGGLE_COLS = quality_column_info["TOGGLE_COLS"]
219
+ PERFORMANCE_SELECTED_COLS = performance_column_info["SELECTED_COLS"]
220
+ QUALITY_SELECTED_COLS = quality_column_info["SELECTED_COLS"]
221
+
222
+
223
+ def performance_filter(
224
+ df,
225
+ columns,
226
+ model_query,
227
+ exclude_models,
228
+ devices,
229
+ os,
230
+ short_speed_slider,
231
+ long_speed_slider,
232
+ short_toks_slider,
233
+ long_toks_slider,
234
+ ):
235
+ """
236
+ Filters the performance DataFrame based on specified criteria.
237
+ :param df: The DataFrame to be filtered.
238
+ :param columns: The columns to be included in the filtered DataFrame.
239
+ :param model_query: The query string to filter the 'Model' column.
240
+ :param exclude_models: Models to exclude from the results.
241
+ :param devices: The devices to filter the 'Device' column.
242
+ :param os: The list of operating systems to filter the 'OS' column.
243
+ :param short_speed_slider: The range of values to filter the 'Short-Form Speed' column.
244
+ :param long_speed_slider: The range of values to filter the 'Long-Form Speed' column.
245
+ :param short_toks_slider: The range of values to filter the 'Short-Form Tok/s' column.
246
+ :param long_toks_slider: The range of values to filter the 'Long-Form Tok/s' column.
247
+ :return: The filtered DataFrame.
248
+ """
249
+ # Select columns based on input and always-present columns
250
+ filtered_df = df[
251
+ PERFORMANCE_ALWAYS_HERE_COLS
252
+ + [c for c in PERFORMANCE_COLS if c in df.columns and c in columns]
253
+ ]
254
+
255
+ # Filter models based on query
256
+ if model_query:
257
+ filtered_df = filtered_df[
258
+ filtered_df["Model"].str.contains(
259
+ "|".join(q.strip() for q in model_query.split(";")), case=False
260
+ )
261
+ ]
262
+
263
+ # Exclude specified models
264
+ if exclude_models:
265
+ exclude_list = [m.strip() for m in exclude_models.split(";")]
266
+ filtered_df = filtered_df[
267
+ ~filtered_df["Model"].str.contains("|".join(exclude_list), case=False)
268
+ ]
269
+
270
+ # Filter by devices
271
+ filtered_df = (
272
+ filtered_df[
273
+ (
274
+ filtered_df["Device"].str.contains(
275
+ "|".join(re.escape(q.strip()) for q in devices), case=False
276
+ )
277
+ )
278
+ ]
279
+ if devices
280
+ else pd.DataFrame(columns=filtered_df.columns)
281
+ )
282
+
283
+ # Filter by operating systems
284
+ filtered_df = (
285
+ filtered_df[
286
+ (
287
+ filtered_df["OS"].str.contains(
288
+ "|".join(q.strip() for q in os), case=False
289
+ )
290
+ )
291
+ ]
292
+ if os
293
+ else pd.DataFrame(columns=filtered_df.columns)
294
+ )
295
+
296
+ # Apply short-form and long-form speed and tokens per second filters
297
+ min_short_speed, max_short_speed = short_speed_slider
298
+ min_long_speed, max_long_speed = long_speed_slider
299
+ min_short_toks, max_short_toks = short_toks_slider
300
+ min_long_toks, max_long_toks = long_toks_slider
301
+
302
+ filtered_df = filtered_df[
303
+ (filtered_df["Short-Form Speed"] >= min_short_speed)
304
+ & (filtered_df["Short-Form Speed"] <= max_short_speed)
305
+ & (filtered_df["Long-Form Speed"] >= min_long_speed)
306
+ & (filtered_df["Long-Form Speed"] <= max_long_speed)
307
+ & (filtered_df["Short-Form Tok/s"] >= min_short_toks)
308
+ & (filtered_df["Short-Form Tok/s"] <= max_short_toks)
309
+ & (filtered_df["Long-Form Tok/s"] >= min_long_toks)
310
+ & (filtered_df["Long-Form Tok/s"] <= max_long_toks)
311
+ ]
312
+
313
+ return filtered_df
314
+
315
+
316
+ def quality_filter(df, columns, model_query, wer_slider, qoi_slider, exclude_models):
317
+ """
318
+ Filters the quality DataFrame based on specified criteria.
319
+ :param df: The DataFrame to be filtered.
320
+ :param columns: The columns to be included in the filtered DataFrame.
321
+ :param model_query: The query string to filter the 'Model' column.
322
+ :param wer_slider: The range of values to filter the 'Average WER' column.
323
+ :param qoi_slider: The range of values to filter the 'QoI' column.
324
+ :param exclude_models: Models to exclude from the results.
325
+ :return: The filtered DataFrame.
326
+ """
327
+ # Select columns based on input and always-present columns
328
+ filtered_df = df[
329
+ QUALITY_ALWAYS_HERE_COLS
330
+ + [c for c in QUALITY_COLS if c in df.columns and c in columns]
331
+ ]
332
+
333
+ # Filter models based on query
334
+ if model_query:
335
+ filtered_df = filtered_df[
336
+ filtered_df["Model"].str.contains(
337
+ "|".join(q.strip() for q in model_query.split(";")), case=False
338
+ )
339
+ ]
340
+
341
+ # Exclude specified models
342
+ if exclude_models:
343
+ exclude_list = [m.strip() for m in exclude_models.split(";")]
344
+ filtered_df = filtered_df[
345
+ ~filtered_df["Model"].str.contains("|".join(exclude_list), case=False)
346
+ ]
347
+
348
+ # Apply WER and QoI filters
349
+ min_wer_slider, max_wer_slider = wer_slider
350
+ min_qoi_slider, max_qoi_slider = qoi_slider
351
+ if "Average WER" in filtered_df.columns:
352
+ filtered_df = filtered_df[
353
+ (filtered_df["Average WER"] >= min_wer_slider)
354
+ & (filtered_df["Average WER"] <= max_wer_slider)
355
+ ]
356
+ if "QoI" in filtered_df.columns:
357
+ filtered_df = filtered_df[
358
+ (filtered_df["QoI"] >= min_qoi_slider)
359
+ & (filtered_df["QoI"] <= max_qoi_slider)
360
+ ]
361
+
362
+ return filtered_df
363
+
364
+
365
+ diff_tab = gr.TabItem("Difference Checker", elem_id="diff_checker", id=2)
366
+ text_diff_elems = []
367
+
368
+ tabs = gr.Tabs(elem_id="tab-elems")
369
+
370
+ multilingual_df = pd.read_csv("dashboard_data/multilingual_results.csv")
371
+ multilingual_models_df = multilingual_df[["Model"]].drop_duplicates()
372
+ multilingual_models_buttons = []
373
+ for model in multilingual_models_df["Model"]:
374
+ elem_id = (
375
+ f"{model}".replace(" ", "_").replace('"', "").replace("'", "").replace(",", "")
376
+ )
377
+ multilingual_models_buttons.append(
378
+ gr.Button(value=model, elem_id=elem_id, visible=False)
379
+ )
380
+ multilingual_models_df["Model"] = multilingual_models_df["Model"].apply(
381
+ lambda x: make_multilingual_model_clickable_link(x)
382
+ )
383
+
384
+ with open("dashboard_data/multilingual_confusion_matrices.json", "r") as file:
385
+ confusion_matrix_map = dict(json.load(file))
386
+
387
+
388
+ def update_multilingual_results(selected_model):
389
+ """
390
+ Updates the multilingual results display based on the selected model.
391
+
392
+ This function processes the multilingual data for the chosen model,
393
+ calculates average WER for different scenarios (language hinted vs. predicted),
394
+ and prepares language-specific WER data for display.
395
+
396
+ :param selected_model: The name of the selected model
397
+ :return: A list containing updated components for the Gradio interface
398
+ """
399
+ if selected_model is None:
400
+ return "# Select a model from the dropdown to view results."
401
+
402
+ # Filter data for the selected model
403
+ model_data = multilingual_df[multilingual_df["Model"] == selected_model]
404
+
405
+ if model_data.empty:
406
+ return f"# No data available for model: {selected_model}"
407
+
408
+ # Separate data for forced and not forced scenarios
409
+ forced_data = model_data[model_data["Forced Tokens"] == True]
410
+ not_forced_data = model_data[model_data["Forced Tokens"] == False]
411
+
412
+ result_text = f"# Model: {selected_model}\n\n"
413
+
414
+ # Prepare average WER data
415
+ average_wer_data = []
416
+ if not forced_data.empty:
417
+ average_wer_data.append(
418
+ {
419
+ "Scenario": "Language Hinted",
420
+ "Average WER": forced_data.iloc[0]["Average WER"],
421
+ }
422
+ )
423
+ if not not_forced_data.empty:
424
+ average_wer_data.append(
425
+ {
426
+ "Scenario": "Language Predicted",
427
+ "Average WER": not_forced_data.iloc[0]["Average WER"],
428
+ }
429
+ )
430
+ average_wer_df = pd.DataFrame(average_wer_data)
431
+ average_wer_df["Average WER"] = average_wer_df["Average WER"].apply(
432
+ lambda x: round(x, 2)
433
+ )
434
+
435
+ # Prepare language-specific WER data
436
+ lang_columns = [col for col in model_data.columns if col.startswith("WER_")]
437
+ lang_wer_data = []
438
+ for column in lang_columns:
439
+ lang = column.split("_")[1]
440
+ forced_wer = forced_data[column].iloc[0] if not forced_data.empty else None
441
+ not_forced_wer = (
442
+ not_forced_data[column].iloc[0] if not not_forced_data.empty else None
443
+ )
444
+ if forced_wer is not None or not_forced_wer is not None:
445
+ lang_wer_data.append(
446
+ {
447
+ "Language": LANGUAGE_MAP[lang],
448
+ "Language Hinted WER": round(forced_wer, 2)
449
+ if forced_wer is not None
450
+ else "N/A",
451
+ "Language Predicted WER": round(not_forced_wer, 2)
452
+ if not_forced_wer is not None
453
+ else "N/A",
454
+ }
455
+ )
456
+ lang_wer_df = pd.DataFrame(lang_wer_data)
457
+ lang_wer_df = lang_wer_df.fillna("No Data")
458
+
459
+ # Create confusion matrix plot for unforced scenario
460
+ unforced_plot = None
461
+ if selected_model in confusion_matrix_map:
462
+ if "not_forced" in confusion_matrix_map[selected_model]:
463
+ unforced_plot = create_confusion_matrix_plot(
464
+ confusion_matrix_map[selected_model]["not_forced"]["matrix"],
465
+ confusion_matrix_map[selected_model]["not_forced"]["labels"],
466
+ False,
467
+ )
468
+
469
+ # Return updated components for Gradio interface
470
+ return [
471
+ gr.update(value=result_text),
472
+ gr.update(visible=True, value=average_wer_df),
473
+ gr.update(visible=True, value=lang_wer_df),
474
+ gr.update(visible=unforced_plot is not None, value=unforced_plot),
475
+ ]
476
+
477
+
478
+ font = [
479
+ "Zwizz Regular", # Local font
480
+ "IBM Plex Mono", # Monospace font
481
+ "ui-sans-serif",
482
+ "system-ui",
483
+ "sans-serif",
484
+ ]
485
+
486
+ # Define the Gradio interface
487
+ with gr.Blocks(css=css, theme=gr.themes.Base(font=font)) as demo:
488
+ # Add header and banner to the interface
489
+ gr.HTML(HEADER)
490
+ gr.HTML(BANNER_TEXT, elem_classes="markdown-text")
491
+
492
+ # Create tabs for different sections of the dashboard
493
+ with tabs.render():
494
+ # Performance Tab
495
+ with gr.TabItem("Performance", elem_id="benchmark", id=0):
496
+ with gr.Row():
497
+ with gr.Column(scale=1):
498
+ with gr.Row():
499
+ with gr.Column(scale=6, elem_classes="filter_models_column"):
500
+ filter_performance_models = gr.Textbox(
501
+ placeholder="🔍 Filter Model (separate multiple queries with ';')",
502
+ label="Filter Models",
503
+ )
504
+ with gr.Column(scale=4, elem_classes="exclude_models_column"):
505
+ exclude_performance_models = gr.Textbox(
506
+ placeholder="🔍 Exclude (separate multiple queries with ';')",
507
+ label="Exclude Models",
508
+ )
509
+ with gr.Row():
510
+ with gr.Accordion("See All Columns", open=False):
511
+ with gr.Row():
512
+ with gr.Column(scale=9, elem_id="performance_columns"):
513
+ performance_shown_columns = gr.CheckboxGroup(
514
+ choices=PERFORMANCE_TOGGLE_COLS,
515
+ value=PERFORMANCE_SELECTED_COLS,
516
+ label="Toggle Columns",
517
+ elem_id="column-select",
518
+ interactive=True,
519
+ )
520
+ with gr.Column(
521
+ scale=1,
522
+ min_width=200,
523
+ elem_id="performance_select_columns",
524
+ ):
525
+ with gr.Row():
526
+ select_all_button = gr.Button(
527
+ "Select All",
528
+ elem_id="select-all-button",
529
+ interactive=True,
530
+ )
531
+ deselect_all_button = gr.Button(
532
+ "Deselect All",
533
+ elem_id="deselect-all-button",
534
+ interactive=True,
535
+ )
536
+
537
+ def select_all_columns():
538
+ return PERFORMANCE_TOGGLE_COLS
539
+
540
+ def deselect_all_columns():
541
+ return []
542
+
543
+ select_all_button.click(
544
+ select_all_columns,
545
+ inputs=[],
546
+ outputs=performance_shown_columns,
547
+ )
548
+ deselect_all_button.click(
549
+ deselect_all_columns,
550
+ inputs=[],
551
+ outputs=performance_shown_columns,
552
+ )
553
+
554
+ with gr.Row():
555
+ with gr.Accordion("Filter Devices", open=False):
556
+ with gr.Row():
557
+ with gr.Column(
558
+ scale=9, elem_id="filter_devices_column"
559
+ ):
560
+ performance_shown_devices = gr.CheckboxGroup(
561
+ choices=PERFORMANCE_DEVICES,
562
+ value=PERFORMANCE_DEVICES,
563
+ label="Filter Devices",
564
+ interactive=True,
565
+ )
566
+ with gr.Column(
567
+ scale=1,
568
+ min_width=200,
569
+ elem_id="filter_select_devices",
570
+ ):
571
+ with gr.Row():
572
+ select_all_devices_button = gr.Button(
573
+ "Select All",
574
+ elem_id="select-all-devices-button",
575
+ interactive=True,
576
+ )
577
+ deselect_all_devices_button = gr.Button(
578
+ "Deselect All",
579
+ elem_id="deselect-all-devices-button",
580
+ interactive=True,
581
+ )
582
+
583
+ def select_all_devices():
584
+ return PERFORMANCE_DEVICES
585
+
586
+ def deselect_all_devices():
587
+ return []
588
+
589
+ select_all_devices_button.click(
590
+ select_all_devices,
591
+ inputs=[],
592
+ outputs=performance_shown_devices,
593
+ )
594
+ deselect_all_devices_button.click(
595
+ deselect_all_devices,
596
+ inputs=[],
597
+ outputs=performance_shown_devices,
598
+ )
599
+ with gr.Row():
600
+ performance_shown_os = gr.CheckboxGroup(
601
+ choices=PERFORMANCE_OS,
602
+ value=PERFORMANCE_OS,
603
+ label="Filter OS",
604
+ interactive=True,
605
+ )
606
+ with gr.Column(scale=1):
607
+ with gr.Accordion("See Performance Filters"):
608
+ with gr.Row():
609
+ with gr.Row():
610
+ min_short_speed, max_short_speed = floor(
611
+ min(performance_df["Short-Form Speed"])
612
+ ), ceil(max(performance_df["Short-Form Speed"]))
613
+ short_speed_slider = RangeSlider(
614
+ value=[min_short_speed, max_short_speed],
615
+ minimum=min_short_speed,
616
+ maximum=max_short_speed,
617
+ step=0.001,
618
+ label="Short-Form Speed",
619
+ )
620
+ with gr.Row():
621
+ min_long_speed, max_long_speed = floor(
622
+ min(performance_df["Long-Form Speed"])
623
+ ), ceil(max(performance_df["Long-Form Speed"]))
624
+ long_speed_slider = RangeSlider(
625
+ value=[min_long_speed, max_long_speed],
626
+ minimum=min_long_speed,
627
+ maximum=max_long_speed,
628
+ step=0.001,
629
+ label="Long-Form Speed",
630
+ )
631
+ with gr.Row():
632
+ with gr.Row():
633
+ min_short_toks, max_short_toks = floor(
634
+ min(performance_df["Short-Form Tok/s"])
635
+ ), ceil(max(performance_df["Short-Form Tok/s"]))
636
+ short_toks_slider = RangeSlider(
637
+ value=[min_short_toks, max_short_toks],
638
+ minimum=min_short_toks,
639
+ maximum=max_short_toks,
640
+ step=0.001,
641
+ label="Short-Form Tok/s",
642
+ )
643
+ with gr.Row():
644
+ min_long_toks, max_long_toks = floor(
645
+ min(performance_df["Long-Form Tok/s"])
646
+ ), ceil(max(performance_df["Long-Form Tok/s"]))
647
+ long_toks_slider = RangeSlider(
648
+ value=[min_long_toks, max_long_toks],
649
+ minimum=min_long_toks,
650
+ maximum=max_long_toks,
651
+ step=0.001,
652
+ label="Long-Form Tok/s",
653
+ )
654
+ with gr.Row():
655
+ gr.Markdown(PERFORMANCE_TEXT, elem_classes="markdown-text")
656
+ with gr.Row():
657
+ leaderboard_df = gr.components.Dataframe(
658
+ value=performance_df[
659
+ PERFORMANCE_ALWAYS_HERE_COLS + performance_shown_columns.value
660
+ ],
661
+ headers=[
662
+ PERFORMANCE_ALWAYS_HERE_COLS + performance_shown_columns.value
663
+ ],
664
+ datatype=[
665
+ c.type
666
+ for c in fields(PerformanceAutoEvalColumn)
667
+ if c.name in PERFORMANCE_COLS
668
+ ],
669
+ elem_id="leaderboard-table",
670
+ elem_classes="large-table",
671
+ interactive=False,
672
+ )
673
+
674
+ # Copy of the leaderboard dataframe to apply filters to
675
+ hidden_leaderboard_df = gr.components.Dataframe(
676
+ value=performance_df,
677
+ headers=PERFORMANCE_COLS,
678
+ datatype=[
679
+ c.type
680
+ for c in fields(PerformanceAutoEvalColumn)
681
+ if c.name in PERFORMANCE_COLS
682
+ ],
683
+ visible=False,
684
+ )
685
+
686
+ # Inputs for the dataframe filter function
687
+ performance_filter_inputs = [
688
+ hidden_leaderboard_df,
689
+ performance_shown_columns,
690
+ filter_performance_models,
691
+ exclude_performance_models,
692
+ performance_shown_devices,
693
+ performance_shown_os,
694
+ short_speed_slider,
695
+ long_speed_slider,
696
+ short_toks_slider,
697
+ long_toks_slider,
698
+ ]
699
+
700
+ filter_output = leaderboard_df
701
+ filter_performance_models.change(
702
+ performance_filter, performance_filter_inputs, filter_output
703
+ )
704
+ exclude_performance_models.change(
705
+ performance_filter, performance_filter_inputs, filter_output
706
+ )
707
+ performance_shown_columns.change(
708
+ performance_filter, performance_filter_inputs, filter_output
709
+ )
710
+ performance_shown_devices.change(
711
+ performance_filter, performance_filter_inputs, filter_output
712
+ )
713
+ performance_shown_os.change(
714
+ performance_filter, performance_filter_inputs, filter_output
715
+ )
716
+ short_speed_slider.change(
717
+ performance_filter, performance_filter_inputs, filter_output
718
+ )
719
+ long_speed_slider.change(
720
+ performance_filter, performance_filter_inputs, filter_output
721
+ )
722
+ short_toks_slider.change(
723
+ performance_filter, performance_filter_inputs, filter_output
724
+ )
725
+ long_toks_slider.change(
726
+ performance_filter, performance_filter_inputs, filter_output
727
+ )
728
+
729
+ # English Quality Tab
730
+ with gr.TabItem("English Quality", elem_id="timeline", id=1):
731
+ with gr.Row():
732
+ with gr.Column(scale=1):
733
+ with gr.Row():
734
+ with gr.Column(scale=6, elem_classes="filter_models_column"):
735
+ filter_quality_models = gr.Textbox(
736
+ placeholder="🔍 Filter Model (separate multiple queries with ';')",
737
+ label="Filter Models",
738
+ )
739
+ with gr.Column(scale=4, elem_classes="exclude_models_column"):
740
+ exclude_quality_models = gr.Textbox(
741
+ placeholder="🔍 Exclude Model (separate multiple models with ';')",
742
+ label="Exclude Models",
743
+ )
744
+ with gr.Row():
745
+ with gr.Accordion("See All Columns", open=False):
746
+ quality_shown_columns = gr.CheckboxGroup(
747
+ choices=QUALITY_TOGGLE_COLS,
748
+ value=QUALITY_SELECTED_COLS,
749
+ label="Toggle Columns",
750
+ elem_id="column-select",
751
+ interactive=True,
752
+ )
753
+ with gr.Column(scale=1):
754
+ with gr.Accordion("See Quality Filters"):
755
+ with gr.Row():
756
+ with gr.Row():
757
+ quality_min_avg_wer, quality_max_avg_wer = (
758
+ floor(min(model_df["Average WER"])),
759
+ ceil(max(model_df["Average WER"])) + 1,
760
+ )
761
+ wer_slider = RangeSlider(
762
+ value=[quality_min_avg_wer, quality_max_avg_wer],
763
+ minimum=quality_min_avg_wer,
764
+ maximum=quality_max_avg_wer,
765
+ label="Average WER",
766
+ )
767
+ with gr.Row():
768
+ quality_min_qoi, quality_max_qoi = floor(
769
+ min(model_df["QoI"])
770
+ ), ceil(max(model_df["QoI"] + 1))
771
+ qoi_slider = RangeSlider(
772
+ value=[quality_min_qoi, quality_max_qoi],
773
+ minimum=quality_min_qoi,
774
+ maximum=quality_max_qoi,
775
+ label="QoI",
776
+ )
777
+ with gr.Row():
778
+ gr.Markdown(QUALITY_TEXT)
779
+ with gr.Row():
780
+ quality_leaderboard_df = gr.components.Dataframe(
781
+ value=model_df[
782
+ QUALITY_ALWAYS_HERE_COLS + quality_shown_columns.value
783
+ ],
784
+ headers=[QUALITY_ALWAYS_HERE_COLS + quality_shown_columns.value],
785
+ datatype=[
786
+ c.type
787
+ for c in fields(QualityAutoEvalColumn)
788
+ if c.name in QUALITY_COLS
789
+ ],
790
+ elem_id="leaderboard-table",
791
+ elem_classes="large-table",
792
+ interactive=False,
793
+ )
794
+
795
+ # Copy of the leaderboard dataframe to apply filters to
796
+ hidden_quality_leaderboard_df = gr.components.Dataframe(
797
+ value=model_df,
798
+ headers=QUALITY_COLS,
799
+ datatype=[
800
+ c.type
801
+ for c in fields(QualityAutoEvalColumn)
802
+ if c.name in QUALITY_COLS
803
+ ],
804
+ visible=False,
805
+ )
806
+
807
+ # Inputs for the dataframe filter function
808
+ filter_inputs = [
809
+ hidden_quality_leaderboard_df,
810
+ quality_shown_columns,
811
+ filter_quality_models,
812
+ wer_slider,
813
+ qoi_slider,
814
+ exclude_quality_models,
815
+ ]
816
+ filter_output = quality_leaderboard_df
817
+ filter_quality_models.change(
818
+ quality_filter, filter_inputs, filter_output
819
+ )
820
+ exclude_quality_models.change(
821
+ quality_filter, filter_inputs, filter_output
822
+ )
823
+ quality_shown_columns.change(
824
+ quality_filter, filter_inputs, filter_output
825
+ )
826
+ wer_slider.change(quality_filter, filter_inputs, filter_output)
827
+ qoi_slider.change(quality_filter, filter_inputs, filter_output)
828
+
829
+ # Timeline Tab
830
+ with gr.TabItem("Timeline", elem_id="timeline", id=4):
831
+ # Create subtabs for different metrics
832
+ with gr.Tabs():
833
+ with gr.TabItem("QoI", id=0):
834
+ with gr.Row():
835
+ with gr.Column(scale=6):
836
+ filter_qoi = gr.Textbox(
837
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
838
+ label="Filter",
839
+ )
840
+ with gr.Column(scale=4):
841
+ exclude_qoi = gr.Textbox(
842
+ placeholder="🔍 Exclude Model-Device-OS (separate multiple with ';')",
843
+ label="Exclude",
844
+ )
845
+ with gr.Row():
846
+ with gr.Column():
847
+ qoi_plot = gr.Plot(container=True)
848
+ demo.load(
849
+ lambda x, y, z: plot_metric(
850
+ x,
851
+ "qoi",
852
+ "QoI",
853
+ "QoI Over Time for Model-Device-OS Combinations",
854
+ y,
855
+ z,
856
+ ),
857
+ [
858
+ gr.Dataframe(benchmark_df, visible=False),
859
+ filter_qoi,
860
+ exclude_qoi,
861
+ ],
862
+ qoi_plot,
863
+ )
864
+ filter_qoi.change(
865
+ lambda x, y, z: plot_metric(
866
+ x,
867
+ "qoi",
868
+ "QoI",
869
+ "QoI Over Time for Model-Device-OS Combinations",
870
+ y,
871
+ z,
872
+ ),
873
+ [
874
+ gr.Dataframe(benchmark_df, visible=False),
875
+ filter_qoi,
876
+ exclude_qoi,
877
+ ],
878
+ qoi_plot,
879
+ )
880
+ exclude_qoi.change(
881
+ lambda x, y, z: plot_metric(
882
+ x,
883
+ "qoi",
884
+ "QoI",
885
+ "QoI Over Time for Model-Device-OS Combinations",
886
+ y,
887
+ z,
888
+ ),
889
+ [
890
+ gr.Dataframe(benchmark_df, visible=False),
891
+ filter_qoi,
892
+ exclude_qoi,
893
+ ],
894
+ qoi_plot,
895
+ )
896
+
897
+ with gr.TabItem("Average WER", id=1):
898
+ with gr.Row():
899
+ with gr.Column(scale=6):
900
+ filter_average_wer = gr.Textbox(
901
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
902
+ label="Filter",
903
+ )
904
+ with gr.Column(scale=4):
905
+ exclude_average_wer = gr.Textbox(
906
+ placeholder="🔍 Exclude Model-Device-OS (separate multiple with ';')",
907
+ label="Exclude",
908
+ )
909
+ with gr.Row():
910
+ with gr.Column():
911
+ average_wer_plot = gr.Plot(container=True)
912
+ demo.load(
913
+ lambda x, y, z: plot_metric(
914
+ x,
915
+ "average_wer",
916
+ "Average WER",
917
+ "Average WER Over Time for Model-Device-OS Combinations",
918
+ y,
919
+ z,
920
+ ),
921
+ [
922
+ gr.Dataframe(benchmark_df, visible=False),
923
+ filter_average_wer,
924
+ exclude_average_wer,
925
+ ],
926
+ average_wer_plot,
927
+ )
928
+ filter_average_wer.change(
929
+ lambda x, y, z: plot_metric(
930
+ x,
931
+ "average_wer",
932
+ "Average WER",
933
+ "Average WER Over Time for Model-Device-OS Combinations",
934
+ y,
935
+ z,
936
+ ),
937
+ [
938
+ gr.Dataframe(benchmark_df, visible=False),
939
+ filter_average_wer,
940
+ exclude_average_wer,
941
+ ],
942
+ average_wer_plot,
943
+ )
944
+ exclude_average_wer.change(
945
+ lambda x, y, z: plot_metric(
946
+ x,
947
+ "average_wer",
948
+ "Average WER",
949
+ "Average WER Over Time for Model-Device-OS Combinations",
950
+ y,
951
+ z,
952
+ ),
953
+ [
954
+ gr.Dataframe(benchmark_df, visible=False),
955
+ filter_average_wer,
956
+ exclude_average_wer,
957
+ ],
958
+ average_wer_plot,
959
+ )
960
+
961
+ with gr.TabItem("Speed", id=2):
962
+ with gr.Row():
963
+ with gr.Column(scale=6):
964
+ filter_speed = gr.Textbox(
965
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
966
+ label="Filter",
967
+ )
968
+ with gr.Column(scale=4):
969
+ exclude_speed = gr.Textbox(
970
+ placeholder="🔍 Exclude Model-Device-OS (separate multiple with ';')",
971
+ label="Exclude",
972
+ )
973
+ with gr.Row():
974
+ with gr.Column():
975
+ speed_plot = gr.Plot(container=True)
976
+ demo.load(
977
+ lambda x, y, z: plot_metric(
978
+ x,
979
+ "speed",
980
+ "Speed",
981
+ "Speed Over Time for Model-Device-OS Combinations",
982
+ y,
983
+ z,
984
+ ),
985
+ [
986
+ gr.Dataframe(benchmark_df, visible=False),
987
+ filter_speed,
988
+ exclude_speed,
989
+ ],
990
+ speed_plot,
991
+ )
992
+ filter_speed.change(
993
+ lambda x, y, z: plot_metric(
994
+ x,
995
+ "speed",
996
+ "Speed",
997
+ "Speed Over Time for Model-Device-OS Combinations",
998
+ y,
999
+ z,
1000
+ ),
1001
+ [
1002
+ gr.Dataframe(benchmark_df, visible=False),
1003
+ filter_speed,
1004
+ exclude_speed,
1005
+ ],
1006
+ speed_plot,
1007
+ )
1008
+ exclude_speed.change(
1009
+ lambda x, y, z: plot_metric(
1010
+ x,
1011
+ "speed",
1012
+ "Speed",
1013
+ "Speed Over Time for Model-Device-OS Combinations",
1014
+ y,
1015
+ z,
1016
+ ),
1017
+ [
1018
+ gr.Dataframe(benchmark_df, visible=False),
1019
+ filter_speed,
1020
+ exclude_speed,
1021
+ ],
1022
+ speed_plot,
1023
+ )
1024
+
1025
+ with gr.TabItem("Tok/s", id=3):
1026
+ with gr.Row():
1027
+ with gr.Column(scale=6):
1028
+ filter_toks = gr.Textbox(
1029
+ placeholder="🔍 Filter Model-Device-OS (separate multiple queries with ';')",
1030
+ label="Filter",
1031
+ )
1032
+ with gr.Column(scale=4):
1033
+ exclude_toks = gr.Textbox(
1034
+ placeholder="🔍 Exclude Model-Device-OS (separate multiple with ';')",
1035
+ label="Exclude",
1036
+ )
1037
+ with gr.Row():
1038
+ with gr.Column():
1039
+ toks_plot = gr.Plot(container=True)
1040
+ demo.load(
1041
+ lambda x, y, z: plot_metric(
1042
+ x,
1043
+ "tokens_per_second",
1044
+ "Tok/s",
1045
+ "Tok/s Over Time for Model-Device-OS Combinations",
1046
+ y,
1047
+ z,
1048
+ ),
1049
+ [
1050
+ gr.Dataframe(benchmark_df, visible=False),
1051
+ filter_toks,
1052
+ exclude_toks,
1053
+ ],
1054
+ toks_plot,
1055
+ )
1056
+ filter_toks.change(
1057
+ lambda x, y, z: plot_metric(
1058
+ x,
1059
+ "tokens_per_second",
1060
+ "Tok/s",
1061
+ "Tok/s Over Time for Model-Device-OS Combinations",
1062
+ y,
1063
+ z,
1064
+ ),
1065
+ [
1066
+ gr.Dataframe(benchmark_df, visible=False),
1067
+ filter_toks,
1068
+ exclude_toks,
1069
+ ],
1070
+ toks_plot,
1071
+ )
1072
+ exclude_toks.change(
1073
+ lambda x, y, z: plot_metric(
1074
+ x,
1075
+ "tokens_per_second",
1076
+ "Tok/s",
1077
+ "Tok/s Over Time for Model-Device-OS Combinations",
1078
+ y,
1079
+ z,
1080
+ ),
1081
+ [
1082
+ gr.Dataframe(benchmark_df, visible=False),
1083
+ filter_toks,
1084
+ exclude_toks,
1085
+ ],
1086
+ toks_plot,
1087
+ )
1088
+
1089
+ # Multilingual Quality Tab
1090
+ with gr.TabItem("Multilingual Quality", elem_id="multilingual", id=5):
1091
+ if multilingual_df is not None:
1092
+ with gr.Row():
1093
+ with gr.Column(scale=1):
1094
+ # Display table of multilingual models
1095
+ model_table = gr.Dataframe(
1096
+ value=multilingual_models_df,
1097
+ headers=["Model"],
1098
+ datatype=["html"],
1099
+ elem_classes="left-side-table",
1100
+ )
1101
+ # Placeholders for confusion matrix plots
1102
+ with gr.Row():
1103
+ unforced_confusion_matrix = gr.Plot(visible=False)
1104
+ with gr.Row():
1105
+ forced_confusion_matrix = gr.Plot(visible=False)
1106
+
1107
+ with gr.Column(scale=1):
1108
+ # Display area for selected model results
1109
+ results_markdown = gr.Markdown(
1110
+ "# Select a model from the table on the left to view results.",
1111
+ elem_id="multilingual-results",
1112
+ )
1113
+ # Tables for displaying average WER and language-specific WER
1114
+ average_wer_table = gr.Dataframe(
1115
+ value=None, elem_id="average-wer-table", visible=False
1116
+ )
1117
+ language_wer_table = gr.Dataframe(
1118
+ value=None, elem_id="general-wer-table", visible=False
1119
+ )
1120
+
1121
+ # Set up click event to update results when a model is selected
1122
+ for button in multilingual_models_buttons:
1123
+ button.render()
1124
+ button.click(
1125
+ fn=lambda x: update_multilingual_results(x),
1126
+ inputs=[button],
1127
+ outputs=[
1128
+ results_markdown,
1129
+ average_wer_table,
1130
+ language_wer_table,
1131
+ unforced_confusion_matrix,
1132
+ ],
1133
+ )
1134
+ else:
1135
+ # Display message if no multilingual data is available
1136
+ gr.Markdown("No multilingual benchmark results available.")
1137
+
1138
+ # Device Support Tab
1139
+ with gr.TabItem("Device Support", elem_id="device_support", id=6):
1140
+ # Load device support data from CSV
1141
+ support_data = pd.read_csv("dashboard_data/support_data.csv")
1142
+ support_data.set_index(support_data.columns[0], inplace=True)
1143
+ support_data["Model"] = support_data["Model"].apply(
1144
+ lambda x: x.replace("_", "/")
1145
+ )
1146
+ support_data["Model"] = support_data["Model"].apply(
1147
+ lambda x: make_model_name_clickable_link(x)
1148
+ )
1149
+ support_data = (
1150
+ support_data.assign(model_len=support_data["Model"].str.len())
1151
+ .sort_values(
1152
+ by=["model_len"],
1153
+ ascending=[True],
1154
+ )
1155
+ .drop(columns=["model_len"])
1156
+ )
1157
+
1158
+ with gr.Row():
1159
+ with gr.Column(scale=1):
1160
+ with gr.Row():
1161
+ with gr.Column(scale=6, elem_id="filter_models_column"):
1162
+ filter_support_models = gr.Textbox(
1163
+ placeholder="🔍 Filter Model (separate multiple queries with ';')",
1164
+ label="Filter Models",
1165
+ )
1166
+ with gr.Column(scale=4, elem_classes="exclude_models_column"):
1167
+ exclude_support_models = gr.Textbox(
1168
+ placeholder="🔍 Exclude Model (separate multiple models with ';')",
1169
+ label="Exclude Models",
1170
+ )
1171
+ with gr.Row():
1172
+ with gr.Accordion("See All Columns", open=False):
1173
+ with gr.Row():
1174
+ with gr.Column(scale=9):
1175
+ support_shown_columns = gr.CheckboxGroup(
1176
+ choices=support_data.columns.tolist()[
1177
+ 1:
1178
+ ], # Exclude 'Model' column
1179
+ value=support_data.columns.tolist()[1:],
1180
+ label="Toggle Columns",
1181
+ elem_id="support-column-select",
1182
+ interactive=True,
1183
+ )
1184
+ with gr.Column(scale=1, min_width=200):
1185
+ with gr.Row():
1186
+ select_all_support_button = gr.Button(
1187
+ "Select All",
1188
+ elem_id="select-all-support-button",
1189
+ interactive=True,
1190
+ )
1191
+ deselect_all_support_button = gr.Button(
1192
+ "Deselect All",
1193
+ elem_id="deselect-all-support-button",
1194
+ interactive=True,
1195
+ )
1196
+ with gr.Column():
1197
+ gr.Markdown(
1198
+ """
1199
+ ### Legend
1200
+ - ✅ Supported: The model is supported and tested on this device.
1201
+ - ⚠️ Failed: Either The model tests failed on this device or the Speed Factor for the test is less than 1.
1202
+ - ? Not Tested: The model is supported on this device but no test information available.
1203
+ - Not Supported: The model is not supported on this device as per the [WhisperKit configuration](https://huggingface.co/argmaxinc/whisperkit-coreml/blob/main/config.json).
1204
+ """
1205
+ )
1206
+
1207
+ # Display device support data in a table
1208
+ device_support_table = gr.Dataframe(
1209
+ value=support_data,
1210
+ headers=support_data.columns.tolist(),
1211
+ datatype=["html" for _ in support_data.columns],
1212
+ elem_id="device-support-table",
1213
+ elem_classes="large-table",
1214
+ interactive=False,
1215
+ )
1216
+
1217
+ # Hidden dataframe to store the original data
1218
+ hidden_support_df = gr.Dataframe(value=support_data, visible=False)
1219
+
1220
+ def filter_support_data(df, columns, model_query, exclude_models):
1221
+ filtered_df = df.copy()
1222
+
1223
+ # Filter models based on query
1224
+ if model_query:
1225
+ filtered_df = filtered_df[
1226
+ filtered_df["Model"].str.contains(
1227
+ "|".join(q.strip() for q in model_query.split(";")),
1228
+ case=False,
1229
+ regex=True,
1230
+ )
1231
+ ]
1232
+
1233
+ # Exclude specified models
1234
+ if exclude_models:
1235
+ exclude_list = [
1236
+ re.escape(m.strip()) for m in exclude_models.split(";")
1237
+ ]
1238
+ filtered_df = filtered_df[
1239
+ ~filtered_df["Model"].str.contains(
1240
+ "|".join(exclude_list), case=False, regex=True
1241
+ )
1242
+ ]
1243
+
1244
+ # Select columns
1245
+ selected_columns = ["Model"] + [
1246
+ col for col in columns if col in df.columns
1247
+ ]
1248
+ filtered_df = filtered_df[selected_columns]
1249
+
1250
+ return filtered_df
1251
+
1252
+ def select_all_support_columns():
1253
+ return support_data.columns.tolist()[1:] # Exclude 'Model' column
1254
+
1255
+ def deselect_all_support_columns():
1256
+ return []
1257
+
1258
+ # Connect the filter function to the input components
1259
+ filter_inputs = [
1260
+ hidden_support_df,
1261
+ support_shown_columns,
1262
+ filter_support_models,
1263
+ exclude_support_models,
1264
+ ]
1265
+ filter_support_models.change(
1266
+ filter_support_data, filter_inputs, device_support_table
1267
+ )
1268
+ exclude_support_models.change(
1269
+ filter_support_data, filter_inputs, device_support_table
1270
+ )
1271
+ support_shown_columns.change(
1272
+ filter_support_data, filter_inputs, device_support_table
1273
+ )
1274
+
1275
+ # Connect select all and deselect all buttons
1276
+ select_all_support_button.click(
1277
+ select_all_support_columns,
1278
+ inputs=[],
1279
+ outputs=support_shown_columns,
1280
+ )
1281
+ deselect_all_support_button.click(
1282
+ deselect_all_support_columns,
1283
+ inputs=[],
1284
+ outputs=support_shown_columns,
1285
+ )
1286
+
1287
+ # Methodology Tab
1288
+ with gr.TabItem("Methodology", elem_id="methodology", id=7):
1289
+ gr.Markdown(METHODOLOGY_TEXT, elem_id="methodology-text")
1290
+
1291
+ # Citation section
1292
+ with gr.Accordion("📙 Citation", open=False):
1293
+ citation_button = gr.Textbox(
1294
+ value=CITATION_BUTTON_TEXT,
1295
+ label=CITATION_BUTTON_LABEL,
1296
+ lines=7,
1297
+ elem_id="citation-button",
1298
+ show_copy_button=True,
1299
+ )
1300
+
1301
+ # Launch the Gradio interface
1302
+ demo.launch(debug=True, share=True, ssr_mode=False)
multilingual_generate.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import shutil
4
+ import sys
5
+ from collections import defaultdict
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from sklearn.metrics import confusion_matrix
10
+
11
+ from utils import compute_average_wer, download_dataset
12
+
13
+
14
+ def main():
15
+ """
16
+ Main function to orchestrate the multilingual data generation process.
17
+
18
+ This function performs the following steps:
19
+ 1. Downloads multilingual evaluation data if requested.
20
+ 2. Processes multilingual evaluation files.
21
+ 3. Calculates and saves results, including Word Error Rate (WER) and
22
+ language detection confusion matrices.
23
+ """
24
+ source_repo = "argmaxinc/whisperkit-evals-multilingual"
25
+ source_subfolder = "WhisperKit"
26
+ source_directory = f"{source_repo}/{source_subfolder}"
27
+ if len(sys.argv) > 1 and sys.argv[1] == "download":
28
+ try:
29
+ shutil.rmtree(source_repo)
30
+ except:
31
+ print("Nothing to remove.")
32
+ download_dataset(source_repo, source_repo, source_subfolder)
33
+
34
+ results = defaultdict(
35
+ lambda: {
36
+ "average_wer": [],
37
+ "language_wer": defaultdict(list),
38
+ "language_detection": [],
39
+ }
40
+ )
41
+
42
+ confusion_matrices = {}
43
+
44
+ for subdir, _, files in os.walk(source_directory):
45
+ for filename in files:
46
+ if not filename.endswith(".json") or "summary" in filename:
47
+ continue
48
+
49
+ file_path = os.path.join(subdir, filename)
50
+ with open(file_path, "r") as f:
51
+ data = json.load(f)
52
+
53
+ subdir_components = subdir.split(os.path.sep)
54
+ is_forced = "forced" in subdir_components
55
+ model = subdir_components[-3] if not is_forced else subdir_components[-4]
56
+
57
+ key = f"{model}/{'forced' if is_forced else 'not_forced'}"
58
+
59
+ for item in data["results"]:
60
+ if "reference_language" not in item:
61
+ continue
62
+ reference_language = item["reference_language"]
63
+ wer = item["wer"]
64
+ detected_language = item["predicted_language"]
65
+
66
+ result = {
67
+ "reference": item["reference"],
68
+ "prediction": item["prediction"],
69
+ }
70
+
71
+ results[key]["average_wer"].append(result)
72
+ results[key]["language_wer"][reference_language].append(result)
73
+ results[key]["language_detection"].append(
74
+ (reference_language, detected_language)
75
+ )
76
+
77
+ calculate_and_save_results(results, confusion_matrices)
78
+
79
+
80
+ def calculate_and_save_results(results, confusion_matrices):
81
+ """
82
+ Calculates final multilingual metrics and saves them to CSV and JSON files.
83
+
84
+ :param results: Dictionary containing raw multilingual evaluation data.
85
+ :param confusion_matrices: Dictionary to store confusion matrices for language detection.
86
+
87
+ This function processes the raw multilingual data, calculates average metrics,
88
+ creates confusion matrices for language detection, and saves the results to:
89
+ 1. A CSV file with WER data for each model and language.
90
+ 2. A JSON file with confusion matrices for language detection.
91
+ """
92
+ wer_data = []
93
+ for key, data in results.items():
94
+ model, forced = key.rsplit("/", 1)
95
+ row = {
96
+ "Model": model,
97
+ "Forced Tokens": forced == "forced",
98
+ "Average WER": compute_average_wer(data["average_wer"]),
99
+ }
100
+ for lang, wers in data["language_wer"].items():
101
+ row[f"WER_{lang}"] = compute_average_wer(wers)
102
+ wer_data.append(row)
103
+
104
+ true_languages, detected_languages = zip(*data["language_detection"])
105
+ unique_languages = sorted(set(true_languages))
106
+ cm = confusion_matrix(
107
+ true_languages, detected_languages, labels=unique_languages
108
+ )
109
+
110
+ row_sums = cm.sum(axis=1)
111
+ cm_normalized = np.zeros_like(cm, dtype=float)
112
+ non_zero_rows = row_sums != 0
113
+ cm_normalized[non_zero_rows] = (
114
+ cm[non_zero_rows] / row_sums[non_zero_rows, np.newaxis]
115
+ )
116
+
117
+ if model not in confusion_matrices:
118
+ confusion_matrices[model] = {}
119
+ confusion_matrices[model][forced] = {
120
+ "matrix": cm_normalized.tolist(),
121
+ "labels": unique_languages,
122
+ }
123
+
124
+ df = pd.DataFrame(wer_data)
125
+ df.to_csv("dashboard_data/multilingual_results.csv", index=False)
126
+
127
+ with open("dashboard_data/multilingual_confusion_matrices.json", "w") as f:
128
+ json.dump(confusion_matrices, f, indent=2)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
performance_generate.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import json
3
+ import os
4
+ import shutil
5
+ import sys
6
+ import urllib
7
+ from collections import defaultdict
8
+ from datetime import datetime
9
+ from statistics import mean
10
+
11
+ import pandas as pd
12
+ import requests
13
+
14
+ from constants import BASE_WHISPERKIT_BENCHMARK_URL
15
+ from text_normalizer import text_normalizer
16
+ from utils import compute_average_wer, dir_to_json, download_dataset
17
+
18
+
19
+ def fetch_evaluation_data(url):
20
+ """
21
+ Fetches evaluation data from the given URL.
22
+ :param url: The URL to fetch the evaluation data from.
23
+ :returns: The evaluation data as a dictionary.
24
+ :rauses: sys.exit if the request fails
25
+ """
26
+ response = requests.get(url)
27
+ if response.status_code == 200:
28
+ return json.loads(response.text)
29
+ else:
30
+ sys.exit(f"Failed to fetch WhisperKit evals: {response.text}")
31
+
32
+
33
+ def generate_device_map(base_dir):
34
+ """
35
+ Generates a mapping of device identifiers to their corresponding device models.
36
+
37
+ This function iterates through all summary files in the specified base directory and its subdirectories,
38
+ extracting device identifier and device model information. It stores this information in a dictionary,
39
+ where the keys are device identifiers and the values are device models.
40
+
41
+ :param base_dir: The base directory to search for summary files.
42
+ :returns: A dictionary mapping device identifiers to device models.
43
+ """
44
+ device_map = {}
45
+
46
+ # Find all summary files recursively
47
+ summary_files = glob.glob(f"{base_dir}/**/*summary*.json", recursive=True)
48
+
49
+ for file_path in summary_files:
50
+ try:
51
+ with open(file_path, "r") as f:
52
+ data = json.load(f)
53
+
54
+ # Extract device information and create simple mapping
55
+ if "deviceModel" in data and "deviceIdentifier" in data:
56
+ device_map[data["deviceIdentifier"]] = data["deviceModel"]
57
+
58
+ except json.JSONDecodeError:
59
+ print(f"Error reading {file_path}")
60
+ except Exception as e:
61
+ print(f"Error processing {file_path}: {e}")
62
+
63
+ # Save the device map to project root
64
+ output_path = "dashboard_data/device_map.json"
65
+
66
+ with open(output_path, "w") as f:
67
+ json.dump(device_map, f, indent=4, sort_keys=True)
68
+
69
+ return device_map
70
+
71
+
72
+ def get_device_name(device):
73
+ """
74
+ Gets the device name from the device map if it exists.
75
+ :param device: String representing the device name.
76
+ :returns: The device name from the device map if it exists, otherwise the input device name.
77
+ """
78
+ with open("dashboard_data/device_map.json", "r") as f:
79
+ device_map = json.load(f)
80
+ return device_map.get(device, device).replace(" ", "_")
81
+
82
+
83
+ def process_benchmark_file(file_path, dataset_dfs, results):
84
+ """
85
+ Processes a single benchmark file and updates the results dictionary.
86
+
87
+ :param file_path: Path to the benchmark JSON file.
88
+ :param dataset_dfs: Dictionary of DataFrames containing dataset information.
89
+ :param results: Dictionary to store the processed results.
90
+
91
+ This function reads a benchmark JSON file, extracts relevant information,
92
+ and updates the results dictionary with various metrics including WER,
93
+ speed, tokens per second, and quality of inference (QoI).
94
+ """
95
+ with open(file_path, "r") as file:
96
+ test_results = json.load(file)
97
+
98
+ if len(test_results) == 0:
99
+ return
100
+
101
+ first_test_result = test_results[0]
102
+ model = first_test_result["testInfo"]["model"]
103
+ device = first_test_result["testInfo"]["device"]
104
+ dataset_dir = first_test_result["testInfo"]["datasetDir"]
105
+ if "iPhone" in device or "iPad" in device:
106
+ version_numbers = first_test_result["staticAttributes"]["osVersion"].split(".")
107
+ if len(version_numbers) == 3 and version_numbers[-1] == "0":
108
+ version_numbers.pop()
109
+ os_info = f"""{'iOS' if 'iPhone' in device else 'iPadOS'}_{".".join(version_numbers)}"""
110
+ else:
111
+ os_info = f"macOS_{first_test_result['staticAttributes']['osVersion']}"
112
+ timestamp = first_test_result["testInfo"]["date"]
113
+ commit_hash_timestamp = file_path.split("/")[-2]
114
+ commit_timestamp, commit_hash = commit_hash_timestamp.split("_")
115
+
116
+ key = (model, device, os_info, commit_timestamp)
117
+ dataset_name = dataset_dir
118
+ for test_result in test_results:
119
+ test_info = test_result["testInfo"]
120
+ audio_file_name = test_info["audioFile"]
121
+
122
+ dataset_df = dataset_dfs[dataset_name]
123
+
124
+ wer_entry = {
125
+ "prediction": text_normalizer(test_info["prediction"]),
126
+ "reference": text_normalizer(test_info["reference"]),
127
+ }
128
+ results[key]["timestamp"] = timestamp
129
+ results[key]["average_wer"].append(wer_entry)
130
+ results[key]["dataset_wer"][dataset_name].append(wer_entry)
131
+
132
+ input_audio_seconds = test_info["timings"]["inputAudioSeconds"]
133
+ full_pipeline = test_info["timings"]["fullPipeline"]
134
+ total_decoding_loops = test_info["timings"]["totalDecodingLoops"]
135
+
136
+ results[key]["dataset_speed"][dataset_name][
137
+ "inputAudioSeconds"
138
+ ] += input_audio_seconds
139
+ results[key]["dataset_speed"][dataset_name]["fullPipeline"] += full_pipeline
140
+
141
+ results[key]["speed"]["inputAudioSeconds"] += input_audio_seconds
142
+ results[key]["speed"]["fullPipeline"] += full_pipeline
143
+
144
+ results[key]["commit_hash"] = commit_hash
145
+ results[key]["commit_timestamp"] = commit_timestamp
146
+
147
+ results[key]["dataset_tokens_per_second"][dataset_name][
148
+ "totalDecodingLoops"
149
+ ] += total_decoding_loops
150
+ results[key]["dataset_tokens_per_second"][dataset_name][
151
+ "fullPipeline"
152
+ ] += full_pipeline
153
+ results[key]["tokens_per_second"]["totalDecodingLoops"] += total_decoding_loops
154
+ results[key]["tokens_per_second"]["fullPipeline"] += full_pipeline
155
+
156
+ audio = audio_file_name.split(".")[0]
157
+ if dataset_name == "earnings22-10mins":
158
+ audio = audio.split("-")[0]
159
+
160
+ dataset_row = dataset_df.loc[dataset_df["file"].str.contains(audio)].iloc[0]
161
+ reference_wer = dataset_row["wer"]
162
+ prediction_wer = test_info["wer"]
163
+
164
+ results[key]["qoi"].append(1 if prediction_wer <= reference_wer else 0)
165
+
166
+ return key, dataset_name
167
+
168
+
169
+ def process_summary_file(file_path, results):
170
+ """
171
+ Processes a summary file and updates the results dictionary with device support information.
172
+
173
+ :param file_path: Path to the summary JSON file.
174
+ :param results: Dictionary to store the processed results.
175
+
176
+ This function reads a summary JSON file, extracts information about supported
177
+ and failed models for a specific device and OS combination, and updates the
178
+ results dictionary accordingly.
179
+ """
180
+ with open(file_path, "r") as file:
181
+ summary_data = json.load(file)
182
+
183
+ device = summary_data["deviceIdentifier"]
184
+ os = f"{'iPadOS' if 'iPad' in device else summary_data['osType']} {summary_data['osVersion']}"
185
+ commit_timestamp = summary_data["commitTimestamp"]
186
+
187
+ key = (device, os)
188
+ if key in results:
189
+ existing_timestamp = results[key]["commitTimestamp"]
190
+
191
+ existing_dt = datetime.strptime(existing_timestamp, "%Y-%m-%dT%H%M%S")
192
+ new_dt = datetime.strptime(commit_timestamp, "%Y-%m-%dT%H%M%S")
193
+
194
+ if new_dt <= existing_dt:
195
+ return
196
+ else:
197
+ results[key] = {}
198
+
199
+ supported_models = set(summary_data["modelsTested"])
200
+ failed_models = set()
201
+
202
+ dataset_count = 2
203
+ for model, value in summary_data["testResults"].items():
204
+ if model not in summary_data["failureInfo"]:
205
+ dataset_count = len(value)
206
+ break
207
+
208
+ for failed_model in summary_data["failureInfo"]:
209
+ if (
210
+ failed_model in summary_data["testResults"]
211
+ and len(summary_data["testResults"][failed_model]) == dataset_count
212
+ ):
213
+ continue
214
+ supported_models.discard(failed_model)
215
+ failed_models.add(failed_model)
216
+
217
+ results[key]["supportedModels"] = supported_models
218
+ results[key]["commitTimestamp"] = commit_timestamp
219
+ results[key]["failedModels"] = (failed_models, file_path)
220
+ results["modelsTested"] |= supported_models
221
+ results["devices"].add(device)
222
+
223
+
224
+ def calculate_and_save_performance_results(
225
+ performance_results, performance_output_path
226
+ ):
227
+ """
228
+ Calculates final performance metrics and saves them to a JSON file.
229
+
230
+ :param performance_results: Dictionary containing raw performance data.
231
+ :param performance_output_path: Path to save the processed performance results.
232
+
233
+ This function processes the raw performance data, calculates average metrics,
234
+ and writes the final results to a JSON file, with each entry representing
235
+ a unique combination of model, device, and OS.
236
+ """
237
+ not_supported = []
238
+ with open(performance_output_path, "w") as performance_file:
239
+ for key, data in performance_results.items():
240
+ model, device, os_info, timestamp = key
241
+ speed = round(
242
+ data["speed"]["inputAudioSeconds"] / data["speed"]["fullPipeline"], 2
243
+ )
244
+
245
+ if speed < 1.0:
246
+ not_supported.append((model, device, os_info))
247
+ continue
248
+
249
+ performance_entry = {
250
+ "model": model.replace("_", "/"),
251
+ "device": get_device_name(device).replace("_", " "),
252
+ "os": os_info.replace("_", " "),
253
+ "timestamp": data["timestamp"],
254
+ "speed": speed,
255
+ "tokens_per_second": round(
256
+ data["tokens_per_second"]["totalDecodingLoops"]
257
+ / data["tokens_per_second"]["fullPipeline"],
258
+ 2,
259
+ ),
260
+ "dataset_speed": {
261
+ dataset: round(
262
+ speed_info["inputAudioSeconds"] / speed_info["fullPipeline"], 2
263
+ )
264
+ for dataset, speed_info in data["dataset_speed"].items()
265
+ },
266
+ "dataset_tokens_per_second": {
267
+ dataset: round(
268
+ tps_info["totalDecodingLoops"] / tps_info["fullPipeline"], 2
269
+ )
270
+ for dataset, tps_info in data["dataset_tokens_per_second"].items()
271
+ },
272
+ "average_wer": compute_average_wer(data["average_wer"]),
273
+ "dataset_wer": {
274
+ dataset: compute_average_wer(wer)
275
+ for dataset, wer in data["dataset_wer"].items()
276
+ },
277
+ "qoi": round(mean(data["qoi"]), 2),
278
+ "commit_hash": data["commit_hash"],
279
+ "commit_timestamp": data["commit_timestamp"],
280
+ }
281
+
282
+ json.dump(performance_entry, performance_file)
283
+ performance_file.write("\n")
284
+ return not_supported
285
+
286
+
287
+ def calculate_and_save_support_results(
288
+ support_results, not_supported, support_output_path
289
+ ):
290
+ """
291
+ Calculates device support results and saves them to a CSV file.
292
+
293
+ :param support_results: Dictionary containing device support information.
294
+ :param support_output_path: Path to save the processed support results.
295
+
296
+ This function processes the device support data and creates a CSV file
297
+ showing which models are supported on different devices and OS versions,
298
+ using checkmarks, warning signs, quesiton marks or Not supported to
299
+ indicate support status.
300
+ """
301
+ all_models = sorted(support_results["modelsTested"])
302
+ all_devices = sorted(set(support_results["devices"]))
303
+
304
+ df = pd.DataFrame(index=all_models, columns=["Model"] + all_devices)
305
+
306
+ for model in all_models:
307
+ row = {"Model": model}
308
+ for device in all_devices:
309
+ row[device] = ""
310
+
311
+ for key, data in support_results.items():
312
+ if key in ["modelsTested", "devices"]:
313
+ continue
314
+ (device, os) = key
315
+ supported_models = data["supportedModels"]
316
+ failed_models, file_path = data["failedModels"]
317
+ directories = file_path.split("/")
318
+ commit_file, summary_file = directories[-2], directories[-1]
319
+ url = f"{BASE_WHISPERKIT_BENCHMARK_URL}/{commit_file}/{urllib.parse.quote(summary_file)}"
320
+
321
+ if model in supported_models:
322
+ current_value = row[device]
323
+ new_value = (
324
+ f"✅ {os}"
325
+ if current_value == ""
326
+ else f"{current_value}<p>✅ {os}</p>"
327
+ )
328
+ elif model in failed_models:
329
+ current_value = row[device]
330
+ new_value = (
331
+ f"""⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href={url}>{os}</a>"""
332
+ if current_value == ""
333
+ else f"""{current_value}<p>⚠️ <a style='color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;' href={url}>{os}</a></p>"""
334
+ )
335
+ else:
336
+ current_value = row[device]
337
+ new_value = (
338
+ f"? {os}"
339
+ if current_value == ""
340
+ else f"{current_value}<p>? {os}</p>"
341
+ )
342
+ row[device] = new_value
343
+
344
+ df.loc[model] = row
345
+
346
+ remove_unsupported_cells(df, not_supported)
347
+
348
+ cols = df.columns.tolist()
349
+ cols = ["Model"] + [
350
+ get_device_name(col).replace("_", " ") for col in cols if col != "Model"
351
+ ]
352
+ df.columns = cols
353
+
354
+ df.to_csv(support_output_path, index=True)
355
+
356
+
357
+ def remove_unsupported_cells(df, not_supported):
358
+ """
359
+ Updates the DataFrame to mark unsupported model-device combinations.
360
+
361
+ This function reads a configuration file to determine which models are supported
362
+ on which devices. It then iterates over the DataFrame and sets the value to "Not supported"
363
+ for any model-device combination that is not supported according to the configuration.
364
+
365
+ :param df: A Pandas DataFrame where the index represents models and columns represent devices.
366
+ """
367
+ with open("dashboard_data/config.json", "r") as file:
368
+ config_data = json.load(file)
369
+
370
+ device_support = config_data["device_support"]
371
+ for info in device_support:
372
+ identifiers = set(info["identifiers"])
373
+ supported = set(info["models"]["supported"])
374
+
375
+ for model in df.index:
376
+ for device in df.columns:
377
+ if (
378
+ any(identifier in device for identifier in identifiers)
379
+ and model not in supported
380
+ ):
381
+ df.at[model, device] = "Not Supported"
382
+
383
+ for model, device, os in not_supported:
384
+ df.at[model, device] = "Not Supported"
385
+
386
+
387
+ def main():
388
+ """
389
+ Main function to orchestrate the performance data generation process.
390
+
391
+ This function performs the following steps:
392
+ 1. Downloads benchmark data if requested.
393
+ 2. Fetches evaluation data for various datasets.
394
+ 3. Processes benchmark files and summary files.
395
+ 4. Calculates and saves performance and support results.
396
+ """
397
+ source_xcresult_repo = "argmaxinc/whisperkit-evals-dataset"
398
+ source_xcresult_subfolder = "benchmark_data/"
399
+ source_xcresult_directory = f"{source_xcresult_repo}/{source_xcresult_subfolder}"
400
+ if len(sys.argv) > 1 and sys.argv[1] == "download":
401
+ try:
402
+ shutil.rmtree(source_xcresult_repo)
403
+ except:
404
+ print("Nothing to remove.")
405
+ download_dataset(
406
+ source_xcresult_repo, source_xcresult_repo, source_xcresult_subfolder
407
+ )
408
+
409
+ datasets = {
410
+ "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",
411
+ "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",
412
+ "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",
413
+ "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",
414
+ "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",
415
+ "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",
416
+ }
417
+
418
+ dataset_dfs = {}
419
+ for dataset_name, url in datasets.items():
420
+ evals = fetch_evaluation_data(url)
421
+ dataset_dfs[dataset_name] = pd.json_normalize(evals["results"])
422
+
423
+ performance_results = defaultdict(
424
+ lambda: {
425
+ "average_wer": [],
426
+ "dataset_wer": defaultdict(list),
427
+ "qoi": [],
428
+ "speed": {"inputAudioSeconds": 0, "fullPipeline": 0},
429
+ "tokens_per_second": {"totalDecodingLoops": 0, "fullPipeline": 0},
430
+ "dataset_speed": defaultdict(
431
+ lambda: {"inputAudioSeconds": 0, "fullPipeline": 0}
432
+ ),
433
+ "dataset_tokens_per_second": defaultdict(
434
+ lambda: {"totalDecodingLoops": 0, "fullPipeline": 0}
435
+ ),
436
+ "timestamp": None,
437
+ "commit_hash": None,
438
+ "commit_timestamp": None,
439
+ }
440
+ )
441
+
442
+ support_results = {"modelsTested": set(), "devices": set()}
443
+
444
+ generate_device_map(source_xcresult_directory)
445
+
446
+ for subdir, _, files in os.walk(source_xcresult_directory):
447
+ for filename in files:
448
+ file_path = os.path.join(subdir, filename)
449
+ if not filename.endswith(".json"):
450
+ continue
451
+ elif "summary" in filename:
452
+ process_summary_file(file_path, support_results)
453
+ else:
454
+ process_benchmark_file(file_path, dataset_dfs, performance_results)
455
+
456
+ not_supported = calculate_and_save_performance_results(
457
+ performance_results, "dashboard_data/performance_data.json"
458
+ )
459
+ calculate_and_save_support_results(
460
+ support_results, not_supported, "dashboard_data/support_data.csv"
461
+ )
462
+
463
+
464
+ if __name__ == "__main__":
465
+ main()
quality_generate.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ metadata = test_results["metadata"]
59
+ test_results = test_results["results"]
60
+ model = file_path.split("/")[-3].replace("_", "/")
61
+ device = metadata["inference_context"]["device_spec"]["product_name"]
62
+ device = get_device_name(device)
63
+ timestamp = file_path.split("/")[-1].split(".")[0]
64
+ key = model
65
+ dataset_name = metadata["dataset_name"]
66
+
67
+ for test_result in test_results:
68
+ audio_file_name = test_result["file"]
69
+
70
+ dataset_key = "Earnings-22" if "earnings22" in dataset_name else "LibriSpeech"
71
+ dataset_df = dataset_dfs[dataset_key]
72
+
73
+ wer_entry = {
74
+ "prediction": text_normalizer(test_result["prediction"]),
75
+ "reference": text_normalizer(test_result["reference"]),
76
+ }
77
+ quality_results[key]["timestamp"] = timestamp
78
+ quality_results[key]["dataset_wer"][dataset_name].append(wer_entry)
79
+
80
+ audio = audio_file_name.split(".")[0]
81
+ dataset_row = dataset_df.loc[dataset_df["file"].str.contains(audio)].iloc[0]
82
+ reference_wer = dataset_row["wer"]
83
+ prediction_wer = test_result["wer"]
84
+
85
+ quality_results[key]["qoi"].append(1 if prediction_wer <= reference_wer else 0)
86
+
87
+
88
+ def calculate_and_save_quality_results(quality_results, quality_output_path):
89
+ """
90
+ Calculates final quality metrics and saves them to a JSON file.
91
+
92
+ :param quality_results: Dictionary containing raw quality data.
93
+ :param quality_output_path: Path to save the processed quality results.
94
+
95
+ This function processes the raw quality data, calculates average metrics,
96
+ and writes the final results to a JSON file, with each entry representing
97
+ a unique model's quality metrics across different datasets, including
98
+ Word Error Rate (WER) and Quality of Inference (QoI).
99
+ """
100
+ with open(quality_output_path, "w") as quality_file:
101
+ for key, data in quality_results.items():
102
+ model = key
103
+
104
+ dataset_wers = {
105
+ dataset: compute_average_wer(wer)
106
+ for dataset, wer in data["dataset_wer"].items()
107
+ }
108
+ average_wer = (
109
+ sum(dataset_wers.values()) / len(dataset_wers)
110
+ if len(dataset_wers) != 0
111
+ else 0
112
+ )
113
+
114
+ quality_entry = {
115
+ "model": model.replace("_", "/"),
116
+ "timestamp": data["timestamp"],
117
+ "average_wer": round(average_wer, 2),
118
+ "dataset_wer": dataset_wers,
119
+ "qoi": round(mean(data["qoi"]), 2),
120
+ }
121
+
122
+ json.dump(quality_entry, quality_file)
123
+ quality_file.write("\n")
124
+
125
+
126
+ def main():
127
+ """
128
+ Main function to orchestrate the quality data generation process.
129
+
130
+ This function performs the following steps:
131
+ 1. Downloads quality data if requested.
132
+ 2. Fetches evaluation data for various datasets.
133
+ 3. Processes quality files for specific datasets.
134
+ 4. Calculates and saves quality results, including WER and QoI metrics.
135
+ """
136
+ if len(sys.argv) > 1 and sys.argv[1] == "download":
137
+ try:
138
+ shutil.rmtree("english")
139
+ except:
140
+ print("Nothing to remove.")
141
+ download_dataset("argmaxinc/whisperkit-evals", "english", "WhisperKit")
142
+
143
+ datasets = {
144
+ "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",
145
+ "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",
146
+ "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",
147
+ "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",
148
+ "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",
149
+ "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",
150
+ }
151
+
152
+ dataset_dfs = {}
153
+ for dataset_name, url in datasets.items():
154
+ evals = fetch_evaluation_data(url)
155
+ dataset_dfs[dataset_name] = pd.json_normalize(evals["results"])
156
+
157
+ source_quality_directory = "argmaxinc/english/WhisperKit/"
158
+
159
+ quality_results = defaultdict(
160
+ lambda: {
161
+ "average_wer": [],
162
+ "dataset_wer": defaultdict(list),
163
+ "qoi": [],
164
+ "timestamp": None,
165
+ }
166
+ )
167
+
168
+ for subdir, _, files in os.walk(source_quality_directory):
169
+ dataset = subdir.split("/")[-1]
170
+ if dataset not in ["earnings22-12hours", "librispeech"]:
171
+ continue
172
+
173
+ for filename in files:
174
+ if not filename.endswith(".json"):
175
+ continue
176
+
177
+ file_path = os.path.join(subdir, filename)
178
+ process_quality_file(file_path, dataset_dfs, quality_results)
179
+
180
+ calculate_and_save_quality_results(
181
+ quality_results, "dashboard_data/quality_data.json"
182
+ )
183
+
184
+
185
+ if __name__ == "__main__":
186
+ 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
static/Zwizz-Medium.woff ADDED
Binary file (28.7 kB). View file
 
static/Zwizz-Regular.woff ADDED
Binary file (28.4 kB). View file
 
static/Zwizz-SemiBold.woff ADDED
Binary file (28.7 kB). View file
 
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,991 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
28
+ :param results: List of dictionaries, each containing 'reference' and 'prediction' keys
29
+ :return: Average WER as a percentage, rounded to 2 decimal places
30
+
31
+ This function calculates the WER for each reference-prediction pair and returns
32
+ the average. If no predictions are provided, it returns 100% WER.
33
+ """
34
+ references = [result["reference"] for result in results]
35
+ predictions = [result["prediction"] for result in results]
36
+ if len(predictions) == 0:
37
+ return 1
38
+ return round(
39
+ wer_metric.compute(references=references, predictions=predictions) * 100.0,
40
+ 2,
41
+ )
42
+
43
+
44
+ def read_json_line_by_line(file_path):
45
+ """
46
+ Read a JSON file line by line, parsing each line as a separate JSON object.
47
+
48
+ :param file_path: Path to the JSON file
49
+ :return: List of parsed JSON objects
50
+
51
+ This function is useful for reading large JSON files that contain one JSON object
52
+ per line. It handles JSON parsing errors gracefully, skipping invalid lines.
53
+ """
54
+ data = []
55
+ with open(file_path, "r") as f:
56
+ for line in f:
57
+ try:
58
+ item = json.loads(line.strip())
59
+ data.append(item)
60
+ except json.JSONDecodeError:
61
+ print(f"Skipping invalid JSON in {file_path}: {line}")
62
+ return data
63
+
64
+
65
+ def group_wer(group):
66
+ """
67
+ Calculate the Word Error Rate (WER) for a group of transcriptions.
68
+
69
+ :param group: DataFrame group containing 'normalized_reference' and 'normalized_prediction' columns
70
+ :return: Average WER for the group
71
+
72
+ This function is typically used with DataFrame groupby operations to calculate
73
+ WER for specific groups of transcriptions.
74
+ """
75
+ return compute_average_wer(
76
+ group[["normalized_reference", "normalized_prediction"]]
77
+ .rename(
78
+ columns={
79
+ "normalized_reference": "reference",
80
+ "normalized_prediction": "prediction",
81
+ }
82
+ )
83
+ .to_dict("records")
84
+ )
85
+
86
+
87
+ def load_multilingual_results(csv_file):
88
+ """
89
+ Load multilingual results from a CSV file into a pandas DataFrame.
90
+
91
+ :param csv_file: Path to the CSV file containing multilingual results
92
+ :return: DataFrame with the loaded results, or None if the file is not found
93
+
94
+ This function attempts to load a CSV file using pandas, handling potential
95
+ FileNotFoundError exceptions.
96
+ """
97
+ try:
98
+ df = pd.json_normalize(csv_file)
99
+ return df
100
+ except FileNotFoundError:
101
+ return None
102
+
103
+
104
+ def download_dataset(repo_id, local_dir, remote_dir, path_includes=""):
105
+ """
106
+ Download benchmark result files from a specified Hugging Face repository to a local directory.
107
+
108
+ :param repo_id: ID of the Hugging Face repository
109
+ :param local_dir: Local directory where downloaded files will be saved
110
+ :param remote_dir: Remote directory within the repository to download from
111
+
112
+ This function uses the Hugging Face Hub API to list and download files from a
113
+ specific directory in a repository. It forces the download to ensure up-to-date files.
114
+ """
115
+ files = list_repo_files(repo_id, repo_type="dataset")
116
+ directory_files = [
117
+ file for file in files if file.startswith(remote_dir) and path_includes in file
118
+ ]
119
+ with ThreadPoolExecutor() as executor:
120
+ executor.map(
121
+ lambda file: hf_hub_download(
122
+ repo_id=repo_id,
123
+ repo_type="dataset",
124
+ filename=file,
125
+ local_dir=local_dir,
126
+ force_download=True,
127
+ ),
128
+ directory_files,
129
+ )
130
+
131
+
132
+ def process_file(file_path):
133
+ """
134
+ Process a file containing JSON objects delimited by new lines.
135
+
136
+ :param file_path: Path to the file to be processed
137
+ :return: List of dictionaries, each representing a parsed JSON object
138
+
139
+ This function reads the file line by line, parsing each line as a JSON object.
140
+ It handles potential JSON decoding errors, printing error messages for invalid lines.
141
+ """
142
+ data = []
143
+ with open(file_path, "r") as file:
144
+ for line in file:
145
+ line = line.strip()
146
+ if not line:
147
+ continue
148
+ try:
149
+ json_obj = json.loads(line)
150
+ data.append(json_obj)
151
+ except json.JSONDecodeError as e:
152
+ print(f"Error decoding JSON in line: {line}")
153
+ print(f"Error message: {str(e)}")
154
+ return data
155
+
156
+
157
+ def dir_to_json(root_dir, output_file):
158
+ """
159
+ Convert a directory of benchmark result files to a single JSON file.
160
+
161
+ :param root_dir: Root directory containing the benchmark result files
162
+ :param output_file: Output file where the JSON data will be saved
163
+
164
+ This function walks through the directory structure, processes each file,
165
+ and writes the combined data to a single JSON file. It extracts metadata
166
+ from the file path and includes it in the JSON output.
167
+ """
168
+ with open(output_file, "w") as outfile:
169
+ for subdir, _, files in os.walk(root_dir):
170
+ for file in files:
171
+ file_path = os.path.join(subdir, file)
172
+ # ignore .DS_Store and summary files
173
+ if file_path.endswith(".DS_Store") or "summary" in file_path:
174
+ continue
175
+ parts = file_path.split(os.sep)
176
+ print(parts)
177
+ model_version = parts[2]
178
+ device_name = parts[3].replace("_", " ")
179
+ os_type_version = parts[4]
180
+ dataset_name = parts[5]
181
+ timestamp_commit = parts[6].replace(".json", "")
182
+ timestamp, commit_hash, commit_timestamp = timestamp_commit.split("_")
183
+
184
+ data_list = process_file(file_path)
185
+ for data in data_list:
186
+ original_entry = {
187
+ "model": model_version.replace("_", "/"),
188
+ "device": device_name,
189
+ "os": os_type_version.replace("_", " "),
190
+ "wer": data["wer"],
191
+ "dataset_name": dataset_name,
192
+ "reference_transcription": data["reference_transcription"],
193
+ "prediction_transcription": data["prediction_transcription"],
194
+ "difference_transcription": data["difference_transcription"],
195
+ "audio_file_url": data["audio_file_url"],
196
+ "timestamp": timestamp.replace("-", ":").replace(":", "-", 2),
197
+ "commit_hash": commit_hash,
198
+ "commit_timestamp": commit_timestamp,
199
+ }
200
+
201
+ outfile.write(json.dumps(original_entry) + "\n")
202
+
203
+
204
+ async def download_audio_to_ndarray(url):
205
+ """
206
+ Downloads an audio file from a URL and converts it to a NumPy array.
207
+
208
+ :param url: The URL of the audio file to download
209
+ :return: A tuple containing the sample rate and audio data as a NumPy array
210
+
211
+ This asynchronous function uses aiohttp to download the audio file,
212
+ converts it to an AudioSegment, and then to a NumPy array. It handles
213
+ both mono and stereo audio files.
214
+ """
215
+ async with aiohttp.ClientSession() as session:
216
+ async with session.get(url) as response:
217
+ if response.status == 200:
218
+ audio_bytes = BytesIO(await response.read())
219
+ audio = AudioSegment.from_file(audio_bytes, format="mp3")
220
+ audio_data = np.array(audio.get_array_of_samples())
221
+ if audio.channels == 2:
222
+ audio_data = audio_data.reshape((-1, 2))
223
+ return audio.frame_rate, audio_data
224
+ else:
225
+ return None, None
226
+
227
+
228
+ async def play_audio(url):
229
+ """
230
+ Wrapper function for Gradio to play audio from a URL.
231
+
232
+ :param url: The URL of the audio file to play
233
+ :return: A tuple of sample rate and audio data, or an error message
234
+
235
+ This function uses download_audio_to_ndarray to get the audio data
236
+ and returns it in a format suitable for Gradio's audio player.
237
+ """
238
+ sample_rate, audio_data = await download_audio_to_ndarray(url)
239
+ if audio_data is None:
240
+ return "Error downloading the file"
241
+ else:
242
+ return sample_rate, audio_data
243
+
244
+
245
+ def get_filter_cond(df, model, device, os, dataset, timestamp=None):
246
+ """
247
+ Creates a filter condition for a DataFrame based on specified parameters.
248
+
249
+ :param df: DataFrame containing the transcription data
250
+ :param model: String representing the model name
251
+ :param device: String representing the device name
252
+ :param os: String representing the OS name
253
+ :param dataset: String representing the dataset name
254
+ :param timestamp: Optional timestamp for filtering (default: None)
255
+ :return: A boolean mask for filtering the DataFrame
256
+
257
+ This function constructs a complex boolean condition for filtering
258
+ the DataFrame based on the provided parameters.
259
+ """
260
+ filter_cond = (
261
+ (df["model"] == model)
262
+ & (df["device"] == device)
263
+ & (df["os"] == os)
264
+ & (df["dataset_name"] == dataset)
265
+ )
266
+ return filter_cond & (df["timestamp"] == timestamp) if timestamp else filter_cond
267
+
268
+
269
+ def get_filtered_transcript(df, model, device, os, dataset, timestamp):
270
+ """
271
+ Retrieves filtered transcription data from a DataFrame.
272
+
273
+ :param df: DataFrame containing the transcription data
274
+ :param model: String representing the model name
275
+ :param device: String representing the device name
276
+ :param os: String representing the OS name
277
+ :param dataset: String representing the dataset name
278
+ :param timestamp: String representing the timestamp
279
+ :return: A filtered DataFrame with transcription data
280
+
281
+ This function applies a filter to the input DataFrame and returns
282
+ relevant columns for transcription analysis.
283
+ """
284
+ filter_cond = get_filter_cond(df, model, device, os, dataset, timestamp)
285
+ df = df[filter_cond][
286
+ [
287
+ "reference_transcription",
288
+ "prediction_transcription",
289
+ "difference_transcription",
290
+ "audio_file_url",
291
+ ]
292
+ ]
293
+ return df
294
+
295
+
296
+ def get_filtered_timestamps(df, model, device, os, dataset):
297
+ """
298
+ Retrieves unique timestamps for a specific model, device, OS, and dataset combination.
299
+
300
+ :param df: DataFrame containing the transcription data
301
+ :param model: String representing the model name
302
+ :param device: String representing the device name
303
+ :param os: String representing the OS name
304
+ :param dataset: String representing the dataset name
305
+ :return: A filtered DataFrame containing unique timestamps
306
+
307
+ This function is useful for getting a list of available timestamps
308
+ for a specific configuration, which can be used for further analysis or UI elements.
309
+ """
310
+ filter_cond = get_filter_cond(df, model, device, os, dataset)
311
+ df = df[filter_cond][["timestamp"]].drop_duplicates()
312
+ return df
313
+
314
+
315
+ def make_model_name_clickable_link(model):
316
+ """
317
+ Creates an HTML link to the Hugging Face model page.
318
+
319
+ :param model: String representing the model name
320
+ :return: An HTML string containing a clickable link to the model page
321
+
322
+ This function generates a formatted HTML link that can be used in
323
+ web interfaces to provide direct access to the model's page on Hugging Face.
324
+ """
325
+ 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>"""
326
+
327
+
328
+ def make_dataset_wer_clickable_link(row, dataset):
329
+ """
330
+ Creates a clickable link for the WER value of a dataset.
331
+
332
+ :param row: Row containing the dataset WER value
333
+ :param dataset: String representing the dataset name
334
+ :return: An HTML string containing a clickable link to the dataset's WER details
335
+
336
+ This function generates a formatted HTML link that can be used in
337
+ web interfaces to provide access to detailed WER information for a specific dataset.
338
+ """
339
+ dataset_column = f"{dataset}"
340
+ href = WHISPER_OPEN_AI_LINK.format(
341
+ row["Model"].replace("/", "_"),
342
+ dataset,
343
+ )
344
+ return f'<a style="color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;" href="{href}">{row[dataset_column]}</a>'
345
+
346
+
347
+ def make_timestamp_clickable_link(model, dataset, timestamp):
348
+ """
349
+ Creates a clickable link for a timestamp.
350
+
351
+ :param model: String representing the model name
352
+ :param dataset: String representing the dataset name
353
+ :param timestamp: Timestamp to be displayed and used in the link
354
+ :return: An HTML string containing a clickable div for the timestamp
355
+
356
+ This function generates a formatted HTML div that can be used as a clickable
357
+ element in web interfaces, typically for displaying and interacting with specific timestamps.
358
+ """
359
+ elem_id = (
360
+ f"{dataset}-{model}-{timestamp}".replace(" ", "_")
361
+ .replace('"', "")
362
+ .replace("'", "")
363
+ .replace(",", "")
364
+ )
365
+ onclick = f"onclick=\"document.getElementById('{elem_id}').click();\""
366
+ return f'<div style="color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;" {onclick} href="#">{timestamp}</div>'
367
+
368
+
369
+ def make_multilingual_model_clickable_link(model):
370
+ """
371
+ Creates a clickable link for a multilingual model name.
372
+
373
+ :param model: String representing the model name
374
+ :return: An HTML string containing a clickable div for the model name
375
+
376
+ This function generates a formatted HTML div that can be used as a clickable
377
+ element in web interfaces, typically for displaying and interacting with multilingual model names.
378
+ """
379
+ elem_id = (
380
+ f"{model}".replace(" ", "_").replace('"', "").replace("'", "").replace(",", "")
381
+ )
382
+ onclick = f"onclick=\"document.getElementById('{elem_id}').click();console.log('hello');\""
383
+ return f'<div style="color: #3B82F6; text-decoration: underline; text-decoration-style: dotted;" {onclick} href="#">{model}</div>'
384
+
385
+
386
+ def plot_metric(
387
+ df, y_axis_col, y_axis_title, fig_title, filter_input=None, exclude_input=None
388
+ ):
389
+ """
390
+ Plots a metric for each model-device-OS group in a DataFrame.
391
+
392
+ :param df: DataFrame containing the benchmark data
393
+ :param y_axis_col: DataFrame column to use as the y-axis
394
+ :param y_axis_title: Display name for the y-axis
395
+ :param fig_title: Display title for the figure
396
+ :param filter_input: Optional string to filter the model-device-OS combinations
397
+ :param exclude_input: Optional string to exclude model-device-OS combinations
398
+ :return: A Plotly figure object
399
+ """
400
+ grouped = df.groupby(["model", "device", "os"])
401
+ sorted_groups = [group.sort_values("commit_timestamp") for _, group in grouped]
402
+
403
+ if filter_input:
404
+ filters = [f.strip().lower() for f in filter_input.split(";")]
405
+ sorted_groups = [
406
+ group
407
+ for group in sorted_groups
408
+ if any(
409
+ f
410
+ in f"{group['model'].iloc[0]}-{group['device'].iloc[0]}-{group['os'].iloc[0]}".lower()
411
+ for f in filters
412
+ )
413
+ ]
414
+
415
+ if exclude_input:
416
+ excludes = [e.strip().lower() for e in exclude_input.split(";")]
417
+ sorted_groups = [
418
+ group
419
+ for group in sorted_groups
420
+ if not any(
421
+ e
422
+ in f"{group['model'].iloc[0]}-{group['device'].iloc[0]}-{group['os'].iloc[0]}".lower()
423
+ for e in excludes
424
+ )
425
+ ]
426
+
427
+ base_colors = ["#4542f4", "#0e0c06", "#ccf0a7", "#ff7f4e", "#ffd15a"]
428
+ num_colors = len(sorted_groups)
429
+ random_colors = generate_random_colors(base_colors, num_colors)
430
+ fig = go.Figure()
431
+ for i, group in enumerate(sorted_groups):
432
+ model_device_os = (
433
+ f"{group['model'].iloc[0]}-{group['device'].iloc[0]}-{group['os'].iloc[0]}"
434
+ )
435
+ fig.add_trace(
436
+ go.Scatter(
437
+ x=group["commit_timestamp"].apply(
438
+ lambda x: datetime.strptime(x, "%Y-%m-%dT%H%M%S").strftime(
439
+ "%Y-%m-%d %H:%M:%S"
440
+ )
441
+ ),
442
+ y=group[y_axis_col],
443
+ mode="lines+markers",
444
+ name=model_device_os,
445
+ line=dict(color=random_colors[i % len(random_colors)]),
446
+ marker=dict(color=random_colors[i % len(random_colors)]),
447
+ hovertemplate=(
448
+ f"<b>{model_device_os}</b><br>"
449
+ "Timestamp: %{x}<br>"
450
+ f"{y_axis_title}: %{{y:.2f}}<br>"
451
+ "<extra></extra>"
452
+ ),
453
+ )
454
+ )
455
+ fig.update_layout(
456
+ title=fig_title,
457
+ xaxis_title="Commit Timestamp",
458
+ yaxis_title=y_axis_title,
459
+ legend_title="Model-Device-OS",
460
+ width=1100,
461
+ height=600,
462
+ plot_bgcolor="rgb(250,249,244)",
463
+ )
464
+ return fig
465
+
466
+
467
+ def fields(raw_class):
468
+ """
469
+ Returns the fields of a dataclass.
470
+
471
+ :param raw_class: The dataclass to inspect
472
+ :return: List of fields in the dataclass
473
+
474
+ This utility function extracts and returns all the fields defined in a dataclass,
475
+ excluding special methods and attributes.
476
+ """
477
+ return [
478
+ v for k, v in raw_class.__dict__.items() if k[:2] != "__" and k[-2:] != "__"
479
+ ]
480
+
481
+
482
+ def get_os_name_and_version(os_string):
483
+ """
484
+ Extracts the OS name and major version from a string.
485
+
486
+ :param os_string: String representing the OS name and version
487
+ :return: Formatted string with OS name and major version
488
+
489
+ This function splits the input string into OS name and version,
490
+ then returns a formatted string with just the major version number.
491
+ """
492
+ os_name, os_version = os_string.split()
493
+ os_version = os_version.split(".")[0]
494
+ return f"{os_name} {os_version}"
495
+
496
+
497
+ def create_initial_quality_column_dict():
498
+ """
499
+ Creates the initial column dictionary for the quality table.
500
+
501
+ :return: A list of column dictionaries
502
+
503
+ This function defines the basic structure of the quality table,
504
+ including columns for model, average WER, and QoI (Quality of Implementation).
505
+ """
506
+ return [
507
+ [
508
+ "model",
509
+ ColumnContent,
510
+ ColumnContent("Model", "html", True, never_hidden=True),
511
+ ],
512
+ ["average_wer", ColumnContent, ColumnContent("Average WER", "html", True)],
513
+ ["qoi", ColumnContent, ColumnContent("QoI", "html", True)],
514
+ ]
515
+
516
+
517
+ def calculate_parity(m2_ultra_wer, row):
518
+ """
519
+ Calculates the WER parity between M2 Ultra and the current model.
520
+
521
+ :param m2_ultra_wer: DataFrame containing WER values for M2 Ultra
522
+ :param row: Current row being processed
523
+ :return: WER difference between M2 Ultra and current model, or None if not applicable
524
+
525
+ This function computes the percentage difference in WER between the M2 Ultra model
526
+ and the current model, providing a measure of relative performance.
527
+ """
528
+ if row["Model"] in m2_ultra_wer.index:
529
+ return round(m2_ultra_wer[row["Model"]] - row["Average WER"], 2)
530
+ return None
531
+
532
+
533
+ def create_initial_performance_column_dict():
534
+ """
535
+ Creates the initial column dictionary for the performance table.
536
+
537
+ :return: A list of column dictionaries
538
+
539
+ This function defines the basic structure of the performance table,
540
+ including columns for model, device, OS, parity, average WER, QoI, speed, and tokens per second.
541
+ """
542
+ return [
543
+ [
544
+ "model",
545
+ ColumnContent,
546
+ ColumnContent("Model", "html", True, never_hidden=True),
547
+ ],
548
+ [
549
+ "device",
550
+ ColumnContent,
551
+ ColumnContent("Device", "html", True, never_hidden=True),
552
+ ],
553
+ ["os", ColumnContent, ColumnContent("OS", "html", True, never_hidden=True)],
554
+ ["parity", ColumnContent, ColumnContent("Parity %", "html", False)],
555
+ ["average_wer", ColumnContent, ColumnContent("Average WER", "html", False)],
556
+ ["qoi", ColumnContent, ColumnContent("QoI", "html", False)],
557
+ ["speed", ColumnContent, ColumnContent("Speed", "html", False)],
558
+ ["toks", ColumnContent, ColumnContent("Tok / s", "html", False)],
559
+ ]
560
+
561
+
562
+ def add_datasets_to_quality_columns(column_dict, datasets):
563
+ """
564
+ Adds dataset-specific columns to the quality table column dictionary.
565
+
566
+ :param column_dict: The initial column dictionary
567
+ :param datasets: List of dataset names to add
568
+ :return: A dictionary containing the updated column dictionary and related metadata
569
+
570
+ This function extends the quality table structure with columns for each dataset,
571
+ and creates a dataclass to represent the table structure. It also generates
572
+ metadata about the columns for use in the UI.
573
+ """
574
+ updated_column_dict = column_dict.copy()
575
+
576
+ for dataset in datasets:
577
+ field_name = dataset.replace("-", "")
578
+ updated_column_dict.append(
579
+ [field_name, ColumnContent, ColumnContent(dataset, "html", True)]
580
+ )
581
+
582
+ AutoEvalColumn = make_dataclass("AutoEvalColumn", updated_column_dict, frozen=True)
583
+
584
+ COLS = [c.name for c in fields(AutoEvalColumn) if not c.hidden]
585
+ TYPES = [c.type for c in fields(AutoEvalColumn) if not c.hidden]
586
+ ALWAYS_HERE_COLS = [c.name for c in fields(AutoEvalColumn) if c.never_hidden]
587
+ TOGGLE_COLS = [c.name for c in fields(AutoEvalColumn) if not c.never_hidden]
588
+ SELECTED_COLS = [
589
+ c.name
590
+ for c in fields(AutoEvalColumn)
591
+ if not c.never_hidden and c.displayed_by_default
592
+ ]
593
+
594
+ return {
595
+ "column_dict": updated_column_dict,
596
+ "AutoEvalColumn": AutoEvalColumn,
597
+ "COLS": COLS,
598
+ "TYPES": TYPES,
599
+ "ALWAYS_HERE_COLS": ALWAYS_HERE_COLS,
600
+ "TOGGLE_COLS": TOGGLE_COLS,
601
+ "SELECTED_COLS": SELECTED_COLS,
602
+ }
603
+
604
+
605
+ def add_datasets_to_performance_columns(column_dict, datasets):
606
+ """
607
+ Adds dataset-specific columns to the performance table column dictionary.
608
+
609
+ :param column_dict: The initial column dictionary
610
+ :param datasets: List of dataset names to add
611
+ :return: A dictionary containing the updated column dictionary and related metadata
612
+
613
+ This function extends the performance table structure with columns for each dataset,
614
+ adding both speed and tokens per second metrics. It also creates a dataclass to
615
+ represent the table structure and generates metadata about the columns for use in the UI.
616
+ """
617
+ updated_column_dict = column_dict.copy()
618
+
619
+ for dataset in datasets:
620
+ field_name = dataset.replace("-", "")
621
+ updated_column_dict.append(
622
+ [
623
+ f"{field_name}_speed",
624
+ ColumnContent,
625
+ ColumnContent(
626
+ f"{'Short-Form' if dataset == 'librispeech-10mins' else 'Long-Form'} Speed",
627
+ "html",
628
+ True,
629
+ ),
630
+ ]
631
+ )
632
+ updated_column_dict.append(
633
+ [
634
+ f"{field_name}_toks",
635
+ ColumnContent,
636
+ ColumnContent(
637
+ f"{'Short-Form' if dataset == 'librispeech-10mins' else 'Long-Form'} Tok/s",
638
+ "html",
639
+ True,
640
+ ),
641
+ ]
642
+ )
643
+
644
+ AutoEvalColumn = make_dataclass("AutoEvalColumn", updated_column_dict, frozen=True)
645
+
646
+ COLS = [c.name for c in fields(AutoEvalColumn) if not c.hidden]
647
+ TYPES = [c.type for c in fields(AutoEvalColumn) if not c.hidden]
648
+ ALWAYS_HERE_COLS = [c.name for c in fields(AutoEvalColumn) if c.never_hidden]
649
+ TOGGLE_COLS = [c.name for c in fields(AutoEvalColumn) if not c.never_hidden]
650
+ SELECTED_COLS = [
651
+ c.name
652
+ for c in fields(AutoEvalColumn)
653
+ if not c.never_hidden and c.displayed_by_default
654
+ ]
655
+
656
+ return {
657
+ "column_dict": updated_column_dict,
658
+ "AutoEvalColumn": AutoEvalColumn,
659
+ "COLS": COLS,
660
+ "TYPES": TYPES,
661
+ "ALWAYS_HERE_COLS": ALWAYS_HERE_COLS,
662
+ "TOGGLE_COLS": TOGGLE_COLS,
663
+ "SELECTED_COLS": SELECTED_COLS,
664
+ }
665
+
666
+
667
+ def create_confusion_matrix_plot(matrix, labels, is_forced):
668
+ """
669
+ Creates a confusion matrix plot for language detection.
670
+
671
+ :param matrix: 2D numpy array representing the confusion matrix
672
+ :param labels: List of language labels
673
+ :param is_forced: Boolean indicating whether language hint was used
674
+ :return: A Plotly figure object representing the confusion matrix
675
+
676
+ This function generates a heatmap visualization of the confusion matrix
677
+ for language detection, with customized layout and hover information.
678
+ """
679
+ fig = go.Figure(
680
+ data=go.Heatmap(
681
+ z=matrix,
682
+ x=labels,
683
+ y=labels,
684
+ colorscale=[
685
+ [0, "rgb(250,249,244)"],
686
+ [0.5, "rgb(69,66,244)"],
687
+ [1.0, "rgb(14,12,6)"],
688
+ ],
689
+ hoverongaps=False,
690
+ hovertemplate="True: %{y}<br>Predicted: %{x}<br>Value: %{z}<extra></extra>",
691
+ )
692
+ )
693
+ fig.update_layout(
694
+ title=f'Language Detection Confusion Matrix with {"Language Hint" if is_forced else "Language Prediction by Model"}',
695
+ xaxis_title="Predicted Language",
696
+ yaxis_title="True Language",
697
+ xaxis=dict(tickangle=-45),
698
+ width=600,
699
+ height=600,
700
+ margin=dict(l=50, r=50, t=50, b=50),
701
+ )
702
+ return fig
703
+
704
+
705
+ def hex_to_rgb(hex_color):
706
+ """
707
+ Converts a hexadecimal color code to RGB values.
708
+
709
+ :param hex_color: String representing a color in hexadecimal format
710
+ :return: Tuple of three integers representing RGB values
711
+
712
+ This function takes a hex color code and returns the corresponding
713
+ RGB values as a tuple of integers.
714
+ """
715
+ hex_color = hex_color.lstrip("#")
716
+ return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
717
+
718
+
719
+ def rgb_to_hex(rgb):
720
+ """
721
+ Converts RGB values to a hexadecimal color code.
722
+
723
+ :param rgb: Tuple of three integers representing RGB values
724
+ :return: String representing the color in hexadecimal format
725
+
726
+ This function takes RGB values as a tuple and returns the corresponding
727
+ hex color code as a string.
728
+ """
729
+ return "#{:02x}{:02x}{:02x}".format(*rgb)
730
+
731
+
732
+ def interpolate_colors(color1, color2, factor):
733
+ """
734
+ Interpolates between two colors in HSV space.
735
+
736
+ :param color1: First color in hexadecimal format
737
+ :param color2: Second color in hexadecimal format
738
+ :param factor: Float between 0 and 1, representing the interpolation factor
739
+ :return: Interpolated color in hexadecimal format
740
+
741
+ This function performs color interpolation in HSV color space, which can
742
+ produce more visually pleasing results than simple RGB interpolation.
743
+ """
744
+ rgb1 = hex_to_rgb(color1)
745
+ rgb2 = hex_to_rgb(color2)
746
+
747
+ hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
748
+ hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
749
+
750
+ h = (hsv1[0] + factor * (hsv2[0] - hsv1[0])) % 1.0
751
+ s = hsv1[1] + factor * (hsv2[1] - hsv1[1])
752
+ v = hsv1[2] + factor * (hsv2[2] - hsv1[2])
753
+
754
+ rgb = colorsys.hsv_to_rgb(h, s, v)
755
+ return rgb_to_hex(tuple(int(x * 255) for x in rgb))
756
+
757
+
758
+ def color_distance(color1, color2):
759
+ """
760
+ Calculates the Euclidean distance between two colors in RGB space.
761
+
762
+ :param color1: First color in hexadecimal format
763
+ :param color2: Second color in hexadecimal format
764
+ :return: Float representing the distance between the two colors
765
+
766
+ This function computes the Euclidean distance between two colors in RGB space,
767
+ which can be used as a measure of color similarity.
768
+ """
769
+ rgb1 = hex_to_rgb(color1)
770
+ rgb2 = hex_to_rgb(color2)
771
+ return sum((a - b) ** 2 for a, b in zip(rgb1, rgb2)) ** 0.5
772
+
773
+
774
+ def generate_random_colors(base_colors, num_colors, min_distance=30):
775
+ """
776
+ Generates a list of random colors based on a set of base colors.
777
+
778
+ :param base_colors: List of base colors in hexadecimal format
779
+ :param num_colors: Number of colors to generate
780
+ :param min_distance: Minimum distance between generated colors (default: 30)
781
+ :return: List of generated colors in hexadecimal format
782
+
783
+ This function creates a list of random colors by interpolating between
784
+ the provided base colors. It attempts to maintain a minimum distance
785
+ between colors to ensure visual distinctiveness.
786
+ """
787
+ generated_colors = []
788
+ attempts = 0
789
+ max_attempts = 1000
790
+
791
+ while len(generated_colors) < num_colors and attempts < max_attempts:
792
+ color1, color2 = random.sample(base_colors, 2)
793
+ factor = random.random()
794
+ new_color = interpolate_colors(color1, color2, factor)
795
+
796
+ if all(color_distance(new_color, c) >= min_distance for c in generated_colors):
797
+ generated_colors.append(new_color)
798
+ attempts = 0
799
+ else:
800
+ attempts += 1
801
+
802
+ if attempts > 100:
803
+ if random.random() < 0.1:
804
+ generated_colors.append(new_color)
805
+ attempts = 0
806
+
807
+ return generated_colors
808
+
809
+
810
+ @dataclass
811
+ class Task:
812
+ """
813
+ Dataclass representing a benchmark task.
814
+
815
+ :param benchmark: String representing the benchmark name
816
+ :param metric: String representing the metric used for evaluation
817
+ :param col_name: String representing the column name in the results DataFrame
818
+ """
819
+
820
+ benchmark: str
821
+ metric: str
822
+ col_name: str
823
+
824
+
825
+ @dataclass(frozen=True)
826
+ class ColumnContent:
827
+ """
828
+ Dataclass representing a column in the results table.
829
+
830
+ :param name: String representing the column name
831
+ :param type: String representing the data type of the column
832
+ :param displayed_by_default: Boolean indicating if the column should be displayed by default
833
+ :param hidden: Boolean indicating if the column should be hidden (default: False)
834
+ :param never_hidden: Boolean indicating if the column should never be hidden (default: False)
835
+ :param dummy: Boolean indicating if this is a dummy column (default: False)
836
+ """
837
+
838
+ name: str
839
+ type: str
840
+ displayed_by_default: bool
841
+ hidden: bool = False
842
+ never_hidden: bool = False
843
+ dummy: bool = False
844
+
845
+
846
+ css = """
847
+ @font-face {
848
+ font-family: 'Zwizz Regular';
849
+ font-style: normal;
850
+ font-weight: normal;
851
+ src: local('Zwizz Regular'), url('static/Zwizz-Regular.woff') format('woff');
852
+ }
853
+
854
+ @font-face {
855
+ font-family: 'Zwizz Medium';
856
+ font-style: normal;
857
+ font-weight: normal;
858
+ src: local('Zwizz Medium'), url('static/Zwizz-Medium.woff') format('woff');
859
+ }
860
+
861
+ @font-face {
862
+ font-family: 'Zwizz SemiBold';
863
+ font-style: normal;
864
+ font-weight: normal;
865
+ src: local('Zwizz SemiBold'), url('static/Zwizz-SemiBold.woff') format('woff');
866
+ }
867
+
868
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap');
869
+ @import url('https://fonts.googleapis.com/css2?family=Sora:wght@300..400&display=swap');
870
+
871
+ /* Typography Scale */
872
+ h1, .h1 {
873
+ font-family: 'Sora', sans-serif;
874
+ font-weight: 300;
875
+ font-size: 2em;
876
+ letter-spacing: -0.05em;
877
+ }
878
+
879
+ h2, .h2 {
880
+ font-family: 'Sora', sans-serif;
881
+ font-weight: 400;
882
+ letter-spacing: -0.05em;
883
+ }
884
+
885
+ h3, h4, h5, .h3, .h4, .h5 {
886
+ font-family: 'Sora', sans-serif;
887
+ font-weight: 400;
888
+ letter-spacing: -0.05em;
889
+ }
890
+
891
+ h6, .h6, pre, code, .monospace {
892
+ font-family: 'IBM Plex Mono', monospace;
893
+ font-weight: 400;
894
+ letter-spacing: 0.01em;
895
+ }
896
+
897
+ /* Add strong tag styling */
898
+ strong, b {
899
+ font-family: 'Zwizz SemiBold', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
900
+ letter-spacing: -0.02em;
901
+ }
902
+
903
+ /* Global Zwizz styles */
904
+ :root {
905
+ --zwizz-spacing: -0.02em;
906
+ }
907
+
908
+ /* All Gradio elements should have Zwizz spacing */
909
+ .gradio-container * {
910
+ letter-spacing: var(--zwizz-spacing);
911
+ line-height: 1.7;
912
+ }
913
+
914
+ /* UI Elements */
915
+ .tab-buttons button, #models-to-add-text, .gradio-button {
916
+ font-family: 'Sora', sans-serif;
917
+ font-weight: 400;
918
+ letter-spacing: -0.05em;
919
+ }
920
+
921
+ /* Specific Table Styling */
922
+ table, .table, th, td {
923
+ font-family: 'IBM Plex Mono', 'Noto Color Emoji', sans-serif, monospace !important;
924
+ font-weight: 400;
925
+ letter-spacing: 0.01em;
926
+ }
927
+
928
+ /* Technical/Code Elements */
929
+ .code-block, .technical-text {
930
+ font-family: 'IBM Plex Mono', monospace;
931
+ font-weight: 400;
932
+ letter-spacing: 0.01em;
933
+ }
934
+
935
+ /* Additional Elements */
936
+ #methodology-text p, #methodology-text li, .markdown-text {
937
+ font-family: 'Zwizz Regular', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
938
+ font-size: 16px !important;
939
+ letter-spacing: var(--zwizz-spacing);
940
+ line-height: 1.7;
941
+ }
942
+
943
+ /* Font weight utilities */
944
+ .zwizz-medium {
945
+ font-family: 'Zwizz Medium', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
946
+ }
947
+
948
+ .zwizz-semibold {
949
+ font-family: 'Zwizz SemiBold', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
950
+ }
951
+
952
+ /* Maintaining Original Layout Rules */
953
+ .gradio-container {
954
+ max-width: 95% !important;
955
+ }
956
+
957
+ /* Table Layouts */
958
+ .large-table,
959
+ .large-table .table-wrap,
960
+ #multilingual-model-table .table-wrap,
961
+ #lookup-table .table-wrap {
962
+ height: 35em !important;
963
+ overflow-y: scroll !important;
964
+ }
965
+
966
+ /* SVG Container Rules */
967
+ .svg-container,
968
+ .main-svg {
969
+ width: 100% !important;
970
+ }
971
+
972
+ .large-table, .large-table .table-wrap, #multilingual-model-table .table-wrap, #lookup-table .table-wrap {
973
+ height: 35em !important;
974
+ overflow-y: scroll !important;
975
+ }
976
+
977
+ .left-side-table .table-wrap {
978
+ height: 15em !important;
979
+ overflow-y: scroll !important;
980
+ }
981
+
982
+ #average-wer-table .table-wrap {
983
+ height: 8em !important;
984
+ overflow-y: scroll !important;
985
+ }
986
+
987
+ #general-wer-table .table-wrap {
988
+ height: 35em !important;
989
+ overflow-y: scroll !important;
990
+ }
991
+ """