Skip to content
mangerikPublic

About

No description, website, or topics provided.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

VoiceMe

VoiceMe

Push-to-talk dictation for macOS.
Hold a key, speak, release — the text lands in whatever app you're in,
including your terminal.

GPL-3.0 macOS 14+ Swift 6 No dependencies Bahasa Indonesia

Recording and processing waveform
The HUD waveform — live microphone level while recording, travelling pulse while processing.


Contents


What it does

Hold Right Option
      ↓  record microphone → 16 kHz mono WAV, in memory
      ↓  ElevenLabs Scribe  → raw transcript
      ↓  LLM pass           → cleaned up, aware of the destination app
      ↓  clipboard + ⌘V     → lands wherever your cursor is

There is no per-app integration and no plugin for anything. VoiceMe puts the text on the clipboard and synthesises a ⌘V keystroke into the frontmost application. To Gmail, Slack, Notion, Xcode, or your terminal, this is an ordinary paste — which is exactly why it works everywhere without knowing anything about any of them.

Bring your own keys. VoiceMe ships with no credentials, no backend, and no accounts. Nothing is stored on a server, phoned home, or proxied. Your audio goes from your Mac directly to the API you configured, and nowhere else.

No dependencies. Pure Swift against system frameworks. No package manager pulls anything at build time, and there is nothing in the binary you did not compile yourself.

Why it exists

Most dictation tools are tuned for English and fall apart in a terminal. VoiceMe was built around two problems the others handle badly.

Mixed-language speech. Indonesian and English in the same sentence — "saya mau deploy edge function-nya" — stays exactly as spoken. Not translated into one language or the other, technical terms intact. Language detection is left on automatic precisely so code-switching survives.

Terminals and coding agents. A transcript that ends in a newline is an Enter keystroke: your prompt is submitted before you can read it. VoiceMe strips trailing newlines, and tells the cleanup pass to emit a single paragraph with file paths, flags, and identifiers preserved character for character. It sounds like a footnote. It is the difference between usable and infuriating in Claude Code.

Requirements

macOS 14 (Sonoma) or later
Toolchain Xcode command line tools — xcode-select --install. Full Xcode is not required
Hardware Apple Silicon or Intel. Any microphone macOS recognises
Accounts An ElevenLabs API key (required). An LLM key for the cleanup pass (optional)

Installation

git clone https://github.com/mangerik/voiceme.git
cd voiceme
./build.sh
open VoiceMe.app

build.sh compiles with SwiftPM, renders the app icon from code, assembles the .app bundle by hand, and code-signs it. It takes a few seconds.

About code signing

macOS ties Accessibility approval to an app's code signature, not its name or location. Ad-hoc signing produces a new signature on every build, so each rebuild looks like a different app and your permission silently stops applying — the checkbox stays ticked while nothing works.

build.sh therefore signs with the first code-signing certificate in your keychain, which keeps the identity stable across rebuilds. To choose a specific one:

security find-identity -v -p codesigning     # list what you have
VOICEME_SIGN_ID="Apple Development: You (XXXXXXXXXX)" ./build.sh

With no certificate at all it falls back to ad-hoc signing and still works — you will just have to re-grant Accessibility after each build.

Building a DMG to share

./release.sh          # → dist/VoiceMe-0.1.0.dmg

release.sh differs from build.sh in three ways that only matter once the app leaves your machine:

  • Universal binary. It compiles arm64 and x86_64 separately and joins them with lipo, so the app runs on Intel Macs too. (swift build can target both in one pass, but only with full Xcode installed.)
  • Hardened runtime. Required for notarisation. It also cuts off microphone access unless Resources/VoiceMe.entitlements is signed in alongside it — which the script does.
  • Notarisation, when credentials are available.

What it takes to actually distribute it

A DMG built on a machine with no Developer ID is fine for your own testing but macOS will refuse it elsewhere. Two things are needed:

A Developer ID Application certificate. Requires an Apple Developer Program membership ($99/year). An Apple Development certificate — the kind Xcode creates for local work — is only valid on machines registered to its provisioning profile; everywhere else the app is rejected outright.

Notarisation. Apple scans the upload and issues a ticket. Store the credentials once:

xcrun notarytool store-credentials voiceme \
  --apple-id you@example.com --team-id TEAMID --password app-specific-password

Then every release picks them up:

VOICEME_NOTARY_PROFILE=voiceme ./release.sh

The script staples the ticket to the DMG, so the recipient's Mac can verify it without being online.

Without both, whoever you send it to has to right-click → Open, or approve the app under System Settings → Privacy & Security. Recent macOS versions have been tightening that escape hatch.

Getting your API keys

ElevenLabs — required

Speech-to-text. Sign up at elevenlabs.io and create an API key in your account settings.

A restricted key scoped to speech-to-text is enough — VoiceMe never calls anything else. Note that such a key will fail against endpoints like /v1/user, which is why --selftest deliberately exercises the transcription endpoint itself rather than a cheaper one.

Scribe is billed per minute of audio. Check current pricing on their site before heavy use.

LLM for the cleanup pass — optional

The cleanup and learning features call an LLM over the OpenAI Responses API shape. The default configuration points at BytePlus Ark, but arkBaseUrl is a plain setting — any endpoint accepting the same request shape should work.

Leave arkApiKey empty and turn polishEnabled off if you would rather run transcription only. Everything else keeps working.

First run and permissions

VoiceMe needs two macOS permissions and asks for both on first launch.

Permission What it's for If denied
Microphone recording your voice no audio is captured
Accessibility listening for the hotkey globally, and sending ⌘V to other apps the hotkey does nothing at all

Grant Accessibility in System Settings → Privacy & Security → Accessibility by ticking VoiceMe. If the app was already running, quit and reopen it.

Then add your key:

open ~/.config/voiceme/config.json

…or open Pengaturan… (⌘,) from the menu bar and paste it there.

Verify both APIs actually respond before assuming anything is broken:

./VoiceMe.app/Contents/MacOS/VoiceMe --selftest

The log at ~/.config/voiceme/voiceme.log records accessibility trusted: on every launch. That line is the ground truth — a ticked checkbox in System Settings is not.

Everyday use

Hold Right Option, speak, release. That is the whole interface.

While you hold it, a small panel appears with a waveform that follows your voice. On release it switches to a travelling pulse, then the text is pasted where your cursor is.

The menu bar icon (🎤) holds the switches you toggle most:

Item
Aktif master on/off without quitting
Rapikan dengan AI the cleanup pass
Kirim otomatis setelah bicara auto-enter — safe in chat, risky in a terminal
Jalankan saat login start VoiceMe whenever the Mac boots
Pengaturan… (⌘,) full settings window
Buka config.json / Muat ulang config for settings not exposed in the UI
Buka log when something needs diagnosing

Presses shorter than minHoldSeconds (0.35 s by default) are discarded as accidental, so brushing the key costs nothing.

Features in depth

The cleanup pass

Raw speech-to-text gives you every "um" and false start. The cleanup pass sends the transcript to an LLM that removes filler, fixes punctuation and capitalisation, and repairs obviously misheard technical terms — while keeping your language, your meaning, and every name and number.

It also knows which app the text is headed for and adapts: complete sentences for Mail, short and casual for Slack, a precise technical instruction for a terminal. Built-in hints cover common terminals, editors, mail clients, chat apps, and note apps.

Override or extend them with appHints in config.json:

"appHints": {
  "Ghostty": "a terminal running Claude Code. Emit a precise technical instruction. Keep file paths and flags exact.",
  "Slack": "a work chat. Casual but clear, two sentences max.",
  "Mail": "a client-facing email. Formal, complete sentences."
}

Keys match as case-insensitive substrings of the application name.

The cleanup pass can never cost you your words. Every failure — timeout, bad key, malformed response — falls back silently to the raw transcript. A slow API costs you polish, never text.

If all you want is filler removal, scribe_v2 can do it during transcription for free via noVerbatim, with no second API call. Turn that on and polishEnabled off, and the whole pipeline drops to about a second.

