feat(npu): download+unzip the hosted OpenVINO runtime bundle w/ progress + sha256 (T3.4)

This commit is contained in:
iamdoubz
2026-07-03 09:16:41 -05:00
parent 7cc437d1b5
commit a38bf20e79
+114 -14
View File
@@ -884,13 +884,20 @@ pub async fn download_npu_model(app: AppHandle) -> WaResult<()> {
.map_err(|e| WaError::new("npu", e.to_string()))
}
/// Stages the ONNX Runtime + OpenVINO DLLs into the app's NPU runtime dir.
///
/// ponytail: sourced by copying DLLs from local dirs named in
/// `WA_NPU_RUNTIME_SRC` (';'-separated) — the same act a bundled installer step
/// would perform. Upgrade path: host a versioned runtime bundle and
/// download+unzip it here instead. Until a source is configured this is a clean
/// typed error, never a panic.
/// Hosted OpenVINO runtime bundle (ORT 1.24.1 + OpenVINO 2025.4.1 DLLs, zipped).
/// Overridable at runtime via `WA_NPU_RUNTIME_URL`. Keep the SHA-256 in step with
/// the uploaded bundle (see `dist/whispassist-npu-runtime-win-x64.zip`).
#[cfg(feature = "npu")]
const NPU_RUNTIME_URL: &str =
"https://git.dou.bet/api/packages/iamdoubz/generic/npu-runtime/2025.4.1/whispassist-npu-runtime-win-x64.zip";
/// SHA-256 of the runtime bundle; empty string disables the integrity check.
#[cfg(feature = "npu")]
const NPU_RUNTIME_SHA256: &str = "c60de07b5b1ddc2fd1e966d8275d9f55ec261efc81355ea814d6dac897adbdc5";
/// Stages the ONNX Runtime + OpenVINO DLLs into the app's NPU runtime dir by
/// downloading the hosted bundle and unzipping it (T3.4). `WA_NPU_RUNTIME_SRC`
/// (';'-separated dirs) is honored as a dev/offline override that copies local
/// DLLs instead of downloading.
#[cfg(feature = "npu")]
async fn stage_npu_runtime(app: &AppHandle) -> WaResult<()> {
if crate::paths::npu_runtime_ready() {
@@ -898,14 +905,24 @@ async fn stage_npu_runtime(app: &AppHandle) -> WaResult<()> {
}
let dir = crate::paths::npu_runtime_dir();
std::fs::create_dir_all(&dir).map_err(|e| WaError::new("npu", e.to_string()))?;
let src = std::env::var_os("WA_NPU_RUNTIME_SRC").ok_or_else(|| {
WaError::new(
"npu",
"NPU runtime source not configured (set WA_NPU_RUNTIME_SRC to the ORT+OpenVINO DLL dir[s])",
)
})?;
// Dev/offline override: copy DLLs from local dirs instead of downloading.
if let Some(src) = std::env::var_os("WA_NPU_RUNTIME_SRC") {
return stage_npu_runtime_from_local(app, &dir, &src);
}
let url = std::env::var("WA_NPU_RUNTIME_URL").unwrap_or_else(|_| NPU_RUNTIME_URL.to_string());
download_and_extract_runtime(app, &dir, &url).await
}
#[cfg(feature = "npu")]
fn stage_npu_runtime_from_local(
app: &AppHandle,
dir: &Path,
src: &std::ffi::OsStr,
) -> WaResult<()> {
let mut copied = 0u32;
for d in std::env::split_paths(&src) {
for d in std::env::split_paths(src) {
let Ok(entries) = std::fs::read_dir(&d) else {
continue;
};
@@ -933,6 +950,89 @@ async fn stage_npu_runtime(app: &AppHandle) -> WaResult<()> {
Ok(())
}
/// Downloads the runtime bundle (streaming progress + SHA-256 check) and unzips
/// its DLLs flat into `dir`.
#[cfg(feature = "npu")]
async fn download_and_extract_runtime(app: &AppHandle, dir: &Path, url: &str) -> WaResult<()> {
use futures_util::StreamExt;
use sha2::{Digest, Sha256};
let resp = reqwest::get(url)
.await
.map_err(|e| WaError::new("npu", e.to_string()))?;
if !resp.status().is_success() {
return Err(WaError::new(
"npu",
format!("runtime download failed: HTTP {}", resp.status()),
));
}
let total = resp.content_length();
let tmp = dir.join("runtime.zip.part");
let mut file = std::fs::File::create(&tmp).map_err(|e| WaError::new("npu", e.to_string()))?;
let mut hasher = Sha256::new();
let mut received = 0u64;
let mut stream = resp.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| WaError::new("npu", e.to_string()))?;
std::io::Write::write_all(&mut file, &chunk)
.map_err(|e| WaError::new("npu", e.to_string()))?;
hasher.update(&chunk);
received += chunk.len() as u64;
let _ = app.emit(
"npu://download",
serde_json::json!({ "stage": "runtime", "received": received, "total": total }),
);
}
drop(file);
let digest = format!("{:x}", hasher.finalize());
if !NPU_RUNTIME_SHA256.is_empty() && digest != NPU_RUNTIME_SHA256 {
let _ = std::fs::remove_file(&tmp);
return Err(WaError::new("npu", "runtime bundle checksum mismatch"));
}
// Unzip off the async runtime (CPU/IO-bound).
let tmp_for_unzip = tmp.clone();
let dir_for_unzip = dir.to_path_buf();
tokio::task::spawn_blocking(move || extract_zip_flat(&tmp_for_unzip, &dir_for_unzip))
.await
.map_err(|e| WaError::new("npu", e.to_string()))?
.map_err(|e| WaError::new("npu", e))?;
let _ = std::fs::remove_file(&tmp);
if !crate::paths::npu_runtime_ready() {
return Err(WaError::new(
"npu",
"runtime bundle extracted but onnxruntime.dll is missing",
));
}
Ok(())
}
/// Extract every file entry of a zip into `dir`, flattening paths to just the
/// file name (which also prevents zip-slip path traversal).
#[cfg(feature = "npu")]
fn extract_zip_flat(zip_path: &Path, dir: &Path) -> Result<(), String> {
let file = std::fs::File::open(zip_path).map_err(|e| e.to_string())?;
let mut archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?;
for i in 0..archive.len() {
let mut entry = archive.by_index(i).map_err(|e| e.to_string())?;
if entry.is_dir() {
continue;
}
let Some(name) = Path::new(entry.name())
.file_name()
.and_then(|n| n.to_str())
.map(str::to_string)
else {
continue;
};
let mut out = std::fs::File::create(dir.join(name)).map_err(|e| e.to_string())?;
std::io::copy(&mut entry, &mut out).map_err(|e| e.to_string())?;
}
Ok(())
}
/// Downloads everything the NPU engine needs (ONNX model + OpenVINO runtime) for
/// the Settings ▸ Hardware "download NPU package" action (T3.4 step 2).
#[tauri::command]