Add livekit-wakeword crate with ONNX-based wake word detection - #926
Conversation
|
The ONNX files should be stored in Git LFS: git lfs track "*.onnx" |
ladvoc
left a comment
There was a problem hiding this comment.
I have left some initial comments. Overall, looking good so far.
1egoman
left a comment
There was a problem hiding this comment.
Generally looks good by eye to me!
| fn resample_to_16k(&mut self, samples: &[i16]) -> Result<Vec<f32>, WakeWordError> { | ||
| let rs = self.resampler.as_mut().unwrap(); | ||
| let chunk_in = rs.fft.chunk_size_input(); | ||
| let chunk_out = rs.fft.chunk_size_output(); | ||
|
|
||
| // Expected output length based on ratio | ||
| let expected_len = (samples.len() as f64 * 16000.0 / rs.input_rate as f64).round() as usize; | ||
|
|
||
| let mut output = Vec::with_capacity(expected_len); | ||
|
|
||
| let mut pos = 0; | ||
| while pos < samples.len() { | ||
| let remaining = samples.len() - pos; | ||
| let n = remaining.min(chunk_in); | ||
|
|
||
| // Fill input buffer, zero-pad if last chunk is short | ||
| for (i, v) in rs.input_buf.iter_mut().enumerate() { | ||
| *v = if i < n { samples[pos + i] as f32 / 32768.0 } else { 0.0 }; | ||
| } | ||
|
|
||
| rs.fft.resample(&rs.input_buf.clone(), &mut rs.output_buf)?; | ||
|
|
||
| let take = if remaining < chunk_in { | ||
| // Last (partial) chunk: scale output proportionally | ||
| (n as f64 * chunk_out as f64 / chunk_in as f64).round() as usize | ||
| } else { | ||
| chunk_out | ||
| }; | ||
|
|
||
| output.extend_from_slice(&rs.output_buf[..take]); | ||
|
|
||
| pos += chunk_in; | ||
| } | ||
|
|
||
| output.truncate(expected_len); | ||
| Ok(output) | ||
| } |
There was a problem hiding this comment.
thought: Is it worth using any of the pre-existing webrtc resampling functions like this?
Mostly asking because I'm guessing this needs to be relatively performant if it is going to be running on edge devices for wake word detection and the pre-existing webrtc algorithms might be taking advantage of some platform specific optimizations which could result in a nice speedup.
There was a problem hiding this comment.
we defin can
i used resampler from @ladvoc's suggestion #926 (comment)
i don't have an opinion on which one to use, can run a simple benchmark for perf
There was a problem hiding this comment.
Ah, I didn't see @ladvoc's comment and it largely is the same thing, ok cool makes sense! I hadn't considered the portability argument / maybe not wanting to link to libwebrtc.
Something that came to mind (maybe a bad idea / overengineering though) - make a WakeWordResampler trait, make a default resampler implementation using your resampler code, and put a generic in the WakeWordModel type so it effectively becomes WakeWordModel<impl WakeWordResampler>. fn new() could then have a regular version with just a model param which returns WakeWordModel<NoopResampler> and also maybe like a fn new_with_sample_rate(model, sample_rate) version which returns WakeWordModel<DefaultResampler>. That leaves the door open for other resampling approaches with different properties (ie, NoopResampler, LibWebrtcResampler, etc) and the ability to use typestates to change WakeWordModel method signatures down the line which could be interesting.
There was a problem hiding this comment.
Hmm, i think that might be a bit overkill for now, but it does make resampling much more extensible.
Tbh, I don't think we'll change the resampling strategy for wake word in the future. Most of the times the user will go from higher sample rate to 16khz so I think all resampling method will be quite the same in terms of quality. The model is very robust as well.
Actually, I think I will change the resampling from FFT based to FIR based so it's much faster.
|
@pham-tuan-binh, the build issue on Windows ARM64 is related to the |
Introduces the livekit-wakeword crate with a MelspectrogramModel that extracts mel-scaled spectrogram features from raw i16 PCM audio using ONNX Runtime. Includes tests verifying output shape and time-frame scaling.
- Move ONNX models from models/ to onnx/ to avoid naming collision - Flatten src/models/ into top-level src/ modules - Colocate unit tests with their respective modules - Remove prelude.rs in favor of direct imports
- Add WakeWordModel: stateless detection pipeline (mel -> embeddings -> classifier) - Classifiers loaded dynamically from ONNX files on disk - Hoist constants (SAMPLE_RATE, MEL_BINS, EMBEDDING_WINDOW, etc.) to lib.rs - Extract shared session builder helpers to eliminate duplication - Consolidate tests into a single end-to-end predict test - Add hey_livekit.onnx classifier model
Replace the default ONNX Runtime C++ backend with the pure-Rust tract backend via ort-tract to avoid upstream test failures from native library linking issues.
Accept plain slices (&[i16], &[f32]) instead of ndarray types in the public detect/predict methods, keeping ndarray as an internal detail.
Add a typed WakeWordError enum (via thiserror) covering Ort, Shape, Io, and ModelNotFound cases, replacing dynamic error types across all public methods.
Accept any common sample rate (16k–384k Hz) via a new sample_rate parameter on WakeWordModel::new(), resampling to 16 kHz internally using the resampler crate. Make embedding and melspectrogram modules pub(crate) since they are implementation details. Change MelspectrogramModel::detect() to accept &[f32] directly, avoiding a redundant i16→f32→i16→f32 round-trip when resampling.
Switch from ResamplerFft to ResamplerFir which accepts arbitrary input buffer sizes, eliminating chunk management and zero-padding complexity. Uses 64-sample latency (~1.3ms at 48kHz) with 90dB stopband attenuation.
tract-linalg has ARM64 assembly (.S files) that MSVC cannot compile. Use a build.rs to auto-detect the target and conditionally enable ort-tract, falling back to native ONNX Runtime on aarch64-pc-windows-msvc.
bedca64 to
cead83c
Compare
The Rust mel spectrogram was missing the x/10 + 2 post-processing normalization from the openWakeWord pipeline, causing near-zero classifier scores. Added positive/negative WAV sample tests with a 0.5 threshold to catch regressions.
|
added a new test which sanity check if hey livekit model can correctly detect a positive and negative wakeword sample |
Accept main's versioned_files format and simplified livekit-ffi scopes, re-add livekit-wakeword package with the new format.
Summary
livekit-wakewordcrate with a stateless wake word detection pipelineinclude_bytes!hey_livekit.onnx) are loaded dynamically from disk at runtimeTest plan
cargo test -p livekit-wakeword— single end-to-end test exercises full pipeline withhey_livekit.onnx