furkanc/parlando

Local voice dictation for macOS on Apple Silicon. Tap a key, speak, and your words are typed into any app — 100% on-device with MLX, nothing leaves your machine.

Python

4

14 commits

updated Sep 10, 2026

See the code

README

parlando — local voice dictation for macOS

Local voice dictation for macOS (Apple Silicon). Tap a key, speak, tap again — your words are typed into whatever window has focus. 100% on-device: audio never leaves your machine.

PyPI Tests License: MIT

Features

  • Fully local & offline — MLX-accelerated Qwen3-ASR on the Apple Neural stack; works on a plane, nothing is uploaded, no API keys.
  • Tap-to-dictate — single tap of right ⌥ Option starts/stops recording (the same pattern superwhisper and Wispr Flow use). No always-on mic.
  • Types anywhere — text is delivered as native keyboard events (Quartz CGEvent unicode), so it works in any app, editor, terminal, or chat box.
  • 30 languages — Turkish by default; switch instantly (e.g. --language English) with no model reload.
  • Voice commands — "nokta", "virgül", "soru işareti", "yeni satır", "gönder" (presses Enter); the English set ("period", "comma", "question mark", "new line", "send") is active with --language English.
  • Cleanup & polish — vocalized fillers ("um", "uh", "eee") are removed automatically; optional --polish runs a small local LLM that drops contextual fillers ("you know", "yani"), stutters and false starts, and fixes punctuation — guarded so it can only edit your words, never answer them.
  • Menu bar app — menu bar icon states: idle, recording, paused (idle · recording · paused) with start/stop, language switcher, start-at-login, and a live status block: model download progress, ready / recording / transcribing, and permission problems with a one-click fix.
  • A real app — parlando --install-app creates Parlando.app Parlando app icon so macOS asks for permissions in Parlando's name (not your terminal's) and keeps them across upgrades.
  • Self-healing — audio watchdog survives sleep/wake and device changes; inference errors never kill the session.
  • Streaming mode (optional) — words appear as you speak, stabilized with LocalAgreement-2 and append-only commits (typed text is never retracted).

Requirements

  • macOS on Apple Silicon
  • uv installed
  • ~2 GB disk for the default ASR model (downloaded once from Hugging Face)

Installation

parlando is on PyPI. With uv (recommended):

uv tool install parlando     # puts the parlando command on your PATH
parlando --install-app       # creates ~/Applications/Parlando.app (see Permissions)

or try it without installing anything permanent:

uvx parlando

or with pipx / pip:

pipx install parlando        # isolated install, commands on PATH
pip install parlando         # into your current Python environment
Other ways: one-line installer · from source · uninstall
# one-liner that also installs uv if you don't have it
curl -fsSL https://raw.githubusercontent.com/furkanc/parlando/main/install.sh | sh

# from source
git clone https://github.com/furkanc/parlando && cd parlando
uv run parlando              # run without installing
uv tool install .            # install commands from the checkout

# uninstall
uv tool uninstall parlando   # (or: pipx uninstall parlando / pip uninstall parlando)

Quick start

parlando                 # menu bar app: icon top right, settings in its menu
parlando --terminal      # ...or dictate from this terminal window

After parlando --install-app, open Parlando from Spotlight or ~/Applications like any other app; permissions are then asked in Parlando's name instead of your terminal's, and stay granted across uv tool upgrade parlando.

  1. On first run the speech model (~2 GB) downloads once; after that everything works offline.
  2. macOS will ask for Microphone and Accessibility permissions (see below) — grant them; parlando picks them up by itself.
  3. Click the window you want to type into → tap right ⌥ Option → speak → tap again. Your words appear at the cursor.

Permissions (one-time)

macOS asks for two permissions and grants them to the responsible app: Parlando when you use Parlando.app, otherwise the terminal you run parlando from (Terminal, iTerm, VS Code, ...). This is why the app bundle is recommended: the permission is asked once, in Parlando's name, and keeps working whichever terminal you use.

