feat(commands): route AMD/Intel to DirectML ONNX engine + download_directml_package (P2)

load_transcriber dispatches OnnxOpenVino/OnnxDirectML through one OnnxTranscriber
block. download_and_extract_runtime is parameterized (sha + readiness) so the new
stage_directml_runtime reuses it; hardware_status reports a directml package field.
This commit is contained in:
iamdoubz
2026-07-05 19:40:27 -05:00
parent 0f2940e52b
commit b69c91a598
+86 -12
View File
@@ -126,18 +126,22 @@ fn load_transcriber(
// backend has a working accel path just because the hardware exists.
let path = resolve_accel(backend);
// ONNX engine — NPU (OpenVINO EP) or AMD/Intel GPU (DirectML EP). Same model
// artifacts either way; the EP is chosen inside OnnxTranscriber::load from
// `backend`, and the resolved `backend` is reported so the UI shows the real
// engine.
#[cfg(feature = "npu")]
if path == AccelPath::OnnxOpenVino {
use crate::transcription::{onnx_models, OnnxNpuTranscriber};
if matches!(path, AccelPath::OnnxOpenVino | AccelPath::OnnxDirectML) {
use crate::transcription::{onnx_models, OnnxTranscriber};
if onnx_models::is_installed(onnx_models::DEFAULT_ONNX_MODEL) {
let dir = onnx_models::model_dir(onnx_models::DEFAULT_ONNX_MODEL);
match OnnxNpuTranscriber::load(&dir, BackendId::Npu) {
Ok(t) => return Ok((Box::new(t), BackendId::Npu)),
Err(e) => tracing::warn!("NPU engine load failed ({e}); falling back to CPU"),
match OnnxTranscriber::load(&dir, backend) {
Ok(t) => return Ok((Box::new(t), backend)),
Err(e) => tracing::warn!("ONNX engine load failed ({e}); falling back to CPU"),
}
} else {
tracing::warn!(
"NPU selected but ONNX model not installed; falling back to whisper.cpp"
"ONNX backend selected but model not installed; falling back to whisper.cpp"
);
}
}
@@ -852,6 +856,12 @@ pub async fn hardware_status() -> WaResult<serde_json::Value> {
"runtimeReady": crate::paths::npu_runtime_ready(),
"modelInstalled": npu_model_installed(),
},
// DirectML GPU package (P2): shares the ONNX model with the NPU path;
// only the runtime differs. Drives a future Settings ▸ Hardware indicator.
"directml": {
"runtimeReady": crate::paths::directml_runtime_ready(),
"modelInstalled": npu_model_installed(),
},
}))
}
@@ -928,7 +938,35 @@ async fn stage_npu_runtime(app: &AppHandle) -> WaResult<()> {
}
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
download_and_extract_runtime(
app,
&dir,
&url,
NPU_RUNTIME_SHA256,
crate::paths::npu_runtime_ready,
)
.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`).
#[cfg(feature = "npu")]
async fn stage_directml_runtime(app: &AppHandle) -> WaResult<()> {
if crate::paths::directml_runtime_ready() {
return Ok(());
}
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
}
#[cfg(feature = "npu")]
@@ -966,10 +1004,21 @@ fn stage_npu_runtime_from_local(
Ok(())
}
/// Downloads the runtime bundle (streaming progress + SHA-256 check) and unzips
/// its DLLs flat into `dir`.
/// Downloads a runtime bundle (streaming progress + SHA-256 check) and unzips
/// its DLLs flat into `dir`. `sha256` empty disables the integrity check;
/// `ready` is the on-disk readiness predicate for the target runtime (NPU or
/// DirectML), so this one function serves both.
// ponytail: progress event + error domain stay "npu"/"npu://download" even on
// the DirectML path — cosmetic only, no frontend consumer for a directml
// channel yet. Split them out when the Settings UI grows a DirectML indicator.
#[cfg(feature = "npu")]
async fn download_and_extract_runtime(app: &AppHandle, dir: &Path, url: &str) -> WaResult<()> {
async fn download_and_extract_runtime(
app: &AppHandle,
dir: &Path,
url: &str,
sha256: &str,
ready: fn() -> bool,
) -> WaResult<()> {
use futures_util::StreamExt;
use sha2::{Digest, Sha256};
@@ -1002,7 +1051,7 @@ async fn download_and_extract_runtime(app: &AppHandle, dir: &Path, url: &str) ->
drop(file);
let digest = format!("{:x}", hasher.finalize());
if !NPU_RUNTIME_SHA256.is_empty() && digest != NPU_RUNTIME_SHA256 {
if !sha256.is_empty() && digest != sha256 {
let _ = std::fs::remove_file(&tmp);
return Err(WaError::new("npu", "runtime bundle checksum mismatch"));
}
@@ -1016,7 +1065,7 @@ async fn download_and_extract_runtime(app: &AppHandle, dir: &Path, url: &str) ->
.map_err(|e| WaError::new("npu", e))?;
let _ = std::fs::remove_file(&tmp);
if !crate::paths::npu_runtime_ready() {
if !ready() {
return Err(WaError::new(
"npu",
"runtime bundle extracted but onnxruntime.dll is missing",
@@ -1070,6 +1119,31 @@ pub async fn download_npu_package(app: AppHandle) -> WaResult<()> {
}
}
/// Downloads everything the DirectML GPU engine needs — the same Whisper ONNX
/// model as the NPU path, plus the DirectML runtime (P2). The non-Vulkan GPU
/// path for AMD/Intel.
#[tauri::command]
pub async fn download_directml_package(app: AppHandle) -> WaResult<()> {
#[cfg(feature = "npu")]
{
download_npu_model(app.clone()).await?; // identical ONNX artifacts
stage_directml_runtime(&app).await?;
let _ = app.emit(
"directml://download",
serde_json::json!({ "stage": "done", "ready": crate::paths::directml_runtime_ready() }),
);
Ok(())
}
#[cfg(not(feature = "npu"))]
{
let _ = app;
Err(WaError::new(
"directml",
"this build has no ONNX/DirectML support",
))
}
}
#[tauri::command]
pub async fn list_models() -> WaResult<Vec<ModelInfo>> {
let settings = load_settings();