# STT companion

Captures mic audio, runs voice-activity detection, transcribes each utterance
with [`faster-whisper`](https://github.com/SYSTRAN/faster-whisper), and pushes
final transcripts over a local WebSocket so the Slidev deck can render them
live. Audio is *always* recorded to disk independently of the transcription
pipeline — a live STT failure becomes a UI glitch, not lost content.

```
   [ mic ]
      │
      ▼
   [ main.py ]──► WAV  (recordings/talk-YYYYMMDD-HHMMSS.wav)
       │     └──► JSONL (recordings/talk-YYYYMMDD-HHMMSS.jsonl)
       │
       └─► ws://localhost:8787 ──► Slidev panel
```

## Install

System deps (PortAudio is the only non-pip thing you need):

```bash
# macOS
brew install portaudio

# Debian/Ubuntu
sudo apt install libportaudio2
```

Python deps in a venv:

```bash
cd companion
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

First run will download the Whisper model (a few hundred MB for `small`). Stored under `~/.cache/huggingface/`.

## Run

```bash
# Pick a mic
python main.py --list-mics

# Default run (small model, default mic)
python main.py

# Specify mic and bigger model
python main.py --device 2 --model medium

# Replay a previous WAV for testing without speaking
python main.py --replay recordings/talk-20260611-101500.wav
```

You should see lines like:

```
[boot] loading whisper model='small' compute_type='int8' device='cpu'
[boot] model loaded in 4.2s
[boot] warmed.
[mic] capturing from default at 16000 Hz, frame=30 ms
[ws] listening on ws://localhost:8787
[ready] speak into the mic — Ctrl+C to stop.
  ⇢ [   1.2s  +1.8s  stt= 220ms]  one of those things you can sometimes verify
  ⇢ [   3.4s  +2.5s  stt= 310ms]  but mostly you take on faith
```

## Verify the WebSocket without Slidev

Open `wsclient.html` in any browser. It connects to `ws://localhost:8787`,
prints incoming transcripts, and auto-reconnects if the companion restarts.
Useful for confirming the pipeline is alive before wiring up the deck.

## Choosing a model

Sized by Whisper's standard names. Latency assumes greedy decoding on CPU.

| Model      | RAM   | Latency (rel)  | Quality          |
|------------|-------|----------------|------------------|
| `tiny`     | ~75M  | ~0.2× realtime | acceptable       |
| `base`     | ~150M | ~0.3× realtime | decent           |
| `small`    | ~480M | ~0.6× realtime | good (default)   |
| `medium`   | ~1.5G | ~1.2× realtime | very good        |
| `large-v3` | ~3.1G | ~2.5× realtime | best, often slow |

For a single-speaker keynote, `small` or `medium` is the sweet spot. Bigger
models add accuracy on rare jargon and accents at the cost of latency.

On Apple Silicon with `--compute-type int8`, CPU mode is fast enough that
GPU/Metal isn't needed for `small`/`medium`. If you want CUDA on Linux:
`--device-type cuda --compute-type int8_float16`.

## Event format

The WebSocket emits one JSON object per *final* utterance:

```json
{
  "type": "final",
  "text": "and that's exactly what makes faking expensive.",
  "t_start": 312.450,
  "t_end":   315.180,
  "duration":  2.730,
  "inference_ms": 287,
  "wall_ts": "2026-06-11T14:32:55.123Z"
}
```

The same line is appended to the JSONL on disk. The WAV is the raw audio.
Those two files are everything you need for the signing step later.

## Tuning

- **VAD too eager (cuts utterances too fast):** `--vad 1` or even `0`.
- **VAD letting noise through:** `--vad 3`.
- **First word clipped:** raise `PREROLL_FRAMES` in `main.py`.
- **Utterances too long:** lower `MAX_UTTERANCE_FRAMES`.
- **Wrong language being transcribed:** `--language fr` (or whichever).

## Reliability checklist

The architecture already covers:

- WAV is always written, regardless of STT.
- JSONL is line-buffered + flushed on every event.
- WAV is flushed every ~3s.
- Transcription runs in a single-worker pool so slow utterances serialize
  instead of compounding latency.
- Late WS clients receive the last 10 events so the deck panel isn't empty
  if it connects after the talk has started.
- WebSocket clients can auto-reconnect on the deck side; the server tolerates
  drops and survivors keep receiving events.

What's still on you to verify before going on stage:

- The right mic is picked (`--list-mics` + `--device N`).
- Headphones not on — system mic input isn't muted/bluetooth-glitching.
- The deck's panel is actually pointed at `ws://localhost:8787`.
- The model is warmed (the `[boot] warmed.` line is the signal).
