Mouthtype
Linux

Linux desktop setup and testing

Install and test the experimental Linux daemon and CLI.

A functional command-line session service for Arch/Omarchy with Hyprland and PipeWire. This is a development candidate, not a supported Linux release. It records only after an explicit start, supports hold and toggle commands, reuses a pinned resident Parakeet worker, and keeps recoverable audio/transcripts locally. The Omarchy bar plugin in ../omarchy-plugin is the desktop frontend and Fast/Thorough cleanup models are optional (see below). Mac pipeline parity and X11, GNOME or KDE adapters are not provided.

Note

The automated tests use synthetic WAVs, fake capture/engine processes, and Unix sockets. They establish application behavior, including real child-process cleanup. They do not establish microphone, model, Hyprland, native Wayland delivery, or Omarchy compatibility. Those require a recorded Linux session test. The earlier Ubuntu model checkpoint belongs to ../linux-engine-proof/README.md; its 5/6 exact result is not desktop acceptance.

Quick start

  1. Check an existing setup: mouthtype-linux --config /absolute/path/linux.json doctor. It's a read-only report.
  2. Install the client: python3 tools/linux-desktop/install.py --prefix "$HOME/.local".
  3. Prepare the speech model: python3 tools/linux-desktop/setup_speech.py --destination "$HOME/.local/share/mouthtype/speech-candidate".
  4. Start a foreground session: mouthtype-linux --config "$HOME/.local/share/mouthtype/speech-candidate/linux.json" serve.
  5. Bind keys for start, stop, toggle and cancel in your compositor, using unused keys. See Hold, toggle and cancellation.

The sections below explain each step. The Omarchy bar plugin in ../omarchy-plugin is the desktop frontend.

Check an existing setup

Run mouthtype-linux --config /absolute/path/linux.json doctor for a read-only report, or add --json after doctor for structured output. Exit status is 0 when required setup checks pass and 2 when something needs attention. It checks the target platform, configuration, configured Python's pinned speech distribution metadata, four model-file hashes, configured spelling/phrase libraries, required helpers and an existing service's status. It does not import the speech runtime, load weights, open a microphone, read the clipboard, install packages, download files or change the desktop. Optional notification feedback and a stopped service are warnings, with next steps.

Passing means the inspected setup is consistent. It does not prove that the audio server is connected, the speech runtime can load its native libraries, the model recognizes speech, a notification appeared, or Hyprland can deliver into a particular field. Physical acceptance remains separate. Type mode requires its existing explicit unknown-secure-field opt-in; recover-only remains the default.

Install and prepare

The initial runtime target is Linux x86_64 with native Python in a dedicated environment, PipeWire's pw-record, and optionally hyprctl plus wtype for automatic typing. Arch's pipewire-audio, hyprland, and wtype packages provide those commands; use the distro's normal signed package management. No root input daemon or input-device group is needed. This script does not install packages, weights, permissions, bindings, autostart, or services.

From a source checkout or the development source archive, run python3 tools/linux-desktop/install.py --prefix "$HOME/.local". The new launcher is ~/.local/bin/mouthtype-linux; ensure that directory is in the compositor's PATH. Installation refuses to overwrite an existing launcher or payload. It leaves user configuration untouched. An existing installation can be preserved by choosing a different prefix.

For a local Arch test package, a clean committed full repository checkout can run python3 scripts/prepare-linux-arch.py --destination /absolute/new/arch-bundle. This creates a standalone local-source PKGBUILD bundle with explicit source integrity/provenance and its own 0.1.0test.r<commit-time>.<revision> version, without changing signed app metadata. The attached test suffix sorts below the matching stable version. Follow the bundle's README to build as an ordinary user and install with normal pacman policy. No AUR entry, public source endpoint, signing key, model download, service or binding is added. The existing 60-file payload is reused. The installer supports --prefix /usr --destdir /absolute/staging-root so a package-stage path never appears in the runtime launcher. Staged system-package payload directories and newly created prefix parents use mode 0755, and payload files use mode 0644 regardless of the build umask. Existing staging ancestors are preserved. The manual user install retains its private mode 0700 payload root. Package removal leaves separately prepared user models, configuration and recovery outside /usr alone.

Prerequisites come from normal signed package management on Arch/Omarchy.

PackageNeeded for
python, python-pip, curl, pipewire-audioRequired.
libnotifyOptional recording feedback.
wtype, wl-clipboardOnly deliberate paste or typing delivery.
ffmpegOnly audio file transcription.
git, cmake, base-develOnly building the pinned cleanup server.

The ordered end-to-end checklist for people and coding agents, covering this client, speech setup, the Omarchy plugin in ../omarchy-plugin and its login service, is website/public/linux/agent-install.md, published at https://mouthtype.app/linux/agent-install.md. Preparation reports missing required helpers and never invokes a package manager. Native Arch Python3.14 installed the pinned sherpa wheels in the remote runtime experiment; actual desktop acceptance remains separate. Missing binary wheels fail preparation without replacing or downgrading system Python.

Explicitly prepare the speech model with python3 tools/linux-desktop/setup_speech.py --destination "$HOME/.local/share/mouthtype/speech-candidate". This downloads the exact published Parakeet INT8 archive, verifies its pinned SHA-256 before extracting only the four regular speech files, and uses the existing verifier to check each extracted digest. It creates a dedicated virtual environment, installs the pinned requirements as binary packages from PyPI, checks the runtime import/version, and writes private engines.json, linux.json, and a provenance receipt. It does not load models, record audio, start a service, register shortcuts or change the clipboard. No cleanup-model download or C++ build is required for this speech client.

The destination must be new. Existing files are refused before downloads or subprocesses; an interrupted new setup remains for inspection. Choose another destination for a clean retry. Every download/install child has a finite deadline and is stopped/reaped. The approximately 465 MiB model download permits slow continuous progress for at most one hour; curl aborts below 1 KiB/s for 60 seconds, retains its 20-second connection limit, and has a slightly larger outer process deadline. The receipt records preparation, not inference or desktop acceptance. Keep the archive, weights and environment outside the source checkout.

The helper prints the exact foreground launch command with its generated recover-only config. For the example above it is mouthtype-linux --config "$HOME/.local/share/mouthtype/speech-candidate/linux.json" serve. The default delivery.mode is recover, which makes no clipboard or keyboard changes. The resident service checks the configured artifact hashes again before each cold worker load. If artifacts are already available, the separate manual engine preparation procedure still works; config.example.json can reference that config and its environment Python.

Relative engine_config paths resolve beside the client config. capture_target optionally selects a PipeWire node name or serial using the documented pw-record --target option. An unavailable target fails rather than switching silently to another device.

Start a bounded foreground session with mouthtype-linux serve. It exits after 12 hours or an explicit mouthtype-linux shutdown; --lifetime-seconds after serve selects 1 second through 24 hours. Start it again explicitly for a new session. No restart loop, recurring job, login item, or systemd unit is installed. Termination cancels active capture/inference, stops owned processes and removes the socket. Audio from an interrupted take stays recoverable. A second service cannot replace the live socket; a later service can reclaim its own stale socket after a crash.

Hold, toggle and cancellation

Choose unused compositor keys. Nothing edits or reloads your compositor configuration automatically.

Note

Omarchy can already bind F9 to Voxtype, so inspect/remove that conflict or choose other keys before using these examples.

Commands

Every verb below is a mouthtype-linux subcommand.

CommandDoes
startStarts one recording. Repeated start is ignored.
stopFinalizes one recording and transcribes it. Repeated stop never duplicates insertion.
toggleStarts when idle and stops when recording. Does nothing during transcription or cancellation.
cancelDiscards an active take and prevents a late inference result from being delivered.
statusReports state, a coarse error category and recovery ID, without transcript, window title or audio.
watchPrints one JSON line per state change until the service exits. --retry keeps reconnecting.
listLists take IDs and whether audio and text exist.
recover IDPrints the selected transcript exactly, with no automatic delivery. --raw prints the original recognition.
transcribe IDAsks the running service to transcribe a take that has audio only, always into recovery.
forget IDDeletes that take's generated files, refusing a take currently in use.
capabilitiesPrints the client's interface version and feature list without contacting the service.
import FILETranscribes one WAV, AIFF or M4A file through the running service.
import-statusReports the file job state, a coarse error code and the decoded duration.
import-cancelStops decoding or inference and discards any partial result.
import-resultPrints the latest file result exactly.
import-save PATHWrites the latest file result as UTF-8.
import-forgetRemoves the latest file result.
doctorRead-only setup check. See above.
serveRuns a bounded foreground session.
shutdownEnds the service explicitly.

