Initial commit
Browse files- README.md +146 -0
- bench.py +375 -0
- requirements-bench.txt +9 -0
- requirements.txt +21 -0
- 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 |
+
[](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 |
+

|
| 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()
|