← Back to work

aiTTS

Native macOS voice layer for AI coding tools: local narration with word-level captions, push-to-talk dictation, lock-screen controls, and an iPhone companion. Sold as a licensed download.

SwiftmacOSKokoroNodeNext.jsStripeLocal MLiOS

The Challenge

Agentic coding tools produce a lot of text, and reading every response pins you to the screen. The product question was whether a developer could hear what the agent says and speak what it needs, without a cloud round trip, without a watermark in the audio, and without the tool’s own loop noticing anything was there.

What I Built

A native macOS app that adds voice in and voice out to Claude Code, Codex CLI, Gemini CLI, Aider, and Pi, with Cline and OpenCode in beta, re-theming itself to match whichever tool is talking.

Narration. Every new assistant response in an attached session is cleaned of code fences and markup, synthesized locally, and read aloud through a persistent floating caption panel with karaoke-style word highlighting. Transport controls, a chunk counter, and per-chunk scrubbing live in the panel; speed and voice selection live in the menu bar. Synthesis streams, so playback starts on the first chunk of a long reply while later ones are still rendering.

Dictation. Hold a hotkey, speak, release. Transcription runs on-device against a locally bundled model, and the text lands in whatever app has focus, at the cursor. No audio leaves the machine.

Speak anything. A global hotkey and a right-click Services entry read the current selection. With nothing selected it reads the clipboard: text, files dragged from Finder, or images, which go through on-device Vision OCR first and fall back to a model-written description when there is no readable text.

Lock screen and AirPods. The app registers with the system’s Now Playing infrastructure, so narration shows up on the lock screen, in Control Center, and on connected headphones with play, pause, skip and scrub all working from there.

iPhone companion. A paired iOS app discovers the Mac over Bonjour, receives the narration stream, and renders the same voice on the phone’s Neural Engine with lock-screen controls, so a long agent run can be followed from another room.

The Engineering Underneath

The default voice is Kokoro, an 82-million-parameter open model that runs offline at 24kHz with no usage fee and no audible AI disclaimer. A cloud voice is available as an opt-in through the user’s own API key. Audio for a whole turn runs through one in-process queue rather than a new player process per chunk, which is what keeps the caption highlighting continuous and the transitions silent.

Getting onto the lock screen has a gotcha worth knowing: macOS only registers Now Playing for processes with a real bundle identifier, and a bare binary silently no-ops. The build wraps the Swift binary in a proper app bundle for exactly that reason.

Licensing is offline by design. A purchase mints an Ed25519-signed license that the app verifies against an embedded public key. Activation touches the network once and the app runs offline indefinitely afterwards. The 14-day trial is mirrored in both a file and the Keychain so a reinstall does not reset it.

The commercial side is a Next.js app on Vercel: Stripe checkout, a magic-link portal for license and device management, and an auto-update feed served from signed releases so the app updates itself in place.

Technical Highlights

  • Native Swift menu-bar app with a floating caption window, global hotkeys, and system Services integration
  • Local-first speech in both directions: Kokoro for synthesis, on-device transcription for dictation
  • One in-process audio queue per turn for gap-free playback and continuous word timing
  • Ed25519 license verification with one-time activation and a reinstall-proof trial
  • iOS companion over Bonjour pairing and a server-sent event stream, with a graceful fallback to the system voice
  • Distributed as a Claude Code plugin and a signed macOS download with in-app updates
  • Five test surfaces, from Node unit tests through Swift self-tests to a rendered UI acceptance pass that runs on every build