Hold Speak

Local push-to-talk dictation for macOS. Hold the hotkey, speak, release — recognized text is inserted into the focused input. Local by default: NVIDIA Parakeet Ultra (macOS 14+) or OpenAI Whisper Turbo (via WhisperKit) runs on this Mac. Optionally, you can switch to Google Gemini or OpenAI with your own API key.

Hold Speak menu bar popover
Fig. 01 — Menu bar popover with recent transcriptions

A free, local alternative to paid Whisper wrappers. Built for one reason: talking to AI is faster than typing to it. Average typing speed hovers around 40–60 wpm; comfortable speech is 130–160 wpm — two to three times faster. As more of the dev loop moves into prompts to AI agents, raw typing throughput becomes a real ceiling on how quickly you can iterate. Hold Speak lifts it: hold the hotkey, speak a full thought, release, it lands in the input.

01

Features

  • Local transcription through WhisperKit (CoreML, GPU) or, on macOS 14+ with Apple Silicon, NVIDIA Parakeet Ultra through FluidAudio (CoreML, Neural Engine)
  • Optional cloud engines — Google Gemini or OpenAI with your own API key, for higher accuracy on mixed-language speech and jargon
  • Code-switching RU/EN/UK and more — in auto mode the language is chosen only from the ones you have in System Settings → Language & Region
  • Insertion without clipboard — via CGEventKeyboardSetUnicodeString; password fields are skipped
  • Menu bar popover with recent transcriptions (click to copy) and metrics: dictations today / yesterday, 7-day avg WPM, each with its change vs. a week ago
  • HUD overlay while you hold the key: dark pill with a live mic level; if a dictation fails (no model, bad API key, quota), the reason shows in the same pill
  • Light text cleanup — trims long "eeeeee / mmmmm / ummm", collapses 3+ consecutive repeats, capitalizes the first letter and adds a period
  • Optional filler-word removal — off by default; drops "ну / короче / как бы / um / you know" from an editable list (Whisper, Parakeet, OpenAI) or by context (Gemini)
  • Liquid Glass on macOS 26 and later — the HUD, popover, Preferences and onboarding use the system glass; macOS 13–15 keep the classic look
  • Terminology dictionary — canonical IT terms (pull request, Kubernetes, Claude Code, …) replace misrecognized Russian transliterations in transcripts; ships with ~110 defaults and is fully editable
02

Install

Requires a Mac with Apple Silicon (M1 or later) and macOS 13 or later.

Grab the latest DMG from the Releases page, open it, and drag HoldSpeak.app into Applications.

Because the app is self-signed, macOS will block the first launch. Open System Settings → Privacy & Security, scroll to the message "HoldSpeak was blocked…" and click Open Anyway. Confirm with Touch ID / password. After that it launches normally from Launchpad / Applications.

On first launch, choose how speech is recognized — NVIDIA Parakeet Ultra (on this Mac, fastest, 25 European languages, ~610 MB download; macOS 14+ on Apple Silicon), OpenAI Whisper Turbo (on this Mac, 99 languages, slower, ~1.5 GB), Gemini (Google cloud) or OpenAI (cloud); the cloud engines ask for your API key — then grant three permissions:

  • Microphone — for audio capture
  • Accessibility — for the global hotkey and text insertion
  • Input Monitoring — to use Right Option / Right Cmd (or the hotkey you choose) as push-to-talk

The onboarding window has Open… and Re-check buttons.

Onboarding · choose NVIDIA Parakeet Ultra, OpenAI Whisper Turbo, Gemini or OpenAI
Fig. 02 — First launch: choose NVIDIA Parakeet Ultra, OpenAI Whisper Turbo, Gemini or OpenAI

Updating

The app checks GitHub for new versions in the background and shows an Update available banner in the menu bar popover. You can also trigger a check manually via Preferences → General → Check for updates.

  1. Click Download in the banner — it opens the latest release on GitHub.
  2. Download the .dmg, open it, and drag HoldSpeak.app into Applications. macOS will ask to replace the old copy — confirm.
  3. Quit the running app from the menu bar (Quit), then launch the new one from Applications.

Your preferences, history, and downloaded models live in ~/Library/Application Support/holdspeak/ and are preserved across updates.

Model

