My standups have a shape that meeting software hates. One laptop in the room carries the call and everyone else just talks; the remote half of the team does the same thing on their side. A tool that promises to capture who said what gets one microphone per room and concludes it is listening to two people. The transcript reads like a dialogue. The action items come out attributed to whoever owned the laptop, and half of the ones assigned to me were never mine.

I was paying for this. I hate paying for a service that doesn't do what it claims, and to be fair, the convenience of tools like Granola is real: they join from your calendar and they're frictionless. But the pitch is "we capture tasks and who said what," and that pitch crumbles the second more than one person shares a microphone. Which is most of the meetings I actually care about.

There was a quieter reason too. A meeting recorder is a service you feed your voice to, continuously. Voice is a biometric now. With where AI is, a leaked archive of my voice is enough to train a model that sounds like me and commits fraud as me, and I would rather that raw material never sit on a server I don't control.

So I built one that stays home.

Two days, most of them not mine

The tool is called overheard, and it took about two days, spread over evenings and build cycles. I want to be precise about the division of labor, because it's the most interesting fact about the project: I did the scaffolding and the architecture, and AI wrote the bulk of the code.

The architecture is small. Two native macOS processes. A menu bar app listens for a global hotkey and records the room. A local server loads WhisperX for transcription and pyannote for speaker diarization once, at startup, and keeps them resident. When a recording lands, the server transcribes it, separates the speakers as actual people in a room rather than "me" and "everyone else," and hands the transcript to an LLM to pull out action items. The LLM is swappable: Claude, OpenAI, or a local Ollama model, in which case nothing about the meeting ever leaves the machine.

I can write the Python side of that in my sleep. What I had never done was build a Mac menu bar app, write a launchd plist, or fight macOS's permission system. The AI taught me those, and by the end I understood the patterns well enough to argue with it. That's what made the pairing work, and I'd state it as a rule: I knew what I wanted, and I know enough systems and programming to see a bad pattern and know it's a bad pattern. The model supplied code faster than I ever could. Judging the code stayed my job.

macOS supplied the one genuinely weird fight. Grant the recorder microphone access and the grant attaches to something called python3.12, because every GUI Python script on the machine funnels through one shared interpreter. Rebuild the app and the permission evaporates, so I was re-approving the same access over and over. The fix was to build a minimal real app with its own signing certificate, so the grant has a stable identity to stick to across rebuilds. Nothing about transcription or diarization was half as stubborn as convincing the operating system who was asking for the microphone.

The parts nobody asked for

Here's the thing I keep chewing on. The code was cheap. Two days, most of it generated. And when code gets that cheap, a filter disappears: effort normally forces you to build only what the spec demands, and everything else stays a wish. This project had no spec. No ticket, no deadline, no user but me. Everything that made it into the tool got there for one reason, which is that I believe tools should work that way.

You can tell what an engineer actually believes by what they build when nobody is paying them.

So here is what I apparently believe, judging by the evidence:

  • Every result should carry its provenance. Each recording writes a small sidecar file next to the transcript: which whisper model ran, which diarization model, the speaker bounds, whether system audio was mixed in, which LLM extracted the tasks. Config changes between recordings, and a transcript alone can't tell you what produced it. Nobody asked for this file. I wrote the logic in on instinct, in case I want the metadata later.
  • A failed job should never cost you the input. If a transcription errors out, the raw audio is preserved and the failure is logged, and you can re-run it later straight from the menu bar. The recording is the one thing you can't get back; everything downstream is retryable.
  • You should be able to see that something is stuck. The first version was a script I babysat. It worked, and I hated it. The reason it became a real menu bar app is that I wanted the icon to change while it's idle, recording, or transcribing, so I know if something is stuck without going log-diving.
  • Fallbacks must leave a trail. If system audio capture is unavailable, the recorder falls back to mic-only rather than losing the meeting. But it logs the fallback and records it in the metadata, because a silent fallback is how you end up trusting a transcript that was made under different conditions than you think.

I spent the last four years building a production system whose entire point was that every result can explain itself months later. I didn't set out to smuggle that discipline into a weekend tool. Apparently lineage, retries, and observability just follow me home.

The boundary that paid rent

One architectural choice paid off in a way I didn't plan. The recorder and the server only ever talk over HTTP; the recorder sends audio to whatever URL the config names. I drew that boundary for tidiness. Then it turned out faster-whisper has no GPU path on Apple Silicon at all: CUDA or CPU, no Metal, no exceptions. On the Mac's CPU, transcription runs at roughly realtime, fine for a short call and painful for a long one.

Because the boundary was already an HTTP hop, the fix was a config change. The server now runs on a Linux box with a CUDA GPU and the Mac just points at it. Transcription got ten to thirty times faster and not a line of the recorder changed. A dead end in the hardware turned into a value in a YAML file. Good boundaries pay rent you didn't know you'd owe.

The audio does cross a network to get there, so this only happens over a VPN, to a box I control. Staying home was always about who owns the machines.

What's still rough

A few days of real use in: I get more out of a meeting from these transcripts than I ever did from the paid tool. The transcription is genuinely good, and the diarization is accurate more often than I expected from a single microphone in a real room. It still struggles with background noise, and when people repeat the same phrase back and forth a few times, the speaker attribution can wander.

Speakers also come out anonymous: SPEAKER_00, SPEAKER_01, and renaming them is a manual find-and-replace, which is exactly as annoying as it sounds. The next thing I want to figure out is tagging a person once and having them recognized in future recordings, some kind of voice signature. I want to build that carefully. A tool born from not wanting my voice signature on someone else's server should be slow to start collecting everyone else's.

The heartbeat

One more thing broke, and it's the right note to end on. Long recordings killed the first version. An hour of audio takes long enough to transcribe that the connection between recorder and server would time out, and the whole job would die with it. So the recorder now holds the connection open with a heartbeat while the server works.

It's a few lines of plumbing, and it's my favorite part of the tool because of what it took to exist. You have to hit the failure in real use, recognize it as the old familiar shape of every long-running job you've ever operated, and care enough about a free tool with one user to fix it properly. No spec mentioned it. No feature list will ever advertise it.

Nobody prompted for the heartbeat.