Post

InterNos: On-Device Voice Dictation for macOS

InterNos: On-Device Voice Dictation for macOS

I talk faster than I type. Tools like Wispr Flow figured this out years ago: hold a key, speak, release, and your words appear at the cursor. The catch is that your voice goes to their servers. Every offhand remark, every half-dictated Slack message, every muttered thing near a password manager, all of it leaves your machine.

macOS 26 shipped the SpeechAnalyzer and SpeechTranscriber APIs, which expose the same on-device speech engine that powers system dictation. That removed the only technical excuse for cloud round-trips. So I built InterNos: a menu bar app that does the whole hold-speak-release workflow with zero network calls in the transcription path.

The name is Latin, roughly “between us.” Dictation that stays between you and your Mac.

What it does

Hold Right Option (configurable, toggle mode available), speak, release. The transcript is inserted at the cursor in whatever app has focus. Measured release-to-inserted-text latency runs 50 to 70 milliseconds for typical utterances, fast enough that it feels like the text was already there.

The pipeline is three stages, all local:

  1. Capture. An AVAudioEngine mic tap, converted to the analyzer’s native format (16 kHz mono).
  2. Transcribe. Apple’s SpeechAnalyzer plus SpeechTranscriber, streaming results as you speak.
  3. Insert. Clipboard swap with a synthetic ⌘V, then your original clipboard is restored.

The insert stage checks for macOS Secure Input first. If a password field has focus, InterNos refuses to inject anything, plays the error sound, and leaves the transcript on the clipboard so nothing is lost. A dictation tool that types into password fields is a keylogger with extra steps.

There’s also a small post-processing pass for spoken commands: “hashtag yard” becomes #yard, “at sign” becomes @, and “emoji thumbs up” becomes 👍. The emoji substitution requires the spoken word “emoji” as a prefix, so telling someone “she sent me a smiley face” stays literal text. That prefix rule sounds obvious in hindsight; it wasn’t in the first draft.

Things that bit me

This was my first serious macOS app, and the Apple stack has some sharp edges that produce silence instead of errors.

Audio format mismatches fail silently. The analyzer wants 16 kHz mono Int16. The mic delivers 48 kHz Float32. Feed it the wrong format and you get empty transcription results, not an exception. I burned a chunk of a day on this before writing a spike CLI that validated every stage of the pipeline in isolation.

AVAudioEngine goes stale. Reuse an engine across utterances after stop/removeTap and it silently yields empty transcripts. A fresh engine per utterance fixed it.

Hardened runtime plus microphone needs an entitlement. A non-sandboxed app signed with --options runtime must carry com.apple.security.device.audio-input. Without it, the mic permission prompt fires, but no grant persists and the app never appears in the Microphone privacy pane. Accessibility and Input Monitoring don’t need entitlements, so those worked while mic didn’t, which is a confusing diagnostic signature. This shipped broken in v1.0.0 and got fixed in v1.0.1 within hours of my own first install.

TCC ties grants to bundle ID, path, and code signature. A debug build sharing a bundle ID with the installed release copy breaks Input Monitoring and Accessibility in ways the privacy panes won’t admit: the toggle shows granted, the app can’t see it. Debug builds now use a separate bundle ID and a separate output path. Related: CGRequestListenEventAccess() can fail to create a TCC record at all, leaving the app absent from the Input Monitoring pane with no way for the user to grant anything. The reliable workaround is attempting a throwaway CGEvent.tapCreate when the request reports not-granted, which forces the record to exist.

Styled DMG installers are cursed. The classic Finder AppleScript layout trick fails silently in headless builds, and baking a .DS_Store doesn’t survive because the background image reference is a Finder alias tied to the original volume’s identity. dmgbuild writes the .DS_Store programmatically per build and just works.

Privacy posture

No accounts, no telemetry, no transcript storage. Settings are the only persisted state. The speech model is downloaded once by macOS itself from Apple’s asset CDN and shared system-wide. The only optional network call in the whole app is “Check for Updates,” which hits the GitHub releases API on demand, with an off-by-default toggle for checking at launch. All of this is verifiable with Little Snitch or any packet monitor, which is the point: a privacy claim you can’t verify is marketing.

Requirements and install

macOS 26 (Tahoe) or later on Apple Silicon, English (US) in v1. Grab the DMG from Releases, drag to Applications, and the onboarding walks you through the three permissions it needs (Microphone, Input Monitoring, Accessibility) plus the one-time model download.

Or build from source:

1
2
3
git clone https://github.com/tksunw/InterNos.git
cd InterNos/App
./scripts/make-app.sh release   # → App/build/Internos.app

My wife and son use it daily, which is a higher bar than any test suite. Issues and PRs welcome, especially additions to the emoji table.

This post is licensed under CC BY 4.0 by the author.