PermissionWhyWhere
Microphonehear youSettings → Privacy & Security → Microphone
Accessibilityglobal hotkey and typing keystrokesSettings → Privacy & Security → Accessibility

parlando triggers the system prompts itself, detects missing permissions and says so explicitly (in the terminal, or in a dialog from the menu bar app) instead of failing silently. Without Accessibility the hotkey is not heard at all. Once you grant it, parlando notices within a few seconds and enables the hotkey by itself. macOS sometimes quits the app the moment the switch is flipped; Parlando.app reopens itself in that case. This is a one-time step: the launcher bundle does not change on upgrades, so macOS keeps the permission.

Parlando.app is a thin launcher (Info.plist, icon, a Mach-O stub and a script that runs the installed package) generated on your Mac and ad-hoc signed, so nothing is downloaded and no developer certificate is needed. Its contents do not change on upgrades, so macOS keeps the permissions. Re-run --install-app only after moving the Python environment that owns the package (that resets the permissions once).

Usage

parlando                      # menu bar app (default)
parlando --install-app        # create ~/Applications/Parlando.app (recommended)
parlando --uninstall-app      # remove it (and the login item)
parlando --install-login      # start the menu bar app at login
parlando --terminal           # dictate from the terminal; the options below need it
parlando -t --language English   # dictate in another language
parlando -t --polish             # LLM cleanup (fillers, false starts, punctuation)
parlando -t --enter              # press Enter after each utterance
parlando -t --mode stream        # live word-by-word streaming
parlando --pipe                  # print to stdout (scriptable; implies --terminal)
parlando -t --hotkey cmd_r       # tap right Command instead
parlando --list-devices          # list microphones

Voice commands

Say (English)Say (Turkish)Result
period / commanokta / virgül. , appended to previous word
question marksoru işareti? appended
new line / new paragraphyeni satır / yeni paragrafline break
sendgönderpresses Enter

Disable with --no-commands.

Custom vocabulary

ASR models spell unknown technical terms phonetically ("MLX" can come out as "Meleiks"). Give parlando your vocabulary and it biases recognition toward the exact spellings — injected into the ASR context and honored by --polish too.

Easiest way: menu bar → Vocabulary → Add Term… (active for the very next dictation) or Edit Vocabulary File… for bulk edits. Changes are picked up automatically — no restart. The same file by hand:

mkdir -p ~/.config/parlando
printf "MLX\nPyPI\nClaude Code\n" >> ~/.config/parlando/vocabulary.txt

(one term per line, # comments allowed), or ad hoc:

parlando -t --vocab "MLX, PyPI"

Models

Speech recognition (--model, MLX Qwen3-ASR — 30 languages):

ModelSizeNotes
mlx-community/Qwen3-ASR-1.7B-8bit~2 GBdefault — best accuracy, recommended
mlx-community/Qwen3-ASR-0.6B-8bit~700 MBgood balance for low-RAM machines
mlx-community/Qwen3-ASR-0.6B-4bit~400 MBfastest, lowest accuracy
mlx-community/Qwen3-ASR-0.6B-bf16~1.3 GBhigher fidelity 0.6B, more RAM

Polish LLM (--polish-model, used only with --polish; scores from scripts/eval_polish.py, an 11-case cleanup benchmark):

ModelSizeEvalLatencyNotes
mlx-community/Qwen3-1.7B-4bit~1 GB10/11~0.4 sdefault — fast, safe
mlx-community/Qwen3-4B-4bit~2.3 GB11/11~0.9 sbest quality; use on 16 GB+ Macs
mlx-community/Qwen3-0.6B-4bit~350 MB—~0.2 sminimal RAM, weakest cleanup
parlando -t --polish --polish-model mlx-community/Qwen3-4B-4bit

All models download once from Hugging Face and are cached in ~/.cache/huggingface; everything runs on-device.

The two modes

record (default) — tap, speak freely (pauses are fine), tap again; the whole recording is transcribed once and typed in one go. Recordings longer than 28 s are split at natural silence points (2 min cap). This mode has no VAD guessing, so it is the most robust.

stream — always listening; an energy VAD (hysteresis, adaptive noise floor, optional --silero hybrid) segments utterances and words appear as you speak. More "live", more sensitive to room noise.

Troubleshooting

SymptomFix
"Microphone is silent"Allow Parlando (or your terminal) under Microphone; parlando reopens the mic by itself
Hotkey does nothing / nothing typedGrant Accessibility to Parlando (or your terminal); parlando enables the hotkey within seconds, no restart
Stalls after sleepThe watchdog reopens the stream within ~5 s automatically
Ghost text while silent (stream mode)--energy-floor 0.008 or --silero
Missing soft speech (stream mode)--energy-floor 0.002
Wrong wordsMove closer to the mic; prefer the built-in mic over AirPods (Bluetooth input drops to a low-quality codec)
Detailed trace~/Library/Logs/parlando.log

Note: --device indices shift when devices connect/disconnect, and virtual devices ("Microsoft Teams Audio", "ZoomAudioDevice") are not microphones. Prefer the default device.

Architecture

microphone ── 30 ms frames ──▶ hotkey-gated recorder (record mode)
                              or energy VAD w/ hysteresis + adaptive floor,
                              optional Silero hybrid, 300 ms pre-roll (stream)
                │ segment boundaries
                ▼
        segment audio only ──▶ Qwen3-ASR (MLX, on-device GPU)
                │ readings
                ▼
        LocalAgreement-2 ──▶ append-only word commits ──▶ CGEvent unicode
        (commit on 2-reading    (typed text is never        keyboard events
         agreement)              retracted)

Key decisions (each one earned by a real failure during development):

  • Segment-based ASR, not a rolling window — re-decoding a growing window revises earlier words (visible flicker) and hallucinates on silence.
  • Append-only, word-count-based commits — exact-prefix matching stalls permanently when the ASR reshapes punctuation on committed words.
  • Normalized agreement + stall safety valve — punctuation/case flapping between readings must not freeze the stream.
  • Asymmetric noise-floor learning — a symmetric EMA lets speech scraps poison the floor and the app goes progressively deaf.
  • Every loop iteration guarded — a single ASR exception must not silently kill the pipeline.
  • Chord-aware hotkey — ⌥+Q-style character chords never toggle dictation.

Development

uvx --with numpy pytest tests -q   # unit tests; no model/mic needed
uv run scripts/eval_polish.py      # polish quality benchmark (real local LLM)
uv run scripts/make_icons.py       # regenerate icons, banner, Parlando.icns
scripts/build_launcher.sh          # rebuild the Parlando.app Mach-O stub
scripts/build_app.sh               # experimental: self-contained Parlando.app + .dmg
scripts/make_signing_cert.sh       # experimental: stable signing identity for that build
uv build                           # build the wheel/sdist

Package layout: src/parlando/ (engine, menubar, ASR engine, assets incl. the prebuilt app launcher). Entry point parlando (menu bar app by default, --terminal for the CLI); parlando-menubar is kept as an alias. CI runs the test suite and a packaging build on macOS via GitHub Actions.

scripts/build_app.sh produces a self-contained Parlando.app + .dmg (embedded Python and dependencies, ~250 MB; the speech model still downloads on first run). It is not the supported distribution yet: a downloaded app needs Developer ID signing and notarization to open without a Gatekeeper detour, and a stable signing identity so permissions survive updates (make_signing_cert.sh provides a self-signed one for local experiments). Until then, install with uv.

Roadmap

  • One-line installer (install.sh) with parlando launcher command
  • Proper Python package (uv tool install, entry points)
  • Publish to PyPI (uvx parlando works)
  • Homebrew tap (brew install parlando)
  • End-to-end regression tests with recorded WAV fixtures
  • Custom vocabulary / context biasing (--vocab, vocabulary.txt)

License & credits

MIT. The Qwen3-ASR MLX engine (stt.py) is from dictate.sh by Marc Puig (MIT), with local modifications (partial streaming, energy gating, error hardening). Turn-taking research: Whisper-Streaming / LocalAgreement-2. VAD: Silero VAD.

furkanc/parlando

Local voice dictation for macOS on Apple Silicon. Tap a key, speak, and your words are typed into any app — 100% on-device with MLX, nothing leaves your machine.

Python

4

14 commits

updated Sep 10, 2026

See the code

README

parlando — local voice dictation for macOS

Local voice dictation for macOS (Apple Silicon). Tap a key, speak, tap again — your words are typed into whatever window has focus. 100% on-device: audio never leaves your machine.

PyPI Tests License: MIT

Features

  • Fully local & offline — MLX-accelerated Qwen3-ASR on the Apple Neural stack; works on a plane, nothing is uploaded, no API keys.
  • Tap-to-dictate — single tap of right ⌥ Option starts/stops recording (the same pattern superwhisper and Wispr Flow use). No always-on mic.
  • Types anywhere — text is delivered as native keyboard events (Quartz CGEvent unicode), so it works in any app, editor, terminal, or chat box.
  • 30 languages — Turkish by default; switch instantly (e.g. --language English) with no model reload.
  • Voice commands — "nokta", "virgül", "soru işareti", "yeni satır", "gönder" (presses Enter); the English set ("period", "comma", "question mark", "new line", "send") is active with --language English.
  • Cleanup & polish — vocalized fillers ("um", "uh", "eee") are removed automatically; optional --polish runs a small local LLM that drops contextual fillers ("you know", "yani"), stutters and false starts, and fixes punctuation — guarded so it can only edit your words, never answer them.
  • Menu bar app — menu bar icon states: idle, recording, paused (idle · recording · paused) with start/stop, language switcher, start-at-login, and a live status block: model download progress, ready / recording / transcribing, and permission problems with a one-click fix.
  • A real app — parlando --install-app creates Parlando.app Parlando app icon so macOS asks for permissions in Parlando's name (not your terminal's) and keeps them across upgrades.
  • Self-healing — audio watchdog survives sleep/wake and device changes; inference errors never kill the session.
  • Streaming mode (optional) — words appear as you speak, stabilized with LocalAgreement-2 and append-only commits (typed text is never retracted).

