/ proof-of-concept / commonroom-recorder
Sign in
proof-of-concept / commonroom-recorder
commonroom-recorder
Go to fileHistoryFork
.githubDocument npx URL caching; link versioned tarball on the landing page
srcAdd transcribe subcommand: merged speaker-attributed transcript
.gitignoreDistribute via npx from a GitHub Pages tarball
CLAUDE.mdAdd transcribe subcommand: merged speaker-attributed transcript
package-lock.jsonRecord commonroom audio and chat from the command line
package.jsonAdd transcribe subcommand: merged speaker-attributed transcript
README.mdDocument npx URL caching; link versioned tarball on the landing page
tsconfig.jsonRecord commonroom audio and chat from the command line

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 <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 <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

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#

npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz transcribe <recording-dir>

produces 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

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 sendonly from 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 RTCAudioSink whose PCM goes straight to an incrementally-written WAV.

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.

โ†‘โ†“ moveโ†ตopenescclose