Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ jobs:
# source (no native deps), so it runs cleanly on Linux even though the app targets macOS/Windows.
- name: rustfmt (app crate)
run: cargo fmt --manifest-path app/src-tauri/Cargo.toml -- --check
# Same for the new Linux native crate — excluded from the root workspace like the other
# per-OS crates, so `cargo fmt --all` above skips it.
- name: rustfmt (linux pulseaudio crate)
run: cargo fmt --manifest-path crates/wisp-pulseaudio/Cargo.toml -- --check
# Exercise the exact dependency-free module used by build.rs with VC143/VC145-shaped fixtures.
- name: Test Windows runtime staging
run: |
Expand Down Expand Up @@ -114,6 +118,51 @@ jobs:
shell: bash
run: ./scripts/verify-windows-runtime.sh app/src-tauri/windows-runtime

# Proves the Linux app builds — including the Linux-ONLY code no other CI job compiles:
# the PulseAudio monitor source and the Linux WebRTC AEC build (meson + ninja + libclang).
# Ubuntu 22.04 keeps the glibc floor low; the default build has no whisper.cpp/Vulkan, so no
# Vulkan SDK is needed here (the release job adds it for the GPU build).
linux-app:
name: app builds (linux)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Linux system deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential pkg-config \
libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev \
libasound2-dev libpulse-dev ninja-build libclang-dev python3-pip
# jammy's apt meson is 0.61; the vendored WebRTC AEC build requires meson >= 0.63.
sudo pip3 install "meson>=0.63,<2"
# glslc is not in jammy's archive — pull the Vulkan toolchain from LunarG's jammy repo.
wget -qO /tmp/lunarg.asc https://packages.lunarg.com/lunarg-signing-key-pub.asc
wget -qO /tmp/lunarg.list https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list
sudo install -m644 /tmp/lunarg.asc /etc/apt/trusted.gpg.d/lunarg.asc
sudo install -m644 /tmp/lunarg.list /etc/apt/sources.list.d/lunarg-vulkan-jammy.list
sudo apt-get update
sudo apt-get install -y vulkan-sdk
- name: Build the frontend (tauri-build needs the dist)
working-directory: app
run: |
npm ci
npm run build
# Same staging race as Windows: the app's build.rs stages sherpa's .so files, so sherpa must
# build first or the resources check fails on the not-yet-present libs.
- name: Stage sherpa runtime libs
run: cargo build --manifest-path app/src-tauri/Cargo.toml -p wisp-engine-sherpa
- name: Build the app
run: cargo build --manifest-path app/src-tauri/Cargo.toml
# The Vulkan whisper.cpp backend is the port's GPU path; compile it on every PR so a breakage
# in build_linux_vulkan (cmake/glslc plumbing) cannot hide until release day.
- name: Build the app (whisper-vulkan)
run: cargo build --manifest-path app/src-tauri/Cargo.toml --features whisper-vulkan

# Proves the macOS app builds — including the macOS-ONLY code no other CI job ever compiles:
# ScreenCaptureKit system audio, the WebRTC AEC3 echo canceller, Metal whisper.cpp, and Apple
# SpeechAnalyzer. Without this job a macOS-only regression slips past CI and only surfaces on a real
Expand Down
36 changes: 33 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
#
# macOS: Wisp_<ver>_aarch64.dmg (Apple Silicon only)
# Windows: Wisp_<ver>_x64-setup.exe (NSIS, smaller) + Wisp_<ver>_x64_en-US.msi
# Linux: Wisp_<ver>_amd64.deb + Wisp_<ver>_amd64.AppImage
#
# Intel macOS is intentionally unsupported (Metal/screencapture need the macOS 15 SDK no Intel runner
# provides; WebRTC AEC won't cross-compile from Apple Silicon). The Windows build is mic + sherpa-onnx
# (system audio / AEC / Metal are macOS-only and degrade gracefully).
# provides; WebRTC AEC won't cross-compile from Apple Silicon). The Windows and Linux builds are the
# universal GPU installers (whisper.cpp on Vulkan, with ggml's CPU fallback when there's no Vulkan
# device; sherpa stays CPU).
#
# Trigger: push a `v*` tag (e.g. `git tag v0.2.0 && git push --tags`), or run manually with a tag.
#
Expand Down Expand Up @@ -57,6 +59,12 @@ jobs:
- os: windows-latest
label: Windows x64
args: "--features whisper-vulkan --config src-tauri/tauri.vulkan.conf.json"
# Same universal GPU build on Linux (Vulkan whisper.cpp + PulseAudio monitor system audio +
# WebRTC AEC). 22.04 keeps the glibc floor usable on the oldest supported Ubuntu LTS; the
# Vulkan loader is bundled into the artifacts like the Windows installer's vulkan-1.dll.
- os: ubuntu-22.04
label: Linux x64
args: "--features whisper-vulkan"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -80,11 +88,33 @@ jobs:
with:
node-version: 20

