use super::ExecutionProvider;
use super::OnnxError;
use super::runtime::{InferenceError, InferenceRuntime, InferenceTensor, NamedTensor};
use std::cell::Cell;
use std::path::Path;
const BACKEND_AUTO: u8 = 0;
#[cfg(feature = "backend-tract")]
const BACKEND_TRACT: u8 = 2;
thread_local! {
static BACKEND_FORCE: Cell<u8> = const { Cell::new(BACKEND_AUTO) };
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InferenceBackend {
#[cfg(feature = "backend-tract")]
Tract,
}
impl InferenceBackend {
pub fn default_backend() -> Self {
#[cfg(feature = "backend-tract")]
{
Self::Tract
}
}
pub fn resolve() -> Self {
let forced = BACKEND_FORCE.with(Cell::get);
match forced {
#[cfg(feature = "backend-tract")]
BACKEND_TRACT => return Self::Tract,
_ => {}
}
match std::env::var("POLYVOICE_INFERENCE_BACKEND") {
Ok(v) => {
let lower = v.to_ascii_lowercase();
match lower.as_str() {
"ort" | "onnxruntime" | "onnx-runtime" => {
tracing::warn!(
"POLYVOICE_INFERENCE_BACKEND=ort but ONNX Runtime is \
not in this crate — using available backend"
);
Self::default_backend()
}
"tract" => {
#[cfg(feature = "backend-tract")]
{
Self::Tract
}
#[cfg(not(feature = "backend-tract"))]
{
tracing::warn!(
"POLYVOICE_INFERENCE_BACKEND=tract but the `backend-tract` \
feature is not enabled — using available backend"
);
Self::default_backend()
}
}
other => {
tracing::warn!(
"unknown POLYVOICE_INFERENCE_BACKEND={other:?}; using default backend"
);
Self::default_backend()
}
}
}
Err(_) => Self::default_backend(),
}
}
pub fn force(backend: Option<Self>) {
let code = match backend {
None => BACKEND_AUTO,
#[cfg(feature = "backend-tract")]
Some(Self::Tract) => BACKEND_TRACT,
};
BACKEND_FORCE.with(|c| c.set(code));
}
}
#[derive(Debug)]
pub enum RuntimeSession {
#[cfg(feature = "backend-tract")]
Tract(super::tract_session::TractSession),
}
impl RuntimeSession {
pub fn from_path(
model_path: &Path,
#[allow(unused_variables)] ep: ExecutionProvider,
intra_threads: Option<usize>,
) -> Result<Self, OnnxError> {
match InferenceBackend::resolve() {
#[cfg(feature = "backend-tract")]
InferenceBackend::Tract => Ok(Self::Tract(
super::tract_session::TractSession::from_path(model_path, intra_threads)?,
)),
}
}
pub fn backend(&self) -> InferenceBackend {
match self {
#[cfg(feature = "backend-tract")]
Self::Tract(_) => InferenceBackend::Tract,
}
}
}
impl InferenceRuntime for RuntimeSession {
fn input_names(&self) -> &[String] {
match self {
#[cfg(feature = "backend-tract")]
Self::Tract(s) => s.input_names(),
}
}
fn run(&mut self, inputs: &[NamedTensor<'_>]) -> Result<Vec<InferenceTensor>, InferenceError> {
match self {
#[cfg(feature = "backend-tract")]
Self::Tract(s) => s.run(inputs),
}
}
fn run_ordered(
&mut self,
inputs: &[&InferenceTensor],
) -> Result<Vec<InferenceTensor>, InferenceError> {
match self {
#[cfg(feature = "backend-tract")]
Self::Tract(s) => s.run_ordered(inputs),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn force_overrides_resolution() {
#[cfg(feature = "backend-tract")]
{
InferenceBackend::force(Some(InferenceBackend::Tract));
assert_eq!(InferenceBackend::resolve(), InferenceBackend::Tract);
}
InferenceBackend::force(None);
assert_eq!(
InferenceBackend::resolve(),
InferenceBackend::default_backend()
);
}
#[test]
fn backend_derives() {
let b = InferenceBackend::default_backend();
let copied = b;
assert_eq!(copied, b);
assert!(!format!("{b:?}").is_empty());
#[cfg(feature = "backend-tract")]
assert_eq!(format!("{:?}", InferenceBackend::Tract), "Tract");
}
#[test]
#[cfg_attr(miri, ignore)]
fn resolve_reads_env_var() {
unsafe { std::env::set_var("POLYVOICE_INFERENCE_BACKEND", "ORT") };
assert_eq!(
InferenceBackend::resolve(),
InferenceBackend::default_backend()
);
unsafe { std::env::set_var("POLYVOICE_INFERENCE_BACKEND", "onnxruntime") };
assert_eq!(
InferenceBackend::resolve(),
InferenceBackend::default_backend()
);
unsafe { std::env::set_var("POLYVOICE_INFERENCE_BACKEND", "tract") };
#[cfg(feature = "backend-tract")]
assert_eq!(InferenceBackend::resolve(), InferenceBackend::Tract);
#[cfg(not(feature = "backend-tract"))]
assert_eq!(
InferenceBackend::resolve(),
InferenceBackend::default_backend()
);
unsafe { std::env::set_var("POLYVOICE_INFERENCE_BACKEND", "bogus") };
assert_eq!(
InferenceBackend::resolve(),
InferenceBackend::default_backend()
);
unsafe { std::env::remove_var("POLYVOICE_INFERENCE_BACKEND") };
assert_eq!(
InferenceBackend::resolve(),
InferenceBackend::default_backend()
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn runtime_session_rejects_garbage_before_backend() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&[0xAB; 64]).unwrap();
let err = RuntimeSession::from_path(tmp.path(), ExecutionProvider::Cpu, None)
.expect_err("garbage must fail header validation");
assert!(
matches!(err, OnnxError::Validation(_)),
"unexpected error: {err}"
);
}
}