Make the transcript closer to what was said, measured. The first four tasks
build the measurement: a fixture the loop can fetch, a scorer, a harness that
runs Ambient’s own recording path over the fixture, and a quality script the
loop runs after scripts/check. Every task after that is one experiment with
one number as its judge. An experiment that does not improve the number still
lands: it records the measurement and reverts the code.
Ground rules for every task in this plan:
- The fixture is LibriSpeech test-clean (CC BY 4.0, openslr.org/12). It is
fetched and cached under
~/.cache/ambient/, never committed. - The number is word error rate as
scripts/qualityprints it, on the modelsambient recorduses. Report it in the pull request body. - A change to the pipeline that makes the number worse is reverted before the
task ends; the finding goes into
docs/developing/measurements.mdinstead. - Nothing here touches capture, the menu bar, or the settings UI.
Task 1: A word error rate scorer with tests
Add src/wer.rs with two public functions and unit tests, no I/O:
normalise(text: &str) -> Vec<String>: lowercase, split on whitespace, strip every character that is not a letter, digit or apostrophe from each word, drop empty words."Hello, world's END."becomes["hello", "world's", "end"].score(reference: &[String], hypothesis: &[String]) -> WerwhereWerholdssubstitutions,insertions,deletions,reference_words, and a methodrate(&self) -> f64returning(s + i + d) / reference_words(0.0 when the reference is empty and the hypothesis is too; 1.0 when only the reference is empty). Use word-level Levenshtein with the standard traceback so the three counts are exact, not just their sum.
Tests: identical sequences score zero; one substitution, one insertion, one
deletion each counted correctly; the empty cases; normalise on punctuation,
mixed case, and a hyphenated word (a hyphen splits nothing: "well-known"
stays one word with the hyphen removed, so document that choice in a comment).
Register the module in src/lib.rs. Nothing else changes.
Task 2: Fetch and stitch the fixture
Add scripts/fetch-fixtures, a bash script that builds the WER fixture and
is safe to run every night:
- Download
https://www.openslr.org/resources/12/test-clean.tar.gz(about 330 MB) to~/.cache/ambient/librispeech/test-clean.tar.gzonly if that file is absent, and extract it there only if the extracted directory is absent. Verify the download withtar -tzfbefore extracting; a truncated file is deleted and the script exits non-zero saying so. - Take the first three speaker directories of
test-cleanin sorted order, and for each the first chapter directory in sorted order, so the choice is deterministic and nothing is hard-coded. Sort the chapter’s utterances by id, decode each FLAC to 16 kHz mono 16-bit wav withffmpeg(present on this machine;afconvertis the fallback if it is not), and concatenate them in order with 700 ms of silence between utterances into~/.cache/ambient/fixtures/wer/<speaker>.wav. - Concatenate the matching lines of the chapter’s
.trans.txtin the same order into<speaker>.txt, one utterance per line. - Write
~/.cache/ambient/fixtures/wer/manifest.json: an array of{"wav": "<abs path>", "reference": "<abs path>", "speaker": "1089", "utterances": N, "seconds": S}.
Idempotent: a second run does no network and rewrites nothing that exists
unless --force is passed. Print one line per file it produces and end with
FIXTURES OK <n> files <total seconds>s. Add the cache path to the
## Fixtures section this task creates at the end of
docs/developing/measurements.md, with the exact command.
Prove it by running it: the report quotes the last line and the three durations.
Task 3: A harness that scores Ambient’s own recording path
Add src/bin/wer.rs. It must run the same path ambient record runs for a
finished track, so the number means what a user gets:
- Read the manifest (
--manifest <path>, default the path Task 2 writes). - For each entry:
features::read_wav, thenVad::loadon the silero modelrecorduses,vad.turns(&samples, 30), thenRecognizer::loadon the ASR model directoryrecorduses (find howsrc/session.rsandsrc/main.rsresolve both paths and reuse that, do not hard-code a second copy), thentranscribe_segments. Join the segment texts with single spaces. - Score with
wer::normaliseandwer::scoreagainst the reference file. - Print a table: speaker, seconds, reference words, S, I, D, WER, decode
seconds, realtime factor. Then a total row where WER is computed over the
summed counts, not averaged. With
--json <path>also write those rows as JSON.
Exit 0 on success and non-zero with a clear message if the models or the
manifest are missing (say which file and which script fetches it). Add the
binary to the table in docs/developing/measurements.md with its command.
cargo clippy --all-targets -D warnings covers it, so it must be warning-free.
Prove it by running it on the fixture and quoting the total row in the report.
Task 4: The quality script the loop runs
Add scripts/quality, quiet like scripts/check:
- Run
scripts/fetch-fixtures(cached after the first night) andcargo run --release --bin wer -- --json <tmp>, both with output captured and shown only on failure. - Compare each speaker’s WER and the total against
quality/wer.json(committed). If the file does not exist, write it from this run and printQUALITY BASELINE <total wer>as the last line. Otherwise, fail if any row’s WER exceeds its baseline by more than 0.005 absolute (half a point), printing the offending rows, and end withQUALITY OK <total wer>otherwise. --updaterewritesquality/wer.jsonfrom the current run; that is how an improvement is recorded, and the diff to that file is the evidence in the pull request.
Then add a paragraph to docs/developing/landing.md under “What a night looks
like” saying the quality script runs after the verifier, what its last line
means, and that pointing CHECK_CMD in loop/config at
scripts/check && scripts/quality is a change a person commits (the hooks
deny loop/ commits from a task, so do not attempt it). Commit the first
quality/wer.json from your own run. The report quotes the baseline line and
its first line says CHECK_CMD still needs the daytime change.
Task 5: Both shipped models, one table
models/ holds sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8 and
sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-fp16. Add --model <dir> to the
harness, run it against both, and record a table in
docs/developing/measurements.md with WER, decode seconds and realtime factor
per model, measured on this machine with the date. No pipeline change; the
task ends with the table and the report says which model is currently the
default and whether the table argues for changing it. Changing the default is
not part of this task.
Task 6: Does the resampler cost words?
The fixture is native 16 kHz, so resample::to_16k is never exercised.
Add --via <rate> to the harness: upsample each fixture wav to that rate with
ffmpeg into a cache directory, read it, run resample::to_16k, and score
as before. Run at 48000 (what Core Audio delivers) and 44100. Record the
three-way table (native, 48 k, 44.1 k) in the measurements page. If the
resampled WER is worse by more than half a point, the report names the
resampler as the suspect and stops; fixing it is a task of its own.
Task 7: Padding at turn edges
vad.rs pads each turn by PAD_MS (200 ms) and trims quiet edges. Words at
cut points are the usual casualty. Run the harness at 100, 200, 300 and
400 ms (make the value a parameter of turns for the experiment, defaulting
to the current constant) and record the table. Keep the value that gives the
lowest total WER only if it beats the current one by more than half a point
on every speaker; otherwise restore 200 and record that the default holds.
Either way, update quality/wer.json only if the shipped value changed.
Task 8: Chunk length
turns(&samples, 30) caps a turn at 30 seconds. Run the harness at 15, 20,
30 and 40 (again a parameter for the experiment) and record WER and peak RSS
(from /usr/bin/time -l) for each. Same keep-or-restore rule as Task 7, with
the extra condition that peak RSS must not rise above the 30-second figure
already recorded in the measurements page.
Task 9: Insertions in the gaps
The fixture has 700 ms of silence between utterances. From the harness’s
S/I/D breakdown, insertions are the words Parakeet invented. Add a
--gap <ms> option to scripts/fetch-fixtures so a second fixture set with
3 s gaps can be built, score both, and record the insertion count per fixture.
If insertions rise with gap length, the report says so and names
last_confidence as the lever; tuning it is a later task with its own
number, because the confidence gate also drops real quiet speech.
Task 10: A second fixture that sounds like a call
LibriSpeech is read audiobooks. Earnings-21 (github.com/revdotcom/speech-datasets) is real earnings-call audio: a conference bridge, several speakers, crosstalk, phone-codec sound, with human transcripts under CC BY-SA 4.0. The audio files carry no stated licence, so they are fetched to the cache and never committed, the same as LibriSpeech.
- Extend
scripts/fetch-fixtureswith acallsset beside the existing one. Take the first three ids in sorted order fromearnings21/transcripts/nlp_references/(list the directory with the GitHub API, or pin the three ids the first run found and say so in the script). Fetch eachearnings21/media/<id>.mp3fromhttps://raw.githubusercontent.com/revdotcom/speech-datasets/main/(plain files, not LFS) and each<id>.nlpfromnlp_references/. - Decode the first five minutes of each mp3 to 16 kHz mono 16-bit wav with
ffmpeg -t 300, into~/.cache/ambient/fixtures/calls/<id>.wav. - The
.nlpfile is pipe-delimited with a header; the columns aretoken|speaker|ts|endTs|punctuation|case|tags|wer_tags. Build the reference from thetokencolumn of every row whosetsis under 300 seconds, joined with single spaces, into<id>.txt. Ignore the punctuation and case columns;wer::normalisediscards both anyway. - Write
~/.cache/ambient/fixtures/calls/manifest.jsonin the same shape as the LibriSpeech one.
Then make scripts/quality run the harness on both manifests and keep both
totals in quality/wer.json under clean and calls; the regression rule
applies to each set separately, and the last line becomes
QUALITY OK clean=<wer> calls=<wer>. Add the calls row to the measurements
page with the date. The report quotes both totals and says, in one sentence,
how far apart they are; that gap is what the later experiments should close.