A Cantonese meeting recorder that keeps every second of client audio on the machine it was spoken into, and ends a meeting with tracked follow-ups instead of a file nobody reopens.

Solo build2026Live, in daily use

01The problem

Hong Kong meetings are held in Cantonese, and the speech engines that handle Chinese well are trained on Mandarin and on written Chinese. They do not fail loudly; they quietly normalise. The particles that carry the meaning of a spoken sentence get rewritten into formal prose nobody in the room actually said, and a decision comes back as a paraphrase of itself.

The second problem is the one that settles the argument for an agency. Sending client meeting audio to a cloud transcription API means confidential commercial discussion leaves the building, which is a conversation no one wants to have with a client afterwards. So the fallback was the usual one: nobody records, somebody scribbles half a page, and the commitments made in forty minutes survive only in whoever happened to remember them. Across three companies of internal and client meetings, that is a steady leak of things people agreed to do.

02What I built

A menu-bar recorder for macOS with no window to find. Double-tapping the left Option key starts capture and double-tapping again stops it; the audio is transcribed on the machine by an open-weights model and written to the Desktop as plain text, one sentence per line. Nothing is uploaded, because there is nothing in the system to upload it with. Downstream, an hourly watcher reads any new transcript and turns it into a meeting summary plus a set of pending action items, so a meeting ends as tracked tasks rather than as a wall of text.

  • Global double-tap hotkey on the left Option key, with a 400 ms window and an auto-repeat guard so holding the key never registers as a second tap.
  • A three-state machine reflected in the menu bar and refreshed every 0.3 s, so there is never any doubt about whether the thing is listening.
  • Cantonese-first transcription: voice-activity detection splits the audio on silence, each segment is transcribed on its own, then converted to Hong Kong Traditional characters in a way that leaves genuine Cantonese words as Cantonese.
  • Automatic office detection from the router rather than from anything typed, so transcripts file themselves by company.
  • An optional local server exposing the same engine as two tools, for use from an assistant.
  • Runs as a keep-alive login agent, so it is armed from the moment the machine is on and never has to be launched.
Schematic of the menu-bar recorder A drawn, abstracted representation of the tool: a menu-bar strip at the top, the three states of the recorder listed below it with the middle one marked as current, and a block of placeholder lines standing in for a finished transcript. No real transcript text is shown. MINUTES RECORDING STATE IDLE RECORDING TRANSCRIBING TRANSCRIPT, PLACEHOLDER LINES

Drawn, not captured. The strip at the top is the menu-bar item, which is the entire visible interface; the three rows below it are the whole state machine, with the current state marked; the block at the bottom stands in for a finished transcript. Every line shown is a placeholder. No real meeting text, filename or network detail appears anywhere on this page.

03As a product

Who buys it
Hong Kong professional-services firms whose working language is Cantonese but whose deliverables are written: agencies, law and accounting practices, consultancies, and any in-house team under a confidentiality clause or a data-residency policy that makes cloud transcription a non-starter.
Value
Meetings held in the language people actually speak become searchable text and tracked follow-ups, without a second of client audio leaving the laptop. Privacy is not a bullet on the list here; it is the condition that makes the tool usable at all in a client-facing business.
Positioning
The mainstream meeting-notes products are cloud-first and Mandarin-or-English-first, which is the wrong answer on both axes. Enterprise cloud speech-to-text solves neither. This sits in the gap: open-weights Cantonese model, inference on the machine, no per-minute price, and it records the room rather than only a video call.
Status
Internal tool. Built for my own use across three companies and wired into my steward system. Not packaged, not priced, no second user, no revenue. The architecture is deliberately licensable and nothing has been licensed.

04How it works

Stack
Python 3.12; FunASR running SenseVoiceSmall for recognition and a small VAD model for silence detection, both on CPU; sounddevice and soundfile for capture; OpenCC for Hong Kong character conversion; pynput for the global hotkey; rumps for the menu bar; a small Swift helper for the network name; launchd for lifecycle.
Shape
One always-on process, one worker thread, one text file as the interface between the recorder and everything downstream. No server, no account, no queue, and no credential of any kind.
Capture to tracked task, five levels, one boundary A vertical flow in five levels. Audio is captured on a hotkey, split at silences, and read by a local model on the machine’s own processor. At that point the flow divides: a text branch carries on to a transcript file and then to an hourly watcher that opens tracked tasks, and an audio branch ends there, because the recording never goes any further than the disk it was written to. The last edge is annotated to say that text crosses and audio never does. Every band is drawn at equal width and none is measured: no volume in this system was ever counted, and the one quantity that was checked — bytes of audio leaving the device — is zero, which shows here as a branch that stops rather than as a width. CAPTURE Double-tap Option, 16 kHz WAV, LOCAL DISK ONLY SEGMENT Silence splits the audio SPEECH SEGMENTS, CPU TRANSCRIBE Local model, no credential TEXT AUDIO TRANSCRIPT Desktop file, one per line TEXT CROSSES, AUDIO NEVER LEAVES THE MAC ACTION ITEMS Hourly watcher, pending

Width uniform throughout — nothing here was ever counted, so every band is drawn at equal width and no quantity is claimed. The one quantity that was checked, bytes of audio leaving the machine, is zero, and it is drawn as a branch that stops rather than as a width.

There is no credential anywhere in the system, so the audio cannot be uploaded even by accident. The branch that ends is the point of the diagram: what crosses is a text file written after the recording has already been read on the machine that made it.

The decision I spent longest on

Knowing which company a recording belongs to, without asking. The obvious signal is the network name, except that macOS now hides it from any process without location permission, so the clean answer disappeared exactly where it was needed. The fix was to fingerprint the router instead: ping the default gateway to force an entry into the address table, read the hardware address back, and match it against a fingerprint captured once per office. It needs no permission at all and it does not move.

The lesson generalises further than the feature. When a platform takes away the clean signal, look one layer down for an equivalent one that is not privacy-gated.

05Retrieval architecture

This system sits on the other side of the usual arrangement: it produces a corpus rather than consulting one, and almost all of its retrieval work is done at write time. Get the key right when the file is created and nothing downstream ever has to search for it.

What that buys is visible in the rows below, where two of the six layers are empty on purpose. A folder with well-named files is already a database for a corpus this small — and the last layer is not about the model at all, but about a boundary the audio is never allowed to cross.

ModelAt index time only. The models run while the corpus is being made — one turning speech into text, one turning a finished transcript into tasks — and nothing is ever asked a question afterwards.

Corpus
A folder of plain-text transcripts on the desktop, one file per meeting, each named for the office, the date and the time it was recorded, with one sentence per line and a short header. A second corpus accumulates downstream in Postgres: one briefing row per meeting and one tracked row per task. Neither has a schema beyond what the filename already says.
Ingestion
Two clocks, not one. Recording is a double-tap on a modifier key: audio is captured at sixteen kilohertz, cut at the silences, read segment by segment by a local model, converted to Hong Kong characters and written out — with the office inferred from a fingerprint of the router the machine is sitting behind rather than typed by anyone. Reading is an hourly watcher inside a working-hours window, so a file written at night waits for the window to open. Nothing is refused at the door: a filename that will not parse is still processed, under a fallback label.
Index
None. The folder is the queue and the filename is the key. Every attribute a lookup would need — which company, which day, which time — is already in the name, and the ordering a watcher needs is already the file’s modification time. An index would be a second store that has to stay in step with a directory that a person can drag a file into, for a corpus that grows by a handful of files a day; the cost is a synchronisation problem and the saving is nothing.
Query
None. Nothing arrives as language and nothing is searched. The watcher never asks which transcripts are relevant to anything; it asks which ones it has not seen yet, which is a question about time and identity rather than about content. There is no user typing on this side of the system at all — the only human input in the whole pipeline is two keystrokes, one at the start of a meeting and one at the end.
Selection
Two rules, both structural. A time window: files modified at or after the start of the last successful run, or the last forty-eight hours on a first run rather than the entire folder. And a dedupe key made of the filename itself, checked against the rows already written, so an overlapping window cannot summarise the same meeting twice. Nothing is trimmed or reordered afterwards, because one meeting fits: the whole transcript goes to the model unchunked and unranked, and is read once and never read again. There are no scores here, so there is nothing to rerank.
Grounding
The real guard is a boundary rather than a prompt. There is no credential anywhere in the codebase and no network path out of the recognition path, so the audio cannot be uploaded even by accident; the only thing that ever leaves is text, written after the transcript already exists on disk. Above that sits a human gate: every task the model extracts opens as a pending row for a person to approve or reject, nothing acts on one automatically, and a notification only fires when the meeting contained a hard commitment inside forty-eight hours.

