From 8c764726df69d3ec3bce54127316522c79b779f1 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sat, 28 Feb 2026 15:22:53 +0800 Subject: [PATCH] Add Rust API for Moonshine v2 --- .github/scripts/test-rust.sh | 2 + rust-api-examples/Cargo.lock | 10 +- rust-api-examples/Cargo.toml | 4 +- rust-api-examples/examples/moonshine_v2.rs | 106 ++++++++++++++++++ rust-api-examples/run-moonshine-v2.sh | 17 +++ sherpa-onnx/rust/sherpa-onnx-sys/Cargo.toml | 2 +- .../rust/sherpa-onnx-sys/src/offline_asr.rs | 1 + sherpa-onnx/rust/sherpa-onnx/Cargo.toml | 4 +- .../rust/sherpa-onnx/src/offline_asr.rs | 7 ++ 9 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 rust-api-examples/examples/moonshine_v2.rs create mode 100755 rust-api-examples/run-moonshine-v2.sh diff --git a/.github/scripts/test-rust.sh b/.github/scripts/test-rust.sh index 2d871fdf27..18ccbafb56 100755 --- a/.github/scripts/test-rust.sh +++ b/.github/scripts/test-rust.sh @@ -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 diff --git a/rust-api-examples/Cargo.lock b/rust-api-examples/Cargo.lock index fd8fc9b948..f3c1c4e657 100644 --- a/rust-api-examples/Cargo.lock +++ b/rust-api-examples/Cargo.lock @@ -553,7 +553,7 @@ dependencies = [ [[package]] name = "rust-api-examples" -version = "0.1.8" +version = "0.1.9" dependencies = [ "anyhow", "clap", @@ -621,9 +621,9 @@ dependencies = [ [[package]] name = "sherpa-onnx" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bc662a40347268c9088f8e52f47e39268c0c5da80c4d30c38e40464e6f6fef" +checksum = "2cb3cd528fb1ab43135fd7a68d45720910da8d6c036b97808125a456f1b86ffb" dependencies = [ "serde", "serde_json", @@ -632,9 +632,9 @@ dependencies = [ [[package]] name = "sherpa-onnx-sys" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5d16f9c5d442c96ca2fdfc5f5344fd58c93da7b9182707e5885f98a67f3682" +checksum = "50748be67c68ff33d4c21c934f8d7c7067bb4ca766583864f05b03219a55d4e9" [[package]] name = "slab" diff --git a/rust-api-examples/Cargo.toml b/rust-api-examples/Cargo.toml index 87a644d7a0..5fe939b7bb 100644 --- a/rust-api-examples/Cargo.toml +++ b/rust-api-examples/Cargo.toml @@ -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" } cpal = { version = "0.16", optional = true } # cross-platform audio I/O diff --git a/rust-api-examples/examples/moonshine_v2.rs b/rust-api-examples/examples/moonshine_v2.rs new file mode 100644 index 0000000000..ab53f8fe98 --- /dev/null +++ b/rust-api-examples/examples/moonshine_v2.rs @@ -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, +} + +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()); + 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"); + } +} diff --git a/rust-api-examples/run-moonshine-v2.sh b/rust-api-examples/run-moonshine-v2.sh new file mode 100755 index 0000000000..c963f46ded --- /dev/null +++ b/rust-api-examples/run-moonshine-v2.sh @@ -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 +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 diff --git a/sherpa-onnx/rust/sherpa-onnx-sys/Cargo.toml b/sherpa-onnx/rust/sherpa-onnx-sys/Cargo.toml index 048b8e570b..8ee32c5ccd 100644 --- a/sherpa-onnx/rust/sherpa-onnx-sys/Cargo.toml +++ b/sherpa-onnx/rust/sherpa-onnx-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sherpa-onnx-sys" -version = "0.1.8" +version = "0.1.9" edition = "2021" description = "Raw FFI bindings to the sherpa-onnx C API" license = "Apache-2.0" diff --git a/sherpa-onnx/rust/sherpa-onnx-sys/src/offline_asr.rs b/sherpa-onnx/rust/sherpa-onnx-sys/src/offline_asr.rs index 0d9cf3cce7..6aa5733053 100644 --- a/sherpa-onnx/rust/sherpa-onnx-sys/src/offline_asr.rs +++ b/sherpa-onnx/rust/sherpa-onnx-sys/src/offline_asr.rs @@ -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)] diff --git a/sherpa-onnx/rust/sherpa-onnx/Cargo.toml b/sherpa-onnx/rust/sherpa-onnx/Cargo.toml index 75d48baf70..838e3c8d7f 100644 --- a/sherpa-onnx/rust/sherpa-onnx/Cargo.toml +++ b/sherpa-onnx/rust/sherpa-onnx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sherpa-onnx" -version = "0.1.8" +version = "0.1.9" 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" diff --git a/sherpa-onnx/rust/sherpa-onnx/src/offline_asr.rs b/sherpa-onnx/rust/sherpa-onnx/src/offline_asr.rs index 39b9121ee0..04dd029cfe 100644 --- a/sherpa-onnx/rust/sherpa-onnx/src/offline_asr.rs +++ b/sherpa-onnx/rust/sherpa-onnx/src/offline_asr.rs @@ -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, pub encoder: Option, pub uncached_decoder: Option, pub cached_decoder: Option, + pub merged_decoder: Option, } impl OfflineMoonshineModelConfig { @@ -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), } } }