Requirements

  • macOS on Apple Silicon
  • uv installed
  • ~2 GB disk for the default ASR model (downloaded once from Hugging Face)

Installation

parlando is on PyPI. With uv (recommended):

uv tool install parlando     # puts the parlando command on your PATH
parlando --install-app       # creates ~/Applications/Parlando.app (see Permissions)

or try it without installing anything permanent:

uvx parlando

or with pipx / pip:

pipx install parlando        # isolated install, commands on PATH
pip install parlando         # into your current Python environment
Other ways: one-line installer · from source · uninstall
# one-liner that also installs uv if you don't have it
curl -fsSL https://raw.githubusercontent.com/furkanc/parlando/main/install.sh | sh

# from source
git clone https://github.com/furkanc/parlando && cd parlando
uv run parlando              # run without installing
uv tool install .            # install commands from the checkout

# uninstall
uv tool uninstall parlando   # (or: pipx uninstall parlando / pip uninstall parlando)

Quick start

parlando                 # menu bar app: icon top right, settings in its menu
parlando --terminal      # ...or dictate from this terminal window

After parlando --install-app, open Parlando from Spotlight or ~/Applications like any other app; permissions are then asked in Parlando's name instead of your terminal's, and stay granted across uv tool upgrade parlando.

  1. On first run the speech model (~2 GB) downloads once; after that everything works offline.
  2. macOS will ask for Microphone and Accessibility permissions (see below) — grant them; parlando picks them up by itself.
  3. Click the window you want to type into → tap right ⌥ Option → speak → tap again. Your words appear at the cursor.

Permissions (one-time)

macOS asks for two permissions and grants them to the responsible app: Parlando when you use Parlando.app, otherwise the terminal you run parlando from (Terminal, iTerm, VS Code, ...). This is why the app bundle is recommended: the permission is asked once, in Parlando's name, and keeps working whichever terminal you use.

PermissionWhyWhere
Microphonehear youSettings → Privacy & Security → Microphone
Accessibilityglobal hotkey and typing keystrokesSettings → Privacy & Security → Accessibility

