feat(runtime): download NPU/DirectML runtimes as 7z from the repo raw URLs
extract_7z_flat replaces extract_zip_flat; NPU + DirectML now default to
git.dou.bet raw/branch/main/runtime/{openvino,directml}.7z with pinned SHA-256s
(DirectML no longer requires WA_DIRECTML_RUNTIME_URL). Adds an extraction test
against the real bundle (proves LZMA2+BCJ decode + flatten).
This commit is contained in:
+74
-38
@@ -948,15 +948,25 @@ pub async fn download_npu_model(app: AppHandle) -> WaResult<()> {
|
||||
.map_err(|e| WaError::new("npu", e.to_string()))
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
/// Hosted OpenVINO runtime bundle (ORT 1.24.1 + OpenVINO 2025.4.1 DLLs, 7z).
|
||||
/// Served from the repo's `runtime/` dir via gitea's raw path; overridable at
|
||||
/// runtime via `WA_NPU_RUNTIME_URL`. Keep the SHA-256 in step with the file.
|
||||
#[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";
|
||||
"https://git.dou.bet/iamdoubz/WhispAssist/raw/branch/main/runtime/openvino.7z";
|
||||
/// SHA-256 of the runtime bundle; empty string disables the integrity check.
|
||||
#[cfg(feature = "npu")]
|
||||
const NPU_RUNTIME_SHA256: &str = "c60de07b5b1ddc2fd1e966d8275d9f55ec261efc81355ea814d6dac897adbdc5";
|
||||
const NPU_RUNTIME_SHA256: &str = "ca0be9fc52c78ee623b152f790450b3d4020c5a7ebe99d27736455b308782191";
|
||||
|
||||
/// Hosted DirectML runtime bundle (ORT 1.24.1 DirectML-EP DLLs, 7z), same repo
|
||||
/// raw path as the OpenVINO one; overridable via `WA_DIRECTML_RUNTIME_URL`.
|
||||
#[cfg(feature = "npu")]
|
||||
const DIRECTML_RUNTIME_URL: &str =
|
||||
"https://git.dou.bet/iamdoubz/WhispAssist/raw/branch/main/runtime/directml.7z";
|
||||
/// SHA-256 of the DirectML runtime bundle; empty string disables the check.
|
||||
#[cfg(feature = "npu")]
|
||||
const DIRECTML_RUNTIME_SHA256: &str =
|
||||
"34369222fcc1be2e72a957b868b1976a90150ba704a06c9e8992c34ee368926b";
|
||||
|
||||
/// 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`
|
||||
@@ -986,10 +996,9 @@ async fn stage_npu_runtime(app: &AppHandle) -> WaResult<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stages the DirectML ONNX Runtime into the app's DirectML runtime dir (P2).
|
||||
/// Unlike the NPU bundle there's no hosted default yet, so the URL must come
|
||||
/// from `WA_DIRECTML_RUNTIME_URL` — or drop `onnxruntime.dll` into
|
||||
/// `runtime\directml\` manually (dev testing can also just set `ORT_DYLIB_PATH`).
|
||||
/// Stages the DirectML ONNX Runtime into the app's DirectML runtime dir (P2) by
|
||||
/// downloading the hosted 7z bundle and unpacking it. Overridable via
|
||||
/// `WA_DIRECTML_RUNTIME_URL`; dev testing can also just set `ORT_DYLIB_PATH`.
|
||||
#[cfg(feature = "npu")]
|
||||
async fn stage_directml_runtime(app: &AppHandle) -> WaResult<()> {
|
||||
if crate::paths::directml_runtime_ready() {
|
||||
@@ -997,14 +1006,16 @@ async fn stage_directml_runtime(app: &AppHandle) -> WaResult<()> {
|
||||
}
|
||||
let dir = crate::paths::directml_runtime_dir();
|
||||
std::fs::create_dir_all(&dir).map_err(|e| WaError::new("directml", e.to_string()))?;
|
||||
let url = std::env::var("WA_DIRECTML_RUNTIME_URL").map_err(|_| {
|
||||
WaError::new(
|
||||
"directml",
|
||||
"no DirectML runtime: set WA_DIRECTML_RUNTIME_URL or place onnxruntime.dll in runtime\\directml\\",
|
||||
)
|
||||
})?;
|
||||
// Empty SHA: no published bundle to pin yet (ponytail — fill in once hosted).
|
||||
download_and_extract_runtime(app, &dir, &url, "", crate::paths::directml_runtime_ready).await
|
||||
let url = std::env::var("WA_DIRECTML_RUNTIME_URL")
|
||||
.unwrap_or_else(|_| DIRECTML_RUNTIME_URL.to_string());
|
||||
download_and_extract_runtime(
|
||||
app,
|
||||
&dir,
|
||||
&url,
|
||||
DIRECTML_RUNTIME_SHA256,
|
||||
crate::paths::directml_runtime_ready,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "npu")]
|
||||
@@ -1070,7 +1081,7 @@ async fn download_and_extract_runtime(
|
||||
));
|
||||
}
|
||||
let total = resp.content_length();
|
||||
let tmp = dir.join("runtime.zip.part");
|
||||
let tmp = dir.join("runtime.7z.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;
|
||||
@@ -1094,10 +1105,10 @@ async fn download_and_extract_runtime(
|
||||
return Err(WaError::new("npu", "runtime bundle checksum mismatch"));
|
||||
}
|
||||
|
||||
// Unzip off the async runtime (CPU/IO-bound).
|
||||
// Un-7z 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))
|
||||
tokio::task::spawn_blocking(move || extract_7z_flat(&tmp_for_unzip, &dir_for_unzip))
|
||||
.await
|
||||
.map_err(|e| WaError::new("npu", e.to_string()))?
|
||||
.map_err(|e| WaError::new("npu", e))?;
|
||||
@@ -1113,27 +1124,23 @@ async fn download_and_extract_runtime(
|
||||
}
|
||||
|
||||
/// Extract every file entry of a zip into `dir`, flattening paths to just the
|
||||
/// file name (which also prevents zip-slip path traversal).
|
||||
/// file name (which also strips the archive's top folder — the bundles nest DLLs
|
||||
/// under `directml/` or `openvino/` — and prevents 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;
|
||||
fn extract_7z_flat(archive_path: &Path, dir: &Path) -> Result<(), String> {
|
||||
sevenz_rust2::decompress_file_with_extract_fn(archive_path, dir, |entry, reader, _dest| {
|
||||
if entry.is_directory() {
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(name) = Path::new(entry.name())
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(str::to_string)
|
||||
else {
|
||||
continue;
|
||||
// Flatten to just the file name, dropping any folder prefix.
|
||||
let Some(name) = Path::new(entry.name()).file_name().and_then(|n| n.to_str()) else {
|
||||
return Ok(true);
|
||||
};
|
||||
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(())
|
||||
let mut out = std::fs::File::create(dir.join(name)).map_err(sevenz_rust2::Error::io)?;
|
||||
std::io::copy(reader, &mut out).map_err(sevenz_rust2::Error::io)?;
|
||||
Ok(true)
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Downloads everything the NPU engine needs (ONNX model + OpenVINO runtime) for
|
||||
@@ -2859,6 +2866,35 @@ pub async fn privacy_self_check(state: State<'_, AppState>) -> WaResult<serde_js
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Extracts the real hosted DirectML bundle (LZMA2+BCJ 7z) and checks the
|
||||
/// DLL lands flat. Skips if the binary isn't present (e.g. a lean checkout).
|
||||
#[cfg(feature = "npu")]
|
||||
#[test]
|
||||
fn extract_7z_flat_unpacks_the_directml_bundle() {
|
||||
let archive = std::path::Path::new("../runtime/directml.7z");
|
||||
if !archive.exists() {
|
||||
eprintln!("skip: {} not present", archive.display());
|
||||
return;
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("wa-7z-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
extract_7z_flat(archive, &dir).expect("7z extract");
|
||||
let dll = dir.join("onnxruntime.dll");
|
||||
assert!(dll.exists(), "onnxruntime.dll missing after extract");
|
||||
assert_eq!(
|
||||
std::fs::metadata(&dll).unwrap().len(),
|
||||
17_253_408,
|
||||
"unexpected DLL size — BCJ/LZMA2 decode may be wrong"
|
||||
);
|
||||
// Flattened: the archive's `directml/` prefix must be stripped.
|
||||
assert!(
|
||||
!dir.join("directml").exists(),
|
||||
"folder prefix not flattened"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
fn segment(speaker: &str) -> TranscriptSegment {
|
||||
TranscriptSegment {
|
||||
id: 0,
|
||||
|
||||
Reference in New Issue
Block a user