Skip to content

Repository files navigation

Echo (Эхо)

Local-first meeting recorder for macOS: records both sides of any call, transcribes on-device, and drops an AI summary into your Obsidian vault. A DIY replacement for Granola that you fully own.

Everything runs locally except the summary step, which uses your own claude CLI subscription. No accounts, no cloud storage, no telemetry. Your meetings never leave your machine.

What it looks like

Echo window

While recording, a pill floats above every app and every Space - full-screen Zoom included - so a forgotten session can't quietly run for hours:

Recording pill

How it works

system audio ──(Core Audio process tap)──┐
                                          ├─→ two CAF tracks ─→ ffmpeg ─→ 16k WAV
microphone ──(AVAudioEngine)─────────────┘
                                          ─→ mlx-whisper (large-v3-turbo, on-device)
                                          ─→ sherpa-onnx speaker diarization
                                          ─→ transcript.md  ("Me" / "Speaker N")
                                          ─→ claude CLI summary ─→ note.md ─→ Obsidian

Two separate tracks mean speaker separation comes for free: the mic track is you, the system track is everyone else (further split by diarization).

The app also watches the default input device (Core Audio HAL listener, no polling): when any process grabs the microphone - Zoom, Meet in a browser tab, FaceTime, Telegram - a small floating panel offers to start recording. Granola-style, but local.

Requirements

  • macOS 14.4+ on Apple Silicon (the process-tap API and mlx-whisper are both hard requirements)
  • ffmpeg (brew install ffmpeg)
  • Python 3.11+ with mlx_whisper, sherpa_onnx, soundfile, certifi
  • A logged-in claude CLI for summaries (subscription; the pipeline works without it - you just get a transcript instead of a note)
  • Diarization models (see below)

Setup

git clone <this repo> ~/echo && cd ~/echo
pip3 install mlx_whisper sherpa_onnx soundfile certifi
bash build.sh                 # builds Echo.app with swiftc, no Xcode project
open Echo.app

Grant Microphone and Screen & System Audio Recording in System Settings → Privacy & Security, then relaunch the app (TCC applies on next launch).

Diarization models go into models/:

  • sherpa-onnx-pyannote-segmentation-3-0/ - from sherpa-onnx releases (note: the release tag is literally speaker-recongition-models, typo included)
  • nemo_en_titanet_small.onnx - same place

Optional: copy config.env.example to config.env to get notes copied into an Obsidian vault and clickable deep links.

The TCC trap (read this if recording is silent)

macOS ties audio permissions to the app's code signature. With the default ad-hoc signature every rebuild changes the app's identity, and your granted permissions silently stop applying: the tap "works" but writes pure silence (-91 dB). If you plan to rebuild, create a self-signed code-signing certificate once (Keychain, 10 years) and build with SIGN_ID="My Cert Name" bash build.sh - permissions then survive rebuilds.

Also: launch the app from Finder, not from a terminal - otherwise TCC attributes the audio capture to your terminal.

Usage

Type a meeting title, hit record. Stop when done - transcription and summary run automatically (a 1-hour meeting takes ~6 minutes on an M-series Mac). Click a finished meeting to open its note.

While recording, a pill floats above everything - including full-screen Zoom and every Space - showing the elapsed time and a Stop button; the menu bar carries the same timer. If you forget anyway, Echo stops itself after 15 minutes of silence in the system track (AUTOSTOP_SILENCE_MIN in config.env, 0 to disable). This listens to the system track only, so anything playing audio - music, a video - keeps the recording alive.

Raw CAFs are deleted once the transcript is written: they cost ~1 GB per hour per track and the text is what you keep. The 16 kHz WAVs stick around for a couple of days (KEEP_WAV_DAYS), because re-running the pipeline on real audio is the only way to tune diarization and the filters. If the transcript came out empty - whisper crashed, or the recording held no speech - all the audio stays put, because then it's your only way to retry.

CLI alternative without the app: ./record_meeting.sh "Title" (Enter stops; uses the compiled rec_system helper - build it with swiftc -O legacy/rec_system.swift -o rec_system).

Repo layout

File Role
EchoApp.swift The whole app: UI, recorder, call detection, popup
finish_session.sh Post-stop pipeline: caf→wav→transcript→note
transcribe_merge.py mlx-whisper both tracks + merge with speaker labels
diarize.py sherpa-onnx segmentation + speaker embeddings
process_meeting.py claude CLI summary → note.md → Obsidian
pick_mic.sh, record_meeting.sh CLI recording path
legacy/ Minimal reference implementations (menu-bar app, standalone tap)

Honest limitations

  • Speaker count auto-detection degrades on long recordings (a 2-hour workshop produced "Speaker 132"), and diarization is the slowest step by far
  • Whisper hallucinates on non-speech audio - hold music makes it emit "Thank you." every 30 seconds with no_speech_prob = 0.000, so its own thresholds never fire. Filtered by text density (speech runs 5-20 chars/sec, filler 0.2-0.7) and by compression_ratio for repetition loops. The cost: a lone "yeah" inside a long noisy stretch gets dropped with the noise
  • Browser-tab calls are detected (mic busy), but the meeting title can't be prefilled from a tab
  • Russian-first: UI strings and summary prompt are in Russian - fork and translate, it's all in two files

License

MIT

About

Local-first meeting recorder for macOS: two-track capture, on-device Whisper, speaker diarization, AI summaries into Obsidian

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages