Files
VoiceAgent/ACCURACY.md
T
2026-08-07 18:15:36 -04:00

126 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Transcription accuracy
What was measured, what shipped, and what turned out to be wrong along the way.
Benchmark throughout: eight sentences of technical speech (85 words) containing
the vocabulary this project actually uses — pipecat, Kokoro, Metamate,
Phabricator, fbsource, SFSpeechRecognizer. The audio is synthesised, so it is
cleaner and more evenly paced than a person at a microphone. Trust the ordering,
not the absolute rates, and prefer `compare_engines.py` on your own voice.
Scoring follows the [Inscribe
benchmark](https://get-inscribe.com/blog/apple-speech-api-benchmark.html):
lowercased, depunctuated, contractions expanded and **digits written out**. That
last part matters — see the correction below.
## Where it ended up
| Configuration | WER |
| --- | --- |
| old `SFSpeechRecognizer`, no help | 20.0% |
| `SpeechTranscriber`, no help | 16.5% |
| old + vocabulary + repair rules | 10.6% |
| `DictationTranscriber` + vocabulary + repair | 9.4% |
| **`SpeechTranscriber` + repair — the default** | **9.4%** |
The top two tie, but not at equal cost. `SpeechTranscriber` reaches 9.4% with
**five repair rules and no vocabulary**, where the old model needs 39 curated
terms *and* seven rules to match it. It is also better with no configuration at
all, which is the state anyone starts in.
## The two mistakes I made measuring this
**Scoring punished the new model for formatting.** My first normaliser
lowercased and stripped punctuation but left digits alone, so
`SpeechTranscriber` was charged errors for writing "400 milliseconds" instead of
"four hundred milliseconds", and "SF speech recognizer" instead of
"SFSpeechRecognizer". Neither is a mishearing. The Inscribe benchmark names this
exact trap. Rescored properly, a 12.9%-vs-14.1% loss for the new model became a
9.4% tie that it wins on effort.
**"Dilution is cheap" was wrong.** Padding the term list with a thousand random
dictionary words cost only 1.1 points, so auto-generating 100 terms from project
filenames looked safe. It erased the entire benefit — back to no-biasing levels
— because filenames like `bot`, `plan` and `hack` are ordinary English, and
boosting ordinary words drags correct speech onto them. Obscure words are inert
by comparison. Discovered terms are now filtered against the system dictionary,
capped at 12, and ranked behind the hand-written list.
## What each mechanism is worth
**Vocabulary biasing** (`vocabulary.txt` → `contextualStrings`) removed 30% of
errors on the old model and costs nothing at runtime. `SpeechTranscriber`
**ignores it** — output is byte-identical with and without terms — so on the
default engine the file now serves only Claude's system prompt and the fallback
engines. Keep it short and specific.
**Repair rules** (`corrections.txt`) are deterministic substitutions for
mistakes that recur identically. Worth more on the new engine than the old,
because its errors land phonetically close to the target — "Kakoro" for Kokoro,
where the old model produced "coral". Five rules do what 39 terms plus seven
rules do elsewhere.
**LLM post-correction** took 16.5% to 7.1% on the old engine, and 5-best beat
1-best (7.1% against 8.2%). Not worth a separate pass on the conversation path,
since Claude already reads every transcript — the vocabulary goes into his
system prompt instead, which is free. Worth a real pass for meeting notes.
**Punctuation and task hint** on the old API measurably do nothing for accuracy.
The new engine punctuates and capitalises automatically.
**Noise beats all of it.** At 10 dB SNR, WER nearly tripled to 69% and neither
vocabulary, high-pass filtering nor gain normalisation recovered any of it. The
microphone is the highest-leverage component in the pipeline.
## Which model is which
| | old `SFSpeechRecognizer` | `DictationTranscriber` | `SpeechTranscriber` |
| --- | --- | --- | --- |
| Model | old | **byte-identical to old** | **new** |
| Honours vocabulary | yes | yes | no |
| Long audio | needs stitching | native | native |
| Punctuation | no | no | automatic |
| Best measured | 10.6% | 9.4% | **9.4%, far less tuning** |
`DictationTranscriber` is not a different model: it produces character-for-
character identical output to `SFSpeechRecognizer` on the same audio, verified
sentence by sentence rather than inferred from the docs. It is the old
recogniser reached through the new API, and its only advantage is vocabulary
support.
Both new-API modules handle long speech natively — 40 of 40 sentences recovered
from 83 seconds — so the transcript stitching in `apple_stt.py` is only needed
on the legacy fallback path.
For scale, the published benchmark measured 5,559 LibriSpeech utterances on an
M2 Pro: SpeechAnalyzer 2.12% clean and 4.56% noisy, against SFSpeechRecognizer's
9.02% and 16.25% — the legacy engine scoring worse than Whisper Tiny. It
validates its harness by reproducing OpenAI's published Whisper numbers within
+0.11 to +0.42. Caveats: English read speech, one machine.
## Ruled out
**Denoising.** *When De-noising Hurts* ([arXiv:2512.17562](https://arxiv.org/abs/2512.17562))
applied MetricGAN+ across 500 recordings and 10 conditions: **all 40
configurations got worse**, Whisper degrading 8.82% → 25.83% at 10 dB SNR, and
even clean audio losing 1.3–3.2 points. So Pipecat's `RNNoiseFilter` is not
worth the dependency surgery it needs, and the commercial filters (Koala, Krisp,
AIC) are the same bet with a licence. Worth checking macOS Voice Isolation is
off, since it is the same class of processing applied for free.
**Custom language models.** `SFSpeechLanguageModel` trains on example sentences
and supports X-SAMPA pronunciations, so it should beat `contextualStrings` — but
its data builder is Swift-only, and it cannot be combined with
`SpeechTranscriber` anyway.
## Still open
- **Real speech.** Every number here is synthesised audio with config files
tuned around it. `compare_engines.py` records you and runs every engine.
- **The external microphone**, versus the built-in one. Probably the largest
real-world effect available and free to test.
- **5-best into Claude.** `--alternatives` exists in the helper and is unused;
measured 8.2% → 7.1% on the old engine.
- **Confidence-gated clarification** — ask again rather than answer a
low-confidence guess.