Muninn

by bnomei

Not rated
GitHub

About

Have your agent start dictation (OSX only)

Details

Author
bnomei
Categories
Productivity, Other

Setup

Install Muninn in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/bnomei/muninn

Follow the installation instructions in the repository README, then restart your MCP client.

AI-native macOS menu-bar dictation for developer text.

Muninn records speech, transcribes it, runs the transcript through a configurable text pipeline, and injects the final text into the active app. The default pipeline is designed for code-adjacent dictation: commands, flags, package names, file paths, environment variables, acronyms, and other tokens that general-purpose dictation often changes.

- What Muninn does
-
Quick start
-
Install and run
-
Configure Muninn
-
Transcription providers
-
Pipeline model
-
Streaming transcription
-
Contextual profiles and voices
-
External control
-
Privacy, replay, and debugging
-
Development
-
Current limits
-
Source map

hotkey or tray click -> record temporary WAV -> resolve transcription provider route -> transcribe with the first usable provider -> run the refine step -> run optional external filters -> inject final text into the active app

- a macOS menu-bar app with a live tray indicator
- global hotkeys for push-to-talk, done-mode toggle, and cancel
- microphone capture to a temporary WAV, defaulting to 16 kHz mono
- a local-first transcription route across Apple Speech, whisper.cpp, Deepgram, OpenAI, Google, and SpaceXAI (xAI) recorded transcription
- an optional streaming mode for providers that support live transcription in this codebase
- a built-inrefinestep that applies a conservative developer-dictation prompt (OpenAI or SpaceXAI / xAI)
- external Unix filter support for custom pipeline steps
- keyboard-event text injection into the current app
- optional external control throughmuninn://URLs and a localhost MCP server
- optional replay artifacts for debugging utterances

Hotkey changes are parsed from config, but live config reload does not replace active hotkey bindings. Restart Muninn after changing hotkeys.

Use this path when you want to run Muninn from this repository.

- macOS
- Rust 1.88.0 or newer
- Xcode command line tools for local builds
- macOS permissions for Microphone, Accessibility, and Input Monitoring
- Optional cloud provider keys when you use Deepgram, OpenAI, Google, SpaceXAI (xAI), or a cloud-backedrefinestep
- MUNINN_CONFIG
- $XDG_CONFIG_HOME/muninn/config.toml
- ~/.config/muninn/config.toml

If the resolved config file is missing, Muninn creates a launchable default config. To start from the sample config instead:

CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/muninn" mkdir -p "$CONFIG_DIR" cp configs/config.sample.toml "$CONFIG_DIR/config.toml"

Muninn loads./.envfrom the current working directory by default. Existing shell environment variables override.envand config values.

OPENAI_API_KEY=<OPENAI_API_KEY> DEEPGRAM_API_KEY=<DEEPGRAM_API_KEY> GOOGLE_API_KEY=<GOOGLE_API_KEY> GOOGLE_STT_TOKEN=<GOOGLE_STT_TOKEN> XAI_API_KEY=<XAI_API_KEY>

SetMUNINN_LOAD_DOTENV=0,false, ornoto disable.envloading.

Expected result: Muninn appears in the macOS menu bar with anMtray indicator.

Grant these permissions to Muninn itself:
- Focus a text field in another app.
- Click the Muninn tray icon to start recording.
- Speak a short phrase.
- Click the tray icon again to stop recording.

Expected result: Muninn transcribes the utterance, runs the pipeline, and types the final text into the focused app.

If macOS stops showing a permission prompt, reset the affected TCC service and relaunch Muninn:

tccutil reset ListenEvent tccutil reset Accessibility tccutil reset Microphone
cargo install muninn-speech-to-text muninn

The package name ismuninn-speech-to-text; the binary name ismuninn.

MUNINN_CONFIG="$PWD/configs/config.sample.toml" cargo run --release --bin muninn

This is useful for local development because it avoids changing your user config.

The release workflow builds tar archives for:

- aarch64-apple-darwin
- x86_64-apple-darwin

After extracting a release archive, keep the binary at a stable path before granting macOS permissions:

mkdir -p "$HOME/.local/bin" mv muninn "$HOME/.local/bin/muninn" chmod +x "$HOME/.local/bin/muninn" "$HOME/.local/bin/muninn"

macOS permissions attach to the exact app or binary identity. Moving or replacing a raw binary can require granting permissions again.

Use the app bundle when you want a stable app identity,muninn://URL handling, and normal Login Items behavior.

cargo build --release --bin muninn bash scripts/package-macos-app.sh open dist/Muninn.app

