HUSH is for people who think faster than they type. Hold a hotkey, speak, release — your words land in whatever app you're working in. Optionally run them through an LLM and get back a polished letter, task list, or formatted note. Speech recognition runs entirely on-device via Apple Neural Engine — no telemetry, no cloud microphone.
The idea came after discovering Spokenly. It showed that voice input with LLM post-processing is genuinely useful — not a gimmick. But I wanted something of my own: no paid restrictions, no cluttered interface, tailored to the workflows I actually use.
HUSH was built and refined in the field — I use it every day. Every detail comes from a real need, not a feature checklist. I hope it works just as well for you.
Everything is configurable: scenarios, providers, themes. No paid tiers — just open source.
- How It Works
- Modes
- Hotkeys
- Scenarios
- Scenario Editor
- History
- LLM Providers
- Color Themes
- Installation
- API Keys
- Privacy
- Architecture
- Languages
- Support
HUSH is an accessory app — no Dock icon, just a menu bar item. Launch it once and forget about it. It's always ready to accept your voice in any application.
Under the hood:
- Audio is captured via
sounddevicedirectly into RAM - Recognition runs through NVIDIA Parakeet TDT 0.6B, compiled for CoreML / Apple Neural Engine (~400 MB, bundled)
- Text is optionally sent to an LLM (local Ollama or cloud-based Anthropic / OpenAI / GLM)
- The result is injected into the active application via Accessibility API — no clipboard, no flicker
The first launch compiles the CoreML model for your specific chip. This takes up to a minute and happens exactly once. All subsequent launches are instant.
The fastest way to dictate. No extra steps needed.
- Hold Right ⌥ → a small floating indicator appears at the screen edge
- Speak — the indicator shows recording is active
- Release → the chunk is transcribed
To dictate in multiple passes:
- Hold and release several times → chunks accumulate
- After a 4-second pause — LLM processing begins
- The indicator bars visualize the countdown: green → red
- Enter during the countdown → paste immediately without LLM
- Hover during processing → an interrupt button appears (paste raw transcript)
The indicator remembers its screen position across sessions. Drag it anywhere.
For complex tasks: dictate in parts, choose a scenario, review before pasting.
- ⇧⌥ → the main window opens (no recording starts immediately)
- Hold ⌥ → record a chunk → release → transcript appears as a block in the window
- Repeat as many times as needed — blocks accumulate
- Pick a scenario and tap its button — text goes to the LLM
- Shift+Enter → paste the result into the previously active app, window closes
If a default full-mode scenario is set (★), it's applied automatically on Shift+Enter — no need to tap a scenario button manually.
When you've dictated a lot and want to read, edit, or simply view the text comfortably — expand the main window.
- Double-click anywhere in the main window's title bar → window expands to 640×680
- Double-click again → returns to compact size
In expanded mode the auxiliary panels (settings, history, providers, editor) automatically gather into a 2×2 cluster next to the window as a side effect:
- Drag any panel in the cluster → the whole cluster moves together
- 🔄 in settings → reset the cluster to its default position
- 🎯 → show / hide all cluster panels
| Gesture | Action |
|---|---|
| Hold Right ⌥ | Start recording (silent mode) |
| Release Right ⌥ | Stop recording, transcribe |
| ⇧⌥ (Shift + Right Option) | Open / close full mode |
| ⌥ in full-mode window | Record next block |
| Enter during countdown | Force immediate paste, skip LLM |
| Shift+Enter (full mode) | Paste (applies default scenario if set) |
| ⌥ during LLM processing | Interrupt LLM, paste raw text |
| Double-click title bar | Expand / collapse main window |
Scenarios are configurable LLM system prompts applied to the transcribed text. Each scenario is a button in the interface. The built-in ones are fully editable and you can create your own.
Configuration lives in ~/.config/hush/scenarios.json.
| Name | What it does |
|---|---|
| MAIN | Smart formatter: detects the text type (prompt, task list, letter, note) and formats accordingly; removes filler words and false starts |
| CLEAN | Adds punctuation, capitalization, typographic quotes; strips filler words without changing meaning |
| LETTER | Formats the text as a business letter: salutation, structure, sign-off |
| Tasks | Converts a stream of thoughts into a Markdown checkbox task list |
| MD | Formats as Markdown with headers, lists, and code blocks |
A scenario with no prompt pastes the raw transcript directly — handy as a quick-paste button.
silent mode— assign this scenario as the default for silent mode (only one allowed)full default ★— assign this scenario as the default for full mode (only one allowed)
Open: ⚙ Settings → click any scenario.
| Field | Description |
|---|---|
| Label (RU / EN / ES) | Up to 6 characters — displayed on the scenario button |
| Model | Override the LLM for this specific scenario (empty = auto) |
| Prompt | System instruction; transcribed text is appended automatically |
| Silent mode | Assign this scenario as default for silent mode |
| Full default ★ | Assign this scenario as default for full mode |
When switching between scenarios with unsaved changes, HUSH will ask: Save / Discard.
The last 50 transcriptions are saved automatically to ~/.config/hush/history.json.
The history panel (🕐) has three tabs:
| Tab | Contents |
|---|---|
| All | All blocks in chronological order |
| Sessions | Grouped: multiple chunks from one session merged into one entry |
| Blocks | Individual chunks only |
What you can do:
- Click an entry → add it as a new block in the current full-mode session
- Checkboxes → multi-select
- Buttons for multi-selection: Delete / Merge / Add / Replace
The panel stays open after pasting — you can keep picking and combining entries.
Configure via: ⚙ → [KEYS].
| Provider | Type | What you need |
|---|---|---|
| Ollama | Local | Ollama installed + a model pulled (ollama pull <model>) |
| Anthropic | Cloud | API key (sk-ant-...) |
| OpenAI | Cloud | API key (compatible with any OpenAI-compatible API) |
| GLM | Cloud | Zhipu GLM-4 API key |
The model for each scenario is chosen in the scenario editor via two dropdowns:
- Provider — select from configured and available providers (Ollama / Anthropic / OpenAI / GLM)
- Model — the list auto-populates with models available from the selected provider
If a provider isn't configured or isn't reachable it won't appear in the list. If no model is selected, HUSH uses an auto strategy: tries Ollama first, falls back to Anthropic if unavailable.
8 built-in themes, switchable from settings (⚙ → Theme). All open panels — settings, scenario editor, history — update instantly.
| Theme | Background | Accent |
|---|---|---|
| emerald | dark green | bright green |
| ocean | dark blue | cyan |
| neon | dark purple | magenta |
| gold | dark amber | yellow |
| paper | cream | dark green |
| sky | light blue | dark blue |
| sand | warm beige | brown |
| arctic | icy white | teal |
- macOS 13+ (Ventura or newer)
- Python 3.14 —
brew install python@3.14 - Accessibility permission — required for automatic text injection (requested on first launch)
- Microphone permission — requested on first launch
Optional, for LLM scenarios:
- Ollama with a model loaded
- Anthropic / OpenAI / GLM API key
git clone https://github.com/alexbic/hush.git
cd hush
# Install dependencies
pip3.14 install pyobjc sounddevice pynput anthropic openai
# Build the app bundle
bash build_app.sh
# Launch
open HUSH.appOn first launch HUSH automatically:
- Copies
parakeet-clito~/.local/bin/ - Places the CoreML model at
~/.local/share/hush/ - Compiles the model for your chip (ANE) — happens once, cache survives app updates
After that HUSH appears in the menu bar and is ready to use.
Option 1 — via the UI: ⚙ → [KEYS] → enter your keys in the fields.
Option 2 — via file ~/.hush_env:
export ANTHROPIC_API_KEY="sk-ant-..."
export OPENAI_API_KEY="sk-..."
export OLLAMA_BASE_URL="http://localhost:11434" # default
export GLM_API_KEY="..."HUSH loads this file at startup.
- All speech recognition runs locally — Parakeet TDT runs on your device via CoreML / Apple Neural Engine
- Audio is never written to disk and is not retained beyond a single session
- Only text is sent to a cloud LLM — and only if you've configured a cloud scenario (Anthropic / OpenAI / GLM)
- With Ollama scenarios, all processing stays on your machine
HUSH is written in Python 3.14 with native UI via PyObjC + AppKit. No Electron, no web tech — everything is native.
main.py Hotkey listener, session lifecycle, paste logic
overlay.py All UI: pill, full-mode card, history panel, themes
recorder.py Audio capture via sounddevice
transcriber.py Parakeet TDT subprocess wrapper + CoreML warmup
processor.py LLM routing (Ollama / Anthropic / OpenAI / GLM)
injector.py Accessibility-based text injection
config.py Paths, API keys, constants
build_app.sh Builds self-contained HUSH.app bundle
launcher.c Thin C launcher (ensures correct NSBundle for status bar)
Hold ⌥ → recorder.start() → audio stream
Release ⌥ → recorder.stop() → wav file queued
transcriber.transcribe(wav) [CoreML / ANE]
text appended to pill accumulation
4-second countdown
processor.process_with_prompt(text, scenario)
injector paste → prev app receives text
⇧⌥ → overlay opens (standby, no recording)
⌥ held → recorder.start()
⌥ released → recorder.stop() → transcribe → block shown in window
(repeat for more blocks)
Shift+Enter → [optional: default scenario LLM] → paste → window closes
Parakeet is a CoreML model (~400 MB, parakeet-tdt-0.6b-v3-coreml). Apple Neural Engine compiles device-specific execution plans on first run and caches them. Subsequent runs skip compilation and start in ~7 seconds.
HUSH preserves the cache by keeping the binary at a stable path (~/.local/bin/parakeet-cli). Rebuilding the app bundle does not invalidate the cache.
All my projects — alexbic.net, tools, apps — I try to ship in three languages: Russian, English, and Spanish. I want people with different language backgrounds to be able to use these things without friction. HUSH is no exception.
The interface and scenarios are translated into all three. I've personally tested recognition primarily in Russian — it's my native language. Parakeet TDT is advertised as multilingual, but how well it handles English or Spanish in practice — I honestly don't know.
If you work in English or Spanish and want to try it out, I'd love your feedback. How Parakeet handles your language, what works, what doesn't, what scenarios you've written — open an Issue or reach out directly. It'll help make HUSH better for everyone.
HUSH is a free, open-source project. Claude Code was used extensively in development — and there's no shame in that: language models today are just another developer tool, like a compiler or a code editor. We value our time, and we value yours.
If HUSH has been useful, if it's saved you even a few minutes a day — any support is genuinely appreciated. It helps keep interesting things getting built.
Thank you. Really.
Stability: parakeet-cli no longer hangs after system sleep
parakeet-cliruns inference on ANE/CoreML; if the process survives a system sleep, its ANE context is torn down and any operation on it blocks forever after wake — until the 360 s hard timeout._SleepObservernow kills the activeparakeet-cli(transcription and warm-up) insystemWillSleep_, and does the same defensively insystemDidWake_before reinitializing PortAudio — no process survives sleep with an unfinished ANE context.transcriber.cancel()now terminates both_current_procand_warmup_proc(previously only the active transcription), which also fixes a latent version of the old "two parakeet-cli processes competing for ANE" race when cancelling during an active warm-up.- A process killed by timeout/cancel is now logged to
/tmp/vi_transcribe.loginstead of silently returning an empty string — hangs of this kind no longer vanish without a trace in the logs.
Stability: crash on macOS 26 (Tahoe) fixed
- Replaced
pynputkeyboard listener withNSEventglobal monitors.pynputcalledTSMGetInputSourcePropertyfrom a background thread; macOS 26 addeddispatch_assert_queueenforcement to this API and crashed HUSH on every monitor disconnect + sleep. NSEvent monitors run on the main RunLoop — no background thread, no TSM call, no crash. - Fixed crash (heap corruption / SIGTRAP) when Alt was tapped rapidly:
_state["stream"]is now cleared immediately on key-release instead of inside the worker thread, preventing two threads from callingrecorder.stop()on the same PortAudio stream simultaneously.
Stability: hotkey never getting stuck
_cancel_all()(Alt+Shift) now clears_processing_lockedimmediately so the next Alt press is never blocked while an LLM call is hanging or timing out.- Ollama request timeout reduced 120 s → 25 s; result is discarded if the session was cancelled while the call was in flight.
recorder.stop()has a 5-second hard timeout onstream.stop()/close()— PortAudio can hang indefinitely on device changes; this previously froze the hotkey until the stream unblocked.recorder.start()failures (device unavailable etc.) are caught and the overlay is hidden cleanly instead of leaving HUSH stuck in "recording" UI.- 800 ms debounce on
_cancel_all()prevents a burst of Alt+Shift presses from firing multiple cancels.
Performance: fast transcription after idle
- Parakeet is now primed with a silent 0.5 s WAV every 30 s to keep the CoreML model hot in Apple Neural Engine cache (TTL ≈ 44 s). Without warmup, the first transcription after any pause longer than 44 s took 60–90 s instead of ~1 s.
transcriber.cancel()uses SIGTERM → 1 s grace → SIGKILL fallback. SIGKILL was invalidating the ANE compiled-model cache; SIGTERM allows CoreML to release it cleanly so the next run is fast. A warm-up is also triggered immediately after every cancel.- Fixed multiple race conditions that caused two
parakeet-cliprocesses to compete for ANE, each slowing the other to 60+ s:- Warmup
Popennow happens inside_proc_locksotranscribe()can never read_warmup_proc = Nonebetween the check and the store. - Periodic warmup skips if a previous warmup or a real transcription is already running.
transcribe()waits up to 90 s for a running warmup to complete (rather than killing it after 1.5 s). When ANE is cold a warmup takes ≈ 60 s; killing it and restarting wastes that work — waiting means the real transcription starts with a hot ANE and finishes in ~1 s.
- Warmup
Minor fixes
- OpenAI-compatible API: use
max_completion_tokensforo1/o3/o4model series (max_tokensreturns HTTP 400 on these models). - Interrupt panel: symmetric 8 px horizontal padding.
Panel grid system — rewritten from scratch
- Reset (
[🔄]) now places all windows as a group centered on screen. The layout pattern is chosen automatically based on available screen space: 3×3 cross for large screens, 3×2 for medium (most MacBooks), 2×2 for compact. - Panels never overlap after reset: positions are computed geometrically before windows are placed, then the entire group is shifted to fit the screen.
- New placement rule: a diagonal cell is only used if at least one face-adjacent cell is already occupied — windows never connect corner-to-corner with empty space in between.
- Live repositioning (
_snap_attached_panels_live) no longer pushes panels into off-screen cells. When a panel drifts off the bottom, it flips to the opposite side rather than growing a 4th horizontal column.
UI consistency
- All buttons unified to
[UPPERCASE]style with square brackets across all three languages (RU/EN/ES). - Action row button colors (
[СЦЕНАРИЙ]/[SCENARIO]) are now theme-aware:C_YELis defined per theme and updates live on theme switch. Previously the yellow was hardcoded and unreadable on light themes.
Provider monitoring
- Scenario buttons turn red when the assigned LLM provider is unreachable. Status is polled every 30 s via background probe threads; the scenario list refreshes automatically on each status change.
Initial public release. See the v1.0 release notes for full details.
MIT © 2026 Alexander Bikmukhametov

