-
Notifications
You must be signed in to change notification settings - Fork 154
Add UniFFI interface for livekit-wakeword #939
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
base: main
Are you sure you want to change the base?
Changes from all commits
917b6a8
a2b017a
ca7101f
d6b1cd4
c39355c
08d2d58
5fbcb4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| livekit-uniffi: minor | ||
| --- | ||
|
|
||
| # Add UniFFI interface for livekit-wakeword | ||
|
|
||
| ## Summary | ||
| - Expose `WakeWordDetector` as a UniFFI Object wrapping `WakeWordModel` with `Mutex` for interior mutability | ||
| - Export `new`, `load_model`, and `predict` methods across FFI | ||
| - Use `#[uniffi::remote(Error)]` with `#[uniffi(flat_error)]` for `WakeWordError`, matching the existing `AccessTokenError` pattern |
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,74 @@ | ||
| // Copyright 2026 LiveKit, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| use livekit_wakeword::{WakeWordError, WakeWordModel}; | ||
| use std::collections::HashMap; | ||
| use std::sync::Mutex; | ||
|
|
||
| #[uniffi::remote(Error)] | ||
| #[uniffi(flat_error)] | ||
| pub enum WakeWordError { | ||
| Ort, | ||
| Shape, | ||
| Io, | ||
| ModelNotFound, | ||
| UnsupportedSampleRate, | ||
| Resample, | ||
| } | ||
|
|
||
| /// Wake word detector backed by ONNX classifier models. | ||
| /// | ||
| /// Wraps [`livekit_wakeword::WakeWordModel`] for use across FFI boundaries. | ||
| /// Uses interior mutability since the underlying model requires `&mut self`. | ||
| #[derive(uniffi::Object)] | ||
| pub struct WakeWordDetector { | ||
| inner: Mutex<WakeWordModel>, | ||
| } | ||
|
|
||
| #[uniffi::export] | ||
| impl WakeWordDetector { | ||
| /// Create a new wake word detector. | ||
| /// | ||
| /// `model_paths` are filesystem paths to ONNX classifier models. | ||
| /// `sample_rate` is the sample rate of audio that will be passed to | ||
| /// [`predict`](Self::predict). Supported rates: 16000 (recommended), | ||
| /// 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 384000 Hz. | ||
| #[uniffi::constructor] | ||
| pub fn new(model_paths: Vec<String>, sample_rate: u32) -> Result<Self, WakeWordError> { | ||
| let model = WakeWordModel::new(&model_paths, sample_rate)?; | ||
| Ok(Self { inner: Mutex::new(model) }) | ||
| } | ||
|
|
||
| /// Load an additional wake word classifier ONNX model from disk. | ||
| /// | ||
| /// If `model_name` is `None`, the file stem is used as the classifier name. | ||
| pub fn load_model( | ||
| &self, | ||
| model_path: String, | ||
| model_name: Option<String>, | ||
| ) -> Result<(), WakeWordError> { | ||
| let mut inner = self.inner.lock().unwrap(); | ||
| inner.load_model(&model_path, model_name.as_deref())?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Get wake word predictions for an audio chunk. | ||
| /// | ||
| /// Pass ~2 seconds of i16 PCM audio at the sample rate configured in | ||
| /// [`new`](Self::new). Returns a map of classifier name to confidence score. | ||
| pub fn predict(&self, audio_chunk: Vec<i16>) -> Result<HashMap<String, f32>, WakeWordError> { | ||
| let mut inner = self.inner.lock().unwrap(); | ||
| Ok(inner.predict(&audio_chunk)?) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ license.workspace = true | |
|
|
||
| [dependencies] | ||
| ndarray = "0.17.2" | ||
| ort = { version = "2.0.0-rc.11", default-features = false, features = ["ndarray", "std"] } | ||
| ort = { version = "2.0.0-rc.11", default-features = false, features = ["ndarray", "std", "download-binaries", "tls-native"] } | ||
|
Contributor
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. question: Which binaries does ORT need to download? If this is necessary, we shouldn't hardcode
Contributor
Author
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. It isn't necessary. It was a suggestion from copilot to solve linking problems on Windows. Would u mind looking into the problem on windows for me @ladvoc ? It has some weird problems with the way our rust config is handled on windows. |
||
| resampler = "0.4" | ||
| thiserror = "2" | ||
|
|
||
|
|
||
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.
suggestion: This can accept a non-mutable slice
&[i16]to avoid cloning. This is also supported by UniFFI.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.
I see, I didn't know that, will make a commit