Present — this layer exists and runs.Absent by decision — the layer is not there, and the sentence beside it is the reason. Every one of the six is answered on every system in this chapter, so the rows can be read across pages.

06Numbers

Measure Figure Basis
Marginal cost per hour of audio HK$0 Verified
Audio leaving the device 0 bytes Verified
Python across six modules 488 lines Verified
Local model weights, downloaded once at install ~1 GB Verified
Interaction cost to start a recording 2 keystrokes Verified
Manual note-taking displaced, per meeting 20–30 min Projected
Transcription wall clock against audio length 0.3–0.6x Projected

Verified — read off the source files, the install script and the login agent on 2026-08-12; the zero-bytes figure is the absence of any credential in the codebase and the fact that the only run-time network call is a ping to the local gateway. Projected — estimated, never measured. The note-taking figure is the write-up normally done after a thirty to sixty minute meeting, with no before-and-after study behind it; the speed figure is an estimate for CPU-only inference on Apple silicon and moves with the machine and with how much of a recording is silence. No accuracy or word-error rate is claimed anywhere on this page, because no Cantonese evaluation was ever run against a reference set and any percentage would therefore be invented.

07Timeline

  1. 2026-06 Six core modules written in a day: capture, engine, hotkey daemon, menu-bar app, office detection, and the optional tool server.
  2. 2026-07 Renamed from its working title. The rename broke the hardcoded paths in the installer and the login agent, and that repoint is still incomplete today.
  3. 2026-07 Adopted as a phase of the steward system: an hourly watcher that reads new transcripts and opens tracked action items from them.
  4. 2026-07 The watcher recorded zero runs for two days without a single error. Interval-based scheduling never fired on that machine; converted to explicit calendar intervals.
  5. 2026-08 In daily use as an always-on agent. The tool server ships in the codebase but is not currently wired into any assistant configuration.

08Looking back

What broke

The scheduler failed by producing nothing. For two days the transcript watcher logged no runs and no errors, because interval-triggered agents bootstrapped into the desktop session simply never fired on that machine while their calendar-triggered siblings fired fine. The production path had been correct the whole time; only the trigger was dead. An automation that fails silently is far more dangerous than one that crashes, and the fix was less about the trigger than about accepting that health has to be observable rather than assumed.

The rename cost real time too. The tool survives on a two-copy layout — an editable source copy and a runtime copy that the login agent actually executes — which is exactly what makes the running recorder immune to moving the source folder, and also exactly what let the installer's stale path go unnoticed until the next reinstall. Edits to source are inert until the install script pushes them across. I fell into that twice.

What it changed

Meetings stopped being write-offs. Dropping the friction of starting a recording to two keystrokes with no window to find is the entire reason it gets used at all; and because the output is a plain text file with a predictable name, it became a feed rather than an archive, something the next program can consume with no integration work. Wiring it into the steward turned a transcriber into the front end of a commitment-tracking loop: a meeting now ends with pending items waiting for approval instead of a file nobody reopens.

Where it stands

Live, unattended, one user, no revenue. The honest gap is measurement: I can defend the privacy property line by line, and I cannot yet put a number on the accuracy.

Back to Top