Skip to content

Commit 6ff3ce7

Browse files
authored
Add more doc for Rust API (k2-fsa#3378)
1 parent c850399 commit 6ff3ce7

11 files changed

Lines changed: 345 additions & 194 deletions

sherpa-onnx/rust/sherpa-onnx/src/kws.rs

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
1+
//! Streaming keyword spotting.
2+
//!
3+
//! This module detects predefined or per-stream override keywords from an
4+
//! online ASR model. See
5+
//! [`rust-api-examples/examples/keyword_spotter.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/keyword_spotter.rs)
6+
//! for a complete example.
7+
//!
8+
//! # Example
9+
//!
10+
//! ```no_run
11+
//! use sherpa_onnx::{KeywordSpotter, KeywordSpotterConfig, Wave};
12+
//!
13+
//! let wave = Wave::read("./test.wav").expect("read wave");
14+
//! let mut config = KeywordSpotterConfig::default();
15+
//! config.model_config.transducer.encoder = Some("./kws/encoder.onnx".into());
16+
//! config.model_config.transducer.decoder = Some("./kws/decoder.onnx".into());
17+
//! config.model_config.transducer.joiner = Some("./kws/joiner.onnx".into());
18+
//! config.model_config.tokens = Some("./kws/tokens.txt".into());
19+
//! config.keywords_file = Some("./keywords.txt".into());
20+
//!
21+
//! let kws = KeywordSpotter::create(&config).expect("create keyword spotter");
22+
//! let stream = kws.create_stream();
23+
//! stream.accept_waveform(wave.sample_rate(), wave.samples());
24+
//! stream.input_finished();
25+
//!
26+
//! while kws.is_ready(&stream) {
27+
//! kws.decode(&stream);
28+
//! }
29+
//!
30+
//! if let Some(result) = kws.get_result(&stream) {
31+
//! println!("{}", result.keyword);
32+
//! }
33+
//! ```
34+
135
use crate::online_asr::{OnlineModelConfig, OnlineStream};
236
use crate::utils::to_c_ptr;
337
use sherpa_onnx_sys as sys;
@@ -8,11 +42,16 @@ fn c_ptr_to_string(ptr: *const c_char) -> String {
842
if ptr.is_null() {
943
String::new()
1044
} else {
11-
unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }
45+
unsafe {
46+
CStr::from_ptr(ptr)
47+
.to_string_lossy()
48+
.into_owned()
49+
}
1250
}
1351
}
1452