Note

For Hyprland 0.55 and newer, adapt the current Lua examples in hyprland.lua using hl.bind and nonblocking hl.dsp.exec_cmd. Its F9 press/release implements hold; F10 toggles; F11 cancels. hyprland-legacy.conf is separately labeled for 0.54 and older.

Custom modifier combinations need release-order acceptance on the chosen compositor version. The 60-second default recording cap cancels an overlong take instead of inserting a partial result. The maximum configurable cap is 120 seconds. Lost capture is preserved as interrupted audio rather than transcribed automatically.

Live state for frontends

The Omarchy bar widget and floating pill that use these interfaces live in ../omarchy-plugin.

mouthtype-linux watch connects once and prints one JSON line per state change until the service exits; --retry keeps reconnecting and prints {"event": "disconnected"} once per outage. Each line carries the session state, coarse error, delivery outcome, recovery ID, elapsed recording seconds, the file-import state and, while recording, a level in dBFS computed from the newest 100 ms of the growing take; no transcript, window title or audio ever appears. The service serves at most eight watchers, drops one that stops reading, and polls at 20 Hz while recording so a meter can follow speech. The Omarchy plugin's floating pill is built on this stream.

Recovery and deliberate delivery

Recovery lives in $XDG_STATE_HOME/mouthtype/recovery, default ~/.local/state/mouthtype/recovery. Directories use mode 0700; recording/transcript files use 0600. Recordings and transcripts remain until explicitly forgotten.

  • At maximum_recovery_takes saved take directories (default 200, 1 to 5000), another recording is refused as recovery_full instead of silently deleting user data. status reports takes and maximum_takes.
  • Completed audio follows the Mac saved-audio policy. When the service starts and after each take, a completed take's WAV older than audio_retention_days (default 90, null for no age limit) or beyond audio_retention_megabytes (default 2048, oldest first) is removed while its transcript and raw recognition stay.
  • Audio without a transcript, waiting for explicit recovery, is never removed by that audio policy.
  • Separately, history_keep (default null) forgets everything but that many newest takes, audio and text alike, after each dictation and at service start. The Omarchy menu offers Keep all, 5, 20 or 50.
  • No text is included in routine status/errors, process arguments or compositor logs.

Recovered takes never replay on startup. mouthtype-linux transcribe ID is for a crash or model error that left audio only: read the result with recover. Existing transcripts are never overwritten. See the command table.

New takes save original engine text in raw-recognition.txt and final text in transcript.txt, both mode 0600. mouthtype-linux recover ID --raw prints the original exactly, including its line endings. An older take without a raw file reports unavailable rather than substituting its final result. If final saving failed after raw recognition was saved, explicit transcribe ID finishes from that saved raw text using the current dictionary; it does not run recognition again or deliver automatically. Forget removes both text files and the audio. Cancelling a recovered take preserves its existing audio/raw text.

Warning

Automatic delivery is opt-in. Set delivery.mode to paste or type, then either provide a nonempty delivery.allowed_classes array of exact Hyprland window classes or set delivery.any_window to true, and set delivery.allow_unknown_secure_field to true only after considering the limitation below.

paste is the Mac-like path: the transcript is placed on the Wayland clipboard with wl-copy and one Ctrl+V chord is sent to the window captured at recording start, through Hyprland's send_shortcut dispatcher first (falling back to the legacy sendshortcut form, then to wtype), after checking that it is still the unlocked active window. Line breaks and tabs travel as text, so multiline Basic cleanup results can be pasted. Windows whose class is in terminal_classes (a built-in list of common terminals plus Omarchy's org.omarchy.agent, overridable) or whose owning process name is in terminal_executables (ghostty, alacritty, kitty, foot and other terminals, so a terminal launched with a custom class is still recognised) receive Ctrl+Shift+V instead, and a transcript containing a line break is refused for a terminal as unsupported_text unless allow_terminal_multiline is true, because a pasted line break runs a command. With restore_clipboard (default true) the previous text clipboard is read once before the paste and put back about half a second later, only if the clipboard still holds the pasted transcript; a clipboard holding an image or other non-text content is not restored. Successful helper exits mean the paste was requested, not that the field received it; the delivered cue says so.

