System-wide, push-to-talk AI voice dictation for Linux.
Hold a shortcut, speak, then release. Oto transcribes your voice, optionally polishes the result, and inserts it into the application you were using.
Features · Installation · Linux setup · Development · Troubleshooting
Note
Oto is an early Linux desktop release. Desktop integration can vary between compositors, portals, accessibility trees, and target applications.
Capture
- Push-to-talk, toggle, or hybrid activation — a quick tap goes hands-free, a hold behaves like push-to-talk.
- Silence detection ends hands-free sessions on its own, using the provider's endpointing when streaming.
- Microphone selection, input gain, and an adaptive noise gate.
- Optional synthesized cues for start, stop, insertion, and errors.
Transcription
- Live streaming transcription through Deepgram's WebSocket API, so the transcript is ready almost as soon as you stop speaking. Any connection failure falls back to a single upload.
- Cloud transcription through Deepgram (Nova-3) and OpenAI-compatible APIs, or fully offline with
whisper-rs. - Provider profiles for Deepgram, OpenAI, Groq, OpenRouter, and compatible custom endpoints.
Text
- Optional LLM cleanup with tone guidance, style presets, and protected vocabulary.
- Spoken edits: "scratch that" retracts what you just said, "new paragraph" and "new line" insert breaks.
- Replacement rules for the words a model always gets wrong, applied after cleanup.
- Exact-trigger voice snippets and select-and-rewrite Command Mode.
Modes and context
- Per-application Modes override the whole pipeline — provider, model, cleanup, style, vocabulary, insertion method — matched by application class and window title, with an optional dedicated shortcut.
- Tiered context sharing tells the cleanup model where the text is going, so it formats for a terminal differently than for an email. Password managers, keyrings, and authenticators are always excluded and disclose nothing.
History and insight
- Local, capped history with full-text search, copy, re-insert, and delete.
- Optional audio retention with playback and re-transcription against different settings.
- Audio-file transcription through the same pipeline as dictation.
- Usage stats: words dictated, sessions, estimated time saved, and daily streaks.
Desktop integration
- Global shortcuts on X11 and Wayland, including XDG GlobalShortcuts, Hyprland, and niri.
- Layered text insertion through AT-SPI, virtual-keyboard typing, clipboard and paste, or clipboard-only fallback — with your clipboard restored afterwards.
- Undo the last insertion from the tray, guarded so it can never delete text it did not write.
- Guided first-run setup covering permissions, microphone, provider, shortcut, and a live test.
- API keys stored in the operating system keyring, separate from the JSON configuration.
- Configurable themes, text size, reduced motion, overlay behavior, and keyboard focus.
- Explicit, user-controlled JSON sync for dictionary entries, snippets, and styles.
flowchart LR
A[Hotkey or silence] --> B[Capture microphone]
B --> N[Resolve Mode from focused window]
N --> C[Stop: release, second tap, or silence]
C --> D[Speech-to-text, streaming or upload]
D --> O[Spoken edits]
O --> E{Snippet trigger?}
E -- Yes --> G[Exact expansion]
E -- No --> F{Dictation or command?}
F -- Dictation --> K[Optional LLM cleanup]
F -- Command --> L[Rewrite selected text]
K --> P[Replacement rules]
L --> P
P --> G[Insert text]
G --> H[AT-SPI]
G --> I[Clipboard and paste]
G --> J[Virtual-keyboard typing]
G --> M[Clipboard fallback]
Pressing the shortcut starts recording and displays the overlay. Releasing it stops the recorder, sends the captured audio to the selected transcription engine, optionally polishes the transcript, and inserts the result into the previously focused application.
Oto includes a source installer for the major Linux distribution families. It detects the distribution, X11 or Wayland session, and desktop/compositor; installs and verifies the native build requirements; clones Oto; runs the frontend checks; builds the appropriate bundle; and installs it.
| Distribution family | Default bundle |
|---|---|
| Debian, Ubuntu, Linux Mint, Pop!_OS | .deb |
| Fedora, RHEL and compatible mutable systems | Flatpak |
| Arch, Manjaro, EndeavourOS, CachyOS | AppImage |
Download and review the installer, then run it as your normal desktop user.
Do not run the whole script with sudo; it requests elevation only for
system packages and system-wide bundle integration.
curl -fsSLO https://raw.githubusercontent.com/0veek/oto/master/install.sh
less install.sh
chmod +x install.sh
./install.shBy default, the source checkout and build cache are kept under
~/.cache/oto-installer/source. Re-running the command updates that checkout
with a fast-forward pull and rebuilds Oto. Useful overrides include:
# Build a particular release
./install.sh --ref v0.1.0
# Override the distro's default format
./install.sh --bundle appimage
# Produce the bundle without installing it
./install.sh --build-only
# Build a checkout you already cloned (useful for contributors)
./install.sh --local .Run ./install.sh --help for all options. The supported bundle overrides are
deb, flatpak, and appimage. Automatic .deb installation remains limited
to Debian-family systems; Flatpak and AppImage can be selected on any supported
family.
Note
Fedora Silverblue, Kinoite, and other rpm-ostree desktops need a mutable
build environment. Run the installer inside a Fedora
Toolbx and choose --build-only, then install
the resulting Flatpak on the host.
A setup wizard runs the first time Oto starts and walks through the desktop capability check, microphone selection, provider and API key, shortcut and activation mode, and a live microphone, transcription, and insertion test. It can be skipped, and it never reappears for an existing configuration.
To configure manually instead:
- Open Providers, choose a preset, and save an API key, or add a compatible provider.
- Under Models, choose cloud transcription or a local Whisper model.
- Under Audio, pick a microphone and an activation mode.
- Keep the default
Ctrl+Shift+Spaceshortcut or select an unused chord. - Run Test microphone, Test transcription, and Test insertion.
- Focus a text field, hold the shortcut while speaking, and release it to transcribe.
If the global shortcut cannot be registered, use Start Listening and Stop Listening from the system tray.
Oto uses different desktop services and insertion tools depending on the active session.
| Environment | Components |
|---|---|
| X11 | Tauri global shortcut support; xdotool for simulated input |
| Wayland | xdg-desktop-portal plus the portal backend for your compositor |
| GNOME Wayland | xdg-desktop-portal-gnome; ydotool recommended for input |
| Hyprland | xdg-desktop-portal-hyprland; Oto creates a runtime global binding |
| niri | No portal shortcut backend — Oto reads the chord from /dev/input, or writes a toggle bind to ~/.config/niri/oto.kdl. See niri |
| Secure key storage | A Secret Service implementation such as GNOME Keyring |
Oto first tries direct AT-SPI insertion. If the target application does not expose an editable accessibility object, it falls back to clipboard and simulated paste, direct typing, and finally clipboard-only delivery.
For reliable insertion on Wayland, install ydotool and run its user daemon.
wtype is an optional fallback.
# Arch
sudo pacman -S --needed ydotool wtype wl-clipboard xdg-desktop-portal
# Fedora
sudo dnf install ydotool wtype wl-clipboard xdg-desktop-portal
# Debian / Ubuntu
sudo apt install ydotool wtype wl-clipboard xdg-desktop-portalOn GNOME, keep xdg-desktop-portal-gnome installed. On Hyprland, install and
run xdg-desktop-portal-hyprland.
ydotoold needs access to /dev/uinput, which commonly requires membership in
the input group:
sudo usermod -aG input "$USER"niri needs its own path because it can supply neither half of a hold-to-talk shortcut. It has no GlobalShortcuts portal backend, and its own keybinds fire on key press only — there is no release event to bind.
Worse, the failure is not obvious: niri sessions usually run
xdg-desktop-portal-gnome, which advertises GlobalShortcuts while the
implementation lives in gnome-shell. Probing the interface therefore succeeds and
binding then fails. Oto detects niri up front and never takes that path.
Two backends are used instead, in order:
1. Direct input (preferred). Oto reads key events from /dev/input/event*,
which gives real press and release, so push-to-talk, Toggle, and Hybrid all work.
It needs read access to the input devices:
sudo usermod -aG input "$USER" # then log out and back inOto matches only your configured chord. No other keystroke is stored, logged, or sent anywhere, and a chord with no modifier is refused outright — reading is not grabbing, so a bare key would start dictation every time you typed it.
2. niri config binds (fallback). Without input access, Oto writes its chords
to ~/.config/niri/oto.kdl and adds one include line to ~/.config/niri/config.kdl,
backing that file up to config.kdl.oto-backup first. niri hot-reloads the change.
Because niri binds carry no release, these are toggles: press once to start,
press again to stop, or let silence detection end the take. Set Activation to
Toggle in settings — Hold and Hybrid cannot work on this path.
The generated file also carries a window rule that keeps the recording overlay floating, instead of letting niri give it a column in the scrolling layout:
window-rule {
match app-id=r#"^oto$"# title="^Oto$"
open-floating true
focus-ring { off; }
border { off; }
shadow { off; }
}To remove Oto's binds, delete ~/.config/niri/oto.kdl and the matching include
line. Everything else works normally: niri supports the virtual-keyboard and
data-control protocols, so wtype, ydotool, and clipboard insertion all behave
as they do on any other wlroots-style compositor.
Per-application Modes, context sharing, and focus restore use niri msg and need
the niri CLI on PATH.
Fully log out of the desktop session and log back in so the new group is applied. Then enable the user service:
groups | grep -w input
systemctl --user enable --now ydotool.service
systemctl --user status ydotool.serviceVerify input by focusing a text field in another application and running:
ydotool type -- 'hello from ydotool 'Important
Use systemctl --user, not the system-level systemctl command. If the
service previously reached its restart limit, run
systemctl --user reset-failed ydotool.service before starting it again.
For portal errors, /dev/uinput permission issues, focus problems, and
application-specific insertion failures, see the
Wayland and GNOME troubleshooting guide.
Oto supports Deepgram’s native listen API for speech-to-text, plus OpenAI-compatible endpoints for transcription and optional transcript polishing.
| Preset | Base URL | Default transcription model | Default polish model |
|---|---|---|---|
| Deepgram | https://api.deepgram.com |
nova-3 |
— (STT only; uses smart_format) |
| OpenAI | https://api.openai.com/v1 |
whisper-1 |
gpt-4o-mini |
| Groq | https://api.groq.com/openai/v1 |
whisper-large-v3 |
llama-3.1-8b-instant |
| OpenRouter | https://openrouter.ai/api/v1 |
openai/whisper-1 |
openai/gpt-4o-mini |
| Custom | User supplied | whisper-1 |
gpt-4o-mini |
Deepgram keys use Authorization: Token … against /v1/listen. Dictionary terms
map to Nova-3 keyterm prompting.
Polish and Command Mode need an OpenAI-compatible chat model; switch provider or
use a custom profile if you need LLM post-processing.
Provider capabilities and model identifiers can change independently of Oto. Confirm that a custom endpoint implements audio transcriptions and, when polish is enabled, chat completions.
Choose Models → Local Whisper and provide the absolute path to a
whisper.cpp-compatible ggml-*.bin model. See the
whisper.cpp model guide
for model sizes and download options.
Leave the language empty for automatic detection. Use a non-.en model for
multilingual speech. Oto caches the selected model after its first load and can
show live partial results while recording.
For a fully local pipeline, disable polishing or point a custom provider at a
localhost OpenAI-compatible LLM. Keyless profiles are allowed only for
http://localhost and http://127.0.0.1; remote endpoints require a key.
- A snippet expands only when its trigger is the complete utterance. A trigger
named
my signaturematchesmy signatureorsnippet my signature, but not a longer sentence containing those words. - Style presets and the free-form tone hint are combined when polishing.
- Command Mode rewrites selected text from a spoken instruction. It reads the selection through AT-SPI when possible and otherwise uses simulated copy.
Command Mode always requires a chat-completions model, even when normal dictation polishing is disabled.
| Mode | Behavior |
|---|---|
| Hold | Press and hold while speaking, release to transcribe. The default. |
| Toggle | Press once to start, press again to stop. |
| Hybrid | A release faster than the tap threshold leaves the session running; holding longer behaves like push-to-talk. |
Silence detection ends hands-free sessions only. A held hotkey always ends on its own release, so a pause mid-sentence can never cut you off. When streaming through Deepgram, the provider's end-of-utterance signal is preferred over local energy measurement, with the minimum-speech guard still applied.
A Mode overrides any part of the pipeline for a specific application: speech-to-text backend, provider, model, cleanup on/off, cleanup model, style, tone, extra vocabulary, insertion method, and context level. Modes are matched top to bottom against the focused window's class and title, and the first match wins; anything a Mode does not set is inherited from the global settings.
- Settings → Modes → Read focused window reports the class and title Oto sees for the window you were last in, along with the exact context that would be sent.
- A Mode with no match rule never applies automatically. It is reachable only through its own shortcut.
- A Mode chord that the compositor refuses is reported but never costs you the primary dictation shortcut.
Window identification works on Hyprland (hyprctl), niri (niri msg), Sway and
i3 (swaymsg), X11 (xdotool), and falls back to AT-SPI on GNOME and KDE
Wayland.
Cleanup produces better formatting when the model knows where the text is going. That information also leaves your machine, so each level is opt-in:
| Level | What is sent |
|---|---|
| Nothing | Only what you said. |
| Application name | slack, kitty, … (default) |
| Application and window title | Titles often contain paths, channel names, subjects. |
| Application, title, and nearby text | Selection or surrounding field, read through AT-SPI. |
Password managers, keyrings, and authenticators are always excluded. A blocked application discloses nothing at all — not even its name, since that alone would reveal where you are typing. The blocklist can be extended but never shortened. Context is only ever sent when cleanup is enabled.
scratch that,delete that,forget that, andstrike thatretract the clause just spoken, back to the previous sentence and no further.new paragraphandnew lineinsert breaks.
Spoken edits are applied before cleanup, so the model never sees the retracted words and cannot decide to keep them.
Replacement rules run last, after cleanup, making them the final say on spelling. Each rule can match whole words only and can be case sensitive. Unlike dictionary terms, which only nudge the recogniser, replacements always apply.
History → Teach a correction turns a transcript you fix by hand into rules. Only consistent one-for-one word substitutions are proposed; insertions, deletions, rewrites, and pure case changes are ignored, because those are edits rather than the kind of repeated mis-hearing a literal rule can fix. Nothing is saved until you accept it.
History is local and capped. Entries can be searched, copied, re-inserted, and deleted. With Keep dictation audio enabled, each entry keeps its recording so it can be replayed or re-transcribed against different settings; deleting an entry or trimming past the limit removes its audio with it. Audio files on disk can also be transcribed through the same pipeline.
Stats are derived from history alone — there is no separate tracking store, and disabling history disables stats. Time saved is an estimate comparing 40 wpm of typing against 150 wpm of speech.
The default shortcut is Ctrl+Shift+Space.
- Wayland uses the XDG GlobalShortcuts portal.
- Hyprland also receives the compositor-side runtime binding required by the portal.
- X11 uses Tauri's native global-shortcut plugin.
- Oto never overwrites an existing Hyprland binding.
- Pressing the shortcut starts listening; releasing it starts processing.
Desktop environments often reserve Super shortcuts, and input methods may
reserve chords such as Ctrl+Alt+Space. Prefer an unused Ctrl+Shift+… chord.
Text insertion has four modes:
| Mode | Behavior |
|---|---|
| Auto | Try AT-SPI, clipboard and simulated paste, direct typing, then clipboard-only |
| Direct type | Type through ydotool, wtype, or xdotool |
| Clipboard + paste | Copy the transcript and invoke a supported paste simulator |
| Clipboard only | Copy the transcript without generating keyboard input |
After a clipboard-based insertion, Oto restores whatever was on your clipboard beforehand — but only if the transcript is still there, so a clipboard you changed in the meantime is left alone.
Undo Last Insertion in the tray removes the text Oto last inserted. It is deliberately narrow: it refuses once the insertion is older than 45 seconds, once focus has moved to a different application, or when the text is longer than 2000 characters. Undo deletes backwards from the caret, which is only correct while the caret is still where the insertion left it.
On Hyprland and niri, Oto attempts to restore the target captured when recording began. On GNOME Wayland, keep the target field focused through the processing stage.
Non-secret settings are normally stored at:
~/.config/oto/config.json
API keys are stored separately through Secret Service under dev.oto.app, with
one account per provider preset. Oto rejects attempts to serialize keys into the
configuration file.
History is normally stored at:
~/.local/share/oto/history.json
Retained dictation audio, when enabled, is stored alongside it:
~/.local/share/oto/audio/<entry-id>.wav
Sync is disabled by default and runs only when Sync now is pressed. The
configured HTTPS endpoint must support GET and PUT for one JSON document;
plain HTTP is accepted only for localhost. Sync includes dictionary entries,
snippets, and styles—not provider credentials, audio, history, or general
settings. An optional bearer token is stored in the OS keyring.
| Requirement | Purpose |
|---|---|
| Node.js 20.19+ or 22.12+ and npm | SvelteKit frontend and Tauri CLI |
| Stable Rust toolchain, Clang, and CMake | Tauri backend and local Whisper bindings |
| Tauri 2 Linux prerequisites | Desktop build libraries |
webkit2gtk-4.1 |
WebView runtime |
| ALSA development libraries | Microphone capture through cpal |
| libsecret development libraries | Secure API-key storage |
| libayatana-appindicator development libraries | System tray and Linux package generation |
| A working microphone | Dictation input |
Package names vary by distribution. A typical Arch or CachyOS setup is:
sudo pacman -S --needed base-devel webkit2gtk-4.1 \
libayatana-appindicator alsa-lib libsecret nodejs npm rust clang cmake \
patchelf wtype ydotool wl-clipboardAppIndicator is required for production packaging because Oto includes a system tray:
| Distribution | Package |
|---|---|
| Arch / CachyOS | libayatana-appindicator |
| Debian / Ubuntu | libayatana-appindicator3-dev |
| Fedora | libayatana-appindicator-gtk3-devel |
Confirm that pkg-config can find it:
pkg-config --exists ayatana-appindicator3-0.1 && echo "appindicator ok"# Install JavaScript dependencies
npm install
# Run the desktop app with frontend hot reload
npm run tauri dev
# Check the Svelte frontend
npm run check
# Build the frontend
npm run build
# Run Rust tests and compile checks
cd src-tauri
cargo test
cargo checkThe Tauri development process opens the settings window and keeps the overlay preloaded but hidden until dictation starts.
Build all configured Linux package formats with:
npm run tauri buildArtifacts are written below src-tauri/target/release/bundle/:
appimage/Oto_<version>_amd64.AppImage
deb/Oto_<version>_amd64.deb
rpm/Oto-<version>-1.x86_64.rpm
The npm run tauri build wrapper checks for AppIndicator and sets NO_STRIP=1
to avoid older linuxdeploy strip binaries failing on modern ELF sections. The
first AppImage build may require network access to download its runtime.
The Flatpak packaging guide explains how to wrap the
Tauri Debian artifact and documents the sandbox limitations. GitHub Actions
runs frontend checks, production compilation, Rust tests, and cargo check;
tags matching v* create draft releases with AppImage, Debian, and RPM
artifacts.
Oto is a Tauri 2 desktop application. Svelte owns the overlay and settings webviews, while Rust owns audio capture, credentials, global shortcuts, providers, text insertion, and pipeline state.
.
├── src/ SvelteKit frontend
│ ├── lib/components/FloatingPill Overlay state and controls
│ ├── lib/components/Onboarding First-run setup wizard
│ ├── lib/components/settings/ Settings sections
│ ├── lib/stores/pipeline.ts Typed frontend pipeline state
│ └── routes/ Overlay and settings routes
├── src-tauri/ Rust/Tauri backend
│ ├── src/audio/ Capture, devices, VAD, and cues
│ ├── src/commands/ Frontend command handlers
│ ├── src/config/ Config, Mode resolution, keyring boundary
│ ├── src/features/ Snippets, spoken edits, replacements,
│ │ history, stats, and opt-in sync
│ ├── src/hotkeys/ Multi-binding X11, portal, Hyprland, and evdev
│ ├── src/injection/ AT-SPI, clipboard, paste, focus detection
│ ├── src/pipeline/ Lifecycle, events, context, cancellation
│ └── src/providers/ Provider traits, batch and streaming clients
├── packaging/ Flatpak manifest and AppStream metadata
├── .github/workflows/ Continuous verification and tagged releases
├── package.json Frontend and Tauri scripts
└── src-tauri/tauri.conf.json Window, security, and bundle configuration
The backend emits typed pipeline://event messages. Hotkey and tray controls
call the same orchestrator, so recording, cancellation, overlay visibility,
error handling, and insertion share one lifecycle.
- Cloud transcription sends recorded audio only to the configured speech-to-text provider, either as a single upload or over a streaming connection to that same provider.
- Local Whisper keeps transcription on the device.
- Polishing sends transcript text to the configured chat-completions provider, plus whatever context level you selected — application name only, by default.
- Password managers, keyrings, and authenticators never have any context described, at any level, and cannot be removed from that list.
- API keys remain in the operating system keyring.
- History, and retained audio when enabled, remain on the device and can be disabled or cleared independently. Deleting an entry deletes its recording.
- Stats are computed from local history alone; nothing is reported anywhere.
- Sync is disabled by default, communicates only with the endpoint you configure, and never includes credentials, audio, history, or general settings.
- Oto does not operate an intermediary cloud service.
Review the policies of the provider you select. Use local transcription and a trusted local endpoint if you need an entirely on-device data boundary.
- Check whether another desktop shortcut owns the configured chord.
- Restore
Ctrl+Shift+Space, save, and restart Oto. - On Wayland, verify that the portal and compositor-specific backend are running.
- Try the tray controls. If they work, the problem is shortcut registration.
- Use Appearance → Preview listening to test the overlay independently.
On niri, the portal is not involved at all — check which backend is active in Settings → Hotkeys:
- Direct input. Confirm
id -nG | grep input. Without it, Oto cannot read key events and falls back to the toggle bind. - niri config binds. Confirm
~/.config/niri/oto.kdlexists, thatconfig.kdlincludes it, and thatniri validatepasses. These binds are press-only, so set Activation to Toggle. - If the overlay takes a column in the scrolling layout instead of floating
above it, the window rule in
oto.kdlis not being loaded — check theincludeline.
The transcript is already on the clipboard when the insertion chain reaches its final fallback. Check the injection log:
tail -n 50 "/tmp/oto-inject-${USER}.log"On Wayland, verify that ydotoold is active and can type into another
application. On X11, install xdotool. Start with Auto, then test
Clipboard + paste, Direct type, or Clipboard only for applications
that block synthetic input.
- Make sure a Secret Service implementation is running and unlocked.
- Save the key again under the currently selected provider.
- The JSON configuration intentionally contains no API keys.
If Tauri reports Can't detect any appindicator library, install the
distribution package listed under
Development prerequisites, verify it with
pkg-config, and rerun npm run tauri build.
If linuxdeploy fails, use the npm wrapper rather than calling tauri build
directly; the wrapper supplies the required NO_STRIP=1 workaround.
For a complete Wayland and GNOME diagnostic catalog, see
errorfix.md.
- Create a branch from the current default branch.
- Keep platform-specific behavior behind clear Linux session checks.
- Run
npm run check,npm run build,cargo test, andcargo check. - In pull requests, describe the tested session: X11 or Wayland, compositor, portal backend, and insertion tool.
Implementation rationale is available in the design specification and MVP implementation plan.
Licensed under the Apache License, Version 2.0.
