# Join the meeting-ASR team

**Handoff URL:** https://capital-region-meeting-asr.canalandcountry.workers.dev/join  ·  Markdown: https://capital-region-meeting-asr.canalandcountry.workers.dev/join.md  ·  Status: https://capital-region-meeting-asr.canalandcountry.workers.dev/status  ·  System map (live): https://capital-region-meeting-asr.canalandcountry.workers.dev/architecture

## Telemetry model
Telemetry is **Mac-pushed on events**: each Mac POSTs `/telemetry/event` at every lane transition, plus per-meeting `/telemetry` and a 10-minute heartbeat.
The Worker only renders what the Macs pushed; the status and system-map pages refresh by client-side fetch. No bot, routine or poller is involved.

## What this is
A small distributed team of Apple Silicon Macs that transcribes public municipal meeting videos (town, village, city,
county, school, planning and zoning boards) across the Capital Region, Rochester and Mid-Hudson regions of New York.
The Worker at https://capital-region-meeting-asr.canalandcountry.workers.dev holds the gap manifest (6,753 meetings), hands out work with leases, and stores
timestamped transcripts. Each Mac downloads audio, transcribes locally with mlx-whisper, and uploads JSON.
As of 2026-10-11T21:37:21.076Z: 1029 completed, 3 in progress, 105 failed, 5616 not started.

## Current team (from heartbeats)
- `m1-mini`: last heartbeat 2026-10-11T21:31:00Z (6 min ago), queue_depth 42, active leases 0
- `m1-mbp`: last heartbeat 2026-10-11T21:28:17Z (9 min ago), queue_depth 0, active leases 0
- `a18-neo`: last heartbeat 2026-10-10T22:46:12Z (1371 min ago, STALE), queue_depth 340, active leases 0

## Lane recipe
- ASR: `mlx-whisper` with `mlx-community/whisper-large-v3-turbo`, greedy decoding (temperature 0 with 0.2/0.4 fallback; mlx-whisper has no beam search), `word_timestamps=True`, `language="en"`, `condition_on_previous_text=False`.
- Audio prep: `ffmpeg` to 16 kHz mono PCM WAV, original timeline kept so time codes match the video.
- Downloads: `yt-dlp` or direct `ffmpeg` per venue (the claim response includes a `fetch` block with method, URL, Referer and UA hints):
  - Granicus: Referer + browser User-Agent (some archives still 403: release as blocked).
  - Vimeo: player URL (`https://player.vimeo.com/video/<id>`) with a Referer.
  - TelVue HLS, Swagit, Zoom shares, direct county mp4: direct (ffmpeg or yt-dlp).
- Prefetch: 3-4 parallel downloads keep 6-8 WAVs ready; decode lanes only read from the buffer. Delete each WAV after upload.

## Hardware guidance
- 16 GB M1: **2 decode lanes max** (one GPU; more lanes only slow each meeting and risk swap on 3-4 h meetings).
- More download prefetch is fine (3-4). Keep ~15 GB free disk for the buffer.
- Keep the Mac on power with the lid open; run under `caffeinate`.

