Website and downloads Β· Releases Β· Changelog Β· The earlier Rust/Tauri version: ghostpen-tauri
GhostPen, AI-driven text editing anywhere on your desktop, ported from Tauri
(Rust) to Oriel (Zig): the first full
application built on Oriel, using only its public oriel CLI and API.
Highlight text in any app, press the hotkey (or click the tray icon), pick an action (proofread, rewrite, translate, β¦); the result is pasted back in place. Also: a Playground with streaming answers, text extraction from clipboard images (OCR through a vision model), live captions of system audio, and voice dictation. Summarize web pages and export them as PDF or Markdown with Mermaid diagrams. Record meetings, revisit speaker-labelled transcripts, and keep summaries and answers alongside the recording.
Any OpenAI-compatible endpoint works (Ollama, LM
Studio, OpenAI, OpenRouter, Groq, β¦; the default is a local Ollama with
gemma4:e4b), or GhostPen runs a downloaded model itself: see
Built-in models.
Downloads (website, releases). The AppImage, the Windows installer and the macOS app update themselves (Settings β About & updates); deb and rpm installs say when a new version is out.
| Package | Whisper and Built-in models run on | |
|---|---|---|
| Linux | AppImage, .deb, .rpm | any GPU through Vulkan (NVIDIA, AMD, Intel); the CPU without one |
Linux, -cuda |
AppImage, .deb, .rpm | NVIDIA GPUs through CUDA (RTX 20xxβ50xx; needs the CUDA 13 runtime), else Vulkan |
| Windows 10/11 | setup.exe (per user) | any GPU through Vulkan (NVIDIA, AMD, Intel); the CPU without one |
| macOS 13+ | .dmg (Apple Silicon) | the GPU through Metal |
Linux needs GTK 4.12+ and WebKitGTK 6.0: Ubuntu 24.04, Debian 13, Fedora 39
or newer (Ubuntu 22.04 and Debian 12 ship older GTK 4; the .deb says so).
On Wayland the overlays sit at the screen's edge when gtk4-layer-shell is
installed (Arch: gtk4-layer-shell); without it they're ordinary windows.
The React frontend is GhostPen's own; the commands, events and settings keep the Tauri version's names and JSON shapes. On first run the Tauri app's settings are imported, and its downloaded whisper models are reused.
| Playground | Settings β Built-in models |
|---|---|
![]() |
![]() |
oriel doctor --fix # toolchain: Zig, Node.js, ...
oriel dev # run with hot reload
oriel build # zig-out/bin/ghostpen (+ ghostpen-cli)
oriel build -Dcuda # whisper on an NVIDIA GPU (CUDA toolkit)
oriel build -Dvulkan # ... or on any GPU through Vulkan (Linux and Windows releases; Windows needs the Vulkan SDK)
oriel package # deb, rpm, AppImage / setup.exe / .app + .dmgCtrl+Shift+A |
Menu for the selected text (hotkeys are editable in Settings) |
Ctrl+Shift+D |
Dictation: speak, β to finish (copied to the clipboard) |
Ctrl+Shift+L |
Live captions of what your computer plays |
| Tray icon | Menu, Dictation, Captions, Playground, Settings, Quit |
The same actions from a terminal or a desktop keybinding, handed to the
running instance: ghostpen --trigger | --voice-input | --captions | --settings | --playground. On Wayland without the GlobalShortcuts portal,
bind these in your compositor, e.g. Hyprland:
bind = CTRL SHIFT, A, exec, ghostpen --trigger
ghostpen-cli runs an action from the terminal with the same settings:
echo "teh quick brown fox" | ghostpen-cli proofread
ghostpen-cli translate --lang French --stream "Good morning"
ghostpen-cli profilesSelect a webpage link and choose Summarize link in GhostPen, or paste the URL into the Summary window. Choose Brief, Standard, or Detailed to set the depth. The answer streams into a reader with adjustable text size, font, and spacing. Read summary reads the answer aloud; Read page reads the source page.
The Summary window keeps exposed model thinking separate from the summary. The Show thinking switch requests thinking for the next summary and reveals any thinking already received, during generation or after it finishes. It is off by default, and the window remembers your choice. Turning it on after a run started with thinking off requires summarizing again to request thinking; the reader explains this when there is nothing to reveal. Models or providers may not expose thinking even when requested. Copy and Read summary use only the summary, even when thinking is visible.
Channels are normalized in the backend from provider reasoning fields, explicit leading reasoning blocks, or a thought channel opened by the local chat template. Ordinary headings and literal tokens in the answer stay intact. Output is limited to 1 MiB per request; exceeding the limit stops the summary with a recoverable error.
Mermaid code fences render as diagrams in the reader. Use Export β PDF to save the summary with those diagrams, or Export β Markdown to keep the editable Markdown and diagram source. Choose a location in the save dialog to write the file. Large diagrams continue in labeled, overlapping panels at a readable size; headings stay with their first panel. Invalid diagrams remain readable as source. PDF diagrams use a light palette even when the reader uses dark mode.
Keep the recording, speaker-labelled transcript, clean text, summary, and saved questions together. Play the audio while following individual words, jump to a line, or ask for decisions and action items with timestamps you can revisit.
Open Meetings and choose Record meeting to capture system audio and the microphone configured in Settings without showing live captions. Choose Stop and save when finished: the existing save flow prepares the transcript, speaker labels, clean text, and optional summary. Recording continues if you hide the window; reopen Meetings to stop it. A recording does not require Whisper to be loaded, but transcription after stopping needs the configured model downloaded. Live captions and recording share audio capture, so stop one before starting the other.
The meeting notebook uses the same paper-and-ink reader design as link summaries, with saved conversations beside the transcript, summary, and questions. You can browse earlier meetings while recording a new one.
The Notes tab is your own notebook for each meeting. Write during recording
or afterwards; notes save automatically, with Markdown preview and a manual
Save notes button. They are stored as notes.md in the meeting folder and
kept when you rerun transcription or summarization.
Use the meeting's Chat tab to ask questions across its transcript, clean text, personal notes, summary, and earlier questions. Latest note edits are saved before asking. Answers distinguish personal notes from spoken content.
Turn on π Meeting in the captions bar, then Start: the captions also
record what your computer plays and your microphone ("Me"). Stop saves the
meeting in Documents/GhostPen/Meetings/<date>_<time>/: the whole audio is
transcribed again (more accurately than the live captions), the speakers
are told apart by voice, and your AI profile proofreads it into a clean
text, with a title. Tray β Meetings lists them: play one and the line
being said is highlighted, click a line to play from it, rename speakers
(every line follows), merge them, move a line to another speaker, edit the
text, copy it as text, Markdown or subtitles, or proofread and find the
speakers again. Name a speaker and pick Remember this voice: later
meetings name them by themselves, live in the captions too. The
Summary tab summarizes the meeting (overview, decisions, action
items; long meetings in parts) and answers your own questions about it
("What did Alex say about me?"), each saved with the meeting and read
aloud on request.
Through speakers instead of a headset the microphone hears the call: those
lines are recognized as the others' (same words, same moment) and dropped.
Each meeting folder holds audio.wav, meeting.json, transcript.md and
final.md; nothing leaves the machine except the text sent to your AI
profile for the proofread.
Speakers come from Oriel's oriel.speakers (voice-detect.cpp) and the
WeSpeaker ResNet34 model trained on VoxCeleb (WeSpeaker, CC-BY-4.0;
downloaded the first time, 26 MB, SHA-256 verified).
A Kokoro-82M voice runs inside the app, on this computer, no network. When a result is shown in the menu, π Speak reads it (the S key does the same; press again to stop). A translated result reads back in that language's voice; the summary window adds Read summary and Read page with its own Stop. Long texts come out in sentence chunks, one after the next, so reading from the menu starts right away. Also from a terminal:
ghostpen --say "Good morning" --lang es --voice ef_doraThe voice is Oriel's oriel.tts: synthesis runs on the GPU where GhostPen
was built with one (-Dvulkan, -Dcuda), else on
the CPU, and the first sound comes after the first sentence. The first use
downloads the voice model (Kokoro 82M Q8_0, ~135 MB; SHA-256 verified) and
the voice pack for the text's language (511 KB each; 14 curated voices
covering the languages the translate menu offers) into
<data dir>/GhostPen/tts. Settings β Voices manages it all: download and
delete the model and the voice packs, with progress bars, where the voice
runs, and the phoneme-data row. The espeak-ng phoneme data ships with
GhostPen (espeak-ng-data next to the program), so no system package is
needed.
A note on licenses: the binaries that read text aloud link espeak-ng, which is GPL-3.0-or-later. GhostPen's own code stays MIT; espeak-ng's data and the Apache-2.0 Kokoro voices come with the shipped parts their licenses apply to (the packaged espeak-ng data itself is GPL'd, the voices Apache). No audio leaves the machine either way.
A profile set to Built-in runs the model inside GhostPen itself, with the llama.cpp compiled into it: no Ollama, LM Studio or other server, and nothing sent over the network. In Settings β Built-in models:
- Download one of the catalog models (Qwen3.5 2B/4B/9B, Gemma 3 4B,
Gemma 4 E4B) from Hugging Face. Downloads can be paused and resumed, and
are checked against the SHA-256 Hugging Face publishes. They go to
<data dir>/GhostPen/models(~/.local/share/GhostPen/modelson Linux). - GGUF files that LM Studio (
~/.lmstudio/models) or GhostReel (~/.ghostreel/models) already downloaded are found and reused, catalog or not. - Use points the "Built-in (GhostPen)" profile at a model (creating the profile if needed) and makes it active.
- Runner settings: the context window, the GPU (as many layers as its free memory holds; the rest runs on the CPU) and how long the model stays loaded between actions (10 minutes by default).
The model runs in a helper process (ghostpen --llm-helper, started
and stopped by GhostPen), so a crash or running out of memory can't take
the app down. The Linux and Windows releases run it on the GPU through Vulkan (any
vendor; the CPU without a Vulkan driver), and the -cuda releases through
CUDA on NVIDIA GPUs when the CUDA 13 runtime is installed (Vulkan
otherwise); build with -Dcuda for CUDA; macOS uses Metal. ghostpen-cli uses Built-in profiles too (ghostpen-cli models
lists them). Extract Text (reading an image's text) works on built-in models
that have their image projector, llama.cpp's mmproj: every catalog model has
one, downloaded with the model ("Add image support" for a model you already
have), and LM Studio's mmproj-*.gguf next to a model is picked up. Those
models show reads images in Settings.
Built-in profiles are new to this port: the Tauri app, if it reads the same settings, sees them as endpoints with no URL.
Measured on an RTX 4070 with an AMD Ryzen 7 7800X3D 8-Core Processor, GhostPen 0.2.4 built with the same CPU target, warm runs:
| CUDA | Vulkan | CPU | |
|---|---|---|---|
| Whisper small, 66 s of speech | 0.52 s | 0.60 s | 4.75 s |
| Qwen3.5 2B: prompt (1,390 tokens) | 12,858 tok/s | 10,571 tok/s | 193 tok/s |
| Qwen3.5 2B: generation | 216 tok/s | 195 tok/s | 36 tok/s |
| Ornith 1.5 9B Q4_K_M: prompt | 3,631 tok/s | 3,068 tok/s | |
| Ornith 1.5 9B Q4_K_M: generation | 74.9 tok/s | 69.6 tok/s |
Vulkan's first transcription after start also compiles its pipelines (1.5 s instead of 0.6 s).
The same app on both stacks: GhostPen's React frontend, the same commands,
settings and prompts. Measured on 2026-09-28 on one Linux machine (Ryzen 7
7800X3D, 16 threads, RTX 4070; Arch Linux, Hyprland) with Zig 0.16.0 and
Rust 1.98.1: GhostPen 0.2.10 on Oriel 0.6.14, and the Tauri version at
ab0c1c4 (0.1.3), both built for release with whisper on the GPU (CUDA).
On this Wayland desktop the Rust version runs in manual mode (no synthetic Ctrl+C, so the menu asks you to copy first; in this test it didn't pick up a manual copy either, and its Playground stopped responding). The Oriel version copies the selection and pastes the result itself.
Running ΒΉ
| Rust / Tauri 2 | Zig / Oriel | |
|---|---|---|
| Start β Settings window shown Β² | 214 ms | 265 ms |
| Idle memory, 20 s after start (PSS, app + WebKit processes) | 659 MB (app 266 MB) | 297 MB (app 142 MB) |
| Memory with the whisper model loaded Β³ | 962 MB (app 554 MB) | 805 MB (app 153 MB + whisper runner) |
| Idle CPU over 30 s | 0.06 s | 0.00 s |
| Transcribing a 6.1 s clip (large-v3-turbo q5_0, CUDA) β΄ | 0.38 s (first request 0.55 s) | 0.22 s (first request 0.33 s) |
| Web content sandboxed | no (WebKitGTK 4.1 default) | yes (bubblewrap, WebKitGTK 6.0) |
| Synthetic copy/paste on Wayland | no (manual mode) | yes (virtual keyboard) |
| Runs AI models itself | no (an endpoint: Ollama, LM Studio, β¦) | yes: Built-in models, or an endpoint |
GhostPen on Oriel creates a window (a web process each) only when it's first shown, and runs whisper in a runner process started on the first transcription and stopped after five idle minutes: the CUDA runtime and the model aren't in the app, and their memory goes back when it's unused.
Shipping
| Rust / Tauri 2 | Zig / Oriel | |
|---|---|---|
| App binary, stripped | 49.4 MB (whisper and its CUDA kernels inside) | 11.1 MB (whisper and llama.cpp) + 38.3 MB libggml-cuda.so |
| CLI binary, stripped | 7.8 MB | 1.5 MB |
.deb / .rpm |
30.3 MB / 30.3 MB (app and CLI, unstripped: 62 + 11 MB) | 24.0 MB / 24.6 MB (app, CLI and libggml-cuda.so, stripped) |
| AppImage | 546 MB (bundles GTK 3, WebKitGTK and 190 more libraries) | 25.1 MB (uses the system's WebKitGTK 6.0) |
| Toolkit (Linux) | GTK 3, WebKitGTK 4.1 | GTK 4, WebKitGTK 6.0 |
Building β΅
| Rust / Tauri 2 | Zig / Oriel | |
|---|---|---|
| Code (app + frontend, generated files excluded) | 8,086 lines (4,851 Rust, 2,227 TS, 1,008 CSS) | 10,191 lines (6,151 Zig, 3,108 TS, 932 CSS), with Built-in models, voice activity detection, draggable overlays |
| Dependencies | 627 crates (Cargo.lock) | 2 packages (Oriel, zigimg); Oriel itself has 9 |
| Clean release build, app and CLI | 289 s | 386 s |
| Release rebuild, nothing changed βΆ | 48 s | 0.7 s |
| Release rebuild after a one-line edit β· | 48 s | 50 s |
| Packages (deb, rpm, AppImage), after a build | 148 s | 25 s |
| Debug build (development), clean | 180 s | 46 s |
| Debug rebuild after a one-line edit βΈ | 3.4 s | 11.7 s (7.2 s with -Ddev_debug_info=false) |
| Build cache, release and debug builds | 7.7 GB target/ + 1.8 GB ~/.cargo/registry (shared by all Rust projects) |
1.9 GB (.zig-cache 1.7 GB + global 218 MB) + 256 MB sources (zig-pkg/) |
Rust's cache grows with every configuration built: GhostReel, a Tauri app
with two model runners, has 9.3 GB in target/ (7.5 GB of it debug
builds) on this machine, besides the shared registry.
ΒΉ On a private X display (Xvfb) with a private D-Bus session, the same for
both.
Β² From exec until the "GhostPen Settings" window is mapped (--settings),
median of 5 runs after a warm-up. About 40 ms of Oriel's difference is
WebKitGTK 6.0's process sandbox.
Β³ With the transcription server (GHOSTPEN_STT_SERVER=1) after four
requests. On Oriel, "app" is the app process alone; the whisper runner
holds the model and the CUDA runtime.
β΄ tests/speech.wav through the transcription server, the same model and
the same text back; the time is the HTTP round trip, median of the warm
requests. Oriel's whisper.cpp (1.9.4) skips the clip's silence with its
voice activity detection; the Rust version uses whisper-rs 0.15. The first
transcription after starting (or five idle minutes) also starts the runner,
about 0.1 s.
β΅ Fresh clones and empty build caches (Zig's global cache too), dependency
sources downloaded beforehand (npm, the Cargo registry, zig-pkg/). Rust:
tauri build --no-bundle --features captions-cuda, then
cargo build --release --features cli --bin ghostpen-cli; Oriel:
oriel build -Dcuda. Both include the frontend and whisper.cpp with its
CUDA kernels; the Oriel build also compiles llama.cpp for Built-in models.
βΆ The same command again. tauri build rebuilds the frontend, which makes
the app crate recompile, and the CLI (another feature set) recompiles it
once more. Zig caches by content, so nothing is recompiled.
β· A string in a log line of the app's main source file.
βΈ cargo build vs zig build build-dev (what oriel dev rebuilds). Rust
recompiles one crate incrementally; Zig compiles the whole app as one unit
through LLVM, and most of that time is debug info.
Run ghostpen logs to print the application log, or ghostpen logs --follow
(-f) to keep watching new entries. Press Ctrl+C to stop following.
These commands work while GhostPen is running and do not open a window.
Logs are stored in $XDG_DATA_HOME/dev.ghostpen.Oriel/app.log (default
~/.local/share/dev.ghostpen.Oriel/app.log) on Linux,
~/Library/Logs/dev.ghostpen.Oriel/app.log on macOS, and
%LOCALAPPDATA%\dev.ghostpen.Oriel\app.log on Windows.
GHOSTPEN_STT_SERVER=1 ghostpen also serves the whisper model as an
OpenAI-compatible API, so local tools (an agent that receives voice notes,
a video indexer) transcribe through GhostPen's model instead of loading
another: POST /v1/audio/transcriptions (multipart file, any format
ffmpeg reads; language; response_format json, text, verbose_json with
timestamped segments, srt or vtt), GET /v1/models, GET /health. It
binds GHOSTPEN_STT_BIND (default 0.0.0.0:8771), serves Settings β Live
Captions' model or GHOSTPEN_STT_MODEL, and shares it with captions and
dictation (one copy in memory). Needs ffmpeg.
Settings β About & updates shows the version, checks for a new release and,
with "Update automatically" on (the default), checks a minute after start
and every 12 hours. The Windows installer, the AppImage and the macOS app
update themselves (the new version starts on the next launch, or with
"Restart now"); deb/rpm installs and source builds only say a new version
exists. Each release publishes a latest.json signed with Ed25519 (the
GHOSTPEN_UPDATE_KEY secret); GhostPen installs only what matches the public
key in build.zig. GHOSTPEN_UPDATE_MANIFEST=<https url> checks another
manifest, e.g. to try a release before publishing it.
Declared with oriel permission add (see build.zig): accessibility
(synthetic copy/paste into other apps; macOS asks on first run), microphone
(dictation) and system audio (captions). On macOS, captions need a loopback
device such as BlackHole.
npm --prefix frontend test # Summary component: channels, toggle, Copy/TTS, snapshots
zig build test # AI client, settings, images, models
tests/e2e.sh # headless: select β trigger β Proofread β pasted; Playground streaming
tests/e2e-audio.sh # headless: captions overlay, dictation β proofread β clipboard
RECORD_ONLY=1 tests/e2e-meetings.sh # Meetings recorder β transcript, summary, playbackThe Summary regression fixture in src/fixtures/summary-gemma4-e4b.json records
a live E4B run with the original standard Summary prompt and think=false, including
the producer's actual initial channel. It also records the comparison run:
the old empty-thought prefill produced Thinking Process: β¦ <channel|>Answer,
while the plain model-turn prompt produced only the summary. Both runs used
the same synthetic article, seed 42, temperature 0.2, 4096-token context, and
a CPU-only validation build of the same application sources (Metal disabled
in the temporary build's Oriel dependency). A third captured run with
think=true contains both channels; its regression checks that no thinking
enters the answer before or after completion. This is evidence for that case,
not a guarantee about arbitrary unmarked model prose.
The fixture also keeps formatted_request and formatted_output for the
clarified Summary prompt. E4B could copy the old bulleted instructions as
- ## Key points and - ## Details when thinking was off. The instructions
now distinguish section headings, prose, and list items. The live check
requires actual heading lines and bullets; the component regression checks
that Key points and Details render as sections in a newly generated summary.
To repeat the live check with an already installed E4B model and your built GhostPen executable (no downloads or settings changes):
python3 tests/summary-live.py --exe zig-out/bin/ghostpen-dev \
--model "/path/to/gemma-4-E4B-it-Q4_K_M.gguf" --record /tmp/summary-live.jsonl
# Request thinking and check that it is followed by the summary:
python3 tests/summary-live.py --exe zig-out/bin/ghostpen-dev \
--model "/path/to/gemma-4-E4B-it-Q4_K_M.gguf" --thinkingThe end-to-end tests run on a private X display (Xvfb + D-Bus) against a mock
AI endpoint (tests/mock_openai.py); the audio test feeds tests/speech.wav
instead of the sound server ($GHOSTPEN_TEST_AUDIO).
| File | |
|---|---|
src/main.zig |
Commands, events, the menu flow, windows, tray, hotkeys, launch flags |
src/ai.zig |
OpenAI-compatible client: prompts, reasoning-leak retry, SSE streaming, vision, /models; local profiles go to local_llm.zig |
src/local_llm.zig, src/llm_helper.zig |
Built-in models: the runner's client (start, idle unload, cancel) and the runner (llama.cpp, JSON lines) |
src/llm_models.zig, src/chat_format.zig |
Model catalog, resumable verified downloads, models found on disk; prompt formats (Gemma 4/3, ChatML, Qwen thinking) |
src/settings.zig, src/store.zig |
Settings schema (Tauri-compatible) and the Oriel store |
src/captions.zig, src/dictation.zig, src/models.zig |
Whisper captions and dictation, one shared model |
src/image.zig |
PNG helpers for OCR and previews |
src/cli.zig |
ghostpen-cli |
frontend/ |
GhostPen's React UI (src/api.ts over Oriel's invoke/listen) |
MIT: see LICENSE.



