| .github | |
| src | |
| .gitignore | |
| CLAUDE.md | |
| package-lock.json | |
| package.json | |
| README.md | |
| tsconfig.json |
commonroom-recorder#
Record a commonroom call from the command line, for transcribing the meeting afterwards.
The recorder joins a room as an ordinary, visible participant (default name "Recorder") that stays muted the whole time. It receives every other participant's audio and writes one WAV file per participant — so a transcript with speaker attribution needs no diarization — plus the room chat and a machine-readable event log. By default it also posts a one-line chat notice to each participant so everyone knows the meeting is being recorded.
Usage#
No install needed — run it straight from this repo's GitHub Pages tarball (nothing is published to npm):
npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz record <room> [options]
(npx github:concept-collection/commonroom-recorder works too, but builds
from source on first run.) Note that npx caches the download per URL — after
a new release, use the versioned tarball listed at
the landing page
or rm -rf ~/.npm/_npx. Or from a clone:
npm install
npm run build
node dist/cli.js record <room> [options]
Options:
--name <name> Display name in the room (default: Recorder)
--out <dir> Output directory (default: ./recordings/<room>-<timestamp>)
--duration <sec> Stop automatically after this many seconds
--notice <text> Chat line sent to each participant on connect
(default: "🔴 This meeting is being recorded.")
--no-notice Don't send any recording notice
--transcribe Transcribe on the fly (needs faster-whisper): the
transcript grows in <out>/transcript.md during the meeting
--model <m> Whisper model for --transcribe (default: small)
--language <xx> Force a language (default: auto-detect)
Stop with Ctrl-C. Requires Node >= 22 (built-in WebSocket). The WebRTC stack
is @roamhq/wrtc, which
ships prebuilt binaries for Linux and macOS.
Transcribing#
With record --transcribe, transcription happens live during the meeting:
a single faster-whisper model stays loaded, audio is transcribed in chunks cut
at natural pauses, and <out>/transcript.md is continuously rewritten — open
it (or watch cat) while the meeting runs, and it is finalized the moment you
stop. Text typically appears within ~8 seconds of a pause (someone speaking
non-stop can lag up to ~30 s until the force-cut). If the transcriber ever
fails, the recording is unaffected.
Alternatively (or to redo a recording with a bigger model), transcribe afterwards:
npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz transcribe <recording-dir>
Both produce transcript.md — a merged, speaker-attributed transcript of the
meeting with the chat and join/left events interleaved on one timeline:
**[10:00:02] Alice:** So the agenda today...
> [10:00:15] 💬 **Alice:** here's the doc link
*[10:00:19] — Bob joined*
**[10:00:20] Bob:** Sorry I'm late...
(plus transcript.json with the same items, structured). Speech recognition
runs locally through whichever engine is found (--engine to force one):
faster-whisper
(pip install faster-whisper), whisper.cpp's whisper-cli (pass the ggml
model file via --model), or the
openai-whisper CLI. --model defaults to
small; --language forces a language instead of auto-detecting. Raw
per-file ASR output is cached in <dir>/asr/, so re-running is instant —
use --force to re-transcribe.
Output#
<out>/
audio/<name>-<peer8>-segN.wav one file per participant per connection
(48 kHz mono s16 PCM, typically)
chat.txt human-readable chat + join/left log
events.jsonl every event with ISO timestamps: join, left,
chat, mute/unmute, segment start/end
manifest.json session summary: room, participants,
segments with start/end times and durations
inbox/ drop a file here to send its content to the
room as a chat message (see below)
AGENT.md instructions for a monitoring AI agent
Everything is written incrementally (tail -f chat.txt works live; the
manifest is rewritten at every segment boundary and every 30 s), so a crash
loses at most about a second of audio. A file only starts when a participant's
first real audio arrives — someone who never unmutes produces no file. If a
participant disconnects and returns, they get a new numbered segment; the
manifest's per-segment start times let a transcript interleave speakers on one
timeline. During a segment, silence is padded by wall clock, so a sample's
position in the file always tracks elapsed time.
The transcribe subcommand consumes exactly these files; because each WAV is
silence-padded to track wall-clock time, an ASR timestamp within a segment
plus the segment's startedAt is already the meeting timeline.
How it works#
The p2p layer is commonroom's, ported to Node: the same nostr presence/signaling topics (knowing the room name IS the key), the same schnorr-signed events (with a fresh ephemeral keypair per run), the same deterministic-initiator WebRTC mesh and control data channel (hello, mute notices, chat, bye). To the browsers in the room the recorder is indistinguishable from a participant whose mic and camera are muted — it counts toward the room cap of 8 and appears in the participant list.
Two deliberate deviations from the browser client:
- Receive-only media. The video m-line is negotiated
sendonlyfrom the recorder's side (a placeholder track that never produces a frame), so no video is ever sent to the recorder — with up to 7 participants that saves several Mbit/s and all the decode CPU. Audio is symmetric (a silent placeholder goes out, like any muted mic). - Files instead of tiles. Each remote audio track feeds an
RTCAudioSinkwhose PCM goes straight to an incrementally-written WAV.
Letting an AI agent join the conversation#
The recording directory doubles as an interface for an independent agent (or anything else) that monitors the meeting and occasionally says something:
- Follow the meeting by re-reading
transcript.md(with--transcribe),chat.txt, orevents.jsonl— they all grow live. - Interject by writing a file into
inbox/: the file's whole content is sent to the room as one chat message, appearing under the recorder's display name, and the file is deleted once sent. Write atomically (create as*.tmpor a dotfile, then rename); messages are capped at 2000 characters. AGENT.md, written into every recording directory, contains ready-made instructions for the agent — point it there. The default guidance: only interject when it clearly helps, keep it to a sentence or two, and any task-specific instructions it was given take precedence.
Testing#
npm run test:loopback runs an end-to-end test with no browser: it starts the
recorder and a synthetic participant that "speaks" a 440 Hz sine and sends a
chat message, then verifies the WAV really contains the tone and the chat made
it to disk. It uses the real public nostr relays, so it needs network access.
For a real-world test, run the recorder and join the same room at
https://concept-collection.github.io/commonroom/ from a browser.