Preferences → Audio → Model lists the models in two groups, best first in each. Under "On this Mac — free, works offline":

  • NVIDIA Parakeet Ultra (610 MB) — the fastest. Runs on the Neural Engine via FluidAudio. 25 European languages, including English, Russian and Ukrainian; it detects the language itself. Needs macOS 14 or later on Apple Silicon — otherwise the option is hidden. The default on macOS 14+. Much faster than Whisper Turbo (see Reducing insertion latency). The first load after installing or updating the app takes about 12 s while macOS compiles the model once; after that it loads in a fraction of a second.
  • OpenAI Whisper Turbo (large-v3 distilled, 1.5 GB) — the default on macOS 13. 99 languages, slower. Runs on macOS 13 and later.

Under "Cloud — your API key, pay per use": OpenAI GPT Transcribe, OpenAI GPT-4o mini, Gemini 3.5 Transcribe and Gemini 3.5 Flash-Lite — see Gemini and OpenAI below. Each item shows its price per hour of speech.

Preferences · Audio · Model list
Fig. 03 — Preferences · Audio · the Model list

Parakeet Ultra by moondream, based on NVIDIA Parakeet TDT 0.6B v3, CC-BY-4.0; Core ML build by FluidInference (FluidAudio).

If you already have MacWhisper / another WhisperKit client installed, its Whisper model will be picked up automatically. Otherwise models are downloaded to ~/Library/Application Support/HoldSpeak/models/ (Parakeet into models/parakeet-ultra).

To free disk space, Preferences → Audio → Downloaded models → Delete… removes every model Hold Speak downloaded. Models that belong to MacWhisper or other apps are left alone.

Gemini (optional, cloud)

Pick a Gemini model in Preferences → Audio → Model (or choose Gemini during onboarding) and paste an API key:

  1. Create a key at Google AI Studio → API keys.
  2. Set up billing for the key's project in AI Studio. Without billing the key runs on the free tier, which stops after a couple dozen dictations a day — Hold Speak then shows "Gemini daily limit reached — set up billing" in the HUD.
  3. Paste the key in Hold Speak and click Save. The key is checked with Google and stored in the macOS Keychain, never in preferences files; it survives app updates. After each update macOS asks once to let the new version read the key — enter your Mac password and click Always Allow (Hold Speak is self-signed, so the Keychain sees every new build as a new app).
Preferences · Audio with Gemini selected
Fig. 04 — Preferences · Audio with Gemini selected

Models:

  • Gemini 3.5 Transcribe (default) — dedicated speech-to-text, ~2 s per dictation, about $0.30 per hour of speech.
  • Gemini 3.5 Flash-Lite — cheaper (~$0.06/hour) and better at following the terminology dictionary, but response time varies more.

If the selected model fails on Google's side (an error that isn't about the key, quota or network), HoldSpeak sends the same audio to the other model and says so in the HUD pill; the failed model is skipped for the next 30 minutes.

If something goes wrong, the reason appears right in the HUD pill instead of a silent empty result:

HUD · Gemini daily limit reached
Fig. 05 — HUD when the Gemini quota runs out

OpenAI (optional, cloud)

Pick an OpenAI model in Preferences → Audio → Model (or choose OpenAI during onboarding) and paste an API key:

  1. Create a key at OpenAI Platform → API keys.
  2. Add credits in OpenAI Platform → Billing — the API is prepaid. When the balance or the monthly budget runs out, Hold Speak shows "OpenAI credits used up" in the HUD.
  3. Paste the key in Hold Speak and click Save. Like the Gemini key, it is checked with OpenAI and stored in the macOS Keychain.

Models:

  • OpenAI GPT Transcribe (default, gpt-transcribe) — OpenAI's current speech-to-text model, about $0.27 per hour of speech. Your dictionary's terms go in as keywords, the primary language as a language hint, and in Auto mode the language it detects switches the dictionary.
  • OpenAI GPT-4o mini (gpt-4o-mini-transcribe) — cheaper, about $0.18 per hour; the terms go into the prompt.

Both are dedicated transcription models rather than chat models, so they don't follow instructions: filler words are removed afterwards from the same editable list as with Whisper. The fallback to the other model on a server-side error works as with Gemini.

Fallback to another engine (optional)

With a cloud engine selected, Preferences → Audio → Fallback: Try other engines if this one fails (off by default) keeps dictation working when the selected engine can't transcribe — credits used up, quota, a rejected key, the service down, no internet. The same audio then goes to the other cloud engine, if its key is saved, and then to the local model you picked last (OpenAI Whisper Turbo or NVIDIA Parakeet Ultra) on this Mac; the HUD says why and who answered ("OpenAI credits used up · Used Gemini instead", "… · Used OpenAI Whisper Turbo instead"). Without internet the other cloud engine is skipped and the local model answers straight away.

