Repository navigation
Add Rust API for Moonshine v2 #3247
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||||||||||||||||||
| // Copyright (c) 2026 Xiaomi Corporation | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // This file demonstrates how to use a Moonshine v2 model with sherpa-onnx's Rust API | ||||||||||||||||||||||
| // for offline speech recognition. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // See ../README.md for how to run it. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| use clap::Parser; | ||||||||||||||||||||||
| use sherpa_onnx::{OfflineRecognizer, OfflineRecognizerConfig, Wave}; | ||||||||||||||||||||||
| use std::time::Instant; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Moonshine v2 offline example | ||||||||||||||||||||||
| #[derive(Parser, Debug)] | ||||||||||||||||||||||
| #[command(author, version, about, long_about = None)] | ||||||||||||||||||||||
| struct Args { | ||||||||||||||||||||||
| /// Path to WAV file | ||||||||||||||||||||||
| #[arg(long)] | ||||||||||||||||||||||
| wav: String, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Path to the encoder model | ||||||||||||||||||||||
| #[arg(long)] | ||||||||||||||||||||||
| encoder: String, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Path to the decoder model | ||||||||||||||||||||||
| #[arg(long)] | ||||||||||||||||||||||
| decoder: String, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Path to tokens file | ||||||||||||||||||||||
| #[arg(long)] | ||||||||||||||||||||||
| tokens: String, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Provider (default: cpu) | ||||||||||||||||||||||
| #[arg(long, default_value = "cpu")] | ||||||||||||||||||||||
| provider: String, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Enable debug logs | ||||||||||||||||||||||
| #[arg(long, default_value_t = false)] | ||||||||||||||||||||||
| debug: bool, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Number of threads | ||||||||||||||||||||||
| #[arg(long, default_value_t = 2)] | ||||||||||||||||||||||
| num_threads: i32, | ||||||||||||||||||||||
|
Comment on lines
+41
to
+42
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. Validate Line 41-Line 42 accepts any 🔧 Proposed fix fn main() {
let args = Args::parse();
+ if args.num_threads <= 0 {
+ eprintln!("--num-threads must be > 0");
+ std::process::exit(2);
+ }
let wave = Wave::read(&args.wav).expect("Failed to read WAV file");Also applies to: 59-59 🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| fn main() { | ||||||||||||||||||||||
| let args = Args::parse(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let wave = Wave::read(&args.wav).expect("Failed to read WAV file"); | ||||||||||||||||||||||
| let audio_duration = wave.samples().len() as f64 / wave.sample_rate() as f64; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let mut recognizer_config = OfflineRecognizerConfig::default(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| recognizer_config.model_config.moonshine.encoder = Some(args.encoder.clone()); | ||||||||||||||||||||||
| recognizer_config.model_config.moonshine.merged_decoder = Some(args.decoder.clone()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| recognizer_config.model_config.tokens = Some(args.tokens.clone()); | ||||||||||||||||||||||
| recognizer_config.model_config.provider = Some(args.provider.clone()); | ||||||||||||||||||||||
|
Comment on lines
+53
to
+57
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. To improve performance and adhere to Rust's ownership principles, you can avoid cloning these
Suggested change
|
||||||||||||||||||||||
| recognizer_config.model_config.debug = args.debug; | ||||||||||||||||||||||
| recognizer_config.model_config.num_threads = args.num_threads; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Measure recognizer creation time | ||||||||||||||||||||||
| println!("Creating recognizer ..."); | ||||||||||||||||||||||
| let start_creation = Instant::now(); | ||||||||||||||||||||||
| let recognizer = | ||||||||||||||||||||||
| OfflineRecognizer::create(&recognizer_config).expect("Failed to create OfflineRecognizer"); | ||||||||||||||||||||||
| let creation_elapsed = start_creation.elapsed().as_secs_f64(); | ||||||||||||||||||||||
| println!("Recognizer created in {:.3} seconds.", creation_elapsed); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let stream = recognizer.create_stream(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Measure recognition time | ||||||||||||||||||||||
| let start_recognition = Instant::now(); | ||||||||||||||||||||||
| stream.accept_waveform(wave.sample_rate(), wave.samples()); | ||||||||||||||||||||||
| recognizer.decode(&stream); | ||||||||||||||||||||||
| let recognition_elapsed = start_recognition.elapsed().as_secs_f64(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Get recognition result | ||||||||||||||||||||||
| if let Some(result) = stream.get_result() { | ||||||||||||||||||||||
| println!("Decoded text: {}", result.text); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let total_time = creation_elapsed + recognition_elapsed; | ||||||||||||||||||||||
| let rtf = recognition_elapsed / audio_duration; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| println!("\n=== Performance Summary ==="); | ||||||||||||||||||||||
| println!("Audio duration : {:.3} seconds", audio_duration); | ||||||||||||||||||||||
| println!("Recognizer creation time: {:.3} seconds", creation_elapsed); | ||||||||||||||||||||||
| println!( | ||||||||||||||||||||||
| "Recognition time : {:.3} seconds", | ||||||||||||||||||||||
| recognition_elapsed | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| println!("Total elapsed time : {:.3} seconds", total_time); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Detailed RTF computation log | ||||||||||||||||||||||
| println!( | ||||||||||||||||||||||
| "Real-Time Factor (RTF) : {:.3} (recognition_elapsed / audio_duration = {:.3} / {:.3})", | ||||||||||||||||||||||
| rtf, recognition_elapsed, audio_duration | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| println!( | ||||||||||||||||||||||
| "Number of threads : {}", | ||||||||||||||||||||||
| recognizer_config.model_config.num_threads | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| eprintln!("Failed to get recognition result"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+103
to
+105
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. Return a non-zero exit code when decoding fails. Line 103-Line 105 only prints an error, so this example can still exit successfully and mask failures in CI. 🔧 Proposed fix- } else {
- eprintln!("Failed to get recognition result");
- }
+ } else {
+ eprintln!("Failed to get recognition result");
+ std::process::exit(1);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #!/usr/bin/env bash | ||
| set -ex | ||
|
|
||
| # see | ||
| # https://k2-fsa.github.io/sherpa/onnx/moonshine | ||
| if [ ! -f ./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/encoder_model.ort ]; then | ||
| curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2 | ||
| tar xvf sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2 | ||
| rm sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2 | ||
|
Comment on lines
+7
to
+9
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. Verify artifact integrity before extraction. Line 7-Line 9 downloads and untars a remote archive without checksum verification. Please add a pinned SHA-256 check before 🔧 Proposed hardening sketch- curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
- tar xvf sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
+ curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
+ echo "<expected_sha256> sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2" | sha256sum -c -
+ tar xvf sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2🤖 Prompt for AI Agents |
||
| fi | ||
|
|
||
| cargo run --example moonshine_v2 -- \ | ||
| --wav ./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/test_wavs/0.wav \ | ||
| --encoder ./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/encoder_model.ort \ | ||
| --decoder ./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/decoder_model_merged.ort \ | ||
| --tokens ./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/tokens.txt \ | ||
| --num-threads 2 | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||
| [package] | ||||||
| name = "sherpa-onnx-sys" | ||||||
| version = "0.1.8" | ||||||
| version = "0.1.9" | ||||||
|
||||||
| version = "0.1.9" | |
| version = "0.2.0" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| [package] | ||
| name = "sherpa-onnx" | ||
| version = "0.1.8" | ||
| version = "0.1.9" | ||
|
Comment on lines
1
to
+3
|
||
| edition = "2021" | ||
| description = "Safe Rust wrapper for sherpa-onnx speech recognition toolkit" | ||
| license = "Apache-2.0" | ||
|
|
@@ -20,6 +20,6 @@ include = [ | |
| ] | ||
|
|
||
| [dependencies] | ||
| sherpa-onnx-sys = { path = "../sherpa-onnx-sys", version = "0.1.8" } | ||
| sherpa-onnx-sys = { path = "../sherpa-onnx-sys", version = "0.1.9" } | ||
| serde = { version = "1.0", features = ["derive"] } | ||
| serde_json = "1.0" | ||
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.
rust-api-examplesdepends onsherpa-onnxfrom crates.io (not the in-repo path dependency). That means CI for this repo won’t exercise the code changes insherpa-onnx/rust/…, and it also assumes0.1.9is already published. If the goal is to test the PR’s code, consider switching this to a path dependency in CI (or using a[patch.crates-io]override) and only using the registry version for end-user examples/releases.