Transcripts reach you three ways: human-readable stdout, machine-readable JSONL, and optional WAV clips.
Each conversation writes to its own folder under session_dir (default transcripts/), named
<YYYY-MM-DD-HH-MM-SS> after its first transcription. The transcript log, saved WAVs, the optional
brief, and TUI screenshots all live inside it. A new folder starts once session_gap_min minutes
pass without speech, or on a fresh process start.
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, at
<session_dir>/<id>/transcripts.jsonl):
{"ts": "2025-09-20T14:23:45.123456", "session_id": "2025-09-20-14-23-45", "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", "session_id": "2025-09-20-14-23-45", "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}
session_id names the conversation folder (<session_dir>/<session_id>/) so records can be
correlated back to their transcript log and audio. Records from one conversation all carry the same
value; a new folder gets a new one.
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):
{"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 -> "<top1>?" (e.g. "spk_0001?"),
unknown/none -> "?".speaker_score — the winning cosine similarity, rounded to 4 dp (null when unknown).Note: "?" above is the stored JSONL speaker value. The assistant resolves the same turn
to the literal "Unknown" before relaying it to viewers, so the TUI header shows Unknown
(bold red) where the JSONL record holds "?".
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 <session_dir>/<id>/audio/, and the record gains
an "audio" field with its path:
{..., "end_reason": "pause", "text": "Hola", "audio": "transcripts/2025-09-20-14-23-45/audio/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
session_dirat a volume with headroom and prune it yourself (e.g. a cron job removing files older than N days), and leavesave_audio = falseunless 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 troubleshoot/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.