parlando triggers the system prompts itself, detects missing permissions and says so explicitly (in the terminal, or in a dialog from the menu bar app) instead of failing silently. Without Accessibility the hotkey is not heard at all. Once you grant it, parlando notices within a few seconds and enables the hotkey by itself. macOS sometimes quits the app the moment the switch is flipped; Parlando.app reopens itself in that case. This is a one-time step: the launcher bundle does not change on upgrades, so macOS keeps the permission.

Parlando.app is a thin launcher (Info.plist, icon, a Mach-O stub and a script that runs the installed package) generated on your Mac and ad-hoc signed, so nothing is downloaded and no developer certificate is needed. Its contents do not change on upgrades, so macOS keeps the permissions. Re-run --install-app only after moving the Python environment that owns the package (that resets the permissions once).

Usage

parlando                      # menu bar app (default)
parlando --install-app        # create ~/Applications/Parlando.app (recommended)
parlando --uninstall-app      # remove it (and the login item)
parlando --install-login      # start the menu bar app at login
parlando --terminal           # dictate from the terminal; the options below need it
parlando -t --language English   # dictate in another language
parlando -t --polish             # LLM cleanup (fillers, false starts, punctuation)
parlando -t --enter              # press Enter after each utterance
parlando -t --mode stream        # live word-by-word streaming
parlando --pipe                  # print to stdout (scriptable; implies --terminal)
parlando -t --hotkey cmd_r       # tap right Command instead
parlando --list-devices          # list microphones

Voice commands

Say (English)Say (Turkish)Result
period / commanokta / virgül. , appended to previous word
question marksoru işareti? appended
new line / new paragraphyeni satır / yeni paragrafline break
sendgönderpresses Enter

Disable with --no-commands.

Custom vocabulary

ASR models spell unknown technical terms phonetically ("MLX" can come out as "Meleiks"). Give parlando your vocabulary and it biases recognition toward the exact spellings — injected into the ASR context and honored by --polish too.

Easiest way: menu bar → Vocabulary → Add Term… (active for the very next dictation) or Edit Vocabulary File… for bulk edits. Changes are picked up automatically — no restart. The same file by hand:

mkdir -p ~/.config/parlando
printf "MLX\nPyPI\nClaude Code\n" >> ~/.config/parlando/vocabulary.txt