type submits the characters directly and keeps the stricter rules below. The service captures address/class/PID at recording start, checks the current unlocked target before delivery, and declines delivery if target identity changed or is unavailable. It submits one UTF-8 text stream to wtype - via stdin. It never borrows or overwrites the clipboard, focuses another window, synthesizes a paste chord, or retries after uncertainty. The helper has a 15-second deadline; a timeout may have typed a prefix, so read recovery and check the target before doing anything else.

Warning

Hyprland window identity does not establish editable-field identity or whether a field is a password field. Automatic typing therefore requires the explicit unknown-field opt-in and a known suitable field.

AT-SPI context, secure-field detection and insertion read-back are unsupported. A helper returning success means delivery is unknown, not verified. Focus can change during key submission. Newline, tab, Escape and other C0/C1 control characters are refused for automatic typing because wtype turns some into Return/Tab actions. The full transcript remains available in recovery. Never enable automatic typing for a terminal or other command-executing field without understanding those limits.

Config keys

These keys live in linux.json. Relative paths resolve beside that file.

KeyValuesDefault
delivery.moderecover, paste, typerecover
delivery.allowed_classesNonempty array of exact Hyprland window classesNot set
delivery.any_windowtrue or falseNot set
delivery.allow_unknown_secure_fieldtrue only after considering the typing limitationOff
delivery.restore_clipboardtrue or falsetrue
delivery.terminal_classes, delivery.terminal_executablesLists that select Ctrl+Shift+VBuilt-in lists
delivery.allow_terminal_multilinetrue lets a multiline transcript paste into a terminalOff
capture_targetA PipeWire node name or serialNot set
maximum_recovery_takes1 to 5000200
audio_retention_daysA number of days, or null for no age limit90
audio_retention_megabytesA size in megabytes2048
history_keepA number of newest takes to keep, or nullnull
dictionaryPath to the saved-spellings JSONNot set
phrasesPath to the saved-phrases JSONNot set
phrase_clipboardtrue enables the {clipboard} variablefalse
notificationstrue or falsetrue
cleanupoff, basic, fast, thoroughoff
cleanup_enginePath to cleanup.jsonNot set

Explicit saved spellings

Optionally add "dictionary": "dictionary.json" to linux.json. Relative paths resolve beside that client configuration; absolute paths and ~ are accepted. The existing Mac library can be copied as a regular UTF-8 JSON file containing an array such as [{"heard":"post hog","written":"PostHog","starred":true}]. heard and written must be strings; starred is optional, boolean or null and does not affect matching. Unknown fields are ignored. No library is created, edited or learned automatically.

The service validates and snapshots the configured file before each recording or explicit audio recovery. A missing/malformed/nonregular file or one over 4 MiB refuses that operation with dictionary_unavailable; it never silently drops a configured library. Fixing the file takes effect on the next start without restarting. Edits during a take apply to the next take. Omitting dictionary or using an empty array leaves text unchanged.

Matching uses whole-word Unicode boundaries, canonical equivalence and per-character lowercase, with the longest normalized source first and the first duplicate winning. Source whitespace matches any whitespace run. Replacement text is inserted literally, once, without chaining into another rule. Empty sources and empty replacements are ignored, matching the Core matcher rather than imposing the Mac editor's validation limits. Final recovery and optional delivery use the replaced text; original recognition stays separate. There is no phonetic matching, speech-model boosting, cleanup-model integration or saved-spelling editor here.

Unicode and vendored matcher details

