Field finding on Safari as the sending client: assets load, setProcessor
reports success, yet suppression level 0-100 makes no audible difference -
because LiveKit swaps the sender track via 'this.sender?.replaceTrack(...)',
and when the sender is not there at that instant the swap is skipped silently,
leaving the raw microphone on the wire. Chromium clients hit the timing,
Safari does not.
applyAiNoiseSuppression now verifies instead of trusting: wait for the sender
if needed, enforce the swap explicitly, and state the outcome in the log line
('Sendepfad gefiltert: ja/NEIN'). Three tests pin the sender cases, including
the exact Safari symptom of a sender still carrying the raw track.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-person call on 2026-08-17: filter effective, keyboard gone, unmuting intact
on both sides - the acceptance that gates this flag, per the standing rule from
the v0.5.0 incident. The gate flips to true, which brings the checkbox and
slider back into the in-call settings.
The dev override stays in the code as the tool for the next test phase of this
kind; a test pins that it does nothing without the regular setting. The rollback
lever for any regression is the gate itself, not a deployment revert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Instead of webAudioMix on the room - which would also rewire playback (sink
selection through the AudioContext, LiveKit's Chrome echo workaround) - only the
local microphone track gets an AudioContext, via setAudioContext() right before
setProcessor(). The attach happens in onLocalTrackPublished, so a failing filter
can no longer prevent unmuting: the track is already published by then.
audioCaptureDefaults now never carry a processor key in any state; the
conditional spread only toggles noiseSuppression. A regression test covers the
active case too.
The gate stays closed. A single test client opts in via two localStorage keys
(ai-noise-suppression-dev plus the regular setting); the regular setting alone
stays inert. Four unit tests pin the attach order - setAudioContext before
setProcessor is exactly what v0.5.0 lacked - and the containment of a failing
attach. 77 tests green across the touched suites.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
threadnet.8 broke unmuting in production, both ways. With the filter on, LiveKit
refuses the processor because the room is built without webAudioMix, so no local
track ever carries an AudioContext. With the filter off, the options builder
still emitted processor: undefined and rewrote noiseSuppression - LiveKit copies
every key of audioCaptureDefaults into the getUserMedia constraints, undefined
included, and Safari stopped unmuting over it.
The off-path is now a conditional spread that produces an object identical to
upstream: no processor key at all, noiseSuppression untouched. Two regression
tests pin this down and were demonstrably red on the old code.
The feature itself is hard-gated off (AI_NOISE_SUPPRESSION_AVAILABLE) until the
webAudioMix decision is made and tested in a two-person call. The gate also
neutralizes clients that enabled the setting before - that state lives in
localStorage and survives every deployment. The settings UI hides behind the
same gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WebRTC noise suppression estimates a running noise profile, so it removes
stationary noise but not keystrokes, which are transient and never look like
noise to it. Chiclet keyboards are as affected as mechanical ones. DeepFilterNet3
runs client-side as a LiveKit track processor and removes them.
Off by default and the model is only fetched once a user switches it on, so
nobody pays the download who does not want the filter. Default strength is 35
percent rather than full: measured, that already gives keystrokes gone with the
voice still natural, and more attenuation only adds artefact risk.
Assets ship with us instead of the package's default CDN, which would report
every participant's IP to a third party at call start and tie call setup to
foreign infrastructure. The Dockerfile gzips the model wasm, which the existing
top-level glob missed — 4.1 MB instead of 15.7 MB per client.
Browser noise suppression is switched off while the filter runs so the two do not
work against each other. Regulation goes through the model's own attenuation
limit, so there is no dry/wet mixer and no delay compensation to get wrong.
Decision and measurements: management ADR-0018 and issue #0054.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Registry-Entscheidung evidenzbasiert: das Package ist pnpm-Dependency von
ThreadNet-Webs apps/web, der Lockfile pinnt die Tarball-URL auf rohana -
Registry bleibt dort. Publish-Auth ueber CI-Variable GITEA_NPM_TOKEN statt
lokaler Klartext-.npmrc.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>