idle
the hotkey is armed, nothing is listening
mynah · the bird that repeats what it hears
Press a key and talk. Your words land in whatever window has focus — a commit message, a chat, a form — as you pause, not when you stop. The speech never leaves the machine you are sitting at.
What it is like
There is no window to arrange and no app to switch to. A pill shows what Mynah is doing, the menu bar or tray item carries start, stop and quit, and the rest of the time you are simply typing — with your voice.
the hotkey is armed, nothing is listening
your voice in twelve bands — the only thing on screen
you paused; that utterance is going into the app
The model stays warm for 45 seconds after you finish, so back-to-back dictation is instant. Then it unloads, and Mynah costs nothing until the next time you press the key.
The difference
Most dictation tools wait until you stop talking, hand the whole recording to a model and paste the result. Mynah splits what you say into utterances and types each one the moment you pause — so a long thought arrives in pieces you can watch, and a short one arrives immediately.
You can see the words while you are still speaking, so a misheard phrase is caught in the sentence you are in, not three paragraphs later. And a pause is a natural place to stop: nothing is lost if you change your mind.
mynah set vad=off turns segmentation off: the whole session is transcribed as one block when it ends. Useful for a dictated paragraph you want punctuated as a whole — and slower to appear, by design.
The part nobody sees
Deciding when an utterance ended is the whole game. Too eager and a sentence is cut in half; too patient and the text arrives after you have already moved on. Those thresholds live in tuning/tuning.toml — how long a silence closes an utterance, how much trailing silence to trim, how far above the room speech must sit.
They are pinned by a golden corpus: recordings with known boundaries, in tuning/golden/. Change a threshold and the test suite says which recording moved.
The noise floor is measured at the start of a session, but its contribution is capped at the speech line — a calibrated gate above it would demand speech louder than speech, and no setting could counter that. Frames captured during calibration are kept, not dropped: start talking immediately and that speech is still segmented.
tuning/tuning.toml utterance_silence = 0.8 # silence that closes one trailing_padding = 0.2 # kept, so endings survive noise_calibration_seconds = 1.0 # measured at session start calibration_speech_floor = 0.03 # and capped here tuning/golden/ — what holds it to that quiet_two_utterances.wav ✓ speech_during_calibration.wav ✓ speech_over_noise_in_calibration ✓ gap_below_silence.wav ✓ trailing_silence_trim.wav ✓ noisy_room.wav ✓
Too sensitive in a café, not sensitive enough in a quiet room: mynah set frame_energy=0.02 raises your own floor. The contract is untouched — calibration can lift the gate above it, never below.
Local by definition
Speech is transcribed on your own hardware — mlx-whisper on the Apple GPU, whisper.cpp elsewhere. There is no account, no API key and no telemetry, because there is nothing to send anything to. The only thing Mynah writes outside its own settings file is the text it types, into the window you were already in.
A dictation tool hears everything you say at your desk. That is the one kind of software where "it runs locally" should not be a setting you have to find.
Where it runs
Speech, typing and the indicator are interfaces; a platform is a set of implementations. That is why the Linux port is a handful of providers rather than a rewrite — and why the tuning contract is shared rather than re-derived.
| macOS | Linux · Wayland | |
|---|---|---|
| The engine, segmentation and tuning | one implementation, shared — a platform never re-derives it | |
| Speech, on your own hardware | mlx-whisper, Apple GPU | whisper.cpp |
| Typing into the focused window | Accessibility API | wtype, on wlroots and KDE |
| The hotkey | pynput | your compositor's, bound to mynah toggle |
| Indicator and menu | native pill and menu bar | the shell's, through mynah watch |
| Runs at login | SMAppService | systemd --user |
| Where the face comes from | Mynah.app, no Python at runtime | the desktop’s own shell |
Wayland only on Linux, deliberately: on X11 any client can already read the keyboard and inject keystrokes, so there is nothing to grant and nothing to revoke. The gap is GNOME, which does not implement the virtual-keyboard protocol wtype needs; a portal injector for it is not written yet.
Install
macOS $ pipx install "git+https://github.com/ReidenXerx/mynah.git" $ pipx inject mynah "mynah[macos] @ git+https://github.com/ReidenXerx/mynah.git" Linux $ pipx inject mynah "mynah[linux] @ git+https://github.com/ReidenXerx/mynah.git" $ sudo pacman -S whisper-cpp wtype wl-clipboard $ mynah setup mynah — first-time setup ✓ Runtime extra — sounddevice, pynput, webrtcvad, pyobjc ✗ Accessibility — Not granted yet System Settings → Privacy & Security → Accessibility Add mynah and enable it, then re-run: mynah setup ✓ Microphone — Reachable ✓ Hotkey — Valid (<cmd>+<shift>+.)
The extras are requested from this repository, never as a bare mynah[macos] — that name on PyPI is an unrelated package. The first run installs the speech runtime itself, then names whatever is still missing and what to do about it. The login service is installed last, once the rest passes — never on top of a check that failed.
| hotkey | The global key, pynput syntax — <f8>, <ctrl>+<space> |
| trigger | toggle to press twice, ptt to hold while you talk |
| language | The language you speak |
| vad | Off means the text arrives only when the session ends |
| frame_energy | Your floor for what counts as speech — lower hears more of the room |
| auto_stop_silence | Seconds of silence that end a session by themselves |
| idle_timeout | How long the model stays warm afterwards (0 = never unload) |
mynah config prints all of them with the file they live in; mynah set key=value changes one.