Skip to content
Open
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: 1 addition & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
rustflags = ["-C", "target-feature=+crt-static"]

[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
rustflags = []

[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-ObjC"]
Expand Down
10 changes: 10 additions & 0 deletions .changeset/add_uniffi_interface_for_livekit_wakeword.md
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
69 changes: 69 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ livekit-api = { version = "0.4.14", path = "livekit-api" }
livekit-ffi = { version = "0.12.48", path = "livekit-ffi" }
livekit-protocol = { version = "0.7.1", path = "livekit-protocol" }
livekit-runtime = { version = "0.4.0", path = "livekit-runtime" }
livekit-wakeword = { version = "0.1.0", path = "livekit-wakeword" }
soxr-sys = { version = "0.1.2", path = "soxr-sys" }
webrtc-sys = { version = "0.3.23", path = "webrtc-sys" }
webrtc-sys-build = { version = "0.3.13", path = "webrtc-sys/build" }
Expand Down
5 changes: 5 additions & 0 deletions livekit-uniffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ repository.workspace = true
readme = "README.md"
publish = false

[features]
default = ["wakeword"]
wakeword = ["livekit-wakeword"]

[dependencies]
livekit-protocol = { workspace = true }
livekit-api = { workspace = true }
livekit-wakeword = { workspace = true, optional = true }
uniffi = { version = "0.30.0", features = ["cli", "scaffolding-ffi-buffer-fns"] }
log = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
Expand Down
4 changes: 4 additions & 0 deletions livekit-uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ pub mod log_forward;
/// Information about the build such as version.
pub mod build_info;

/// Wake word detection from [`livekit-wakeword`].
#[cfg(feature = "wakeword")]
pub mod wakeword;

uniffi::setup_scaffolding!();
74 changes: 74 additions & 0 deletions livekit-uniffi/src/wakeword.rs
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> {
Copy link
Contributor

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.

Copy link
Contributor Author

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

let mut inner = self.inner.lock().unwrap();
Ok(inner.predict(&audio_chunk)?)
}
}
2 changes: 1 addition & 1 deletion livekit-wakeword/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Copy link
Contributor

@ladvoc ladvoc Mar 12, 2026

Choose a reason for hiding this comment

The 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 tls-native and instead allow the consumer of the crate to choose the TLS feature they want to use based on their requirements (much like how ort does so itself).

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"

Expand Down
Loading