From 88e1fcf1d5579d9bf982420740cee73bae3529f0 Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:06:42 -0500
Subject: [PATCH 01/14] fix(ui): show real error message (not [object Object])
on OAuth link failure
---
src/lib/stores/settings.svelte.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/lib/stores/settings.svelte.ts b/src/lib/stores/settings.svelte.ts
index 23efffd..e1b6c8b 100644
--- a/src/lib/stores/settings.svelte.ts
+++ b/src/lib/stores/settings.svelte.ts
@@ -5,6 +5,7 @@
import {
api,
+ errorMessage,
events,
type AppSettings,
type SyncTargetInfo,
@@ -104,7 +105,7 @@ class SettingsStore {
window.open(authUrl, "_blank");
this.linkMessage = "Finish sign-in in your browser…";
} catch (e) {
- this.linkMessage = `Link failed: ${e}`;
+ this.linkMessage = `Link failed: ${errorMessage(e)}`;
}
}
From 6b215cae36587a8b4a8318e53125d18ddb263712 Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:06:42 -0500
Subject: [PATCH 02/14] fix(ui): show real error message on NPU download +
vault failures
---
src/lib/views/Settings.svelte | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/lib/views/Settings.svelte b/src/lib/views/Settings.svelte
index 3ccf708..273b4c3 100644
--- a/src/lib/views/Settings.svelte
+++ b/src/lib/views/Settings.svelte
@@ -7,7 +7,7 @@
import { calendar } from "../stores/calendar.svelte";
import ConsentNotice from "../components/ConsentNotice.svelte";
import { open } from "@tauri-apps/plugin-dialog";
- import { api, events } from "../api";
+ import { api, errorMessage, events } from "../api";
import type { BackendId, SyncKind, SyncTargetConfig } from "../api";
import { trapFocus } from "../actions/trapFocus";
import {
@@ -84,7 +84,7 @@
vaultMsg = "Vault enabled and unlocked.";
await loadVault();
} catch (e) {
- vaultMsg = `${e}`;
+ vaultMsg = errorMessage(e);
}
}
async function unlockVault() {
@@ -95,7 +95,7 @@
vaultMsg = "Unlocked.";
await loadVault();
} catch (e) {
- vaultMsg = `${e}`;
+ vaultMsg = errorMessage(e);
}
}
async function lockVault() {
@@ -111,7 +111,7 @@
vaultPw2 = "";
vaultMsg = "Password changed.";
} catch (e) {
- vaultMsg = `${e}`;
+ vaultMsg = errorMessage(e);
}
}
@@ -126,7 +126,7 @@
await settings.loadHardware();
npuMsg = "NPU package ready.";
} catch (e) {
- npuMsg = `Failed: ${e}`;
+ npuMsg = `Failed: ${errorMessage(e)}`;
} finally {
npuBusy = false;
}
From 4001d38b9b6948baecc30606c3bbf598dbd1cdf4 Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:06:42 -0500
Subject: [PATCH 03/14] fix(npu): ship npu in the default build so the shipped
app can use/download it (T3.4); bump 0.1.1
---
src-tauri/Cargo.toml | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index f53d0f5..bf016db 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "whispassist"
-version = "0.1.0"
+version = "0.1.1"
description = "Privacy-first, fully local Windows meeting assistant"
authors = ["WhispAssist contributors"]
license = "MIT OR Apache-2.0"
@@ -81,7 +81,11 @@ windows = { version = "0.58", features = [
wasapi = { version = "0.15", optional = true } # Phase 1
[features]
-default = ["audio", "cpu-transcription", "diarization", "pst", "sync"]
+# `npu` ships in the default build: `ort` uses load-dynamic (no build-time
+# linking/OpenVINO needed), and the on-demand runtime/model download only works
+# if this code path is actually compiled in. NPU stays inert until an NPU is
+# detected AND its runtime is downloaded, so shipping it is safe (NFR-MNT-4).
+default = ["audio", "cpu-transcription", "diarization", "pst", "sync", "npu"]
# Phase 1
audio = ["dep:wasapi", "dep:hound"]
cpu-transcription = ["dep:whisper-rs"] # whisper-rs CPU build
From 368be69ff73d3161ae0dc4308ec42d661968107f Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:06:43 -0500
Subject: [PATCH 04/14] chore(release): bump version to 0.1.1
---
package.json | 2 +-
src-tauri/tauri.conf.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 749ef6f..ccb399c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "whispassist",
"private": true,
- "version": "0.1.0",
+ "version": "0.1.1",
"type": "module",
"description": "Privacy-first, fully local Windows meeting assistant.",
"license": "MIT OR Apache-2.0",
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index baf212d..7328bd6 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "WhispAssist",
- "version": "0.1.0",
+ "version": "0.1.1",
"identifier": "bet.dou.whispassist",
"build": {
"frontendDist": "../dist",
From 164cecae7aaa95e80a7064ae62b6a95a13c1f8bb Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:16:40 -0500
Subject: [PATCH 05/14] chore: gitignore dist/ (build artifacts)
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 2113534..2f99204 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@ Thumbs.db
.env
.env.*
!.env.example
+dist/
From 7cc437d1b593ee86f49182b54ed57109067913ce Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:16:40 -0500
Subject: [PATCH 06/14] build(npu): add zip crate to extract the runtime bundle
(T3.4)
---
src-tauri/Cargo.lock | 54 ++++++++++++++++++++++++++++++++++++++++++--
src-tauri/Cargo.toml | 1 +
2 files changed, 53 insertions(+), 2 deletions(-)
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index f94bb37..b881792 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -63,6 +63,15 @@ version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
[[package]]
name = "argon2"
version = "0.5.3"
@@ -795,6 +804,17 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -928,7 +948,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.18",
- "zip",
+ "zip 0.6.6",
]
[[package]]
@@ -5925,7 +5945,7 @@ dependencies = [
[[package]]
name = "whispassist"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"argon2",
"async-trait",
@@ -5958,6 +5978,7 @@ dependencies = [
"whisper-rs",
"windows 0.58.0",
"zeroize",
+ "zip 2.4.2",
]
[[package]]
@@ -6852,12 +6873,41 @@ dependencies = [
"flate2",
]
+[[package]]
+name = "zip"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "crossbeam-utils",
+ "displaydoc",
+ "flate2",
+ "indexmap 2.14.0",
+ "memchr",
+ "thiserror 2.0.18",
+ "zopfli",
+]
+
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
+
[[package]]
name = "zune-core"
version = "0.5.1"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index bf016db..2ed239e 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -64,6 +64,7 @@ tauri-plugin-dialog = "2" # native Save/choose
# conversion service, consistent with the fully-local invariant.
pulldown-cmark = "0.12"
printpdf = "0.7"
+zip = { version = "2", default-features = false, features = ["deflate"] } # unzip the on-demand NPU runtime bundle (T3.4)
docx-rs = "0.4"
[target.'cfg(windows)'.dependencies]
From a38bf20e79de43c394df1af46cae4256792b93d2 Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:16:41 -0500
Subject: [PATCH 07/14] feat(npu): download+unzip the hosted OpenVINO runtime
bundle w/ progress + sha256 (T3.4)
---
src-tauri/src/commands.rs | 128 +++++++++++++++++++++++++++++++++-----
1 file changed, 114 insertions(+), 14 deletions(-)
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index af496c0..ca69f18 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -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]
From 62283edec23a3b8d49b8b3cf1ca5f189cabd8abb Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:18:11 -0500
Subject: [PATCH 08/14] chore: gitignore the NPU runtime zip (was wrongly under
Vite's dist/)
---
.gitignore | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 2f99204..352527e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,4 +37,7 @@ Thumbs.db
.env
.env.*
!.env.example
-dist/
+
+# NPU runtime bundle: a large binary artifact hosted as a Gitea package, not
+# committed. The folder's README is tracked; the zip is produced locally.
+packaging/npu-runtime/*.zip
From 2093b39fe1c5aa602d5a696629209b891d15f672 Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:18:12 -0500
Subject: [PATCH 09/14] docs(npu): runtime bundle README (contents, sha,
hosting)
---
packaging/npu-runtime/README.md | 39 +++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 packaging/npu-runtime/README.md
diff --git a/packaging/npu-runtime/README.md b/packaging/npu-runtime/README.md
new file mode 100644
index 0000000..3aa8420
--- /dev/null
+++ b/packaging/npu-runtime/README.md
@@ -0,0 +1,39 @@
+# NPU runtime bundle
+
+`whispassist-npu-runtime-win-x64.zip` is the on-demand OpenVINO runtime for the
+NPU transcription path (T3.4). It is **not committed to git** (it's a ~40 MB
+binary) — it's hosted as a Gitea generic package and downloaded by the app on
+demand when an NPU is detected.
+
+## Contents
+The 22 flat DLLs of the validated, version-pinned runtime:
+
+- **ONNX Runtime 1.24.1** with the OpenVINO execution provider
+ (`onnxruntime.dll`, `onnxruntime_providers_openvino.dll`,
+ `onnxruntime_providers_shared.dll`)
+- **OpenVINO 2025.4.1** runtime + NPU plugin + TBB (`openvino.dll`,
+ `openvino_intel_npu_plugin.dll`, `tbb12.dll`, …)
+
+> ⚠️ The ORT↔OpenVINO versions are pinned. A mismatch makes ORT silently fall
+> back to CPU. Regenerate the bundle from a matched pip install
+> (`onnxruntime-openvino==1.24.1` + `openvino==2025.4.1`) if you bump either.
+
+## Current bundle
+
+- **SHA-256:** `c60de07b5b1ddc2fd1e966d8275d9f55ec261efc81355ea814d6dac897adbdc5`
+
+This hash is pinned in `src-tauri/src/commands.rs` (`NPU_RUNTIME_SHA256`); the
+app verifies the download against it. If you regenerate the zip, update that
+constant.
+
+## Hosting
+
+Upload this zip as a Gitea generic package, e.g.:
+
+```
+PUT https://git.dou.bet/api/packages/iamdoubz/generic/npu-runtime/2025.4.1/whispassist-npu-runtime-win-x64.zip
+```
+
+Then set the resulting download URL in `NPU_RUNTIME_URL`
+(`src-tauri/src/commands.rs`). It's also overridable at runtime without a
+rebuild via the `WA_NPU_RUNTIME_URL` environment variable.
From 78335b3e698923f5b3699f16974d075570523dd2 Mon Sep 17 00:00:00 2001
From: iamdoubz <>
Date: Fri, 3 Jul 2026 09:49:34 -0500
Subject: [PATCH 10/14] feat(llm): treat private LAN endpoints (RFC-1918) as
local, not internet egress (FR-LLM-6)
---
src-tauri/src/llm/mod.rs | 47 +++++++++++++++++++++++++++++++---------
1 file changed, 37 insertions(+), 10 deletions(-)
diff --git a/src-tauri/src/llm/mod.rs b/src-tauri/src/llm/mod.rs
index 2362724..ad2fa0c 100644
--- a/src-tauri/src/llm/mod.rs
+++ b/src-tauri/src/llm/mod.rs
@@ -1,6 +1,8 @@
-//! Local LLM integration (Phase 5, FR-LLM-*). Ollama HTTP on localhost
-//! (ADR-0007). The ONLY network egress WA originates for content, and it must be
-//! local — `is_local` gates a "data leaves WA" warning for remote endpoints.
+//! Local LLM integration (Phase 5, FR-LLM-*). Ollama HTTP on localhost or a
+//! self-hosted box on your LAN (ADR-0007). The ONLY network egress WA originates
+//! for content, and it must stay on your own machine/network — `is_local`
+//! (loopback + private LAN) gates a "data leaves your network" warning for
+//! genuinely remote/internet endpoints.
use async_trait::async_trait;
use futures_util::StreamExt;
@@ -151,22 +153,38 @@ fn bullet_text(line: &str) -> Option
+ Summaries run on a local LLM. Point this at Ollama on this PC or another machine on your
+ LAN (e.g. 192.168.0.x) — both count as local, so nothing leaves your network.
+
Models: {settings.llmStatus.models.slice(0, 6).join(", ")}
+ {/if} + {:else} +