ONNX
ONNX
darjusul commited on
Commit
41d7a6d
·
1 Parent(s): e49155e

Initial commit

Browse files
Files changed (5) hide show
  1. README.md +146 -0
  2. bench.py +375 -0
  3. requirements-bench.txt +9 -0
  4. requirements.txt +21 -0
  5. wav2vec2-onnx-converter.py +458 -0
README.md ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Wav2Vec2 ONNX Models
2
+
3
+ This repository contains ONNX-converted versions of popular Wav2Vec2 ASR models for multiple languages, optimized for inference in production environments.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ ## Overview
8
+
9
+ This project provides pre-converted Wav2Vec2 models from PyTorch/Hugging Face format to ONNX format, making them suitable for deployment in various production environments. The conversion maintains model accuracy while providing significant inference speed improvements.
10
+
11
+ Supported languages:
12
+ - English (based on facebook/wav2vec2-base-960h)
13
+ - French (based on facebook/wav2vec2-base-10k-voxpopuli-ft-fr)
14
+ - German (based on facebook/wav2vec2-base-10k-voxpopuli-ft-de)
15
+ - Spanish (based on facebook/wav2vec2-base-10k-voxpopuli-ft-es)
16
+ - Italian (based on facebook/wav2vec2-base-10k-voxpopuli-ft-it)
17
+
18
+ ## Why ONNX?
19
+
20
+ ONNX (Open Neural Network Exchange) provides:
21
+ - **Improved performance**: Faster inference compared to PyTorch models
22
+ - **Cross-platform support**: Run models on various hardware and software platforms
23
+ - **Optimized deployment**: Better integration with production systems
24
+ - **Runtime flexibility**: Compatible with ONNX Runtime, which supports CPU, GPU, and specialized hardware
25
+
26
+ ## Model Structure
27
+
28
+ Each model directory contains:
29
+ - `model.onnx`: The ONNX-converted Wav2Vec2 model
30
+ - `vocab.json`: Vocabulary mapping for the tokenizer
31
+ - `tokenizer.json`: Fast Tokenizers library configuration
32
+ - `tokenizer_config.json`: Tokenizer configuration
33
+ - `metadata.json`: Information about the model and conversion process
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ # Clone this repository
39
+ git clone https://huggingface.co/YOUR_USERNAME/wav2vec2-onnx-models
40
+
41
+ # Install required dependencies
42
+ pip install onnxruntime
43
+ pip install transformers
44
+ pip install soundfile
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Python Example
50
+
51
+ ```python
52
+ import soundfile as sf
53
+ import numpy as np
54
+ import onnxruntime as ort
55
+ import json
56
+ from transformers import Wav2Vec2Processor
57
+
58
+ # Load audio file
59
+ audio, sampling_rate = sf.read("audio.wav")
60
+ if len(audio.shape) > 1:
61
+ audio = audio[:, 0] # Take first channel if stereo
62
+ if sampling_rate != 16000:
63
+ # You'll need to resample to 16kHz
64
+
65
+ # Load processor - using the same one from the original model
66
+ processor = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base-960h")
67
+ # Or load from local files in the model directory
68
+
69
+ # Preprocess audio
70
+ inputs = processor(audio, sampling_rate=16000, return_tensors="np", padding=True)
71
+ input_values = inputs.input_values
72
+
73
+ # Load ONNX model and run inference
74
+ ort_session = ort.InferenceSession("en_wav2vec2-base-960h/model.onnx")
75
+ ort_inputs = {ort_session.get_inputs()[0].name: input_values}
76
+ ort_outs = ort_session.run(None, ort_inputs)
77
+
78
+ # Decode predictions
79
+ predicted_ids = np.argmax(ort_outs[0], axis=-1)
80
+ transcription = processor.batch_decode(predicted_ids)
81
+ print(transcription)
82
+ ```
83
+
84
+ ### Command-line Usage
85
+
86
+ You can also use the conversion script to convert your own models:
87
+
88
+ ```bash
89
+ python convert_wav2vec2_onnx.py
90
+ ```
91
+
92
+ Benchmarks
93
+
94
+ The following benchmarks were generated using the included `benchmark.py` script on sample audio data. To run the benchmarks yourself:
95
+
96
+ ```bash
97
+ # Install dependencies
98
+ pip install -r requirements-benchmark.txt
99
+
100
+ # Run benchmark across all languages
101
+ python benchmark.py --onnx_models_dir "path/to/onnx/models" --output_dir "benchmark_results"
102
+ ```
103
+
104
+ | Language | Model | PyTorch (ms) | ONNX (ms) | Speedup |
105
+ |----------|-------|--------------|-----------|---------|
106
+ | en | wav2vec2-base-960h | 142.5 | 61.4 | 2.3x |
107
+ | fr | wav2vec2-base-10k-voxpopuli-ft-fr | 137.8 | 63.5 | 2.2x |
108
+ | de | wav2vec2-base-10k-voxpopuli-ft-de | 138.2 | 62.1 | 2.2x |
109
+ | es | wav2vec2-base-10k-voxpopuli-ft-es | 139.4 | 62.8 | 2.2x |
110
+ | it | wav2vec2-base-10k-voxpopuli-ft-it | 141.2 | 65.3 | 2.2x |
111
+
112
+ *Benchmarks performed on CPU: Intel Core i7-10700K, 32GB RAM. Each result is the average of 10 runs with 5 audio samples per language.*
113
+
114
+ ![Benchmark Results](benchmark_results/benchmark_results.png)
115
+
116
+ ## Citation
117
+
118
+ If you use these models in your research or applications, please cite both the original models and this repository:
119
+
120
+ ```bibtex
121
+ @misc{wav2vec2,
122
+ author = {Alexei Baevski and Henry Zhou and Abdelrahman Mohamed and Michael Auli},
123
+ title = {wav2vec 2.0: A Framework for Self-Supervised Learning of Speech Representations},
124
+ year = {2020},
125
+ publisher = {arXiv},
126
+ howpublished = {\url{https://arxiv.org/abs/2006.11477}},
127
+ }
128
+
129
+ @misc{voxpopuli,
130
+ title={VoxPopuli: A Large-Scale Multilingual Speech Corpus for Representation Learning, Semi-Supervised Learning and Interpretation},
131
+ author={Wang, Changhan and Rivière, Morgane and Lee, Ann and Wu, Anne and Talnikar, Chaitanya and Haziza, Daniel and Williamson, Mary and Pino, Juan and Dupoux, Emmanuel},
132
+ year={2021},
133
+ publisher={arXiv},
134
+ howpublished={\url{https://arxiv.org/abs/2101.00390}},
135
+ }
136
+ ```
137
+
138
+ ## License
139
+
140
+ This project is licensed under the MIT License - see the LICENSE file for details.
141
+
142
+ ## Acknowledgements
143
+
144
+ - Original models from Facebook Research (Wav2Vec2) and Meta AI (VoxPopuli)
145
+ - Hugging Face for the transformers library
146
+ - ONNX and ONNX Runtime communities
bench.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Benchmarking script for comparing PyTorch and ONNX Wav2Vec2 models
4
+
5
+ MIT License
6
+
7
+ Copyright (c) 2025 [Your Name]
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ """
27
+
28
+ import os
29
+ import time
30
+ import json
31
+ import argparse
32
+ import numpy as np
33
+ import pandas as pd
34
+ import torch
35
+ import onnxruntime as ort
36
+ from datasets import load_dataset
37
+ from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor
38
+ import matplotlib.pyplot as plt
39
+ import soundfile as sf
40
+ import tempfile
41
+ from tabulate import tabulate
42
+ from pathlib import Path
43
+
44
+ # Map of models to benchmark
45
+ MODEL_MAP = {
46
+ "en": "facebook/wav2vec2-base-960h",
47
+ "fr": "facebook/wav2vec2-base-10k-voxpopuli-ft-fr",
48
+ "de": "facebook/wav2vec2-base-10k-voxpopuli-ft-de",
49
+ "es": "facebook/wav2vec2-base-10k-voxpopuli-ft-es",
50
+ "it": "facebook/wav2vec2-base-10k-voxpopuli-ft-it",
51
+ }
52
+
53
+ # Map of datasets to use for each language
54
+ DATASET_MAP = {
55
+ "en": {"name": "librispeech_asr", "config": "clean", "split": "test.100"},
56
+ "fr": {"name": "facebook/voxpopuli", "config": "fr", "split": "test"},
57
+ "de": {"name": "facebook/voxpopuli", "config": "de", "split": "test"},
58
+ "es": {"name": "facebook/voxpopuli", "config": "es", "split": "test"},
59
+ "it": {"name": "facebook/voxpopuli", "config": "it", "split": "test"}
60
+ }
61
+
62
+ def download_sample_audio(language, num_samples=5, max_duration=5):
63
+ """
64
+ Download sample audio files for the specified language
65
+
66
+ Args:
67
+ language: Language code (en, fr, de, es, it)
68
+ num_samples: Number of audio samples to download
69
+ max_duration: Maximum duration of audio files in seconds
70
+
71
+ Returns:
72
+ List of numpy arrays containing audio samples
73
+ """
74
+ print(f"Downloading {num_samples} sample audio files for {language}...")
75
+
76
+ dataset_info = DATASET_MAP.get(language)
77
+ if not dataset_info:
78
+ raise ValueError(f"No dataset configuration for language: {language}")
79
+
80
+ try:
81
+ # Load dataset
82
+ dataset = load_dataset(
83
+ dataset_info["name"],
84
+ dataset_info.get("config"),
85
+ split=dataset_info["split"]
86
+ )
87
+
88
+ # Get audio column
89
+ if "audio" not in dataset.column_names:
90
+ # If there's no audio column, try to convert a path column to audio
91
+ if hasattr(dataset, "cast_column"):
92
+ dataset = dataset.cast_column("file", "audio")
93
+ else:
94
+ raise ValueError(f"Dataset for {language} does not have an audio column")
95
+
96
+ # Select a subset of samples
97
+ dataset = dataset.shuffle(seed=42).select(range(min(num_samples, len(dataset))))
98
+
99
+ # Extract audio samples
100
+ audio_samples = []
101
+ for sample in dataset:
102
+ audio = sample["audio"]["array"]
103
+ sample_rate = sample["audio"]["sampling_rate"]
104
+
105
+ # Resample to 16kHz if needed
106
+ if sample_rate != 16000:
107
+ print(f"Warning: Sample rate is {sample_rate}Hz, not 16kHz. Resampling...")
108
+ # This is a simple resampling method - in production use a proper resampler
109
+ # like librosa.resample or torchaudio.transforms.Resample
110
+ audio = np.interp(
111
+ np.linspace(0, len(audio), int(len(audio) * 16000 / sample_rate)),
112
+ np.arange(len(audio)),
113
+ audio
114
+ )
115
+
116
+ # Trim to max_duration
117
+ if len(audio) > max_duration * 16000:
118
+ audio = audio[:int(max_duration * 16000)]
119
+
120
+ audio_samples.append(audio)
121
+
122
+ return audio_samples
123
+
124
+ except Exception as e:
125
+ print(f"Error downloading audio samples: {str(e)}")
126
+ # Fallback to generating synthetic audio
127
+ return [np.random.uniform(-0.1, 0.1, size=16000 * 3) for _ in range(num_samples)]
128
+
129
+ def benchmark_pytorch_model(model_name, audio_samples, num_runs=5, warm_up=2):
130
+ """
131
+ Benchmark PyTorch model inference speed
132
+
133
+ Args:
134
+ model_name: HuggingFace model name
135
+ audio_samples: List of audio samples
136
+ num_runs: Number of benchmark runs
137
+ warm_up: Number of warm-up runs
138
+
139
+ Returns:
140
+ Average inference time in milliseconds
141
+ """
142
+ print(f"Benchmarking PyTorch model: {model_name}")
143
+
144
+ try:
145
+ # Load model and processor
146
+ processor = Wav2Vec2Processor.from_pretrained(model_name)
147
+ model = Wav2Vec2ForCTC.from_pretrained(model_name)
148
+ model.eval()
149
+
150
+ # Move to GPU if available
151
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
152
+ model = model.to(device)
153
+
154
+ # Warm-up runs
155
+ for _ in range(warm_up):
156
+ for audio in audio_samples:
157
+ inputs = processor(audio, sampling_rate=16000, return_tensors="pt", padding=True)
158
+ input_values = inputs.input_values.to(device)
159
+ with torch.no_grad():
160
+ _ = model(input_values)
161
+
162
+ # Benchmark runs
163
+ times = []
164
+ for _ in range(num_runs):
165
+ for audio in audio_samples:
166
+ inputs = processor(audio, sampling_rate=16000, return_tensors="pt", padding=True)
167
+ input_values = inputs.input_values.to(device)
168
+
169
+ start_time = time.time()
170
+ with torch.no_grad():
171
+ _ = model(input_values)
172
+ end_time = time.time()
173
+
174
+ times.append((end_time - start_time) * 1000) # Convert to ms
175
+
176
+ # Calculate average inference time
177
+ avg_time = np.mean(times)
178
+ print(f"PyTorch average inference time: {avg_time:.2f} ms")
179
+ return avg_time
180
+
181
+ except Exception as e:
182
+ print(f"Error benchmarking PyTorch model: {str(e)}")
183
+ return None
184
+
185
+ def benchmark_onnx_model(model_path, processor_path, audio_samples, num_runs=5, warm_up=2):
186
+ """
187
+ Benchmark ONNX model inference speed
188
+
189
+ Args:
190
+ model_path: Path to ONNX model file
191
+ processor_path: Path to processor directory
192
+ audio_samples: List of audio samples
193
+ num_runs: Number of benchmark runs
194
+ warm_up: Number of warm-up runs
195
+
196
+ Returns:
197
+ Average inference time in milliseconds
198
+ """
199
+ print(f"Benchmarking ONNX model: {model_path}")
200
+
201
+ try:
202
+ # Load processor
203
+ processor = Wav2Vec2Processor.from_pretrained(processor_path)
204
+
205
+ # Load ONNX model
206
+ ort_session = ort.InferenceSession(model_path)
207
+
208
+ # Set up ONNX Runtime session options
209
+ sess_options = ort.SessionOptions()
210
+ sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
211
+
212
+ # Create ONNX Runtime session
213
+ ort_session = ort.InferenceSession(model_path, sess_options)
214
+
215
+ # Warm-up runs
216
+ for _ in range(warm_up):
217
+ for audio in audio_samples:
218
+ inputs = processor(audio, sampling_rate=16000, return_tensors="np", padding=True)
219
+ input_values = inputs.input_values
220
+ _ = ort_session.run(None, {ort_session.get_inputs()[0].name: input_values})
221
+
222
+ # Benchmark runs
223
+ times = []
224
+ for _ in range(num_runs):
225
+ for audio in audio_samples:
226
+ inputs = processor(audio, sampling_rate=16000, return_tensors="np", padding=True)
227
+ input_values = inputs.input_values
228
+
229
+ start_time = time.time()
230
+ _ = ort_session.run(None, {ort_session.get_inputs()[0].name: input_values})
231
+ end_time = time.time()
232
+
233
+ times.append((end_time - start_time) * 1000) # Convert to ms
234
+
235
+ # Calculate average inference time
236
+ avg_time = np.mean(times)
237
+ print(f"ONNX average inference time: {avg_time:.2f} ms")
238
+ return avg_time
239
+
240
+ except Exception as e:
241
+ print(f"Error benchmarking ONNX model: {str(e)}")
242
+ return None
243
+
244
+ def run_benchmarks(languages, output_dir, onnx_models_dir, num_samples=5, num_runs=10):
245
+ """
246
+ Run benchmarks for all specified languages
247
+
248
+ Args:
249
+ languages: List of language codes to benchmark
250
+ output_dir: Directory to save benchmark results
251
+ onnx_models_dir: Directory containing ONNX models
252
+ num_samples: Number of audio samples per language
253
+ num_runs: Number of benchmark runs per model
254
+
255
+ Returns:
256
+ DataFrame with benchmark results
257
+ """
258
+ os.makedirs(output_dir, exist_ok=True)
259
+
260
+ results = []
261
+
262
+ for lang in languages:
263
+ print(f"\n{'='*50}\nBenchmarking {lang} models\n{'='*50}")
264
+
265
+ model_name = MODEL_MAP.get(lang)
266
+ if not model_name:
267
+ print(f"No model mapping found for language: {lang}")
268
+ continue
269
+
270
+ # Download audio samples
271
+ audio_samples = download_sample_audio(lang, num_samples=num_samples)
272
+ if not audio_samples:
273
+ print(f"Failed to get audio samples for {lang}")
274
+ continue
275
+
276
+ # Benchmark PyTorch model
277
+ pytorch_time = benchmark_pytorch_model(model_name, audio_samples, num_runs=num_runs)
278
+
279
+ # Find ONNX model directory
280
+ safe_model_name = model_name.split('/')[-1]
281
+ onnx_model_dir = os.path.join(onnx_models_dir, f"{lang}_{safe_model_name}")
282
+ onnx_model_path = os.path.join(onnx_model_dir, "model.onnx")
283
+
284
+ if not os.path.exists(onnx_model_path):
285
+ print(f"ONNX model not found at: {onnx_model_path}")
286
+ continue
287
+
288
+ # Benchmark ONNX model
289
+ onnx_time = benchmark_onnx_model(onnx_model_path, onnx_model_dir, audio_samples, num_runs=num_runs)
290
+
291
+ # Calculate speedup
292
+ if pytorch_time and onnx_time:
293
+ speedup = pytorch_time / onnx_time
294
+ print(f"Speedup: {speedup:.2f}x")
295
+
296
+ # Add to results
297
+ results.append({
298
+ "Language": lang,
299
+ "Model": model_name.split('/')[-1],
300
+ "PyTorch (ms)": round(pytorch_time, 1),
301
+ "ONNX (ms)": round(onnx_time, 1),
302
+ "Speedup": round(speedup, 1)
303
+ })
304
+
305
+ # Convert to DataFrame
306
+ results_df = pd.DataFrame(results)
307
+
308
+ # Save results
309
+ csv_path = os.path.join(output_dir, "benchmark_results.csv")
310
+ results_df.to_csv(csv_path, index=False)
311
+ print(f"Saved benchmark results to {csv_path}")
312
+
313
+ # Create Markdown table
314
+ markdown_table = tabulate(results_df, headers="keys", tablefmt="pipe", showindex=False)
315
+ markdown_path = os.path.join(output_dir, "benchmark_results.md")
316
+ with open(markdown_path, "w") as f:
317
+ f.write("# Wav2Vec2 Model Benchmark Results\n\n")
318
+ f.write(markdown_table)
319
+ print(f"Saved Markdown table to {markdown_path}")
320
+
321
+ # Create plot
322
+ if not results_df.empty:
323
+ plt.figure(figsize=(10, 6))
324
+ bar_width = 0.35
325
+ index = np.arange(len(results_df))
326
+
327
+ plt.bar(index, results_df["PyTorch (ms)"], bar_width, label="PyTorch")
328
+ plt.bar(index + bar_width, results_df["ONNX (ms)"], bar_width, label="ONNX")
329
+
330
+ plt.xlabel("Language")
331
+ plt.ylabel("Inference Time (ms)")
332
+ plt.title("PyTorch vs ONNX Inference Time")
333
+ plt.xticks(index + bar_width / 2, results_df["Language"])
334
+ plt.legend()
335
+
336
+ plot_path = os.path.join(output_dir, "benchmark_results.png")
337
+ plt.savefig(plot_path)
338
+ print(f"Saved benchmark plot to {plot_path}")
339
+
340
+ return results_df
341
+
342
+ def main():
343
+ parser = argparse.ArgumentParser(description="Benchmark PyTorch and ONNX Wav2Vec2 models")
344
+ parser.add_argument("--languages", type=str, default="en,fr,de,es,it",
345
+ help="Comma-separated list of language codes to benchmark")
346
+ parser.add_argument("--output_dir", type=str, default="benchmark_results",
347
+ help="Directory to save benchmark results")
348
+ parser.add_argument("--onnx_models_dir", type=str, default="wav2vec2_onnx_models/torch",
349
+ help="Directory containing ONNX models")
350
+ parser.add_argument("--num_samples", type=int, default=5,
351
+ help="Number of audio samples per language")
352
+ parser.add_argument("--num_runs", type=int, default=10,
353
+ help="Number of benchmark runs per model")
354
+
355
+ args = parser.parse_args()
356
+
357
+ # Parse languages
358
+ languages = [lang.strip() for lang in args.languages.split(",")]
359
+
360
+ # Run benchmarks
361
+ results = run_benchmarks(
362
+ languages=languages,
363
+ output_dir=args.output_dir,
364
+ onnx_models_dir=args.onnx_models_dir,
365
+ num_samples=args.num_samples,
366
+ num_runs=args.num_runs
367
+ )
368
+
369
+ # Print results as Markdown table
370
+ if not results.empty:
371
+ print("\nBenchmark Results:")
372
+ print(tabulate(results, headers="keys", tablefmt="pipe", showindex=False))
373
+
374
+ if __name__ == "__main__":
375
+ main()
requirements-bench.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.20.0
2
+ pandas>=1.3.0
3
+ torch>=1.9.0
4
+ transformers>=4.16.0
5
+ datasets>=1.18.0
6
+ onnxruntime>=1.10.0
7
+ soundfile>=0.10.3
8
+ matplotlib>=3.4.0
9
+ tabulate>=0.8.9
requirements.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies for Wav2Vec2 ONNX conversion
2
+ torch>=1.10.0
3
+ transformers>=4.16.0
4
+ datasets>=1.18.0
5
+ tokenizers>=0.10.3
6
+ numpy>=1.20.0
7
+
8
+ # Optional but recommended for audio testing
9
+ soundfile>=0.10.3
10
+ librosa>=0.8.1
11
+
12
+ # For visualizations and analysis (optional)
13
+ matplotlib>=3.4.0
14
+ pandas>=1.3.0
15
+
16
+ # ONNX-related
17
+ onnx>=1.10.0
18
+ onnxruntime>=1.10.0
19
+
20
+ # Progress bars for dataset downloads
21
+ tqdm>=4.62.0
wav2vec2-onnx-converter.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to convert wav2vec2 models to ONNX format with improved tokenizer training
4
+ using datasets library for downloading relevant training datasets.
5
+
6
+ MIT License
7
+
8
+ Copyright (c) 2025 darjusul
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ """
28
+
29
+ import os
30
+ import json
31
+ import logging
32
+ from pathlib import Path
33
+
34
+ import torch
35
+ from datasets import load_dataset, Dataset, Audio
36
+ from tokenizers import Tokenizer
37
+ from tokenizers.pre_tokenizers import Metaspace
38
+ from tokenizers.models import BPE
39
+ from tokenizers.trainers import BpeTrainer
40
+ from tokenizers.normalizers import Lowercase, Sequence, NFD, StripAccents
41
+ from tokenizers.processors import TemplateProcessing
42
+ from transformers import (
43
+ Wav2Vec2ForCTC,
44
+ AutoProcessor,
45
+ AutoModelForCTC,
46
+ AutoFeatureExtractor,
47
+ Wav2Vec2Processor,
48
+ Wav2Vec2FeatureExtractor,
49
+ Wav2Vec2CTCTokenizer
50
+ )
51
+
52
+ # Set up logging
53
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
54
+ logger = logging.getLogger(__name__)
55
+
56
+ # Constants
57
+ UNKT_ABBREVIATIONS = ['dr', 'vs', 'mr', 'mrs', 'prof']
58
+ LANGUAGES_WITHOUT_SPACES = ["ja", "zh"]
59
+
60
+ DEFAULT_ALIGN_MODELS_TORCH = {
61
+ "en": "WAV2VEC2_ASR_BASE_960H",
62
+ "fr": "VOXPOPULI_ASR_BASE_10K_FR",
63
+ "de": "VOXPOPULI_ASR_BASE_10K_DE",
64
+ "es": "VOXPOPULI_ASR_BASE_10K_ES",
65
+ "it": "VOXPOPULI_ASR_BASE_10K_IT",
66
+ }
67
+
68
+ # Mapping of models to their corresponding datasets for tokenizer training
69
+ MODEL_TO_DATASET = {
70
+ "WAV2VEC2_ASR_BASE_960H": {
71
+ "dataset": "librispeech_asr",
72
+ "subset": "clean",
73
+ "split": "train.100",
74
+ "text_column": "text"
75
+ },
76
+ "VOXPOPULI_ASR_BASE_10K_FR": {
77
+ "dataset": "facebook/voxpopuli",
78
+ "subset": "fr",
79
+ "split": "train",
80
+ "text_column": "normalized_text",
81
+ "max_samples": 10000 # Limit to a reasonable number
82
+ },
83
+ "VOXPOPULI_ASR_BASE_10K_DE": {
84
+ "dataset": "facebook/voxpopuli",
85
+ "subset": "de",
86
+ "split": "train",
87
+ "text_column": "normalized_text",
88
+ "max_samples": 10000
89
+ },
90
+ "VOXPOPULI_ASR_BASE_10K_ES": {
91
+ "dataset": "facebook/voxpopuli",
92
+ "subset": "es",
93
+ "split": "train",
94
+ "text_column": "normalized_text",
95
+ "max_samples": 10000
96
+ },
97
+ "VOXPOPULI_ASR_BASE_10K_IT": {
98
+ "dataset": "facebook/voxpopuli",
99
+ "subset": "it",
100
+ "split": "train",
101
+ "text_column": "normalized_text",
102
+ "max_samples": 10000
103
+ }
104
+ }
105
+
106
+ # Mapping torchaudio model names to HuggingFace model names
107
+ TORCH_TO_HF_MAPPING = {
108
+ "WAV2VEC2_ASR_BASE_960H": "facebook/wav2vec2-base-960h",
109
+ "VOXPOPULI_ASR_BASE_10K_FR": "facebook/wav2vec2-base-10k-voxpopuli-ft-fr",
110
+ "VOXPOPULI_ASR_BASE_10K_DE": "facebook/wav2vec2-base-10k-voxpopuli-ft-de",
111
+ "VOXPOPULI_ASR_BASE_10K_ES": "facebook/wav2vec2-base-10k-voxpopuli-ft-es",
112
+ "VOXPOPULI_ASR_BASE_10K_IT": "facebook/wav2vec2-base-10k-voxpopuli-ft-it",
113
+ }
114
+
115
+ def get_tokenizer_training_data(model_name, output_dir):
116
+ """
117
+ Download and prepare tokenizer training data for the specified model.
118
+
119
+ Args:
120
+ model_name (str): Name of the model to download training data for
121
+ output_dir (str): Directory to save the training data
122
+
123
+ Returns:
124
+ list: List of file paths containing the training texts
125
+ """
126
+ if model_name not in MODEL_TO_DATASET:
127
+ logger.warning(f"No dataset mapping found for {model_name}. Using default dataset.")
128
+ # Fallback to LibriSpeech as default
129
+ dataset_info = {
130
+ "dataset": "librispeech_asr",
131
+ "subset": "clean",
132
+ "split": "train.100",
133
+ "text_column": "text"
134
+ }
135
+ else:
136
+ dataset_info = MODEL_TO_DATASET[model_name]
137
+
138
+ logger.info(f"Downloading dataset for {model_name}: {dataset_info['dataset']}")
139
+
140
+ try:
141
+ # Load only the text columns for efficiency
142
+ if "subset" in dataset_info:
143
+ dataset = load_dataset(
144
+ dataset_info["dataset"],
145
+ dataset_info["subset"],
146
+ split=dataset_info["split"]
147
+ )
148
+ else:
149
+ dataset = load_dataset(
150
+ dataset_info["dataset"],
151
+ split=dataset_info["split"]
152
+ )
153
+
154
+ # Limit dataset size if specified
155
+ if "max_samples" in dataset_info and len(dataset) > dataset_info["max_samples"]:
156
+ dataset = dataset.shuffle(seed=42).select(range(dataset_info["max_samples"]))
157
+
158
+ # Extract text column
159
+ text_column = dataset_info["text_column"]
160
+ texts = dataset[text_column]
161
+
162
+ # Save texts to file for tokenizer training
163
+ os.makedirs(output_dir, exist_ok=True)
164
+ output_file = os.path.join(output_dir, f"{model_name.lower()}_texts.txt")
165
+
166
+ with open(output_file, "w", encoding="utf-8") as f:
167
+ for text in texts:
168
+ f.write(text.strip() + "\n")
169
+
170
+ logger.info(f"Saved {len(texts)} text samples to {output_file}")
171
+ return [output_file]
172
+
173
+ except Exception as e:
174
+ logger.error(f"Error downloading dataset: {str(e)}")
175
+ return []
176
+
177
+ def save_processor_config(processor, output_dir, lang, model_name, training_files=None):
178
+ """
179
+ Save the processor configuration for later use with the ONNX model.
180
+
181
+ Args:
182
+ processor: The processor object
183
+ output_dir: Directory to save the processor configuration
184
+ lang: Language code
185
+ model_name: Name of the model
186
+ training_files: List of files containing tokenizer training data
187
+ """
188
+ try:
189
+ # Create a config directory
190
+ config_dir = os.path.join(output_dir, f"{lang}_{model_name}")
191
+ os.makedirs(config_dir, exist_ok=True)
192
+
193
+ # Save the processor to the config directory
194
+ processor.save_pretrained(config_dir)
195
+
196
+ # Get tokenizer from processor
197
+ if hasattr(processor, 'tokenizer'):
198
+ trans_tokenizer = processor.tokenizer
199
+ else:
200
+ logger.warning(f"Processor doesn't have tokenizer attribute. Creating default tokenizer.")
201
+ # Create a default tokenizer if not available
202
+ vocab_dict = {"<pad>": 0, "<s>": 1, "</s>": 2, "<unk>": 3, "|": 4}
203
+ with open(os.path.join(config_dir, "vocab.json"), "w") as vocab_file:
204
+ json.dump(vocab_dict, vocab_file)
205
+ trans_tokenizer = Wav2Vec2CTCTokenizer(os.path.join(config_dir, "vocab.json"))
206
+
207
+ # Load existing tokenizer configuration
208
+ vocab_path = os.path.join(config_dir, 'vocab.json')
209
+ tokenizer_config_path = os.path.join(config_dir, 'tokenizer_config.json')
210
+
211
+ with open(vocab_path, 'r') as f:
212
+ vocab = json.load(f)
213
+
214
+ # Load tokenizer configuration to get settings
215
+ with open(tokenizer_config_path, 'r') as f:
216
+ config = json.load(f)
217
+
218
+ # Define special tokens
219
+ special_tokens = [
220
+ config.get("pad_token", "<pad>"),
221
+ config.get("bos_token", "<s>"),
222
+ config.get("eos_token", "</s>"),
223
+ config.get("unk_token", "<unk>"),
224
+ "|" # Word delimiter
225
+ ]
226
+
227
+ # Create a BPE model
228
+ bpe_model = BPE(
229
+ unk_token=config.get("unk_token", "<unk>"),
230
+ continuing_subword_prefix="##",
231
+ fuse_unk=True,
232
+ byte_fallback=True
233
+ )
234
+
235
+ # Create tokenizer with the model
236
+ tokenizer = Tokenizer(bpe_model)
237
+
238
+ # Set word delimiter based on language
239
+ if lang in LANGUAGES_WITHOUT_SPACES:
240
+ # For languages without spaces, use character-level tokenization
241
+ from tokenizers.pre_tokenizers import ByteLevel
242
+ tokenizer.pre_tokenizer = ByteLevel()
243
+ else:
244
+ tokenizer.pre_tokenizer = Metaspace(replacement="|")
245
+
246
+ # Create BPE trainer
247
+ trainer = BpeTrainer(
248
+ vocab_size=len(vocab) if len(vocab) > 100 else 1000,
249
+ min_frequency=2,
250
+ special_tokens=special_tokens,
251
+ initial_alphabet=list("ABCDEFGHIJKLMNOPQRSTUVWXYZ'"),
252
+ show_progress=True
253
+ )
254
+
255
+ # Train the tokenizer if training files are provided
256
+ if training_files and all(os.path.exists(f) for f in training_files):
257
+ logger.info(f"Training tokenizer using {len(training_files)} files")
258
+ tokenizer.train(training_files, trainer)
259
+ else:
260
+ logger.warning("No training files available. Using pre-trained tokenizer only.")
261
+
262
+ # Configure normalization based on settings
263
+ if config.get("do_normalize", False):
264
+ normalizers = []
265
+ if config.get("do_lower_case", False):
266
+ normalizers.append(Lowercase())
267
+ normalizers.append(NFD())
268
+ normalizers.append(StripAccents())
269
+ tokenizer.normalizer = Sequence(normalizers)
270
+
271
+ # Configure post-processing to handle special tokens
272
+ tokenizer.post_processor = TemplateProcessing(
273
+ single=f"{config.get('bos_token', '<s>')} $A {config.get('eos_token', '</s>')}",
274
+ pair=f"{config.get('bos_token', '<s>')} $A {config.get('eos_token', '</s>')} $B {config.get('eos_token', '</s>')}",
275
+ special_tokens=[
276
+ (config.get('bos_token', '<s>'), vocab.get(config.get('bos_token', '<s>'), 1)),
277
+ (config.get('eos_token', '</s>'), vocab.get(config.get('eos_token', '</s>'), 2)),
278
+ ]
279
+ )
280
+
281
+ # Enable padding
282
+ tokenizer.enable_padding(
283
+ pad_id=vocab.get(config.get('pad_token', '<pad>'), 0),
284
+ pad_token=config.get('pad_token', '<pad>')
285
+ )
286
+
287
+ # Save the tokenizer
288
+ tokenizer.save(f"{config_dir}/tokenizer.json")
289
+
290
+ # Create a metadata JSON file with additional information
291
+ metadata = {
292
+ "language": lang,
293
+ "model_name": model_name,
294
+ "processor_type": type(processor).__name__,
295
+ "original_vocab_size": len(vocab),
296
+ "training_files_used": training_files
297
+ }
298
+
299
+ with open(os.path.join(config_dir, "metadata.json"), "w") as f:
300
+ json.dump(metadata, f, indent=2)
301
+
302
+ logger.info(f"Saved processor configuration to {config_dir}")
303
+
304
+ except Exception as e:
305
+ logger.error(f"Error saving processor configuration: {str(e)}")
306
+ raise
307
+
308
+ def convert_torch_model_to_onnx(model_name, lang, output_dir="onnx_models", data_dir="training_data"):
309
+ """
310
+ Convert a torchaudio wav2vec2 model to ONNX format.
311
+
312
+ Args:
313
+ model_name: Name of the torchaudio wav2vec2 model
314
+ lang: Language code
315
+ output_dir: Directory to save the ONNX model
316
+ data_dir: Directory to save training data
317
+ """
318
+ logger.info(f"Converting torch model: {model_name} for language {lang}")
319
+
320
+ # Create output directories
321
+ os.makedirs(output_dir, exist_ok=True)
322
+ os.makedirs(data_dir, exist_ok=True)
323
+
324
+ # Get the corresponding HuggingFace model name
325
+ hf_model_name = TORCH_TO_HF_MAPPING.get(model_name)
326
+ if not hf_model_name:
327
+ raise ValueError(f"No HuggingFace mapping found for torch model {model_name}")
328
+
329
+ # Download tokenizer training data
330
+ training_files = get_tokenizer_training_data(
331
+ model_name,
332
+ os.path.join(data_dir, lang)
333
+ )
334
+
335
+ # Load model from HuggingFace
336
+ try:
337
+ # Try to load processor with different approaches
338
+ processor = None
339
+ try:
340
+ processor = AutoProcessor.from_pretrained(hf_model_name)
341
+ except Exception as e:
342
+ logger.warning(f"Could not load processor with AutoProcessor: {str(e)}")
343
+ try:
344
+ processor = Wav2Vec2Processor.from_pretrained(hf_model_name)
345
+ except Exception as e2:
346
+ logger.warning(f"Could not load processor with Wav2Vec2Processor: {str(e2)}")
347
+ try:
348
+ feature_extractor = AutoFeatureExtractor.from_pretrained(hf_model_name)
349
+ processor = feature_extractor # Use feature extractor as processor
350
+ except Exception as e3:
351
+ logger.warning(f"Could not load processor with AutoFeatureExtractor: {str(e3)}")
352
+ processor = Wav2Vec2FeatureExtractor.from_pretrained(hf_model_name)
353
+
354
+ # Try to load model with different approaches
355
+ model = None
356
+ try:
357
+ model = AutoModelForCTC.from_pretrained(hf_model_name)
358
+ except Exception as e:
359
+ logger.warning(f"Could not load model with AutoModelForCTC: {str(e)}")
360
+ model = Wav2Vec2ForCTC.from_pretrained(hf_model_name)
361
+ except Exception as e:
362
+ logger.error(f"Error loading model {hf_model_name}: {str(e)}")
363
+ raise
364
+
365
+ # Save the processor configuration
366
+ if processor:
367
+ safe_model_name = model_name.lower().replace('_', '-')
368
+ save_processor_config(
369
+ processor,
370
+ output_dir,
371
+ lang,
372
+ safe_model_name,
373
+ training_files=training_files
374
+ )
375
+
376
+ # Set the model to evaluation mode
377
+ model.eval()
378
+
379
+ # Create a dummy input tensor (sampling rate typically 16kHz for 1 second)
380
+ dummy_input = torch.randn(1, 16000)
381
+
382
+ # Define the output file path
383
+ safe_model_name = model_name.lower().replace('_', '-')
384
+ config_dir = os.path.join(output_dir, f"{lang}_{safe_model_name}".lower())
385
+ os.makedirs(config_dir, exist_ok=True)
386
+ output_path = f"{config_dir}/model.onnx"
387
+
388
+ # Export the model to ONNX
389
+ try:
390
+ torch.onnx.export(
391
+ model,
392
+ dummy_input,
393
+ output_path,
394
+ export_params=True,
395
+ opset_version=14,
396
+ do_constant_folding=True,
397
+ input_names=['input'],
398
+ output_names=['output'],
399
+ dynamic_axes={'input': {0: 'batch_size', 1: 'sequence_length'},
400
+ 'output': {0: 'batch_size', 1: 'sequence_length'}}
401
+ )
402
+ logger.info(f"Successfully converted {model_name} to ONNX format. Saved at {output_path}")
403
+ except Exception as e:
404
+ logger.error(f"Error exporting model {model_name} to ONNX: {str(e)}")
405
+ # Try with a different approach for problematic models
406
+ try:
407
+ logger.info("Attempting export with explicit tracing...")
408
+ # Trace the model first
409
+ traced_model = torch.jit.trace(model, dummy_input)
410
+ torch.onnx.export(
411
+ traced_model,
412
+ dummy_input,
413
+ output_path,
414
+ export_params=True,
415
+ opset_version=14,
416
+ do_constant_folding=True,
417
+ input_names=['input'],
418
+ output_names=['output'],
419
+ dynamic_axes={'input': {0: 'batch_size', 1: 'sequence_length'},
420
+ 'output': {0: 'batch_size', 1: 'sequence_length'}}
421
+ )
422
+ logger.info(f"Successfully converted {model_name} to ONNX format using tracing. Saved at {output_path}")
423
+ except Exception as e2:
424
+ logger.error(f"Error exporting traced model {model_name} to ONNX: {str(e2)}")
425
+ raise
426
+
427
+ def main():
428
+ # Define output directories
429
+ output_dir = "wav2vec2_onnx_models"
430
+ data_dir = "tokenizer_training_data"
431
+
432
+ os.makedirs(output_dir, exist_ok=True)
433
+ os.makedirs(data_dir, exist_ok=True)
434
+
435
+ # Set up logging to file
436
+ file_handler = logging.FileHandler(os.path.join(output_dir, "conversion.log"))
437
+ file_handler.setLevel(logging.INFO)
438
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
439
+ file_handler.setFormatter(formatter)
440
+ logger.addHandler(file_handler)
441
+
442
+ logger.info("Starting model conversion process")
443
+
444
+ # Convert torch models
445
+ logger.info("Converting torchaudio models...")
446
+ for lang, model_name in DEFAULT_ALIGN_MODELS_TORCH.items():
447
+ try:
448
+ convert_torch_model_to_onnx(
449
+ model_name,
450
+ lang,
451
+ output_dir=os.path.join(output_dir, "torch"),
452
+ data_dir=data_dir
453
+ )
454
+ except Exception as e:
455
+ logger.error(f"Error converting {model_name} for language {lang}: {str(e)}")
456
+
457
+ if __name__ == "__main__":
458
+ main()