Local speech-to-text and text-to-speech tray app for Linux.
Whisper (STT) + Chatterbox multilingual (TTS). No cloud STT/TTS APIs.
Status: v0.2 (X11) — always-on tray, rebindable hotkeys, streaming TTS, transport controls.
- Always-on system tray (Open / Load-Unload STT-TTS / Quit). Close or minimize hides to tray; process and hotkeys stay alive. Quit only via tray → Quit (or confirmed Exit in Settings).
- GUI tabs: Dictate, Speak, Settings (dark theme, status chips + cards)
- Load / unload models (free VRAM and RAM when unloaded)
- Model selectors (Whisper small/medium; Chatterbox multilingual)
- Default voice + tone picker (Chatterbox reference WAVs + emotion knobs; see
assets/voices/ATTRIBUTION.md) - Streaming / chunked TTS: long text is split into sentences; first audio starts after the first segment
- Playback transport: Play / Pause / Resume / Stop / Replay, progress scrubber, volume (mpv IPC; falls back to ffplay/paplay)
- Global hotkeys (rebindable; Capture picker + Apply):
- Super+Shift+S — read selected text aloud (optional: clipboard)
- Super+Shift+R — hold-to-talk → insert transcript at cursor (no auto-send)
- Installer with optional per-user start-on-boot; boot starts tray-only
The installer fails if any hard tool below is missing (message names the tool and what breaks).
| Requirement | Why (what breaks without it) |
|---|---|
| Linux x86_64 | Supported target |
X11 session (DISPLAY) |
GUI, tray, global hotkeys, paste injection |
| GNOME (or DE with AppIndicator/SNI tray) | Always-on tray icon (StatusNotifier) |
NVIDIA GPU + CUDA-capable drivers (nvidia-smi) |
Fast local models (CPU is unusably slow) |
Rust toolchain (rustc, cargo) |
Build the app |
| Python 3.10+ | STT/TTS workers |
| ffmpeg | Audio decode/helpers for workers and tooling |
arecord (alsa-utils) |
Mic capture (dictation / hold-to-talk) |
| xclip, xdotool | Clipboard/selection and paste-at-cursor |
| PulseAudio or PipeWire (Pulse compat) | Mic + playback |
| Disk: ~5–15 GB free for models + venv | Whisper + Chatterbox weights |
nvidia-smi and DISPLAY are checked with a clear WARN (install continues) because some headless/CI dry-runs still need the script to plan; live GUI+CUDA use still requires them.
| Tool | What you lose if missing |
|---|---|
| mpv (preferred) | TTS pause/seek and multi-chunk playlist; falls back to per-file ffplay/paplay |
| ffplay | Last-resort TTS player when mpv is missing (usually ships with ffmpeg) |
| pactl | Mic source listing/refresh degraded (Pulse/PipeWire control) |
| AppIndicator / SNI host | Always-on tray UX may break (see tray section below) |
sudo apt update
sudo apt install -y \
build-essential pkg-config \
libgtk-3-dev libayatana-appindicator3-dev \
libx11-dev libxi-dev libxtst-dev \
ffmpeg mpv alsa-utils xclip xdotool \
python3 python3-venv python3-dev \
portaudio19-dev
# NVIDIA driver already working: nvidia-smi
# Rust: https://rustup.rs
# GNOME tray: gnome-shell-extension-appindicator (often preinstalled on Pop/Ubuntu)Install creates a default neutral reference (Piper ljspeech — public-domain dataset; see assets/voices/ATTRIBUTION.md). Optional YAPPER_TTS_CLONE is for local dev only (do not redistribute proprietary WAVs).
- Extra VRAM headroom (12 GB class GPU is comfortable if you unload when using other GPU apps)
- Whisper medium (~1.5 GiB) — not downloaded by default; see install flags below
For the best French accent, you may install a native French reference as
{voice}_fr_{tone}.wav (for example default_fr_neutral.wav). Without one,
Yapper automatically limits cross-language CFG guidance so the bundled English
reference does not pull French pronunciation toward an English accent.
Yapper is a tray app, not a document window:
- Launch → tray icon appears (GNOME needs AppIndicator / StatusNotifier host).
- Window close (X) or minimize → window hides; tray + hotkeys + models stay.
- Tray Open → show + focus window.
- Tray Quit → only hard exit (unloads models).
If the tray icon is missing, yapper doctor reports host/lib status and the GUI shows a loud error. Install/enable gnome-shell-extension-appindicator (package) / ubuntu-appindicators@ubuntu.com on GNOME.
At boot, Yapper stays hidden only after tray creation succeeds; if tray creation fails, the window is revealed so recovery instructions are not stranded off-screen.
- Wayland-only sessions (Phase 13 follow-on)
- macOS / Windows
- Silent success without a tray host (GUI still runs, but always-on UX is broken — doctor warns)
Normal install copies the binary and non-editable Python workers into stable
XDG locations. After install you may move or delete the source checkout; the
app keeps working via ~/.local/share/yapper/venv (workers in site-packages).
Runtime config does not point workers at the git tree.
git clone <repo-url> yapper
cd yapper
./install.sh
# prompts: start on boot (tray-only)? this user / no
# non-interactive examples:
# YAPPER_AUTOSTART=user ./install.sh
# YAPPER_AUTOSTART=no ./install.sh
# YAPPER_DRY_RUN=1 ./install.sh # plan only; no cargo/pip/model network
# YAPPER_MODELS=small ./install.sh # default: Whisper small only
# YAPPER_MODELS=small,medium ./install.sh # also pre-fetch medium (~1.5 GiB)| What | Where |
|---|---|
| Binary | ~/.local/bin/yapper |
| Python venv + workers | ~/.local/share/yapper/venv |
| Models / voices / logs | ~/.local/share/yapper/ |
| Config | ~/.config/yapper/config.toml (python_bin = install venv; python_root empty) |
| Variable | Default | Effect |
|---|---|---|
YAPPER_DRY_RUN=1 |
off | Print plan only: dep checks + which Whisper sizes would be ensured. No cargo build, pip, model download, or writes under XDG install paths. |
YAPPER_MODELS |
small |
Comma/space list of Whisper sizes to ensure at install: small, medium, or small,medium. Invalid sizes abort install. |
YAPPER_AUTOSTART |
prompt / migrate managed legacy entry if non-TTY | user | no (no removes only Yapper-managed user autostart; unsafe legacy system-wide entries are removed) |
YAPPER_DEV_INSTALL=1 |
off | Editable python[dev] into app venv (dev only; not for daily-driver) |
YAPPER_PREFIX |
~/.local |
Binary install prefix ($PREFIX/bin/yapper) |
System-wide autostart is intentionally unsupported: Yapper's binary, Python
environment, config, and models are installed per user. Older
YAPPER_AUTOSTART=all entries that exactly match Yapper's former installer
output are removed during upgrade; unrecognized customized entries are
preserved with a warning for manual review.
- Default install ensures small only (faster, less disk). The UI still offers medium.
- Pre-fetch both:
YAPPER_MODELS=small,medium ./install.sh(or re-run download:YAPPER_MODELS_DIR=~/.local/share/yapper/models python scripts/download_models.py small medium). - Lazy first use: if you select a size that is not on disk, the STT worker’s
whisper.load_model(..., download_root=…)may network-download that checkpoint into the models dir on first load. Prefer pre-fetching offline or withYAPPER_MODELSif you need a hermetic install. - Chatterbox multilingual weights come from Hugging Face cache / first TTS load
(same local-only runtime; no cloud TTS API). Yapper pins the compatible
multilingual-v2 repository commit and verifies the size + SHA-256 of every
checkpoint before loading it; mutable
mainweights are never deserialized.
yapper doctor # host + worker ping checks (import from venv)
yapper # launch GUI + tray + hotkeys
yapper --hidden # launch tray + hotkeys without opening the window (used at boot)Self-contained smoke (isolated temp venv, no checkout on PYTHONPATH):
timeout 180s env YAPPER_SCRATCH=/tmp/yapper-smoke ./scripts/smoke_self_contained_install.shInstaller honesty unit tests (dep classification + YAPPER_MODELS parsing, no network):
timeout 30s ./scripts/test_install_truth.shDev-only. Editable install and [dev] extras stay in the checkout .venv.
This path is for hacking on workers; it is not what ./install.sh does for
users. Prefer cargo run + repo python/ on PYTHONPATH.
cd ~/projects/yapper
python3 -m venv --system-site-packages .venv
.venv/bin/pip install -U pip setuptools wheel
.venv/bin/pip install -e 'python[dev]' # editable + pytest/mypy/ruff — dev only
.venv/bin/python scripts/install_chatterbox.py --requirements python/requirements.txt
./scripts/install_voices.sh
.venv/bin/python scripts/download_models.py small medium
cargo run -- doctor
cargo run -- gui
# non-GPU Python suite (from repo root; markers registered in root pytest.ini)
PYTHONPATH=python pytest -q -m 'not gpu'
# equivalent: PYTHONPATH=python pytest -q python/tests -m 'not gpu'
# optional GPU smokes: PYTHONPATH=python pytest -q -m gpu
cargo test --locked
timeout 60s ./scripts/pip_audit.shOptional: force the installer into editable mode (still not recommended for
daily-driver installs): YAPPER_DEV_INSTALL=1 ./install.sh.
Worker smokes from the repo:
echo '{"id":"1","cmd":"ping"}' | PYTHONPATH=python .venv/bin/python -m yapper_stt
echo '{"id":"1","cmd":"list_tones"}' | PYTHONPATH=python .venv/bin/python -m yapper_tts| Action | Default | Notes |
|---|---|---|
| Read aloud | Super+Shift+S |
Primary selection by default; toggle clipboard in GUI |
| Hold-to-talk | Super+Shift+R |
Press start / release stop → insert via clipboard+ctrl+v; restores prior CLIPBOARD when Copy transcript is off (best-effort on X11) |
Rebind in Settings → Hotkeys: Capture a combo (or type advanced), then Apply hotkeys. Apply drops previous X11 grabs and registers the new ones live. Grab failures stay as a yellow banner until fixed (DE conflict or bad combo). Defaults and host examples (Alt+Shift+S / Alt+Shift+Q) parse the same way.
Long text is split into sentences (EN/FR-aware, abbreviation-safe) and synthesized one segment at a time. Playback starts when the first segment is ready. Controls on the Speak tab:
| Control | Behavior |
|---|---|
| Speak | Chunk + synth + play queue |
| Pause / Resume | mpv pause (when available) |
| Stop | Clear queue, kill player |
| Replay | Last successful chunk without re-synth |
| Seek scrubber | Seek within current segment |
| Volume | App-level mpv volume |
Read-aloud hotkey uses the same streaming path.
While yapper gui is running, agents and scripts on the same user session can queue speech via a Unix socket (not TCP). See docs/tts-api.md and docs/agent-tts-tool.md. Client: scripts/yapper-tts speak "Hello".
| Path | Purpose |
|---|---|
~/.config/yapper/config.toml |
Settings, hotkeys, mic source, model/voice dirs |
~/.local/share/yapper/models/ |
Default Whisper weights ([models] dir) |
~/.local/share/yapper/voices/ |
Chatterbox reference WAVs (default_*.wav, optional knobs.json) |
~/.local/share/yapper/venv/ |
Installer Python env |
~/.local/share/yapper/logs/ |
Logs |
config.toml [models] is honored at runtime (not decorative):
[models]
dir = "/home/you/.local/share/yapper/models" # Whisper under <dir>/whisper/
voices_dir = "/home/you/.local/share/yapper/voices" # e.g. default_neutral.wav + knobs.jsonThe shell injects these as YAPPER_MODELS_DIR and YAPPER_VOICES_DIR when spawning
STT/TTS workers. Standalone tools use the same env:
YAPPER_MODELS_DIR=/path/to/models python scripts/download_models.py small
# or: python scripts/download_models.py --models-dir /path/to/models small
YAPPER_VOICES_DIR=/path/to/voices ./scripts/install_voices.shDefaults match XDG under ~/.local/share/yapper/. yapper doctor reports the
configured roots and checks Whisper/voice files there.
Capture uses arecord via the Pulse ALSA device (-D pulse or -D pulse:<source>). Continuous ffmpeg pulse capture writes empty files when stopped without -t on PipeWire hosts, so it is not used for hold-to-talk. Empty audio.mic_source in config means the system default source.
[audio]
# Empty = Pulse default. Or a full source name from `pactl list sources short`:
# mic_source = "alsa_input.usb-…_TONOR_TC30_….mono-fallback"
mic_source = ""Tips:
- List sources:
pactl list sources short - Default source:
pactl get-default-source/ set withpactl set-default-source <name> - Prefer a real input (mic) over
*.monitorsinks (those capture playback) - On this class of host, USB mics like TONOR TC30 should appear as
alsa_input.usb-…TONOR… - GUI: microphone dropdown + Refresh, live level while recording, device name in status
yapper doctorprints the default source and runs a ~1s energy probe (non-silence / silence / skip reason)- Flatpak sandboxes often block Pulse device access — run the native binary for mic capture
yapper/
AGENTS.md
README.md
LICENSE
CONTRIBUTING.md
install.sh
Cargo.toml
src/
python/
scripts/
docs/
MIT — see LICENSE.