# The WebRTC AEC crate's bundled build compiles vendored WebRTC with meson + ninja (macOS only).
# The WebRTC AEC crate's bundled build compiles vendored WebRTC with meson + ninja.
- name: Install meson + ninja
if: runner.os == 'macOS'
run: brew install meson ninja

# Linux system deps: Tauri's webkit/gtk stack, ALSA for cpal mic capture, libpulse for the
# monitor system-audio source, meson+ninja for the WebRTC AEC build, and the Vulkan SDK for
# the whisper.cpp Vulkan backend. glslc is not packaged on Ubuntu 22.04, so the Vulkan pieces
# come from LunarG's official jammy repo. At package time `app/src-tauri/build.rs` bundles the
# loader (`libvulkan.so.1`) into the artifacts — the same approach as `vulkan-1.dll` on Windows —
# and the deb also declares `libvulkan1` as a dependency.
- name: Install Linux system deps
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential pkg-config curl wget file gnupg ca-certificates \
libwebkit2gtk-4.1-dev librsvg2-dev patchelf \
libasound2-dev libpulse-dev meson ninja-build libclang-dev \
libayatana-appindicator3-dev
wget -qO /tmp/lunarg.asc https://packages.lunarg.com/lunarg-signing-key-pub.asc
wget -qO /tmp/lunarg.list https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list
sudo install -m644 /tmp/lunarg.asc /etc/apt/trusted.gpg.d/lunarg.asc
sudo install -m644 /tmp/lunarg.list /etc/apt/sources.list.d/lunarg-vulkan-jammy.list
sudo apt-get update
sudo apt-get install -y vulkan-sdk

# Windows ships the universal GPU installer (whisper.cpp + Vulkan). ggml-vulkan's vulkan-shaders-gen
# sub-build nests deeply and overruns Windows' 260-char MAX_PATH from the default target dir, so
# shorten it; the Ninja generator needs cl.exe on PATH; and the SDK provides the headers +
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/target
**/target

# Build-time staging dir for the sherpa/onnxruntime .so copies the Linux bundle ships
/app/src-tauri/linux-runtime/

# Node / frontend
node_modules/
dist/
Expand Down
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ resolver = "2"
members = ["crates/*"]
# Kept out of the library CI build: the Tauri app, and the per-OS native crates (heavy vendored
# libs built at compile time — sherpa/onnxruntime, whisper.cpp/Metal — or OS frameworks:
# ScreenCaptureKit, WebRTC AEC3, WASAPI loopback). The Windows one is covered by the app build on
# the Windows CI runner; the macOS ones by running the app on a Mac.
# ScreenCaptureKit, WebRTC AEC3, WASAPI loopback, PulseAudio monitor). The Windows one is covered
# by the app build on the Windows CI runner; the macOS ones by running the app on a Mac; the Linux
# one by the app build on the Linux release runner.
exclude = [
"app/src-tauri",
"crates/wisp-engine-sherpa",
"crates/wisp-engine-whisper-cpp",
"crates/wisp-screencapture",
"crates/wisp-loopback",
"crates/wisp-pulseaudio",
"crates/wisp-aec",
"crates/wisp-embed",
]
Expand Down
36 changes: 36 additions & 0 deletions app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ sherpa-cpu = ["wisp-engine-sherpa/download-binaries"]
# weekly smoke workflow (cargo build --no-default-features --features sherpa-gpu) — a ~2h compile,
# too slow to gate every PR. Mutually exclusive with sherpa-cpu (sherpa-rs-sys enforces this).
sherpa-gpu = ["wisp-engine-sherpa/directml"]
# whisper.cpp with the Vulkan GPU backend on Windows (generic GPU — AMD/Intel/NVIDIA — with a CPU
# fallback). Off by default; the Windows GPU release turns it on. No effect on macOS (always Metal).
# whisper.cpp with the Vulkan GPU backend on Windows and Linux (generic GPU — AMD/Intel/NVIDIA —
# with a CPU fallback). Off by default; the Windows and Linux GPU releases turn it on. No effect on
# macOS (always Metal).
whisper-vulkan = ["wisp-engine-whisper-cpp/vulkan", "wisp-models/whisper-vulkan"]

