murmur-youtube README

The project's README from github.com/per-simmons/murmur-youtube. As one markdown file: README.md.

Murmur YouTube

Push-to-talk dictation for macOS. Hold a key, talk, release — cleaned-up text lands in whatever text field has focus. A Wispr Flow-shaped app, built native and fully on-device.

Status: working skeleton. Builds, launches, arms the hotkey, transcribes, injects. Branding and the LLM cleanup tier are the next passes.


Coexisting with another dictation app

This app is built to run alongside other dictation tools without colliding with them, which is not automatic on macOS and is worth understanding before changing anything:

If you run more than one dictation app, give each a different push-to-talk key. Two apps on the same key both record, and whichever injects text will fight the other.


Quick start

make install     # builds, bundles, signs, copies to /Applications, launches

Then grant two permissions — neither is optional, and neither can be requested silently:

Permission Where Needed for
Accessibility System Settings ▸ Privacy & Security ▸ Accessibility The CGEventTap that sees the hotkey, and the AX text insert
Microphone Prompted on first dictation Audio capture

Restart Murmur YouTube after granting Accessibility. Then hold Right ⌥ and talk.

Why grants survive rebuilds here

TCC stores a code-signing requirement per entry, not just a path. An ad-hoc signature changes on every build, so the rebuilt binary stops satisfying the stored requirement — and the symptom is nasty: the Accessibility toggle still shows as on while the app is reported untrusted, and flipping it changes nothing because the stale row is the problem.

The Makefile therefore signs with a stable Developer ID (auto-detected via security find-identity, falling back to ad-hoc). Verified: rebuild + reinstall keeps both grants with no re-prompt.

If a grant ever does get wedged, reset that one row and re-add — never toggle:

tccutil reset Accessibility ai.pivotstudio.murmur-youtube
tccutil reset Microphone   ai.pivotstudio.murmur-youtube

Always pass the bundle ID. A bare tccutil reset Accessibility wipes every app on the machine. Then quit System Settings entirely (⌘Q) before reopening — that pane caches its list and will otherwise show the row you just deleted.

Keep the build out of iCloud. ~/Desktop and ~/Documents are file-provider synced on this machine; the sync engine can materialize/dematerialize files inside an .app and corrupt its signature. make install puts the running copy in /Applications.

Other targets: make app (bundle only), make run (run in place), make clean.


Architecture

 hold key ─► HotkeyMonitor ──► DictationController ◄── Settings
                                │
                     ┌──────────┼──────────┐
                     ▼          ▼          ▼
              AudioCapture  HUDPanel   TranscriptionEngine
                     │                      │
                (AudioChunk) ──ordered──► AppleSpeechEngine
                                            │
                                       (transcript)
                                            ▼
                                      TextFormatter
                                            ▼
                                      TextInjector ─► focused app

Decisions worth knowing

The HUD must never take focus. HUDPanel is a .nonactivatingPanel with canBecomeKey == false. This is the load-bearing detail of the whole app: if the overlay took key status, the user's text field would lose focus and there'd be nothing left to inject into. Everything else is replaceable; this isn't.

The hotkey needs a CGEventTap, not NSEvent. fn and left/right modifier discrimination don't surface through NSEvent.addGlobalMonitorForEvents or the Carbon hotkey API. A session event tap is the only way to see them — which is why Accessibility permission is a hard requirement rather than a nicety.

Audio ordering is explicit. AudioCapture yields into an AsyncStream drained by a single task. Spawning a Task per buffer would be simpler and would silently corrupt the transcript, because unstructured tasks have no ordering guarantee.

Buffers are copied, never borrowed. AVAudioEngine recycles the buffer it hands to a tap the instant the callback returns. AudioChunk's @unchecked Sendable is only sound because AudioCapture always allocates fresh storage before handing off.

Two swappable seams. TranscriptionEngine and TextFormatter are protocols so the two components most likely to change can change without touching anything else.

Layout

Sources/MurmurYouTube/
├── MurmurYouTubeApp.swift              @main, AppDelegate, MenuBarExtra
├── Core/
│   ├── DictationController.swift   state machine, wires everything
│   ├── HotkeyMonitor.swift         CGEventTap on .flagsChanged
│   ├── AudioCapture.swift          AVAudioEngine tap + format conversion + RMS
│   └── TextInjector.swift          AX insert, pasteboard+⌘V fallback
├── Transcription/
│   ├── TranscriptionEngine.swift   protocol + AudioChunk
│   └── AppleSpeechEngine.swift     SpeechAnalyzer / SpeechTranscriber
├── Formatting/
│   └── TextFormatter.swift         protocol + RuleBasedFormatter
├── UI/
│   ├── HUDPanel.swift              non-activating floating panel
│   └── HUDView.swift               waveform + live transcript, Brand palette
└── Support/
    ├── Settings.swift, Permissions.swift, Log.swift

Speech engine

Default is Apple's SpeechAnalyzer / SpeechTranscriber, new in macOS 26: no dependency, no bundled model, no cloud path, real streaming with .volatileResults so text appears while you're still talking. The OS downloads and manages model assets, so the first run for a locale may pause on AssetInstallationRequest.

