Live, 100% local transcription of your Mac's system audio — videos, meetings, calls — to text in real time. No audio ever leaves your machine.
whisper-loopback captures whatever your Mac is playing — not your microphone —
through the free BlackHole loopback device,
transcribes it live with a local Whisper engine tuned for Apple Silicon, and
auto-saves every session as a Markdown transcript. Everything runs on-device: no
cloud, no API keys, no data leaving your Mac.
Use it from the terminal or from a one-click menu bar app.
- Why whisper-loopback
- Features
- How it works
- Requirements
- Quick start
- Usage
- Menu bar app
- Meetings: include your own voice
- macOS audio setup
- Automatic audio routing
- Language auto-detection
- Auto-saved transcripts
- Session summary (local, via Ollama)
- Configuration
- Troubleshooting
- Run locally / Development
- Project layout
- Roadmap & scope
- Contributing
- License
- 🔒 Private by design. Audio is processed entirely on your Mac. There is no cloud service, no account, and no API key — nothing to leak.
- 🖥️ It transcribes what you hear. Captures system output (a lecture, a YouTube video, a Zoom/Teams call), not just the mic — the use cases most tools miss.
- ⚡ Real-time. Text appears as the audio plays, using a streaming strategy on top of the batch Whisper model.
- 🌐 Zero-config language. Detects English or Spanish from the first seconds of speech and pins it for the session — no flag required.
- 💾 Never lose a transcript. Every session auto-saves to a tidy Markdown file.
- 🍎 Fast on Apple Silicon. Default engine is whisper.cpp with Metal acceleration.
- 🖱️ Two front doors. A scriptable CLI and a menu bar app, sharing one engine and one config file.
| 🎧 Loopback capture | Records system output via BlackHole, not the microphone |
| ⚡ Local engines | whisper.cpp + Metal (default) or faster-whisper (CPU fallback) |
| 🌐 Auto language | English & Spanish, auto-detected (--redetect-on-pause for bilingual audio) |
| 📝 Live + files | Live console, plus optional .txt and .srt, always a Markdown transcript |
| 🎙️ Meeting mode | --mic mixes your own voice in; --label-speakers splits you (speaker_1) from the other side (speaker_2) |
| 🔀 Auto routing | Switches system output to your loopback device for the session and restores it |
| 🧾 Local summaries | Optional post-session Markdown summary (--summary) via a local Ollama model |
| 🛡️ Robust shutdown | Ctrl-C and SIGTERM both flush and save; audio dropped under load is reported at session end |
| 🖱️ Menu bar app | Start/stop, settings, and recording state without a terminal |
| ⚙️ Layered config | CLI flags, TOML file, and .env / environment variables |
system audio ─► BlackHole ─► downmix + resample (16 kHz mono) ─► buffer + energy VAD
│
LocalAgreement-2 (commit stable text)
│
console (committed + dim hypothesis) · .txt · .srt · .md
Whisper is a batch model, so "live" transcription re-runs the engine on a growing, VAD-trimmed audio buffer and commits only the text that two consecutive passes agree on (LocalAgreement-2). Committed text is stable and never duplicated; the unstable tail shows as a dim, rewritable hypothesis in the console and is never written to files.
The transcription engine sits behind a single interface (TranscriptionEngine →
Segment), so swapping models touches only the engine/ package.
- macOS on Apple Silicon (M1 or newer).
- Python 3.11+.
- BlackHole 2ch — a free virtual audio
device (
brew install blackhole-2ch). - (Optional) switchaudio-osx for one-click automatic audio routing
(
brew install switchaudio-osx).
# Apple Silicon, Python 3.11+
pip install . # or: uv pip install .Installing pulls in pywhispercpp, which compiles whisper.cpp with Metal. The GGML
model (base by default) is downloaded on first run and cached.
Optional extras:
pip install '.[faster]' # CPU fallback engine (faster-whisper)
pip install '.[menubar]' # the macOS menu bar appmacOS can't record system output directly, so you route it through BlackHole. Follow macOS audio setup once to:
- Install BlackHole (
brew install blackhole-2ch). - Create a Multi-Output Device (your speakers + BlackHole) in Audio MIDI Setup, so you still hear the audio while it's captured.
- (Optional)
brew install switchaudio-osxso the app switches output for you.
⚠️ Nothing captures until this setup is done and system output is pointed at the Multi-Output Device (the app can do that last step automatically — see Automatic audio routing).
whisper-loopback --list-devices # confirm BlackHole shows up
whisper-loopback # auto-detects language; Ctrl-C to stopThat's it — text streams live and the transcript is saved to
~/Documents/whisper-loopback/.
# List input devices (confirm BlackHole is present)
whisper-loopback --list-devices
# Auto-detect the language (en/es) from the audio — the default
whisper-loopback
# Force English (skips detection)
whisper-loopback --language en
# Spanish, writing subtitles and a transcript alongside the console
whisper-loopback --language es --srt talk.srt --txt talk.txt
# Use the CPU fallback engine and a bigger model
whisper-loopback --language en --engine faster-whisper --model small
# Label the session (used in the auto-saved filename)
whisper-loopback --language es --name "curso python"
# Throwaway session: console only, nothing saved
whisper-loopback --language en --no-saveStop with Ctrl-C — or send SIGTERM (e.g. kill), which triggers the same graceful
flush-and-save. If capture had to drop audio while the recognizer fell behind, a one-line
notice at session end reports how much, so you know the transcript may have gaps.
Prefer clicking to typing? An optional macOS menu bar app wraps the same engine — start/stop a session, see the recording state, and get a notification when the transcript is saved, without ever opening a terminal.
pip install '.[menubar]' # or: uv pip install -e '.[menubar]'
whisper-loopback-menubar # a ● appears in the menu barThe menu offers:
- Start / Stop transcription — one click each; the icon shows a running
MM:SSwhile recording. Sessions auto-save exactly like the CLI. - Language / Model / Set session name… — the routine knobs, with the current value
checkmarked. Changes are written to the shared config
(
~/.config/whisper-loopback.toml), so the CLI and app stay in sync. - Capture microphone — a checkable toggle: off by default (transcribe only what's playing), check it to also mix in your voice for a call. Persisted; applies to the next session.
- Label speakers — a checkable toggle for a two-speaker call transcript
(
speaker_1= you,speaker_2= the other side). Checking it also turns on Capture microphone (labeling needs the mic); persisted, applies to the next session. - Output device — pick a specific Multi-Output Device (e.g.
AirPods + BlackHole) instead of auto-detection, handy when you have more than one set up. "Auto-detect" restores the default behavior. The list is captured at launch — restart the app after creating a new Multi-Output Device in Audio MIDI Setup to see it here. - Open transcripts folder — reveals the auto-save folder in Finder.
- Notifications — when the transcript is saved (and, with
summary = truein the shared config, when the summary is ready), plus errors (e.g. BlackHole missing) surfaced as a dialog with the same guidance the CLI prints. Quitting mid-session shows a brief "finishing…" state while the final text is flushed; if a slow summary generation is still running at quit, a notification warns it may be incomplete.
Launch it automatically at login (LaunchAgent)
Create ~/Library/LaunchAgents/com.whisper-loopback.menubar.plist pointing at the
installed whisper-loopback-menubar binary, with RunAtLoad enabled and a PATH
that includes Homebrew (/opt/homebrew/bin) so automatic routing can find
SwitchAudioSource. Then:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.whisper-loopback.menubar.plist
# reload after a code change:
launchctl kickstart -k gui/$(id -u)/com.whisper-loopback.menubarPackaging as a standalone signed
.appis out of scope for now; the menu bar app runs from the installed console script.
By default the tool transcribes system output — the other participants in a call —
but not you. --mic mixes your microphone into the same stream so the transcript
captures both sides:
whisper-loopback --mic # default microphone
whisper-loopback --mic --mic-device 2 # a specific input (see --list-devices)- Off by default. The first
--micrun triggers the macOS microphone permission prompt (System Settings → Privacy & Security → Microphone). - Use headphones. With open speakers the mic also picks up the system audio, so those words can appear twice. Headphones eliminate that echo.
- Software mixing — a second capture stream summed with the loopback stream, no extra Audio MIDI setup. If the mic can't be opened, the session continues loopback-only after a one-line warning.
- Everything stays local; nothing leaves your machine.
For a 1-on-1 call you can get a two-speaker transcript that separates you from the
other side. Add --label-speakers on top of --mic:
whisper-loopback --mic --label-speakersspeaker_1is you (the microphone);speaker_2is the other side (the system-output / loopback audio). Every committed line, subtitle cue, and Markdown paragraph is attributed to its speaker.- No diarization model. Your mic and the system audio already arrive as two separate streams before they'd be mixed — labeling just transcribes each stream on its own lane, so attribution is near-perfect with no extra dependencies.
- Requires
--mic(the menu bar "Label speakers" toggle turns the mic on for you). - Use headphones. Even more important here than with plain
--mic: with open speakers your mic hears the other side, which would leak their words intospeaker_1. Headphones keep each speaker on its own lane. - Graceful fallback. If the mic can't be opened, the session continues as a single unlabeled loopback transcript after a one-line warning.
Without
--label-speakers,--micproduces a single mixed transcript with no speaker labels. Distinguishing individual remote participants (speaker 2 vs speaker 3 within the call audio) is true diarization and remains out of scope.
macOS can't record system output directly, so audio is routed through BlackHole, a free virtual audio device, via a Multi-Output Device — it sends audio to both your normal output (speakers/headphones) and BlackHole at the same time, so you keep hearing everything while it's captured. This one-time setup takes a few minutes.
Install the 2-channel build:
brew install blackhole-2ch(Or download the installer from https://existential.audio/blackhole/.)
Log out and back in if the device does not appear immediately. Verify it is visible:
whisper-loopback --list-devices # look for a "BlackHole 2ch" inputIf you send audio straight to BlackHole, your speakers go silent — the Multi-Output Device is what keeps you hearing the audio while it's captured.
- Open Audio MIDI Setup (Applications → Utilities, or Spotlight search).
- Click the + at the bottom-left → Create Multi-Output Device.
- In the right-hand panel, check both:
- Your normal output (e.g. MacBook Pro Speakers)
- BlackHole 2ch
- Set your normal output as the primary/master device (leftmost checkbox column) and enable Drift Correction on BlackHole 2ch only.
- Rename it to something recognizable, e.g. Speakers + BlackHole.
In System Settings → Sound → Output, select the Multi-Output Device — or let
whisper-loopback switch it for you at session start and restore it at exit
(see Automatic audio routing).
- A single app only: some apps and tools (e.g. Loopback) let you choose an output device per app. Point that app at BlackHole 2ch directly if you only want to transcribe that one source.
- Volume keys: while a Multi-Output Device is selected, the system volume keys may not work — adjust volume on the underlying output device in Audio MIDI Setup. With automatic routing this only lasts for the session.
Want to listen on headphones (wired or Bluetooth, e.g. AirPods) instead of your Mac's speakers? Because the primary device inside a Multi-Output Device can't be changed on the fly, create a separate Multi-Output Device per headphone pair:
- Connect the headphones first.
- Audio MIDI Setup → + → Create Multi-Output Device.
- Check only:
- Your headphones' output entry (Bluetooth devices often list a separate input-only entry — don't check that one)
- BlackHole 2ch
- Primary Device: your headphones. Drift Correction: only on BlackHole 2ch.
- Rename it, e.g. AirPods + BlackHole.
Switch between devices in System Settings → Sound → Output, or route to one directly:
whisper-loopback --route-device "AirPods + BlackHole"Once the Multi-Output Device exists, point the call app at it so the other participants' audio is captured, while keeping your own microphone input separate:
| Setting in the call app | What to select | Why |
|---|---|---|
| Speaker / Audio output | Your Multi-Output Device (e.g. Speakers + BlackHole, or AirPods + BlackHole if using headphones) | This is what routes the other participants' voices through BlackHole so they get transcribed. |
| Microphone / Audio input | Your Mac's built-in microphone (not the Bluetooth headphones' mic) | Keeps the connection in high-quality mode — using a Bluetooth headset's mic often downgrades the whole link (both directions) to a lower-quality call profile (HFP), which also hurts what gets captured. |
Where to find it in each app:
- Zoom: Settings → Audio → Speaker and Microphone dropdowns.
- Google Meet: click the three-dot menu (or the gear icon) in the meeting → Audio settings → Microphone and Speakers.
- Microsoft Teams: Settings → Devices → Speaker and Microphone dropdowns (also available from the audio icon in the pre-join screen).
If you also want your own voice included in the transcript, add --mic on top of this
(see Meetings: include your own voice) — that's a
separate, software-only mix and needs no additional Audio MIDI Setup device.
The most repetitive chore is switching macOS output to your Multi-Output Device before
a session and switching it back after. whisper-loopback does it for you: at session
start it routes the default output to the detected Multi-Output Device, and at exit —
including Ctrl-C and SIGTERM — it restores the previous device.
Requires the SwitchAudioSource CLI (brew install switchaudio-osx). If it or a
Multi-Output Device is missing, routing is skipped with a one-line hint and
transcription runs normally — it's an enhancement, never a blocker. A manual change you
make mid-session is left in place at exit.
whisper-loopback # auto-route + restore
whisper-loopback --route-device "Estudio + BlackHole" # pick a specific device
whisper-loopback --no-route # manage audio yourselfBy default (--language auto) the tool identifies the spoken language — en or es —
from the first ~2 seconds of speech, pins it for the session, and prints what it chose
(language: es (auto, p=0.93)). Detection is restricted to the supported pair, so a
noisy clip that Whisper ranks as Portuguese still pins whichever of en/es is more
likely. The resolved language is recorded in the saved transcript's metadata.
- Pass
--language enor--language esto skip detection entirely. --redetect-on-pausere-detects after each pause — useful for a bilingual meeting that switches languages between segments (already-committed text is never rewritten).
Every session writes a Markdown transcript automatically — forgetting a flag no longer
loses an hour of class. Files land in one folder (default ~/Documents/whisper-loopback/)
named by session start time plus the optional --name label:
2026-07-04_20-15.md # whisper-loopback
2026-07-04_20-15_curso-python.md # whisper-loopback --name "Curso Python"
Each file starts with a metadata header (date, name, language, model, engine, duration)
followed by the committed transcript in paragraphs that follow speech pauses. Explicit
--txt/--srt paths are written in addition to the auto-saved transcript;
--no-save turns auto-saving off.
Pass --summary to get a Markdown summary (TL;DR, key points, decisions, action items) of
the session's transcript, generated fully locally by a model served on your machine — no
cloud API, no key, no transcript text leaving the machine.
-
Install Ollama and pull a model:
brew install ollama ollama pull llama3.2
-
Make sure the Ollama server is running — the desktop app runs it for you, or start it manually with
ollama serve. -
Run a session with
--summary:whisper-loopback --summary
At session end (transcript already flushed and saved), the console names the model being
used, then writes the summary to <session-base>.summary.md beside the transcript. Summary
generation is opt-in and off by default, requires auto-save, and never blocks or delays
transcription: if Ollama isn't running or the model isn't pulled, a one-line hint explains
what to do and the session transcribes normally without a summary; a slow or failing
generation degrades to a one-line hint too, with the transcript unaffected either way.
--summary-model(or thesummary_modelsetting, defaultllama3.2) picks the model.ollama_url(or theOLLAMA_HOSTenvironment variable) points at a non-default Ollama endpoint; the default ishttp://localhost:11434, so nothing leaves the machine unless you explicitly point it elsewhere.- Sessions under 50 committed words skip generation (a "transcript too short" notice).
- The summary is written in the transcript's language (configured or auto-detected en/es).
Settings can come from CLI flags, a TOML file, or environment variables, with this
precedence: defaults < TOML file < environment (.env) < CLI flags.
| Flag | Meaning | Default |
|---|---|---|
--language, -l |
en, es, or auto (detect) |
auto |
--redetect-on-pause |
re-detect language after each pause | off |
--model, -m |
tiny/base/small/medium/large-v3 |
base |
--engine |
whisper.cpp or faster-whisper |
whisper.cpp |
--device, -d |
input device index (from --list-devices) |
auto-detect BlackHole |
--mic |
mix your microphone into the transcript (meetings) | off |
--mic-device |
microphone input index (from --list-devices) |
system default input |
--label-speakers |
label speakers (speaker_1 = you, speaker_2 = the other side); requires --mic |
off |
--txt |
append committed text to this file | off |
--srt |
write SRT subtitles to this file | off |
--output-dir |
folder for auto-saved transcripts | ~/Documents/whisper-loopback |
--name |
session label for the auto-saved filename | none |
--no-save |
disable transcript auto-save | auto-save on |
--no-route |
don't switch the macOS output device for the session | routing on |
--route-device |
Multi-Output Device to route output to | auto-detected |
--no-console |
disable live console output | on |
--summary |
generate a local Markdown summary via Ollama at session end | off |
--summary-model |
Ollama model used for the summary | llama3.2 |
--config, -c |
TOML config file | none |
--list-devices |
list inputs and exit | — |
The menu bar app reads and writes ~/.config/whisper-loopback.toml; the CLI accepts any
file via --config. Example:
language = "es"
model = "small"
output_dir = "~/Documents/transcripciones"
mic = falseA few tuning knobs are file/env-only (no CLI flag), for uncommon setups:
vad_aggressiveness(0-3, default2): loopback VAD sensitivity.mic_vad_threshold(default0.008): RMS energy threshold for the microphone lane's VAD in--label-speakersmode only — raise it in a loud room, lower it for a very quiet one. Leave unset to use the built-in ambient-noise default.vad(defaulttrue): disable the energy VAD entirely (false).min_chunk_s/max_window_s/silence_s(defaults1.0/15.0/0.5): streaming pass cadence, buffer window cap, and the pause length that triggers a commit-and-trim.ollama_url(defaulthttp://localhost:11434): non-default Ollama endpoint for--summary; falls back to theOLLAMA_HOSTenvironment variable when unset.
Any setting is also available as WHISPER_LOOPBACK_<FIELD> (uppercased). A .env file
in the directory you launch from is loaded automatically:
WHISPER_LOOPBACK_OUTPUT_DIR=~/Documents/transcripciones
WHISPER_LOOPBACK_LANGUAGE=es
WHISPER_LOOPBACK_MODEL=small| Symptom | Fix |
|---|---|
No BlackHole input device found |
BlackHole is not installed or not visible. Reinstall, log out/in, re-run --list-devices. |
| Transcribes nothing / silence | System output is not routed to the Multi-Output Device, or the app is playing to a different device. |
| You can't hear audio anymore | Your output is set to raw BlackHole 2ch instead of the Multi-Output device. Switch to the Multi-Output device. |
| Output stuck on the Multi-Output device after a crash | A force-kill (SIGKILL, power loss) skipped the restore. Switch output back in System Settings → Sound — or just run another session and exit cleanly. |
| Routing didn't happen | SwitchAudioSource isn't installed (brew install switchaudio-osx), no Multi-Output Device was found, or --no-route is set. Check the console hint. |
| Auto-routing picked the wrong Multi-Output Device | Several exist (speakers, AirPods, …) — pass --route-device "Name" or set route_device in the TOML. |
My own words appear duplicated with --mic |
Your microphone is also picking up the system audio through open speakers. Use headphones (see Meetings). |
Call audio sounds worse after enabling --mic |
The call app is using the Bluetooth headphones' own microphone, dropping the connection to a lower-quality mode. Set the call app's microphone to your Mac's built-in mic instead (see call app setup). |
| First run is slow | The GGML model is downloaded and cached on first use; later runs are fast. |
# Clone and set up an editable environment
git clone https://github.com/jasonssdev/whisper-loopback.git
cd whisper-loopback
uv venv && uv pip install -e '.[faster,dev]' # or python -m venv .venv && pip install -e '.[faster,dev]'
# Tests run without any audio hardware or models (mock source + fake engine)
pytest
# Lint / format (Ruff, line length 100)
ruff check .
ruff check --fix .
# Type check (mypy strict; the package ships a py.typed marker)
mypyThe test suite (254 tests) drives the real streaming loop with a MockAudioSource
and a fake engine, so it's fully headless and CI-friendly. GitHub Actions CI runs
ruff, mypy, and pytest on macOS for every push and pull request. Tools under tools/
(benchmark.py, record_wav.py) need real hardware and are not part of CI.
src/whisper_loopback/
├── cli.py # Typer CLI entry point
├── menubar.py # optional macOS menu bar app (rumps)
├── runner.py # wires capture → DSP → streaming → output
├── config.py # dataclass config (defaults < TOML < env < flags)
├── audio/ # capture (BlackHole), MixedSource (mic), output routing
├── engine/ # TranscriptionEngine ABC + whisper.cpp / faster-whisper
├── stream.py # LocalAgreement-2 streaming over a VAD-trimmed buffer
├── vad.py # dependency-free energy VAD
├── dsp.py # downmix + 16 kHz resample
├── naming.py # session file naming
├── summary.py # local post-session summary via Ollama
└── output/ # console, txt, srt, markdown, transcript-buffer sinks
Changes are managed with OpenSpec under
openspec/ — capability specs live in openspec/specs/.
In scope (v1): macOS + Apple Silicon, English & Spanish transcription, CLI and menu bar app.
Out of scope for now: Windows/Linux, translation, speaker diarization, and a
standalone signed .app bundle.
Contributions are welcome — bug fixes, robustness improvements, docs, and performance work especially. See CONTRIBUTING.md for the development setup, checks, and PR process. Because the entire test suite is headless (mock audio source + fake engines), you can develop and contribute without any audio hardware or downloaded models.
MIT © Jason Dev