{"id":51429495,"url":"https://github.com/bnomei/muninn","last_synced_at":"2026-07-05T03:02:09.464Z","repository":{"id":342630504,"uuid":"1173641321","full_name":"bnomei/muninn","owner":"bnomei","description":"AI-native macOS menu bar dictation with a post-recording text pipeline.","archived":false,"fork":false,"pushed_at":"2026-06-28T15:22:01.000Z","size":740,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-28T16:23:34.939Z","etag":null,"topics":["ai-agent","apple-speech","automation","bring-your-own-key","deepgram-stt","developer-tools","dictation","google-stt","macos","mcp","menu-bar","openai-stt","pipeline","productivity","prompt-engineering","speech-to-text","streaming","text-transformation","transcription","whisper-cpp"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/muninn-speech-to-text","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bnomei.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"bnomei","buy_me_a_coffee":"bnomei"}},"created_at":"2026-03-05T15:37:30.000Z","updated_at":"2026-06-28T15:22:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bnomei/muninn","commit_stats":null,"previous_names":["bnomei/muninn"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/bnomei/muninn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bnomei%2Fmuninn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bnomei%2Fmuninn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bnomei%2Fmuninn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bnomei%2Fmuninn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bnomei","download_url":"https://codeload.github.com/bnomei/muninn/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bnomei%2Fmuninn/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35141966,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agent","apple-speech","automation","bring-your-own-key","deepgram-stt","developer-tools","dictation","google-stt","macos","mcp","menu-bar","openai-stt","pipeline","productivity","prompt-engineering","speech-to-text","streaming","text-transformation","transcription","whisper-cpp"],"created_at":"2026-07-05T03:02:08.784Z","updated_at":"2026-07-05T03:02:09.449Z","avatar_url":"https://github.com/bnomei.png","language":"Rust","funding_links":["https://github.com/sponsors/bnomei","https://buymeacoffee.com/bnomei","https://www.buymeacoffee.com/bnomei"],"categories":[],"sub_categories":[],"readme":"# muninn\n\n[![Crates.io Version](https://img.shields.io/crates/v/muninn-speech-to-text)](https://crates.io/crates/muninn-speech-to-text)\n[![CI](https://img.shields.io/github/actions/workflow/status/bnomei/muninn/ci.yml?branch=main)](https://github.com/bnomei/muninn/actions/workflows/ci.yml)\n[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\u0026style=flat)](https://codspeed.io/bnomei/muninn?utm_source=badge)\n[![Crates.io Downloads](https://img.shields.io/crates/d/muninn-speech-to-text)](https://crates.io/crates/muninn-speech-to-text)\n[![License](https://img.shields.io/crates/l/muninn-speech-to-text)](https://crates.io/crates/muninn-speech-to-text)\n[![Discord](https://flat.badgen.net/badge/discord/bnomei?color=7289da\u0026icon=discord\u0026label)](https://discordapp.com/users/bnomei)\n[![Buymecoffee](https://flat.badgen.net/badge/icon/donate?icon=buymeacoffee\u0026color=FF813F\u0026label)](https://www.buymeacoffee.com/bnomei)\n\nAI-native macOS menu-bar dictation for developer text.\n\nMuninn 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.\n\n\u003ca title=\"click to open\" target=\"_blank\" style=\"cursor: zoom-in;\" href=\"https://raw.githubusercontent.com/bnomei/muninn/main/screenshot.avif\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/bnomei/muninn/main/screenshot.avif\" alt=\"Muninn menu-bar dictation screenshot\" style=\"width: 100%;\" /\u003e\u003c/a\u003e\n\n## Contents\n\n- [What Muninn does](#what-muninn-does)\n- [Quick start](#quick-start)\n- [Install and run](#install-and-run)\n- [Configure Muninn](#configure-muninn)\n- [Transcription providers](#transcription-providers)\n- [Pipeline model](#pipeline-model)\n- [Streaming transcription](#streaming-transcription)\n- [Contextual profiles and voices](#contextual-profiles-and-voices)\n- [External control](#external-control)\n- [Privacy, replay, and debugging](#privacy-replay-and-debugging)\n- [Development](#development)\n- [Current limits](#current-limits)\n- [Source map](#source-map)\n\n## What Muninn does\n\nDefault recorded-mode flow:\n\n```txt\nhotkey or tray click\n-\u003e record temporary WAV\n-\u003e resolve transcription provider route\n-\u003e transcribe with the first usable provider\n-\u003e run the refine step\n-\u003e run optional external filters\n-\u003e inject final text into the active app\n```\n\nMuninn includes:\n\n- a macOS menu-bar app with a live tray indicator\n- global hotkeys for push-to-talk, done-mode toggle, and cancel\n- microphone capture to a temporary WAV, defaulting to 16 kHz mono\n- a local-first transcription route across Apple Speech, whisper.cpp, Deepgram, OpenAI, and Google recorded transcription\n- an optional streaming mode for providers that support live transcription in this codebase\n- a built-in `refine` step that applies a conservative developer-dictation prompt\n- external Unix filter support for custom pipeline steps\n- keyboard-event text injection into the current app\n- optional external control through `muninn://` URLs and a localhost MCP server\n- optional replay artifacts for debugging utterances\n\nDefault controls:\n\n| Action | Default |\n| --- | --- |\n| Push-to-talk | `ctrl` with `double_tap` trigger and a 300 ms double-tap window |\n| Done-mode toggle | `ctrl` + `shift` + `d` |\n| Cancel active capture | `ctrl` + `shift` + `x` |\n| Tray left click | Toggle: start when idle, stop when recording |\n\nHotkey changes are parsed from config, but live config reload does not replace active hotkey bindings. Restart Muninn after changing hotkeys.\n\n## Quick start\n\nUse this path when you want to run Muninn from this repository.\n\n### Prerequisites\n\n- macOS\n- Rust 1.88.0 or newer\n- Xcode command line tools for local builds\n- macOS permissions for Microphone, Accessibility, and Input Monitoring\n- Optional cloud provider keys when you use Deepgram, OpenAI, Google, or the default OpenAI-backed `refine` step\n\n### 1. Build the binary\n\n```bash\ncargo build --release --bin muninn\n```\n\n### 2. Create a config file\n\nMuninn reads config in this order:\n\n1. `MUNINN_CONFIG`\n2. `$XDG_CONFIG_HOME/muninn/config.toml`\n3. `~/.config/muninn/config.toml`\n\nIf the resolved config file is missing, Muninn creates a launchable default config. To start from the sample config instead:\n\n```bash\nCONFIG_DIR=\"${XDG_CONFIG_HOME:-$HOME/.config}/muninn\"\nmkdir -p \"$CONFIG_DIR\"\ncp configs/config.sample.toml \"$CONFIG_DIR/config.toml\"\n```\n\n### 3. Set credentials when needed\n\nMuninn loads `./.env` from the current working directory by default. Existing shell environment variables override `.env` and config values.\n\nCreate `.env` only with the keys you use:\n\n```dotenv\nOPENAI_API_KEY=\u003cOPENAI_API_KEY\u003e\nDEEPGRAM_API_KEY=\u003cDEEPGRAM_API_KEY\u003e\nGOOGLE_API_KEY=\u003cGOOGLE_API_KEY\u003e\nGOOGLE_STT_TOKEN=\u003cGOOGLE_STT_TOKEN\u003e\n```\n\nSet `MUNINN_LOAD_DOTENV=0`, `false`, or `no` to disable `.env` loading.\n\n### 4. Run the tray app\n\n```bash\ncargo run --release --bin muninn\n```\n\nExpected result: Muninn appears in the macOS menu bar with an `M` tray indicator.\n\n### 5. Grant permissions and verify\n\nGrant these permissions to Muninn itself:\n\n| Permission | Why Muninn needs it | System Settings path |\n| --- | --- | --- |\n| Microphone | Record your speech | Privacy \u0026 Security \u003e Microphone |\n| Accessibility | Inject final text into the active app | Privacy \u0026 Security \u003e Accessibility |\n| Input Monitoring | Listen for global hotkeys while another app is active | Privacy \u0026 Security \u003e Input Monitoring |\n\nTo verify the app:\n\n1. Focus a text field in another app.\n2. Click the Muninn tray icon to start recording.\n3. Speak a short phrase.\n4. Click the tray icon again to stop recording.\n\nExpected result: Muninn transcribes the utterance, runs the pipeline, and types the final text into the focused app.\n\nIf macOS stops showing a permission prompt, reset the affected TCC service and relaunch Muninn:\n\n```bash\ntccutil reset ListenEvent\ntccutil reset Accessibility\ntccutil reset Microphone\n```\n\n## Install and run\n\n### Install from crates.io\n\n```bash\ncargo install muninn-speech-to-text\nmuninn\n```\n\nThe package name is `muninn-speech-to-text`; the binary name is `muninn`.\n\n### Run from the sample config\n\n```bash\nMUNINN_CONFIG=\"$PWD/configs/config.sample.toml\" cargo run --release --bin muninn\n```\n\nThis is useful for local development because it avoids changing your user config.\n\n### Install a release binary\n\nThe release workflow builds tar archives for:\n\n- `aarch64-apple-darwin`\n- `x86_64-apple-darwin`\n\nAfter extracting a release archive, keep the binary at a stable path before granting macOS permissions:\n\n```bash\nmkdir -p \"$HOME/.local/bin\"\nmv muninn \"$HOME/.local/bin/muninn\"\nchmod +x \"$HOME/.local/bin/muninn\"\n\"$HOME/.local/bin/muninn\"\n```\n\nmacOS permissions attach to the exact app or binary identity. Moving or replacing a raw binary can require granting permissions again.\n\n### Build a local `.app` bundle\n\nUse the app bundle when you want a stable app identity, `muninn://` URL handling, and normal Login Items behavior.\n\n```bash\ncargo build --release --bin muninn\nbash scripts/package-macos-app.sh\nopen dist/Muninn.app\n```\n\nThe packaging script creates `dist/Muninn.app`, signs it ad hoc by default, and creates `dist/Muninn.app.zip` when `ditto` is available. Set `CODESIGN_IDENTITY` to use a Developer ID certificate, or set `CODESIGN_APP=0` to skip signing.\n\nRecommended app-bundle setup:\n\n1. Move `dist/Muninn.app` to `/Applications/Muninn.app`.\n2. Launch it once and grant permissions to `Muninn`.\n3. Add it under System Settings \u003e General \u003e Login Items.\n4. Keep `[app].autostart = false` when using Login Items.\n\nFinder and Login Items do not inherit your shell environment. Store credentials in config or make sure Muninn's working directory contains the `.env` file you expect it to read.\n\n### Enable raw-binary autostart\n\nSet `[app].autostart = true` to let Muninn write a LaunchAgent for the current executable path.\n\nBehavior:\n\n- Muninn writes `~/Library/LaunchAgents/com.bnomei.muninn.plist` when it starts or reloads config.\n- Changes take effect on the next macOS login.\n- The LaunchAgent includes `MUNINN_CONFIG`.\n- The LaunchAgent does not inherit interactive shell exports.\n- When using `Muninn.app`, prefer macOS Login Items over this raw-binary LaunchAgent path.\n\n## Configure Muninn\n\nThe canonical sample is [configs/config.sample.toml](configs/config.sample.toml). The root schema lives in [src/config.rs](src/config.rs).\n\n### Important config sections\n\n| Section | Purpose |\n| --- | --- |\n| `[app]` | Default profile, strict step contract, raw-binary autostart |\n| `[hotkeys.*]` | Push-to-talk, done-mode toggle, and cancel bindings |\n| `[indicator]` | Tray indicator visibility and colors |\n| `[recording]` | WAV capture format, default `mono = true` and `sample_rate_khz = 16` |\n| `[transcription]` | Recorded versus streaming mode and ordered provider route |\n| `[pipeline]` | Pipeline deadline, payload format, and post-transcription steps |\n| `[transcript]` | Base prompt and prompt append text for the built-in refine step |\n| `[refine]` | OpenAI-compatible refine endpoint, model, temperature, and guardrails |\n| `[voices.*]` | Named refine behavior and optional one-letter tray glyph |\n| `[profiles.*]` | Context-specific overrides for recording, route, pipeline, transcript, or refine |\n| `[[profile_rules]]` | Ordered matchers for the frontmost app and window title |\n| `[external_control]` | URL scheme and MCP recording-control settings |\n| `[logging]` | Replay artifacts, retention, and debug detail |\n| `[providers.*]` | Provider credentials, endpoints, models, and streaming settings |\n\n### Provider route\n\nThe default provider route is local-first:\n\n```toml\n[transcription]\nproviders = [\"apple_speech\", \"whisper_cpp\", \"deepgram\", \"openai\", \"google\"]\n```\n\nProfiles can override only the route:\n\n```toml\n[profiles.mail.transcription]\nproviders = [\"deepgram\", \"openai\", \"google\"]\n```\n\nIf you still have explicit `stt_*` steps in `pipeline.steps`, Muninn accepts them and infers the route from that order. New configs should prefer `[transcription].providers`.\n\n### Pipeline steps\n\nEach pipeline step has:\n\n- `id`\n- `cmd`\n- optional `args`\n- optional `io_mode`\n- `timeout_ms`\n- `on_error`\n\nSupported `io_mode` values:\n\n| Value | Behavior |\n| --- | --- |\n| `auto` | Built-ins use envelope JSON; external commands default to text filtering |\n| `envelope_json` | Step reads and writes the full JSON envelope |\n| `text_filter` | Step reads transcript text and writes replacement text |\n\nSupported `on_error` values:\n\n| Value | Behavior |\n| --- | --- |\n| `continue` | Keep the previous envelope and run later steps |\n| `fallback_raw` | Substitute `transcript.raw_text` and continue |\n| `abort` | Stop the pipeline and surface the failure |\n\nExample:\n\n```toml\n[transcription]\nproviders = [\"apple_speech\", \"whisper_cpp\", \"deepgram\", \"openai\", \"google\"]\n\n[[pipeline.steps]]\nid = \"refine\"\ncmd = \"refine\"\ntimeout_ms = 2500\non_error = \"continue\"\n\n[[pipeline.steps]]\nid = \"uppercase\"\ncmd = \"/usr/bin/tr\"\nargs = [\"[:lower:]\", \"[:upper:]\"]\ntimeout_ms = 250\non_error = \"continue\"\n```\n\n### Refine prompt hints\n\n`transcript.system_prompt` and `transcript.system_prompt_append` steer the built-in `refine` step. They do not change the speech-to-text provider, and Muninn does not parse appended JSON into provider-native adaptation APIs.\n\n```toml\n[transcript]\nsystem_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.\"\nsystem_prompt_append = \"\"\"\nVocabulary JSON:\n{\"terms\":[\"Muninn\",\"whisper.cpp\",\"Deepgram\",\"Cargo.toml\"],\"commands\":[\"cargo test --all-targets\",\"rg --files\"],\"paths\":[\"src/config.rs\",\".env\"]}\n\"\"\"\n```\n\n## Transcription providers\n\n| Provider | Recorded mode | Streaming mode | Credentials | Notes |\n| --- | --- | --- | --- | --- |\n| Apple Speech | Yes | No | None | Local macOS 26+ provider. Uses Apple-managed Speech assets for the selected locale. |\n| whisper.cpp | Yes | No | None | Local provider. Defaults to `tiny.en`, stored under `~/.local/share/muninn/models`, with `device = \"auto\"`. |\n| Deepgram | Yes | Yes | `DEEPGRAM_API_KEY` or `providers.deepgram.api_key` | Recorded uploads use `/v1/listen`; streaming uses the live WebSocket API. |\n| OpenAI | Yes | Yes | `OPENAI_API_KEY` or `providers.openai.api_key` | Recorded uploads are preflighted against OpenAI's 25 MB audio limit; streaming uses Realtime transcription. |\n| Google | Yes | Not currently callable | `GOOGLE_API_KEY`, `GOOGLE_STT_TOKEN`, or config values | Recorded REST transcription works through the configured endpoint. The Google streaming adapter builds Speech-to-Text v2 requests, but the pinned `google-cloud-speech-v2` 1.12.0 dependency does not expose a callable streaming RPC, so Muninn reports `google_official_client_streaming_rpc_unavailable`. |\n\nThe `refine` step is not an STT provider. It uses the `[refine]` config and OpenAI-compatible chat completions by default.\n\n### Environment variables\n\n| Concern | Variables |\n| --- | --- |\n| Config path | `MUNINN_CONFIG` |\n| `.env` loading | `MUNINN_LOAD_DOTENV` |\n| Deepgram | `DEEPGRAM_API_KEY`, `DEEPGRAM_STT_ENDPOINT`, `DEEPGRAM_STT_MODEL`, `DEEPGRAM_STT_LANGUAGE`, `MUNINN_DEEPGRAM_STUB_TEXT` |\n| OpenAI transcription and refine | `OPENAI_API_KEY`, `MUNINN_OPENAI_STUB_TEXT`, `MUNINN_REFINE_STUB_TEXT` |\n| Google recorded transcription | `GOOGLE_API_KEY`, `GOOGLE_STT_TOKEN`, `GOOGLE_STT_ENDPOINT`, `GOOGLE_STT_MODEL`, `MUNINN_GOOGLE_STUB_TEXT` |\n\nStub variables are intended for local smoke checks and tests. They bypass live provider calls for the matching step.\n\n### whisper.cpp model lifecycle\n\nDefault behavior:\n\n- `providers.whisper_cpp.model` unset resolves to `tiny.en`\n- `tiny.en` resolves to `ggml-tiny.en.bin`\n- default model directory is `~/.local/share/muninn/models`\n- Muninn auto-downloads known canonical models on first use\n- explicit custom model paths must already exist\n- `device = \"auto\"` uses Metal on supported Apple Silicon builds and CPU otherwise\n\nPre-warm the default model cache:\n\n```bash\nmkdir -p \"$HOME/.local/share/muninn/models\"\ncurl -L \\\n  -o \"$HOME/.local/share/muninn/models/ggml-tiny.en.bin\" \\\n  \"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin\"\n```\n\nIf a local-only route points at a missing custom model, Muninn records a `missing_whisper_cpp_model` diagnostic and injects nothing unless another provider later produces `transcript.raw_text`.\n\n## Pipeline model\n\nMuninn passes an envelope through every built-in and external step. Built-in STT steps fill `transcript.raw_text`; transform steps such as `refine` write `output.final_text`. Injection prefers `output.final_text` and can fall back to `transcript.raw_text`.\n\nBuilt-in step commands:\n\n| Command | Purpose |\n| --- | --- |\n| `stt_apple_speech` | Completed-recording Apple Speech transcription |\n| `stt_whisper_cpp` | Completed-recording local whisper.cpp transcription |\n| `stt_deepgram` | Completed-recording Deepgram transcription |\n| `stt_openai` | Completed-recording OpenAI transcription |\n| `stt_google` | Completed-recording Google REST transcription |\n| `refine` | OpenAI-compatible developer-dictation cleanup |\n\nRun a built-in step directly for smoke checks:\n\n```bash\ncargo run -q -- __internal_step \u003cstt_apple_speech|stt_whisper_cpp|stt_deepgram|stt_openai|stt_google|refine\u003e\n```\n\nUse the JSON fixtures in [tests/fixtures](tests/fixtures) for example input envelopes.\n\n## Streaming transcription\n\nRecorded mode is the default. Enable streaming explicitly:\n\n```toml\n[transcription]\nmode = \"streaming\"\nproviders = [\"deepgram\", \"openai\"]\n\n[transcription.streaming]\nframe_ms = 100\nfinish_timeout_ms = 10000\nfallback_to_recorded_on_error = true\n```\n\nStreaming behavior:\n\n- Deepgram streaming sends mono LINEAR16 audio over WebSocket.\n- OpenAI streaming uses Realtime transcription and forces 24 kHz mono capture for that utterance.\n- Google streaming is not currently callable because the pinned `google-cloud-speech-v2` 1.12.0 dependency exposes request and response types but no callable streaming method.\n- Muninn still writes the completed WAV during streaming.\n- When streaming fails and `fallback_to_recorded_on_error = true`, Muninn can run the completed-WAV route.\n- A successful streaming transcript seeds `transcript.raw_text`; `refine`, scoring, replay, and injection use the same downstream pipeline as recorded mode.\n- Interim streaming results are transient. Muninn does not show a partial transcript UI or persist partial transcript history.\n\n## Contextual profiles and voices\n\nMuninn 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 matching `profile_rules` entry. If no rule matches, behavior falls back to `[app].profile`; the idle tray glyph falls back to `M`.\n\nResolution order:\n\n1. Start from the base config.\n2. Apply the matched voice, if the matched profile names one.\n3. Apply profile overrides last.\n\nVoice means text-shaping behavior plus an optional tray glyph, not an audio voice.\n\n```toml\n[app]\nprofile = \"default\"\n\n[voices.codex]\nindicator_glyph = \"C\"\nsystem_prompt = \"Prefer terse developer dictation. Keep commands, flags, file names, and code tokens intact.\"\nsystem_prompt_append = \"\"\"\nVocabulary JSON:\n{\"terms\":[\"Codex\",\"Muninn\",\"Cargo.toml\"],\"commands\":[\"cargo test --all-targets\",\"cargo clippy --all-targets -- -D warnings\"]}\n\"\"\"\n\n[voices.terminal]\nindicator_glyph = \"T\"\nsystem_prompt = \"Preserve shell commands exactly. Prefer minimal punctuation changes.\"\n\n[profiles.codex]\nvoice = \"codex\"\n\n[profiles.terminal]\nvoice = \"terminal\"\n\n[[profile_rules]]\nid = \"codex-app\"\nprofile = \"codex\"\napp_name = \"Codex\"\n\n[[profile_rules]]\nid = \"terminal-app\"\nprofile = \"terminal\"\nbundle_id = \"com.apple.Terminal\"\n```\n\nTray behavior:\n\n- idle preview shows the glyph for the matched voice, or `M`\n- recording and processing freeze the resolved glyph for that utterance\n- `?` is reserved for missing-credentials feedback\n\n## External control\n\nMuninn can be driven by agents and scripts through two transports:\n\n- `muninn://` URL scheme, available for the packaged macOS `.app`\n- localhost streamable-HTTP MCP server, disabled by default\n\nBoth transports use the same recording-control vocabulary as tray and hotkey events.\n\n```toml\n[external_control]\nurl_scheme_enabled = true\nmcp_enabled = false\nstart_recording_enabled = false\nmcp_bind_address = \"127.0.0.1:2769\"\n```\n\nAction semantics:\n\n| Action | Behavior |\n| --- | --- |\n| `start` | Starts recording only when idle and `start_recording_enabled = true` |\n| `stop` | Stops an active recording and runs the pipeline; no-op when idle |\n| `toggle` | Starts when idle and allowed; otherwise stops an active recording |\n| `cancel` | Discards an active recording without transcription or injection |\n\nExternal start is disabled by default because it starts microphone capture. Enabling `start_recording_enabled = true` is the local trust decision for configured agents and scripts.\n\n### URL scheme\n\nThe packaged `.app` registers `muninn://` through `CFBundleURLTypes`.\n\n| URL | Action |\n| --- | --- |\n| `muninn://record`, `muninn://start` | start |\n| `muninn://stop`, `muninn://done` | stop |\n| `muninn://toggle` | toggle |\n| `muninn://cancel`, `muninn://abort` | cancel |\n\n```bash\nopen \"muninn://record\"\n```\n\nA binary launched with `cargo run` does not receive these LaunchServices links.\n\n### MCP server\n\nWhen `mcp_enabled = true`, Muninn serves MCP at:\n\n```txt\nhttp://127.0.0.1:2769/mcp\n```\n\nTools:\n\n- `get_status`\n- `start_recording`\n- `stop_recording`\n- `cancel_recording`\n\nExample registration with an MCP-aware client:\n\n```bash\nauggie mcp add muninn --transport http --url http://127.0.0.1:2769/mcp\n```\n\n`get_status` is read-only and returns JSON like:\n\n```json\n{\n  \"state\": \"idle\",\n  \"recording_active\": false,\n  \"busy\": false,\n  \"permissions\": {\n    \"microphone\": \"granted\",\n    \"accessibility\": \"granted\",\n    \"input_monitoring\": \"granted\"\n  }\n}\n```\n\n`state` is one of `idle`, `recording_active`, `permission_blocked`, `already_running`, or `failed`.\n\nSecurity constraints:\n\n- The MCP server has no authentication.\n- `mcp_bind_address` must be an explicit loopback socket address such as `127.0.0.1:2769` or `[::1]:2769`.\n- Muninn refuses wildcard, LAN, hostname, and other non-loopback binds.\n- The MCP server starts only at app launch. Changing `mcp_enabled` later requires restarting Muninn.\n\n## Privacy, replay, and debugging\n\nTracing logs go to stderr and are controlled with `RUST_LOG`.\n\n```bash\nRUST_LOG=recording=debug cargo run --release --bin muninn\n```\n\nReplay logging is disabled by default. When enabled:\n\n- `replay_detail = \"minimal\"` stores sparse utterance metadata only\n- `replay_detail = \"full_debug\"` stores redacted config, target context, final envelopes, pipeline outcome, refine context, and injection route\n- `replay_retain_audio = true` keeps audio only when `replay_detail = \"full_debug\"`\n- retained audio uses a hard link when possible and falls back to a copy\n- full-debug snapshots redact provider secrets and prompt fields\n- replay artifacts are for inspection, not re-run\n\n```toml\n[logging]\nreplay_enabled = true\nreplay_detail = \"minimal\"\nreplay_retain_audio = false\nreplay_dir = \"~/.local/state/muninn/replay\"\nreplay_retention_days = 7\nreplay_max_bytes = 52428800\n```\n\nCommon recovery checks:\n\n| Symptom | Check |\n| --- | --- |\n| Hotkey does not start recording | Grant Input Monitoring to Muninn and restart after changing hotkey config |\n| Tray click records but hotkey does not | Input Monitoring is missing or the hotkey listener needs restart |\n| Text is not injected | Grant Accessibility to Muninn |\n| No text is injected after a local-only Whisper route | Check for `missing_whisper_cpp_model` and verify the configured model path |\n| External MCP start is rejected | Set `external_control.start_recording_enabled = true` and restart if the MCP server was not enabled at launch |\n| Google streaming falls back or reports unavailable | Use recorded Google transcription, Deepgram streaming, or OpenAI streaming |\n\n## Development\n\nRun the core checks:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --all-targets --all-features -- -D warnings\ncargo test --all-targets\n```\n\nThe repository also includes `prek` hooks:\n\n```bash\nprek validate-config\nprek run --all-files\nprek install\n```\n\nRun the benchmark suite:\n\n```bash\ncargo bench --bench runtime_bottlenecks\n```\n\nFilter to one benchmark group:\n\n```bash\ncargo bench --bench runtime_bottlenecks pipeline_runner\ncargo bench --bench runtime_bottlenecks replay_persist\n```\n\nThe benchmark target focuses on per-utterance latency paths that do not require network calls:\n\n- audio output transform and resampling\n- envelope JSON round trips\n- Google request-body construction\n- profile and voice resolution\n- replacement scoring\n- in-process pipeline runner overhead\n- replay persistence with and without retained audio artifacts\n\n## Current limits\n\n- Muninn's supported runtime is macOS.\n- Apple Speech requires macOS 26+ and Apple-managed Speech assets.\n- whisper.cpp and Apple Speech are completed-recording providers only.\n- Google streaming request construction exists, but live Google streaming is not callable until the pinned official client exposes a streaming RPC.\n- Streaming mode uses provider final text only. There is no partial transcript UI.\n- Replay artifacts are for inspection, not deterministic replay.\n- Provider-backed transcription needs realistic timeout budgets.\n- The external-control MCP server has no authentication, is disabled by default, binds loopback-only, and starts only at app launch.\n- The repository release workflow packages raw binaries; use the local packaging script when you need a `.app` bundle.\n\n## Source map\n\nUse these files when checking README claims against source:\n\n| Area | Source |\n| --- | --- |\n| Package metadata and MSRV | [Cargo.toml](Cargo.toml) |\n| Sample config | [configs/config.sample.toml](configs/config.sample.toml) |\n| Config schema and defaults | [src/config.rs](src/config.rs), [src/config/logging.rs](src/config/logging.rs) |\n| Provider vocabulary and default route | [src/transcription.rs](src/transcription.rs) |\n| Pipeline runner and built-ins | [src/runner.rs](src/runner.rs), [src/internal_tools.rs](src/internal_tools.rs) |\n| Apple Speech provider | [src/stt_apple_speech_tool.rs](src/stt_apple_speech_tool.rs), [src/apple_speech_transcriber.swift](src/apple_speech_transcriber.swift) |\n| whisper.cpp provider | [src/stt_whisper_cpp_tool.rs](src/stt_whisper_cpp_tool.rs) |\n| Deepgram provider | [src/stt_deepgram_tool.rs](src/stt_deepgram_tool.rs), [src/streaming_transcription/deepgram.rs](src/streaming_transcription/deepgram.rs) |\n| OpenAI provider and refine | [src/stt_openai_tool.rs](src/stt_openai_tool.rs), [src/streaming_transcription/openai.rs](src/streaming_transcription/openai.rs), [src/refine.rs](src/refine.rs) |\n| Google provider | [src/stt_google_tool.rs](src/stt_google_tool.rs), [src/streaming_transcription/google.rs](src/streaming_transcription/google.rs) |\n| Hotkeys, tray, and runtime flow | [src/hotkeys.rs](src/hotkeys.rs), [src/runtime_tray.rs](src/runtime_tray.rs), [src/runtime_flow.rs](src/runtime_flow.rs) |\n| macOS permissions | [src/runtime_permissions.rs](src/runtime_permissions.rs), [src/permissions.rs](src/permissions.rs) |\n| External control | [src/external_control.rs](src/external_control.rs), [src/external_control/action.rs](src/external_control/action.rs), [src/external_control/mcp.rs](src/external_control/mcp.rs), [src/external_control/url_scheme.rs](src/external_control/url_scheme.rs) |\n| Packaging and release scripts | [scripts/package-macos-app.sh](scripts/package-macos-app.sh), [scripts/package-release.sh](scripts/package-release.sh), [.github/workflows/release.yml](.github/workflows/release.yml) |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbnomei%2Fmuninn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbnomei%2Fmuninn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbnomei%2Fmuninn/lists"}