https://github.com/bdombro/active-listener
Record and transcribe meetings using a CLI, fully local
https://github.com/bdombro/active-listener
Last synced: about 2 months ago
JSON representation
Record and transcribe meetings using a CLI, fully local
- Host: GitHub
- URL: https://github.com/bdombro/active-listener
- Owner: bdombro
- License: mit
- Created: 2026-04-08T20:04:55.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-04-20T10:12:47.000Z (3 months ago)
- Last Synced: 2026-04-20T11:34:32.616Z (3 months ago)
- Language: Rust
- Homepage:
- Size: 643 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Active Listener
Record and transcribe meetings using a CLI, fully local. Press Ctrl+C to stop — you get a WAV and a Whisper-transcribed markdown file. No cloud, no subscriptions.
Why use over WhisperX/Python? See [Architecture](#architecture)
## Quick start
Download the binary for your platform from the [releases page](https://github.com/bdombro/active-listener/releases):
| Platform | File |
|---|---|
| macOS Apple Silicon | `active-listener-*-aarch64-apple-darwin.tar.gz` |
| macOS Intel | `active-listener-*-x86_64-apple-darwin.tar.gz` |
| Linux x86_64 | `active-listener-*-x86_64-unknown-linux-gnu.tar.gz` |
| Windows x86_64 | `active-listener-*-x86_64-pc-windows-gnu.tar.gz` |
Extract and install (macOS / Linux):
```bash
tar -xzf active-listener-*.tar.gz
./active-listener install
source ~/.zshrc
```
`install` copies the binary to `~/.local/bin`, adds zsh completions, and pre-downloads the default Whisper weights (`small`) from Hugging Face. Diarization models are also pre-fetched so your first `--diarize` run starts immediately.
> **Windows**: `install` is not supported — place `active-listener.exe` on your `PATH` manually and run it directly. Note that the Windows release binary is built without diarization support, so `--diarize` has no effect.
**To build from source** (requires Rust 1.74+ and [just](https://github.com/casey/just)):
```bash
just install
```
## Usage
```bash
# Interactive: choose mic, system audio, or both (TTY only)
active-listener start
# Record mic only; write to current directory
active-listener start --mic
# Record mic + desktop audio (requires Screen Recording permission on macOS)
active-listener start --mic --system-audio
# Custom output folder and filename stem
active-listener start --mic --dir ~/notes --name standup
# Speaker labels in the output (adds ~10–30s post-processing)
active-listener start --mic --diarize
# Transcribe an existing WAV (no re-record)
active-listener process recording.wav --dir ~/notes
active-listener process recording.wav --diarize
```
## Output
Every session produces two files with the same basename:
- **`YYYY-MM-DD_HHMMSS.wav`** — raw 16 kHz mono recording
- **`YYYY-MM-DD_HHMMSS.md`** — frontmatter + transcript
### Default (no `--diarize`)
```markdown
---
date: 2026-04-08T14:30:00
duration: 32m 15s
whisper_model: small
---
# Meeting notes — 2026-04-08 14:30
## Transcript
**[00:00]** Welcome everyone, let's get started with today's standup.
**[00:12]** I finished the auth refactor yesterday and pushed it for review.
**[00:28]** Looks good to me, I'll take a look after this call.
**[01:04]** Any blockers? Nothing from my side, shipping the feature branch today.
```
### With `--diarize`
How it works:
- Whisper and the speaker diarizer run in parallel; labels are merged into the transcript by timestamp overlap.
Tips:
- Diarization isn't great. Can be improved by having an LLM "improve diarization on this meeting transcript"
```markdown
---
date: 2026-04-08T14:30:00
duration: 32m 15s
whisper_model: small
---
# Meeting notes — 2026-04-08 14:30
## Transcript
**Speaker 1**
**[00:00]** Welcome everyone, let's get started with today's standup.
**Speaker 2**
**[00:12]** I finished the auth refactor yesterday and pushed it for review.
**Speaker 1**
**[00:28]** Looks good to me, I'll take a look after this call.
**Speaker 2**
**[01:04]** No blockers, shipping the feature branch today.
```
Labels are `Speaker 1`, `Speaker 2`, … — not real names.
## Options
All flags work on both `start` and `process` unless noted.
| Flag | Default | Description |
|---|---|---|
| `--mic` | off | Capture microphone (`start` only; at least one source required) |
| `--system-audio` | off | Mix in desktop audio (`start` only) |
| `--dir PATH` | `.` | Output directory |
| `--name STEM` | datetime | Output filename without extension |
| `--whisper-model` | `small` | `tiny` / `base` / `small` / `medium` / `large` |
| `--diarize` | off | Add speaker labels via sherpa-onnx |
| `--num-speakers N` | auto | Fix speaker count (more reliable than threshold mode) |
| `--diarize-threshold` | `0.9` | Cluster merge/split — higher → fewer speakers |
| `--diarize-embedding PATH` | auto | Custom speaker embedding ONNX (see below) |
| `--duration SECS` | unlimited | Auto-stop after N seconds (`start` only) |
| `--device NAME` | default | Mic device name — see `--list-devices` |
| `--cpu` | off | Force CPU; skip Metal |
| `--verbose` | off | Print device and model details |
`ACTIVE_LISTENER_DIARIZE_EMBEDDING` is the env-var equivalent of `--diarize-embedding`.
## Speaker diarization
Powered by [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) (pyannote segmentation + NeMo Titanet small embedding). Models are downloaded once to `~/.cache/active-listener/diarize/` on first use (or pre-fetched by `install`).
Offline diarization is fuzzy — one threshold cannot perfectly split every conversation. Practical tips:
- **`--num-speakers N`** is the most reliable option when you know the count (two-person call, panel of three, etc.).
- **`--diarize-threshold`** (default `0.9`) is the fallback knob: higher merges more aggressively (fewer speakers), lower splits more.
- **`--diarize-embedding`** — swap in another sherpa-onnx embedding ONNX if you want to experiment (e.g. [Titanet large](https://github.com/k2-fsa/sherpa-onnx/releases/tag/speaker-recongition-models) for better accuracy at the cost of size and speed).
Expect imperfect results in difficult audio (crosstalk, TV in the background, single mixed channel).
## Privacy
- Everything runs locally. No audio, transcript, or metadata leaves your machine.
- Whisper weights are downloaded from Hugging Face once (model files only; no audio uploaded).
- Diarization models are downloaded from GitHub releases once (no audio uploaded).
- `active-listener uninstall` removes the binary, shell snippets from `~/.zshrc`, and all cached `openai/whisper-*` Hugging Face hub folders.
- The `.wav` and `.md` files contain raw audio and full transcripts — treat them accordingly.
- macOS requires **Microphone** permission for `--mic` and **Screen Recording** for `--system-audio`.
## Platform notes
| Platform | Audio capture | Diarization |
|---|---|---|
| macOS | Mic (cpal) + ScreenCaptureKit system audio | Yes (`diarize` feature, default) |
| Linux | Mic (cpal) + PipeWire (`linux-system-audio` feature, default) | Yes (native build); No (cross-compiled `--no-default-features` build) |
| Windows | Mic (cpal) + WASAPI loopback | Yes (native build); No (cross-compiled build) |
Linux native builds need `libpipewire-0.3-dev` and `libspa-0.2-dev`.
## Development
Requires [just](https://github.com/casey/just) (`brew install just`) and Rust 1.74+.
```bash
just build # cargo build --release (native host)
just install # build + install binary + shell config
just build-cross # cross-compile: macOS aarch64/x86_64, Linux x86_64, Windows x86_64 (needs Docker)
just release # build-cross + GitHub release (needs gh)
```
Quick dev loop: `cargo run -- start --mic` — records, transcribes, writes WAV + markdown.
To strip the diarization dependency for a smaller binary:
```bash
cargo build --release --no-default-features --features metal,linux-system-audio
```
## Requirements
- Rust 1.74+
- macOS: Metal is the default GPU backend (`metal` feature). On other platforms build with `--no-default-features` and re-add what you need.
## Architecture
Is built on top of:
- **Candle** (`candle-transformers`) — Whisper transcription in Rust using Hugging Face `openai/whisper-*` weights; **`hf-hub`** fetches and caches those checkpoints.
- **sherpa-onnx** — ONNX Runtime–backed offline diarization; default segmentation weights are **`sherpa-onnx-pyannote-segmentation-3-0`** (pyannote segmentation 3.0 exported to ONNX), plus a separate default **speaker-embedding** ONNX (NeMo Titanet small).
**Compared to [WhisperX](https://github.com/m-bain/whisperX)**:
- **active-listener** includes audio capture, is a **single native binary** optimized for MacOS with some Linux/Windows support, no hugging-face account or terms-agreement required.
- **WhisperX** no audio capture, feature-bloated, and is glue-code for a collection of python projects which were never really meant to work well together. Is fragile, has several hidden host requirements (stale FFMpeg, python versions, hugging-face user account and usage/agreement for non-commercial models).
- Quality: I, personally, did not get better quality with WhisperX over `active-listener`.
## License
MIT