The first production image carrying the filter broke unmuting for everyone.
Records both causes (missing AudioContext on the on-path, a stray
processor: undefined leaking into getUserMedia constraints on the off-path),
the fix with its deliberately-red-first regression tests, the passed two-person
acceptance call, and the standing lesson: for changes in the microphone path,
a real-call acceptance is a rollout precondition, not an afterthought.
The filter stays gated off until the webAudioMix decision - that follow-up is
what makes ADR-0018 implementable or refutes it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rolling out revealed that the first implementation would not have worked in
production: Element Call ships only as the embedded npm package, and that build
sets publicDir: false because upstream's public/ holds nothing but a favicon.
With that value everything builds, standalone works, and the filter is dead only
inside the widget — verified, not assumed.
Fixed in threadnet-call d270e0c and confirmed end to end: the widget URL resolves
to the model path and the CI artifact carries the assets, not just the local build.
Also corrects a number I gave when asking for the asset decision: the package goes
from 41 to 66 MB, not from 2 to 25 — it already contained source maps and the
crypto and vision wasm.
publish_npm stays manual by design and is sorb's to trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All repo checks green including the connection factory tests that cover the
changed noiseSuppression logic. Two findings while building improved the numbers:
the Dockerfile's gzip glob missed the model wasm in its subdirectory, costing
15.7 MB instead of 4.1 MB per client, and the 23 MB verifiably stay out of the JS
bundle, so the opt-in lazy load works as designed.
sorb chose to commit the assets rather than fetch them at build time — fetching
would have reintroduced the very third-party dependency we removed at runtime,
just moved to build time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sorb's decision after the prototype: integrate DeepFilterNet3 as a LiveKit track
processor, off by default, assets fetched only when the user enables it, checkbox
plus slider, 35 percent default.
Opt-in is what makes the 23.3 MB affordable — only those who benefit pay for it.
Three of the source specification's assumptions did not survive measurement and are
recorded as rejected alternatives: the dry/wet mixer (the model limits attenuation
natively, and mixing raw signal back would return the keystrokes), the Rust/wasm
build (a maintained package makes it unnecessary), and loading assets from the
vendor CDN (every participant's IP to a third party at call start).
Mobile stays untested by choice; since the filter is opt-in it simply stays off on
weak devices, so that is a follow-up rather than a blocker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Built the throwaway prototype and sorb tested it: keyboard gone, voice natural,
at 35 percent rather than the 100 percent the spec assumed as default. That
settles the control question — the package exposes setSuppressionLevel and the
wasm carries atten_lim, so DeepFilterNet limits attenuation natively and the
spec's dry/wet mix drops out entirely, taking its missing delay node and phase
problem with it.
Measured what the spec had guessed: 23.27 MB per client, an order of magnitude
above RNNoise. Also found that the package fetches model and wasm from a
third-party CDN at call time, which a self-hosted platform cannot accept — the
asset URL is configurable and the prototype already serves them locally, so that
path is proven rather than assumed.
Still open and decision-relevant: CPU figures, the phone, and the standing cost of
carrying this through every upstream rebase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ADR-0006 (Docusaurus as the shared reading surface) is superseded by ADR-0014,
which ADR-0014 had only recorded for ADR-0007. The schema has no 'deprecated', so
superseded with a pointer is the fitting lifecycle state, same shape as ADR-0007.
#0054 evaluates an external architecture spec for filtering keyboard noise with a
WebAssembly model in the client. It holds up on diagnosis, placement and the
awkward parts (128-vs-480 sample buffering, the Chromium worklet leak, SIMD), and
it does not contradict the fork's earlier rejection of ML denoising — that one was
about the server side, for a reason that does not apply here.
It does not hold up on: a missing delay node, which would make the dry/wet mix comb
filter audibly; the premise behind dry/wet at all, since DeepFilterNet can limit
attenuation natively and mixing raw signal back in returns the very keystrokes we
want gone; PESQ figures compared across different test sets; unmeasured bundle size;
throwaway npm packages; and no mention of the standing cost of carrying this through
every upstream rebase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>