9 Commits
Author SHA1 Message Date
aharrison-fullerandClaude Opus 5 2e4c1b2371 Rewrite setup instructions for non-specialist readers
The audience for this is Home Assistant users, who are not necessarily
developers. The instructions assumed too much and buried the step most likely
to trip someone up.

Biggest fix: connecting to Home Assistant is two steps, not one. Adding the
Wyoming integration does nothing on its own -- the assistant keeps using
whatever speech-to-text it used before until you change it under Settings ->
Voice assistants. That was previously half a sentence at the end of a
paragraph, and its failure mode is silent: everything looks installed and
nothing improves. It is now its own numbered step with an explanation.

Also adds:

- "Check it's working" and "Troubleshooting", the latter drawn from failures
  actually hit while building and deploying this, including the wrong-Python
  error and the service-did-not-start-after-changing-user case.
- IP address guidance that survives contact with a real machine. Checking on
  the deployment host showed five addresses -- two Parallels interfaces, a
  Tailscale one, and two real ones -- so the docs now say which to ignore.
  The obvious `ipconfig getifaddr en0` was rejected: it returns nothing there.
- Requirements now lead with `brew install python@3.13` rather than an
  explanation of lzma; the reasoning moved into a collapsed block.
- A plain-English guide to reading the benchmark table, and an explicit note
  that the numbers are a snapshot nothing re-runs automatically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 04:13:05 +01:00
aharrison-fullerandClaude Opus 5 ef1b10e7aa Move benchmarks to BENCHMARKS.md and correct stale README claims
The benchmark table, analysis, caveats and reproduction steps were about half
the README and are reference material, not something you read to install this.
They move to BENCHMARKS.md, leaving a four-row summary and a link.

Stale claims found while auditing, all now corrected against the running
system:

- "37 unit tests" -- there are 44.
- The Logs section claimed each request logs the transcript. It has not since
  transcripts moved behind --debug, and the claim directly contradicted the
  Security section two headings later.
- The notable-tests list predated the security work and omitted the buffer cap
  and transcript-privacy tests.
- Title said wyoming-parakeet; the repo is wyoming-parakeet-mlx.

Also documents the server's own flags, which were only discoverable via
--help or the Security section, and drops the duplicated speed claim now that
Performance carries the numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:56:40 +01:00
aharrison-fullerandClaude Opus 5 128c174672 Fix daemon startup when --user changes, and document service accounts
launchd opens StandardOutPath/StandardErrorPath as the service user. Re-running
install.sh with a different --user leaves those files owned by the previous
one, so the job fails to start -- and since it never gets far enough to write
anything, the log gives no clue why. Chown them before bootstrapping.

README now documents the full dedicated-service-account setup, including the
two things that are easy to get wrong: macOS home directories are drwxr-x--- so
the checkout has to live outside one, and the HuggingFace cache follows HOME so
it has to move with the service user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:52:36 +01:00
aharrison-fullerandClaude Opus 5 0889d2bf02 Run the daemon as the service account's own group, not staff
install.sh hardcoded GroupName staff in the plist. For the default case
(installing as yourself) that is a no-op, but it silently undermines the
--user flag: a dedicated service account would still run with group staff and
inherit read access to every staff-group path, including other users' home
directories. Derive the group from the user instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:48:08 +01:00
aharrison-fullerandClaude Opus 5 5a6fc62106 Cap buffered audio and keep transcripts out of INFO logs
Wyoming has no authentication, so buffered audio is attacker-controlled.
self.audio grew until AudioStop with no bound: measured at ~11 MB/s over
loopback, one connection exhausts 32 GB in under an hour, and a stuck
satellite that never sends AudioStop does the same by accident. Cap it at
--max-audio-seconds (default 120), dropping the excess with a single warning
while still transcribing what was captured. Verified: a client streaming
10.8 GB now moves server RSS by 213 MB rather than 10.8 GB.

