Common failure modes and their fixes.
Error: Device 'VirtualMicSink.monitor' not found. Available sources: [...]
To create VirtualMicSink, run: scripts/audio-sink.sh start
Fix:
uv run stt-daemon setup
Then route audio into VirtualMicSink and try again. (scripts/audio-sink.sh start is a thin
wrapper over the same subcommand.)
Cause: Textual enables terminal mouse reporting, so mouse events are consumed by the app instead of the terminal, and native drag-selection never starts.
Fix: hold Shift while dragging to force your terminal's native selection, then copy
as usual. (Alternatively, launching the app with mouse support disabled —
app.run(mouse=False) — restores plain selection but loses in-app scrolling/clicks.)
By default, the daemon uses compute_type = "int8" and device = "cpu", which trades speed for accuracy. To speed up:
cpu_threads to the physical core count: cpu_threads = 16 in [whisper]
(CTranslate2's automatic choice is often slower than an explicit count on many-core CPUs)--model base or --model tiny (the CLI --model is a valid override;
--device is not, it selects the audio capture device)[whisper] in config.toml and set device = "cuda" (or "auto") with
compute_type = "float16". These are config keys, not CLI flags: --device overrides the audio
capture device and --backend chooses parec/sounddevice.beam_size = 1 (already default, very fast)Example:
# config.toml
[whisper]
device = "auto" # or "cuda"; "cpu" on a machine without a GPU
compute_type = "float16" # "int8" for CPU
uv run stt-daemon --model base
A CUDA-only CTranslate2 wheel does not fail loudly on a ROCm host: it loads and silently runs on
CPU, so transcription keeps working but stays slow. The transcriber now logs the resolved device at
startup and warns when device = "auto" or "cuda" resolves to CPU while CTranslate2 reports no
GPU device.
Fix: install the ROCm stack after uv sync, then confirm the resolved device:
scripts/install-rocm.sh
.venv/bin/stt-daemon --check # not `uv run`: it syncs the venv and reverts the wheels
Re-run scripts/install-rocm.sh after any later uv sync, or after a plain uv run (it syncs
the venv implicitly), which puts the CUDA wheels back. Run the test suite as
.venv/bin/python -m pytest on this host so it does not re-sync.
For scale, a 25.9 s Spanish utterance with large-v3 and beam_size = 1 measured RTF ~0.37-0.43
on CPU (auto language) versus ~0.08-0.11 on the R9700, and a 12-utterance mixed Spanish/English run
auto-detected correctly on ROCm once the allocator was configured.
This warning means the single transcription worker is falling behind: utterances are arriving faster than they are being transcribed, and once the queue is full the oldest ones get dropped. The worker is serial by design (faster-whisper's model isn't thread-safe), so fix it by making transcription faster rather than by queueing more:
--model base or tiny[whisper] in config.toml: device = "cuda" (or "auto") and
compute_type = "float16". There is no Whisper-level --device flag.backlog_warn_at or queue_size in [daemon] only to make the
warning/dropping less likely — it hides the symptom, not the cause.The Faster-Whisper model is multilingual by default. It auto-detects language or can be forced:
uv run stt-daemon --language es # Force Spanish
uv run stt-daemon --language en # Force English
uv run stt-daemon --language auto # Auto-detect (default)
The output includes the detected language tag: [14:23:45] (es) ....
--language es or --language en--model medium or --model large (slower, more accurate)If parec seems stuck:
pactl list short sourcessystemctl status pulseaudio or check journalctlsystemctl restart pulseaudioIf using --backend sounddevice and getting import errors:
uv sync --extra portaudio
Then verify available devices:
python -c "import sounddevice as sd; print(sd.query_devices())"
Use the device index or name as --device 0 or --device "Microphone (...).
uv run stt-daemon -v (verbose logging)uv run stt-daemon --check (check RMS)aggressiveness (0–1 instead of 2–3) to detect quieter speechmin_utterance_ms isn't too high (default 300ms should be fine)Error loading model: ...
The model is downloaded on first run (~1.5 GB for small). Check:
df -h (needs ~2 GB free)~/.cache/huggingface/hub/ by defaultYou can override the cache:
export HF_HOME=/path/to/cache
uv run stt-daemon --model small
With [debug] enabled = true, the debug log is the first place to look — it is the only
structured record (child stdout/stderr is discarded, so there is no raw stderr complement).
Each line is one JSON object in troubleshoot/debug/YYYY-MM-DD.jsonl; no transcript text is stored.
tail -n 100 troubleshoot/debug/$(date +%F).jsonl
Read the tail in this order:
device_ok: false means the capture device went away — check pactl list short
sources, restart PulseAudio, and re-run (see "Audio device not responding" above).worker_stall_s: compare against [assistant] worker_stall_ms (default 15000).
A rising stall with a growing queue_depth means transcription is falling behind —
use a smaller model or GPU (see "Transcription backlog" above). A worker-stall
signal line marks the threshold crossing.reopen lines: repeated reopen attempt/fail lines mean the source keeps
dropping; fix the device rather than raising queue sizes.uv run stt-daemon up --with-tui and check which child exited first.queue-full / eviction-drop / llm-timeout / triage-brief-failure:
backpressure or LLM-side stalls on the assistant lane, not a capture problem.triage-submit / triage-skip / triage-result / triage-brief:
why triage did or did not run. triage-submit.trigger is settled or periodic (a long
monologue fires periodic), triage-skip.reason is empty_state / lane_refused, and
triage-brief.action is dispatch, refuse (stale / outstanding), or skip
(gate / extract_disabled / no_brief_fn). A run that only ever shows triage-submit
with no triage-result means the Laya predict is still loading or stalled.When the system is healthy again, clear the log manually (rm -r troubleshoot/); there is no
automatic retention job. Keep troubleshoot/ on local storage — a slow/NFS/FUSE mount can block
the only capturer.
See SPEAKERS.md for the feature reference.
sherpa-onnx not installedCause: the speaker extra is opt-in and a plain uv sync does not install sherpa-onnx.
Fix:
uv sync --extra speaker
ImportError: libonnxruntime.so: cannot open shared object fileCause: the native onnxruntime that sherpa-onnx-core bundles is missing. This happens when
the environment was synced without the extra, or before the core wheel was included.
Fix: re-sync the speaker extra, which now installs sherpa-onnx-core:
uv sync --extra speaker
If you also use the triage panel, request both extras so neither stack is dropped:
uv sync --extra speaker --extra triage
The daemon itself only warns and keeps running with speaker_status = "none"; the hard error
appears when you run the one-off stt-daemon speaker-enroll.
enabled = true with no model_pathCause: with enabled = true but model_path unset, missing, or pointing at a model that
fails to load, the feature disables gracefully. It warns once and never crashes the daemon:
no lane is built, no store is touched, and every record keeps speaker_status = "none".
Fix: download the model into the gitignored models/ directory and set model_path:
curl -L -o models/nemo_en_titanet_small.onnx \
"https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/nemo_en_titanet_small.onnx"
# then: [speaker] model_path = "models/nemo_en_titanet_small.onnx"
UnknownCause: either the feature is disabled (check speaker_status = "none" in the JSONL
records), or the thresholds do not fit your audio. threshold_id (0.60) and threshold_new
(0.45) are model- and channel-dependent starting points; the wrong model for the room, or a
bad microphone, can keep every score below threshold_new.
Fix: confirm enabled = true and a loadable model_path first. Then calibrate on real
far-field audio: read speaker_score from the JSONL records and lower the thresholds until
repeated voices cross them, or switch to a fallback model. See SPEAKERS.md for
the bands and the model options.
unknown_speakerCause: stt-ctl speaker-name failed because the id is not in the assistant's cached
roster. The assistant caches speakers from the speakers bus events, so an id that has not
yet appeared in an event is unknown to it.
Fix: run uv run stt-ctl speaker-list to see the ids the assistant knows, then retry with
one of those. If the roster looks stale, let a turn settle so a fresh speakers event
arrives, or restart the assistant.
Cause: a bad prototype, an unwanted provisional, or a corrupt store.
Fix: delete and merge are not supported in v1. Stop the daemon, remove the id from
speakers/speakers.json and its rows from speakers/prototypes.npz, then restart. To start
clean, stop the daemon, delete speakers/, and restart.