1# commonroom-recorder
3Record a [commonroom](https://github.com/concept-collection/commonroom) call
4from the command line, for transcribing the meeting afterwards.
6The recorder joins a room as an ordinary, **visible** participant (default
7name "Recorder") that stays muted the whole time. It receives every other
8participant's audio and writes one WAV file per participant β so a transcript
9with speaker attribution needs no diarization β plus the room chat and a
10machine-readable event log. By default it also posts a one-line chat notice to
11each participant so everyone knows the meeting is being recorded.
13## Usage
15No install needed β run it straight from this repo's GitHub Pages tarball
16(nothing is published to npm):
18```
19npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz record <room> [options]
20```
22(`npx github:concept-collection/commonroom-recorder` works too, but builds
23from source on first run.) Note that npx caches the download per URL β after
24a new release, use the versioned tarball listed at
25[the landing page](https://concept-collection.github.io/commonroom-recorder/)
26or `rm -rf ~/.npm/_npx`. Or from a clone:
28```
29npm install
30npm run build
31node dist/cli.js record <room> [options]
32```
34Options:
36```
37--name <name> Display name in the room (default: Recorder)
38--out <dir> Output directory (default: ./recordings/<room>-<timestamp>)
39--duration <sec> Stop automatically after this many seconds
40--notice <text> Chat line sent to each participant on connect
41 (default: "π΄ This meeting is being recorded.")
42--no-notice Don't send any recording notice
43--transcribe Transcribe on the fly (needs faster-whisper): the
44 transcript grows in <out>/transcript.md during the meeting
45--model <m> Whisper model for --transcribe (default: small)
46--language <xx> Force a language (default: auto-detect)
47```
49Stop with Ctrl-C. Requires Node >= 22 (built-in WebSocket). The WebRTC stack
50is [`@roamhq/wrtc`](https://github.com/WonderInventions/node-webrtc), which
51ships prebuilt binaries for Linux and macOS.
53## Transcribing
55With `record --transcribe`, transcription happens **live during the meeting**:
56a single faster-whisper model stays loaded, audio is transcribed in chunks cut
57at natural pauses, and `<out>/transcript.md` is continuously rewritten β open
58it (or `watch cat`) while the meeting runs, and it is finalized the moment you
59stop. If the transcriber ever fails, the recording is unaffected.
61Alternatively (or to redo a recording with a bigger model), transcribe
62afterwards:
64```
65npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz transcribe <recording-dir>
66```
68Both produce `transcript.md` β a merged, speaker-attributed transcript of the
69meeting with the chat and join/left events interleaved on one timeline:
71```
72**[10:00:02] Alice:** So the agenda today...
74> [10:00:15] π¬ **Alice:** here's the doc link
76*[10:00:19] β Bob joined*
78**[10:00:20] Bob:** Sorry I'm late...
79```
81(plus `transcript.json` with the same items, structured). Speech recognition
82runs locally through whichever engine is found (`--engine` to force one):
83[faster-whisper](https://github.com/SYSTRAN/faster-whisper)
84(`pip install faster-whisper`), whisper.cpp's `whisper-cli` (pass the ggml
85model file via `--model`), or the
86[openai-whisper](https://github.com/openai/whisper) CLI. `--model` defaults to
87`small`; `--language` forces a language instead of auto-detecting. Raw
88per-file ASR output is cached in `<dir>/asr/`, so re-running is instant β
89use `--force` to re-transcribe.
91## Output
93```
94<out>/
95 audio/<name>-<peer8>-segN.wav one file per participant per connection
96 (48 kHz mono s16 PCM, typically)
97 chat.txt human-readable chat + join/left log
98 events.jsonl every event with ISO timestamps: join, left,
99 chat, mute/unmute, segment start/end
100 manifest.json session summary: room, participants,
101 segments with start/end times and durations
102```
104Everything is written incrementally (`tail -f chat.txt` works live; the
105manifest is rewritten at every segment boundary and every 30 s), so a crash
106loses at most about a second of audio. A file only starts when a participant's
107first real audio arrives β someone who never unmutes produces no file. If a
108participant disconnects and returns, they get a new numbered segment; the
109manifest's per-segment start times let a transcript interleave speakers on one
110timeline. During a segment, silence is padded by wall clock, so a sample's
111position in the file always tracks elapsed time.
113The `transcribe` subcommand consumes exactly these files; because each WAV is
114silence-padded to track wall-clock time, an ASR timestamp within a segment
115plus the segment's `startedAt` is already the meeting timeline.
117## How it works
119The p2p layer is commonroom's, ported to Node: the same nostr
120presence/signaling topics (knowing the room name IS the key), the same
121schnorr-signed events (with a fresh ephemeral keypair per run), the same
122deterministic-initiator WebRTC mesh and control data channel (hello, mute
123notices, chat, bye). To the browsers in the room the recorder is
124indistinguishable from a participant whose mic and camera are muted β it
125counts toward the room cap of 8 and appears in the participant list.
127Two deliberate deviations from the browser client:
129- **Receive-only media.** The video m-line is negotiated `sendonly` from the
130 recorder's side (a placeholder track that never produces a frame), so no
131 video is ever sent to the recorder β with up to 7 participants that saves
132 several Mbit/s and all the decode CPU. Audio is symmetric (a silent
133 placeholder goes out, like any muted mic).
134- **Files instead of tiles.** Each remote audio track feeds an `RTCAudioSink`
135 whose PCM goes straight to an incrementally-written WAV.
137## Testing
139`npm run test:loopback` runs an end-to-end test with no browser: it starts the
140recorder and a synthetic participant that "speaks" a 440 Hz sine and sends a
141chat message, then verifies the WAV really contains the tone and the chat made
142it to disk. It uses the real public nostr relays, so it needs network access.
143For a real-world test, run the recorder and join the same room at
144https://concept-collection.github.io/commonroom/ from a browser.