{"slug": "sayitermano-local-first-voice-dictation-for-linux", "title": "SayItErmano – local-first voice dictation for Linux", "summary": "SayItErmano v0.4.0, an unofficial community port of FluidVoice for Linux, is now available as a local-first voice dictation app that transcribes speech on-device and optionally polishes text via OpenAI-compatible endpoints. The GTK 4 app, installable via a one-shot script or .deb package, supports global hotkey recording, faster-whisper transcription on CUDA or CPU, and text insertion into any focused application.", "body_md": "**Dictation app for Linux** — press a key, speak, and polished text lands in any app.\n\n  100% local speech-to-text · optional AI polish · native GTK 4 app\n\nNote\n\nThis is an **unofficial, community port** of [FluidVoice](https://github.com/altic-dev/FluidVoice)\n— the free, open-source, on-device dictation app for macOS — to Linux. It is\nnot built by the FluidVoice authors: the macOS app is Swift/Xcode, this is a\nPython implementation of the same behavior and, where licensed to do so\n(GPLv3), the same prompts, rules and sounds. See\n[docs/BEHAVIOR-SPEC.md](/acailic/SayItErmano/blob/linux/docs/BEHAVIOR-SPEC.md) for what was ported, with\nfile:line evidence from the upstream sources.\n\n**Naming:** the project, repo, package, command and env-var overrides\n(`SAYITERMANO_CONFIG`, `SAYITERMANO_SOCKET`, `SAYITERMANO_API_KEY`, …) are\n**SayItErmano** (`sayit-ermano`). Only the Python module keeps the upstream\n`fluidvoice` naming on purpose — internals credit the port's origin.\nInstalling `sayit-ermano` replaces the pre-rename `fluidvoice-linux` package\nand takes over its config, history and models.\n\n**[v0.4.0](https://github.com/acailic/SayItErmano/releases/tag/v0.4.0)** — the\nSayItErmano identity: repo, .deb package, command, launcher and tray entry all\ncarry the new name, plus an **original app icon** (gold tile, speech bubble +\nwaveform — no FluidVoice artwork anywhere). One-shot installer now defaults to\na user-space install with no sudo.\n\n1. **Global hotkey** (default: Right Ctrl, toggle mode) starts recording — 16 kHz mono via PipeWire.\n2. **Local transcription** — faster-whisper on CUDA GPU when available, CPU int8 otherwise (whisper.cpp, torch, and NVIDIA Parakeet TDT via ONNX Runtime backends also supported).\n3. **Post-processing chain** — filler-word removal → custom dictionary →**spoken punctuation commands** (`literal comma` ,`literal new line` ,`example literal dot com` , …) — the full FluidVoice rule table.\n4. **Optional AI polish** — the*verbatim* FluidVoice dictation prompt sent to any OpenAI-compatible endpoint (OpenAI, Groq, Ollama, LM Studio, llama.cpp server). Turns*\"um lets meet on tuesday around 3 no wait 4 p.m.\"* into*\"Let's meet on Tuesday at 4 p.m.\"*\n5. **Text insertion** —`xdotool type` keystrokes (clipboard-free), or clipboard paste with automatic restore for long texts. Paste is verified by observing the target read the selection before your clipboard is restored, dictation text flashes are hidden from clipboard managers (CopyQ live-verified), and terminal apps get`ctrl+shift+v` . Plus history, start/stop sounds (the original GPLv3 FluidVoice SFX), and desktop notifications.\n\n```\nhotkey ─▶ pw-record 16k mono ─▶ faster-whisper (CUDA/int8) ─▶ fillers/dictionary/\n                                                                spoken-punctuation\n                                                                        │\n        typed into focused app ◀─ xdotool type / paste+restore ◀─ optional AI polish\n                                                  (OpenAI-compatible / Ollama)\n```\n\nOne download + one command, then SayItErmano appears in your app launcher,\nautostarts at login, and needs no terminal. The default install is\n**user-space and needs no sudo at all** (`~/.local/…` + a systemd user unit\nthat shadows any system unit); it only asks for sudo if a required system\npackage (GTK/pygobject, xdotool, …) is missing:\n\n```\ncurl -fsSL https://raw.githubusercontent.com/acailic/SayItErmano/linux/scripts/install-one-shot.sh | bash\n```\n\nPrefer the classic system-wide .deb (root-owned, `/opt` runtime)?\n\n```\ncurl -fsSL https://raw.githubusercontent.com/acailic/SayItErmano/linux/scripts/install-one-shot.sh | bash -s -- --system\ncurl -LO https://github.com/acailic/SayItErmano/releases/download/v0.4.0/sayit-ermano_0.4.0-2_amd64.deb\nsudo apt install ./sayit-ermano_0.4.0-2_amd64.deb\n```\n\nGrab a specific version from the [releases page](https://github.com/acailic/SayItErmano/releases),\nor build it yourself: `git clone … -b linux && ./packaging/build-deb.sh`.\n\n**What you get after install** (log out/in once):\n\n- **App launcher entry \"SayItErmano\"** (opens the native app) with its own icon\n- **Daemon autostarts at login** (XDG autostart; a systemd user unit is also\nprovided:`systemctl --user enable --now sayit-ermano` )\n- `sayit-ermano` available everywhere in PATH (`doctor` ,`toggle` ,`settings` ,`history` , …)\n- Removes cleanly with `sudo apt remove sayit-ermano` — upgrading from the\npre-rename`fluidvoice-linux` package replaces it automatically; your\nconfig, history and downloaded models are kept\n\n```\ngit clone https://github.com/acailic/SayItErmano.git -b linux\ncd SayItErmano\n./scripts/install.sh          # apt deps + venv (reuses your CUDA torch if present)\n\n# run it (foreground; systemd unit in systemd/)\n.venv/bin/sayit-ermano daemon\n```\n\nPress **Right Ctrl**, speak, press **Right Ctrl** again. Done.\n\nUseful commands:\n\n```\nsayit-ermano app               # native GTK app: History, Settings, onboarding\nsayit-ermano doctor            # environment check\nsayit-ermano toggle            # CLI trigger (bind to a DE shortcut on Wayland)\nsayit-ermano cancel            # abort a recording\nsayit-ermano language          # cycle the dictation language (language_cycle)\nsayit-ermano transcribe x.opus --json   # one-shot file transcription\nsayit-ermano history -n 10\nsayit-ermano config init       # write ~/.config/sayit-ermano/config.toml\nsayit-ermano update            # check for a newer release + print the upgrade command\n```\n\nWorks on any Linux with Python 3.11+ — a pipx install lands under\n`~/.local/pipx` (or `~/.local/share/pipx`) and never touches the system\nPython:\n\n```\npipx install sayit-ermano          # from PyPI (publishing is manual — if the\n                                   # latest release isn't on PyPI yet, use:)\npipx install git+https://github.com/acailic/SayItErmano.git@linux\n```\n\nUpgrades are one command (`sayit-ermano update` detects the pipx install\nand prints exactly this):\n\n```\npipx upgrade sayit-ermano\n```\n\nYou can also install a locally built wheel (e.g. after `git clone` +\n`uv build --wheel`): `pipx install ./dist/sayit_ermano-<ver>-py3-none-any.whl`.\nVerify an install any time with `./scripts/verify-pipx.sh` (entry points,\ndata files, and the updater's install-method detection, in a sandbox).\n\nUsing the one-shot user install instead? Its bundled venv can be upgraded\ndirectly (the exact line `sayit-ermano update` prints for that layout):\n\n```\n~/.local/share/sayit-ermano/venv/bin/pip install -U sayit-ermano\n```\n\n(re-running the one-shot installer is the fully supported path — it also restarts the daemon and cleans up duplicate installs.)\n\nAn AUR recipe is maintained in [`packaging/aur/`](/acailic/SayItErmano/blob/linux/packaging/aur)\n(`sayit-ermano-bin`, built from the release `.deb` asset) — community-\nadopted, **not published by us** (project rule: manual releases only).\n\n- **X11** : the full experience (global hotkey grab + xdotool typing + the\npill preview).**Wayland is supported** since v0.3 — see the matrix\nbelow; you need`wtype` or`ydotool` for text insertion and a\ndesktop-environment custom shortcut for the hotkey (Settings → Wayland\nassists with both).\n- Python 3.11+ (tested 3.12), `pipewire` (`pw-record` ),`xdotool` ,`xclip` ,`libnotify-bin` ,`pulseaudio-utils` (sounds).\n- A whisper model is downloaded on first use (~75 MB tiny … ~3.1 GB large-v3;\ndefault `small` ≈ 484 MB, or`base` on CPU). For the whisper.cpp backend,\nthe curated GGUF models are one-click downloads in Settings → Models.\n- GPU is optional: faster-whisper uses CUDA automatically when cuBLAS 12 + cuDNN 9 are resolvable; otherwise it falls back to CPU int8.\n\n| Capability | X11 | Wayland | \n|---|---|---|\n| Global hotkey | `XGrabKey` (toggle + hold) | DE custom shortcut → the generated `sayit-ermano-toggle` script (Settings → Wayland prints the per-GNOME/KDE/COSMIC steps;`sayit-ermano doctor` too). Optional**evdev push-to-talk** : hold a physical key read from`/dev/input` (`hotkey.wayland_evdev` , privileged —`input` group +`pip install 'sayit-ermano[wayland]'` ) | \n| Text insertion | `xdotool type` | `wtype` (wlroots/KDE — GNOME has no virtual-keyboard protocol) or`ydotool` (any compositor; needs`ydotoold` running +`/dev/uinput` access).`insertion.wayland_tool` =`auto\\|wtype\\|ydotool` | \n| Paste mode | verified read-observation + clipboard restore | `wl-clipboard` + fixed settle + restore —**paste verification is impossible on Wayland** (no client can observe another client's selection reads), and no clipboard-manager hygiene markers can be advertised | \n| Live preview | X11 pill | notification bubble (the layer-shell pill on wlroots compositors is future work; no pill on GNOME-Wayland) | \n| Tray | StatusNotifierItem | StatusNotifierItem (same) | \n| App hints / terminal quirks | WM_CLASS via `xdotool` | unavailable (AT-SPI is future work) — `general.terminal_apps` quirks are inert | \n\nInstall the tools:\n\n```\nsudo apt install wtype wl-clipboard        # sway/wlroots, KDE\nsudo apt install ydotool wl-clipboard       # any compositor incl. GNOME:\nsudo systemctl enable --now ydotool         # ydotoold must run; /dev/uinput\nsudo usermod -aG input $USER                # only for evdev push-to-talk\n```\n\n`sayit-ermano doctor` prints the per-capability matrix with per-tool\nfound/missing on your session, and `sayit-ermano status` carries the same\nmatrix (additive `session`/` capabilities` keys in the JSON).\n\n`sayit-ermano app` opens a native GTK 4 / libadwaita app (single instance;\nfollows your system theme) that mirrors the macOS app's windows:\n\n- **History** (main window) — live status header, search, copy/delete,\ninline audio replay for retained recordings.\n- **Settings** — General / Models (one-click switch + download) / AI polish\n(any OpenAI-compatible endpoint, live Test connection, per-app prompts) /\nDictation (hotkeys with press-to-capture, mic picker, live-preview sizes,\nspoken send) / History. Saving hot-applies what the daemon can take live\n(hotkey re-grab, recorder/tray/model rebuild) and says what needs a restart.\n- **Onboarding** — opens once on first launch with a real 3-second tryout.\n\nWith the daemon stopped, History still works and Settings saves to the\nconfig file directly (applies on next daemon start). Everything it saves goes\nto the same `config.toml` (with a strict whitelist; API keys are never exposed\nthrough the UI — use the env var). Settings talk to the daemon over the\nuser-owned unix control socket — no network listener exists. The config file\nis written with 0600 permissions.\n\n```\n[ai]\nenabled  = true\nbase_url = \"http://localhost:11434/v1\"   # Ollama; or OpenAI/Groq/LM Studio\nmodel    = \"qwen3:8b\"                    # pick a general-purpose chat model\n```\n\nWith Ollama: `ollama pull qwen3:8b`. No key needed for local endpoints; for cloud\nproviders set `api_key_env = \"SAYITERMANO_API_KEY\"` and export the variable\n(keys are never written to disk by the tooling).\n\n**Refusal guardrail** (`ai.refusal_guard`, default on): a polish reply that\nreads as a model refusal — \"I'm sorry, I can't assist with that.\" — is never\ntyped into your document; the raw transcript is used instead and a\nnotification explains why. Opt out with `refusal_guard = false` if you want\nthe model's reply verbatim (English patterns only in v1).\n\nSet `command_key` under `[hotkey]` to a spare keysym (e.g. `F10`); like the\nrewrite key it needs `[ai]` enabled with a base URL and model. Press it and\ndictate an instruction (\"list the biggest files in my downloads folder\");\nstop the recording with the main dictation key. The model answers in a\nstrict-JSON tool-call protocol (the upstream `execute_terminal_command`\nschema) and proposes shell commands — shown in the pill overlay in an\nawaiting-confirmation state — and you press the command hotkey again to run\neach one; `Escape` cancels. **Every** command requires that explicit\nconfirmation before anything executes; output is fed back to the model and\nthe loop continues (bounded by `[command] max_turns`).\n\nCommands matching the built-in **destructive list** (ported from upstream:\n`rm`, `mv`, `sudo`, `kill`, `chmod`, `dd`, redirections, `xargs rm`, …) or\nyour own patterns get a stronger gate: an amber ⚠ pill and a **two-press**\nconfirm — the first press only arms, the second runs.\n\nFollow-up context: within `command.context_window_s` (default 300 s, `0`\ndisables) the last five executed results in the *same* focused app are\nreplayed to the model so \"now the biggest one\" works; say **\"new session\"**\nto clear it. Nothing is persisted — a daemon restart starts cold.\n\nExecuted commands land in History, which has a **Commands** page showing\ncommand, purpose, exit code, duration and collapsible output, with Copy and\n**Re-run** (re-run only re-posts a proposal — it still needs the hotkey\nconfirm, never silent):\n\n```\n[command]\ncontext_window_s = 300.0\n# extra strings treated as destructive (case-insensitive substrings):\ndestructive_patterns = [\"git push\", \"shutdown\"]\n```\n\nNothing leaves your machine unless you point SayItErmano at a URL you\nchoose — the remote backend is **off by default** and LAN-friendly. When\n`remote_url` is set, each dictation POSTs the recorded WAV (the same\n16 kHz mono audio the local backends decode) as multipart to\n`<url>/v1/audio/transcriptions` on **any OpenAI-compatible server** and\ntypes back the returned text:\n\n```\n[model]\nremote_url  = \"http://192.168.1.50:8000\"  # empty = local models only\nremote_model = \"whisper-large-v3\"          # model name sent with the request\n# remote_api_key = \"\"                      # optional bearer token (masked, never logged)\n# remote_timeout_s = 30\n```\n\nServer examples:\n\n- **vLLM** :`vllm serve openai/whisper-large-v3 --port 8000` →`remote_url = \"http://<lan-host>:8000\"` ,`remote_model = \"whisper-large-v3\"`\n- **whisper.cpp** :`whisper-server -m models/ggml-large-v3.bin --port 8080` (its OpenAI-compatible route)\n- anything else speaking `POST /v1/audio/transcriptions` works: NVIDIA NIM,\nDGX Spark, Groq/OpenAI cloud (set`remote_api_key` there)\n\n`sayit-ermano doctor` shows the endpoint and probes reachability. To go\nback to local models, clear `remote_url` (Settings → Models → Remote\nsends the empty value — that means \"off\") or click **Use** on a local\nmodel, which clears it for you. Notes: remote takes have **no live\npreview and no VAD auto-stop** (one batch decode per take); transient\nfailures (connection refused, 429/5xx) are retried once, other HTTP\nerrors are not; a saved API key is removed by editing `config.toml`\n(same limitation as `ai.api_key`).\n\n## **File transcription** (`sayit-ermano transcribe`)\n\nAccepts **wav, flac, mp3, opus, oga, ogg, m4a, aac, wma, aiff, webm** (verified\nto decode via PyAV). Unknown extensions are still attempted: anything PyAV\ncan't open is converted with **ffmpeg** to 16 kHz mono WAV first\n(`sudo apt install ffmpeg` if it's missing). The whisper.cpp backend always\nconverts via ffmpeg since `whisper-cli` reliably reads WAV only.\n\n- `--json` prints`{text, language, duration_s, segments}` where`segments` are raw`{start, end, text}` per-segment entries with timestamps\n(not post-processed;`[]` on the whisper.cpp backend — segment parsing\nisn't wired up there in v1).\n- `--out PATH` writes the result to a file instead of stdout (JSON with`--json` ); missing parent directories are created.\n- Inputs over 25 MB warn: transcription is **not chunked** in v1 and may be\nslow/memory-heavy — shrink first with`ffmpeg -i in.opus -ar 16000 -ac 1 out.wav` .\n\n| Feature | macOS (upstream) | Linux port (SayItErmano) | \n|---|---|---|\n| Push hotkey → dictate → text in any app | ✅ (Right ⌥) | ✅ (Right Ctrl / any key) | \n| 100% local transcription | ✅ (Parakeet/Nemotron/Whisper/Apple) | ✅ (faster-whisper/whisper.cpp/Parakeet TDT via ONNX; `backend = \"parakeet\"` ) | \n| Toggle & hold (push-to-talk) modes | ✅ | ✅ toggle; hold for non-modifier keys (other keys pass through while held) | \n| Mouse-button push-to-talk | ✅ (PR #939) | ✅ spare button 6–255 ( `recording.push_to_talk_button = \"button8\"` ; clicks pass through while held; buttons 1–5 refused) | \n| Hotkeys pause while the screen is locked | ✅ | ✅ `general.pause_when_locked` (logind watch, resolves the session under the systemd user unit too; active dictation cancels, tray notes`paused (locked)` ) | \n| Filler-word removal + custom dictionary | ✅ | ✅ (same defaults) | \n| Spoken punctuation (\"literal comma\") | ✅ full rule table | ✅ ported (dot/slash/at-sign contexts included) | \n| AI polish with the original prompt | ✅ (local Fluid Intelligence or cloud) | ✅ (any OpenAI-compatible endpoint; no bundled local LLM yet) | \n| Start/stop sounds | ✅ | ✅ (same GPLv3 SFX) | \n| Live streaming preview overlay | ✅ | ✅ Mac-style pill (live waveform, mode accent colors, state labels, send indicator) | \n| Write/Rewrite selected text | ✅ (⌥R) | ✅ dedicated rewrite hotkey | \n| Command mode (voice → terminal agent) | ✅ (notch chat panel) | ✅ dedicated hotkey, live conversation panel, upstream tool schema in the JSON protocol, destructive strong-confirm, per-app context, History Commands view | \n| Per-app prompt sets | ✅ | 🚧 roadmap (app hint is already captured) | \n| Settings UI with model picker | ✅ | ✅ native GTK app ( `sayit-ermano app` ): Settings + History windows | \n| Onboarding (setup + tryout) | ✅ | ✅ opens once on first launch ( `sayit-ermano app --onboard` ) | \n| Overlay sizes (pill/small/medium/large) | ✅ | ✅ `recording.preview_overlay_size` | \n| Notch overlay / menu bar | ✅ | ✅ tray/panel icon (StatusNotifierItem): click = dictate, state badge, tooltip with hotkey | \n\nSee [docs/STATUS.md](/acailic/SayItErmano/blob/linux/docs/STATUS.md) for the full done/left ledger (verified\nby a 5-agent audit against the upstream Swift sources),\n[docs/COMPARISON.md](/acailic/SayItErmano/blob/linux/docs/COMPARISON.md) for how this relates to other Linux\ndictation tools (Handy, Vocalinux, nerd-dictation, …),\n[docs/ROADMAP.md](/acailic/SayItErmano/blob/linux/docs/ROADMAP.md) for the forward plan, and\n[docs/UPSTREAM-TRACKING.md](/acailic/SayItErmano/blob/linux/docs/UPSTREAM-TRACKING.md) for the\nmacOS-vs-Linux capability matrix and the upstream changelog we track\n(refresh it with `scripts/upstream-diff.sh`).\n\nSayItErmano checks GitHub **once per daemon start and once a day** for a\nnewer release (10 s timeout, on a background thread — startup is never\ndelayed). When a newer release is seen you get **one desktop\nnotification**; `sayit-ermano status`, the History window's status row,\nSettings → About and `sayit-ermano doctor` show it too. Nothing is ever\ninstalled automatically — run:\n\n```\nsayit-ermano update    # prints the exact copy-paste upgrade command for\n                       # YOUR install method (deb dpkg -i / one-shot\n                       # installer / pipx upgrade / git pull)\nsayit-ermano update --dismiss   # stop the notification for this release\n```\n\n`doctor` also warns when a system deb (`/opt/sayit-ermano`) and a user\ninstall (`~/.local/share/sayit-ermano`) coexist — the two-daemon hotkey\nfight this project's lock file guards against at runtime.\n\nThe control socket (JSON lines, filesystem-scoped to your runtime dir —\nno TCP by design) is a scriptable API. Beyond `toggle`/` status`/\n`set-config`/` select-model`, two routes exist for on-device agents:\n\n``` python\nfrom fluidvoice import control\n# transcribe a file through the daemon's WARM model (no reload)\nr = control.request(\"transcribe\", path=\"/tmp/note.wav\", process=True)\nprint(r[\"text\"])\n# query stored dictations (chronological, newest last)\nh = control.request(\"history\", limit=5, since_ts=1788800000.0)\necho '{\"action\": \"transcribe\", \"path\": \"/tmp/note.wav\"}' \\\n  | socat - UNIX-CONNECT:/run/user/$(id -u)/sayit-ermano.sock\n```\n\n`transcribe` refuses while a dictation is running (the GPU stays\ndedicated to your take) and rejects files over 200 MB (v1 does not\nchunk); `process: true` runs the standard filler/punctuation chain.\n\nDisable the checks entirely:\n\n```\n[updates]\ncheck = false   # no GitHub probe at all (notify = false keeps checks, drops\n                # only the desktop notification)\n```\n\n(`SAYITERMANO_SKIP_UPDATE_CHECK=1` does the same per-run.)\n\nEverything lives in `~/.config/sayit-ermano/config.toml` (generated by\n`sayit-ermano config init`; full commented template). Highlights:\n\n```\n[hotkey]\nkey = \"Right_Control\"       # any keysym: F9, space, Pause, right_alt...\nmode = \"toggle\"             # or \"hold\" (push-to-talk; other keys pass through while held)\n# language_key = \"F7\"       # optional: cycles the language at runtime\n\n[model]\nname = \"small\"              # tiny/base/small/medium/large-v3/large-v3-turbo\n# backend = \"whisper.cpp\"    # use the external whisper-cli binary instead\nwhispercpp_model = \"ggml-base.bin\"  # catalog name or path — download via Settings → Models\n# backend = \"parakeet\"       # NVIDIA Parakeet TDT via ONNX Runtime (pip install '.[parakeet]')\n# name = \"parakeet-tdt-0.6b-v2\"     # or parakeet-tdt-0.6b-v3 (multilingual)\ndevice = \"auto\"             # auto | cuda | cpu\n# idle_unload_s = 0          # unload the model after N idle seconds (0 = never; 30..86400)\n# Remote OpenAI-compatible STT server — nothing leaves this machine\n# while remote_url is empty (see \"Remote STT server\" above)\n# remote_url = \"http://192.168.1.50:8000\"\n# remote_model = \"whisper-large-v3\"\n# remote_api_key = \"\"        # optional bearer token\n# remote_timeout_s = 30\n\n[general]\n# Fast language switching: the cycle key steps this list at runtime\n# (state is never persisted); the guard re-decodes once with the first\n# whitelist entry when auto-detection lands outside it\n# language_cycle = [\"auto\", \"en\", \"sl\"]\n# language_whitelist = [\"sl\", \"en\"]\n# Case-insensitive WM_CLASS substrings identifying terminals — spoken-send\n# never presses Enter here and typed insertions gain one autocomplete space\nterminal_apps = [\"gnome-terminal\", \"kgx\", \"konsole\", \"xterm\", \"alacritty\", \"kitty\", \"wezterm\", \"ghostty\", \"foot\", \"tilix\", \"terminator\", \"guake\", \"yakuake\", \"st-256color\", \"warp\"]\n\n[recording]\nmic_priority = [\"bluez\", \"usb-cam\"]  # fallback order when the chosen mic vanishes\n# (Bluetooth headset first, then a USB webcam; switch never happens mid-take)\n# Mouse push-to-talk: hold a spare button to dictate (always hold-style,\n# independent of hotkey.mode). Thumb buttons are usually 8/9; 1–5 refused.\npush_to_talk_button = \"button8\"  # \"\" = off\n# Spoken-send quiet countdown: 0.5 s of silence after saying the send\n# phrase finishes the dictation by itself (speak again to cancel) — no\n# hotkey press needed. 0 = off; needs spoken-send enabled in Settings.\nspoken_send_countdown_s = 1.2\n\n[general]\n# Ignore hotkeys + cancel active dictation while the session is locked\npause_when_locked = true\n\n[processing]\ndictionary = [ { triggers = [\"miro board\"], replacement = \"Miro board\" } ]\n# Corrections you make in History teach the dictionary: after the same\n# fix is seen twice, Settings -> Dictation suggests it under \"Suggested words\"\n# Chat-app literal squeeze: \"/ fix the deploy\" -> \"/fix the deploy\",\n# \"@ John Smith\" -> \"@John Smith\" (runs after AI cleanup)\nslash_mention_squeeze = true\n\n[insertion]\nmode = \"auto\"               # typed | paste | auto (paste for long texts)\n# One trailing space after typed insertions in terminal apps so the shell's\n# autocomplete commits\nterminal_autocomplete_space = true\n# Verify the paste landed (selection read) before restoring the clipboard;\n# false = legacy fixed-delay restore\nverify_paste = true\n# Keystroke used to paste in terminal apps (general.terminal_apps); X11\n# terminals pass ctrl+v through to the app, they need ctrl+shift+v\nterminal_paste_key = \"ctrl+shift+v\"\n```\n\n**Why is my first dictation slow after a break?** With `idle_unload_s`\nset (Settings → Models → Memory), the speech model is released after the\nidle window to free RAM — and VRAM on GPU machines — instead of staying\npinned in memory forever. The first dictation afterwards pays the model\nload again (a few seconds; the load overlaps your speech), while the\nfollowing ones are instant. Set it to `0` — the default — to keep the\nmodel always loaded, and check the live state any time with\n`sayit-ermano doctor` (it prints `model: loaded/unloaded (idle Xm; policy Ns)`).\n\n**Branch layout:** the port lives on `linux` — treat it as this fork's main\nline (all commits, merges, and releases go there). The `main` branch mirrors\nthe upstream macOS repo for reference only and is **never** updated with port\nwork; to see what moved upstream, run `scripts/upstream-diff.sh`.\n\n```\n.venv/bin/python -m pytest tests -m \"not slow and not integration\"  # unit: offline, fast\n.venv/bin/python -m pytest tests -m \"integration and not desktop\"   # real subsystems, deterministic\n.venv/bin/python -m pytest tests -m desktop                          # live session (grabs, pixels)\n.venv/bin/python -m pytest tests -m \"not desktop\"                    # deterministic everything\n```\n\nThe test pyramid — **558 automated tests** at v0.4.0:\n\n| Layer | What it exercises | Count | \n|---|---|---|\n| Unit | processing engines, AI client (mocked transport), daemon state machine (stubs), insertion command construction, config validation (apply_settings), overlay/pill painting, GTK app offscreen smoke tests | 527 | \n| E2E (slow) | real whisper model transcribing the JFK sample | 2 | \n| Integration | real `pw-record` capture + raw→WAV, GPU transcription, streaming preview with the loaded model, a real daemon**subprocess** (socket control incl. get/set-config + select-model, toggle/cancel, clean shutdown), live X11 hotkey grab + overlay pixel proof, real CLI invocations (doctor/transcribe/history/config), live AI polish + rewrite against local Ollama (skipped when absent), .deb extract + relocated-venv import, one-shot installer DRY_RUN download | 29 | \n\nIntegration tests run against your real PipeWire/X11/CUDA environment and are\nisolated through `SAYITERMANO_CONFIG` / `SAYITERMANO_SOCKET` / `XDG_DATA_HOME`\nenv overrides (the same overrides work for running multiple daemons).\n\nLayout: `fluidvoice/backends/` (speech engines) · `processing/` (fillers,\ndictionary, spoken punctuation) · `ai/` (prompts + OpenAI-compatible client) ·\n`insertion.py` · `hotkey.py` (XGrabKey) · `control.py` (unix socket) ·\n`daemon.py` (orchestration).\n\n- **GPL-3.0** — same license as upstream. The dictation/edit prompts and the\nstart/stop sounds are copied from[altic-dev/FluidVoice](https://github.com/altic-dev/FluidVoice) (GPLv3). Huge thanks to the FluidVoice authors for open-sourcing it.\n- Speech by [faster-whisper](https://github.com/SYstran/faster-whisper) (MIT) /[whisper.cpp](https://github.com/ggml-org/whisper.cpp) (MIT) /[OpenAI Whisper](https://github.com/openai/whisper) (MIT).\n- \"FluidVoice\" is the upstream project's name; SayItErmano is this community-maintained Linux port of it and is not affiliated with or endorsed by altic-dev.", "url": "https://wpnews.pro/news/sayitermano-local-first-voice-dictation-for-linux", "canonical_source": "https://github.com/acailic/SayItErmano", "published_at": "2026-09-08 00:09:20+00:00", "updated_at": "2026-09-08 00:31:47.250713+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "natural-language-processing"], "entities": ["SayItErmano", "FluidVoice", "faster-whisper", "OpenAI", "Groq", "Ollama", "LM Studio", "llama.cpp"], "alternates": {"html": "https://wpnews.pro/news/sayitermano-local-first-voice-dictation-for-linux", "markdown": "https://wpnews.pro/news/sayitermano-local-first-voice-dictation-for-linux.md", "text": "https://wpnews.pro/news/sayitermano-local-first-voice-dictation-for-linux.txt", "jsonld": "https://wpnews.pro/news/sayitermano-local-first-voice-dictation-for-linux.jsonld"}}