Transcripts were logged at INFO. Log files are long-lived and world-readable
under /tmp on macOS, so every voice command sat in plaintext readable by any
local account. INFO now records duration, latency and character count; the
text moved behind --debug.

Both are covered by mutation-checked tests, and the README gains a Security
section covering the unauthenticated trust boundary, the 0.0.0.0 bind that
also exposes VPN interfaces, and running the daemon as a non-admin user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:44:25 +01:00
aharrison-fullerandClaude Opus 5 9bf887e780 Expand benchmark reproduction instructions
Give the exact command behind every table row, document the whisper.cpp
server setup those rows need (including why -nc matters for a fair
comparison), and state the venv/model cost plus cleanup.

Call out that bare 'python3' is the system 3.9 on macOS and fails with
'No matching distribution found for parakeet-mlx' -- verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:40:52 +01:00
aharrison-fullerandClaude Opus 5 4475f6d39c Add benchmark harness and compare against 11 other STT backends
Measures median latency, strict exact-match accuracy, digit formatting and
silence behaviour across 48 clips (16 Home Assistant commands x 3 macOS TTS
voices), on an M4 Mac mini.

Headline: Parakeet v2 at 102ms median is 5x faster than the best whisper.cpp
configuration and lands within one clip of it on accuracy. mlx-whisper
large-v3 is the only backend to score 48/48, at 11x the latency. Moonshine is
2x faster again but gives up real accuracy (35/48).

Also quantifies the reason this defaults to v2 over v3: v3 returned digits
for only 10 of 21 number-bearing commands, against 21/21 for v2, which is
most of the gap between their exact-match scores.

The harness feeds audio to every backend as an array rather than a path --
mlx-whisper and moonshine otherwise shell out to ffmpeg, which this project
deliberately does not require.

Clips are gitignored; bench/make_clips.sh regenerates them. Caveats are
documented in the README: this is clean synthetic TTS, so it measures latency
rigorously and accuracy only as a domain smoke test, and faster-whisper is
CPU-only on Apple Silicon because CTranslate2 has no Metal backend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:32:57 +01:00
aharrison-fullerandClaude Opus 5 c846bc245c Wait for bootout to complete before bootstrapping the daemon
launchctl bootout returns before the job is actually gone. Bootstrapping into
that gap fails with "Bootstrap failed: 5: Input/output error" and, because the
script runs under set -e, exits with nothing loaded at all -- taking
speech-to-text down rather than reinstalling it.

Poll until the label disappears, and fail with an actionable message if it
does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:06:31 +01:00
aharrison-fullerandClaude Opus 5 4286e88344 Wyoming speech-to-text server for Home Assistant using NVIDIA Parakeet
Loads parakeet-mlx in-process (no HTTP hop) and serves it over the Wyoming
protocol. On an M4 Mac mini this transcribes typical voice commands in ~110ms
versus ~1150ms for a whisper.cpp large-v3 setup, with identical accuracy on a
ten-command benchmark.

Two behaviours matter beyond speed: silence returns an empty string rather
than whisper's "Thank you." hallucination, and there is no decoder context
carried between requests.

Notable implementation details, all covered by mutation-checked regression
tests:

- MLX streams are thread-local, so the model is loaded and evaluated on a
  single dedicated worker thread. Splitting those raises
  "There is no Stream(cpu, 1) in current thread".
- parakeet_mlx.load_audio() shells out to ffmpeg, which is unnecessary here
  since Wyoming delivers 16kHz mono PCM. The mel is built directly via
  get_logmel(), whose input must be float32 -- it views the complex STFT
  output as the input dtype, so anything narrower doubles the mel bin count.
- Wyoming's run loop has no except clause, so an exception escaping
  handle_event closes the connection without sending a Transcript and Home
  Assistant waits indefinitely. Failures are caught and returned as an empty
  transcript instead.

Defaults to parakeet-tdt-0.6b-v2 rather than the newer multilingual v3
because v2 emits digits ("21 degrees") where v3 spells numbers out, and
Home Assistant's local intent matching expects digits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:04:48 +01:00