Files
management/docs/issues/0054-ki-geraeuschunterdrueckung-element-call.md
T

175 lines
9.6 KiB
Markdown
Raw Normal View History

---
type: issue
id: "0054"
status: open
created: 2026-08-15
milestone: M4
priority: medium
area: element
related:
- "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.
## Entscheidung 2026-08-15 → ADR-0018
sorb: **integrieren, opt-in mit Nachladen, Checkbox plus Regler, Standard 35 %.**
Festgehalten als [ADR-0018](../adr/0018-ki-geraeuschunterdrueckung-clientseitig-opt-in.md).
Der Größeneinwand entfällt damit für alle, die den Filter nicht nutzen: erst das Einschalten
löst den Download aus. Der Telefontest wurde bewusst ausgesetzt — vertretbar, weil der Filter
auf schwachen Geräten schlicht aus bleibt.
### Umsetzung (offen)
1. `deepfilternet3-noise-filter` als Abhängigkeit in `threadnet-call`.
2. `TrackProcessor` an den lokalen Audio-Track hängen; `assetConfig.cdnUrl` auf das eigene
Deployment zeigen (**nicht** auf `cdn.mezon.ai`).
3. Assets (23,3 MB) in die Auslieferung aufnehmen.
4. Audio-Einstellungen: Checkbox + Regler, Standard aus / 35 %.
5. `getUserMedia`: `noiseSuppression: false`, `echoCancellation: true`.
6. Eintrag in `threadnet-call:docs/axion1337-fork.md` als Fork-Anpassung mit Portier-Hinweis
(§5 dort ergänzen — die Absage galt der Server-Seite und bleibt gültig).
Der Prototyp liegt außerhalb der Repos und ist Wegwerf-Material; er wird nicht eingecheckt.