diff --git a/STATUS.md b/STATUS.md index f242b74..b10b56d 100644 --- a/STATUS.md +++ b/STATUS.md @@ -34,7 +34,7 @@ Verteilung: M1 5 · M2 12 · M4 3 · M5 1 _none active_ -## ADRs (17) +## ADRs (18) | ADR | Status | Title | |---|---|---| @@ -55,6 +55,7 @@ _none active_ | [0015](docs/adr/0015-wiki-git-storage-ueber-gitea-kanonisieren.md) | accepted | 0015 — Wiki.js Git-Storage: Inhalt fließt Cluster→Gitea→kanonisiert nach git.lab | | [0016](docs/adr/0016-notfallhandbuch-nicht-spiegeln.md) | accepted | 0016 — Das Notfallhandbuch bleibt lab-intern und wird nicht gespiegelt | | [0017](docs/adr/0017-split-dns-cfgmon-vier-zonen.md) | accepted | 0017 — Split-DNS auf CFGMON: vier Zonen statt einer, je Zone begründet | +| [0018](docs/adr/0018-ki-geraeuschunterdrueckung-clientseitig-opt-in.md) | accepted | 0018 — KI-Geräuschunterdrückung in threadnet-call: client-seitig, opt-in, selbst ausgeliefert | ## Open AARs (2) diff --git a/docs/adr/0018-ki-geraeuschunterdrueckung-clientseitig-opt-in.md b/docs/adr/0018-ki-geraeuschunterdrueckung-clientseitig-opt-in.md new file mode 100644 index 0000000..a238366 --- /dev/null +++ b/docs/adr/0018-ki-geraeuschunterdrueckung-clientseitig-opt-in.md @@ -0,0 +1,97 @@ +--- +type: adr +id: "0018" +status: accepted +date: 2026-08-15 +supersedes: null +superseded_by: null +related: + - "docs/issues/0054-ki-geraeuschunterdrueckung-element-call.md" +--- + +# 0018 — KI-Geräuschunterdrückung in threadnet-call: client-seitig, opt-in, selbst ausgeliefert + +**Status:** akzeptiert · **Datum:** 2026-08-15 · **Entscheider:** sorb + +## Kontext + +Der WebRTC-Standardfilter (`noiseSuppression`) schätzt ein laufendes Rauschprofil und ist +damit auf **stationäre** Störungen ausgelegt. **Transiente** Geräusche — Tastaturanschläge — +erkennt er nicht als Störung; sie werden mitübertragen. Betroffen sind ausdrücklich auch +leise Chiclet-Tastaturen, nicht nur mechanische. Push-to-Talk als Ausweg wurde verworfen +(„inakzeptabel", sorb). + +`threadnet-call:docs/axion1337-fork.md` §5 hatte ML-Rauschunterdrückung bereits einmal +verworfen — allerdings **server-seitig** (LiveKit Agents), weil es dort keinen unterstützten +Weg gibt, bereinigtes Audio an andere Teilnehmer weiterzureichen. Client-seitig greift dieser +Einwand nicht; diese ADR widerspricht der damaligen Entscheidung also nicht, sondern setzt sie +fort. + +Eine externe Architekturspezifikation empfahl DeepFilterNet3 via WebAssembly. Statt sie zu +übernehmen, wurde ein **Wegwerf-Prototyp** gebaut und gemessen (#0054). Das hat drei ihrer +Kernannahmen korrigiert und die Entscheidungsgrundlage von Schätzung auf Messung gestellt. + +## Entscheidung + +**DeepFilterNet3 wird client-seitig integriert — opt-in, nachgeladen, selbst ausgeliefert.** + +1. **Modell:** DeepFilterNet3 über `deepfilternet3-noise-filter` (Apache-2.0 ODER MIT) als + LiveKit-`TrackProcessor`. Nicht RNNoise: es ist zwar zehnmal kleiner, aber bei Transienten + deutlich schwächer — genau dem Fall, um den es hier geht. +2. **Opt-in mit Nachladen.** Standard **AUS**. Die Assets (23,3 MB) werden **erst beim + Einschalten** geladen. Damit zahlt nur, wer profitiert — das Größenargument entfällt für + alle anderen. +3. **Bedienung:** Checkbox zum Aktivieren **plus Regler** für die Stärke. +4. **Standardwert 35 %**, nicht 100 %. Gemessen reicht gut ein Drittel für „Tastatur weg und + Stimme natürlich"; weniger Dämpfung heißt weniger Artefaktrisiko. +5. **Regelung über den Modellparameter** (`setSuppressionLevel` / `atten_lim`), **nicht** über + einen Dry/Wet-Mix. +6. **Assets werden selbst ausgeliefert.** Der Default-Pfad des Pakets lädt sie von + `cdn.mezon.ai`; `assetConfig.cdnUrl` wird auf das eigene Deployment gezeigt. +7. **`getUserMedia`:** `noiseSuppression: false` (sonst arbeiten Browser-Filter und Modell + gegeneinander), `echoCancellation: true`. + +## Gemessene Grundlage (Prototyp 2026-08-15) + +| | | +|---|---| +| Wirkung | Tastatur weg, Stimme natürlich — **bei 35 %** | +| Download je Client | **23,27 MB** (15,66 MB wasm + 7,61 MB Modell) | +| Vergleich RNNoise | 2,0–4,6 MB, also ~ein Zehntel | +| Paketpflege | 20 Versionen, 4 Maintainer, ~20k Downloads/Monat | +| CDN-freier Betrieb | im Prototyp nachgewiesen | + +## Konsequenzen + +- **Kein Rust/wasm-Build.** Die Spezifikation hielt ihn für nötig; das fertige Paket macht ihn + entbehrlich. Die Fork-Anpassung bleibt dadurch klein: Abhängigkeit, `TrackProcessor`, + Bedienelement, Assets in der Auslieferung. +- **Kein Dry/Wet-Mixer, kein Delay-Node.** Da über den Modellparameter geregelt wird, gibt es + keinen zweiten Signalpfad — und damit weder Phasenauslöschung noch Latenzkompensation. +- **23,3 MB gehören ins Deployment.** Sie müssen mit ausgeliefert werden (Image/Ingress) und + wachsen bei einem Modell-Update mit. +- **Dauerhafte Fork-Anpassung.** Sie muss jeden Upstream-Rebase überleben und gehört in + `axion1337-fork.md` samt Portier-Hinweis. +- ⚠️ **Mobil ist ungeprüft.** Der Telefontest wurde bewusst ausgesetzt. Weil der Filter opt-in + ist, ist das vertretbar: Auf schwachen Geräten bleibt er schlicht aus. Zeigt sich später, + dass er dort unbrauchbar ist, ist das ein Folge-Issue, kein Widerruf dieser Entscheidung. +- **Neue Lieferkette.** Ein npm-Paket eines Drittanbieters liefert Code, der WebAssembly lädt. + Die **Assets** kontrollieren wir (selbst gehostet); der 23-KB-Wrapper bleibt Fremdcode. + Vendoring wäre möglich und wurde bewusst nicht gewählt — dann müssten wir Updates selbst + nachziehen. + +## Verworfene Alternativen + +- **Push-to-Talk / bewusstes Stummschalten dokumentieren:** billigste Lösung, aber ein + Rückschritt gegenüber dem, was Konferenzsysteme heute leisten. Ausdrücklich verworfen. +- **RNNoise** statt DFN3: ein Zehntel der Größe, aber konzeptionell schwach bei genau den + transienten Geräuschen, die das Problem sind. +- **Server-seitige ML-Filterung:** bereits in `axion1337-fork.md` §5 verworfen — LiveKit + bietet keinen unterstützten Weg, bereinigtes Audio an andere Teilnehmer weiterzugeben. +- **Standardmäßig AN:** beste Wirkung ohne Zutun, aber jeder Client lädt 23 MB — auch auf + Telefonen, die ungetestet sind. +- **Dry/Wet-Mix als Regler** (Vorschlag der Spezifikation): mischt ungefiltertes Signal + zurück, **inklusive der Tastaturanschläge**, und braucht eine Latenzkompensation, die im + Spec-Entwurf fehlte. Der native Modellparameter leistet dasselbe ohne diese Nachteile. +- **Assets vom Anbieter-CDN laden:** würde bei jedem Call-Start die IP jedes Teilnehmers an + einen Dritten melden und die Verfügbarkeit an fremde Infrastruktur hängen. diff --git a/docs/issues/0054-ki-geraeuschunterdrueckung-element-call.md b/docs/issues/0054-ki-geraeuschunterdrueckung-element-call.md index e40306a..78c2ca2 100644 --- a/docs/issues/0054-ki-geraeuschunterdrueckung-element-call.md +++ b/docs/issues/0054-ki-geraeuschunterdrueckung-element-call.md @@ -150,3 +150,25 @@ Die `getUserMedia`-Constraints, die im finalen Spec-Code fehlten, sind im Protot 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.