The local model isn't kept in memory while a cloud engine is selected: the first fallback loads the model (a few extra seconds), then it stays loaded. If the model isn't downloaded yet, the first fallback starts the download and that dictation is lost; without internet it can't be downloaded at all, so download it in advance (the checkbox offers a button).

Privacy: with Gemini or OpenAI, each dictation's audio is sent to Google or OpenAI respectively. The local models (NVIDIA Parakeet Ultra, OpenAI Whisper Turbo) stay the default and never send anything anywhere. Text cleanup, the "Add period" / "Capitalize" / "Remove filler words" options and the terminology dictionary apply to every engine (the Whisper-specific filter for subtitle-style hallucinations doesn't apply to Parakeet); the cloud engines also receive your dictionary's terms as spelling hints.

Reducing insertion latency

The delay between releasing the hotkey and text appearing in the input is dominated by the speech model itself. Two levers:

  • Switch to NVIDIA Parakeet Ultra (610 MB; macOS 14+, Apple Silicon). Preferences → Audio → Model → "On this Mac — free, works offline" → NVIDIA Parakeet Ultra. In our test on an M2 Max, the wait after the end of speech was 0.06–0.18 s with Parakeet versus 2.5–3.4 s with Whisper Turbo for 4–19 s clips, and accuracy was the same or slightly better (Russian with English IT terms: 23–30% word error rate versus 26–40%). The clips were synthetic macOS say speech on one machine, so treat these as rough figures. Parakeet covers 25 European languages; for other languages, or on macOS 13, use OpenAI Whisper Turbo (1.5 GB, the default; 99 languages).
  • With Whisper, set a fixed language instead of Auto. Preferences → Audio → Primary language: picking a specific language (English, Russian, Ukrainian, Spanish, German… 16 in total) skips an extra language-detection forward pass that Auto mode runs before transcription. Parakeet always detects the language itself, so this setting doesn't change its speed.
03

Usage

  1. Hold Right Option (or whatever you set in Preferences).
  2. Speak. A HUD appears in the top right corner (or bottom center — configurable) showing the mic level.
  3. Release the key. The text is inserted into the focused field — in a fraction of a second with Parakeet Ultra, after about 2.5–3.5 s with Whisper Turbo.
  4. If the field lost focus — open the menu bar icon: it shows the recent transcriptions; click to copy.

Short taps

By default, presses shorter than 150 ms don't start recording — the key behaves as a normal Option. The threshold is configurable in Preferences → General (50–800 ms).

04

Preferences

  • General — hotkey, hold threshold, HUD position (under the icon / bottom center), theme (Auto / Light / Dark), launch at login, update check
  • Audio — microphone, language (Auto-detect or one of 16 explicit languages), model (on this Mac: NVIDIA Parakeet Ultra, OpenAI Whisper Turbo; cloud: OpenAI, Gemini), model download / deletion, cloud API key, text cleanup options (period, capitalization, filler words)
  • Terms — terminology dictionary (see below)
  • History — clear history and reset metrics
  • Support
Preferences · General
Fig. 06 — Preferences · General
Preferences · Audio with NVIDIA Parakeet Ultra
Fig. 07 — Preferences · Audio with NVIDIA Parakeet Ultra

Filler words

Preferences → Audio → Remove filler words (off by default) cleans up the "ну", "короче", "как бы", "um", "you know" you say while thinking:

  • Whisper, Parakeet, OpenAI — a text field appears with an editable, comma-separated list, prefilled with common Russian and English fillers. Only whole words are removed (any case), together with the comma after them, so "Ну, короче, давай созвонимся" becomes "Давай созвонимся." and "нужно" is left alone. Words that often carry meaning — типа, вот, like — aren't in the default list; add them if you want.
  • Gemini — no list: the model is asked to drop fillers by context and keep the ones that mean something ("объект типа Promise", "I like it").

Auto language detection

In Auto mode the app reads Locale.preferredLanguages from the system and restricts Whisper to those languages only. So if macOS has RU, EN, UK enabled, Whisper will pick among them and won't drift into, say, Bulgarian.

When two of your preferred languages score close in detection (e.g. a sentence mixes Russian with English terms), the app drops the forced language for that utterance and lets Whisper switch per-segment — this tends to preserve English terms verbatim instead of transliterating them.

Parakeet Ultra doesn't take a language setting: it always detects the language on its own, among its 25. With Parakeet, Primary language only picks which terminology list is used.

Terminology dictionary

