feat(audio): opt-in AI noise suppression against keyboard noise
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c63be9ab94
commit
3f17001720
+6
-1
@@ -4,7 +4,12 @@ COPY ./dist /dist
|
|||||||
|
|
||||||
# Compress assets to work with nginx-gzip-static-module
|
# Compress assets to work with nginx-gzip-static-module
|
||||||
WORKDIR /dist/assets
|
WORKDIR /dist/assets
|
||||||
RUN gzip -k ../index.html *.js *.map *.css *.wasm *-app-*.json
|
RUN gzip -k ../index.html *.js *.map *.css *.wasm *-app-*.json
|
||||||
|
|
||||||
|
# ThreadNet-Fork: das DeepFilterNet3-wasm liegt in einem Unterordner und wird vom
|
||||||
|
# Glob oben nicht erfasst - ungzippt waeren das 15,7 MB statt 4,1 MB pro Client.
|
||||||
|
# Das .tar.gz des Modells ist bereits komprimiert und wird bewusst ausgelassen.
|
||||||
|
RUN find dfn3 -name '*.wasm' -exec gzip -k {} + || true
|
||||||
|
|
||||||
FROM nginxinc/nginx-unprivileged:alpine-slim
|
FROM nginxinc/nginx-unprivileged:alpine-slim
|
||||||
|
|
||||||
|
|||||||
+52
-12
@@ -70,6 +70,45 @@ ML-Noise-Cancellation (z.B. selbst gehostetes DTLN/RNNoise) - LiveKits eigene Do
|
|||||||
beschreibt diesen Baustein als für AI-Voice-Agents gedacht, nicht für Mensch-zu-Mensch-Calls
|
beschreibt diesen Baustein als für AI-Voice-Agents gedacht, nicht für Mensch-zu-Mensch-Calls
|
||||||
(kein unterstützter Weg, bereinigtes Audio an andere Teilnehmer weiterzuleiten).
|
(kein unterstützter Weg, bereinigtes Audio an andere Teilnehmer weiterzuleiten).
|
||||||
|
|
||||||
|
## 5b. KI-Geraeuschunterdrueckung client-seitig (ADR-0018)
|
||||||
|
|
||||||
|
Ergaenzt §5: dort ging es um **server-seitige** ML-Filterung, die verworfen bleibt. Diese
|
||||||
|
Anpassung ist **client-seitig** und damit von jenem Einwand nicht betroffen.
|
||||||
|
|
||||||
|
**Warum:** Der WebRTC-Standardfilter schaetzt ein laufendes Rauschprofil und filtert damit
|
||||||
|
STATIONAERE Stoerungen. Tastaturanschlaege sind TRANSIENT und werden nicht als Stoerung
|
||||||
|
erkannt - sie gehen ungefiltert raus. Betroffen sind auch leise Chiclet-Tastaturen.
|
||||||
|
|
||||||
|
**Was geaendert wurde:**
|
||||||
|
|
||||||
|
| Datei | Aenderung |
|
||||||
|
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `src/livekit/aiNoiseSuppression.ts` | **neu** - baut den DeepFilterNet3-TrackProcessor |
|
||||||
|
| `src/settings/settings.ts` | zwei Settings: `ai-noise-suppression` (bool, Default **false**), `ai-noise-suppression-level` (number, Default **35**) |
|
||||||
|
| `src/state/CallViewModel/remoteMembers/ConnectionFactory.ts` | `processor:` in `audioCaptureDefaults`; `noiseSuppression` wird bei aktivem KI-Filter **abgeschaltet** |
|
||||||
|
| `src/settings/SettingsModal.tsx` | `AiNoiseSuppressionSettings` im Audio-Tab (Checkbox + Regler) |
|
||||||
|
| `public/assets/dfn3/**` | Modell + wasm, **23 MB**, selbst ausgeliefert |
|
||||||
|
| `package.json` | `deepfilternet3-noise-filter` |
|
||||||
|
|
||||||
|
**Drei Dinge, die beim Portieren nicht verlorengehen duerfen:**
|
||||||
|
|
||||||
|
1. **Assets selbst ausliefern.** Das Paket laedt Modell und wasm sonst von `cdn.mezon.ai` -
|
||||||
|
das meldet bei jedem Call-Start die IP jedes Teilnehmers an einen Dritten und haengt die
|
||||||
|
Verfuegbarkeit an fremde Infrastruktur. `assetConfig.cdnUrl` zeigt deshalb auf
|
||||||
|
`public/assets/dfn3/`. **Beim Aktualisieren des Pakets pruefen, ob sich die Asset-Pfade
|
||||||
|
geaendert haben** (`v3/pkg/df_bg.wasm`, `v3/models/DeepFilterNet3_onnx.tar.gz`).
|
||||||
|
2. **Browser-`noiseSuppression` bleibt bei aktivem KI-Filter AUS.** Sonst arbeiten zwei
|
||||||
|
Filter gegeneinander und der Browser schneidet dem Modell bereits Signalanteile weg.
|
||||||
|
3. **Default 35 %, nicht 100 %.** Gemessen reicht das fuer "Tastatur weg und Stimme
|
||||||
|
natuerlich"; mehr Daempfung erhoeht nur das Artefaktrisiko.
|
||||||
|
|
||||||
|
**Kein** Rust/wasm-Build noetig (das fertige Paket genuegt), **kein** Dry/Wet-Mixer und
|
||||||
|
**kein** Delay-Node - geregelt wird ueber den Modellparameter (`noiseReductionLevel` /
|
||||||
|
`atten_lim`), nicht ueber Zurueckmischen des Originalsignals.
|
||||||
|
|
||||||
|
⚠️ **Mobil ungeprueft.** Der Telefontest wurde bewusst ausgesetzt; weil der Filter opt-in
|
||||||
|
ist, bleibt er auf schwachen Geraeten schlicht aus. Messung und Herleitung: management #0054.
|
||||||
|
|
||||||
## 6. Produktname im Widget (`.env.production`)
|
## 6. Produktname im Widget (`.env.production`)
|
||||||
|
|
||||||
`VITE_PRODUCT_NAME=aXion1337.Chat` in `.env.production` — eine Zeile, keine
|
`VITE_PRODUCT_NAME=aXion1337.Chat` in `.env.production` — eine Zeile, keine
|
||||||
@@ -82,24 +121,24 @@ heißt es aXion1337.Chat — und das Call-Widget läuft in der Anwendung.
|
|||||||
|
|
||||||
**Was dadurch tatsächlich umbenannt wird** (Inventur 2026-08-06, embedded-Modus):
|
**Was dadurch tatsächlich umbenannt wird** (Inventur 2026-08-06, embedded-Modus):
|
||||||
|
|
||||||
| Stelle | sichtbar wo |
|
| Stelle | sichtbar wo |
|
||||||
|---|---|
|
| ------------------------------------ | ------------------------------------------------------- |
|
||||||
| `error.matrix_rtc_transport_missing` | Fehlermeldung „Der Server ist nicht für … konfiguriert" |
|
| `error.matrix_rtc_transport_missing` | Fehlermeldung „Der Server ist nicht für … konfiguriert" |
|
||||||
| `error.open_elsewhere_description` | „… wurde in einem anderen Tab geöffnet" |
|
| `error.open_elsewhere_description` | „… wurde in einem anderen Tab geöffnet" |
|
||||||
| `DeveloperSettingsTab` Versionszeile | Einstellungen → Entwicklermodus |
|
| `DeveloperSettingsTab` Versionszeile | Einstellungen → Entwicklermodus |
|
||||||
| `usePageTitle` → `document.title` | im iframe unsichtbar, der Vollständigkeit halber |
|
| `usePageTitle` → `document.title` | im iframe unsichtbar, der Vollständigkeit halber |
|
||||||
|
|
||||||
**Was im Embedded-Build ohnehin nie erscheint** — und deshalb *nicht* angefasst
|
**Was im Embedded-Build ohnehin nie erscheint** — und deshalb _nicht_ angefasst
|
||||||
wurde:
|
wurde:
|
||||||
|
|
||||||
- **Die Logo-SVGs.** Alle vier sind importiert und liegen im Bundle — gerendert wird
|
- **Die Logo-SVGs.** Alle vier sind importiert und liegen im Bundle — gerendert wird
|
||||||
im Widget aber keins, und zwar jeweils durch dieselbe Bedingung gesperrt:
|
im Widget aber keins, und zwar jeweils durch dieselbe Bedingung gesperrt:
|
||||||
|
|
||||||
| SVG | gerendert von | Sperre |
|
| SVG | gerendert von | Sperre |
|
||||||
|---|---|---|
|
| ---------------------- | --------------------------- | ----------------------------------------------------------------- |
|
||||||
| `Logo` | `Header.tsx` (`HeaderLogo`) | `FullScreenView` zeigt den Header nur bei `header === "standard"` |
|
| `Logo` | `Header.tsx` (`HeaderLogo`) | `FullScreenView` zeigt den Header nur bei `header === "standard"` |
|
||||||
| `LogoMark`, `LogoType` | `CallFooter.tsx` | `showLogo = headerStyle === HeaderStyle.Standard` |
|
| `LogoMark`, `LogoType` | `CallFooter.tsx` | `showLogo = headerStyle === HeaderStyle.Standard` |
|
||||||
| `LogoLarge` | `LoginPage`, `RegisterPage` | gibt es nur standalone |
|
| `LogoLarge` | `LoginPage`, `RegisterPage` | gibt es nur standalone |
|
||||||
|
|
||||||
`HeaderStyle.Standard` ist der Default für **Nicht**-Widget-Nutzung; als Widget ist
|
`HeaderStyle.Standard` ist der Default für **Nicht**-Widget-Nutzung; als Widget ist
|
||||||
der Header `None` (Desktop) oder `AppBar` (Web/Mobile) — siehe `UrlParams.ts`. Das
|
der Header `None` (Desktop) oder `AppBar` (Web/Mobile) — siehe `UrlParams.ts`. Das
|
||||||
@@ -109,10 +148,11 @@ wurde:
|
|||||||
⚠️ Nicht danach greppen, ob ein Logo „irgendwo vorkommt" — im gebauten Bundle steht
|
⚠️ Nicht danach greppen, ob ein Logo „irgendwo vorkommt" — im gebauten Bundle steht
|
||||||
`<desc>Element Call (Beta)</desc>` aus `Logo.svg` drin, obwohl es nie gezeichnet
|
`<desc>Element Call (Beta)</desc>` aus `Logo.svg` drin, obwohl es nie gezeichnet
|
||||||
wird. Maßgeblich ist die Render-Bedingung, nicht der Treffer.
|
wird. Maßgeblich ist die Render-Bedingung, nicht der Treffer.
|
||||||
|
|
||||||
- **`header_label` („Element Call-Startseite") und `login_subheading`.** Startseite
|
- **`header_label` („Element Call-Startseite") und `login_subheading`.** Startseite
|
||||||
und Login gibt es nur im Standalone-Modus.
|
und Login gibt es nur im Standalone-Modus.
|
||||||
- **Die drei deutschen `developer_mode.matrixRTCMode.*.description`.** Die reden über
|
- **Die drei deutschen `developer_mode.matrixRTCMode.*.description`.** Die reden über
|
||||||
*Gegenstellen* („alle beteiligten Element Call Clients v0.17.0 oder neuer") — das
|
_Gegenstellen_ („alle beteiligten Element Call Clients v0.17.0 oder neuer") — das
|
||||||
ist eine Kompatibilitätsaussage über fremde Clients, keine Selbstbezeichnung. Sie
|
ist eine Kompatibilitätsaussage über fremde Clients, keine Selbstbezeichnung. Sie
|
||||||
umzubenennen wäre inhaltlich falsch.
|
umzubenennen wäre inhaltlich falsch.
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"deepfilternet3-noise-filter": "^1.3.0",
|
||||||
"eslint": "^8.14.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
|
|||||||
Generated
+13
@@ -168,6 +168,9 @@ importers:
|
|||||||
copy-to-clipboard:
|
copy-to-clipboard:
|
||||||
specifier: ^3.3.3
|
specifier: ^3.3.3
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
|
deepfilternet3-noise-filter:
|
||||||
|
specifier: ^1.3.0
|
||||||
|
version: 1.3.0(livekit-client@2.19.2(@types/dom-mediacapture-record@1.0.22))
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.14.0
|
specifier: ^8.14.0
|
||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
@@ -4659,6 +4662,12 @@ packages:
|
|||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
|
deepfilternet3-noise-filter@1.3.0:
|
||||||
|
resolution: {integrity: sha512-yYFUlPuvPguqcd/R6/OSsr0noGqlqOE50JkCWYHogk+PjLj9qrNgwTt5zKraVkKnw0l4+eXJagkz2SUWtUl4sQ==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
livekit-client: ^2.0.0
|
||||||
|
|
||||||
default-browser-id@5.0.1:
|
default-browser-id@5.0.1:
|
||||||
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
|
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -11994,6 +12003,10 @@ snapshots:
|
|||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
|
deepfilternet3-noise-filter@1.3.0(livekit-client@2.19.2(@types/dom-mediacapture-record@1.0.22)):
|
||||||
|
dependencies:
|
||||||
|
livekit-client: 2.19.2(@types/dom-mediacapture-record@1.0.22)
|
||||||
|
|
||||||
default-browser-id@5.0.1: {}
|
default-browser-id@5.0.1: {}
|
||||||
|
|
||||||
default-browser@5.5.0:
|
default-browser@5.5.0:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 aXion1337.chat
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
|
||||||
|
ThreadNet-Fork-Anpassung (ADR-0018) - nicht Upstream. Siehe docs/axion1337-fork.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
type AudioProcessorOptions,
|
||||||
|
type Track,
|
||||||
|
type TrackProcessor,
|
||||||
|
} from "livekit-client";
|
||||||
|
import { DeepFilterNoiseFilterProcessor } from "deepfilternet3-noise-filter";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
|
import {
|
||||||
|
aiNoiseSuppressionLevelSetting,
|
||||||
|
aiNoiseSuppressionSetting,
|
||||||
|
} from "../settings/settings";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KI-Geraeuschunterdrueckung (DeepFilterNet3) als LiveKit-TrackProcessor.
|
||||||
|
*
|
||||||
|
* Warum ueberhaupt: Der WebRTC-Standardfilter schaetzt ein laufendes Rauschprofil
|
||||||
|
* und filtert damit STATIONAERE Stoerungen (Luefter, Brummen). Tastaturanschlaege
|
||||||
|
* sind TRANSIENT - sehr kurzer Anstieg, unvorhersehbares Spektrum - und werden
|
||||||
|
* nicht als Stoerung erkannt. Genau die filtert DeepFilterNet3 weg.
|
||||||
|
*
|
||||||
|
* Warum die Assets von uns kommen: Das Paket laedt Modell und wasm sonst von
|
||||||
|
* cdn.mezon.ai. Fuer eine selbstgehostete Plattform hiesse das, dass jeder
|
||||||
|
* Teilnehmer bei jedem Call-Start seine IP an einen Dritten meldet und die
|
||||||
|
* Verfuegbarkeit an fremder Infrastruktur haengt. `assetConfig.cdnUrl` zeigt
|
||||||
|
* deshalb auf unsere eigene Auslieferung (public/assets/dfn3/).
|
||||||
|
*/
|
||||||
|
const ASSET_PFAD = "assets/dfn3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut den Prozessor - oder gibt `undefined` zurueck, wenn der Nutzer den Filter
|
||||||
|
* nicht eingeschaltet hat.
|
||||||
|
*
|
||||||
|
* Der Import ist statisch, kostet aber nur den ~23-KB-Wrapper. Die ~23 MB
|
||||||
|
* Modell-Assets holt das Paket erst in seinem `init()`, also erst wenn der
|
||||||
|
* Prozessor wirklich an einen Track gehaengt wird. Damit bleibt das Opt-in aus
|
||||||
|
* ADR-0018 auch wirtschaftlich eines: wer den Filter aus laesst, laedt nichts.
|
||||||
|
*/
|
||||||
|
export function createAiNoiseSuppressionProcessor():
|
||||||
|
| TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>
|
||||||
|
| undefined {
|
||||||
|
if (!aiNoiseSuppressionSetting.getValue()) return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new DeepFilterNoiseFilterProcessor({
|
||||||
|
sampleRate: 48000,
|
||||||
|
noiseReductionLevel: aiNoiseSuppressionLevelSetting.getValue(),
|
||||||
|
assetConfig: {
|
||||||
|
cdnUrl: new URL(ASSET_PFAD, window.location.href).href,
|
||||||
|
},
|
||||||
|
}) as unknown as TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>;
|
||||||
|
} catch (e) {
|
||||||
|
// Bewusst kein Abbruch: lieber ein Call ohne Filter als kein Call.
|
||||||
|
logger.error("KI-Geraeuschunterdrueckung nicht verfuegbar", e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import { useMediaDevices } from "../MediaDevicesContext";
|
|||||||
import { widget } from "../widget";
|
import { widget } from "../widget";
|
||||||
import {
|
import {
|
||||||
useSetting,
|
useSetting,
|
||||||
|
aiNoiseSuppressionSetting,
|
||||||
|
aiNoiseSuppressionLevelSetting,
|
||||||
soundEffectVolume as soundEffectVolumeSetting,
|
soundEffectVolume as soundEffectVolumeSetting,
|
||||||
backgroundBlur as backgroundBlurSetting,
|
backgroundBlur as backgroundBlurSetting,
|
||||||
developerMode,
|
developerMode,
|
||||||
@@ -109,6 +111,68 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ThreadNet-Fork (ADR-0018): KI-Geraeuschunterdrueckung gegen Tastaturgeraeusche.
|
||||||
|
// Bewusst OPT-IN - die Modell-Assets sind ~23 MB und werden erst geladen, wenn
|
||||||
|
// der Filter eingeschaltet ist. Standard-Daempfung 35 %: gemessen reicht das
|
||||||
|
// fuer "Tastatur weg und Stimme natuerlich", mehr erhoeht nur das
|
||||||
|
// Artefaktrisiko (#0054).
|
||||||
|
const AiNoiseSuppressionSettings: React.FC = (): ReactNode => {
|
||||||
|
const [aiActive, setAiActive] = useSetting(aiNoiseSuppressionSetting);
|
||||||
|
const [level, setLevel] = useSetting(aiNoiseSuppressionLevelSetting);
|
||||||
|
const [levelRaw, setLevelRaw] = useState(level);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>
|
||||||
|
{t("settings.ai_noise_suppression_header", "AI noise suppression")}
|
||||||
|
</h4>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="activateAiNoiseSuppression"
|
||||||
|
label={t(
|
||||||
|
"settings.ai_noise_suppression_label",
|
||||||
|
"Filter keyboard and background noise",
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"settings.ai_noise_suppression_description",
|
||||||
|
"Downloads a ~23 MB model the first time it is switched on. Applies on the next call join.",
|
||||||
|
)}
|
||||||
|
type="checkbox"
|
||||||
|
checked={aiActive}
|
||||||
|
onChange={(e): void => setAiActive(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
{aiActive && (
|
||||||
|
<div className={styles.volumeSlider}>
|
||||||
|
<label>
|
||||||
|
{t("settings.ai_noise_suppression_strength_label", "Strength")}
|
||||||
|
{": "}
|
||||||
|
<span className={styles.settingValue}>{levelRaw}%</span>
|
||||||
|
</label>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
"settings.ai_noise_suppression_strength_description",
|
||||||
|
"Lower keeps the room sound natural, higher isolates the voice more strictly.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<Slider
|
||||||
|
label={t(
|
||||||
|
"settings.ai_noise_suppression_strength_label",
|
||||||
|
"Strength",
|
||||||
|
)}
|
||||||
|
value={levelRaw}
|
||||||
|
onValueChange={setLevelRaw}
|
||||||
|
onValueCommit={setLevel}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const devices = useMediaDevices();
|
const devices = useMediaDevices();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) devices.requestDeviceNames(); // No-op after the first call
|
if (open) devices.requestDeviceNames(); // No-op after the first call
|
||||||
@@ -181,6 +245,8 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
step={0.01}
|
step={0.01}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AiNoiseSuppressionSettings />
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -226,6 +226,23 @@ export const autoGainControlSetting = new Setting<boolean>(
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// KI-Geraeuschunterdrueckung (ThreadNet-Fork, ADR-0018).
|
||||||
|
// Bewusst OPT-IN: die Modell-Assets sind ~23 MB und werden erst geladen, wenn
|
||||||
|
// der Prozessor tatsaechlich an den Track gehaengt wird. Wer den Filter nicht
|
||||||
|
// einschaltet, laedt nichts.
|
||||||
|
export const aiNoiseSuppressionSetting = new Setting<boolean>(
|
||||||
|
"ai-noise-suppression",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Daempfung in Prozent. 35 statt 100 ist gemessen, nicht geraten: bei ~35 %
|
||||||
|
// waren Tastaturanschlaege weg UND die Stimme klang natuerlich (#0054).
|
||||||
|
// Mehr Daempfung heisst mehr Artefaktrisiko, nicht mehr Nutzen.
|
||||||
|
export const aiNoiseSuppressionLevelSetting = new Setting<number>(
|
||||||
|
"ai-noise-suppression-level",
|
||||||
|
35,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seed setting defaults from config.json's media_quality section.
|
* Seed setting defaults from config.json's media_quality section.
|
||||||
* Call this after Config.init() has resolved.
|
* Call this after Config.init() has resolved.
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ import {
|
|||||||
echoCancellationSetting,
|
echoCancellationSetting,
|
||||||
noiseSuppressionSetting,
|
noiseSuppressionSetting,
|
||||||
autoGainControlSetting,
|
autoGainControlSetting,
|
||||||
|
aiNoiseSuppressionSetting,
|
||||||
} from "../../../settings/settings.ts";
|
} from "../../../settings/settings.ts";
|
||||||
|
import { createAiNoiseSuppressionProcessor } from "../../../livekit/aiNoiseSuppression.ts";
|
||||||
|
|
||||||
// TODO evaluate if this should be done like the Publisher Factory
|
// TODO evaluate if this should be done like the Publisher Factory
|
||||||
export interface ConnectionFactory {
|
export interface ConnectionFactory {
|
||||||
@@ -175,8 +177,16 @@ function generateRoomOption({
|
|||||||
...liveKitOptions.audioCaptureDefaults,
|
...liveKitOptions.audioCaptureDefaults,
|
||||||
deviceId: devices.audioInput.selected$.value?.id,
|
deviceId: devices.audioInput.selected$.value?.id,
|
||||||
echoCancellation: echoCancellationSetting.getValue(),
|
echoCancellation: echoCancellationSetting.getValue(),
|
||||||
noiseSuppression: noiseSuppressionSetting.getValue(),
|
// Bei aktiver KI-Filterung die Browser-Rauschunterdrueckung ausschalten:
|
||||||
|
// sonst arbeiten zwei Filter gegeneinander und der Browser schneidet dem
|
||||||
|
// Modell bereits Signalanteile weg (ADR-0018).
|
||||||
|
noiseSuppression:
|
||||||
|
noiseSuppressionSetting.getValue() &&
|
||||||
|
!aiNoiseSuppressionSetting.getValue(),
|
||||||
autoGainControl: autoGainControlSetting.getValue(),
|
autoGainControl: autoGainControlSetting.getValue(),
|
||||||
|
// ThreadNet-Fork: KI-Geraeuschunterdrueckung gegen Tastaturgeraeusche.
|
||||||
|
// `undefined`, wenn nicht eingeschaltet - dann wird auch nichts geladen.
|
||||||
|
processor: createAiNoiseSuppressionProcessor(),
|
||||||
},
|
},
|
||||||
audioOutput: {
|
audioOutput: {
|
||||||
// When using controlled audio devices, we don't want to set the
|
// When using controlled audio devices, we don't want to set the
|
||||||
|
|||||||
Reference in New Issue
Block a user