Skip to content

Repository files navigation

murmur

System-wide dictation for macOS. A small Swift speech daemon (native/speechd, Apple Speech / SFSpeechRecognizer) over a local loopback API, spawned and managed by an Electron + React desktop app — one self-contained package, no separate service to run by hand. Speak, and the transcript lands wherever your cursor is.

Electron shell built on an Electron + React + Vite template: renderer and main process talk through a strongly typed window.desktopBridge defined once in packages/contracts.

Layout

apps/
  desktop/       Electron main + preload (tsdown -> dist-electron/*.cjs)
                 spawns/kills native/speechd on startup/quit (speechd-manager.ts)
  web/           Vite + React renderer (also runnable as a standalone web app)
packages/
  contracts/     Shared DesktopBridge interface + IPC channel constants
native/
  speechd/       Swift package — Apple Speech + AVAudioEngine behind a
                 loopback HTTP API (127.0.0.1:8722)

Develop

bun install
bun run native:build   # builds native/speechd once (debug)
bun run dev

This starts Vite on http://localhost:5173 and Electron, which loads the dev server and spawns native/speechd's debug binary as a child process (see apps/desktop/src/speechd-manager.ts). Edit web code and HMR works; edit desktop or Swift code and restart bun run dev to pick it up.

The whole lifecycle runs off Option: double-tap it anywhere to start dictating, tap it once to stop and insert. Holding Option instead dictates for exactly as long as you hold it. Which Option key it listens to — left, right, or either — is configurable in Settings → Shortcuts. The transcript is pasted into whatever app is frontmost. Requires Accessibility (global key hook + paste keystroke), Microphone, and Speech Recognition permissions — macOS prompts for each on first run.

To discard a dictation instead of inserting it, use Discard Dictation in the menu bar item.

The Flow Bar — the floating bar that shows your voice while you talk — rests against the bottom edge of the screen by default. Drag it to the left or right edge if it's covering something; it remembers where you put it (Settings → General → Flow Bar position).

Build

bun run build   # builds web -> apps/web/dist and desktop -> apps/desktop/dist-electron
bun run start   # runs the built Electron app against the built renderer

Package (installer)

bun run package   # builds native/speechd in release mode, then the app + DMG

bun run package builds native/speechd in release first (native:build:release), then the web/desktop bundles, then hands off to electron-builder — the compiled speechd binary is embedded as an extraResource (apps/desktop/electron-builder.config.cjs) so the packaged .app is self-contained. Output lands in apps/desktop/release/.

Stack

  • Renderer: Vite 5, React 19, TanStack Router (file-based routing in apps/web/src/routes/), Tailwind v4 (@tailwindcss/vite).
  • Main: Electron 33, bundled with tsdown to CJS in dist-electron/.
  • Contract: shared DesktopBridge interface in packages/contracts.
  • Packaging: electron-builder; bundles native/speechd's release binary as an extraResource.
  • Speech backend: native/speechd (Swift package), Apple Speech framework over a loopback HTTP API — spawned as a child process by apps/desktop/src/speechd-manager.ts.

Adding an IPC method

  1. Add the method to DesktopBridge and add a channel constant in packages/contracts/src/index.ts.
  2. Expose it in apps/desktop/src/preload.ts (one line forwarding to ipcRenderer.invoke).
  3. Handle it in apps/desktop/src/ipc/handlers.ts with ipcMain.handle(CHANNEL, ...).
  4. Call it from the renderer via window.desktopBridge.yourMethod(...).

The interface is the contract — TypeScript will tell you if any of the four sides drifts.

About

System-wide macOS dictation. Speak, and text lands at your cursor.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages