Browse Source

Updated documentation

Efren Yevale Varela 1 tháng trước cách đây
mục cha
commit
ee33e71b49
5 tập tin đã thay đổi với 156 bổ sung và 70 xóa
  1. 63 0
      AGENTS.md
  2. 5 0
      LICENSE.md
  3. 29 14
      README.md
  4. 55 55
      config.example.toml
  5. 4 1
      pyproject.toml

+ 63 - 0
AGENTS.md

@@ -0,0 +1,63 @@
+# AGENTS.md
+
+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.
+
+## Commands
+
+```bash
+uv sync                                                  # install into .venv
+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 (no mypy/pyright)
+uv build                                                 # sdist + wheel (hatchling)
+```
+
+There is no CI, pre-commit, Makefile, or task runner. Run ruff + pytest manually before finishing.
+
+## Process architecture (don't break this)
+
+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.
+
+Stateful logic belongs in the assistant. Transcription runs on a single worker thread by design
+(faster-whisper isn't thread-safe) — do not parallelize it.
+
+`stt-daemon` with no subcommand runs the daemon (legacy `--check` / `--list-sources` still work).
+
+## Config
+
+- Loaded from `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); the README config section is the
+  annotated reference. Keep all three in sync when adding a key.
+- Mapping is **manual** — no pydantic/dynaconf. Adding a key means editing the dataclass *and*
+  `load_config()` in `config.py`.
+- Env vars: `STT_DAEMON_STATE` / `XDG_STATE_HOME` (provisioning state at
+  `~/.local/state/stt-daemon/audio.json`), `XDG_RUNTIME_DIR` (sockets), `HF_HOME` (Whisper cache).
+- Never treat the live `config.toml` as a source of defaults; it is gitignored user state.
+
+## Testing
+
+- `tests/` has 206 tests, all hardware- and network-free. Preserve that: inject fakes instead of
+  touching real `pactl`, audio devices, or an LLM.
+- Key seams: `Daemon(...)` accepts `capture_factory`, `vad_factory`, `transcriber_factory`,
+  `sink_factory`; `PulseAudio` takes an injectable `runner`; `AssistantService` takes `time_fn`,
+  `executor`, `audit`.
+- No `conftest.py` — per-file fakes are the convention. Tests are the behavioral spec; read them
+  before changing behavior.
+
+## Conventions & gotchas
+
+- Never commit gitignored runtime output: `config.toml`, `transcripts/`, `audio/`, `audit/`, `.omo/`.
+- New assistant tools: register in `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.
+- Whisper models must be multilingual — do not use `.en` variants (defaults auto-detect es/en).
+- `.omo/plans/` holds local design plans (gitignored), including the assistant-layer spec.

+ 5 - 0
LICENSE.md

@@ -0,0 +1,5 @@
+Copyright 2026 Syavne.Net
+
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

+ 29 - 14
README.md

@@ -63,7 +63,8 @@ stateful lives in the assistant, which is a separate process (see
 Clone the repo and install with `uv`:
 
 ```bash
-cd /home/efren/Repositories/STT-TTS
+# From the repo root (wherever you cloned it)
+cd /path/to/STT-TTS
 
 # Sync dependencies (installs into .venv)
 uv sync
@@ -85,7 +86,8 @@ cp config.example.toml config.toml
 
 ### Configuration Reference
 
-The config file is TOML. Here are all available options with defaults:
+The config file is TOML. Here are all available options with defaults.
+`config.example.toml` ships values only; this section is the annotated reference.
 
 ```toml
 [audio]
@@ -109,7 +111,7 @@ rms_floor        = 0.008              # Drop segments quieter than this RMS (noi
 trailing_trim_ms = 200                # Keep only this much trailing silence (less hallucination-prone padding)
 
 [whisper]
-model        = "small"                # "tiny", "base", "small", "medium", "large"
+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"
@@ -264,7 +266,9 @@ uv run stt-daemon --backend sounddevice --device 0  # device 0 = default input
 
 ### Subcommands
 
-The `stt-daemon` CLI supports these subcommands:
+The `stt-daemon` CLI supports these subcommands. Every subcommand also accepts the
+common overrides `--config`, `--device`, `--backend`, `--model`, `--language`, and
+`-v/--verbose` (for example `stt-daemon setup --device ...`).
 
 #### `setup` — Create virtual sink and loopbacks
 
@@ -462,8 +466,12 @@ refused unless a TUI is attached.
 `stt-ctl` verbs (one message each): `command-start`, `command-execute`, `command-cancel`,
 `command-edit --text "..."`, `chat-toggle`, `status`.
 
+Clients attach by sending `subscribe` first (`viewer: true` for the TUI, which makes the
+assistant treat it as a watcher for the `require_viewer` gate).
+
 Assistant → clients: `mode`, `command_text`, `transcript`, `speech_start`/`speech_stop`,
-`tool_call`, `tool_result`, `chat_message`, `heartbeat`, `error`.
+`tool_call`, `tool_result`, `chat_message`, `heartbeat`, `error`, plus the direct
+replies `ack` (command accepted) and `status`.
 
 ### Audit
 
@@ -674,7 +682,12 @@ Tests cover:
 - **Pre-roll buffer** preservation: confirm the first frames are not cut
 - **Silence finalization**: utterances end after N silence frames
 - **Min/max utterance duration**: blips are discarded, long utterances are finalized
+- **Pause-aligned chunking**: long speech splits at pauses (`end_reason = "pause"`)
 - **Config loading and language normalization**
+- **Provisioning**: `setup`/`ensure`/`teardown`, orphan-name cleanup, and `clear` idempotency
+- **Event bus**: envelope codec, ring replay, subscriber fan-out
+- **Assistant**: mode machine, command readiness/timeouts, tool schema validation, LLM client/history trimming, audit log
+- **TUI**: pure view-state reducer and control-socket client
 
 ## Development
 
@@ -688,6 +701,10 @@ src/stt_daemon/
 ├── ctl.py               - One-shot control client (`stt-ctl`, hotkeys)
 ├── config.py            - Config loading (TOML)
 ├── audio.py             - Audio capture (parec, sounddevice)
+├── pulse.py             - PulseAudio/pactl wrapper (devices, modules, sink checks)
+├── provisioner.py       - Virtual sink + loopback lifecycle (setup/ensure/teardown)
+├── state.py             - Provisioning state file (~/.local/state/stt-daemon/audio.json)
+├── picker.py            - Interactive device picker (questionary / numbered menu)
 ├── segmenter.py         - VAD + pre-roll buffer + utterance segmentation (core)
 ├── transcriber.py       - Faster-Whisper wrapper
 ├── output.py            - Sinks (stdout, JSONL, optional WAV)
@@ -702,13 +719,14 @@ src/stt_daemon/
 │   ├── tools.py         - Tool registry + schema validation + get_datetime
 │   ├── llm.py           - llama.cpp client + chat session (block trimming)
 │   ├── audit.py         - Daily action audit log
-│   └── runner.py        - `stt-assistant` process
+│   ├── runner.py        - `stt-assistant` process
+│   └── __main__.py      - Entry point for `python -m stt_daemon.assistant`
 └── tui/
     ├── state.py         - Pure view-state reducer
     ├── client.py        - Control-socket client
     └── app.py           - Textual app + `stt-tui` entry point
 
-tests/                    - 196 tests, no audio hardware or live LLM required
+tests/                    - 206 tests, no audio hardware or live LLM required
 
 scripts/
 ├── audio-sink.sh        - Thin wrapper over `stt-daemon setup|clear|list`
@@ -724,18 +742,15 @@ uv run ruff check .
 ### Building/packaging
 
 ```bash
-# Install hatchling if needed
-uv pip install hatchling
-
-# Build wheel
-uv run python -m build
+# Build sdist + wheel (hatchling backend, installed in an isolated env by uv)
+uv build
 ```
 
 ## Limitations & Design Decisions
 
 1. **Mono only**: Stereo support can be added, but speech is typically mono in transcription pipelines
 2. **Single language per transcription**: Whisper detects one language per utterance; mixed-language speech is attributed to the dominant language
-3. **No LLM integration**: This is transcription-only; use the JSONL output to feed an LLM if needed
+3. **LLM is optional and separate**: the daemon itself is transcription-only; the `stt-assistant` process adds chat/commands/tools on top, and can be disabled with `[llm].enabled = false`
 4. **PulseAudio/PipeWire-Pulse**: The default `parec` backend assumes UNIX-like systems with PulseAudio. macOS/Windows should use `sounddevice`
 5. **Real-time**: Latency depends on frame duration (30ms), silence threshold (default 700ms), and model size. Expect ~1–3 second latency with `small` model on CPU
 6. **Thread-safe queues**: Reader thread (audio) is separate from worker thread (transcription) so audio never blocks waiting for transcription
@@ -748,7 +763,7 @@ uv run python -m build
 
 ## License
 
-This project is MIT-licensed. See LICENSE file (if included).
+This project is ISC-licensed. See [`LICENSE.md`](LICENSE.md).
 
 ---
 

+ 55 - 55
config.example.toml

@@ -1,77 +1,77 @@
 [audio]
-backend = "parec"                     # "parec" (PulseAudio by name) | "sounddevice"
-device  = "VirtualMicSink.monitor"    # PulseAudio source name (parec) or device index/name (sounddevice)
+backend     = "parec"
+device      = "VirtualMicSink.monitor"
 sample_rate = 16000
 channels    = 1
-frame_ms    = 30                      # webrtcvad requires 10/20/30 ms
+frame_ms    = 30
 gain        = 1.0
 
 [vad]
-aggressiveness   = 2                  # webrtcvad 0-3
-pre_roll_ms      = 400                # ring buffer kept BEFORE speech is confirmed
-speech_start_ms  = 90                 # consecutive speech frames needed to confirm start
-silence_ms       = 700                # trailing silence that ends 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 (kills noise-triggered blips)
-trailing_trim_ms = 200                # keep only this much trailing silence (less hallucination-prone padding)
+aggressiveness   = 2
+pre_roll_ms      = 400
+speech_start_ms  = 90
+silence_ms       = 700
+min_utterance_ms = 300
+max_utterance_s  = 30.0
+chunk_pause_ms   = 350
+soft_max_s       = 10.0
+rms_floor        = 0.008
+trailing_trim_ms = 200
 
 [whisper]
-model        = "small"                # multilingual (DO NOT use .en suffix)
-device       = "cpu"                  # "cpu" | "cuda"
-compute_type = "int8"                 # "int8" cpu, "float16" cuda
-language     = "auto"                 # "auto" | "en" | "es"
-beam_size    = 1
-vad_filter   = false
-no_speech_prob_max = 0.6              # drop segments Whisper thinks are non-speech above this
-avg_logprob_min    = -1.0             # drop segments with confidence below this
-
-[output]
-stdout    = true
-jsonl_dir = "transcripts"             # one JSONL file per day: YYYY-MM-DD.jsonl
-save_audio = false                    # write each transcribed utterance as a 16 kHz mono WAV
-audio_dir  = "audio"                  # WAV destination (YYYY-MM-DD subfolders); used only if save_audio
+model              = "small"
+device             = "cpu"
+compute_type       = "int8"
+language           = "auto"
+beam_size          = 1
+vad_filter         = false
+no_speech_prob_max = 0.6
+avg_logprob_min    = -1.0
 
 [sink]
-sink_name             = "VirtualMicSink"    # Name of the null sink to create
-remap_source_name     = "VirtualMic"        # Name of the exposed microphone
-loopback_latency_msec = 1                   # Loopback latency in ms
-monitor_output        = false               # Loop sink.monitor to default sink (AVOID: causes acoustic feedback)
+sink_name             = "VirtualMicSink"
+remap_source_name     = "VirtualMic"
+loopback_latency_msec = 1
+monitor_output        = false
+
+[output]
+stdout     = true
+jsonl_dir  = "transcripts"
+save_audio = false
+audio_dir  = "audio"
 
 [daemon]
-queue_size      = 50                  # max utterances waiting for transcription (oldest dropped when full)
-backlog_warn_at = 5                   # warn when the queue backs up this far (worker falling behind)
+queue_size      = 50
+backlog_warn_at = 5
 
 [events]
-socket       = ""                     # publish-only event 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
+socket       = ""
+heartbeat_ms = 1000
+ring_size    = 200
 
 [assistant]
-control_socket        = ""            # 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            # disarm only if armed with NOTHING captured for this long
-chat_idle_timeout_s   = 300           # 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
+control_socket        = ""
+stall_timeout_ms      = 3000
+worker_stall_ms       = 15000
+command_arm_timeout_s = 10
+chat_idle_timeout_s   = 300
+turn_gap_s            = 1.5
+require_viewer        = true
 chat_exit_phrases     = ["end conversation", "that's all", "stop listening"]
 
 [audit]
-enabled = true                        # write executed actions to a daily JSONL file
-dir     = "audit"                     # audit/YYYY-MM-DD.jsonl
+enabled = true
+dir     = "audit"
 
 [llm]
-enabled              = true
-endpoint             = "http://localhost:9931/v1/chat/completions"  # llama.cpp, OpenAI-compatible
-temperature          = 0.7
-max_tokens           = 1024           # reasoning models spend tokens before the answer; keep this generous
-timeout_s            = 120
-cache_prompt         = true           # reuse the KV prefix (append-only history keeps it warm)
-max_prompt_tokens    = 64000          # ~half of the 131072 context
-trim_block_turns     = 4              # drop history in blocks (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."
+enabled               = true
+endpoint              = "http://localhost:9931/v1/chat/completions"
+temperature           = 0.7
+max_tokens            = 1024
+timeout_s             = 120
+cache_prompt          = true
+max_prompt_tokens     = 64000
+trim_block_turns      = 4
+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."

+ 4 - 1
pyproject.toml

@@ -5,7 +5,10 @@ build-backend = "hatchling.build"
 [project]
 name = "stt-daemon"
 version = "0.1.0"
-description = "Always-on speech-to-text daemon with pre-roll buffer"
+description = "Always-on speech-to-text daemon with pre-roll buffer and a voice assistant layer"
+readme = "README.md"
+license = "ISC"
+license-files = ["LICENSE.md"]
 requires-python = ">=3.11"
 authors = [{name = "Efren", email = "efren@example.com"}]
 dependencies = [