[build-dependencies]
Expand Down Expand Up @@ -100,3 +101,13 @@ wisp-engine-whisper-cpp = { path = "../../crates/wisp-engine-whisper-cpp" }
# Total physical memory (GlobalMemoryStatusEx) to recommend a machine-appropriate model. Same
# `windows` major as wisp-loopback, so no extra version in the tree.
windows = { version = "0.61", features = ["Win32_System_SystemInformation"] }

# Linux-only native deps. System-audio capture via the PulseAudio/PipeWire monitor — the counterpart
# to macOS ScreenCaptureKit and Windows WASAPI loopback, the same one-click "capture the meeting"
# experience — plus WebRTC AEC3 echo cancellation (also used on macOS).
[target.'cfg(target_os = "linux")'.dependencies]
wisp-pulseaudio = { path = "../../crates/wisp-pulseaudio" }
wisp-aec = { path = "../../crates/wisp-aec" }
# whisper.cpp ASR engine — built with the Vulkan GPU backend only under the `whisper-vulkan` feature
# (generic GPU + CPU fallback); an empty shell in the default CPU build.
wisp-engine-whisper-cpp = { path = "../../crates/wisp-engine-whisper-cpp" }
150 changes: 150 additions & 0 deletions app/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,159 @@ fn main() {
#[cfg(target_os = "windows")]
stage_windows_runtime_libs();

#[cfg(target_os = "linux")]
{
// The sherpa-onnx + onnxruntime shared libs the binary links against land in the cargo
// target dir at build time. `tauri dev` finds them via `$ORIGIN`; the .deb/AppImage ship
// them under <prefix>/lib/Wisp (the Tauri resource dir). Emit RPATH (not the modern
// RUNPATH, which does not apply transitively) so libsherpa-onnx-c-api.so also resolves
// its own dep — the unversioned libonnxruntime.so — from the same directory.
println!("cargo:rustc-link-arg=-Wl,--disable-new-dtags");
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/../lib/Wisp");
stage_linux_runtime_libs();
stage_linux_vulkan_loader();
}

tauri_build::build()
}

/// Copies the sherpa-onnx + onnxruntime runtime libs from the cargo target dir into
/// `linux-runtime/` (next to this build script) so `tauri.linux.conf.json` can bundle them into
/// the installer's resource dir. Every `lib*.so*` the binary needs is staged flat, so the
/// `$ORIGIN/../lib/Wisp` rpath finds each DT_NEEDED name (the sherpa-rs-sys prebuilt libs are
/// unversioned — plain `libonnxruntime.so`, `libsherpa-onnx-c-api.so`, `libsherpa-onnx-cxx-api.so`).
#[cfg(target_os = "linux")]
fn stage_linux_runtime_libs() {
use std::path::Path;

let target_dir = cargo_target_dir_linux();
let staged = Path::new(env!("CARGO_MANIFEST_DIR")).join("linux-runtime");
std::fs::create_dir_all(&staged).expect("create linux-runtime dir");

let mut staged_any = false;
let mut seen = std::collections::HashSet::new();
for entry in std::fs::read_dir(&target_dir)
.unwrap_or_else(|e| panic!("read {}: {e}", target_dir.display()))
{
let entry = entry.expect("read target dir entry");
let name = entry.file_name().to_string_lossy().into_owned();
let is_runtime_lib = (name.starts_with("libsherpa-onnx") || name.starts_with("libonnxruntime"))
&& name.contains(".so")
// The C++ wrapper lib is unused — the Rust FFI binds the C API only, and neither the
// macOS nor the Windows upstream installer ships it.
&& name != "libsherpa-onnx-cxx-api.so";
if !is_runtime_lib {
continue;
}
let src = entry.path();
if !src.is_file() {
continue; // dirs, fifos etc. — a soname symlink resolves through and copies under its link name
}
let dst = staged.join(&name);
let stale = match (dst.metadata(), src.metadata()) {
(Ok(d), Ok(s)) => d.len() != s.len() || s.modified().ok() > d.modified().ok(),
_ => true,
};
if stale {
std::fs::copy(&src, &dst).unwrap_or_else(|e| panic!("stage {}: {e}", src.display()));
}
seen.insert(name);
staged_any = true;
}

// Drop staged libs the target dir no longer produces (e.g. after a sherpa version bump), so
// linux-runtime can't keep shipping stale copies.
for entry in std::fs::read_dir(&staged)
.unwrap_or_else(|e| panic!("read {}: {e}", staged.display()))
.flatten()
{
let name = entry.file_name().to_string_lossy().into_owned();
let stale_lib = (name.starts_with("libsherpa-onnx") || name.starts_with("libonnxruntime"))
&& name.contains(".so")
&& !seen.contains(&name);
if stale_lib {
let _ = std::fs::remove_file(entry.path());
}
}

assert!(
staged_any,
"no sherpa/onnxruntime shared libs found in {} — build wisp-engine-sherpa first",
target_dir.display()
);
}