(one term per line, # comments allowed), or ad hoc:

parlando -t --vocab "MLX, PyPI"

Models

Speech recognition (--model, MLX Qwen3-ASR — 30 languages):

ModelSizeNotes
mlx-community/Qwen3-ASR-1.7B-8bit~2 GBdefault — best accuracy, recommended
mlx-community/Qwen3-ASR-0.6B-8bit~700 MBgood balance for low-RAM machines
mlx-community/Qwen3-ASR-0.6B-4bit~400 MBfastest, lowest accuracy
mlx-community/Qwen3-ASR-0.6B-bf16~1.3 GBhigher fidelity 0.6B, more RAM

Polish LLM (--polish-model, used only with --polish; scores from scripts/eval_polish.py, an 11-case cleanup benchmark):

ModelSizeEvalLatencyNotes
mlx-community/Qwen3-1.7B-4bit~1 GB10/11~0.4 sdefault — fast, safe
mlx-community/Qwen3-4B-4bit~2.3 GB11/11~0.9 sbest quality; use on 16 GB+ Macs
mlx-community/Qwen3-0.6B-4bit~350 MB—~0.2 sminimal RAM, weakest cleanup
parlando -t --polish --polish-model mlx-community/Qwen3-4B-4bit

All models download once from Hugging Face and are cached in ~/.cache/huggingface; everything runs on-device.

The two modes

record (default) — tap, speak freely (pauses are fine), tap again; the whole recording is transcribed once and typed in one go. Recordings longer than 28 s are split at natural silence points (2 min cap). This mode has no VAD guessing, so it is the most robust.

stream — always listening; an energy VAD (hysteresis, adaptive noise floor, optional --silero hybrid) segments utterances and words appear as you speak. More "live", more sensitive to room noise.

Troubleshooting

SymptomFix
"Microphone is silent"Allow Parlando (or your terminal) under Microphone; parlando reopens the mic by itself
Hotkey does nothing / nothing typedGrant Accessibility to Parlando (or your terminal); parlando enables the hotkey within seconds, no restart
Stalls after sleepThe watchdog reopens the stream within ~5 s automatically
Ghost text while silent (stream mode)--energy-floor 0.008 or --silero
Missing soft speech (stream mode)--energy-floor 0.002
Wrong wordsMove closer to the mic; prefer the built-in mic over AirPods (Bluetooth input drops to a low-quality codec)
Detailed trace~/Library/Logs/parlando.log

Note: --device indices shift when devices connect/disconnect, and virtual devices ("Microsoft Teams Audio", "ZoomAudioDevice") are not microphones. Prefer the default device.

Architecture

microphone ── 30 ms frames ──▶ hotkey-gated recorder (record mode)
                              or energy VAD w/ hysteresis + adaptive floor,
                              optional Silero hybrid, 300 ms pre-roll (stream)
                │ segment boundaries
                ▼
        segment audio only ──▶ Qwen3-ASR (MLX, on-device GPU)
                │ readings
                ▼
        LocalAgreement-2 ──▶ append-only word commits ──▶ CGEvent unicode
        (commit on 2-reading    (typed text is never        keyboard events
         agreement)              retracted)

Key decisions (each one earned by a real failure during development):

  • Segment-based ASR, not a rolling window — re-decoding a growing window revises earlier words (visible flicker) and hallucinates on silence.
  • Append-only, word-count-based commits — exact-prefix matching stalls permanently when the ASR reshapes punctuation on committed words.
  • Normalized agreement + stall safety valve — punctuation/case flapping between readings must not freeze the stream.
  • Asymmetric noise-floor learning — a symmetric EMA lets speech scraps poison the floor and the app goes progressively deaf.
  • Every loop iteration guarded — a single ASR exception must not silently kill the pipeline.
  • Chord-aware hotkey — ⌥+Q-style character chords never toggle dictation.

Development

uvx --with numpy pytest tests -q   # unit tests; no model/mic needed
uv run scripts/eval_polish.py      # polish quality benchmark (real local LLM)
uv run scripts/make_icons.py       # regenerate icons, banner, Parlando.icns
scripts/build_launcher.sh          # rebuild the Parlando.app Mach-O stub
scripts/build_app.sh               # experimental: self-contained Parlando.app + .dmg
scripts/make_signing_cert.sh       # experimental: stable signing identity for that build
uv build                           # build the wheel/sdist

Package layout: src/parlando/ (engine, menubar, ASR engine, assets incl. the prebuilt app launcher). Entry point parlando (menu bar app by default, --terminal for the CLI); parlando-menubar is kept as an alias. CI runs the test suite and a packaging build on macOS via GitHub Actions.

scripts/build_app.sh produces a self-contained Parlando.app + .dmg (embedded Python and dependencies, ~250 MB; the speech model still downloads on first run). It is not the supported distribution yet: a downloaded app needs Developer ID signing and notarization to open without a Gatekeeper detour, and a stable signing identity so permissions survive updates (make_signing_cert.sh provides a self-signed one for local experiments). Until then, install with uv.

Roadmap

  • One-line installer (install.sh) with parlando launcher command
  • Proper Python package (uv tool install, entry points)
  • Publish to PyPI (uvx parlando works)
  • Homebrew tap (brew install parlando)
  • End-to-end regression tests with recorded WAV fixtures
  • Custom vocabulary / context biasing (--vocab, vocabulary.txt)

License & credits

MIT. The Qwen3-ASR MLX engine (stt.py) is from dictate.sh by Marc Puig (MIT), with local modifications (partial streaming, energy gating, error hardening). Turn-taking research: Whisper-Streaming / LocalAgreement-2. VAD: Silero VAD.

Project health

Latest release

0.6.1 · Sep 10, 2026

Releases this year

6