Always-on speech-to-text daemon plus a separate voice-assistant layer. Python 3.11+ (dev env is
3.13, but requires-python = ">=3.11" and ruff targets py311 — don't use 3.12+ syntax),
uv, src/ layout. stt_daemon/ is the only package; console scripts live in pyproject.toml
(stt-daemon, stt-assistant, stt-ctl, stt-tui).
uv sync # install into .venv (see pywhispercpp below)
uv sync --extra portaudio # sounddevice capture backend only
uv sync --extra triage # optional: in-process Laya triage lane
uv sync --extra speaker # optional: in-process speaker embedding lane
# extras are exclusive under uv sync: pass both flags (--extra speaker --extra triage) to keep both
uv run pytest -v # full suite (hardware- and network-free)
uv run pytest tests/test_segmenter.py::test_name -v # single test
uv run ruff check . # only static check: E,F,W, line-length 100, no typechecker
uv build # sdist + wheel (hatchling)
uv run stt-daemon up --with-tui # supervise all three processes (main launcher)
uv run stt-daemon setup && uv run stt-daemon run # manual flow without supervisor
There is no CI, pre-commit, Makefile, or task runner. Run ruff + pytest manually before finishing.
The repo's lock/vendor pins CUDA wheels (torch 2.14.0+cu130, vendored pywhispercpp). After any
uv sync, reinstall the ROCm stack; otherwise the venv silently goes back to CUDA. One command:
scripts/install-rocm.sh
It fetches the torch ROCm wheels and the CTranslate2 ROCm wheel. The manual equivalent, if the script cannot run on this host:
TMPDIR=$HOME/.tmp .venv/bin/pip install --force-reinstall torch torchvision torchaudio \
--index-url https://download.pytorch.org/whl/rocm7.2 \
--extra-index-url https://pypi.org/simple
TMPDIR=$HOME/.tmp .venv/bin/pip install --force-reinstall ~/.tmp/ct2-rocm/ctranslate2-*-cp313-*.whl
rocm-python-wheels-Linux.zip; the script
fetches it); PyPI's ctranslate2 is CUDA-only.uv run re-syncs the venv implicitly, so it also reverts these wheels (uv sync is not
required to lose them). After installing the ROCm stack, run the suite as
.venv/bin/python -m pytest (or uv run --no-sync ...), and re-run scripts/install-rocm.sh
after any deliberate sync.hipMallocAsync allocator silently corrupts CTranslate2 buffers
(OpenNMT/CTranslate2 #2090): wrong language detection, empty transcripts, hangs, or segfaults.
The transcriber sets CT2_CUDA_ALLOCATOR=cub_caching automatically on ROCm hosts (CTranslate2
4.8.4 makes it the HIP default); an explicit user-set value is respected.[whisper] backend = "faster-whisper", device = "auto", compute_type = "int8".
auto resolves to the discrete R9700 (verified by VRAM delta) while the integrated GPU stays
idle, and auto language detection works on ROCm once the allocator is configured. The vendored
pywhispercpp wheel is CUDA-only and cannot import on this host.HIP_VISIBLE_DEVICES=0 for CTranslate2 — it selects the wrong device and transcribes
garbage. Leave both devices visible; device = "auto" picks the R9700.TMPDIR=$HOME/.tmp for pip/uv: /tmp is a 1 GiB tmpfs and large wheels fail with ENOSPC.Three processes wired by UNIX sockets under $XDG_RUNTIME_DIR (fallback /tmp):
stt-daemon (cli.py) — capture → pre-roll/VAD → segment → transcribe → stdout + JSONL.
Publish-only on stt-daemon.sock: it never accepts inbound messages; lifecycle is signals only.stt-assistant (assistant/runner.py) — subscribes to that bus, owns all state (modes, command
buffer, chat history, LLM, tools, audit), serves stt-assistant.sock.stt-tui / stt-ctl — clients of the assistant control socket only.stt-daemon up [--with-tui] (supervisor.py) launches these as child processes. With --with-tui,
TUI startup is mandatory (no silent headless fallback: socket timeout or early child exit kills all),
and the supervisor restores the tty's ISIG on exit so a killed TUI doesn't leave Ctrl+C dead.
Stateful logic belongs in the assistant (the speaker embedding lane is the daemon-side exception:
it owns the prototype store). Four single-worker-thread lanes exist by design, and none may be
parallelized: transcription (faster-whisper isn't thread-safe), the speaker embedding lane
(in-process in stt-daemon: src/stt_daemon/speakers.py + src/stt_daemon/embedder.py, owns the
SpeakerStore, drains the rename inbox), all LLM calls (assistant/inference.py InferenceQueue;
commands submit with priority -1 to preempt chat), and the Laya triage lane
(assistant/laya_lane.py: one background thread plus one loader, running in-process in the
assistant). Neither the speaker embedding lane nor the Laya lane is a process or a socket, and
neither changes supervisor spawn counts (2 without the TUI, 3 with it); the daemon socket stays
publish-only.
stt-daemon with no subcommand runs the daemon (legacy --check / --list-sources still work).
After a session, the session-summary skill turns transcripts/<session>/ into the files worth
sharing. It is an agent run, not a process: no console script, no socket, no effect on
supervisor spawn counts. Full reference: docs/SESSION-SUMMARY.md.
transcripts.jsonl is the truth; brief.md is low-trust input that the skill
rewrites. Never modify transcripts.jsonl or audio/, and work on one session directory only
(no cross-session reads; shared knowledge lives in the skill's glossary, which is gitignored).session-summary.svg, session-summary.png,
session-summary.content.json, the .es. trio, and a corrected brief.md. A run is done only
when both SVGs and both PNGs exist; a ~1 MB PNG means quantization did not run.scripts/render.py <session> <content.json> <out.svg> [--no-png] renders and validates the SVG
and exports the PNG via Inkscape + ImageMagick, both required on PATH. --no-png is for
drafts only — never hand-write or hand-export the SVG..opencode/ is gitignored except the session-summary skill, which is tracked repo tooling
(see the progressive un-ignore in .gitignore) — with one exception inside it:
references/glossary.md is private client vocabulary and stays ignored. Nothing the skill
writes is committed: session directories stay ignored with transcripts/.config.toml in the current working directory only (no env var, no XDG lookup);
--config overrides. A missing file means dataclass defaults.config.example.toml is the shipped template (values only); docs/CONFIGURATION.md is the
annotated reference. Keep the dataclass, example, and docs in sync when adding a key.load_config() in config.py. Unknown top-level sections warn and are ignored; unknown keys
inside a known section are silently ignored (dict.get per field).STT_DAEMON_STATE (provisioning state; default audio.json in the cwd),
XDG_RUNTIME_DIR (sockets), HF_HOME (Whisper cache, honored by faster-whisper/huggingface).[triage] is the display-only signal panel, off by default (enabled = false). Its
questions_file (questions.json in the cwd) is private user state and is gitignored, like
config.toml. dump_io = true writes one JSON per triage under troubleshoot/triage/ (also gitignored);
a coalesced brief update writes two files. The brief engine is driven by
[llm].triage_system_prompt and returns two plain-text sections (NEW FACTS: + BRIEF:);
code owns the append-only FactLedger (dedupe/eviction/render, ledger_max_facts), so the
model never rewrites recorded facts. The optional fail-open substantive_update gate is
brief_gate_enabled (default true); periodic_s re-runs triage every N seconds while
speech accumulates without a settled turn.[speaker] is opt-in per-utterance speaker identification, off by default, backed by the
SpeakerConfig dataclass (12th known section; 14 keys: enabled, model_path, provider,
threshold_id, threshold_new, min_enroll_s, rms_floor, margin, mint_cooldown_s,
confirm_s, max_prototypes, provisional_ttl_h, store_dir, join_timeout_s). Needs
uv sync --extra speaker plus a model under models/. Full reference: docs/SPEAKERS.md.[brief] is opt-in durable-brief persistence, backed by the BriefConfig dataclass (13th
known section; 1 key: write (default false, gates disk persistence only)). The writer
defaults to brief/brief.md (cwd-relative, gitignored; directory/filename are BriefFile
ctor defaults, not config keys) and set_directory() repoints it at the active session folder;
with write = false the brief stays in the TUI only. Full reference: docs/TRIAGE.md.config.toml as a source of defaults; it is gitignored user state.[whisper].backend picks faster-whisper (default) or pywhispercpp through
build_transcriber() in transcriber.py. device/compute_type apply to faster-whisper only;
pywhispercpp ignores them (GPU comes from the built wheel, quantization from the ggml model).vendor/.gitignore is *, the cp313-only
wheel is not tracked, and [tool.uv.sources] points at that path. A fresh clone on Python 3.13
fails uv sync until the wheel is rebuilt — see docs/PYWHISPERCPP.md. Never delete vendor/.tests/ has 1038 tests, all hardware- and network-free. Preserve that: inject fakes instead of
touching real pactl, audio devices, an LLM, or sherpa_onnx (the speaker lane uses a fake
embedder; no ONNX in tests).Daemon(...) accepts capture_factory, vad_factory, transcriber_factory,
embedder_factory, sink_factory; PulseAudio takes an injectable runner; AssistantService
takes time_fn, executor, audit.tests/test_speakers.py, tests/test_daemon_speaker.py,
tests/test_assistant_speaker.py, tests/test_enroll_assist.py (the offline seed helper
src/stt_daemon/enroll_assist.py), and tests/test_evaluate.py (the speaker-eval scorer
src/stt_daemon/evaluate.py).conftest.py — per-file fakes are the convention. Tests are the behavioral spec; read them
before changing behavior.triage and speaker extras. Install triage
(uv sync --extra triage) only to exercise the Laya measurement test, which otherwise skips;
speaker (uv sync --extra speaker) is needed only for real ONNX embeddings, never the suite.config.toml, transcripts/, audio.json, audio/,
troubleshoot/ (audit/debug/triage), brief/, speakers/ (biometric), models/, .omo/,
questions.json, and .opencode/skills/session-summary/references/glossary.md (client
vocabulary).assistant/tools.py. Model-supplied arguments are JSON-parsed and
schema-validated before running; set side_effect and requires_confirmation for anything
non-read-only..en variants (defaults auto-detect es/en).docs/ holds the full references (config, CLI, audio setup, assistant, speaker identification,
output, troubleshooting, development); .omo/plans/ holds local design plans (gitignored).