CONFIGURATION.md 12 KB

Configuration

Annotated reference for every stt-daemon configuration key; config.example.toml ships the values only.

Quick Start

Copy the example config and edit as needed:

cp config.example.toml config.toml
# Edit config.toml to adjust settings (see section below)

Configuration Reference

The config file is TOML. Here are all available options with defaults. config.example.toml ships values only; this section is the annotated reference.

[audio]
backend = "parec"                     # "parec" (PulseAudio by name) | "sounddevice"
device  = "VirtualMicSink.monitor"    # PulseAudio source name (parec) or device index/name (sounddevice)
sample_rate = 16000                   # Hz
channels    = 1                       # Mono only
frame_ms    = 30                      # webrtcvad requires 10, 20, or 30
gain        = 1.0                     # Amplitude scaling (1.0 = no change, >1 = louder)

[vad]
aggressiveness   = 2                  # 0–3: higher = fewer false positives (speech misses more)
pre_roll_ms      = 400                # Ring buffer duration: keep this many ms of pre-speech
speech_start_ms  = 90                 # Consecutive speech frames to confirm speech start
silence_ms       = 700                # Consecutive silence to end a turn
min_utterance_ms = 300                # Discard utterances with less actual speech than this
max_utterance_s  = 30.0               # Hard cap: force-finalize a very long utterance
chunk_pause_ms   = 350                # Once soft_max_s is reached, end a chunk at a pause this long
soft_max_s       = 10.0               # After this much speech, chunk-ends become eligible at pauses
rms_floor        = 0.008              # Drop segments quieter than this RMS (noise-triggered blips)
trailing_trim_ms = 200                # Keep only this much trailing silence (less hallucination-prone padding)

[whisper]
backend                 = "faster-whisper"     # "faster-whisper" (CTranslate2) | "pywhispercpp" (whisper.cpp; see docs/PYWHISPERCPP.md)
model                   = "small"               # "tiny"/"base"/"small"/"medium"/"large" (multilingual; do NOT use `.en` variants)
device                  = "cpu"                 # "cpu" or "cuda"
compute_type            = "int8"                # "int8" (CPU) or "float16" (CUDA, slower but more accurate)
language                = "auto"                # "auto" (detect), "en", "es", or None/""/"none"
beam_size               = 1                     # 1 = fast greedy, >1 = slower beam search
vad_filter              = false                 # Use Whisper's built-in VAD (we already have webrtcvad)
cpu_threads             = 0                     # CPU inference threads (0 = CTranslate2's automatic choice).
                                                # On many-core CPUs, set this to the physical core count
                                                # (e.g. 16 on a Ryzen 9 8945HX) - measurably faster than auto.
no_speech_prob_max      = 0.6                   # Drop segments Whisper scores as non-speech above this
avg_logprob_min         = -1.0                  # Drop segments with confidence below this
language_strategy       = "static"              # "static" (detect per utterance) | "sticky" (detect once, pin)
sticky_redetect_logprob = -0.8                  # Unpin and re-detect if segment avg_logprob falls below this.
                                                # Segments below avg_logprob_min (-1.0) are dropped; between
                                                # -1.0 and -0.8 they are accepted but trigger re-detection.
                                                # No accepted segments also clears the pin for re-detection.

[sink]                                # Virtual audio sink configuration
sink_name             = "VirtualMicSink"       # null-sink name
remap_source_name     = "VirtualMic"           # Microphone source name
loopback_latency_msec = 1                      # Module latency (ms)
monitor_output        = false                  # Loop sink.monitor → default sink (OFF = no feedback risk)

[output]
stdout     = true                     # Print transcripts to stdout
jsonl      = false                    # Persist transcripts to JSONL files (off by default for agent-only use)
jsonl_dir  = "transcripts"            # JSONL destination (one file per day); only used if jsonl = true
save_audio = false                    # Also write each transcribed utterance as a 16 kHz mono WAV
audio_dir  = "audio"                  # WAV destination (YYYY-MM-DD subfolders); only used if save_audio

[daemon]
queue_size      = 50                        # Max utterances queued for transcription (oldest dropped when full)
backlog_warn_at = 5                         # Warn when the queue backs up this far (worker falling behind)

[events]                                    # Publish-only event bus
socket       = ""                           # Default: $XDG_RUNTIME_DIR/stt-daemon.sock
heartbeat_ms = 1000                         # Heartbeat interval (progress-driven, from the capture loop)
ring_size    = 200                          # Recent events replayed to a newly attached subscriber

[assistant]                                 # Controller process
control_socket        = ""                  # Default: $XDG_RUNTIME_DIR/stt-assistant.sock
stall_timeout_ms      = 3000                # No heartbeat for this long -> STT considered offline
worker_stall_ms       = 15000               # Heartbeats arriving but a transcription runs this long -> degraded
command_arm_timeout_s = 10.0                # Disarm a command if no turn is captured in this time
chat_idle_timeout_s   = 300.0               # Leave chat mode after this much silence
turn_gap_s            = 1.5                 # Quiet gap that ends a turn
require_viewer        = true                # Refuse to execute commands with no TUI attached
chat_exit_phrases     = ["end conversation", "that's all", "stop listening"]

[audit]                                     # Executed actions only (commands/chats are volatile)
enabled = true                              # audit/YYYY-MM-DD.jsonl
dir     = "audit"

[llm]                                       # local llama.cpp (OpenAI-compatible)
enabled                  = true
endpoint                 = "http://localhost:9931/v1/chat/completions"
temperature              = 0.7
max_tokens               = 1024             # max output per request; reasoning models spend tokens before the answer, so keep generous
timeout_s                = 120.0
cache_prompt             = true             # reuse the KV prefix (append-only history keeps it warm)
max_context_tokens       = 131072           # the server's --ctx-size (llama.cpp `GET /props` -> n_ctx); the prompt budget derives from it
compaction_safety_margin = 256              # tokens reserved for the token estimate error + template/tool overhead
compaction_enabled       = true             # summarize old turns instead of deleting them
compaction_trigger_ratio = 0.8              # compact when the prompt reaches this fraction of the budget
compaction_keep_turns    = 6                # recent turns kept verbatim
compaction_max_tokens    = 512              # summary length cap
compaction_timeout_s     = 30.0             # summarizer timeout, so a compaction cannot hold the inference lane
compaction_input_tokens  = 8192             # summarizer input cap before chunk-and-fold
compaction_system_prompt = "You compress a conversation. Preserve names, numbers, decisions, user preferences, and unresolved questions. Omit pleasantries. Output only the summary."
inference_queue_size     = 8                # bounded single-lane inference queue
trim_block_turns         = 4                # fallback block trimming, not per-turn, to limit re-prefill
max_tool_iterations      = 3
system_prompt_chat       = "You are a concise voice assistant. Reply in 1-3 sentences."
system_prompt_command    = "You translate a spoken request into a single tool call when one applies."
triage_system_prompt     = "You extract structured signals from a spoken conversation. Output only a JSON object with exactly these keys: {\"summary\": str, \"opportunities\": [str], \"doubts\": [str], \"action_items\": [{\"what\": str, \"why\": str}], \"risks\": [str]}. Output no prose and no markdown fences. Use empty lists when a category is empty."

[triage]                                    # Display-only signal panel (Laya lane; no socket, no extra process)
enabled              = false                # Master gate: false = no lane, no panel, behavior identical to today
questions_file       = "questions.json"     # Laya questions JSON in the cwd; missing/invalid -> no_questions
window               = 500                    # Utterances available per classification; the char budget is the real limiter
state_char_budget    = 3000                 # Char cap (~750 tokens) for the condensed state sent to Laya
predict_max_tokens   = 1024                 # Max tokens sent to Laya; the multilingual checkpoint supports 1024; state shares this budget with the ~192-token question head (~830-token state room)
confidence_field     = "answer_confidence"  # Gating confidence: "answer_confidence" (calibrated max(p)) | "confidence"
escalate_threshold   = 0.6                  # noul answers at or above this p escalate to extraction
choice_min_confidence = 0.85                # choice answers below this confidence escalate
extract_enabled      = true                 # Cascade to the LLM JSON extraction when signals escalate
extract_max_tokens   = 512                  # Extraction reply token cap
extract_timeout_s    = 30.0                 # Extraction request timeout
device               = ""                   # "" = auto -> None (Laya picks the multilingual checkpoint's default device)
preload              = ["multilingual"]     # Fixed default: only the multilingual checkpoint is preloaded
default_checkpoint   = "multilingual"       # Router default; multilingual is pinned for every call

backend picks the transcription engine. faster-whisper is the default, so nothing changes unless you opt in. pywhispercpp selects the whisper.cpp backend (GPU via the vendored CUDA wheel; see PYWHISPERCPP.md). model, language, beam_size, cpu_threads, no_speech_prob_max, avg_logprob_min, language_strategy, and sticky_redetect_logprob apply to both; device and compute_type apply only to faster-whisper; vad_filter is ignored (with a warning) on pywhispercpp. For pywhispercpp the ggml quantization is chosen by the model name (e.g. small, small-q5_1).

max_prompt_tokens was removed: the prompt budget is derived as max_context_tokens - max_tokens - compaction_safety_margin, so the window and the trim budget can never disagree. A legacy max_prompt_tokens key is ignored with a warning.

[triage] adds a display-only signal panel. On each settled turn in any mode, independent of chat, the assistant classifies the last window transcribed utterances (from the STT stream, not the chat history) with a local Laya multilingual checkpoint on a dedicated in-process lane, then, when signals escalate, asks the [llm] lane for a structured JSON extraction (extract_enabled). device = "" means auto: it maps to None, so Laya picks the default device for the multilingual checkpoint. preload = ["multilingual"] is the fixed default and default_checkpoint = "multilingual"; the multilingual checkpoint is pinned for every call (there is no .en English-only variant). predict_max_tokens is the Laya token budget (the multilingual checkpoint supports 1024); the condensed state shares it with the ~192-token question head, leaving ~830 tokens of room for state at the default. confidence_field selects which of Laya's two confidence values gates escalation, with answer_confidence (the calibrated max(p)) as the default. With enabled = false (the default) the lane is never built and the system behaves exactly as before.

[llm].triage_system_prompt is the strict JSON-only instruction used for the extraction cascade; it must return {summary, opportunities[], doubts[], action_items[{what, why}], risks[]} and nothing else.

Upgrading from a v5-draft config: socket, model, predict_timeout_s, checkpoint, subfolder, and max_len are not [triage] keys and are silently ignored by load_config (per-field .get()). checkpoint is superseded by default_checkpoint; the others have no successor. The 8192-token long-document max_len capability is out of scope for this feature.

For audio source selection, virtual sink creation, and audio routing, see AUDIO-SETUP.md.