Skip to content

test: cover earshot backend and frame adapter - #57

Merged
wavekat-eason merged 1 commit into
mainfrom
test/earshot-coverage
Aug 30, 2026
Merged

wavekat-eason merged 1 commit into
mainfrom
test/earshot-coverage

Conversation

@wavekat-eason

Copy link
Copy Markdown
Contributor

Follow-up to #55. The backend landed with plumbing tests — frame sizes, error types, the carry invariant, allocation counts — but nothing that measured whether it actually detects speech, and it was not registered with the accuracy or benchmark harnesses. This adds that, plus coverage for the parts of FrameAdapter the rework left untested.

Accuracy

tests/accuracy.rs now evaluates earshot on the same 30-file TEN-VAD testset as every other backend, and it has a baseline entry:

Backend Precision Recall F1 Frame Avg inference RTF
Earshot 0.909 0.949 0.928 256 (16 ms) ~4 µs 0.0003

F1 matches TEN-VAD (0.928) at roughly an order of magnitude less time per frame, with no native code in the build.

earshot is added to the accuracy jobs in ci.yml and vad-accuracy.yml, and to make accuracy, make accuracy-update-baseline, and make bench — so the README benchmark table picks up an Earshot row the next time the VAD Accuracy workflow runs, and the baseline is checked on every push to main.

Backend tests (src/backends/earshot.rs)

Range and finiteness checks hold for any implementation that returns a number, so these pin down behaviour instead:

  • a_signal_scores_clearly_above_digital_silence / silence_never_crosses_the_suggested_threshold — the model responds to its input and separates voiced content from silence at the suggested 0.5 threshold.
  • rejected_frames_leave_the_stream_state_untouched — the documented "validated before any state is touched" property: interleaving wrong-rate and wrong-length calls between valid frames reproduces the clean score sequence exactly.
  • context_carries_across_frame_boundaries — a continuous stream scores differently from the same frames scored in isolation, so the per-stream recurrent context is reaching the model and frame ordering matters.
  • default_matches_new, two_fresh_detectors_score_identically — no hidden global or non-deterministic state.

Adapter tests (src/adapter.rs)

  • reset_forwards_to_the_inner_detector — reset() clears the carry buffer and the wrapped detector. Only the buffer half was covered.
  • timings_come_from_the_inner_detector, accessors_mirror_the_inner_detectors_capabilities — delegation.
  • zero_frame_size_is_rejected_by_every_api — the frame_size == 0 branch, across all four entry points, with no input consumed.
  • errors_still_report_the_frames_scored_before_the_failure — the documented on_score contract on the error path.
  • randomised_chunk_boundaries_preserve_exact_framing — the existing exact-ordering assertions, driven by a seeded LCG across five frame sizes rather than a fixed list of chunk sizes.

End-to-end (tests/earshot_stream.rs)

Public API only, in the shape a transport uses it:

  • transport_chunks_score_identically_to_aligned_frames and jittery_chunk_sizes_score_identically_to_aligned_frames — feeding audio as 320-sample packets, or as pseudo-random chunk sizes, produces the exact same score sequence as feeding aligned 256-sample frames directly. This is the property the adapter exists to provide, and it ties the framing fix in feat: add pure-Rust earshot VAD backend #55 to the backend.
  • a_minute_of_streaming_stays_in_step — 3000 packets, asserting after each one that every sample fed in has either been scored or is still carried, and that the carry stays under a frame.
  • a_voiced_region_scores_above_the_silence_around_it — silence → signal → silence through the adapter.
  • reset_restarts_the_stream_through_the_adapter, a_wrong_rate_packet_does_not_disturb_the_stream.

Verification

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, and clippy with --no-default-features and --features earshot are clean. Tests pass with no features, with earshot, and on the default workspace set; the full feature combination compiles. The accuracy report was run locally against the downloaded testset to produce the baseline numbers above.

Each new test was checked to fail when the behaviour it describes is removed, so none of them pass by construction.

🤖 Generated with Claude Code

https://claude.ai/code/session_012PF9YKsA6GfchGTPDAWYzL

Adds behavioural coverage for the earshot backend and the FrameAdapter
rework, and registers earshot with the accuracy and benchmark harnesses
so it appears alongside the other backends.

- accuracy harness: earshot is evaluated on the TEN-VAD testset and has
  a baseline entry (P 0.909 / R 0.949 / F1 0.928, ~4 us per frame).
- backend tests: score separation between signal and silence, stream
  state after a rejected frame, Default/new equivalence, reproducibility
  across instances, and recurrent context across frame boundaries.
- adapter tests: reset and timings delegation to the wrapped detector,
  capability accessors, zero frame size, score reporting on the error
  path, and framing under randomised chunk boundaries.
- tests/earshot_stream.rs: end-to-end streaming through the public API,
  asserting that packetisation does not change scores, that a minute of
  audio stays in step, and that reset restarts the stream.
- earshot is added to the accuracy jobs, `make accuracy`, and
  `make bench`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PF9YKsA6GfchGTPDAWYzL
@wavekat-eason
wavekat-eason merged commit 9d4965d into main Aug 30, 2026
11 checks passed
@wavekat-eason
wavekat-eason deleted the test/earshot-coverage branch August 30, 2026 00:37
@github-actions github-actions Bot mentioned this pull request Aug 30, 2026
wavekat-eason added a commit that referenced this pull request Aug 30, 2026
Follow-up to #57.

