A privacy-first recording & transcription desktop app for macOS.
Wisp captures your microphone and system audio (the other side of a call) at
the same time and transcribes both on-device with Apple's SpeechAnalyzer.
- Offline-first — recordings and transcripts stay on your device. Nothing is uploaded; there is no online transcription service involved.
- On-device transcription — Uses
SpeechAnalyzer, the new API in Apple's Speech framework. Both the microphone and the far side of the call are transcribed locally. - System audio + microphone capture — Uses macOS 14.2+ Core Audio Process Taps. Each source is captured as a separate track.
- Built in Rust with a GPU-rendered shell — The window is built on GPUI, the framework that powers the Zed editor, and hosts the interface as an embedded web UI rendered with Kumo (Cloudflare's React component system) inside a wry webview. The bundle is served offline over a custom
wisp://scheme; no network is used at runtime. - Simple local storage — Recordings are stored as Ogg/Opus and metadata as SQLite under
$WISP_DATA_DIR, or$HOME/Library/Application Support/dev.mokmok.wisp/when the override is unset. Completed transcripts can be copied as plain text or exported as Markdown.
Wisp is a small Cargo workspace with cleanly separated concerns:
| Crate / target | Responsibility |
|---|---|
apps/wisp-desktop |
GPUI desktop shell. Handles setup, recording controls, session history, and transcript export. The UI itself is a React app (apps/wisp-desktop/ui) hosted in an embedded webview. |
apps/wisp-desktop/ui |
The web UI: Vite + React + Kumo components, bridged to the Rust host over IPC. |
crates/wisp-webview |
Vendored gpui-wry (from longbridge/gpui-kit) adapted to the official gpui crate: wry webview support for GPUI. |
crates/wisp-core |
Shared, platform-agnostic types (Session, Segment, IDs, SourceLabel). |
crates/wisp-audiokit |
macOS audio/transcription backends and the backend-neutral session orchestrator. |
crates/wisp-audiokit-sys |
Raw C ABI bindings to the macOS WispAudioKit library. |
crates/wisp-lifecycle |
Session lifecycle state machine used by the runtime and formal verification. |
crates/wisp-storage |
Session/segment persistence on SQLite (bundled rusqlite). |
native/WispAudioKit |
macOS Swift package handling Core Audio Process Tap capture and SpeechAnalyzer transcription. Linked into the Rust binary as a static library. |
Roughly, data flows like this:
Core Audio Process Tap ─┐
├─► WispAudioKit ─► wisp-audiokit ─► wisp-desktop (GPUI)
Microphone input ───────┘ │ ▲
├─► SpeechAnalyzer ─────────────┘
└─► recordings (Ogg/Opus) │
└─► wisp-storage (SQLite)
wisp-coreowns stableTrackId/SourceKind, device-nativeAudioFormat/AudioFrame,CaptureEvent, and partial/finalTranscriptEventcontracts. The existingSourceLabel::MicandSourceLabel::Systemmap to fixed track IDs, so storage and UI behavior stay compatible while future application/process tracks remain possible.wisp-audiokitowns capability probes,CaptureBackendandTranscriberBackend, andSessionOrchestrator. macOS production capture and transcription run through concreteMacosCaptureBackendandMacosTranscriberBackendadapters managed bySessionOrchestrator. Swift sends typed mic/system PCM into a bounded, nonblocking capture queue while retaining Ogg/Opus recording. Capture PCM reaches Rust first;MacosTranscriberBackend::pushthen submits only frames accepted bySessionOrchestratorback toSpeechAnalyzer.MacosCaptureBackendalso has an independent recording-only constructor so another transcriber could consume the exposed PCM without requesting speech permission. Transcript and compatibility callbacks retain their original ordering. The legacySessionAPI remains available but is no longer the macOS desktop path.- Real-time capture producers use a bounded, non-blocking frame queue.
Overflow is reported as a
CaptureEventwith the affected track and dropped PCM frame count (not packet count). The separate control queue is also bounded and cannot carry sample payloads. Microphone and system audio remain separate tracks.
- macOS 26 (Tahoe) — Wisp relies on
SpeechAnalyzer, Core Audio Process Taps, and the new Metal Toolchain, so macOS 26 is required. - Xcode 26 — for the Swift 6.0 / macOS 26 SDK.
- Rust 1.97.1 — pinned in
rust-toolchain.toml. - Microphone and speech-recognition permissions. macOS will prompt on first launch.
On macOS, first launch shows the microphone and speech-recognition permissions
needed for capture and on-device transcription. The desktop currently requests
the ja-JP transcription locale for each session.
To record and review a session:
- Select New Session, then Record. Wisp keeps microphone and system audio as separate tracks.
- Use Mute mic when needed, then Stop to drain and finalize the recording.
- Open a saved session from the library to review its transcript.
- Use Copy for
[MIC]/[SYS]plain text, or Export for a Markdown file with YAML frontmatter.
The application menu also provides these shortcuts:
| Action | Shortcut |
|---|---|
| Start / stop recording | ⌘R |
| Copy transcript | ⌘⇧C |
| Export transcript | ⌘⇧E |
A Nix flake is included, so the dev environment is one command away:
# Enter the dev shell
nix develop
# Build the web UI (embedded into the binary at compile time)
cd apps/wisp-desktop/ui && npm install && npm run build && cd -
# Run a debug build
cargo run -p wisp-desktopWithout the UI build step the app still runs and shows a placeholder page
pointing at the commands above. For hot reload during UI work, run
npm run dev in apps/wisp-desktop/ui and launch the app with
WISP_UI_DEV_URL=http://localhost:5183 cargo run -p wisp-desktop.
The default dev shell is turnkey on macOS: it provides the pinned Rust
toolchain, sccache, the treefmt formatter, applies the macOS
Xcode/DEVELOPER_DIR handling automatically, and installs the project's
pre-commit git hooks (treefmt + clippy) on entry. If you use
direnv, the committed .envrc (use flake) does all of
this on cd.
The flake does not expose a wisp-desktop Nix package: the desktop binary
needs host Xcode (swift for WispAudioKit, and the macOS SDK). Build it through
the dev shell instead:
nix develop -c cargo build -p wisp-desktop --release
# Formatting / hook checks available for the current Nix platform
nix flake checknix flake check runs the unified treefmt check (and related hook checks).
Workspace Clippy and tests stay on nix develop / the CI rust job for the
same Xcode reason.
Formatting is unified through treefmt-nix:
nix fmt (or treefmt inside the dev shell) formats Nix (nixfmt), Rust
(rustfmt, pinned to the workspace toolchain), and Swift (swiftformat) in one
pass, and the same configuration backs the treefmt flake check.
CI uses Determinate CI with
FlakeHub Cache so flake outputs built in GitHub
Actions are pushed to and pulled from FlakeHub. Locally, nix develop / nix build pick up the same cache when authenticated to FlakeHub (for example via
Determinate Nix).
If you'd rather use Rust + Xcode directly:
cargo build -p wisp-desktop --releaseSet WISP_DATA_DIR to override where sessions.db and the recordings/
directory are stored. When unset, Wisp uses
$HOME/Library/Application Support/dev.mokmok.wisp. HOME must be available
unless WISP_DATA_DIR is set. Settings are stored as settings.json.
If a completed transcript cannot be committed to SQLite, Wisp writes an
atomic transcript-recovery.json beside that session's Ogg files, blocks a
new recording, and retries reconciliation immediately or on the next launch.
Wisp exits before recording if the durable database cannot be opened; it never
treats an in-memory fallback as successful persistence.
The session worker protocol and navigation/session guards are checked against the production Rust implementation with Kani and Shuttle:
bash formal/check.shSee formal/README.md for setup, verified properties, and
the extension workflow.
See .github/workflows/release.yaml for how the release .app bundle is produced — pushing a v* tag builds Wisp.app on a macOS 26 runner.
- Copy transcript to clipboard (plain text) and export as Markdown (
.md) with a lightweight, CloudEvents-inspired YAML frontmatter envelope (id,type,source,time,subject, …). - Export to SRT / JSON.
- Speaker diarization within a single channel.
Issues and pull requests are welcome. Before sending a PR, run the checks that apply to your platform:
# Nix checks (the check set is platform-dependent; see Build & run)
nix flake check
# Complete Rust workspace checks on a supported host
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --all-targets
# Formal lifecycle checks (requires cargo-kani)
bash formal/check.sh
# Swift formatting (available in the Nix CI shell)
nix develop .#default --command swiftformat --lint \
native/WispAudioKit/Sources native/WispAudioKit/Tests
# macOS 26 only
swift test --package-path native/WispAudioKitTBD (will be added before public release).
