# commonroom-recorder Record a [commonroom](https://github.com/concept-collection/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 record [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](https://concept-collection.github.io/commonroom-recorder/) or `rm -rf ~/.npm/_npx`. Or from a clone: ``` npm install npm run build node dist/cli.js record [options] ``` Options: ``` --name Display name in the room (default: Recorder) --out Output directory (default: ./recordings/-) --duration Stop automatically after this many seconds --notice Chat line sent to each participant on connect (default: "🔴 This meeting is being recorded.") --no-notice Don't send any recording notice --transcribe Transcribe on the fly (needs faster-whisper): the transcript grows in /transcript.md during the meeting --model Whisper model for --transcribe (default: small) --language Force a language (default: auto-detect) ``` Stop with Ctrl-C. Requires Node >= 22 (built-in WebSocket). The WebRTC stack is [`@roamhq/wrtc`](https://github.com/WonderInventions/node-webrtc), which ships prebuilt binaries for Linux and macOS. ## Transcribing With `record --transcribe`, transcription happens **live during the meeting**: a single faster-whisper model stays loaded, audio is transcribed in chunks cut at natural pauses, and `/transcript.md` is continuously rewritten — open it (or `watch cat`) while the meeting runs, and it is finalized the moment you stop. Text typically appears within ~8 seconds of a pause (someone speaking non-stop can lag up to ~30 s until the force-cut). If the transcriber ever fails, the recording is unaffected. Alternatively (or to redo a recording with a bigger model), transcribe afterwards: ``` npx https://concept-collection.github.io/commonroom-recorder/commonroom-recorder.tgz transcribe ``` Both produce `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](https://github.com/SYSTRAN/faster-whisper) (`pip install faster-whisper`), whisper.cpp's `whisper-cli` (pass the ggml model file via `--model`), or the [openai-whisper](https://github.com/openai/whisper) CLI. `--model` defaults to `small`; `--language` forces a language instead of auto-detecting. Raw per-file ASR output is cached in `/asr/`, so re-running is instant — use `--force` to re-transcribe. ## Output ``` / audio/--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 inbox/ drop a file here to send its content to the room as a chat message (see below) AGENT.md instructions for a monitoring AI agent ``` 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. ## Letting an AI agent join the conversation The recording directory doubles as an interface for an independent agent (or anything else) that monitors the meeting and occasionally says something: - **Follow** the meeting by re-reading `transcript.md` (with `--transcribe`), `chat.txt`, or `events.jsonl` — they all grow live. - **Interject** by writing a file into `inbox/`: the file's whole content is sent to the room as one chat message, appearing under the recorder's display name, and the file is deleted once sent. Write atomically (create as `*.tmp` or a dotfile, then rename); messages are capped at 2000 characters. - `AGENT.md`, written into every recording directory, contains ready-made instructions for the agent — point it there. The default guidance: only interject when it clearly helps, keep it to a sentence or two, and any task-specific instructions it was given take precedence. ## 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.