feat: promote linux native audio path

This commit is contained in:
Edison Jwa
2026-05-25 17:42:06 +09:00
parent c19de3a370
commit a2d686d9d0
73 changed files with 26156 additions and 467 deletions
+137 -2
View File
@@ -33,12 +33,16 @@
//! ## Fallback
//!
//! `try_new` returns `None` when the model file is missing, the ONNX
//! Runtime is unavailable, or the platform is not iOS/macOS. The caller
//! Runtime is unavailable, or the platform cannot load ONNX Runtime. The caller
//! falls back to `WebRtcFallbackVad`.
use super::{VadOutput, VoiceActivityDetector};
#[cfg(target_os = "linux")]
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
#[cfg(target_os = "linux")]
use std::sync::OnceLock;
use std::thread::JoinHandle;
/// 16 kHz frame size for Silero VAD v6 (32 ms).
@@ -88,7 +92,7 @@ impl SileroOnnxVad {
/// Attempt to load the Silero v6 ONNX model from `model_path`.
///
/// Returns `None` when the model file is missing, the ONNX Runtime
/// is unavailable, or the platform does not support ONNX.
/// is unavailable, or the platform cannot initialize ONNX Runtime.
pub fn try_new(model_path: &str) -> Option<Self> {
Self::try_new_onnx(model_path)
}
@@ -105,6 +109,17 @@ impl SileroOnnxVad {
return None;
}
#[cfg(target_os = "linux")]
if let Err(err) = ensure_linux_onnxruntime_loaded() {
error!(
target: "chanora_audio",
path = model_path,
error = %err,
"SileroOnnxVad: ONNX Runtime is unavailable on Linux; falling back to WebRtcFallbackVad"
);
return None;
}
let session_result = std::panic::catch_unwind(|| {
ort::session::Session::builder().and_then(|mut b| b.commit_from_file(model_path))
});
@@ -301,6 +316,111 @@ impl VoiceActivityDetector for SileroOnnxVad {
}
}
#[cfg(target_os = "linux")]
fn ensure_linux_onnxruntime_loaded() -> Result<(), String> {
static ORT_INIT: OnceLock<Result<(), String>> = OnceLock::new();
ORT_INIT
.get_or_init(|| {
let dylib_path = linux_onnxruntime_dylib_path();
match dylib_path.as_ref() {
Some(path) => {
tracing::info!(
target: "chanora_audio",
path = %path.display(),
"SileroOnnxVad: loading Linux ONNX Runtime from discovered shared library"
);
ort::init_from(path)
.map_err(|e| format!("init_from({}): {e}", path.display()))?
.with_name("chanora_audio")
.commit()
.then_some(())
.ok_or_else(|| {
format!("commit(init_from {}): environment already initialized", path.display())
})
}
None => {
tracing::warn!(
target: "chanora_audio",
"SileroOnnxVad: no explicit Linux ONNX Runtime shared library path found; trying loader default"
);
ort::init()
.with_name("chanora_audio")
.commit()
.then_some(())
.ok_or_else(|| "commit(init): environment already initialized".to_string())
}
}
})
.clone()
}
#[cfg(target_os = "linux")]
fn linux_onnxruntime_dylib_path() -> Option<PathBuf> {
if let Some(path) = existing_env_file("ORT_DYLIB_PATH") {
return Some(path);
}
let mut candidates = Vec::new();
if let Ok(exe) = std::env::current_exe() {
if let Some(exe_dir) = exe.parent() {
candidates.push(exe_dir.join("lib").join("libonnxruntime.so"));
candidates.push(exe_dir.join("libonnxruntime.so"));
}
}
if let Ok(cwd) = std::env::current_dir() {
candidates.push(cwd.join("libonnxruntime.so"));
}
candidates.extend([
PathBuf::from("/usr/lib/libonnxruntime.so"),
PathBuf::from("/usr/lib64/libonnxruntime.so"),
PathBuf::from("/usr/local/lib/libonnxruntime.so"),
PathBuf::from("/lib/x86_64-linux-gnu/libonnxruntime.so"),
PathBuf::from("/usr/lib/x86_64-linux-gnu/libonnxruntime.so"),
PathBuf::from("/lib/aarch64-linux-gnu/libonnxruntime.so"),
PathBuf::from("/usr/lib/aarch64-linux-gnu/libonnxruntime.so"),
]);
for candidate in candidates {
if candidate.is_file() {
return Some(candidate);
}
}
first_matching_dir_entry("/usr/lib", "libonnxruntime.so")
.or_else(|| first_matching_dir_entry("/usr/lib64", "libonnxruntime.so"))
.or_else(|| first_matching_dir_entry("/usr/local/lib", "libonnxruntime.so"))
.or_else(|| first_matching_dir_entry("/usr/lib/x86_64-linux-gnu", "libonnxruntime.so"))
.or_else(|| first_matching_dir_entry("/usr/lib/aarch64-linux-gnu", "libonnxruntime.so"))
}
#[cfg(target_os = "linux")]
fn existing_env_file(name: &str) -> Option<PathBuf> {
let path = std::env::var_os(name).map(PathBuf::from)?;
path.is_file().then_some(path)
}
#[cfg(target_os = "linux")]
fn first_matching_dir_entry(dir: &str, prefix: &str) -> Option<PathBuf> {
let mut matches: Vec<PathBuf> = std::fs::read_dir(dir)
.ok()?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| {
path.is_file()
&& path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(prefix))
})
.collect();
matches.sort();
matches.into_iter().next()
}
// SAFETY: ONNX Runtime sessions are thread-safe for inference.
// State arrays are owned by this struct and accessed only from
// the single capture callback thread.
@@ -401,6 +521,8 @@ impl Drop for SileroOnnxVadWorker {
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
fn make_stub_vad() -> SileroOnnxVad {
SileroOnnxVad {
@@ -496,4 +618,17 @@ mod tests {
&completed_frame[SILERO_FRAME_16K - SILERO_CONTEXT_16K..]
);
}
#[cfg(target_os = "linux")]
#[test]
fn existing_env_file_ignores_missing_paths() {
let var_name = format!("CHANORA_TEST_ORT_{}", std::process::id());
std::env::remove_var(&var_name);
assert!(existing_env_file(&var_name).is_none());
let missing = PathBuf::from("/definitely/missing/libonnxruntime.so");
std::env::set_var(&var_name, &missing);
assert!(existing_env_file(&var_name).is_none());
std::env::remove_var(&var_name);
}
}