The portable matcher vendors the MIT grapheme v0.7.2 iterator at immutable commit 56f7f2d16d54576290634b7484eeece52c538bcb, with Unicode 16.0 data and licenses in vendor/grapheme. Alphabetic/numeric boundary ranges are also pinned to Unicode 16.0 in spelling-unicode.json; whitespace uses Unicode White_Space. Lowercase mappings come from the running Python's Unicode database, so recently introduced characters can differ across Python/Swift versions. The committed shared spelling fixtures pass against the actual Core matcher, covering combining accents, Greek/Cyrillic, Hangul, Indic clusters, emoji, boundaries, precedence and nonchaining. This is not a promise that all Unicode versions agree. The local Swift 6.4 Character implementation differs on one of 1,093 official Unicode 16 grapheme cases (✁‍✁); the vendored iterator passes all 1,093. No runtime dependency download is needed.

Saved phrases

Optionally add "phrases": "phrases.json" to linux.json. Paths resolve beside that configuration, like dictionary. The Mac library format is an array of {"trigger":"my address","expansion":"First Street"} entries. No library is created or edited automatically. A configured missing, malformed, nonregular or oversized library refuses recording/recovery with phrases_unavailable. The limit is 1 MiB, with unique nonblank triggers up to 128 graphemes and nonblank expansions up to 16,000 graphemes.

Each recording or explicit recovery snapshots the library and its variables before capture/inference. Saved spellings run first, then phrases use the same whole-word, case-insensitive, longest-trigger and single-pass matcher. Expansion text is literal, preserves its whitespace and is never rewritten by spelling rules or another phrase. {date} uses an English long date such as October 17, 2026; Linux does not yet mirror the Mac's locale-dependent date formatting. Unknown variables stay literal. The final result is limited to 2 MiB; a failed expansion keeps the original recognized text and audio for explicit recovery.

{clipboard} additionally requires "phrase_clipboard": true and the optional distro wl-clipboard package. It defaults off. If any configured expansion uses that variable, a start/recovery reads the default Wayland clipboard once through wl-paste --no-newline --type text; no clipboard watcher, write or key event is started. The read has a one-second deadline and 1 MiB limit. Missing permission, helper, text or failed reads refuse that operation with phrase_context_unavailable, rather than silently deleting the trigger. A successfully read empty text offer expands to empty, like Core. Use the initial Hyprland/data-control target; other compositors and clipboard behavior still require acceptance. These are explicit clipboard reads even in recovery-only delivery mode.

Raw recognition is saved before replacements as private raw-recognition.txt. Phrase/spelling failure leaves it available for recovery without another inference. Final text stays in transcript.txt; explicit raw recovery and existing completed results are unchanged. Cancelling a new unfinished take removes only its own generated audio/raw data; cancelling recovery preserves the old originals. No retry is delivered automatically.

Transcribe an audio file

mouthtype-linux capabilities prints this client's interface version and feature list without contacting the service, so a frontend shows only controls the installed client supports. mouthtype-linux import /absolute/file.m4a asks the running service to transcribe one WAV, AIFF or M4A file of at most 10 minutes and 512 MB, matching the Mac guide. The container is identified by content through ffprobe; the optional distro ffmpeg package provides both helpers, and a missing helper fails the job as decoder_unavailable. The file is decoded by an owned, bounded ffmpeg process into a private 16 kHz mono PCM16 WAV below $XDG_STATE_HOME/mouthtype/imports, then recognized by the same resident Parakeet worker, with a deadline that grows with the file's duration rather than the microphone cap. The original file is never written. Source paths and transcript text stay out of status replies and diagnostics.

import-status reports preparing, transcribing, cancelling, finished, empty, cancelled or failed, a coarse error code, and the decoded duration. import-cancel stops decoding or inference when the current operation returns and discards any partial result; the decoder is stopped and the speech worker is reloaded on the next request, as with cancelled dictation. One file job runs at a time, and a job is refused as session_active while dictation or recovery is active; start, toggle and transcribe are refused as file_import_active while a file job holds the speech engine. status includes the current file_import state.

The result is the full raw recognition. Cleanup, saved spellings and phrases are not applied, nothing is pasted, and the job does not appear in list, recent dictation history or word counts. import-result prints the latest result exactly, and import-save /path/notes.txt writes it as UTF-8, refusing a destination that resolves to the original audio file and refusing to replace an existing file without --overwrite. Both read saved state and work without the service. Starting a new file job replaces the previous result; import-forget removes it explicitly. Interrupted jobs are removed on the next service start. The decoded WAV is deleted after recognition, cancellation or failure.

