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. Text typically appears within ~8 seconds of a pause (someone speaking
60non-stop can lag up to ~30 s until the force-cut). If the transcriber ever
61fails, the recording is unaffected.
63Alternatively (or to redo a recording with a bigger model), transcribe
64afterwards:
66```
67npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz transcribe <recording-dir>
68```
70Both produce `transcript.md` โ a merged, speaker-attributed transcript of the
71meeting with the chat and join/left events interleaved on one timeline:
73```
74**[10:00:02] Alice:** So the agenda today...
76> [10:00:15] ๐ฌ **Alice:** here's the doc link
78*[10:00:19] โ Bob joined*
80**[10:00:20] Bob:** Sorry I'm late...
81```
83(plus `transcript.json` with the same items, structured). Speech recognition
84runs locally through whichever engine is found (`--engine` to force one):
85[faster-whisper](https://github.com/SYSTRAN/faster-whisper)
86(`pip install faster-whisper`), whisper.cpp's `whisper-cli` (pass the ggml
87model file via `--model`), or the
88[openai-whisper](https://github.com/openai/whisper) CLI. `--model` defaults to
89`small`; `--language` forces a language instead of auto-detecting. Raw
90per-file ASR output is cached in `<dir>/asr/`, so re-running is instant โ
91use `--force` to re-transcribe.
93## Output
95```
96<out>/
97 audio/<name>-<peer8>-segN.wav one file per participant per connection
98 (48 kHz mono s16 PCM, typically)
99 chat.txt human-readable chat + join/left log
100 events.jsonl every event with ISO timestamps: join, left,
101 chat, mute/unmute, segment start/end
102 manifest.json session summary: room, participants,
103 segments with start/end times and durations
104```
106Everything is written incrementally (`tail -f chat.txt` works live; the
107manifest is rewritten at every segment boundary and every 30 s), so a crash
108loses at most about a second of audio. A file only starts when a participant's
109first real audio arrives โ someone who never unmutes produces no file. If a
110participant disconnects and returns, they get a new numbered segment; the
111manifest's per-segment start times let a transcript interleave speakers on one
112timeline. During a segment, silence is padded by wall clock, so a sample's
113position in the file always tracks elapsed time.
115The `transcribe` subcommand consumes exactly these files; because each WAV is
116silence-padded to track wall-clock time, an ASR timestamp within a segment
117plus the segment's `startedAt` is already the meeting timeline.
119## How it works
121The p2p layer is commonroom's, ported to Node: the same nostr
122presence/signaling topics (knowing the room name IS the key), the same
123schnorr-signed events (with a fresh ephemeral keypair per run), the same
124deterministic-initiator WebRTC mesh and control data channel (hello, mute
125notices, chat, bye). To the browsers in the room the recorder is
126indistinguishable from a participant whose mic and camera are muted โ it
127counts toward the room cap of 8 and appears in the participant list.
129Two deliberate deviations from the browser client:
131- **Receive-only media.** The video m-line is negotiated `sendonly` from the
132 recorder's side (a placeholder track that never produces a frame), so no
133 video is ever sent to the recorder โ with up to 7 participants that saves
134 several Mbit/s and all the decode CPU. Audio is symmetric (a silent
135 placeholder goes out, like any muted mic).
136- **Files instead of tiles.** Each remote audio track feeds an `RTCAudioSink`
137 whose PCM goes straight to an incrementally-written WAV.
139## Testing
141`npm run test:loopback` runs an end-to-end test with no browser: it starts the
142recorder and a synthetic participant that "speaks" a 440 Hz sine and sends a
143chat message, then verifies the WAV really contains the tone and the chat made
144it to disk. It uses the real public nostr relays, so it needs network access.
145For a real-world test, run the recorder and join the same room at
146https://concept-collection.github.io/commonroom/ from a browser.