## The release PR's table has no Earshot row

There are four places that spell out the backend feature list. #57
updated three of them, but missed the one that actually matters for a
release: the `update-benchmarks` job in `release-plz.yml`. That job
checks out the release PR branch, runs the accuracy report, and pushes
the regenerated table onto it — and it was still running with
`--features webrtc,silero,ten-vad,firered`.

That is why #56 currently carries a table stamped *v0.1.17* listing four
backends, with Earshot absent from the release that introduces it.

Verified there are no other stragglers:

```
$ grep -rn "ten-vad,firered" --include="*.yml" --include=Makefile --include="*.rs" .
.github/workflows/release-plz.yml:97      <- this PR
.github/workflows/vad-accuracy.yml:32     <- has earshot
.github/workflows/ci.yml:53,88            <- has earshot
Makefile:54,59,63,68                      <- has earshot
crates/wavekat-vad/tests/accuracy.rs      <- has earshot
```

## The manual accuracy workflow cannot open its PR

Dispatching **VAD Accuracy** on main measured everything correctly and
updated the README in the workspace, then failed on the last step:

```
remote: Duplicate header: "Authorization"
fatal: unable to access 'https://github.com/wavekat/wavekat-vad/': The requested URL returned error: 400
```

`actions/checkout` leaves a persisted
`http.https://github.com/.extraheader` credential behind, and
`create-pull-request` adds its own — two `Authorization` headers, and
the push is rejected. The checkout in that workflow no longer persists
credentials, and the action moves from v6 to the current major (v8 is v7
plus a Node 24 runner requirement; nothing this workflow uses changed).

`release-plz.yml`'s benchmark job pushes with plain `git push`, so it
was never affected by this — which is why the table on the release
branch updates while the manual workflow fails.

## Measured numbers

From the run on `ubuntu-latest`, which matches the local measurement
exactly on the deterministic metrics:

| Backend | Precision | Recall | F1 | Frame | Avg inference | RTF |
|---|---|---|---|---|---|---|
| WebRTC | 0.821 | 0.983 | 0.895 | 480 (30 ms) | 2.9 µs | 0.0001 |
| Silero | 0.938 | 0.938 | 0.938 | 512 (32 ms) | 117.9 µs | 0.0037 |
| TEN-VAD | 0.942 | 0.915 | 0.928 | 256 (16 ms) | 51.0 µs | 0.0032 |
| FireRedVAD | 0.950 | 0.879 | 0.913 | 160 (10 ms) | 647.4 µs | 0.0647 |
| Earshot | 0.909 | 0.949 | 0.928 | 256 (16 ms) | 8.5 µs | 0.0005 |

Once this lands, re-running Release-plz regenerates the table on #56
with the Earshot row, and v0.1.17 can go out.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_012PF9YKsA6GfchGTPDAWYzL

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wavekat-eason added a commit that referenced this pull request Aug 30, 2026
Follow-up to #55 / #57 / #58.

## What was missing

#55 added Earshot to the backends table, the dependency snippet, and the
feature-flag table — but not to the part of the README where each
backend is actually explained. WebRTC, Silero, TEN-VAD and FireRedVAD
each have a `###` section with a description and a runnable example;
Earshot went straight from the FireRedVAD example to the trait docs.
Someone who enabled the feature had nowhere to look for how to construct
it.

The new section follows the same shape as the other four and calls out
the three things that differ from them:

- construction is infallible, so there is no `.unwrap()` on `new()`
- the frame size is exactly 256 samples, with a pointer to
`FrameAdapter` for 20 ms / 320-sample transports
- each detector holds ~8 KiB of streaming state, so one per stream, and
`reset()` between streams

## `FrameAdapter::process_each`

The adapter rework in #55 added `process_each` and made the allocation
behaviour of each method meaningful, but the README still listed only
the three older methods. Added it to the example, with a note on which
methods allocate and what `buffered_samples()` reports — this is the
method to reach for on a real-time path, and it was invisible.

## Acknowledgements

Added the earshot crate (MIT OR Apache-2.0, by pyke — the same authors
as `ort`, already credited).

## Notes

- Both new code examples were compiled and run against the crate. The
README is not wired into doctests, so CI would not have caught a mistake
in them.
- I did not repeat upstream's accuracy claim. Their README states
Earshot is more accurate than Silero VAD v6 and TEN VAD; on the TEN-VAD
testset this crate benchmarks against, our measurement puts it at F1
0.928 — level with TEN-VAD and below Silero's 0.938, with the speed
advantage intact. The benchmark table speaks for itself, so the section
sticks to characteristics rather than comparisons.
- The benchmark table itself is untouched. It is generated by the
`update-benchmarks` job and #56 already has the regenerated five-row
version, so hand-editing it here would only conflict.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_012PF9YKsA6GfchGTPDAWYzL

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wavekat-eason added a commit that referenced this pull request Aug 30, 2026
## 🤖 New release

* `wavekat-vad`: 0.1.16 -> 0.1.17 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

##
[0.1.17](v0.1.16...v0.1.17)
- 2026-08-30

### Added

- add pure-Rust earshot VAD backend
([#55](#55))

### Other

- add earshot section to the README
([#59](#59))
- add conformance suite and semver check
([#60](#60))
- cover earshot backend and frame adapter
([#57](#57))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Eason WaveKat <eason@wavekat.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant