Annotated reference for every stt-daemon configuration key; config.example.toml ships the values only.
Copy the example config and edit as needed:
cp config.example.toml config.toml
# Edit config.toml to adjust settings (see section below)
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."
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.
For audio source selection, virtual sink creation, and audio routing, see AUDIO-SETUP.md.