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
aaf4452Distribute via npx from a GitHub Pages tarballJeremy Magland 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 <room> [options]
20```
22(`npx github:concept-collection/commonroom-recorder` works too, but builds
23from source on first run.) Or from a clone:
26npm install
27npm run build
28node dist/cli.js <room> [options]
29```
31Options:
33```
34--name <name> Display name in the room (default: Recorder)
35--out <dir> Output directory (default: ./recordings/<room>-<timestamp>)
36--duration <sec> Stop automatically after this many seconds
37--notice <text> Chat line sent to each participant on connect
38 (default: "π΄ This meeting is being recorded.")
39--no-notice Don't send any recording notice
40```
42Stop with Ctrl-C. Requires Node >= 22 (built-in WebSocket). The WebRTC stack
43is [`@roamhq/wrtc`](https://github.com/WonderInventions/node-webrtc), which
44ships prebuilt binaries for Linux and macOS.
cbaa294Add transcribe subcommand: merged speaker-attributed transcriptJeremy Magland 46## Transcribing
48```
49npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz transcribe <recording-dir>
50```
52produces `transcript.md` β a merged, speaker-attributed transcript of the
53meeting with the chat and join/left events interleaved on one timeline:
55```
56**[10:00:02] Alice:** So the agenda today...
58> [10:00:15] π¬ **Alice:** here's the doc link
60*[10:00:19] β Bob joined*
62**[10:00:20] Bob:** Sorry I'm late...
63```
65(plus `transcript.json` with the same items, structured). Speech recognition
66runs locally through whichever engine is found (`--engine` to force one):
67[faster-whisper](https://github.com/SYSTRAN/faster-whisper)
68(`pip install faster-whisper`), whisper.cpp's `whisper-cli` (pass the ggml
69model file via `--model`), or the
70[openai-whisper](https://github.com/openai/whisper) CLI. `--model` defaults to
71`small`; `--language` forces a language instead of auto-detecting. Raw
72per-file ASR output is cached in `<dir>/asr/`, so re-running is instant β
73use `--force` to re-transcribe.
77```
78<out>/
79 audio/<name>-<peer8>-segN.wav one file per participant per connection
80 (48 kHz mono s16 PCM, typically)
81 chat.txt human-readable chat + join/left log
82 events.jsonl every event with ISO timestamps: join, left,
83 chat, mute/unmute, segment start/end
84 manifest.json session summary: room, participants,
85 segments with start/end times and durations
86```
88Everything is written incrementally (`tail -f chat.txt` works live; the
89manifest is rewritten at every segment boundary and every 30 s), so a crash
90loses at most about a second of audio. A file only starts when a participant's
91first real audio arrives β someone who never unmutes produces no file. If a
92participant disconnects and returns, they get a new numbered segment; the
93manifest's per-segment start times let a transcript interleave speakers on one
94timeline. During a segment, silence is padded by wall clock, so a sample's
95position in the file always tracks elapsed time.
cbaa294Add transcribe subcommand: merged speaker-attributed transcriptJeremy Magland 97The `transcribe` subcommand consumes exactly these files; because each WAV is
98silence-padded to track wall-clock time, an ASR timestamp within a segment
99plus the segment's `startedAt` is already the meeting timeline.
101## How it works
103The p2p layer is commonroom's, ported to Node: the same nostr
104presence/signaling topics (knowing the room name IS the key), the same
105schnorr-signed events (with a fresh ephemeral keypair per run), the same
106deterministic-initiator WebRTC mesh and control data channel (hello, mute
107notices, chat, bye). To the browsers in the room the recorder is
108indistinguishable from a participant whose mic and camera are muted β it
109counts toward the room cap of 8 and appears in the participant list.
111Two deliberate deviations from the browser client:
113- **Receive-only media.** The video m-line is negotiated `sendonly` from the
114 recorder's side (a placeholder track that never produces a frame), so no
115 video is ever sent to the recorder β with up to 7 participants that saves
116 several Mbit/s and all the decode CPU. Audio is symmetric (a silent
117 placeholder goes out, like any muted mic).
118- **Files instead of tiles.** Each remote audio track feeds an `RTCAudioSink`
119 whose PCM goes straight to an incrementally-written WAV.
121## Testing
123`npm run test:loopback` runs an end-to-end test with no browser: it starts the
124recorder and a synthetic participant that "speaks" a 440 Hz sine and sends a
125chat message, then verifies the WAV really contains the tone and the chat made
126it to disk. It uses the real public nostr relays, so it needs network access.
127For a real-world test, run the recorder and join the same room at
128https://concept-collection.github.io/commonroom/ from a browser.