Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions python-api-examples/offline-omnilingual-asr-ctc-v2-decode-files.py
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"
Comment on lines +27 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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"
repo_dir = Path("./sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-v2-int8-2026-02-05")
model = str(repo_dir / "model.int8.onnx")
tokens = str(repo_dir / "tokens.txt")
test_wav_en = str(repo_dir / "test_wavs/en.wav")
test_wav_de = str(repo_dir / "test_wavs/de.wav")
test_wav_fr = str(repo_dir / "test_wavs/fr.wav")
test_wav_es = str(repo_dir / "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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to PEP 8, imports should be at the top of the file. This makes dependencies clear and avoids re-importing librosa every time load_audio is called. Please move import librosa to the top of the script with the other imports.

    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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential division by zero if audio file is empty.

If the audio file exists but contains no samples, audio_duration will be 0, causing a ZeroDivisionError on line 77. For an example script this is a minor edge case, but consider adding a guard.

🛡️ 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
In `@python-api-examples/offline-omnilingual-asr-ctc-v2-decode-files.py` around
lines 65 - 85, The decode_single_file function can divide by zero when the
loaded audio has zero samples; modify decode_single_file to check samples (or
audio_duration) after load_audio and handle the empty case—e.g., if len(samples)
== 0: log/print a clear message including filename and return early (or set
audio_duration to a safe nonzero value) so computing real_time_factor =
elapsed_seconds / audio_duration and the formatted prints cannot raise
ZeroDivisionError; ensure the guard covers both the real_time_factor computation
and any prints that use audio_duration or derived values.



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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same division by zero risk; consider strict=True for zip().

  1. Line 107 has the same potential division by zero issue as decode_single_file() if all audio files are empty.

  2. Per static analysis, adding strict=True to zip() (Python 3.10+) helps catch length mismatches early.

,

♻️ 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: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
In `@python-api-examples/offline-omnilingual-asr-ctc-v2-decode-files.py` around
lines 88 - 119, The decode_multiple_files function can divide by zero if
audio_duration is 0 and should use strict zip to catch mismatched lists; update
decode_multiple_files to guard the real_time_factor calculation by checking if
audio_duration > 0 (or set real_time_factor to float("inf")/0.0-safe value)
before computing elapsed_seconds / audio_duration, and change the for loop that
iterates filenames and streams to use zip(filenames, streams, strict=True) so
length mismatches raise immediately; reference symbols: decode_multiple_files,
audio_duration, real_time_factor, filenames, streams, and the zip(...) call.



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