# Output & Data Transcripts reach you three ways: human-readable stdout, machine-readable JSONL, and optional WAV clips. **Stdout** (human-readable): ``` [14:23:45] (es) Hola, cómo estás? [rms=0.041 end_of_turn] [14:23:52] (en) What time is it? [rms=0.038 end_of_turn] [14:24:10] (es) Gracias por tu ayuda. [rms=0.052 pause] ``` **JSONL** (one line per utterance; only written when `jsonl = true`): ```json {"ts": "2025-09-20T14:23:45.123456", "lang": "es", "lang_prob": 0.95, "duration_s": 2.5, "rms": 0.04123, "end_reason": "end_of_turn", "text": "Hola, cómo estás?", "transcribe_s": 0.42} {"ts": "2025-09-20T14:23:52.654321", "lang": "en", "lang_prob": 0.98, "duration_s": 1.8, "rms": 0.03811, "end_reason": "pause", "text": "What time is it?", "transcribe_s": 0.31} ``` `duration_s` is the length of the utterance audio; `transcribe_s` is the wall-clock time the worker spent transcribing it (inference latency), rounded to milliseconds. Their ratio is the real-time factor. The field is omitted if the timing was not measured (e.g. `emit()` called directly without `transcribe_s`). `end_reason` is one of `end_of_turn` (full `silence_ms` elapsed), `pause` (chunked at `chunk_pause_ms` because it passed `soft_max_s`), `hard_cap` (`max_utterance_s` reached), or `flush` (shutdown mid-utterance). **Speaker fields** — every JSONL record carries five additive, back-compatible speaker fields (always present; `null` when unknown): ```json {"ts": "2026-09-24T10:01:03.123456", "lang": "en", "lang_prob": 0.98, "duration_s": 3.2, "rms": 0.031, "end_reason": "pause", "text": "turn on the lights", "seg_id": 142, "speaker_id": "spk_0001", "speaker_status": "matched", "speaker": "Efren", "speaker_score": 0.71} ``` - `seg_id` — stable correlation key for the segment (int, `null` when unknown). - `speaker_id` — stable cluster key for cross-reference (e.g. `"spk_0001"`, `null` when unknown). - `speaker_status` — one of: - `matched` — best score >= `T_id` (threshold_id). - `uncertain` — `T_new <= score < T_id` (threshold_new <= score < threshold_id). - `unknown` — below `T_new` and no profile minted this utterance. - `none` — no embedding computed (model off, audio too short, or error). - `speaker` — display label: matched+named -> the name (`"Efren"`), matched+unnamed -> `"spk_0001"`, uncertain -> `"?"` (e.g. `"spk_0001?"`), unknown/none -> `"?"`. - `speaker_score` — the winning cosine similarity, rounded to 4 dp (`null` when unknown). Existing tooling that ignores unknown JSON keys keeps working unchanged. **Audio files** — when `save_audio = true`, each transcribed utterance is also written as a 16 kHz mono WAV under `audio_dir/YYYY-MM-DD/`, and the record gains an `"audio"` field with its path: ```json {..., "end_reason": "pause", "text": "Hola", "audio": "audio/2025-09-20/142345-123-pause.wav"} ``` Saving is best-effort: if a write fails (e.g. disk full), the transcript is still emitted and a warning is logged. > ⚠️ **Disk space** — saved audio is **never rotated or cleaned up** automatically. > At 16 kHz mono 16-bit it costs 32 KB/s, i.e. **~1.9 MB/min, ~115 MB/hour, > ~2.8 GB/day** of *continuous speech*. Silence is not written, so real usage is > lower — but an always-on daemon in a talkative room still accumulates quickly. > Point `audio_dir` at a volume with headroom and prune it yourself (e.g. a cron > job removing files older than N days), and leave `save_audio = false` unless you > actually need the clips for auditing. **Triage I/O dump (assistant-side)** — with `[triage].dump_io = true` the assistant writes one self-contained JSON per triage under `triage/YYYY-MM-DD/HHMMSS-mmm.json` (the Laya request/response plus the LLM request/response). It is separate from the daemon transcript output above, off by default, and best-effort. See [TRIAGE.md](TRIAGE.md#opt-in-io-dump-dump_io).