Local-first desktop dictation for macOS, Windows, and Linux. Speak into a push-to-talk or hands-free session; local Whisper transcribes; deterministic rules and an optional local LM Studio model clean the text; JiSpr inserts one polished copy into the focused app.
Audio stays on your computer. Transcripts are sent only to your configured local LM Studio server, if enabled. Known cloud AI endpoints are refused.
The product is JiSpr Flow. The command and Python package remain
local-flowandlocal_flow.
The native macOS app (JiSpr) is the primary way to run this. The Python
local-flow engine underneath it is a component, not the finished product — stop
after uv sync and you have the engine only, not the app.
1. Get the actual latest. There are no tagged releases, so "latest" is always
the tip of main:
git clone https://github.com/jayozer/jispr_flow.git # fresh machine
# ...or, in an existing clone:
git checkout main && git pull2. Verify you really have it — a fork or a stale branch looks just like a good clone. All four must be true:
git remote -v # points at github.com/jayozer/jispr_flow
# (if it's your fork, add an `upstream` remote or re-clone)
git branch --show-current # prints: main
git status # says "up to date with 'origin/main'"
ls macos/JiSpr # exists — this directory IS the native appIf macos/JiSpr/ is missing you do not have the app; fix the remote/branch above.
3. Build and launch the app — one command does the whole thing:
./script/bootstrap.shIt checks prerequisites (full Xcode, uv, xcodegen), installs the engine,
generates the Xcode project, then builds and launches JiSpr.app. See
Native macOS app (JiSpr) for the step-by-step version and
the manual follow-up (LM Studio, permissions, model selection).
This runs the dictation engine directly from the command line — no app. For the native menu-bar app (recommended), see New machine? Start here.
Requirements: uv, macOS microphone permissions, and optionally LM Studio with a local instruct model loaded and its server running.
git clone <repository-url>
cd jispr_flow
uv sync --all-extras
uv run local-flow setupFor the recommended accurate local setup, edit
~/.config/local-flow/config.toml or use the native Settings app:
asr_profile = "accuracy"
asr_language = "en"
# Optional local-LLM cleanup. Use rules to run without LM Studio.
polish_backend = "lmstudio"
lmstudio_model = ""
floating_pill = true
pill_style = "compact"
hotkey = "fn"Then verify and run:
uv run local-flow check
uv run local-flow demo
uv run local-flow run --pillHold Fn, speak, and release. Press Esc while recording to discard. The first MLX run downloads the selected model once; later launches use the local cache.
Without a prior uv sync, the equivalent one-shot command is:
uv run --extra mlx-asr --extra audio --extra desktop local-flow run --pillflowchart LR
Idle[Idle bar] -->|hold Fn| Record[Record audio]
Record -->|release Fn| ASR[Local ASR]
ASR --> Rules[Cleanup rules]
Rules --> Polish[LM Studio or rules-only]
Polish --> Personalize[Dictionary + aliases + snippets]
Personalize --> Insert[Paste one copy]
Insert --> Success[Success, then idle]
The compact floating surface stays as a thin idle bar. It expands while
recording, shows processing after release, briefly confirms success, and then
returns to idle. Use --no-pill to hide it or set
LOCAL_FLOW_PILL_STYLE=expanded for the larger labeled version.
LM Studio does not run Whisper. JiSpr runs MLX/faster-whisper directly for speech recognition and uses LM Studio only for optional text cleanup and transforms.
Parakeet v3 is also loaded directly by JiSpr; LM Studio never receives audio. Install its adapter and FFmpeg, then select it as a custom backend:
brew install ffmpeg
uv sync --extra parakeet-asrasr_profile = "custom"
asr_backend = "mlx-parakeet"
asr_model = "mlx-community/parakeet-tdt-0.6b-v3"
asr_language = "auto"Parakeet v3 manages multilingual recognition itself. It does not currently accept JiSpr's Whisper vocabulary prompt; dictionary terms are still supplied to local polish and deterministically enforced on the final text.
| Profile | Model | Best for |
|---|---|---|
accuracy |
MLX Whisper Large-v3-Turbo | recommended accuracy/speed balance on the evaluated Mac |
fast |
MLX Whisper Small.en | lowest latency and memory use |
custom |
configured backend and model | faster-whisper, multilingual, or custom paths |
asr_profile = "fast"
# or
asr_profile = "accuracy"On one 11.3-second technical sample, Turbo reduced WER from 0.190 to
0.048 versus Small.en while median transcription increased from 0.129s
to 0.153s. Results depend on hardware and voice; reproduce them with:
uv run local-flow benchmark-asr sample.wav --profile fast \
--reference "expected words" --json /tmp/fast.json
uv run local-flow benchmark-asr sample.wav --profile accuracy \
--reference "expected words" --json /tmp/accuracy.jsonSee MLX evaluation for the complete methodology.
benchmark-models freezes one ASR transcript per recording, then sends that
byte-identical text through each requested LM Studio model. Raw audio,
transcripts, and review sheets belong under the ignored benchmarks/private/
directory.
uv run local-flow benchmark-models benchmarks/private/corpus.jsonl \
--output benchmarks/private/parakeet-v3 \
--polisher gemma-4-26B-A4B-it-UD-Q4_K_M.gguf \
--polisher Qwen3.5-35B-A3B-Q4_K_M.gguf \
--polisher Qwen3.5-9B-Q4_K_M.ggufComplete the generated blind review sheet before applying --reviews together
with the saved --benchmark-report; JiSpr evaluates those exact saved outputs
without calling ASR or LM Studio again. It never emits a recommendation before
every output has a safety decision. To compare Whisper Turbo with the winner,
rerun the same manifest with
--asr-backend mlx-whisper --asr-model mlx-community/whisper-large-v3-turbo.
See the benchmark guide for the full procedure.
JiSpr uses three safe correction layers:
- dictionary terms bias Whisper before decoding;
- the local polish model is told the canonical spellings;
- dictionary and snippet rules enforce the final output.
The dictionary fixes exact spelling and casing. Use snippets as explicit aliases for pronunciation-dependent ASR results; JiSpr intentionally avoids broad fuzzy autocorrect that could change valid words.
~/.local/share/local-flow/dictionary.json:
{
"terms": ["JiSpr Flow", "PostgreSQL", "Kubernetes"]
}~/.local/share/local-flow/snippets.json:
{
"snippets": {
"juiceflow": "JiSpr Flow",
"GSPR Flow": "JiSpr Flow",
"sig block": "Best regards,\nJay"
}
}You can also say add JiSpr Flow to the dictionary, or review terms mined
from local history:
uv run local-flow learn
uv run local-flow learn --add 1 2| Command | Purpose |
|---|---|
uv run local-flow run --pill |
push-to-talk dictation with the macOS bar |
uv run local-flow run --mode hands-free |
VAD-controlled dictation |
uv run local-flow check |
inspect models, LM Studio, microphone, and desktop setup |
uv run local-flow transcribe memo.m4a --polish |
transcribe an existing audio file |
uv run local-flow history |
list local dictation history |
uv run local-flow history --retry 1 |
re-polish and insert a previous rough transcript |
uv run local-flow recover |
process audio preserved after a crash |
uv run local-flow transform Polish --selection |
rewrite selected text in place |
uv run local-flow pad --window |
open the local Markdown scratchpad |
uv run local-flow stats |
show local words, streaks, and app statistics |
uv run local-flow tray |
start the menu-bar app |
uv run local-flow settings |
open native macOS Settings & Personalization |
uv run local-flow benchmark-models … |
freeze ASR and compare local polishers |
Run uv run local-flow --help or <command> --help for every option.
The native SwiftUI menu-bar app under macos/JiSpr is the recommended way to run
JiSpr on macOS. It drives the same local Python dictation engine behind a
redesigned menu bar and Settings UI, with Launch at Login and recovery if the
engine stops unexpectedly. It lives as a waveform icon in the menu bar (no Dock
icon); closing Settings leaves it running there — use the icon's Quit JiSpr
action to stop the app.
Fast path: from a checkout on the latest
main,./script/bootstrap.shdoes step 1 (engine install) and thexcodegen generate+ build/launch of step 3 in one command. It only checks for full Xcode — it does not run the one-timesudo xcode-select/xcodebuild -license acceptcommands in step 3, so do those first if you haven't. The numbered steps remain the manual equivalent, plus the parts a script can't do (LM Studio, permissions, model selection).
- Apple Silicon Mac, macOS 14 or later
- uv
- Full Xcode (not just the Command Line Tools) — required to build the app
- LM Studio for writing polish (optional — without it, JiSpr falls back to deterministic rules-only cleanup)
uv sync --all-extras # audio + asr + desktop + tray extras
uv run local-flow check # optional: verify mic / ASR / clipboard / LM StudioPlain uv sync installs only the core (httpx). Without --all-extras the
engine cannot capture the microphone or run local speech recognition.
LM Studio downloads models from Hugging Face, which requires authentication —
anonymous downloads return HTTP 403.
- Authenticate with Hugging Face (any one of):
- Add a Hugging Face Read token in LM Studio's settings, or
launchctl setenv HF_TOKEN <token>, then fully quit and reopen LM Studio, oruv run hf auth login
- Download a model, for example:
uv run hf download lmstudio-community/gemma-4-12B-it-MLX-4bit \ --local-dir ~/.cache/lm-studio/models/lmstudio-community/gemma-4-12B-it-MLX-4bit - In LM Studio, load the model and start the local server:
Developer → Start Server (defaults to
http://localhost:1234).
Building the SwiftUI app needs full Xcode. Point the toolchain at it and accept the license (one time):
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunchThen build and run — the Debug build is wired to this repo's .venv engine:
xcodegen generate --spec macos/JiSpr/project.yml # first time / after project.yml changes
./script/build_and_run.sh --verifyOn first launch, grant JiSpr the following in System Settings → Privacy & Security:
- Microphone
- Accessibility
- Input Monitoring (use the Request Access button in JiSpr's General settings)
Restart JiSpr after granting. The global Fn hotkey tap is created at launch, so a running instance will not pick up a new grant — the "Fn hotkey needs permission" banner clears only after you quit and reopen the app, even though the toggles already read Granted.
JiSpr has two independent model stages — don't confuse them:
| Stage | Where | Value |
|---|---|---|
| Speech recognition (voice → text) | Settings → Models → Speech Recognition | Backend mlx-whisper, model e.g. small.en |
| Writing polish (text → clean text) | Settings → Models → Writing Polish | Backend lmstudio, model e.g. gemma-4-12b-it-mlx |
- If a speech model won't fully load under the custom preset, switch Preset → accuracy.
- In Writing Polish, set the backend to
lmstudio, click Refresh, and explicitly select your chat model (e.g.gemma-4-12b-it-mlx). Leaving it blank auto-selects the first loaded model, which can be the wrong one when an embedding model is also loaded in LM Studio.
| Symptom | Cause / fix |
|---|---|
| Model download fails with HTTP 403 | Not a network/proxy issue — Hugging Face requires auth. Add an HF_TOKEN or log in (step 2). |
xcodebuild reports "requires Xcode" or a license error |
Only Command Line Tools are active. Run the xcode-select + -license accept commands (step 3). |
| "Fn hotkey needs permission" despite Granted | Quit and relaunch JiSpr — grants only apply to a freshly launched process (step 4). |
| Polish output looks unchanged (rules-only) | LM Studio server isn't running, or the wrong LM Studio model is selected (steps 2 & 5). |
The Debug build resolves the engine from this checkout's .venv/bin/local-flow.
The Release pipeline bundles that engine and its Python runtime into the app:
./script/package_beta.shWithout a Developer ID certificate this produces an ad-hoc DMG for local
validation only. With Developer ID and a notarytool Keychain profile it creates
the signed, notarized friend beta. See
Beta distribution. The legacy local-flow tray and
local-flow settings commands remain available as fallbacks during beta testing.
Copy .env.example for the complete annotated list. Environment variables override local-flow.example.toml, which overrides application defaults.
| Setting | Values / purpose |
|---|---|
LOCAL_FLOW_ASR_PROFILE |
accuracy, fast, or custom |
LOCAL_FLOW_POLISH_BACKEND |
lmstudio or deterministic rules |
LOCAL_FLOW_LMSTUDIO_MODEL |
loaded local instruct model; empty auto-selects |
LOCAL_FLOW_LMSTUDIO_SYSTEM_PROMPT |
optional extra cleanup instructions |
LOCAL_FLOW_MODE |
push-to-talk or hands-free |
LOCAL_FLOW_HOTKEY |
fn, space, f9, or another supported key |
LOCAL_FLOW_CLEANUP_LEVEL |
none, light, medium, or high |
LOCAL_FLOW_INSERT_METHOD |
auto, paste, type, or clipboard |
LOCAL_FLOW_MIC_PRIORITY |
comma-separated device-name preferences |
LOCAL_FLOW_VAD_PRESET |
normal or whisper for quiet speech |
LOCAL_FLOW_DATA_DIR |
personalization, history, notes, and recovery data |
Personal data defaults to ~/.local/share/local-flow. Do not commit .env,
history, dictionaries, notes, or generated audio.
On macOS, grant the process you launch—your terminal for CLI use, or JiSpr for the native beta—Microphone, Accessibility, and Input Monitoring access under System Settings → Privacy & Security, then restart it. In JiSpr, open General → Permissions and click Request Access first; this registers the native app so it appears in the corresponding macOS privacy list.
Settings writes validated TOML only. Legacy .env settings remain read-only
until migrated with uv run local-flow migrate-config --apply; true
parent-process overrides always remain read-only.
- No bar: run with
--pilland confirmLOCAL_FLOW_FLOATING_PILL=true. - Bar appears but no recording: check Microphone and Input Monitoring access.
- Text reaches the clipboard but not the app: grant Accessibility access or
set
LOCAL_FLOW_INSERT_METHOD=clipboardand paste manually. - LM Studio unavailable: dictation still inserts rules-only text; start its local server when you want model polish.
- Unexpected spelling: add the canonical dictionary term and an exact snippet alias for the observed ASR output.
- Process interrupted after capture: run
uv run local-flow recover. - Wayland: use hands-free mode and clipboard insertion; global hotkeys and synthetic typing are commonly blocked by the compositor.
- Architecture 2.0: complete pipeline, thread model, advanced features, configuration groups, platform behavior, and recovery.
- Adapter overview: short code-oriented architecture.
- MLX evaluation: benchmark harness and results.
- Roadmap: product sequencing and remaining work.
- Contributor guide: repository conventions and validation.
uv run pytest
uv run ruff check .
uv run local-flow demoMIT licensed. JiSpr Flow is not affiliated with or derived from Wispr Flow or any other proprietary dictation product.