The intended upgrade is Parakeet v3 via FluidAudio (CoreML on the Neural Engine) — measurably better English WER, ~110× realtime, ~66 MB resident. Implementing TranscriptionEngine is the entire cost of switching; DictationController doesn't change.

Apple SpeechTranscriber Parakeet v3 (FluidAudio) Whisper large-v3 (WhisperKit)
Dependency none SwiftPM SwiftPM
Model download OS-managed ~600 MB ~1.5 GB
English accuracy good best good
Languages many 25 99
Latency low ~80 ms 200–500 ms

Not built yet

  1. LLM cleanup tier. RuleBasedFormatter strips fillers, fixes spacing, capitalizes sentences and adds terminal punctuation — genuinely useful, entirely deterministic. The real win is a second TextFormatter backed by Apple's on-device Foundation Models (macOS 26) for tone, list formatting, and honoring spoken corrections, with Claude as an optional higher-quality tier.
  2. Command Mode. Select text, hold a second hotkey, say "make this more formal." Needs AX read of kAXSelectedTextAttribute plus an LLM round-trip.
  3. Personal dictionary. Names and jargon the ASR keeps missing. SpeechAnalyzer supports this through AnalysisContext / SFCustomLanguageModelData.
  4. Branding. Brand in HUDView.swift is a two-color placeholder gradient. App icon, real palette, HUD motion design, onboarding.
  5. Onboarding. A first-run window that walks through both permissions instead of relying on the menu's "Grant…" items.
  6. Developer ID signing + notarization. Ends the TCC-reset churn and makes the app distributable.

Verified

Driven with a synthetic Right ⌥ hold (scratchpad/ptt/ptt2.swift posts flagsChanged events) and confirmed via /usr/bin/log show --predicate 'subsystem == "ai.pivotstudio.murmur-youtube"':

Not yet verified: speech → transcript → cleanup → injection. Synthetic key events can't produce audio, so this needs a human to hold the key and talk.

log is shadowed in this shell — use /usr/bin/log explicitly or it returns nothing.

murmur-youtube (per-simmons/murmur-youtube)

Searching Database…

这是什么?Murmur 是一个按键说话(push-to-talk)听写应用:按住热键、说话、松开,清理好的转录文本就会输入到当前获得焦点的任何文本框。它原生运行且完全在设备端处理,分别提供 macOS(Swift)和 Windows(C#/Avalonia)构建。
这是一个跨平台应用,还是两个独立的原生应用?它是两套不同的原生代码库——macOS 上是 Swift,Windows 上是 C#/Avalonia——而不是一个共享框架,但构建时遵循相同的行为契约(例如更正词典的工作方式)。
怎么构建和安装它?克隆仓库并运行 make install,它会在 macOS 上构建、代码签名并把应用复制到 /Applications;其他目标还有 make app(仅打包)、make run(就地运行)和 make clean。
它需要哪些权限才能工作?需要两项 macOS 权限且必须手动授予:辅助功能(用于热键事件监听和文本插入)和麦克风(用于音频采集,首次听写时提示)。授予辅助功能权限后需要重启应用。
它已经能日常使用了吗?作者称它是一个“能跑起来的骨架”:它能构建、注册热键、转录并注入文本,但品牌标识、LLM 清理层、命令模式、个人词典和公证尚未实现,而且从语音到转录再到注入的完整路径也还没有经过人工端到端验证。
转录是在设备端进行还是在云端进行?听写默认通过 Apple 的 SpeechAnalyzer/SpeechTranscriber(macOS 26 新增)完全在设备端运行,不捆绑模型、不调用云端;未来计划通过 FluidAudio/CoreML 可选升级到 Parakeet v3,以获得更好的准确性和速度。
它能和 Wispr Flow 之类的其他听写应用共存吗?可以——它使用独特的 bundle ID 和进程名,因此它的辅助功能和麦克风授权与其他工具互不干扰,而且它的按键说话热键(右 Option / fn / 右 Command)可以配置,不会与其他应用的热键冲突。
当前的文本清理实际做了什么?RuleBasedFormatter 以确定性的方式去除填充词、修正空格、将句子首字母大写并添加句末标点。用于语气和口语修正的基于 LLM 的格式化工具(Claude 作为可选的更高质量层)已在计划中,但尚未构建。
为什么必须用 CGEventTap 而不是普通热键 API?区分 fn 键和左/右修饰键这件事无法通过 NSEvent 的全局监视器或 Carbon 热键 API 做到,因此会话级 CGEventTap 是检测它们的唯一方法——这也是辅助功能权限是必需而非可选的原因。
为什么权限授予有时在重新构建后会失效?macOS TCC 会为每个授权存储一条代码签名要求,而临时(ad-hoc)签名每次构建都会变化,导致授权失效。Makefile 在可用时会用稳定的 Developer ID 签名,让辅助功能和麦克风授权在重新构建和重装后依然有效。
What are similar projects?better-voice, parrot, lipflow, rooms, Handy, DigUp, tendedero, FluidVoice
Size
0.8 MB
Stars
219
Updated
CommitAug 22
Age
1mo
Commits · 90 Days