Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions .github/scripts/test-rust.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ cd rust-api-examples

./run-version.sh

./run-moonshine-v2.sh

./run-fire-red-asr-ctc.sh

./run-silero-vad-remove-silence.sh
Expand Down
10 changes: 5 additions & 5 deletions rust-api-examples/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions rust-api-examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "rust-api-examples"
version = "0.1.8"
version = "0.1.9"
edition = "2021"

[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
sherpa-onnx = "0.1.8"
sherpa-onnx = "0.1.9"
# sherpa-onnx = { path = "../sherpa-onnx/rust/sherpa-onnx" }
Comment on lines +9 to 10

Copilot AI Feb 28, 2026

Copy link

Choose a reason for hiding this comment

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

rust-api-examples depends on sherpa-onnx from crates.io (not the in-repo path dependency). That means CI for this repo won’t exercise the code changes in sherpa-onnx/rust/…, and it also assumes 0.1.9 is 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.

Suggested change
sherpa-onnx = "0.1.9"
# sherpa-onnx = { path = "../sherpa-onnx/rust/sherpa-onnx" }
# sherpa-onnx = "0.1.9"
sherpa-onnx = { path = "../sherpa-onnx/rust/sherpa-onnx" }

Copilot uses AI. Check for mistakes.

cpal = { version = "0.16", optional = true } # cross-platform audio I/O
Expand Down
106 changes: 106 additions & 0 deletions rust-api-examples/examples/moonshine_v2.rs
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

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

Validate num_threads before using it.

Line 41-Line 42 accepts any i32, and Line 59 forwards it directly. Reject <= 0 to prevent invalid recognizer settings.

🔧 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
Verify each finding against the current code and only fix it if needed.

In `@rust-api-examples/examples/moonshine_v2.rs` around lines 41 - 42, Validate
the command-line field num_threads (the struct field named num_threads with
#[arg(...)]) before it is forwarded to the recognizer creation call where it's
used later (the use at the site that forwards num_threads around line 59);
reject or handle values <= 0 instead of passing them through. Add a check right
after parsing CLI args (or immediately before the recognizer is constructed)
that returns a clear error/exit or clamps to a safe minimum if num_threads <= 0,
and convert the validated positive value to the expected unsigned type (usize)
before passing it into the recognizer creation/initialization call.

}

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

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

To improve performance and adhere to Rust's ownership principles, you can avoid cloning these String values. Since the args struct is not used after this configuration block, you can move the values directly into the recognizer_config. This prevents unnecessary memory allocations.

Suggested change
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());
recognizer_config.model_config.moonshine.encoder = Some(args.encoder);
recognizer_config.model_config.moonshine.merged_decoder = Some(args.decoder);
recognizer_config.model_config.tokens = Some(args.tokens);
recognizer_config.model_config.provider = Some(args.provider);

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

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 | 🟠 Major

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
eprintln!("Failed to get recognition result");
}
} else {
eprintln!("Failed to get recognition result");
std::process::exit(1);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust-api-examples/examples/moonshine_v2.rs` around lines 103 - 105, The
example currently only prints an error when decoding fails (the else branch that
calls eprintln!("Failed to get recognition result")), which allows the program
to exit with code 0; update that failure branch to terminate with a non-zero
exit status (for example call std::process::exit(1) or return an Err from main)
so CI sees the failure—locate the else block around the recognition result
handling in moonshine_v2.rs and replace the simple eprintln! with an error log
plus a non-zero exit/Err return.

}
17 changes: 17 additions & 0 deletions rust-api-examples/run-moonshine-v2.sh
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

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 | 🟠 Major

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 tar xvf.

🔧 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
Verify each finding against the current code and only fix it if needed.

In `@rust-api-examples/run-moonshine-v2.sh` around lines 7 - 9, Add a SHA-256
integrity check for the downloaded artifact before extraction: after the curl
download command (the line that fetches
sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2) compute the file's
SHA-256 (using sha256sum or shasum -a 256) and compare it against a pinned
expected checksum constant; if the checksums do not match, print an error and
exit non‑zero so the subsequent tar xvf step is never run, otherwise proceed to
tar and then remove the archive. Ensure the check is fail-fast (exit on
mismatch) and references the exact filename used in the curl/tar commands so it
cannot be bypassed.

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
2 changes: 1 addition & 1 deletion sherpa-onnx/rust/sherpa-onnx-sys/Cargo.toml
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"

Copilot AI Feb 28, 2026

Copy link

Choose a reason for hiding this comment

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

This release adds a new public field to the OfflineMoonshineModelConfig FFI struct, which is a breaking API change for downstream users of sherpa-onnx-sys. Consider using a breaking-change version bump (or documenting the semver policy for 0.x) before publishing.

Suggested change
version = "0.1.9"
version = "0.2.0"

Copilot uses AI. Check for mistakes.
edition = "2021"
description = "Raw FFI bindings to the sherpa-onnx C API"
license = "Apache-2.0"
Expand Down
1 change: 1 addition & 0 deletions sherpa-onnx/rust/sherpa-onnx-sys/src/offline_asr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub struct OfflineMoonshineModelConfig {
pub encoder: *const c_char,
pub uncached_decoder: *const c_char,
pub cached_decoder: *const c_char,
pub merged_decoder: *const c_char,
}

#[repr(C)]
Expand Down
4 changes: 2 additions & 2 deletions sherpa-onnx/rust/sherpa-onnx/Cargo.toml
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

Copilot AI Feb 28, 2026

Copy link

Choose a reason for hiding this comment

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

OfflineMoonshineModelConfig gained a new public field (merged_decoder), which is a breaking change for downstream crates that construct the struct via a literal or destructure it in patterns. Consider bumping the crate version with a breaking-change increment (e.g., 0.2.0) or otherwise providing a backwards-compatible migration path before publishing.

Copilot uses AI. Check for mistakes.
edition = "2021"
description = "Safe Rust wrapper for sherpa-onnx speech recognition toolkit"
license = "Apache-2.0"
Expand All @@ -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"
7 changes: 7 additions & 0 deletions sherpa-onnx/rust/sherpa-onnx/src/offline_asr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,18 @@ impl OfflineFireRedAsrModelConfig {
}
}

/// For Moonshine v1, you need 4 models:
/// - preprocessor, encoder, uncached_decoder, cached_decoder
///
/// For Moonshine v2, you need 2 models:
/// - encoder, merged_decoder
#[derive(Clone, Debug, Default)]
pub struct OfflineMoonshineModelConfig {
pub preprocessor: Option<String>,
pub encoder: Option<String>,
pub uncached_decoder: Option<String>,
pub cached_decoder: Option<String>,
pub merged_decoder: Option<String>,
}

impl OfflineMoonshineModelConfig {
Expand All @@ -122,6 +128,7 @@ impl OfflineMoonshineModelConfig {
encoder: to_c_ptr(&self.encoder, cstrings),
uncached_decoder: to_c_ptr(&self.uncached_decoder, cstrings),
cached_decoder: to_c_ptr(&self.cached_decoder, cstrings),
merged_decoder: to_c_ptr(&self.merged_decoder, cstrings),
}
}
}
Expand Down