1553
#[derive(Clone, Debug)]
54+
/// Configuration for [`KeywordSpotter`].
1655
pub struct KeywordSpotterConfig {
1756
pub feat_config: sys::FeatureConfig,
1857
pub model_config: OnlineModelConfig,
@@ -46,19 +85,25 @@ impl KeywordSpotterConfig {
4685
fn to_sys(&self, cstrings: &mut Vec<CString>) -> sys::KeywordSpotterConfig {
4786
sys::KeywordSpotterConfig {
4887
feat_config: self.feat_config,
49-
model_config: self.model_config.to_sys(cstrings),
88+
model_config: self
89+
.model_config
90+
.to_sys(cstrings),
5091
max_active_paths: self.max_active_paths,
5192
num_trailing_blanks: self.num_trailing_blanks,
5293
keywords_score: self.keywords_score,
5394
keywords_threshold: self.keywords_threshold,
5495
keywords_file: to_c_ptr(&self.keywords_file, cstrings),
5596
keywords_buf: to_c_ptr(&self.keywords_buf, cstrings),
56-
keywords_buf_size: self.keywords_buf.as_ref().map_or(0, |s| s.len() as i32),
97+
keywords_buf_size: self
98+
.keywords_buf
99+
.as_ref()
100+
.map_or(0, |s| s.len() as i32),
57101
}
58102
}
59103
}
60104

61105
#[derive(Clone, Debug)]
106+
/// Decoded keyword spotting result for one stream.
62107
pub struct KeywordResult {
63108
pub keyword: String,
64109
pub tokens: String,
@@ -68,13 +113,15 @@ pub struct KeywordResult {
68113
pub json: String,
69114
}
70115

116+
/// Streaming keyword spotter.
71117
pub struct KeywordSpotter {
72118
ptr: *const sys::KeywordSpotter,
73119
}
74120

75121
unsafe impl Send for KeywordSpotter {}
76122

77123
impl KeywordSpotter {
124+
/// Create a keyword spotter from [`KeywordSpotterConfig`].
78125
pub fn create(config: &KeywordSpotterConfig) -> Option<Self> {
79126
let mut cstrings = Vec::new();
80127
let sys_config = config.to_sys(&mut cstrings);
@@ -86,38 +133,47 @@ impl KeywordSpotter {
86133
}
87134
}
88135

136+
/// Create a stream that uses the keywords configured in [`KeywordSpotterConfig`].
89137
pub fn create_stream(&self) -> OnlineStream {
90138
let ptr = unsafe { sys::SherpaOnnxCreateKeywordStream(self.ptr) };
91139
OnlineStream { ptr }
92140
}
93141

142+
/// Create a stream that uses `keywords` instead of the configured keyword list.
94143
pub fn create_stream_with_keywords(&self, keywords: &str) -> OnlineStream {
95144
let keywords = CString::new(keywords).unwrap();
96-
let ptr = unsafe {
97-
sys::SherpaOnnxCreateKeywordStreamWithKeywords(self.ptr, keywords.as_ptr())
98-
};
145+
let ptr =
146+
unsafe { sys::SherpaOnnxCreateKeywordStreamWithKeywords(self.ptr, keywords.as_ptr()) };
99147
OnlineStream { ptr }
100148
}
101149

150+
/// Return `true` if `stream` has enough audio for another decode step.
102151
pub fn is_ready(&self, stream: &OnlineStream) -> bool {
103152
unsafe { sys::SherpaOnnxIsKeywordStreamReady(self.ptr, stream.ptr) != 0 }
104153
}
105154

155+
/// Decode one incremental step for `stream`.
106156
pub fn decode(&self, stream: &OnlineStream) {
107157
unsafe { sys::SherpaOnnxDecodeKeywordStream(self.ptr, stream.ptr) }
108158
}
109159

160+
/// Decode multiple streams in one batch.
110161
pub fn decode_multiple_streams(&self, streams: &[&OnlineStream]) {
111-
let ptrs: Vec<*const sys::OnlineStream> = streams.iter().map(|s| s.ptr).collect();
162+
let ptrs: Vec<*const sys::OnlineStream> = streams
163+
.iter()
164+
.map(|s| s.ptr)
165+
.collect();
112166
unsafe {
113167
sys::SherpaOnnxDecodeMultipleKeywordStreams(self.ptr, ptrs.as_ptr(), ptrs.len() as i32)
114168
}
115169
}
116170

171+
/// Reset the detector state for `stream`.
117172
pub fn reset(&self, stream: &OnlineStream) {
118173
unsafe { sys::SherpaOnnxResetKeywordStream(self.ptr, stream.ptr) }
119174
}
120175

176+
/// Get the structured keyword spotting result for `stream`.
121177
pub fn get_result(&self, stream: &OnlineStream) -> Option<KeywordResult> {
122178
unsafe {
123179
let p = sys::SherpaOnnxGetKeywordResult(self.ptr, stream.ptr);
@@ -126,7 +182,11 @@ impl KeywordSpotter {
126182
}
127183

128184
let result = &*p;
129-
let tokens_arr = if result.tokens_arr.is_null() || result.count <= 0 {
185+
let tokens_arr = if result
186+
.tokens_arr
187+
.is_null()
188+
|| result.count <= 0
189+
{
130190
Vec::new()
131191
} else {
132192
slice::from_raw_parts(result.tokens_arr, result.count as usize)
@@ -135,7 +195,11 @@ impl KeywordSpotter {
135195
.collect()
136196
};
137197

138-
let timestamps = if result.timestamps.is_null() || result.count <= 0 {
198+
let timestamps = if result
199+
.timestamps
200+
.is_null()
201+
|| result.count <= 0
202+
{
139203
Vec::new()
140204
} else {
141205
slice::from_raw_parts(result.timestamps, result.count as usize).to_vec()
@@ -155,14 +219,17 @@ impl KeywordSpotter {
155219
}
156220
}
157221

222+
/// Get the result for `stream` as a JSON string.
158223
pub fn get_result_as_json(&self, stream: &OnlineStream) -> Option<String> {
159224
unsafe {
160225
let p = sys::SherpaOnnxGetKeywordResultAsJson(self.ptr, stream.ptr);
161226
if p.is_null() {
162227
return None;
163228
}
164229

165-
let ans = CStr::from_ptr(p).to_string_lossy().into_owned();
230+
let ans = CStr::from_ptr(p)
231+
.to_string_lossy()
232+
.into_owned();
166233
sys::SherpaOnnxFreeKeywordResultJson(p);
167234
Some(ans)
168235
}
@@ -172,7 +239,10 @@ impl KeywordSpotter {
172239
impl Drop for KeywordSpotter {
173240
fn drop(&mut self) {
174241
unsafe {
175-
if !self.ptr.is_null() {
242+
if !self
243+
.ptr
244+
.is_null()
245+
{
176246
sys::SherpaOnnxDestroyKeywordSpotter(self.ptr);
177247
}
178248
}

sherpa-onnx/rust/sherpa-onnx/src/lib.rs

Lines changed: 4 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
//! - [`pocket_tts.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/pocket_tts.rs)
3838
//! - [`silero_vad_remove_silence.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/silero_vad_remove_silence.rs)
3939
//! - [`online_punctuation.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/online_punctuation.rs)
40+
//! - [`offline_punctuation.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/offline_punctuation.rs)
41+
//! - [`keyword_spotter.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/keyword_spotter.rs)
42+
//! - [`spoken_language_identification.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/spoken_language_identification.rs)
4043
//! - [`offline_speaker_diarization.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/offline_speaker_diarization.rs)
44+
//! - [`speaker_embedding_manager.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/speaker_embedding_manager.rs)
4145
//!
4246
//! # Offline recognition example
4347
//!
@@ -130,140 +134,6 @@
130134
//! let tts = OfflineTts::create(&config).expect("create tts");
131135
//! println!("{}", tts.sample_rate());
132136
//! ```
133-
//! Safe Rust bindings for the public sherpa-onnx inference APIs.
134-
//!
135-
//! This crate wraps the sherpa-onnx C API with RAII-owned Rust types and
136-
//! idiomatic configuration structs. The main feature families are:
137-
//!
138-
//! - offline ASR through [`OfflineRecognizer`]
139-
//! - streaming ASR through [`OnlineRecognizer`]
140-
//! - offline text-to-speech through [`OfflineTts`]
141-
//! - voice activity detection through [`VoiceActivityDetector`]
142-
//! - speaker embeddings and diarization
143-
//! - online punctuation
144-
//! - offline and streaming speech denoising
145-
//! - audio tagging
146-
//! - WAV I/O helpers through [`Wave`] and [`write()`]
147-
//!
148-
//! # How the Rust API is organized
149-
//!
150-
//! Most APIs follow the same pattern:
151-
//!
152-
//! 1. Start with a `*Config` value and fill the fields for exactly one model
153-
//! family.
154-
//! 2. Call `create()` to construct the runtime object.
155-
//! 3. Create a stream if the API is stream-based.
156-
//! 4. Feed audio or text, then fetch results with the provided accessor methods.
157-
//!
158-
//! All runtime wrappers automatically free their underlying C resources on drop.
159-
//!
160-
//! # Examples
161-
//!
162-
//! The repository contains end-to-end Rust examples under
163-
//! [`rust-api-examples/examples/`](https://github.com/k2-fsa/sherpa-onnx/tree/master/rust-api-examples/examples).
164-
//! Good entry points are:
165-
//!
166-
//! - [`sense_voice.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/sense_voice.rs)
167-
//! - [`nemo_parakeet.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/nemo_parakeet.rs)
168-
//! - [`streaming_zipformer.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/streaming_zipformer.rs)
169-
//! - [`pocket_tts.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/pocket_tts.rs)
170-
//! - [`silero_vad_remove_silence.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/silero_vad_remove_silence.rs)
171-
//! - [`online_punctuation.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/online_punctuation.rs)
172-
//! - [`offline_speaker_diarization.rs`](https://github.com/k2-fsa/sherpa-onnx/blob/master/rust-api-examples/examples/offline_speaker_diarization.rs)
173-
//!
174-
//! # Offline recognition example
175-
//!
176-
//! ```no_run
177-
//! use sherpa_onnx::{
178-
//! OfflineRecognizer, OfflineRecognizerConfig, OfflineSenseVoiceModelConfig, Wave,
179-
//! };
180-
//!
181-
//! let wave = Wave::read("./test.wav").expect("read wave");
182-
//!
183-
//! let mut config = OfflineRecognizerConfig::default();
184-
//! config.model_config.sense_voice = OfflineSenseVoiceModelConfig {
185-
//! model: Some(
186-
//! "./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17-int8/model.int8.onnx".into(),
187-
//! ),
188-
//! language: Some("auto".into()),
189-
//! use_itn: true,
190-
//! };
191-
//! config.model_config.tokens = Some(
192-
//! "./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17-int8/tokens.txt".into(),
193-
//! );
194-
//!
195-
//! let recognizer = OfflineRecognizer::create(&config).expect("create recognizer");
196-
//! let stream = recognizer.create_stream();
197-
//! stream.accept_waveform(wave.sample_rate(), wave.samples());
198-
//! recognizer.decode(&stream);
199-
//!
200-
//! let result = stream.get_result().expect("result");
201-
//! println!("{}", result.text);
202-
//! ```
203-
//!
204-
//! # Streaming recognition example
205-
//!
206-
//! ```no_run
207-
//! use sherpa_onnx::{OnlineRecognizer, OnlineRecognizerConfig, Wave};
208-
//!
209-
//! let wave = Wave::read("./test.wav").expect("read wave");
210-
//!
211-
//! let mut config = OnlineRecognizerConfig::default();
212-
//! config.model_config.transducer.encoder = Some(
213-
//! "./sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/encoder-epoch-99-avg-1.int8.onnx".into(),
214-
//! );
215-
//! config.model_config.transducer.decoder = Some(
216-
//! "./sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/decoder-epoch-99-avg-1.onnx".into(),
217-
//! );
218-
//! config.model_config.transducer.joiner = Some(
219-
//! "./sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/joiner-epoch-99-avg-1.int8.onnx".into(),
220-
//! );
221-
//! config.model_config.tokens = Some(
222-
//! "./sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/tokens.txt".into(),
223-
//! );
224-
//! config.enable_endpoint = true;
225-
//! config.decoding_method = Some("greedy_search".into());
226-
//!
227-
//! let recognizer = OnlineRecognizer::create(&config).expect("create recognizer");
228-
//! let stream = recognizer.create_stream();
229-
//! stream.accept_waveform(wave.sample_rate(), wave.samples());
230-
//! stream.input_finished();
231-
//! while recognizer.is_ready(&stream) {
232-
//! recognizer.decode(&stream);
233-
//! }
234-
//! ```
235-
//!
236-
//! # TTS example
237-
//!
238-
//! ```no_run
239-
//! use sherpa_onnx::{
240-
//! OfflineTts, OfflineTtsConfig, OfflineTtsModelConfig, OfflineTtsPocketModelConfig,
241-
//! };
242-
//!
243-
//! let config = OfflineTtsConfig {
244-
//! model: OfflineTtsModelConfig {
245-
//! pocket: OfflineTtsPocketModelConfig {
246-
//! lm_flow: Some("./sherpa-onnx-pocket-tts-int8-2026-01-26/lm_flow.int8.onnx".into()),
247-
//! lm_main: Some("./sherpa-onnx-pocket-tts-int8-2026-01-26/lm_main.int8.onnx".into()),
248-
//! encoder: Some("./sherpa-onnx-pocket-tts-int8-2026-01-26/encoder.onnx".into()),
249-
//! decoder: Some("./sherpa-onnx-pocket-tts-int8-2026-01-26/decoder.int8.onnx".into()),
250-
//! text_conditioner: Some(
251-
//! "./sherpa-onnx-pocket-tts-int8-2026-01-26/text_conditioner.onnx".into(),
252-
//! ),
253-
//! vocab_json: Some("./sherpa-onnx-pocket-tts-int8-2026-01-26/vocab.json".into()),
254-
//! token_scores_json: Some(
255-
//! "./sherpa-onnx-pocket-tts-int8-2026-01-26/token_scores.json".into(),
256-
//! ),
257-
//! ..Default::default()
258-
//! },
259-
//! ..Default::default()
260-
//! },
261-
//! ..Default::default()
262-
//! };
263-
//!
264-
//! let tts = OfflineTts::create(&config).expect("create tts");
265-
//! println!("{}", tts.sample_rate());
266-
//! ```
267137
mod audio_tagging;
268138
mod display;
269139
mod kws;

sherpa-onnx/rust/sherpa-onnx/src/offline_asr.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,9 @@ impl OfflineStream {
673673
if p.is_null() {
674674
String::new()
675675
} else {
676-
CStr::from_ptr(p).to_string_lossy().into_owned()
676+
CStr::from_ptr(p)
677+
.to_string_lossy()
678+
.into_owned()
677679
}
678680
}
679681
}

0 commit comments

Comments
 (0)