Repository navigation
Add support for Meta Omnilingual ASR v2 models #3138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| """ | ||
| This file shows how to use a non-streaming Omnilingual ASR CTC v2 model from | ||
| https://github.com/facebookresearch/omnilingual-asr | ||
| to decode files. | ||
|
|
||
| Please download model files from | ||
| https://huggingface.co/Edison2ST/sherpa-onnx-omnilingual-asr-1600-languages-ctc-v2 | ||
|
|
||
| For instance, | ||
|
|
||
| wget https://huggingface.co/Edison2ST/sherpa-onnx-omnilingual-asr-1600-languages-ctc-v2/resolve/main/sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05.tar.bz2 # noqa: E501 | ||
| tar xvf sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05.tar.bz2 | ||
| rm sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05.tar.bz2 | ||
| """ | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import numpy as np | ||
| import time | ||
| import sherpa_onnx | ||
| import soundfile as sf | ||
|
|
||
|
|
||
| def create_recognizer(): | ||
| model = "./sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05/model.int8.onnx" | ||
| tokens = "./sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05/tokens.txt" | ||
| test_wav_en = "./sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05/test_wavs/en.wav" | ||
| test_wav_de = "./sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05/test_wavs/de.wav" | ||
| test_wav_fr = "./sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05/test_wavs/fr.wav" | ||
| test_wav_es = "./sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05/test_wavs/es.wav" | ||
|
|
||
| for f in [model, tokens, test_wav_en, test_wav_de, test_wav_fr, test_wav_es]: | ||
| if not Path(f).is_file(): | ||
| print(f"{f} does not exist") | ||
|
|
||
| raise ValueError("""Please download model files from | ||
| https://huggingface.co/Edison2ST/sherpa-onnx-omnilingual-asr-1600-languages-ctc-v2 | ||
| """) | ||
|
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The multi-line string for the error message includes leading whitespace from the source code indentation, which might not be intended. Using standard string literal concatenation results in a cleaner error message. raise ValueError(
"Please download model files from "
"https://huggingface.co/Edison2ST/sherpa-onnx-omnilingual-asr-1600-languages-ctc-v2"
) |
||
| return ( | ||
| sherpa_onnx.OfflineRecognizer.from_omnilingual_asr_ctc( | ||
| model=model, | ||
| tokens=tokens, | ||
| num_threads=1, | ||
| ), | ||
| test_wav_en, | ||
| test_wav_de, | ||
| test_wav_fr, | ||
| test_wav_es, | ||
| ) | ||
|
|
||
|
|
||
| def load_audio(filename): | ||
| audio, sample_rate = sf.read(filename, dtype="float32", always_2d=True) | ||
| audio = audio[:, 0] # only use the first channel | ||
| if sample_rate != 16000: | ||
| import librosa | ||
|
|
||
| audio = librosa.resample(audio, orig_sr=sample_rate, target_sr=16000) | ||
|
Comment on lines
+57
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to PEP 8, imports should be at the top of the file. This makes dependencies clear and avoids re-importing if sample_rate != 16000:
audio = librosa.resample(audio, orig_sr=sample_rate, target_sr=16000) |
||
|
|
||
| return np.ascontiguousarray(audio) | ||
|
|
||
|
|
||
| def decode_single_file(recognizer, filename): | ||
| samples = load_audio(filename) | ||
|
|
||
| start_time = time.time() | ||
|
|
||
| stream = recognizer.create_stream() | ||
| stream.accept_waveform(sample_rate=16000, waveform=samples) | ||
| recognizer.decode_stream(stream) | ||
|
|
||
| end_time = time.time() | ||
| elapsed_seconds = end_time - start_time | ||
| audio_duration = len(samples) / 16000 | ||
| real_time_factor = elapsed_seconds / audio_duration | ||
|
|
||
| print("---") | ||
| print(filename) | ||
| print(stream.result) | ||
| print(f"Elapsed seconds: {elapsed_seconds:.3f}") | ||
| print(f"Audio duration in seconds: {audio_duration:.3f}") | ||
| print(f"RTF: {elapsed_seconds:.3f}/{audio_duration:.3f} = {real_time_factor:.3f}") | ||
| print() | ||
|
Comment on lines
+65
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential division by zero if audio file is empty. If the audio file exists but contains no samples, 🛡️ Suggested fix audio_duration = len(samples) / 16000
- real_time_factor = elapsed_seconds / audio_duration
+ real_time_factor = elapsed_seconds / audio_duration if audio_duration > 0 else float('inf')🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def decode_multiple_files(recognizer, filenames): | ||
| streams = [] | ||
|
|
||
| start_time = time.time() | ||
|
|
||
| audio_duration = 0 | ||
|
|
||
| for filename in filenames: | ||
| samples = load_audio(filename) | ||
| audio_duration += len(samples) / 16000 | ||
|
|
||
| stream = recognizer.create_stream() | ||
| stream.accept_waveform(sample_rate=16000, waveform=samples) | ||
| streams.append(stream) | ||
|
|
||
| recognizer.decode_streams(streams) | ||
|
|
||
| end_time = time.time() | ||
| elapsed_seconds = end_time - start_time | ||
| real_time_factor = elapsed_seconds / audio_duration | ||
|
|
||
| for name, stream in zip(filenames, streams): | ||
| print("---") | ||
| print(name) | ||
| print(stream.result) | ||
| print() | ||
|
|
||
| print(f"Elapsed seconds: {elapsed_seconds:.3f}") | ||
| print(f"Audio duration in seconds: {audio_duration:.3f}") | ||
| print(f"RTF: {elapsed_seconds:.3f}/{audio_duration:.3f} = {real_time_factor:.3f}") | ||
| print() | ||
| print() | ||
|
Comment on lines
+88
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same division by zero risk; consider
, ♻️ Suggested improvements- real_time_factor = elapsed_seconds / audio_duration
+ real_time_factor = elapsed_seconds / audio_duration if audio_duration > 0 else float('inf')
- for name, stream in zip(filenames, streams):
+ for name, stream in zip(filenames, streams, strict=True):🧰 Tools🪛 Ruff (0.14.14)[warning] 109-109: Add explicit value for parameter (B905) 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def main(): | ||
| recognizer, *filenames = create_recognizer() | ||
|
|
||
| decode_single_file(recognizer, filenames[0]) | ||
| decode_single_file(recognizer, filenames[1]) | ||
| decode_multiple_files(recognizer, filenames[2:]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file paths for the model, tokens, and test waves are long and repetitive. To improve readability and maintainability, you can define a base directory path using
pathlib.Path. This makes it easier to update the model version in the future.