The packaging script createsdist/Muninn.app, signs it ad hoc by default, and createsdist/Muninn.app.zipwhendittois available. SetCODESIGN_IDENTITYto use a Developer ID certificate, or setCODESIGN_APP=0to skip signing.
- Movedist/Muninn.appto/Applications/Muninn.app.
- Launch it once and grant permissions toMuninn.
- Add it under System Settings > General > Login Items.
- Keep
[app].autostart = falsewhen using Login Items.

Finder and Login Items do not inherit your shell environment. Store credentials in config or make sure Muninn's working directory contains the.envfile you expect it to read.

Set[app].autostart = trueto let Muninn write a LaunchAgent for the current executable path.

- Muninn writes~/Library/LaunchAgents/com.bnomei.muninn.plistwhen it starts or reloads config.
- Changes take effect on the next macOS login.
- The LaunchAgent includesMUNINN_CONFIG.
- The LaunchAgent does not inherit interactive shell exports.
- When usingMuninn.app, prefer macOS Login Items over this raw-binary LaunchAgent path.

The canonical sample isconfigs/config.sample.toml. The root schema lives insrc/config.rs.

Set a speed above1.0to post-process each completed capture with FFmpeg before a recorded provider reads it. The filter preserves pitch:

The default,1.0, leaves the finalized WAV unchanged. Values from1.0through16.0are accepted; values above 2× are expressed as multiple FFmpegatempostages to avoid the filter's sample-skipping behavior at a single high factor. Muninn needsffmpegonPATHwhen the configured value is above1.0(for example,brew install ffmpeg); if it is unavailable, the tray flashes a red error state and the capture is discarded. Streaming providers still receive live capture frames at normal speed; the post-processed WAV is used for recorded transcription, fallback, and replay after capture finishes.

The default provider route is local-first:

[transcription] providers = ["apple_speech", "whisper_cpp", "deepgram", "openai", "google", "xai"]
[profiles.mail.transcription] providers = ["deepgram", "openai", "google", "xai"]

If you still have explicitstt_*steps inpipeline.steps, Muninn accepts them and infers the route from that order. New configs should prefer[transcription].providers.

- id
- cmd
- optionalargs
- optionalio_mode
- timeout_ms
- on_error

[transcription] providers = ["apple_speech", "whisper_cpp", "deepgram", "openai", "google", "xai"] [[pipeline.steps]] id = "refine" cmd = "refine" timeout_ms = 2500 on_error = "continue" [[pipeline.steps]] id = "uppercase" cmd = "/usr/bin/tr" args = ["[:lower:]", "[:upper:]"] timeout_ms = 250 on_error = "continue"

transcript.system_promptandtranscript.system_prompt_appendsteer the built-inrefinestep. They do not change the speech-to-text provider, and Muninn does not parse appended JSON into provider-native adaptation APIs.

[transcript] system_prompt = "Prefer minimal corrections. Focus on technical terms, developer tools, package names, commands, flags, file names, paths, env vars, acronyms, and obvious dictation errors. If uncertain, keep the original wording." system_prompt_append = """ Vocabulary JSON: {"terms":["Muninn","whisper.cpp","Deepgram","Cargo.toml"],"commands":["cargo test --all-targets","rg --files"],"paths":["src/config.rs",".env"]} """

