/ concept-collection / commonroom-recorder
Sign in
concept-collection / commonroom-recorder
commonroom-recorder / README.md
166 lines · 7.6 KBPreviewCodeBlameHistoryRaw
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 inbox/ drop a file here to send its content to the
105 room as a chat message (see below)
106 AGENT.md instructions for a monitoring AI agent
107```
109Everything is written incrementally (`tail -f chat.txt` works live; the
110manifest is rewritten at every segment boundary and every 30 s), so a crash
111loses at most about a second of audio. A file only starts when a participant's
112first real audio arrives β€” someone who never unmutes produces no file. If a
113participant disconnects and returns, they get a new numbered segment; the
114manifest's per-segment start times let a transcript interleave speakers on one
115timeline. During a segment, silence is padded by wall clock, so a sample's
116position in the file always tracks elapsed time.
118The `transcribe` subcommand consumes exactly these files; because each WAV is
119silence-padded to track wall-clock time, an ASR timestamp within a segment
120plus the segment's `startedAt` is already the meeting timeline.
122## How it works
124The p2p layer is commonroom's, ported to Node: the same nostr
125presence/signaling topics (knowing the room name IS the key), the same
126schnorr-signed events (with a fresh ephemeral keypair per run), the same
127deterministic-initiator WebRTC mesh and control data channel (hello, mute
128notices, chat, bye). To the browsers in the room the recorder is
129indistinguishable from a participant whose mic and camera are muted β€” it
130counts toward the room cap of 8 and appears in the participant list.
132Two deliberate deviations from the browser client:
134- **Receive-only media.** The video m-line is negotiated `sendonly` from the
135 recorder's side (a placeholder track that never produces a frame), so no
136 video is ever sent to the recorder β€” with up to 7 participants that saves
137 several Mbit/s and all the decode CPU. Audio is symmetric (a silent
138 placeholder goes out, like any muted mic).
139- **Files instead of tiles.** Each remote audio track feeds an `RTCAudioSink`
140 whose PCM goes straight to an incrementally-written WAV.
142## Letting an AI agent join the conversation
144The recording directory doubles as an interface for an independent agent
145(or anything else) that monitors the meeting and occasionally says something:
147- **Follow** the meeting by re-reading `transcript.md` (with `--transcribe`),
148 `chat.txt`, or `events.jsonl` β€” they all grow live.
149- **Interject** by writing a file into `inbox/`: the file's whole content is
150 sent to the room as one chat message, appearing under the recorder's
151 display name, and the file is deleted once sent. Write atomically (create
152 as `*.tmp` or a dotfile, then rename); messages are capped at 2000
153 characters.
154- `AGENT.md`, written into every recording directory, contains ready-made
155 instructions for the agent β€” point it there. The default guidance: only
156 interject when it clearly helps, keep it to a sentence or two, and any
157 task-specific instructions it was given take precedence.
159## Testing
161`npm run test:loopback` runs an end-to-end test with no browser: it starts the
162recorder and a synthetic participant that "speaks" a 440 Hz sine and sends a
163chat message, then verifies the WAV really contains the tone and the chat made
164it to disk. It uses the real public nostr relays, so it needs network access.
165For a real-world test, run the recorder and join the same room at
166https://concept-collection.github.io/commonroom/ from a browser.
↑↓ move↡openescclose