Files
management/docs/issues/0054-ki-geraeuschunterdrueckung-element-call.md
T
Thore CimbalandClaude Opus 4.8 9ff6e09b1d docs(issues): #0054 — prototype works, 35 percent suppression is enough
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>
2026-08-15 12:00:00 +00:00

8.5 KiB
Raw Blame History

type, id, status, created, milestone, priority, area, related
type id status created milestone priority area related
issue 0054 open 2026-08-15 M4 medium element
docs/issues/0029-ui-harmonisieren-gleiche-farben-und-formen.md

Tastaturgeräusche in Calls: quelloffene KI-Geräuschunterdrückung im Client prüfen

Problem

Der WebRTC-Standardfilter (noiseSuppression) ist auf stationäres Rauschen ausgelegt (Lüfter, Netzbrummen). Transiente Geräusche — Tastaturanschläge — rutschen durch: sie haben eine sehr schnelle Anstiegszeit und ein unvorhersehbares Spektrum, sodass die laufende Rauschprofil-Schätzung sie nicht als Störung erkennt. Betroffen sind ausdrücklich auch leise Chiclet-Tastaturen (MacBook), nicht nur mechanische.

Grundlage ist eine externe Architekturspezifikation (Gemini Deep Research, 2026-07-29): client-seitige KI-Filterung via WebAssembly, eingehängt über das LiveKit-TrackProcessor- Interface, mit Intensitätsregler in den Audio-Einstellungen.

Bewertung der Spezifikation

Trägt

  • Diagnose stimmt. Stationär vs. transient ist die richtige Erklärung dafür, warum die vorhandenen Toggles nicht helfen.
  • Client-seitig ist der richtige Ort — und kein Widerspruch zur bisherigen Linie. threadnet-call:docs/axion1337-fork.md §5 verwirft ML-Rauschunterdrückung server-seitig (LiveKit Agents), weil es dort keinen unterstützten Weg gibt, bereinigtes Audio an andere Teilnehmer weiterzureichen. Genau dieser Einwand greift client-seitig nicht. Die Spezifikation setzt die alte Entscheidung fort, statt ihr zu widersprechen.
  • AudioWorklet statt ScriptProcessor, eigener hochpriorer Audio-Thread: richtig und nicht verhandelbar.
  • Der 128-vs-480-Sample-Mismatch (Web Audio liefert 128er-Blöcke, die Modelle brauchen 480) und der nötige Ringpuffer sind sauber benannt — daran scheitern naive Umsetzungen.
  • Chromium-AudioWorklet-Leak und die Gegenmaßnahme (eigener AudioContext, hart schließen) sind real und richtig adressiert.
  • Wasm SIMD ist tatsächlich Voraussetzung, nicht Optimierung.