Optional basic cleanup

"cleanup": "off" is the default and leaves recognition text exactly as it arrived before explicit saved-spelling/phrase rules. Choose "cleanup": "basic" in linux.json for deterministic spoken punctuation, paragraph/newline commands, bullet/numbered lists, sentence-start casing and whitespace formatting. Configuration is loaded when the foreground service starts; an admitted take keeps its own cleanup setting. No additional model or network request is needed. Paragraph/newline/list commands produce line breaks. Automatic type delivery refuses those control-bearing results as unsupported_text; the full formatted transcript stays available through recovery. Single-line punctuation results can use the existing guarded typing path. This does not add multiline paste or send Return/Tab keys.

Basic uses Core's command vocabulary and noun/clause/list guards: “hello comma world new paragraph thanks” becomes Hello, world, a blank line, and Thanks. Ordinary uses such as “the Oxford comma,” “a new line of products” and “next week” remain words. It does not remove fillers, change spoken numbers, paraphrase or perform Mac AI cleanup. Command intent is still heuristic, including Core's known ambiguous bare “bullet”/“next” cases; review the result or keep cleanup off when literal wording matters.

Quoted/backticked text, fenced code and code-looking paths, addresses and identifiers are protected from command/casing/inner-whitespace changes. These are explicit preservation rules beyond the unprotected Core stages, not a claim of identical output for every input. Unknown or unclosed prose quotations can still be ambiguous. Saved spellings and phrases run after cleanup, so their inserted case, indentation and line endings remain literal. No surrounding app text is read and no cross-take history is used to guess words.

Input/output formatting is limited to 2 MiB. Raw recognition is saved first. A formatting failure preserves the original raw text/audio; explicit recovery uses the current cleanup setting without another inference when raw text is available, and never pastes automatically. Cancellation while formatting runs remains immediate and cannot publish late text. Existing completed results are never reformatted on startup.

Fast and Thorough cleanup with local models

LevelModelNotes
BasicNoneDeterministic. No extra model or network request.
FastS1-mini by Superwhisper, GGUF Q4_K_MBudget of 1 s plus 35 ms a word, at most 10 s.
ThoroughQwen3-4B-Instruct-2507, GGUF Q4_K_MBudget of 2 s plus 200 ms a word, at most 30 s. Slow on CPU.

"cleanup": "fast" or "thorough" adds a local model pass after Basic cleanup, matching the Mac levels: Fast is S1-mini by Superwhisper (GGUF Q4_K_M, Apache-2.0 with its naming clause) with its native control line; Thorough is Qwen3-4B-Instruct-2507 (GGUF Q4_K_M, Apache-2.0) with the Mac app's shared cleanup prompt, because the Mac Thorough model's terms are gated and it has no pinned llama.cpp build here. Both run through the pinned llama.cpp CPU server from ../linux-engine-proof/pins.json, listening on loopback only, started once by the service and stopped with it. Neither level is implied by the other, and neither is on by default.

Prepare explicitly with python3 tools/linux-desktop/setup_cleanup.py --destination "$HOME/.local/share/mouthtype/cleanup-candidate" --level fast (add --level thorough for both). It clones and builds the pinned llama-server revision for the CPU unless --server names an existing pinned build, downloads each GGUF with a bounded curl, verifies the pinned SHA-256 before placing it, hashes the server, and writes private cleanup.json, a receipt and a cleanup-setup.json progress file. It never edits linux.json; add "cleanup_engine": "/path/cleanup.json" and the chosen level yourself, then restart the service. doctor verifies the configured model and server hashes without loading them.

At each take the raw recognition is saved first, Basic cleanup runs, then the model receives that text within a budget derived from the on-machine benchmark (Fast: 1 s plus 35 ms a word, at most 10 s; Thorough: 2 s plus 200 ms a word, at most 30 s). Takes under two or over 400 words skip the model. A timeout closes only the HTTP request, so the server stays warm; cancellation stops the worker and the next take reloads it. The model text replaces the Basic text only when the ported Mac guard accepts it: non-empty, within the length cap, same negations, certainty words, question form and time of day, every number, acronym, capitalised name, quoted or code-like literal kept, at least half the content words retained and at most half new. Any other outcome, including a crashed or missing model, keeps the Basic text and the audio. Each take records cleanup.json with the level, outcome (ok, guard-rejected, timeout, failed, skipped), milliseconds and reason, never text; status reports the level, last outcome and the model state (absent, idle, loading, ready, failed). Saved spellings and phrases still run after the model.