Learning mode

Correction panel

Turn it on in Settings → Belajar, choose the language you are learning, and carry on working. Every sentence you dictate is reviewed: the corrected version is what gets pasted, and the panel shows what you got wrong and why.

The point is that you never schedule practice. You learn while writing the email you were going to write anyway.

Explanations come back in Indonesian, since that is the learner's own language — change the why instruction in TutorClient.swift for a different one. Ten target languages ship in TutorClient.languages; adding more is one line.

It cannot assess pronunciation. Scribe returns text, not phonemes. If you mispronounce a word and it is still recognised correctly, VoiceMe has no way to know. Grammar, word choice, and phrasing are what get corrected. Anything claiming otherwise from this stack would be guessing.

Speech-to-text does not sanitise your grammar — this was measured, not assumed. Dictating "This is test for the learning mode" produced exactly that in the raw transcript, missing article included, and the tutor caught it. scribe_v2 is verbatim by default. The panel always shows your original sentence so you can verify this for yourself at any time.

One consequence worth knowing: to make a sentence natural, the tutor may add words you did not say. Good for practice, less so for a contract. Turn learning mode off for those and let the ordinary cleanup pass run — it is far more conservative.

Auto-enter

With Kirim otomatis setelah bicara on, VoiceMe presses Return after pasting. Speak, and the message is sent. Natural in Slack, WhatsApp, or a search field.

Return is sent as a separate keystroke, not as a newline appended to the text. Applications disagree about what a trailing newline means; a keypress always means send. This also lets stripTrailingNewline keep doing its job, so the two cannot combine into a double submit.

In a terminal this removes your last chance to read. The command runs exactly as transcribed, misheard words included. The toggle is in the menu bar as well as in Settings precisely because the right value depends on which app you just switched to.

Custom vocabulary and shortcuts

keyterms — names, product names, technical jargon. Sent to Scribe so it is more likely to hear them correctly, and passed to the cleanup pass as the canonical spelling. Maximum 50 characters and 5 words per entry.

shortcuts — plain find-and-replace applied last, just before pasting. For things a model will never get right on its own:

"shortcuts": {
  "my work email": "me@company.com",
  "the main repo": "github.com/mangerik/voiceme"
}

Both are editable in Settings → Kamus, one per line.

Configuration reference

Settings live in ~/.config/voiceme/config.json, written with mode 0600. Most of it is editable from Pengaturan… (⌘,); the file is there for the rest. See config.example.json for a blank template.

Changes made in the window save as you type. If you edit the file by hand, use Muat ulang config in the menu bar.

Speech to text

Field Default Notes
elevenLabsApiKey "" Required. A speech-to-text-scoped key is enough
sttModel scribe_v2 or scribe_v1, the older model. v2 is better in essentially every way
languageCode null null = auto-detect. Leave it for mixed-language speech
keyterms [] Custom vocabulary. Max 50 chars / 5 words each
noVerbatim false Let Scribe drop fillers itself. scribe_v2 only; forced off in learning mode

Cleanup pass

Field Default Notes
polishEnabled true Turn off for raw transcripts and roughly a second less latency
arkApiKey "" Leave empty to disable the pass entirely
arkModel dola-seed-2-1-turbo-260628
arkBaseUrl BytePlus Ark responses endpoint Any Responses-API-compatible endpoint
arkThinking disabled enabled is marginally better and roughly 5× slower
polishTimeout 10 s On timeout the raw transcript is used instead
appHints {} Per-app instructions, overriding the built-ins

Learning mode

Field Default Notes
learningMode false Replaces the cleanup pass while active
learningLanguage en See TutorClient.languages

Input

Field Default Notes
hotkey rightOption leftOption, rightCommand, rightControl, fn, f13
minHoldSeconds 0.35 Shorter presses are treated as accidental
maxRecordSeconds 120 Safety valve for a stuck key

Output

Field Default Notes
shortcuts {} Replacements applied just before pasting
stripTrailingNewline true Keep this on if you use a terminal
autoEnter false Press Return after pasting
playSounds true Tink on start, Pop on finish
donationURL "" Override the project's support link when forking

Launch at login is deliberately not here. macOS owns that state through SMAppService — the same list you see under System Settings › General › Login Items. Mirroring it into config.json would create a second source of truth that can disagree with the first, and a checkbox that lies. If macOS is holding the registration back (you turned VoiceMe off in Login Items at some point), no amount of code can turn it back on — only you can, from System Settings.

The registration follows wherever VoiceMe.app sat when you enabled it. Move the app and you need to toggle the switch off and on again.

Command-line tools

The binary inside the bundle doubles as a diagnostic tool.

BIN=./VoiceMe.app/Contents/MacOS/VoiceMe
Command What it does
$BIN --selftest Sends a test tone to Scribe and a sample sentence to the cleanup pass. Confirms keys, model IDs, and keyterms encoding are all accepted. Exits non-zero on failure
$BIN --tutor "sentence" Runs the language tutor on text, no microphone needed. The fastest way to iterate on the correction prompt
$BIN --render-lesson out.png Renders the correction panel to an image, so layout can be checked without waiting for it to appear
$BIN --render-waveform out.png Renders the HUD waveform in both states
$BIN --render-icon out.iconset Regenerates the app icon at all ten required sizes

The same self-test is available as a button in Settings → Transkripsi, sharing one code path so the two can never disagree about what passing means.

How it works

Sources/VoiceMe/
  main.swift           entry point, CLI flag dispatch
  AppDelegate.swift    menu bar, permissions, pipeline orchestration
  Config.swift         settings model, lenient decoder, atomic 0600 writes
  SettingsStore.swift  single source of truth at runtime, debounced saves
  SettingsView.swift   SwiftUI settings form
  SettingsWindow.swift the only window allowed to take focus

  HotKeyMonitor.swift  global push-to-talk via NSEvent monitors
  AudioRecorder.swift  AVAudioEngine → 16 kHz mono WAV, plus RMS levels
  WAV.swift            PCM container, shared by recorder and self-test

  ScribeClient.swift   ElevenLabs speech-to-text
  ArkClient.swift      shared HTTP for LLM calls
  PolishClient.swift   cleanup pass, falls back to the raw transcript
  TutorClient.swift    language corrections, structured output

  TextInjector.swift   clipboard save → paste → restore, synthetic keys
  HUD.swift            non-activating status panel
  WaveformView.swift   audio-driven bars
  LessonView.swift     correction panel contents
  IconRenderer.swift   app icon, drawn from code
  Permissions.swift    Accessibility and Microphone
  LaunchAtLogin.swift  SMAppService registration, state owned by macOS
  Log.swift            file logging

Four invariants hold the design together. Break any of them and VoiceMe misbehaves in ways that are hard to trace:

The HUD must never take focus. It is an NSPanel with .nonactivatingPanel, and the app runs as .accessory. If either changes, the frontmost app stops being frontmost and the paste lands in the wrong window.

The cleanup pass must never block output. Every failure path returns the raw transcript.

Trailing newlines are dangerous. In a terminal a newline is Enter. When the app wants to submit, it sends a Return keystroke instead — explicit, and never mistaken for part of your text.

The signature affects permissions. See About code signing.

The clipboard is saved before pasting and restored roughly a third of a second later, so dictating does not destroy whatever you had copied.

The HUD animation

The waveform is drawn in WaveformView.swift, not Lottie — and that is a constraint, not a preference. A Lottie file replays a pre-rendered timeline and has no way to receive microphone amplitude. What makes the HUD feel alive is that each bar's height genuinely comes from the RMS of the audio buffer, mapped to a decibel scale (hearing is logarithmic; a linear mapping makes normal speech nearly invisible) and smoothed asymmetrically — fast attack so it responds, slow decay so it does not flicker between syllables.

The panel is pinned to a dark appearance because the HUD material is always dark, including in light mode, where labelColor would otherwise resolve to black on a dark background.

Latency

Measured on Apple Silicon, from releasing the key to text appearing:

Stage Typical
Scribe transcription 0.8 – 1.7 s
Cleanup pass 0.4 – 1.5 s
Learning-mode review ~2.3 s
Total, dictation ~2 – 4 s
Total, learning mode ~4 s

To go faster:

  • Turn Rapikan dengan AI off → about one second, straight from Scribe. Punctuation is already decent; filler words survive.
  • Or turn noVerbatim on and leave cleanup off → about a second, with fillers removed by Scribe at no extra cost.
  • Keep arkThinking on disabled. Enabling reasoning took the cleanup pass from roughly 3 s to 14 s in testing, for a marginal quality gain.

Privacy and security

  • No backend. VoiceMe has no server. Audio goes from your Mac to the API you configured, and nowhere else.
  • No bundled credentials. Nothing ships with the app; you supply your own keys.
  • No telemetry. Nothing is collected, counted, or reported.
  • Audio is never written to disk. Recording lives in memory and is discarded after transcription.
  • Config lives at ~/.config/voiceme/config.json, mode 0600 — outside the repository, so it cannot be committed by accident.
  • Transcripts are not logged, with one exception: learning mode records what Scribe heard, so you can verify the transcript is faithful to what you actually said. The log is local, at ~/.config/voiceme/voiceme.log, and you can delete it at any time.

Accessibility permission is powerful — it allows synthesising keystrokes into any application. Everything VoiceMe does with it lives in TextInjector.swift, which is under a hundred lines. Read it.

Troubleshooting

tail -f ~/.config/voiceme/voiceme.log
Symptom Cause and fix
Hotkey does nothing Accessibility. Check the accessibility trusted: line at the top of the log — it is authoritative, the checkbox is not
Accessibility keeps re-prompting although it is enabled The binary's signature changed. Use a keychain certificate (see signing), then tccutil reset Accessibility com.socai.voiceme and grant once more
"Tidak ada suara terekam" Microphone permission, or the wrong input device in Sound Settings
Text lands in the wrong window You switched apps while it was still processing. The destination is captured when you press the key, not when the text is ready
Scribe error 401 Wrong key, or a restricted key without speech-to-text scope
Scribe error 422 A keyterms entry exceeds 50 characters or 5 words
Cleanup pass appears to do nothing By design it fails silently to the raw transcript. The log says why
Text pasted but nothing was sent autoEnter is off — that is the default
A message sent before you could read it autoEnter is on. Turn it off from the menu bar
Clipboard contents disappeared Should not happen; the clipboard is saved and restored. Please open an issue with the log

FAQ

Does it work offline? No. Transcription is an API call. A local Whisper backend would be a very welcome contribution — it would also make the whole thing free to run.

Can I use a different speech-to-text provider? Not yet without editing code. ScribeClient.swift is small and self-contained; a pull request generalising it would be welcome.

Can I use a different LLM? Yes, if it accepts the Responses API request shape — set arkBaseUrl, arkModel, and arkApiKey.

Why is the interface in Indonesian? It was built for its author first. The code, the model prompts, and this README are in English; localising the UI would be a straightforward contribution.

Does it support languages other than Indonesian and English? Yes. Scribe handles 90+ languages and detection is automatic by default. Learning mode ships with ten target languages and adding more is one line.

Why does it need Accessibility just to type? macOS treats synthesising keystrokes into another application as a privileged operation, and it is the same permission that allows listening for a hotkey while another app is focused.

Can I bind it to Caps Lock or a mouse button? Not currently. Supported keys are listed under Input; adding more means extending PushToTalkKey.

Contributing

Bug reports, hardware I cannot test on, and prompt improvements are all genuinely useful. See CONTRIBUTING.md — it documents the invariants above and how to verify a change before opening a pull request.

Support

VoiceMe is free software and will stay that way. GPL-3.0 guarantees that for you and for anyone who forks it.

If it saves you time, chip in — entirely optional, and it changes nothing about the software. Bug reports and prompt improvements are worth more than a small donation anyway.

License

GPL-3.0. You may use, modify, and even sell it, but derivative works must remain open source under the same licence.

About

No description, website, or topics provided.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages