Skip to content
Closed
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
28 changes: 25 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,11 @@ 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 .deb's glibc floor usable on the oldest supported Ubuntu LTS.
- os: ubuntu-22.04
label: Linux x64
args: "--features whisper-vulkan"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -80,11 +87,26 @@ 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 headers +
# glslc compiler the whisper.cpp Vulkan backend needs (the runtime loader comes from the
# user's GPU driver, so nothing Vulkan-related is bundled).
- 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 \
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf \
libasound2-dev libpulse-dev meson ninja-build libclang-dev \
libvulkan-dev glslc spirv-headers spirv-tools glslang-tools \
libayatana-appindicator3-dev

# 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" }
80 changes: 80 additions & 0 deletions app/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,89 @@ 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 deps — libonnxruntime.so.1 and libcargs.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();
}

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 — including the versioned sonames
/// (`libonnxruntime.so.1`) the prebuilt libs require.
#[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;
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("libcargs")
|| name.starts_with("libonnxruntime"))
&& name.contains(".so");
if !is_runtime_lib {
continue;
}
let src = entry.path();
if !src.is_file() {
continue; // skip soname symlinks — the real lib is staged under its own name
}
let dst = staged.join(&name);
let stale = dst
.metadata()
.map(|m| m.len() != src.metadata().map(|s| s.len()).unwrap_or(0))
.unwrap_or(true);
if stale {
std::fs::copy(&src, &dst).unwrap_or_else(|e| panic!("stage {}: {e}", src.display()));
}
staged_any = true;
}

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

/// 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
44 changes: 31 additions & 13 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use tauri::{path::BaseDirectory, AppHandle, Emitter, Manager, State};

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
use wisp_aec::WebrtcEchoCanceller;
use wisp_audio::{
normalize_for_asr_in_place, tee, to_mono_16k, ChannelSource, EchoCancellingSource, MediaSource,
Expand Down Expand Up @@ -57,6 +57,8 @@ use wisp_pipeline::{
remap_to_original, transcribe_in_windows, EnergySegmenter, EnergyVad, GatedClip, LiveStream,
Segmenter, Session, Transcriber, Vad, DEFAULT_SILENCE_HANGOVER,
};
#[cfg(target_os = "linux")]
use wisp_pulseaudio::PulseMonitorSource;
#[cfg(target_os = "macos")]
use wisp_screencapture::ScreenCaptureSource;

Expand Down Expand Up @@ -611,11 +613,15 @@ fn coerce_param(raw: &serde_json::Value, kind: &ParamKind) -> Option<ParamValue>
}
}

/// Builds the GPU whisper.cpp engine from a downloaded GGUF model — Metal on macOS, Vulkan on Windows
/// (under the `whisper-vulkan` feature). Where the engine isn't built, the stub below reports it.
/// Builds the GPU whisper.cpp engine from a downloaded GGUF model — Metal on macOS, Vulkan on
/// Windows and Linux (under the `whisper-vulkan` feature). Where the engine isn't built, the stub
/// below reports it.
#[cfg(any(
target_os = "macos",
all(target_os = "windows", feature = "whisper-vulkan")
all(
any(target_os = "windows", target_os = "linux"),
feature = "whisper-vulkan"
)
))]
fn build_whisper_cpp_engine(
descriptor: &ModelDescriptor,
Expand All @@ -634,15 +640,18 @@ fn build_whisper_cpp_engine(

#[cfg(not(any(
target_os = "macos",
all(target_os = "windows", feature = "whisper-vulkan")
all(
any(target_os = "windows", target_os = "linux"),
feature = "whisper-vulkan"
)
)))]
fn build_whisper_cpp_engine(
_descriptor: &ModelDescriptor,
_dir: &Path,
_language: &str,
) -> WispResult<Box<dyn AsrEngine>> {
Err(WispError::Engine(
"the whisper.cpp GPU engine is only available on macOS, and on Windows GPU builds"
"the whisper.cpp GPU engine is only available on macOS, and on Windows/Linux GPU builds"
.to_owned(),
))
}
Expand Down Expand Up @@ -3178,7 +3187,16 @@ fn open_system_capture() -> Result<Box<dyn AudioSource>, String> {
.map_err(|e| e.to_string())
}

#[cfg(not(any(target_os = "macos", target_os = "windows")))]
/// Linux captures the default output's monitor through the PulseAudio/PipeWire server — one
/// click, no virtual device, like ScreenCaptureKit and WASAPI loopback.
#[cfg(target_os = "linux")]
fn open_system_capture() -> Result<Box<dyn AudioSource>, String> {
PulseMonitorSource::new()
.map(|s| Box::new(s) as Box<dyn AudioSource>)
.map_err(|e| e.to_string())
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn open_system_capture() -> Result<Box<dyn AudioSource>, String> {
Err("system-audio capture isn't available on this platform yet".to_owned())
}
Expand Down Expand Up @@ -3210,11 +3228,11 @@ fn open_mic_within(device: Option<String>) -> Result<Box<dyn AudioSource>, Strin
})
}

/// The echo canceller for this platform: WebRTC AEC on macOS (falling back to passthrough if it
/// won't init), passthrough elsewhere. Keeping it a `Box<dyn EchoCanceller>` lets the dual-stream
/// path stay identical on every platform — the cross-stream dedup handles residual echo where there
/// is no real AEC.
#[cfg(target_os = "macos")]
/// The echo canceller for this platform: WebRTC AEC on macOS and Linux (falling back to
/// passthrough if it won't init), passthrough elsewhere. Keeping it a `Box<dyn EchoCanceller>`
/// lets the dual-stream path stay identical on every platform — the cross-stream dedup handles
/// residual echo where there is no real AEC.
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn echo_canceller() -> Box<dyn EchoCanceller> {
match WebrtcEchoCanceller::new() {
Ok(c) => Box::new(c),
Expand All @@ -3225,7 +3243,7 @@ fn echo_canceller() -> Box<dyn EchoCanceller> {
}
}

#[cfg(not(target_os = "macos"))]
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn echo_canceller() -> Box<dyn EchoCanceller> {
Box::new(PassthroughEchoCanceller)
}
Expand Down
2 changes: 1 addition & 1 deletion app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"bundle": {
"active": true,
"targets": ["nsis", "dmg"],
"targets": ["nsis", "dmg", "deb", "appimage"],
"windows": {
"minimumWebview2Version": "120.0.2210.61"
},
Expand Down
9 changes: 9 additions & 0 deletions app/src-tauri/tauri.linux.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://schema.tauri.app/config/2",
"bundle": {
"resources": {
"resources/silero_vad.onnx": "resources/silero_vad.onnx",
"linux-runtime/*.so*": ""
}
}
}
4 changes: 2 additions & 2 deletions crates/wisp-engine-whisper-cpp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ wisp-core = { path = "../wisp-core" }
num_cpus = "1"

[features]
# Build whisper.cpp with the Vulkan backend on Windows (generic GPU — AMD/Intel/NVIDIA — with a CPU
# fallback). Off by default so the standard Windows build stays a no-op shell and needs no Vulkan SDK.
# Build whisper.cpp with the Vulkan backend on Windows/Linux (generic GPU — AMD/Intel/NVIDIA — with
# a CPU fallback). Off by default so the standard build stays a no-op shell and needs no Vulkan SDK.
# No effect on macOS, which always builds the Metal + Core ML backend.
vulkan = []

Expand Down
Loading