Skip to content

Repository files navigation

Oto logo

Oto

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

Also available for
macOS  ·  Windows

Oto feature overview

Note

Oto is an early Linux desktop release. Desktop integration can vary between compositors, portals, accessibility trees, and target applications.

Features

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.

How it works

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]
Loading

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.

Installation

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.sh

By 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.

First launch

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:

  1. Open Providers, choose a preset, and save an API key, or add a compatible provider.
  2. Under Models, choose cloud transcription or a local Whisper model.
  3. Under Audio, pick a microphone and an activation mode.
  4. Keep the default Ctrl+Shift+Space shortcut or select an unused chord.
  5. Run Test microphone, Test transcription, and Test insertion.
  6. 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.

Linux desktop setup

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.

Wayland input

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-portal

On 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

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 in

Oto 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.service

Verify 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.

Configuration

Providers and models

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.

Local transcription

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.

Snippets, styles, and Command Mode

  • A snippet expands only when its trigger is the complete utterance. A trigger named my signature matches my signature or snippet 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.

Activation and silence detection

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.

Modes

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.

Context sharing

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.

Spoken edits and replacements

  • scratch that, delete that, forget that, and strike that retract the clause just spoken, back to the previous sentence and no further.
  • new paragraph and new line insert 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, audio, and stats

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.

Hotkeys and text insertion

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.

Storage and optional sync

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.

Development

Development prerequisites

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-clipboard

AppIndicator 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"

Common commands

# 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 check

The Tauri development process opens the settings window and keeps the overlay preloaded but hidden until dictation starts.

Production builds

Build all configured Linux package formats with:

npm run tauri build

Artifacts 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.

Architecture

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.

Privacy and security

  • 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.

Troubleshooting

The hotkey or overlay does not appear

  1. Check whether another desktop shortcut owns the configured chord.
  2. Restore Ctrl+Shift+Space, save, and restart Oto.
  3. On Wayland, verify that the portal and compositor-specific backend are running.
  4. Try the tray controls. If they work, the problem is shortcut registration.
  5. 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.kdl exists, that config.kdl includes it, and that niri validate passes. 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.kdl is not being loaded — check the include line.

Text is transcribed but not inserted

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.

API key or keyring errors

  • 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.

Packaging fails after compilation

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.

Contributing

  1. Create a branch from the current default branch.
  2. Keep platform-specific behavior behind clear Linux session checks.
  3. Run npm run check, npm run build, cargo test, and cargo check.
  4. 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.

License

Licensed under the Apache License, Version 2.0.

About

Oto — system-wide, push-to-talk AI voice dictation for Linux. Hold a shortcut, speak, release: it transcribes, optionally polishes, and inserts clean text into the app you’re already using.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages