feat(npu): download+unzip the hosted OpenVINO runtime bundle w/ progress + sha256 (T3.4)
This commit is contained in:
+114
-14
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user