OUTPUT.md 3.8 KB

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):

{"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):

{"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).

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:

{..., "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.