## YouTube policy
All hosts share one home IP. **Only hosts the operator started with `--allow-youtube` download YouTube** (see each host's role on /architecture). New hosts default to
non-YouTube venues (`exclude_venues: ["youtube"]`). Do not enable YouTube unless the operator says so on the log.

## Getting the telemetry token
The token is **never** published here. Copy `~/.secrets/capreg-telemetry-token` from the existing team Mac to the same path
on the new Mac (AirDrop, or `scp` over the local network), then `chmod 600 ~/.secrets/capreg-telemetry-token`. Never print, paste or log it.

## Bootstrap (one line)
```
curl -fsSL https://capital-region-meeting-asr.canalandcountry.workers.dev/join/bootstrap.sh | bash
```
Installs Homebrew packages (ffmpeg, yt-dlp, python@3.11) if missing, creates `~/capital-region-asr/joiner` with a venv and
mlx-whisper, downloads `https://capital-region-meeting-asr.canalandcountry.workers.dev/join/agent.py`, checks the token file, and runs a self-test (dry-run claim that takes no lease,
plus a 60 s download and decode that is not uploaded). Set `CAPREG_HOST_LABEL` to change the label (default `m1-mbp`).

Start:
```
cd ~/capital-region-asr/joiner && caffeinate -dimsu ./venv/bin/python agent.py --host-label m1-mbp
```

## API contract (Bearer token = telemetry token)
- `POST /work/claim` `{host_label, region?, venues?, exclude_venues?, body_priority?: "governing-first", include_failed?, dry_run?}`
  → `{row: {municipality_id, meeting_id, meeting_date, region, venue, body_type, source_url, fetch, telemetry_source, transcript_put_path} | null, lease: {lease_id, host_label, expires_at} | null, remaining_eligible}`.
  Returns the next not-started, unleased row (governing bodies first) and leases it for 3 h. `dry_run: true` (or `?dry_run=1`) previews without leasing.
- `POST /work/release` `{municipality_id, meeting_id, host_label, reason, status: "blocked" | "retry"}`. `blocked` hides the row from *your host* for 24 h (another host may still take it); `retry` returns it to the pool.
- `POST /telemetry` `{municipality_id, meeting_id, meeting_date, status: transcribing|transcribed|failed, source: youtube|town_mp4, ts, audio_minutes?, word_count?, asr_engine?}`.
- `PUT /telemetry/transcript/<county>/<municipality>/<meeting_id>` (application/json) `{text, segments:[{start,end,text,words?:[{w,start,end,p}]}], asr_engine, audio_minutes, word_count, qa?, pipeline?}`. Clears your lease.
- `POST /telemetry/event` (one POST at **every lane transition**) `{host_label, lane, event: "claimed"|"download_start"|"download_done"|"transcribe_start"|"transcribe_done"|"upload_done"|"blocked"|"idle", municipality_id, meeting_id, ts, audio_minutes?, word_count?, detail?}`.
  `lane` is a short id: decode lanes `1`, `2`; download slots `dl1`..`dl4`. `municipality_id`/`meeting_id` may be omitted only for `idle`. This drives the live system map and the per-host rates.
- `POST /telemetry/heartbeat` `{host_label, queue_depth, ts, lanes?: [{lane, municipality_id, meeting_id, state: "claimed"|"downloading"|"downloaded"|"transcribing"|"uploading"|"blocked"|"idle", since}]}` every 10 min. `lanes` is optional and backward compatible (fallback when events are missed).
- Public, read-only: `GET /work/leases`, `GET /status.json`, `GET /log.json?since=<id>`, `GET /manifest/gaps.json?region=...`.

Order per meeting: claim (event `claimed`) → download (`download_start`, `download_done`) → telemetry `transcribing` + event `transcribe_start` → decode (`transcribe_done`) → PUT transcript → telemetry `transcribed` + event `upload_done`. Lane goes `idle` when it has nothing.
Download wall / 403 / login → release `blocked` with the reason; do not mark the row failed.

## Feedback and questions
`POST /log` with the Bearer token: `{kind: "note"|"question"|"blocker", message: "[m1-mbp] ...", municipality_id?, meeting_id?}`.
Always start the message with your host label. The log is public: no tokens, secrets or personal names.

## Etiquette
- Heartbeat every 10 min; read `/log.json` every 10 min.
- Act on `progops` blockers/questions addressed to your host label or to "all hosts", and answer with `kind: "answer"`, `reply_to: <id>`.
- Never upload to the R2 bucket directly (it triggers paid Worker Whisper). Never invent transcript text or timestamps.

## Stop and leave cleanly
Press Ctrl-C once: the agent releases its leases (`status: "retry"`) and deletes partial audio. If a host dies, its leases expire after 3 h.
To leave the team for good, stop the agent and post a `note` on the log saying so.
