OmaYap
OmaYap is an Omarchy shell plugin (omayap.read-aloud) that reads
the current text selection aloud across the desktop. It is local after setup:
Piper synthesizes with a selected official Piper voice (the default is
en_US-lessac-medium) and sends signed 16-bit PCM to
PipeWire's pw-play.
The plugin is a focused service/bar-widget adaptation of
calebhat/omarchy-read-aloud.
The adapted upstream portions remain MIT-licensed and the original attribution
is retained here. This repository's new code is also MIT-licensed; see
LICENSE. Piper is a separate GPL-3.0-or-later dependency and the
voice model has its own model-card/dataset terms. Neither Piper nor the model
is vendored into this repository.
Install
Omarchy intentionally does not run plugin hooks or install dependencies. Use two explicit phases:
omarchy plugin add https://github.com/Ray-4Ws/OmaYap --enable
~/.config/omarchy/plugins/omayap.read-aloud/bin/setup
bin/setup:
- requires
uv(if it is missing, it prints the exact commandomarchy pkg add uvand stops); - uses the system
python3standard library to inspecthyprctl binds -jsafely before touching the binding file; - creates a private Python 3.13 environment under
~/.local/share/omayap-read-aloud/and installs the hash-locked Piper runtime and the optional-backend OpenVINO runtime; OpenVINO telemetry is disabled by setup, and OpenVINO is imported only for an accelerated read; - installs only the default official voice (
en_US-lessac-medium) through the checked-in voice catalog, verifying the model, JSON configuration, and model card with pinned byte counts and SHA-256 checksums; - runs a silent synthesis smoke test; and
- checks both effective and user F10 and Ctrl+F10 bindings before adding its two tagged bindings.
If either exact chord is already used, setup refuses to override it. If both
bindings are free, setup backs up ~/.config/hypr/bindings.lua, adds the
tagged block, reloads Hyprland, and checks hyprctl configerrors. A failed
validation rolls the bindings back. Re-running setup is safe and replaces only
its own tagged block.
The default model is about 63 MB and setup needs network access. Runtime and
model data are private (0700 directories, 0600 files) under
~/.local/share/omayap-read-aloud/models/ (or the private
XDG_DATA_HOME equivalent). Setup never auto-downloads alternate voices. This
repository does not run setup during development or plugin installation.
The worker is loaded on the first read and exits after 60 seconds without a reading. The QML service and bar stay loaded, so this releases Piper and ONNX Runtime memory while OmaYap is idle. Changing speed while the worker is cold only saves the setting; it does not load or restart the worker. A later read starts a fresh worker with the saved speed.
Use
- Select text in any application and press F10.
- Left-click the bar icon to read the selection, or to stop active playback.
- Hover over the bar icon for its current status and left/right-click actions.
Right-click it for the voice selector, character count, and the
0.5×–2.0×speed slider. The voice dropdown contains the four checked-in official Piper choices: Lessac, Kristin, John, and Alba. It shows each voice's US/UK region, medium quality, and approximate download size. Choosing an uninstalled voice downloads its three files, verifies their pinned sizes and SHA-256 checks, and selects it automatically. The selected voice is persisted in the privateselected-voicestate file. A voice can be changed only while OmaYap is idle; a warm old worker is evicted before the next read starts. Only one model session is loaded at a time, and idle workers exit after 60 seconds. Speed changes are persisted in~/.config/omayap-read-aloud/settings.jsonand affect the next chunk. The same popup selects the reading cleanup profile: Safe (the default), Off, or Article. - Use Ctrl+F10 or the bar popup's Read text from screen (OCR) action to
run Omarchy's installed local screen-text capture. It does not modify
CLIPBOARD; the native OCR language setting is inherited from
OMARCHY_OCR_LANGS. Empty capture means cancellation, and recognized text is capped at 20,000 Unicode code points before it reaches Piper. - A selection or OCR shortcut pressed during the initial voice-catalog scan is queued and starts as soon as that metadata-only scan finishes; the first shortcut after setup is not discarded.
- The service IPC target is
omayap.read-aloudwithtoggleSelection,readSelection,readOcr,stop,setSpeed,setCleanupProfile,setBackend, andstatusmethods. - The private backend stdin/stdout contract is documented in
docs/backend-protocol.md; selection reads use its genericspeakcommand.
F10 and Ctrl+F10 are installed by setup rather than by the plugin manifest because Omarchy plugin installation does not modify keybindings. The bar widget defaults to the right section and cannot be duplicated.
Backend selection (local development branch)
The bar menu's Backend selector offers CPU only, CPU + NPU, and GPU. CPU is the default. All four catalog voices—Lessac, Kristin, John, and Alba—can use either accelerated option. Each exact model and configuration is checksum-pinned; there are no extra voice downloads.
CPU uses the existing low-memory ONNX Runtime path. CPU + NPU uses CPU for text/duration processing and the Intel NPU for waveform generation, with overlapping fixed-size blocks to preserve continuity. GPU runs the neural model on the Intel GPU through OpenVINO in FP32; text preparation and audio playback remain ordinary host work. Neither accelerated option silently falls back to CPU after a device or compilation failure. You can select CPU again from the same menu.
Change backends while OmaYap is idle. The previous worker is shut down before the next read, and the choice is saved alongside speed and cleanup settings. All backends retain the stop mechanism and 60-second idle unloading. Older settings files without a backend field default to CPU.
The first accelerated read can take several seconds to load/compile, and GPU inference may prepare additional kernels for a new input size. Acceleration can increase retained memory; it is not a memory-saving switch. Support has been checked on an Intel Core Ultra 7 255H. Other GPU vendors and Piper voices outside the four-item catalog have not been validated. See backend details.
Agent announcements
Local AI agents with shell access can use the portable
omayap-yaps skill when the user
requests audible announcements. This uses the same selected voice, speed,
and CPU/GPU/CPU+NPU backend as ordinary reading. Hosted agents need a local
execution connection; the skill alone does not grant desktop access.
To make the skill discoverable, copy or symlink its folder into your agent's
skills directory (for example, ~/.agents/skills/omayap-yaps). The folder is
portable Agent Skills format; installation is opt-in and does not grant agents
permission to speak without your instruction.
The client is ~/.config/omarchy/plugins/omayap.read-aloud/bin/yap. Fixed
announcements use --event task-complete, input-needed, permission-needed,
or error. Custom announcements are read from stdin with no text arguments.
Success produces no output, which keeps repeated agent calls inexpensive in
context. A successful call acknowledges acceptance; audio completion is
asynchronous. A busy reader refuses the request; agents should not poll or
retry repeatedly. The skill does not enable announcements without user intent.
Enable Agent speaking indicator in the OmaYap popup to show a small robot head next to the middle of the bar while agent audio is playing. It is off by default, follows the bar's screen and orientation, and does not intercept clicks or keyboard focus. Selection/OCR speech does not display the robot. F10 and the usual stop action still stop speech.
The bridge is a private Unix socket in the user's runtime directory, verifies same-user credentials, and carries bounded text in memory. No extra TTS service or model is kept resident for agents; the existing worker still expires after 60 idle seconds.
Selection and privacy
The service first reads Wayland PRIMARY with wl-paste --primary; CLIPBOARD is
untouched on that path. If PRIMARY is unavailable, it uses a conservative
fallback only when CLIPBOARD is empty or advertises exactly one supported
plain-text MIME type:
- snapshot the plain text and clear CLIPBOARD;
- detect a terminal from
hyprctl activewindow -jand send Ctrl+Insert there, or Ctrl+C elsewhere withwtype; - poll for newly copied text; and
- restore the original empty/text clipboard before speaking.
If the user stops or starts another action during fallback capture, the pending read is canceled and the service waits for clipboard restoration to finish before accepting another capture. This prevents a late clipboard helper completion from submitting stale text or producing a post-stop notification.
Rich, multi-format, and even multiple-plain-text-MIME clipboards are refused to avoid destroying images, HTML, or other data. Fallback capture can still add a copy to a user's clipboard history, depending on the history daemon. Wayland does not expose a universal selection API: applications that refuse copy access, protected documents, and some sandboxed/XWayland clients cannot be supported automatically. Copy such a selection manually and try again.
Selected and OCR text is held only in process memory and worker stdin. It is never
placed in command-line arguments, worker stdout, logs, settings, or
notifications. OCR uses the installed Omarchy capture flow through a
fail-closed adapter: only its reviewed stdout-through-wl-copy contract is
intercepted, and the adapter supplies a stdout-only shim while making the
native notification command silent. No clipboard write is performed by
OmaYap's OCR path. Every replaceable command that produces data for QML runs
through the dependency-free share/bounded_capture.py helper: MIME metadata
is capped at 4 KiB, selection reads at 80,004 bytes (enough to count 20,001
four-byte UTF-8 code points), active-window metadata at 16 KiB, and the
temporary clipboard backup at 1 MiB. Non-interactive captures have a five-second
deadline; interactive OCR has a two-minute safety deadline. On overflow or
timeout, the helper discards all output, terminates the producer's complete
process group, and makes bounded attempts to reap it. The worker and QML service then enforce
a 20,000-Unicode-code-point limit; an oversized selection is rejected in full
and, when the bounded read completes, the notification reports the exact
code-point count and allowed limit. A producer that overflows its byte cap has
no usable count and gets only a fixed-limit notification. An oversized
clipboard backup is refused before the clipboard is cleared, so it can never
be restored truncated. Empty/stale selections are not spoken.
Settings are accepted only from a private 0600 regular file in OmaYap's
private 0700 configuration directory. Reads use no-follow and nonblocking
descriptor verification with a 4 KiB limit. Writes arrive over helper stdin
and replace the file atomically through a randomized, exclusive temporary
file; symlinks, FIFOs, and non-private objects are rejected.
Each new playback stream begins with a 160 ms silent PCM lead-in so USB,
Bluetooth, and power-saving audio paths can wake without clipping the first
spoken character. The bundled voices are CPU-based and English-only. There is no cloud TTS
endpoint, language detection, document import, pause/seek UI, arbitrary-model
input, multi-speaker selection, accelerator/NPU path, preview, or voice
marketplace. OCR remains local to Omarchy's installed capture tools; it does
not upload screenshots or recognized text. The checked-in catalog and its
official links are in share/voices.json. The voice
files are distributed by the official Piper voice repository.
For clear attribution and terms disclosure, the four shipped choices link to
their official model cards and dataset terms here:
- Lessac medium model card · Blizzard Challenge 2013 license
- Kristin medium model card · LibriVox public-domain terms
- John medium model card · LibriVox public-domain terms
- Alba medium model card · Creative Commons Attribution 4.0 terms
Piper's runtime documentation is available in the official OHF-Voice/piper1-gpl repository.
Update and remove
After omarchy plugin update, rerun setup when the plugin version or lock/model
stamp changes:
omarchy plugin update omayap.read-aloud --yes
~/.config/omarchy/plugins/omayap.read-aloud/bin/setup
If an update succeeds but the bar still shows the previous OmaYap interface, restart the Omarchy shell once so its long-running QML process loads the new plugin files:
omarchy restart shell
The runtime stamp makes an incompatible update show setup required until setup succeeds. To remove the plugin cleanly, remove its managed binding first, then remove the Omarchy checkout:
~/.config/omarchy/plugins/omayap.read-aloud/bin/uninstall
omarchy plugin remove omayap.read-aloud
Uninstall stops playback, removes only the tagged F10 and Ctrl+F10 block, validates Hyprland, and preserves model/runtime/settings data. To delete only those plugin-owned data directories after an explicit confirmation:
~/.config/omarchy/plugins/omayap.read-aloud/bin/uninstall --purge
Development tests
Tests use a fake Piper voice, fake audio sink, and temporary HOME/config/data directories. They do not download packages/models, enable the plugin, or edit the live Hyprland configuration:
tests/run
The rationale for retaining the current Python backend is recorded in
docs/native-backend-decision.md. The
controlled serialization comparison is recorded in
docs/memory-benchmark-results.md.
To measure cold-start latency and worker memory, use the installed runtime and model. The practical default matrix uses a 1,000-character input with 200/400/800-character synthesis chunks. Input is sent through private stdin; the report contains only counters and fixed status/error codes:
~/.local/share/omayap-read-aloud/venv/bin/python \
benchmarks/memory.py --format csv --output /tmp/omayap-memory.csv
The report includes first-status and first-audio latency, sampled PSS,
private-dirty and anonymous memory peaks, thread peaks, total completion
latency, and worker exit status. Each matrix case starts a fresh worker. Pass
--lengths 1000,5000,20000 for the full input-size matrix, or
--lengths 20000 --chunk-targets 200,400,800 for a maximum-length chunk
comparison. To compare the old Piper ONNX defaults, repeat the same command
with the benchmark-only --legacy-defaults option; normal OmaYap startup
never enables that mode. The benchmark discards PCM after synthesis, so its
memory results do not include a pw-play process. It exits nonzero if any case
fails or times out.
To check whether memory grows across reads in one long-lived worker, use the
same-process modes. Each mode defaults to 10 cycles and records one result per
cycle, including peak and post-completion settled PSS, private-dirty,
anonymous memory, and thread counts. settled_* is the last sample after
--settle-time seconds; compare it across cycles to distinguish a rising
baseline from allocator high-water retention:
# Completed reads with no interruption between cycles
~/.local/share/omayap-read-aloud/venv/bin/python \
benchmarks/memory.py --repeat-mode serial --repeat-cycles 10 \
--lengths 5000 --chunk-targets 800 --format csv \
--output /tmp/omayap-repeat-serial.csv
# Stop, then immediately submit a replacement read on every cycle
~/.local/share/omayap-read-aloud/venv/bin/python \
benchmarks/memory.py --repeat-mode interrupt --repeat-cycles 10 \
--lengths 5000 --chunk-targets 800 --format csv \
--output /tmp/omayap-repeat-interrupt.csv
Repeat reports use schema 2. In interrupt mode, completion_event must be
replacement-idle; stop_idle_events counts idle events observed after the
stop request and before replacement work, while replacement_idle_events
counts the idle event that actually completed the replacement read. The input
is synthetic and is sent only through the worker's private stdin; no selected
or generated text is written to the report, command line, or diagnostics. The
harness cannot observe the number of ONNX session.run calls active inside
Piper because the worker protocol intentionally exposes no inference-level
instrumentation; use the interrupt-mode memory and timing series as external
evidence of retention or overlap. The original cold benchmark remains the
default when --repeat-mode is omitted.
The final validation used for a checkout is:
tests/run
omarchy plugin validate .
bash -n bin/setup bin/capture-ocr bin/uninstall tests/run
python3 -m compileall -q worker share tests benchmarks bin/manage-voices
git diff --check