Whisper reliably recognizes common speech but routinely mangles IT terminology in mixed RU+EN dictation (пулл реквест instead of pull request, кубернетес instead of Kubernetes, and so on). The Terms tab lets you map your spoken variants to a single canonical form, which is then substituted in the transcript before it's inserted. Each Primary language keeps its own set of terms — in Auto mode the active set follows the detected language of the current utterance, so Russian-heavy speech uses your Russian dictionary and English-heavy speech uses the English one.

If Whisper keeps mangling the same word or name — a project codename, a library you use daily, a colleague's surname — stop fighting the model. Open Preferences → Terms, put the correct spelling in Canonical, and add the two or three variants Whisper tends to produce. Next time the word shows up it'll come out right without any hand-editing. That's the whole point of this feature: if you have to fix the transcript by hand every time, it's not dictation — it's a slower way to type. Teach the app once, save the corrections forever.
Preferences · Terms
Fig. 08 — Preferences · Terms

Default dictionaries. The app ships with curated IT defaults for Russian (~110 entries), English (~120), and Ukrainian (~130) — all spanning the whole dev cycle: VCS (pull request, rebase, cherry-pick), languages (TypeScript, Swift, Rust), frontend (React, Tailwind, Next.js), UX (wireframe, mockup, accessibility), backend (endpoint, middleware, migration), data (Postgres, Redis, ClickHouse), DevOps (Docker, Kubernetes, Helm chart), cloud (AWS, S3, Lambda), and AI tooling (Claude, MCP, Opus). On first launch the bundled lists are copied into your Application Support directory — from then on the files are yours.

Switching languages in the editor. Preferences → Terms has a Last detected picker (top-right, styled like the General dropdowns). It defaults to the language of your most recent utterance, but you can switch it to any other language to edit that set — handy for seeding an English dictionary before you start dictating in English.

How updates work. App updates do not touch your dictionary — your edits, additions, and deletions persist verbatim. To pull in new entries from the latest bundled default, open Preferences → Terms and click Load defaults…:

  • Merge — adds only canonical forms that aren't already in your list. Existing entries and your custom terms are untouched.
  • Replace — discards your list entirely and reloads the bundled defaults. Use with care.

Adding your own terms. Click Add term, put the target form in Canonical (webp), and list the variants Whisper tends to produce in Variants (one per line: Веб-пи, вебпп, вепп, Беппи). Matching is case-insensitive by default and respects word boundaries, so пулреквест won't hit inside пулреквестер.

Import / Export. Pure JSON — commit it to a dotfiles repo, share with a team, seed a new machine.

Storage. ~/Library/Application Support/holdspeak/terminology/<lang>.json (one file per language: ru.json, en.json, uk.json, …).

05

Architecture

  • Sources/Core — pure logic (hotkey, recorder, inserter, text cleaner, storage, metrics, Gemini and OpenAI API clients, Keychain)
  • Sources/Whisper — transcription engines: WhisperKit and Parakeet (local), Gemini and OpenAI (cloud) behind one TranscriptionEngine facade, model manager
  • Sources/Parakeet — Parakeet engine on FluidAudio, built as a separate framework for macOS 14+ and loaded at runtime
  • Sources/UI — SwiftUI: menu bar popover, preferences, onboarding, HUD
  • Sources/App — AppDelegate and entry point

Transcription history is stored in a GRDB-SQLite database at ~/Library/Application Support/holdspeak/history.sqlite.

06

Logs

Diagnostic events go to ~/Library/Logs/holdspeak.log. Useful for microphone / language detection / hotkey issues:

tail -f ~/Library/Logs/holdspeak.log
07

Troubleshooting

Hotkey doesn't fire. Check Input Monitoring and Accessibility in System Settings → Privacy & Security. When the signature changes (e.g. a fresh ad-hoc build) TCC may drop entries — delete and re-add, or run scripts/setup-signing.sh and rebuild.

Recognizes silence / empty result. Check the input level in System Settings → Sound → Input. In the logs, look at finalize: rms=... — normal speech is ≥ 0.02. If you see 0.0005 the mic is quiet (wrong device / muted / mic TCC not granted).

Confuses Ukrainian / Russian with English in auto. Auto relies on Locale.preferredLanguages. Make sure the language is actually listed in System Settings → Language & Region, or switch Preferences → Audio from Auto to an explicit language (e.g. Russian, Ukrainian or English).

TCC permissions reset on every rebuild. You haven't run scripts/setup-signing.sh yet. It creates a persistent self-signed certificate in the login keychain. After that every rebuild is signed with the same key, and macOS treats it as the same app.