rokib16x/listnr
Local meeting listener for macOS. Captures your mic and system audio as two separate lanes, transcribes both on-device with WhisperKit, and labels You plus remote Speaker 1..N. Nothing leaves your ...
0 stars · 0 forks · 0 watchers
README
Listnr A local meeting listener for macOS. It captures your microphone and your speaker / system audio as two separate lanes, transcribes both on-device, and labels You plus remote Speaker 1...N. No bot in the call, no cloud, no account. Speak English, Bangla, Hindi, Spanish, French, German, Japanese, or Chinese, and optionally have any of them translated to English as it is transcribed.
Requires: macOS 14 or later on Apple Silicon (M1 or newer). Intel Macs are not supported.
Verbatim output from a real session, replayed for the recording. Nothing was cleaned up. You is the microphone lane, identified by which wire the audio arrived on, so it cannot be misattributed. Speaker 1 and Speaker 2 share the single speaker-output lane and are split apart by diarization once the session ends. How this was recorded. brew tap rokib16x/listnr https://github.com/rokib16x/listnr brew trust --tap rokib16x/listnr brew install listnr listnr setup && listnr
Why I built this My team decides things on calls. Every one of them ended the same way: either somebody took notes and stopped participating, or nobody took notes and we rebuilt the decision from memory two days later, badly. Every tool I found wanted one of three things I was not willing to give it. A bot in the call. It only works on the platforms they support, and it shows up in the invite; we move between Discord, Meet, and a phone on speaker depending on who is around. My conversations on somebody's server. These calls are about unreleased work, salaries, and things I have not thought through yet, and an M-series Mac will run Whisper locally, so there is no reason the audio has to leave the room. A subscription per seat, forever, for a transcript. The design comes from what failed first. I tried mixing the microphone and the call audio into one waveform and handing that to Whisper. You get plausible text with the speakers scrambled, invented sentences whenever two people talk at once, and a transcript that is confidently wrong, which is worse than no transcript, because you cannot tell which lines to trust. So the two lanes never get mixed before transcription. Your voice is identified by which wire it arrived on, so it can never be misattributed. The remote voices share one wire, so they go through speaker diarization to be split apart. More work than mixing, and the only way I found to get a transcript I actually believe. I use it for standups, design and debugging calls, and client calls with everyone's agreement, including calls in Bangla and Hindi mixed with English, which most tools handle poorly. It is a CLI because that is what I needed first. A menubar app is on the roadmap. If you make it better, send a PR.
Status: beta Listnr is at 0.1.3-beta. The core pipeline works and produces transcripts I rely on, but this is early software with one maintainer, and the CLI may change in any 0.x release. These are all known. Please read before filing an issue:
Limitation Detail
Diarization runs after the session While a session runs, remote audio is labeled Others. The Speaker 1...N split is computed when you stop. Live labels are planned.
Memory grows with session length Whole-session audio is kept in RAM, roughly 460 MB per hour across both lanes. Spilling to disk is planned.
Non-English is much weaker than English Whisper itself is far better at English than at Bangla or Hindi, at every model size. Listnr tunes its thresholds per writing system so it does not make this worse (details), but it cannot close the gap.
Translation is English-only and one-way /translate uses Whisper's own task, which only targets English. No Bangla → Hindi, and the original wording is not kept alongside it.
Voice detection thresholds are absolute Speech detection uses absolute RMS, so a very quiet or very hot mic needs /sensitivity high or low. Auto-calibration is planned.
Settings do not persist /lang, /model, /speakers, /diarize, /translate, and /dump all reset when you quit.
The capture layer has no automated tests 147 unit tests cover the pure logic. Anything touching real hardware is verified manually.
No GUI CLI only for now.
Found something not on this list? Open an issue.
Before you record other people Listnr records the voice of everyone on your call. In a lot of places, doing that without their consent is illegal.
All-party consent jurisdictions, including California, Illinois, Florida, Pennsylvania, Washington, and much of the EU under GDPR, require consent from every participant, not just you. The rules vary by state, by country, and by whether the call is business or personal. They apply to the act of recording, not only to sharing it.
Complying with the law where you and every participant are located is your responsibility. Tell people you are recording, get their agreement, and accept no for an answer. This software is provided as-is with no liability for how it gets used; see the LICENSE. The polite version is also the easy version. Say "I'm running a local transcriber, any objections?" at the top of the call and get on with it.
Privacy Your audio and your transcripts never leave your machine. No cloud transcription, no telemetry, no analytics, no crash reporting, no account. There is exactly one outbound request Listnr ever makes:
On first run it downloads model weights from Hugging Face (argmaxinc/whisperkit-coreml for transcription, argmaxinc/speakerkit-coreml for diarization), between 73 MB and 1.5 GB depending on the model, plus about 150 MB for diarization. No audio, no text, nothing identifying is sent. A plain file download, once per model.
After that it runs fully offline. To confirm, fetch a model and pull your network connection: listnr models download whisper-base.en What gets written to disk:
What Where Notes
Transcripts ~/Documents/Listnr/listnr_*.md Not encrypted. Nothing deletes them for you.
Models ~/Documents/huggingface/models/argmaxinc/ WhisperKit's cache; safe to delete to reclaim disk.
Config ~/Library/Application Support/listnr/config.json Read, but currently never written.
/dump audio ~/Documents/Listnr/debug/listnr-*.wav Raw session audio, owner-only (0600). Delete after debugging.
Install Homebrew This repository is its own tap, so there is no separate homebrew-listnr repo: brew tap rokib16x/listnr https://github.com/rokib16x/listnr brew trust --tap rokib16x/listnr brew install listnr Three commands rather than one, for two reasons. brew tap needs the explicit URL because its one-argument shorthand assumes a repo named homebrew-<name>. And Homebrew 6 refuses to load formulae from third-party taps until you trust them, which applies to any tap, not just this one. This installs the prebuilt binary, so it takes seconds rather than the several minutes a source build spends compiling WhisperKit. You get the binary, a man page, and completions for bash, zsh, and fish.
Signed installer Download listnr-<version>.pkg from the latest release and open it. The package is signed with a Developer ID and notarized by Apple, so it installs without the "unidentified developer" warning. listnr goes to /usr/local/bin. Pre-built binary Apple Silicon only. Each release ships the binary, shell completions, and a man page. V=0.1.3-beta curl -LO "https://github.com/rokib16x/listnr/releases/download/v$V/listnr-$V-macos-arm64.tar.gz" tar -xzf "listnr-$V-macos-arm64.tar.gz" sudo cp "listnr-$V-macos-arm64/listnr" /usr/local/bin/listnr The binary is signed and notarized, but a notarization ticket cannot be stapled to a bare binary, only to a .pkg, .dmg, or .app. So if you fetch this tarball in a browser rather than with curl, macOS quarantines it and Gatekeeper falls back to an online check on first run. The .pkg above has no such caveat, and brew and curl never set the quarantine attribute at all. While Listnr is a 0.x prerelease, the URL needs the explicit tag. GitHub's /releases/latest/ excludes prereleases and will 404. If you download it through a browser instead of curl, macOS quarantines it and refuses to run it. Clear that with xattr -dr com.apple.quarantine listnr. From source Needs Xcode 15 or later (Swift 5.9+). Homebrew builds from source too, so either way the first install takes a few minutes. git clone https://github.com/rokib16x/listnr.git cd listnr swift build -c release sudo cp .build/release/listnr /usr/local/bin/listnr Grant the permissions listnr setup # mic + Screen & System Audio Recording listnr doctor # verify Listnr needs two permissions, and macOS attaches both to your terminal application rather than to the listnr binary:
Permission Why
Microphone Lane A, your voice
Screen & System Audio Recording Lane B, the speaker output, which is everyone else
Two things follow. Switching from Terminal to iTerm means granting them again, and macOS usually needs the Settings toggle plus a relaunch. brew upgrade will not re-prompt. listnr doctor tells you which one is missing. Both are required to start; there is no microphone-only mode, because a meeting transcript without the other side is not one. The Screen Recording permission is how macOS gates system audio through ScreenCaptureKit. Listnr asks for a 2x2 pixel video stream it never reads, purely because the API insists on one. No screen content is captured, stored, or transmitted. See SystemAudioCapture.swift.
Usage
Put on a headset. This matters more than anything else on this page. Open speakers leak the remote voices back into your microphone, which puts the same person on both lanes and ruins the speaker separation. Join a call, or just play audio through the speakers. Run listnr, then:
listnr> /live # listen until you stop it listnr> /live 30 # 30 seconds, then report
Type q or /stop and press Enter to finish. Ctrl+C cancels without a transcript. The transcript prints, and a Markdown copy lands in ~/Documents/Listnr/.
[00:02] You: Morning. Quick one today, I want to close out the export bug. [00:09] Speaker 1: Sounds good. I reproduced it, it only happens when the file name has a colon. [00:17] You: That matches what I saw. I'll strip reserved characters and add a test. [00:26] Speaker 1: Ship it behind the flag first, the installer folks asked us not to change file names silently.
Your microphone is always You. Remote voices become Speaker 1...N after the session ends, when diarization runs. Language and translation listnr> /lang en # English (whisper-base.en, 139 MB) listnr> /lang bn # Bangla · also hi, es, fr, de, ja, zh listnr> /lang auto # detect once at the start, then lock listnr> /translate # speak any language, transcript comes out English
Each language picks a sensible default model, so /lang is usually all you touch. Name the language rather than using auto. Detection runs once per session and locks, but it still has to make that call on a second or two of speech. Two things are worth knowing before you tune anything:
The non-English defaults are not the most accurate models, on purpose. They aim to keep pace with a live conversation, because a model that cannot has its audio dropped by the pipeline, so you lose whole utterances rather than getting rougher text. Reach for /model whisper-large-v2 when accuracy matters more than latency. /translate switches models automatically. The turbo builds cannot translate at all (they silently return the original language), so turning it on moves you to whisper-large-v2.
Full model table, the reasoning behind each default, and why non-English used to look broken: docs/models.md. Other commands listnr> /speakers 2 # how many remote people to expect (1-6) listnr> /model <id> # see `listnr models list` listnr> /diarize # toggle SpeakerKit listnr> /sensitivity high # quiet mic: hear more (see troubleshooting) listnr> /dump # toggle WAV dumps to ~/Documents/Listnr/debug listnr> /status # show current options listnr> /help
One-shot, no shell listnr start --seconds 60 --speakers 2 listnr start --language bn --seconds 120 # Bangla transcript listnr start --language bn --translate --seconds 120 # English transcript listnr start # until Ctrl+C listnr models list listnr models download whisper-large-v2 # pre-fetch before a call The transcript goes to stdout; progress, level meters, and diagnostics go to stderr, so you can redirect just the transcript. Structured --json output is planned. Contradictory options, such as an English-only model with a non-English language or a turbo model with --translate, are reported before the session starts rather than quietly producing the wrong thing. Something wrong? Missing text, gibberish, or dropped words each print a specific line to stderr that says which it is. docs/troubleshooting.md maps each one to a fix.
How it works Mic ──► Lane A ──► VAD ──► Whisper ────────────────► You Speakers ──► Lane B ──► VAD ──► Whisper ───────────────► Others (live) └─► SpeakerKit ──► Speaker 1..N ──► Whisper per span (after you stop) (A and B are never mixed before transcription)
Three rules hold the whole thing together:
Lane A and Lane B are never mixed before transcription. Once two voices share one waveform, no post-processing recovers who said what, and Whisper will invent text rather than admit it. You are always Lane A. Your identity comes from which wire the audio arrived on, so it cannot be misattributed. Remote people live on Lane B, where several voices arrive on a single wire, which is exactly why diarization is needed instead of a simple "You versus Others" split.
docs/plan.md has the full architecture and milestone history. It predates some of the implementation and describes a couple of choices (TOML config, a Core Audio process tap) the code does not use. Stack
Swift, one SPM package: a ListnrCore library plus a one-line executable WhisperKit for on-device speech recognition (Core ML + Apple Neural Engine) SpeakerKit for on-device diarization (Pyannote via Core ML), shipped inside WhisperKit AVAudioEngine for the microphone (Lane A), ScreenCaptureKit for system audio (Lane B)
Contributing Bug reports and small, focused PRs are both very welcome. CONTRIBUTING.md has build instructions, good first issues, and the concurrency rules for the audio path. Security problems go through SECURITY.md rather than the public tracker. Everyone taking part is covered by the Code of Conduct. Cutting a release or setting up the Homebrew tap: packaging/README.md. Acknowledgments Listnr is original code, but it learned from other people's work:
Project What it gave Listnr
parrot by digimata Patterns I adapted rather than forked: AVAudioEngine microphone into 16 kHz Float32, the WhisperKit warm-up and transcribe wrapper, the doctor and setup permission flow, and the WAV dump helper. The meeting loop itself is different.
meetily by Zackriya-Solutions Ideas only: separate microphone and system audio lanes, not mixing before transcription, and the general shape of a session-and-notes product. No Meetily code ships here, and Lane B is ScreenCaptureKit in Swift rather than Meetily's Rust and Core Audio tap.
WhisperKit and SpeakerKit by Argmax Direct dependencies for on-device transcription and diarization.
License MIT, Copyright (c) 2026 Rokibul Hasan. Dependencies are MIT (WhisperKit, SpeakerKit, yyjson) or Apache-2.0 (swift-argument-parser, swift-transformers, and the other Apple and Hugging Face packages). Model weights download at runtime from Hugging Face and carry their own licenses.