Benchmark numbers on the development machine

On this machine (Ryzen 5 3600, 6 threads, CPU only) the pinned S1-mini loaded in about 0.9 s and answered the 75 Mac cleanup fixtures with a median of 0.4 s, under 1 s up to about 30 words and about 7 s at 220 words, with 2.2 GiB resident; 39 of 75 matched the Mac reference exactly, the differences being S1's habit of keeping spoken register. The Thorough candidate on the same CPU loaded in about 2.3 s and answered the same fixtures with a median of 2.7 s, about 3 s at 10 to 19 words, 11 s at 50 words and 38 s at 220 words, with 5.3 GiB resident; 42 of 75 matched exactly and, on the 19 smart-correction fixtures, 8 against S1-mini's 11. Thorough is therefore selectable but slow here: its budget of 2 s plus 200 ms a word, capped at 30 s, means takes over roughly 140 words keep the Basic text. It is a different model with a different prompt, evaluated separately, not Fast under another name. GPU offload is possible through gpu_layers in cleanup.json with a Vulkan build of the same revision, which this setup script does not produce.

Recording feedback

Coarse desktop notifications are enabled by default through the existing notify-send helper. They identify ready, recording, transcribing, saved, cancelled and error states without transcript text, window titles or audio. Subsequent cues replace the previous notification when the desktop returns a notification ID. Active cues expire if a later replacement cannot be submitted; shutdown replaces an active recording cue with cancellation. Desktop notification presentation is best effort and never counts as delivery verification.

Set notifications to false to disable this helper. If it is missing, hung or rejected by the desktop, dictation and recovery continue; status reports feedback as unavailable. submitted_unknown means the helper accepted a cue, not that the desktop displayed it. Each helper has a short deadline and its owned process group is stopped/reaped. This client does not install the helper, notification service, bindings or login integration. Arch's libnotify and Ubuntu's libnotify-bin provide notify-send through their normal signed package management.

For maintainers

The rest of this page is for people working on the client itself.

Checks and development archive

Run python3 -W error::ResourceWarning -m unittest discover -s tools/linux-desktop -p 'test_*.py'. No test opens a microphone, launches a compositor or loads/downloads a model.

In a full repository checkout with the Swift compiler, python3 tools/linux-desktop/check_cleanup.py verifies the generated command data, shared formatting baselines and command-context/Unicode differential cases against the actual Core stages. The nine recorded literal-policy cases keep independent preservation expectations; they are not counted as identical Core output. Separately, python3 tools/linux-desktop/check_phrases.py checks the shared phrase fixtures against the actual Core SnippetExpander and span rewriting without launching the app. The check copies only the exact current edit-record definition from its GRDB-owning file into a temporary compilation unit. Separately, python3 tools/linux-desktop/check_spellings.py checks the shared fixtures against the real Core sources. This bounded test does not build or start the Mac app. The Linux source archive contains the portable fixtures/conformance checks and all vendored dependencies; it does not include the Mac-only comparison utility or Core sources.

Create a new source-only archive with python3 tools/linux-desktop/install.py --archive /absolute/new/mouthtype-linux-development.tar.gz. Its explicit file list includes the service, installer, bindings, engine adapter/pins/requirements, GPL license and third-party notices. It includes no model weights, user configuration, recordings or private planning. It is not a signed release package and does not promise a tested desktop baseline.

Runtime references

These sources were inspected on 2026-10-16. No Voxtype/Omarchy implementation was copied. Existing runtime/model licenses and provenance remain in the engine pins and third-party notices.

Explicit remote component acceptance

Component runner, workflow and evidence notes

acceptance.py is a separate opt-in Linux runner. Its capabilities command records available helpers and /dev/dri nodes without starting a compositor or microphone. pipewire starts an isolated PipeWire graph with the upstream synthetic 440 Hz test source and WirePlumber's policy-only profile, captures it through the real production adapter, checks PCM format/signal/frequency, and stops its processes. It does not load speech weights or open physical sources. Run it in an isolated D-Bus session using dbus-run-session -- python3 tools/linux-desktop/acceptance.py pipewire.

The separate sway command runs an owned headless Sway/pixman session, opens the synthetic native GTK3 Wayland target, sends one Unicode stdin stream through real wtype, and checks exact target text, focus and an existing clipboard sentinel. Run dbus-run-session -- python3 tools/linux-desktop/acceptance.py sway as an ordinary non-root user on the disposable runner. Its dependencies are Sway, wtype, wl-clipboard, GTK3, Python GObject bindings and D-Bus. This is a Sway/wtype component check, not a Hyprland adapter or Omarchy acceptance check. It has not been run when this source was initially written. A successful result must be tied to fetched runner evidence before changing that status.

Each component has a 45-second execution limit, short per-operation/readiness deadlines and owned-group cleanup before returning. All fixtures are synthetic. Logs from a failed component are bounded and emitted for investigation; no user recordings or dictation are used. The runtime scripts neither grant privileges nor disable compositor security restrictions. Current Hyprland/Aquamarine renderer paths require a DRM/dmabuf render device, including for nested sessions. An output-free session does not remove that requirement; record actual render-device availability before scheduling its desktop acceptance.

The narrow Linux desktop components workflow runs on PRs that change these files and supports an explicit manual dispatch. It uses the existing hosted Ubuntu 24.04 kernel with separately pinned official Ubuntu 26.04 and Arch starting userlands, normal signed package updates, ordinary-user D-Bus/component sessions and read-only repository permissions. Exact installed packages and raw component results are retained for 14 days. There are no weights, secrets, physical audio devices, paid runners or recurring schedules. Ubuntu 26.04 is used for the current WirePlumber policy-profile API rather than pretending Ubuntu 24.04's older WirePlumber has it. Passing these synthetic components still does not mark Arch/Omarchy desktop support complete.

Recording start/toggle requests carry a short local monotonic admission deadline, checked before and after target lookup. Expired or queued requests cannot begin a microphone take after their client has already timed out. Target probes have shorter individual deadlines than the client response budget. Repeated cancellation remains busy until inference has stopped. Cancelling an explicitly recovered audio take preserves its original saved WAV; only a newly recorded active take is disposable.

The first actual component runs exposed two integration mistakes and an environment boundary. Upstream pw-record intentionally returns exit status 1 after an operator's SIGINT closes the WAV, because success status is reserved for a drained playback stream. The adapter now accepts that status only after explicitly stopping a previously live recorder, while retaining complete PCM format/frame validation and treating a natural early exit as interruption. The native Wayland test waits for the exact app node in the compositor tree before requesting focus; a fixture file appearing is not proof the window has mapped.

Arch's signed packaged Sway 1.12 carries cap_sys_nice, and its executable was denied by the default hosted container boundary before startup. That failure is preserved as evidence. The Arch component job therefore builds a separately labeled unprivileged upstream Sway 1.12 component from immutable release commit 88869399f421d9180dd8b6ed0b5a1f4a3585d252, verifies the source archive SHA-256 in sway-build.json, and builds with two jobs against the recorded signed Arch libraries. It neither changes the installed package/file capabilities nor grants container privileges. The test result identifies this source build, while capabilities evidence continues to probe the original packaged executable. This establishes no packaged-Sway, Hyprland or Omarchy support claim. Source download, build commands and owned child groups have finite deadlines; source/binary digests and build logs accompany the component evidence.

The source-built component includes the exact upstream circular-scroll compatibility fix 1652c54b73f67df17b7b4ab0b0f7048204aa8104, verified by its pinned patch digest and applied with exact context. Released Sway 1.12 predates Arch's libinput 1.32 enum; the backport handles that enum rather than disabling upstream -Werror. Source and patch identities remain separate in the receipt. Failed component artifacts may include the generated synthetic WAV and final owned compositor/target state for diagnosis. These fixtures contain no user recordings or desktop state.

On this page