/// Upstream ships the Vulkan runtime (`vulkan-1.dll` + license) inside the Windows installer —
/// see `scripts/stage-vulkan-loader.sh` — so the whisper.cpp Vulkan build launches even with no
/// Vulkan loader installed; the loader then discovers whatever ICD the GPU driver provides.
/// Mirror that here: with `whisper-vulkan` enabled the binary has a hard `DT_NEEDED` on
/// `libvulkan.so.1`, and the loader is a separate package (`libvulkan1`/`vulkan-loader`), not part
/// of the GPU driver — absent on minimal installs it would stop the app before `main`, making
/// ggml's CPU fallback unreachable. Stage the build host's loader next to the sherpa libs so the
/// `$ORIGIN/../lib/Wisp` rpath always finds one; a bundled loader still loads the host's ICDs.
#[cfg(target_os = "linux")]
fn stage_linux_vulkan_loader() {
use std::path::Path;

if std::env::var_os("CARGO_FEATURE_WHISPER_VULKAN").is_none() {
return;
}
let staged = Path::new(env!("CARGO_MANIFEST_DIR")).join("linux-runtime");
for dir in [
"/usr/lib/x86_64-linux-gnu",
"/lib/x86_64-linux-gnu",
"/usr/lib64",
"/usr/lib",
] {
let src = Path::new(dir).join("libvulkan.so.1");
if !src.is_file() {
continue;
}
// Resolve the soname symlink and stage the real loader under the NEEDED name.
let real = std::fs::canonicalize(&src).unwrap_or(src);
let dst = staged.join("libvulkan.so.1");
let stale = dst
.metadata()
.map(|m| m.len() != real.metadata().map(|s| s.len()).unwrap_or(0))
.unwrap_or(true);
if stale {
std::fs::copy(&real, &dst).unwrap_or_else(|e| panic!("stage {}: {e}", real.display()));
}
std::fs::write(
staged.join("VULKAN_LOADER_LICENSE.txt"),
"The bundled libvulkan.so.1 is the Khronos Vulkan loader (Vulkan-Loader),\n\
copyright the Khronos Group and contributors, licensed under Apache-2.0.\n\
Source: https://github.com/KhronosGroup/Vulkan-Loader\n",
)
.expect("write Vulkan loader attribution");
return;
}
panic!(
"the whisper-vulkan build links libvulkan.so.1 but none was found on the build host — \
install libvulkan1 or the LunarG vulkan-sdk so the loader can be bundled"
);
}

/// The cargo `target/<profile>` dir, found by walking up from `OUT_DIR` — the same place
/// sherpa-rs-sys copies its shared libs to. Same logic as the Windows `cargo_target_dir` below.
#[cfg(target_os = "linux")]
fn cargo_target_dir_linux() -> std::path::PathBuf {
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
let profile = std::env::var("PROFILE").unwrap();
let mut dir = out_dir.as_path();
while let Some(parent) = dir.parent() {
if parent.ends_with(&profile) {
return parent.to_path_buf();
}
dir = parent;
}
panic!(
"could not find target/{profile} above OUT_DIR {}",
out_dir.display()
);
}

/// Copies the sherpa-onnx + onnxruntime runtime DLLs from the cargo target dir into
/// `windows-runtime/` (next to this build script) so they can be bundled as Tauri resources. A
/// size check skips the copy when already current, so the build script doesn't perpetually re-fire.
Expand Down
Loading