Skip to content

bundles: meeting recorder with local transcription - #336

Merged
kh0pper merged 3 commits into
mainfrom
feat/meeting-recorder-bundle
Sep 9, 2026
Merged

bundles: meeting recorder with local transcription#336
kh0pper merged 3 commits into
mainfrom
feat/meeting-recorder-bundle

Conversation

@kh0pper

@kh0pper kh0pper commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Records a meeting in the browser and transcribes it on the same host. Built because a meeting worth referencing later should not have to pass through a cloud transcription service to become text.

What it does

The panel asks the browser for two audio sources, the meeting itself (a shared tab or window, which is the only way a page is allowed to hear another tab) and the microphone, mixes them in WebAudio, and uploads Opus every 15 seconds. A crashed tab or a closed laptop costs at most those 15 seconds. On stop, a detached worker converts the audio to 16 kHz mono, cuts it into ten-minute slices, sends each to a local OpenAI-compatible transcription endpoint, and writes a timestamped markdown transcript with any notes taken while listening.

It also takes a recording made some other way: a call app's own local recording, a phone voice memo, an old meeting. Same path from upload onward.

Shape

  • No port of its own. The page and its upload endpoints ride the gateway through panel + panelRoutes, so recordings inherit the dashboard session rather than needing their own auth. No entry in the port registry.
  • Bodies stream to disk. Audio arrives as application/octet-stream, which the gateway's global 1 MB JSON parser ignores, so a long upload never passes through memory.
  • Auth is path-scoped (router.use("/dashboard/meeting-recorder-api", authMiddleware)), per the mount-time check on unpathed router middleware.
  • Slice timestamps are offset back into meeting time before anything is written, so a two-hour transcript reads correctly end to end.

Requires

ffmpeg on the host, and a transcription endpoint. The faster-whisper-server bundle is the intended pairing and is the default (http://localhost:8004, CPU, int8, loopback). Browsers only hand over tab audio in a secure context, so the dashboard has to be reached over HTTPS or localhost; the panel says so on screen when it is not, rather than letting someone record silence.

Tested

Routes mounted on a bare Express app with a stub auth middleware, against a real faster-whisper server:

  • three separately uploaded chunks reassembled and transcribed correctly (71 words, identical to the single-chunk result)
  • an uploaded .m4a took the same path to the same output
  • session listing and status polling

Throughput measured at about 3.5x real time with large-v3 int8 on CPU, so a ninety-minute meeting finishes roughly twenty-five minutes after it ends.

Deliberately not included

Speaker labels (diarization is a second model), a live transcript (transcription starts on stop), and any automatic summary. Every transcript carries a line saying it is a machine transcript whose names get misheard, because confident-looking text invites quotation.

Kevin Hopper added 3 commits September 9, 2026 09:43
Records a meeting in the browser (shared tab audio plus microphone, mixed
in WebAudio, uploaded every 15 s) and transcribes it on the same host with
a local OpenAI-compatible endpoint, defaulting to the faster-whisper-server
bundle on loopback. No cloud call, no API key, no port of its own: the page
and its upload endpoints ride the gateway through panel + panelRoutes, so
recordings inherit the dashboard session.

- panel/meeting-recorder.js: capture UI, level meter and a silence warning
  so a silent recording surfaces in the first 20 seconds, notes box, recent
  recordings table, and an insecure-context warning (tab audio needs HTTPS)
- panel/routes.js: session/chunk/finish/upload/status/sessions, auth scoped
  under one path prefix, bodies streamed to disk rather than buffered
- server/transcribe.js: ten-minute slices with timestamps offset back into
  meeting time; a detached worker, so a long meeting survives a closed page
- server/store.js: session directories, id validation, listing
- also transcribes a recording made some other way (call-app recording,
  phone voice memo) through the same path

Measured about 3.5x real time with large-v3 int8 on CPU.
The panel registry copies panel/*.js into <crow-home>/panels/, which breaks
bundle-relative imports: a static "../server/store.js" resolves to
<crow-home>/server/store.js and the router never loads. Both files now try
the installed bundle path first (honouring CROW_HOME, so alternate instances
find their own copy) and fall back to the repo layout, the same pattern
knowledge-base-routes.js uses.

Verified end to end in both layouts against a real faster-whisper server:
chunked record, finish, detached worker, transcript written, panel rendered
with the session table.
@kh0pper
kh0pper merged commit e089ad4 into main Sep 9, 2026
3 checks passed
@kh0pper
kh0pper deleted the feat/meeting-recorder-bundle branch September 9, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant