pub mod local_transcriber;
pub mod tts;
pub mod voice;
pub(crate) mod wake_word;
use anyhow::{Context as _, Result, anyhow};
use candle_core::Tensor;
use std::borrow::Cow;
use std::collections::HashMap;
use std::ops::AsyncFn;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use std::time::Duration;
use tracing::{info, warn};
use crate::util::model_state::{AtomicModelState, ModelLoadGuard, ModelState};
pub(crate) fn onnx_output_name(model: &crate::onnx::Model) -> String {
model.output_name().to_string()
}
pub(crate) fn extract_output(
mut outputs: HashMap<String, Tensor>,
model: &crate::onnx::Model,
label: &str,
) -> Result<Tensor> {
let name = onnx_output_name(model);
outputs
.remove(&name)
.ok_or_else(|| anyhow!("{label}: output '{name}' not found"))
}
pub(crate) fn models_subdir(name: &str) -> Option<PathBuf> {
crate::util::models_dir().map(|dir| dir.join(name))
}
const MAX_DOWNLOAD_RETRIES: u32 = 10;
pub(crate) async fn run_download_retry_loop<D, L, F>(
state: &AtomicModelState,
dir_name: &str,
label: &str,
timeout: Duration,
download: D,
load: L,
on_retry_cap: F,
) where
D: AsyncFn(&Path) -> anyhow::Result<()>,
L: Fn(&Path) -> anyhow::Result<()>,
F: FnOnce(),
{
let _guard = ModelLoadGuard::new(state);
let Some(dir) = models_subdir(dir_name) else {
warn!("{label}: cannot resolve model directory");
state.store(ModelState::Failed, Ordering::Release);
return;
};
let mut retry_delay = Duration::from_secs(5);
let mut retry_count = 0u32;
loop {
if state.load(Ordering::Acquire) == ModelState::Ready {
return;
}
retry_count += 1;
if retry_count > MAX_DOWNLOAD_RETRIES {
on_retry_cap();
return;
}
match tokio::time::timeout(timeout, download(&dir)).await {
Ok(Ok(())) => match load(&dir) {
Ok(()) => return,
Err(e) => warn!("Failed to load {label} models (will retry): {e}"),
},
Ok(Err(e)) => warn!("Failed to download {label} models (will retry): {e}"),
Err(_) => warn!("{label} download timed out (will retry)"),
}
if state.load(Ordering::Acquire) == ModelState::Failed {
return;
}
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(Duration::from_mins(2));
}
}
#[expect(clippy::cast_precision_loss, clippy::too_many_arguments)]
pub(crate) async fn ensure_downloaded(
client: Option<&reqwest::Client>,
path: &Path,
url: &str,
sha256: &str,
min_size: u64,
timeout: Duration,
label: &str,
mut on_progress: impl FnMut(u64, u64),
) -> Result<bool> {
if path.exists() {
if sha256.is_empty() {
let meta = tokio::fs::metadata(path).await?;
if meta.len() >= min_size {
return Ok(false);
}
warn!(
"{label} too small ({} bytes), re-downloading: {}",
meta.len(),
path.display()
);
} else if let Err(e) = crate::util::verify_sha256(path, sha256) {
warn!("{label} corrupt, re-downloading {}: {e}", path.display());
} else {
return Ok(false);
}
tokio::fs::remove_file(path).await?;
}
info!("Downloading {label}...");
let client = match client {
Some(c) => Cow::Borrowed(c),
None => Cow::Owned(
crate::util::http::build_download_client(timeout)
.context("Failed to build HTTP client")?,
),
};
let mut size = 0u64;
crate::util::http::download_verified(
&client,
url,
path,
sha256,
Some(timeout),
crate::util::http::DownloadSizeCheck::Min(min_size),
|d, total| {
size = d;
on_progress(d, total);
},
)
.await?;
info!("Downloaded {label} ({:.1} MB)", size as f64 / 1_048_576.0);
Ok(true)
}