Trägt nicht

  1. ⚠️ Konkreter Fehler: die Latenzkompensation fehlt. Der finale Dry/Wet-Code mischt das unverzögerte Original mit dem ~40 ms verzögerten KI-Signal. Das erzeugt Kammfilter und Phasenauslöschung — hörbar als blechernes Echo, also genau das Gegenteil des Ziels. Ein früherer Entwurf im selben Gespräch hatte dafür einen DelayNode; in der Endfassung ist er verschwunden. Das ist kein Detail.
  2. Der Dry/Wet-Ansatz ist konzeptionell fragwürdig. Die Begründung („neuronale Netze kennen nur An/Aus") ist für DeepFilterNet falsch — es hat einen nativen Parameter zur Begrenzung der Dämpfung. „Weniger aggressiv" heißt richtig: das Modell weniger dämpfen lassen. Dry/Wet mischt stattdessen ungefiltertes Signal zurück — inklusive der Tastaturanschläge, die man loswerden wollte.
  3. Die Zahlen taugen nicht als Entscheidungsgrundlage. PESQ „RNNoise ~3.88" gegen „DFN3 3.54.34": die untere DFN3-Grenze läge unter RNNoise. Werte aus verschiedenen Testsets, nicht vergleichbar.
  4. Bundle-Größe geschätzt, nicht gemessen („1525 MB"). Für eine Browser-App, die beim Call-Start lädt, ist das der kritische Wert überhaupt — muss gemessen werden.
  5. Die genannten NPM-Pakete sind Experimente (deepfilternet3-worker-test, …-noise-filter-trong). Die Spec empfiehlt selbst, aus dem Rust-Quellcode zu bauen — dann gehört ehrlich dazu: wir übernehmen eine Rust/wasm-Toolchain in die Build-Kette.
  6. Mobil fehlt. Element Call läuft auf Telefonen; DFN3 auf einem Mittelklasse-Android ist offen und wird mit „läuft auf modernen Prozessoren" abgetan.
  7. getUserMedia-Constraints fehlen im Code. Wer selbst filtert, muss die Browser-eigene noiseSuppression abschalten (sonst arbeiten zwei Filter gegeneinander) und echoCancellation erhalten. Im Gespräch erwähnt, im finalen Code verschwunden.
  8. Erzwungene 48 kHz ohne Fallback — Geräte mit 44,1 kHz brauchen einen Pfad.
  9. ⚠️ Der größte Posten fehlt ganz: Fork-Wartung. Das wäre eine erhebliche Eigenentwicklung in threadnet-call, die bei jedem Upstream-Rebase mitgeschleppt und in axion1337-fork.md gepflegt werden muss. Die Spec erwähnt das mit keinem Wort.
  10. Lizenz nur behauptet. DeepFilterNet-Code ist MIT/Apache-2.0 — die Modellgewichte sind separat zu prüfen, bevor „null Lizenzkosten" behauptet wird.

Vorgeschlagenes Vorgehen (vor jeder Zeile Produktivcode)

  1. Messen statt annehmen. Reproduzierbarer A/B-Test mit mechanischer und Chiclet-Tastatur gegen die heutigen Toggles — belegt das Problem und liefert die Referenz für „besser".
  2. Wegwerf-Prototyp außerhalb des Forks. DFN3 als Wasm auf einer eigenen Testseite: Bundle-Größe, CPU und Latenz auf echten Geräten messen (inkl. Telefon). Erst diese Zahlen entscheiden über Modell und Machbarkeit.
  3. Regler über den Modellparameter, nicht über Dry/Wet. Falls doch Dry/Wet: Delay-Node zur Latenzkompensation ist Pflicht.
  4. Dann erst Integration als TrackProcessor und Eintrag in axion1337-fork.md.

Offen (Entscheidung sorb)

Ob der Aufwand lohnt. Die Plattform hat derzeit einen sehr kleinen Nutzerkreis; dem steht eine dauerhaft zu pflegende Fork-Anpassung mit Rust/wasm-Build gegenüber. Die Alternative — Tastaturgeräusche als hinnehmbar erklären und stattdessen Push-to-Talk bzw. bewusstes Stummschalten dokumentieren — ist billiger und sollte bewusst verworfen werden, nicht übersehen.

Prototyp gebaut und getestet 2026-08-15

Wegwerf-Aufbau außerhalb des Forks (deepfilternet3-noise-filter v1.3.0 + livekit-client, lokale Testseite, Assets selbst ausgeliefert). Ziel war, die drei ungeklärten Punkte der Spezifikation durch Messung zu ersetzen.

Ergebnis: es funktioniert — und zwar besser als nötig

Höreindruck sorb: „die Tastatur ist weg, Stimme klingt natürlich" — bei 35 % Dämpfung.

Das ist der wichtigste Einzelbefund, und er entscheidet die Reglerfrage:

  • Der Dry/Wet-Mix der Spezifikation entfällt ersatzlos. Das Paket bietet setSuppressionLevel(), und die wasm-Signatur trägt atten_lim — DeepFilterNet begrenzt die Dämpfung nativ. Die Prämisse der Spec („neuronale Netze kennen nur An/Aus") ist widerlegt. Damit entfallen zugleich der fehlende Delay-Node und das Phasenproblem — es gibt gar keinen zweiten Signalpfad mehr, der phasenversetzt zurückgemischt werden müsste.
  • 35 % statt 100 % als Vorgabe. Die Spec setzt „standardmäßig 100 % Filter-Aktivität"; gemessen reicht gut ein Drittel für „Tastatur weg und Stimme natürlich". Weniger Dämpfung heißt weniger Artefaktrisiko — der Standardwert sollte bei ~35 % liegen, nicht am Anschlag.

Gemessen (ersetzt die Schätzungen der Spec)

Größe Wert Bemerkung
Download je Client 23,27 MB 15,66 MB df_bg.wasm + 7,61 MB Modell — Spec-Schätzung („1525 MB") bestätigt, oberer Rand
Vergleich RNNoise 2,04,6 MB rund ein Zehntel (@jitsi/rnnoise-wasm, @shiguredo/rnnoise-wasm)
Lizenz Paket Apache-2.0 oder MIT wie behauptet; Modellgewichte kommen aus dem DeepFilterNet-Projekt

⚠️ Befund, der die Paketwahl bestimmt: fremdes CDN

deepfilternet3-noise-filter lädt Modell und wasm zur Laufzeit von cdn.mezon.ai. Für eine selbstgehostete Plattform ist das nicht hinnehmbar: jeder Teilnehmer meldet bei jedem Call-Start seine IP an einen Dritten, und die Verfügbarkeit des Calls hinge an fremder Infrastruktur.

Entschärft: assetConfig.cdnUrl ist konfigurierbar. Der Prototyp liefert die Assets bereits lokal aus — der CDN-freie Betrieb ist damit nachgewiesen, nicht nur angenommen. Für eine Integration hieße das: die 23 MB gehören mit ausgeliefert (Image/Ingress), nicht nachgeladen.

Ebenfalls korrigiert gegenüber der Spec

Die getUserMedia-Constraints, die im finalen Spec-Code fehlten, sind im Prototyp gesetzt: noiseSuppression: false (sonst arbeiten Browser-Filter und Modell gegeneinander), echoCancellation: true.

Weiterhin offen — und entscheidungsrelevant

  1. CPU-Last (Jitter mit/ohne Filter) noch nicht abgelesen.
  2. Telefon. Die eigentliche Härteprobe: 23 MB Download und DFN3-Inferenz auf einem Mittelklasse-Gerät. Fällt das durch, braucht es einen Pfad (RNNoise als leichte Variante, oder Filter auf Mobilgeräten aus).
  3. Fork-Wartung bleibt der ungemessene Posten — die Anpassung muss jeden Upstream-Rebase überleben.