short confirmation tones on daemon events so the user gets eyes-free "did it hear me?"
feedback without watching the terminal. NOT TTS — short pre-generated .wav beeps.
- audio_out.py — reusable audio-OUT module (the reverse of audio.py's capture, the
less-tested WSLg direction). three-tier player: paplay-first (a SEPARATE process, so
it doesn't contend with the sounddevice mic stream on the duplex-flaky WSLg bridge),
then in-process sounddevice, then powershell.exe SoundPlayer. best-effort per-backend
volume. plays a wav path and knows nothing about events — v0.3 TTS reuses it.
- sound.py — Earcons: the single event->tone map (wake/accept/no_match/submit) gated by
[sound] config (master enabled + per-event flags). daemon._handle wiring: an injected
command plays accept (submit plays submit); no-match / target-missing / unknown-context
plays no_match; pure daemon-control commands (list/version/…) play nothing.
- sounds/ — committed earcon wavs + generate.py (regen-only). committed (not generated
at install) so the package is self-contained and a missing tone can never appear.
packaged via pyproject [tool.setuptools.package-data].
- [sound] config: enabled (master, on), on_wake (OFF by default — bleed/chatty),
on_accept/on_no_match/on_submit (on), volume (0-1 best-effort), [sound.files] overrides.
- claudedo test-tone — plays each tone, the audio-OUT gate (mirrors test-audio).
- install.sh now also checks RDPSink (audio-out) alongside RDPSource.
INVARIANT: earcons are fire-and-forget on a worker thread and NEVER block or break the
inject path. a missing tone file or dead speaker logs once and is swallowed, never
raised — a broken speaker must never stop "claudedo yes" from injecting.
de-risks the WSLg audio-OUT path that v0.3 TTS-readback will reuse.
Signed-off-by: disqualifier <dev@disqualifier.me>
context injection — named reference blurbs from contexts.toml injected ahead of a
dictated instruction, read-before-send (never auto-submits):
- new contexts.py mirrors config.py: [contexts] name = "blurb"; missing file = empty
set; names validated as simple words, looked up on a despaced/lowercased key so
"web hooks"/"web-hooks"/"webhooks" all resolve the same block.
- grammar: context|prepare <name> <instruction> -> Action("context", (name, dictation)).
same-utterance dictation (everything after <name> is literal, incl. "send"); bare
context <name> injects just the blurb. one-shot targeting composes:
[target <name>] [context <ctx>] [filler] <dictation>.
- daemon assembles blurb + (Shift+Enter soft newline | flattened separator) + dictation
via the existing send_literal/type path, tracks the uncommitted-input buffer, and
WAITS. config-gated by behavior.context_multiline / context_separator. unknown context
name announces and injects nothing.
system daemon-control namespace — lands the pass-through vs control split the router was
structured for. reserved leading "system" routes to _do_system (never injects to
claude): system status (mode/target/model/contexts) and system reload [config|contexts].
live reload — voice reload + CLI claudedo reload (SIGHUP) re-read config.toml +
contexts.toml without reinitializing the loaded whisper model. customs now lists loaded
contexts. install.sh installs the contexts.toml template copy-if-absent (else .new).
keys.NEWLINE (S-Enter) added for the soft-newline assembly. wake list unchanged.
Signed-off-by: disqualifier <dev@disqualifier.me>
commands menu now prints under a single [HELP] header with bare indented rows
(brightblue usage) instead of 15 repeated [SYSTEM] tags. raise [vad].max_seconds
10 -> 15 for long dictation. wake_fuzzy_threshold 0.6 -> 0.65 (slightly fewer false
wakes; note short spellings 'ok/okay claude' still admit some). carries the prior
small.en default, [vad].silence_ms 700, lighter (brightblue) command color, lean
injection lines, .en model variants in the validator. README/CLAUDE.md synced.
Signed-off-by: disqualifier <dev@disqualifier.me>
default stt.model -> medium (biggest accuracy gain for the coined wake word;
small/large-v3 documented alternatives). seed faster-whisper with an initial_prompt
derived from the configured wake phrases + command vocabulary (grammar.vocabulary /
initial_prompt, one source — command synonyms now live in named _*_VERBS tuples).
split the single fuzzy threshold into wake_fuzzy_threshold (0.6, lenient — a false
wake is cheap) and command_fuzzy_threshold (0.8, tight — a false command fires the
wrong action); grammar.parse() takes both. add a [vad] config section (silence_ms,
max_seconds) for the existing Alexa-style record-until-pause endpointing, which
captures a command whole and lets the trailing pause separate it from following
chatter (that chatter is a separate capture the wake gate discards). bump to 0.1.3.
Signed-off-by: disqualifier <dev@disqualifier.me>
voice editing: 'space [<n>]' inserts spaces, 'backspace [<n>]' (alias delete)
deletes chars, 'erase' (alias clear/wipe) wipes the current input. the daemon
tracks a per-session uncommitted-input char count so backspace is capped at the
last submit boundary and erase clears exactly back to it; submit/set reset it.
keys.py gains BSpace/space; grammar gains a count parser (digits + number words).
new console.py renders every daemon line as 'HH:MM:SS [prefix] message' with
color: [<session>] for injected lines (green), [SYSTEM] for state, [VOICE] for
recognition/drops (red/dim). bump to 0.1.2.
Signed-off-by: disqualifier <dev@disqualifier.me>
redefine targeting: 'set' (aliases sticky/switch) is the persistent sticky
default (~/.claude-active); 'target <name> <command>' is a one-shot override that
routes a single command without changing the sticky default. add 'unset' and
'list'. resolution moves to a single target.resolve(one_shot) implementing the
order: one-shot -> sticky-if-exists -> only-session auto -> ambiguous/none do
nothing (never falls through, never injects into a missing session).
grammar.parse now returns ParsedCommand(one_shot, action) and skips optional
leading filler words (config behavior.filler_words: select/use/choose), with a
filler-before-digit still meaning the select command. CLI gains set/unset/list
(switch kept as a set alias). daemon console shows the targeting reason per line.
docs updated; no stale 'target = sticky' wording remains. bump to 0.1.1.
Signed-off-by: disqualifier <dev@disqualifier.me>