Voice dictation for your Mac. Built at Ship Systems, shared as open source.
A macOS voice dictation app built from FlyOnTheWall's Rust audio and transcription stack. Hold Control, speak, release to insert. Esc cancels. Murmur turns your speech into text, inspired by the dictation workflow of Wispr Flow. It is an independent, open-source Apache-2.0 project.
Developer build with native hold-Control dictation, Deepgram/local Whisper and cloud-to-local fallback, bounded microphone ownership, a nonactivating indicator, and macOS text insertion. Language selection, vocabulary hints and deliberate spoken snippets now have native menu controls and guarded editing forms. It is not feature-complete or release-validated; permissions, IME and app/device compatibility still require the manual QA checklist.
The GitHub issues contain acceptance criteria, dependencies and test-first evidence. See vocabulary editing for the native forms and capability limits. Optional text cleanup now has explicit provider opt-in and Offline-all controls. Real-model cleanup quality/latency, optional history, notarized distribution and additional platforms remain follow-on work. No claim of full Wispr Flow parity is made.
Requires macOS 14.4+, Git, Xcode command-line tools, Rust installed through rustup (the toolchain is pinned to 1.95.0), and either a configured local Whisper engine or your own Deepgram API key. FlyOnTheWall does not need to be installed. This is a source-based developer preview; there is no signed/notarized public release yet.
git clone https://github.com/nolanmak/Murmur.git
cd Murmur
cp .env.example .env
# Edit .env and set DEEPGRAM_API_KEY before starting the app.
cargo test --locked --workspace
cargo clippy --locked --all-targets -- -D warnings
./scripts/build-app.sh
open Murmur.appThe script signs with MURMUR_SIGN_IDENTITY when set, otherwise the local
FlyOnTheWall Dev identity when present (unlocking its dev keychain), so macOS
permissions survive rebuilds. Without either it signs ad-hoc and warns: macOS then
drops Accessibility and Input Monitoring on every rebuild. Set
MURMUR_REQUIRE_IDENTITY=1 to fail instead. The bundle is signed and verified in
target/stage first; a failed signature leaves the installed app unchanged.
It does not install a trust certificate or require a signing account. Start capture from the app bundle,
not a shell process, so macOS assigns permissions to this app.
- On first launch Murmur asks for Microphone, Accessibility and Input Monitoring one at a time. Until all are granted, the menu status names each missing permission and Set up permissions opens the right System Settings pane.
- After granting Input Monitoring, choose Reopen Murmur; macOS applies it only to a new launch. Dictation that could not be delivered (no Microphone, or no Accessibility for local insertion) is refused before recording.
- Keep Wispr Flow on Fn. This app does not bind Fn. Hold Control for 350 ms to begin dictation; a quick tap remains a normal Control key. Modified Control shortcuts pass through normally.
- Click a supported editable field, hold Control and speak, then release. Esc cancels during capture or processing. Pressing another key cancels dictation.
- If insertion is blocked, use Copy Last Transcript (replaces clipboard) from the Control menu and paste manually. After a terminal or browser paste request, wait for the text to appear, then choose Restore clipboard from local paste…. A newer copy is kept.
Same provider and Keychain namespace as FlyOnTheWall, read-only. Resolution order:
- Nonempty
DEEPGRAM_API_KEYin the process environment. - The file explicitly named by
FOTW_ENV_FILE, otherwise.envbeside the app bundle, then.envin the working directory. - FlyOnTheWall's Keychain entry: service
com.flyonthewall.fotw, accountapikey:deepgram.
Use .env.example as the schema for your local .env. If your file is elsewhere,
specify its path with FOTW_ENV_FILE; contents are parsed as data, never executed.
Secrets are never bundled or committed. Finder launches can read .env beside
Murmur.app. Keychain may request permission for the app's signing identity.
Murmur retains its original internal bundle identifier, com.shipsystems.texttospeech,
for macOS permission continuity. Existing TTS_SIGN_IDENTITY configuration remains
supported; MURMUR_SIGN_IDENTITY takes precedence. Ad-hoc rebuilds may still require
permission renewal.
cargo run -- doctor # shows credential source, never the value- Microphone capture starts through Control or the Start dictation menu. No system-audio or screen recording. Clipboard insertion temporarily retains the previous clipboard in memory for restoration.
- Deepgram nova-3, mono 16 kHz PCM, smart formatting and model-improvement opt-out. Audio goes directly to Deepgram under your key while dictating; its API charges apply.
- 120-second session limit; bounded audio queue, capture watchdog and network deadlines.
- Audio/transcripts stay in memory. Nothing is written to a history database or log. Manual Copy Last Transcript changes the clipboard only when selected. Local insertion also uses a temporary clipboard item until explicit restore.
- Focus, field identity, secure-field status and editability are checked before local insertion. Local insertion uses a native paste shortcut for recognized text inputs in any app. Multiline/control-character output requires manual copying; no Enter key is synthesized.
- Changing apps or fields blocks automatic insertion. Cancelling invalidates late results.
- Optional Deepgram-to-local fallback is explicit; Local Whisper never falls back to cloud. LLM cleanup is separately opt-in; Offline-all selects local speech and disables cleanup. No screen context, analytics or vendor relay.
- The microphone adapter uses the default device. Device faults cancel the current dictation; bounded background checks can report when input metadata is stable again. Start a new dictation deliberately. See microphone recovery for ownership limits and the physical-device checks still pending.
- Experimental RustDesk review mode keeps the transcript for explicit review and manual clipboard paste. Automatic remote insertion and the macOS/Linux acceptance matrix are not yet verified.
See architecture, provenance, QA and contributing.
Local macOS paste behavior and recovery are documented in local paste.
Bug reports, documentation improvements, and pull requests are welcome. See CONTRIBUTING.md for the development workflow, or open an issue with your macOS version, reproduction steps, and expected behavior. Use synthetic text and remove private data from logs and screenshots. See SECURITY.md for sensitive reports.
Murmur is licensed under Apache 2.0. Reused FlyOnTheWall components are credited in NOTICE and the provenance record. Murmur is an independent project inspired by Wispr Flow, with no affiliation or claim of feature parity.
Launch the packaged Murmur.app once. It enables Open at login by default,
so Murmur opens when you sign in after restarting macOS. The menu's Open at
login: On/Off item controls this setting; Off persists across manual launches.
Launching an unpackaged development executable does not register a login item.
The app maintains ~/Library/LaunchAgents/com.shipsystems.texttospeech.login.plist
and updates its target when launched from a new location. Keep the installed app
at a permanent path, not in a temporary build/worktree directory. If setup fails,
the menu offers a retry; check write access to ~/Library/LaunchAgents and
~/Library/Application Support/Murmur. Registration does not bypass macOS login
or system background-item restrictions. Quitting Murmur intentionally stops it
for the current session; the next login starts it again if the setting is On.
Native validation and restart acceptance steps: login startup QA.
Two local engines work offline after an explicit model install:
- Local Parakeet (default for new installs): Parakeet Unified 0.6B, English
only, run by the bundled
fotw-speechdhelper with Silero VAD in front. Install its model with Transcription → Install Parakeet model… ormurmur speech install-parakeet, then choose Local Parakeet or runmurmur speech parakeet. See Local Parakeet. - Local Whisper: the managed whisper.cpp engine, for languages other than
English. Choose Local Whisper or run
murmur speech whisper. See setup, privacy and acceptance evidence.
A settings file from before Parakeet keeps Whisper as its local engine until you choose Parakeet.
Local streaming (Nemotron 3.5 through the bundled fotw-speechd helper)
shows words in the indicator while Control is held and types the final text
about 50 ms after release. Run murmur speech install-streaming once, then
choose it in the Transcription menu, run murmur speech streaming, or say
"Murmur, use streaming". It falls back to the default local engine (Local Parakeet on a new install)
when it isn't ready, and never uses the cloud. See local streaming.
murmur speech fallback [parakeet|whisper] selects Deepgram with a local backup
(the default local engine unless named). The Transcription menu lists every
choice: Deepgram, Local Parakeet, Local Whisper, and Deepgram with either as
backup. Existing saved choices are unchanged. Install the local engine first:
local and fallback modes check it is ready before opening the microphone and
never download a model while recording.
Fallback retains the complete utterance (at most 120 seconds) in memory. A
transient cloud failure gets at most one bounded retry; an unavailable cloud or
stalled finalization sends the whole utterance to the local engine after release.
Only one final transcript is returned. Auth/quota errors demote directly; invalid
requests are surfaced instead of hidden. Rate-limit waits beyond the retry budget
use local rather than retrying early. Escape and original-field validation remain
in force. This is not persistent recording/history and never switches local-only
speech to cloud. See docs/local-whisper.md for deadlines and test evidence.
Hold Control and say only a command, starting with "Murmur":
| Say | Next dictation uses |
|---|---|
| "Murmur, use local" | The default local engine (Local Parakeet on a new install) |
| "Murmur, use Parakeet" | Local Parakeet |
| "Murmur, use Whisper" | Local Whisper |
| "Murmur, use streaming" | Local streaming |
| "Murmur, use Deepgram" (or "use cloud") | Deepgram only |
| "Murmur, use backup" | Deepgram with the default local engine as backup |
| "Murmur, undo" | The engine before the last switch |
| "Murmur, which engine" | Nothing changes; shows the current engine |
The status bubble shows the result for a moment, and a command is never typed. Rules:
- Only the whole utterance counts. "Tell him to use local" or "Murmur, use local and send it" is dictated normally.
- Naming Parakeet or Whisper, by voice or in the menu, also makes it the default local engine.
- Switching to local, or to backup, first checks that the local engine is ready. If it isn't, Murmur says why and keeps the current engine.
- With Offline-all on, voice can't switch to a cloud engine; use the menu.
- A switch applies from the next dictation.
- Commands are checked before snippets, so a snippet whose cue starts with "Murmur" can't shadow a command.
Use murmur vocabulary example vocabulary.json to create an editable profile,
then murmur vocabulary import vocabulary.json to apply it to the next utterance.
Recognition hints are sent to Deepgram; local Whisper explicitly reports that
those hints are unavailable. Exact whole-utterance cues such as “insert snippet
sign off” expand locally with either provider. See the versioned schema,
limits and import/export commands. Native vocabulary editing is available; real-model accuracy evaluation remains in progress.
Choose Language in the menu bar, or save it with murmur language es
(English is en). It shares the per-utterance vocabulary snapshot and profile import/export.
Cloud support and local model capabilities are checked before recording. The
managed runtime supports languages advertised by its loaded model. Default
base.en is English-only; other languages require an explicitly imported
multilingual model. Plain external whisper.cpp servers without capabilities
remain English-only from Murmur's point of view. See language behavior.