Open-source AI voice dictation for macOS. Hold a hotkey, speak, release — text appears at your cursor in whatever app is focused.
The product spec lives in specs/README.md. The MVP technical spec lives in specs/spec.md.
- macOS (Apple Silicon or Intel)
- Node.js 20+ —
node -vto check. Install via nvm orbrew install node. - Xcode Command Line Tools —
xcode-select --install. Provides the C/C++ compiler used to buildwhisper.cpp. - CMake —
brew install cmake. Required to buildwhisper.cppon first install (cloud-only mode does not need this). - Homebrew — needed only for the two
brew installlines above.
If you plan to use cloud mode only, CMake is optional — the postinstall step will warn and skip the local build.
freestyle/
├── app/ # all source code, configs, package.json — everything you run
├── specs/ # product + technical specs
├── designs/ # brand and UI design files
├── README.md
├── CONTRIBUTING.md
├── ROADMAP.md
└── LICENSE
All commands below assume you're inside app/.
git clone <this-repo>
cd freestyle/app
npm installWhat npm install does:
- Installs Electron, React, Hono, the OpenAI SDK,
nodejs-whisper, andnode-global-key-listener. - Runs
scripts/fix-keylistener.mjs— sets the+xbit on the bundledMacKeyServerbinary (npm doesn't always preserve it). - Runs
scripts/build-whisper.mjs— compileswhisper.cpponce via CMake intonode_modules/nodejs-whisper/cpp/whisper.cpp/build/bin/whisper-cli. Takes ~30–60s the first time and is skipped on subsequent installs.
The Whisper model itself (ggml-base.en.bin, ~60 MB) is not bundled. It downloads on first local-mode use into ~/Library/Application Support/Freestyle/models/ and is symlinked into the directory nodejs-whisper expects.
npm run devLaunches electron-vite dev: the Hono backend boots on 127.0.0.1:<random-port>, the renderer is served by Vite with HMR, the Electron window opens.
On first launch macOS will prompt for:
- Microphone — granted automatically when you trigger recording.
- Accessibility — required to paste into other apps via synthetic ⌘V. macOS opens System Settings → Privacy & Security → Accessibility; toggle Freestyle (or Electron in dev) on.
npm run build
npm run startbuild outputs to out/ (main, preload, renderer). start runs electron-vite preview, which boots Electron against the bundled production code. Useful for verifying release behavior before packaging.
One footgun: a microphone you pinned in dev is identified by an origin-salted device ID and won't carry over to the prod build's
file://origin. Settings detects this on launch and resets the picker to the default mic.
- Hotkey: the Fn / 🌐 globe key (bottom-left corner of the Mac keyboard). Hold to record, release to transcribe and paste at the cursor of whichever app was focused.
- In the Freestyle window, pick a backend:
- Local — runs
whisper.cppon-device. First use downloads the model. - Cloud — uses OpenAI. Paste your API key in the field (
sk-...). Key is stored encrypted in the macOS Keychain via Electron'ssafeStorage.
- Local — runs
- If using Cloud, pick a model:
gpt-4o-mini-transcribe(default),gpt-4o-transcribe, orwhisper-1. - Microphone picker: pin to the built-in mic so Bluetooth headphones stay in high-quality A2DP mode.
By default macOS uses the Fn / globe key for emoji picker, dictation, or input-source switching, which fires alongside Freestyle. Disable it:
System Settings → Keyboard → "Press 🌐 key to" → Do Nothing
(If you still want emoji on a shortcut, set it to a key combo instead in the same panel.)
| Command | What it does |
|---|---|
npm install |
Installs deps + runs both postinstall scripts (key listener chmod, whisper.cpp build). |
npm run dev |
Dev mode with HMR. |
npm run build |
Production bundle to out/. |
npm run start |
Preview the production bundle in Electron. |
npm run typecheck |
TS check both main + renderer. |
npm run typecheck:node |
TS check Electron main + backend only. |
npm run typecheck:web |
TS check renderer only. |
scripts/fix-keylistener.mjs— sets+xonnode_modules/node-global-key-listener/bin/MacKeyServer. Runs only on macOS; no-op on other platforms or if the file is already executable.scripts/build-whisper.mjs— runscmake -B build && cmake --build build --config Releaseinside the vendoredwhisper.cppsource. No-op ifwhisper-clialready exists. Prints a clear warning if CMake isn't installed rather than failing the whole install.
Both run automatically via the postinstall hook in package.json. To re-run manually:
node scripts/fix-keylistener.mjs
node scripts/build-whisper.mjsapp/frontend/ Electron main process + React renderer
app/backend/ Hono HTTP API, STT adapters (local + OpenAI), settings, secrets
app/shared/ Types shared by both sides
app/scripts/ Postinstall helpers
nodejs-whisperinstall fails or first transcribe returns "Cannot read properties of undefined (reading 'code')" — CMake is not installed. Runbrew install cmake, thennpm installagain so the postinstall buildswhisper.cpp.- Hotkey does nothing — Accessibility permission may not be granted. Open System Settings → Privacy & Security → Accessibility and enable the Freestyle (or Electron) entry.
- Gatekeeper warning about
MacKeyServer— the bundled key-listener helper is unsigned. Click Allow in System Settings → Privacy & Security after the first prompt; the warning won't recur. - "chmod: Operation not permitted" on macOS Sequoia — the postinstall handles this. If you see it anyway, run
node scripts/fix-keylistener.mjsfrom insideapp/. - Cloud mode says "API key not set" — paste the key in the Settings panel and click Save. The field disappears after save and only the last 4 characters are shown.
- Production build records no audio after working in dev — Chromium device IDs are origin-salted; the saved ID from dev doesn't exist on the prod build's
file://origin. Settings auto-clears the stale ID on launch; if it persists, deleteinputDeviceIdfrom~/Library/Application Support/Electron/settings.json.
See CONTRIBUTING.md. The shipping plan is in ROADMAP.md.