Therefinestep is not an STT provider. It uses the[refine]config and chat-completions-compatible HTTP by default (provider = "openai"). Setprovider = "xai"to refine with SpaceXAI / xAI (https://api.x.ai/v1/chat/completions, modelgrok-4.5by default). Setting only the provider switches endpoint and model to that vendor’s defaults so an xAI key is never sent to an OpenAI URL.

Stub variables are intended for local smoke checks and tests. They bypass live provider calls for the matching step.

- providers.whisper_cpp.modelunset resolves totiny.en
- tiny.enresolves toggml-tiny.en.bin
- default model directory is~/.local/share/muninn/models
- Muninn auto-downloads known canonical models on first use
- explicit custom model paths must already exist
- device = "auto"uses Metal on supported Apple Silicon builds and CPU otherwise

mkdir -p "$HOME/.local/share/muninn/models" curl -L \ -o "$HOME/.local/share/muninn/models/ggml-tiny.en.bin" \ "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin"

If a local-only route points at a missing custom model, Muninn records amissing_whisper_cpp_modeldiagnostic and injects nothing unless another provider later producestranscript.raw_text.

Muninn passes an envelope through every built-in and external step. Built-in STT steps filltranscript.raw_text; transform steps such asrefinewriteoutput.final_text. Injection prefersoutput.final_textand can fall back totranscript.raw_text.

Run a built-in step directly for smoke checks:

cargo run -q -- __internal_step <stt_apple_speech|stt_whisper_cpp|stt_deepgram|stt_openai|stt_google|stt_xai|refine>

Use the JSON fixtures intests/fixturesfor example input envelopes.

Recorded mode is the default. Enable streaming explicitly:

[transcription] mode = "streaming" providers = ["deepgram", "openai", "xai"] [transcription.streaming] frame_ms = 100 finish_timeout_ms = 10000 fallback_to_recorded_on_error = true

- Deepgram streaming sends mono LINEAR16 audio over WebSocket.
- OpenAI streaming uses Realtime transcription and forces 24 kHz mono capture for that utterance.
- SpaceXAI / xAI streaming useswss://api.x.ai/v1/sttwith BearerXAI_API_KEY, waits fortranscript.created, sends 16 kHz mono PCM frames, and finishes withaudio.done. When xAI is the active streaming provider, Muninn forces 16 kHz mono capture for that utterance.
- Google streaming is not currently callable because the pinnedgoogle-cloud-speech-v21.12.0 dependency exposes request and response types but no callable streaming method.
- Muninn still writes the completed WAV during streaming.
- When streaming fails andfallback_to_recorded_on_error = true, Muninn can run the completed-WAV route.
- A successful streaming transcript seedstranscript.raw_text;refine, scoring, replay, and injection use the same downstream pipeline as recorded mode.
- Interim streaming results are transient. Muninn does not show a partial transcript UI or persist partial transcript history.

Muninn can change refine behavior based on the frontmost app. It captures the bundle id, app name, and a best-effort window title, then applies the first matchingprofile_rulesentry. If no rule matches, behavior falls back to[app].profile; the idle tray glyph falls back toM.
- Start from the base config.
- Apply the matched voice, if the matched profile names one.
- Apply profile overrides last.

Voice means text-shaping behavior plus an optional tray glyph, not an audio voice.

[app] profile = "default" [voices.codex] indicator_glyph = "C" system_prompt = "Prefer terse developer dictation. Keep commands, flags, file names, and code tokens intact." system_prompt_append = """ Vocabulary JSON: {"terms":["Codex","Muninn","Cargo.toml"],"commands":["cargo test --all-targets","cargo clippy --all-targets -- -D warnings"]} """ [voices.terminal] indicator_glyph = "T" system_prompt = "Preserve shell commands exactly. Prefer minimal punctuation changes." [profiles.codex] voice = "codex" [profiles.terminal] voice = "terminal" [[profile_rules]] id = "codex-app" profile = "codex" app_name = "Codex" [[profile_rules]] id = "terminal-app" profile = "terminal" bundle_id = "com.apple.Terminal"

- idle preview shows the glyph for the matched voice, orM
- recording and processing freeze the resolved glyph for that utterance
- ?is reserved for missing-credentials feedback

Muninn can be driven by agents and scripts through two transports:

- muninn://URL scheme, available for the packaged macOS.app
- localhost streamable-HTTP MCP server, disabled by default

Both transports use the same recording-control vocabulary as tray and hotkey events.

[external_control] url_scheme_enabled = true mcp_enabled = false start_recording_enabled = false mcp_bind_address = "127.0.0.1:2769"

External start is disabled by default because it starts microphone capture. Enablingstart_recording_enabled = trueis the local trust decision for configured agents and scripts.

The packaged.appregistersmuninn://throughCFBundleURLTypes.

A binary launched withcargo rundoes not receive these LaunchServices links.

Whenmcp_enabled = true, Muninn serves MCP at:

- get_status
- start_recording
- stop_recording
- cancel_recording

Example registration with an MCP-aware client:

auggie mcp add muninn --transport http --url http://127.0.0.1:2769/mcp

get_statusis read-only and returns JSON like:

{ "state": "idle", "recording_active": false, "busy": false, "permissions": { "microphone": "granted", "accessibility": "granted", "input_monitoring": "granted" } }

stateis one ofidle,recording_active,permission_blocked,already_running, orfailed.

- The MCP server has no authentication.
- mcp_bind_addressmust be an explicit loopback socket address such as127.0.0.1:2769or[::1]:2769.
- Muninn refuses wildcard, LAN, hostname, and other non-loopback binds.
- The MCP server starts only at app launch. Changingmcp_enabledlater requires restarting Muninn.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.