Skip to content

jasonssdev/whisper-loopback

Repository files navigation

🎧 whisper-loopback

Live, 100% local transcription of your Mac's system audio — videos, meetings, calls — to text in real time. No audio ever leaves your machine.

Python 3.11+ Platform: macOS (Apple Silicon) License: MIT Privacy: 100% local Engine: whisper.cpp + Metal Code style: Ruff Tests: 254 passing

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.


Table of contents


Why whisper-loopback

  • 🔒 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.

Features

🎧 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

How it works

 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 (TranscriptionEngineSegment), so swapping models touches only the engine/ package.

Requirements

  • 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).

Quick start

1. Install

# 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 app

2. One-time macOS audio setup

macOS can't record system output directly, so you route it through BlackHole. Follow macOS audio setup once to:

  1. Install BlackHole (brew install blackhole-2ch).
  2. Create a Multi-Output Device (your speakers + BlackHole) in Audio MIDI Setup, so you still hear the audio while it's captured.
  3. (Optional) brew install switchaudio-osx so 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).

3. Run

whisper-loopback --list-devices   # confirm BlackHole shows up
whisper-loopback                  # auto-detects language; Ctrl-C to stop

That's it — text streams live and the transcript is saved to ~/Documents/whisper-loopback/.

Usage

# 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-save

Stop 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.

Menu bar app

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 bar

The menu offers:

  • Start / Stop transcription — one click each; the icon shows a running MM:SS while 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 = true in 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.menubar

Packaging as a standalone signed .app is out of scope for now; the menu bar app runs from the installed console script.

Meetings: include your own voice

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 --mic run 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.

Label who's speaking (--label-speakers)

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-speakers
  • speaker_1 is you (the microphone); speaker_2 is 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 into speaker_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, --mic produces 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 audio setup

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.

1. Install BlackHole

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" input

2. Create the Multi-Output Device

If 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.

  1. Open Audio MIDI Setup (Applications → Utilities, or Spotlight search).
  2. Click the + at the bottom-left → Create Multi-Output Device.
  3. In the right-hand panel, check both:
    • Your normal output (e.g. MacBook Pro Speakers)
    • BlackHole 2ch
  4. Set your normal output as the primary/master device (leftmost checkbox column) and enable Drift Correction on BlackHole 2ch only.
  5. Rename it to something recognizable, e.g. Speakers + BlackHole.

3. Route audio to it

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.

4. Adding headphones

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:

  1. Connect the headphones first.
  2. Audio MIDI Setup+Create Multi-Output Device.
  3. Check only:
    • Your headphones' output entry (Bluetooth devices often list a separate input-only entry — don't check that one)
    • BlackHole 2ch
  4. Primary Device: your headphones. Drift Correction: only on BlackHole 2ch.
  5. 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"

5. Configuring a Zoom / Google Meet / Microsoft Teams call

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.

Automatic audio routing

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 yourself

Language auto-detection

By 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 en or --language es to skip detection entirely.
  • --redetect-on-pause re-detects after each pause — useful for a bilingual meeting that switches languages between segments (already-committed text is never rewritten).

Auto-saved transcripts

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.

Session summary (local, via Ollama)

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.

  1. Install Ollama and pull a model:

    brew install ollama
    ollama pull llama3.2
  2. Make sure the Ollama server is running — the desktop app runs it for you, or start it manually with ollama serve.

  3. 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 the summary_model setting, default llama3.2) picks the model.
  • ollama_url (or the OLLAMA_HOST environment variable) points at a non-default Ollama endpoint; the default is http://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).

Configuration

Settings can come from CLI flags, a TOML file, or environment variables, with this precedence: defaults < TOML file < environment (.env) < CLI flags.

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

TOML file

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 = false

A few tuning knobs are file/env-only (no CLI flag), for uncommon setups:

  • vad_aggressiveness (0-3, default 2): loopback VAD sensitivity.
  • mic_vad_threshold (default 0.008): RMS energy threshold for the microphone lane's VAD in --label-speakers mode 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 (default true): disable the energy VAD entirely (false).
  • min_chunk_s / max_window_s / silence_s (defaults 1.0 / 15.0 / 0.5): streaming pass cadence, buffer window cap, and the pause length that triggers a commit-and-trim.
  • ollama_url (default http://localhost:11434): non-default Ollama endpoint for --summary; falls back to the OLLAMA_HOST environment variable when unset.

Environment variables & .env

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

Troubleshooting

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.

Run locally / Development

# 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)
mypy

The 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.

Project layout

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/.

Roadmap & scope

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.

Contributing

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.

License

MIT © Jason Dev

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages