/ concept-collection / commonroom-recorder
Sign in
concept-collection / commonroom-recorder
commonroom-recorder / CLAUDE.md
81 lines · 4.8 KBPreviewCodeBlameHistoryRaw
1# CLAUDE.md
3Tips for future agents working in this repo — a Node CLI that joins a
4[commonroom](https://github.com/concept-collection/commonroom) call as a
5muted, visible participant and records every other participant's audio (one
6WAV per participant) plus the room chat. Read commonroom's CLAUDE.md first:
7this tool speaks its protocol verbatim, and the protocol is documented there.
9## Architecture
11```
12src/
13 identity.ts ported from commonroom; EPHEMERAL keypair (no localStorage)
14 nostr.ts ported near-verbatim (Node >= 22 global WebSocket); close() added
15 peer.ts ported, adapted to @roamhq/wrtc; receive-only media (see below)
16 wav.ts incremental WAV writer (buffers ~1 s, re-patches header sizes)
17 recorder.ts the heart: presence, mesh, control channel, audio sinks, files
18 transcribe.ts `transcribe` subcommand: per-WAV ASR + merged transcript.md
19 cli.ts arg parsing, subcommand dispatch, signal handling, log lines
20 test/
21 speaker.ts synthetic participant: sine tone + one chat message
22 loopback.ts test: recorder + speaker in a random room -> tone + chat
23 transcribe-test.ts test: fabricated 2-speaker recording dir (JFK sample)
24 -> transcript ordering + words; uses --model tiny
25```
27## Key design decisions
29- **Protocol-identical participant.** Same announcements, per-peer signaling
30 topics, deterministic initiator (smaller peer ID), control-channel messages
31 (hello/mute/chat/bye), stalled-connection retry, room-full handling, and the
32 soft cap of 8 — the recorder counts toward it. Don't invent protocol; if the
33 browser client changes, port the change.
34- **Receive-only media, but symmetric-looking.** Outgoing tracks are wrtc
35 `RTCAudioSource`/`RTCVideoSource` placeholders that never produce data (=
36 a fully muted participant). The VIDEO m-line is negotiated `sendonly` from
37 our side (as initiator via `addTransceiver(track, {direction:'sendonly'})`;
38 as answerer by flipping the transceiver direction after
39 setRemoteDescription) so browsers never send us video — saves bandwidth and
40 decode CPU. Audio stays sendrecv.
41- **Recording gates on first non-zero frame.** Before the first RTP packet,
42 `RTCAudioSink` delivers all-zero frames at a PROVISIONAL sample rate (16 kHz
43 observed), then switches to the real one (48 kHz) — opening the file eagerly
44 yields junk stub segments. A never-unmuting participant produces no file. A
45 mid-stream format change (rare) closes the segment and starts a new one.
46- **Wall-clock silence padding.** If the sink stalls > 1 s (network gap, DTX),
47 silence is inserted so sample position keeps tracking elapsed time — the
48 manifest's segment `startedAt` plus the file offset IS the meeting timeline.
49- **connectionState flaps.** wrtc can pass through 'connected' several times
50 while ICE settles; the connect handler must be idempotent or hello/notice
51 get re-sent per flap.
52- **Bye cooldown (3 s).** An announcement published just before a peer's bye
53 can arrive just after it and would trigger an instant reconnect (and a stub
54 recording); after a bye we ignore that peer's announcements briefly.
55- **Every exit path MUST end in `process.exit()`.** @roamhq/wrtc segfaults in
56 its static destructors on a natural process exit whenever nonstandard
57 media sources exist. The CLI, the speaker, and any future script that
58 touches wrtc must exit explicitly.
59- **Crash-safe outputs.** events.jsonl and chat.txt are appended per event;
60 WAVs flush (with header re-patch) about once a second; manifest.json is
61 written atomically (tmp + rename) at segment boundaries and every 30 s.
62- **Transcription needs no alignment step.** The silence padding means an ASR
63 timestamp within a segment plus the manifest `startedAt` is the wall-clock
64 time; transcribe.ts just merges utterances with chat/join/left events and
65 groups adjacent same-speaker utterances (< 3 s gap) into turns. ASR engines
66 are probed (faster-whisper via an embedded python3 stdin script — VAD on,
67 which also skips the padded silence — then whisper-cli, then whisper);
68 raw ASR is cached per WAV in `<dir>/asr/`. cli.ts imports recorder.js
69 LAZILY so transcribe works where the wrtc native module doesn't load.
71## Testing
73`npm run build && npm run test:loopback` — full end-to-end over the real
74public relays (needs network): asserts the recorded WAV contains the 440 Hz
75tone (RMS + zero-crossing rate) and the chat message landed exactly once.
76`npm run test:transcribe` — real-speech transcription test (downloads the
77whisper.cpp JFK sample + the tiny model on first run).
78Segfault-at-exit in a child process = some path bypassed `process.exit()`.
79For manual testing against real browsers, record a room and join it at
80https://concept-collection.github.io/commonroom/ — let the user do
81multi-person tests; don't try to automate browser media.
moveopenescclose