diff --git a/apps/client/src/components/devices-provider/index.tsx b/apps/client/src/components/devices-provider/index.tsx
index ccab8e9..01b05d1 100644
--- a/apps/client/src/components/devices-provider/index.tsx
+++ b/apps/client/src/components/devices-provider/index.tsx
@@ -45,11 +45,20 @@ const sanitizeDeviceSettings = (
if (merged.noiseSuppressionRnnoise && merged.noiseSuppressionEnhanced) {
merged.noiseSuppressionEnhanced = false;
}
+
if (merged.noiseSuppressionDeepFilterNet) {
merged.noiseSuppressionEnhanced = false;
merged.noiseSuppressionRnnoise = false;
}
+ const hasWasmSuppression =
+ merged.noiseSuppressionDeepFilterNet || merged.noiseSuppressionRnnoise;
+ if (hasWasmSuppression) {
+ merged.echoCancellation = false;
+ merged.noiseSuppression = false;
+ merged.autoGainControl = false;
+ }
+
if (!Number.isFinite(merged.voiceSensitivity)) {
merged.voiceSensitivity = DEFAULT_DEVICE_SETTINGS.voiceSensitivity;
}
diff --git a/apps/client/src/components/server-screens/user-settings/devices/index.tsx b/apps/client/src/components/server-screens/user-settings/devices/index.tsx
index 854fd27..4851e85 100644
--- a/apps/client/src/components/server-screens/user-settings/devices/index.tsx
+++ b/apps/client/src/components/server-screens/user-settings/devices/index.tsx
@@ -63,7 +63,7 @@ const getConfiguredMicChain = (values: {
const chain: string[] = [];
if (values.noiseSuppressionDeepFilterNet) chain.push('DeepFilterNet');
if (values.noiseSuppressionRnnoise) chain.push('RNNoise');
- if (values.keyboardSuppression !== false) chain.push('Keyboard Gate');
+ if (values.keyboardSuppression) chain.push('Keyboard Gate');
if (values.noiseSuppressionEnhanced) chain.push('Enhanced (browser)');
if (
!chain.length &&
@@ -131,6 +131,8 @@ const Devices = memo(() => {
if (checked) {
onChange('noiseSuppressionEnhanced', false);
onChange('noiseSuppressionDeepFilterNet', false);
+ onChange('echoCancellation', false);
+ onChange('autoGainControl', false);
onChange('noiseSuppression', false);
}
},
@@ -143,12 +145,50 @@ const Devices = memo(() => {
if (checked) {
onChange('noiseSuppressionEnhanced', false);
onChange('noiseSuppressionRnnoise', false);
+ onChange('echoCancellation', false);
+ onChange('autoGainControl', false);
onChange('noiseSuppression', false);
}
},
[onChange]
);
+ const handleEchoCancellationToggle = useCallback(
+ (checked: boolean) => {
+ onChange('echoCancellation', checked);
+ if (checked) {
+ onChange('noiseSuppressionRnnoise', false);
+ onChange('noiseSuppressionDeepFilterNet', false);
+ }
+ },
+ [onChange]
+ );
+
+ const handleNoiseSuppressionToggle = useCallback(
+ (checked: boolean) => {
+ onChange('noiseSuppression', checked);
+ if (checked) {
+ onChange('noiseSuppressionRnnoise', false);
+ onChange('noiseSuppressionDeepFilterNet', false);
+ }
+ },
+ [onChange]
+ );
+
+ const handleAutoGainControlToggle = useCallback(
+ (checked: boolean) => {
+ onChange('autoGainControl', checked);
+ if (checked) {
+ onChange('noiseSuppressionRnnoise', false);
+ onChange('noiseSuppressionDeepFilterNet', false);
+ }
+ },
+ [onChange]
+ );
+
+ const hasWasmSuppression =
+ !!values.noiseSuppressionRnnoise || !!values.noiseSuppressionDeepFilterNet;
+
const saveDeviceSettings = useCallback(() => {
saveDevices(values);
toast.success('Device settings saved');
@@ -186,21 +226,23 @@ const Devices = memo(() => {
- onChange('echoCancellation', checked)
- }
+ onCheckedChange={handleEchoCancellationToggle}
/>
- onChange('noiseSuppression', checked)
- }
+ onCheckedChange={handleNoiseSuppressionToggle}
/>
+ {hasWasmSuppression ? (
+
+ Browser echo/noise/AGC are disabled while RNNoise or DeepFilterNet is active.
+
+ ) : null}
+
{
description="Speech-safe gate to reduce key clicks while keeping voice pickup."
>
onChange('keyboardSuppression', checked)
}
@@ -279,9 +321,7 @@ const Devices = memo(() => {
- onChange('autoGainControl', checked)
- }
+ onCheckedChange={handleAutoGainControlToggle}
/>
diff --git a/apps/client/src/components/voice-provider/index.tsx b/apps/client/src/components/voice-provider/index.tsx
index ad48a77..ca35906 100644
--- a/apps/client/src/components/voice-provider/index.tsx
+++ b/apps/client/src/components/voice-provider/index.tsx
@@ -100,7 +100,8 @@ const getConfiguredMicChain = (devices: TDeviceSettings): string => {
if (devices.keyboardSuppression) chain.push('Keyboard Gate');
if (devices.noiseSuppressionEnhanced) chain.push('Enhanced (browser)');
if (
- !chain.length &&
+ !devices.noiseSuppressionDeepFilterNet &&
+ !devices.noiseSuppressionRnnoise &&
(devices.echoCancellation || devices.noiseSuppression || devices.autoGainControl)
) {
chain.push('Browser DSP');
@@ -114,14 +115,19 @@ const buildMicConstraints = (devices: TDeviceSettings): MediaTrackConstraints =>
const deepFilterNet = !!devices.noiseSuppressionDeepFilterNet;
const keyboardSuppression = !!devices.keyboardSuppression;
const processed = enhanced || rnnoise || deepFilterNet || keyboardSuppression;
+ const hasWasmSuppression = rnnoise || deepFilterNet;
const constraints: MediaTrackConstraints = {
deviceId: devices.microphoneId
? { exact: devices.microphoneId }
: undefined,
- autoGainControl: devices.autoGainControl,
- echoCancellation: devices.echoCancellation,
- noiseSuppression: (rnnoise || deepFilterNet) ? false : (enhanced ? true : devices.noiseSuppression),
+ autoGainControl: hasWasmSuppression ? false : devices.autoGainControl,
+ echoCancellation: hasWasmSuppression ? false : devices.echoCancellation,
+ noiseSuppression: hasWasmSuppression
+ ? false
+ : enhanced
+ ? true
+ : devices.noiseSuppression,
sampleRate: 48000,
channelCount: processed ? 1 : 2
};
@@ -560,8 +566,8 @@ const VoiceProvider = memo(({ children }: TVoiceProviderProps) => {
track.onended = () => {
logVoice('Audio track ended, cleaning up microphone');
- localAudioStream?.getAudioTracks().forEach((track) => {
- track.stop();
+ stream.getAudioTracks().forEach((streamTrack) => {
+ streamTrack.stop();
});
localAudioProducer.current?.close();
diff --git a/apps/server/src/http/deepfilter-assets.ts b/apps/server/src/http/deepfilter-assets.ts
index 268a50e..5b179de 100644
--- a/apps/server/src/http/deepfilter-assets.ts
+++ b/apps/server/src/http/deepfilter-assets.ts
@@ -2,12 +2,12 @@ import fs from 'fs';
import fsp from 'fs/promises';
import http from 'http';
import path from 'path';
+import { DATA_PATH } from '../helpers/paths';
const ROUTE_PREFIX = '/deepfilter-assets/';
const ALLOWED_CDN_PATH_PREFIX =
'AI/models/datas/noise_suppression/deepfilternet3/';
-const CDN_BASE_URL = 'https://cdn.mezon.ai/';
-const CACHE_DIR = '/root/.config/pulse/deepfilter-assets';
+const CACHE_DIR = path.join(DATA_PATH, 'deepfilter-assets');
const guessContentType = (filePath: string): string => {
if (filePath.endsWith('.wasm')) return 'application/wasm';
@@ -37,7 +37,15 @@ const deepfilterAssetsRouteHandler = async (
return;
}
- const relativePath = decodeURIComponent(routePath.slice(ROUTE_PREFIX.length));
+ let relativePath = '';
+ try {
+ relativePath = decodeURIComponent(routePath.slice(ROUTE_PREFIX.length));
+ } catch {
+ res.writeHead(400, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: 'Invalid path' }));
+ return;
+ }
+
if (!relativePath.startsWith(ALLOWED_CDN_PATH_PREFIX)) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Forbidden' }));
@@ -46,7 +54,8 @@ const deepfilterAssetsRouteHandler = async (
const resolvedPath = path.resolve(CACHE_DIR, relativePath);
const cacheBasePath = path.resolve(CACHE_DIR);
- if (!resolvedPath.startsWith(cacheBasePath)) {
+ const relativeToBase = path.relative(cacheBasePath, resolvedPath);
+ if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Forbidden' }));
return;
@@ -56,31 +65,9 @@ const deepfilterAssetsRouteHandler = async (
await sendFile(res, resolvedPath);
return;
} catch {
- // Cache miss; continue to fetch from upstream.
+ res.writeHead(404, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: 'DeepFilter asset not bundled on server' }));
}
-
- await fsp.mkdir(path.dirname(resolvedPath), { recursive: true });
-
- const upstreamUrl = new URL(relativePath, CDN_BASE_URL).toString();
- const upstream = await fetch(upstreamUrl);
- if (!upstream.ok) {
- res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ error: 'Upstream fetch failed' }));
- return;
- }
-
- const body = Buffer.from(await upstream.arrayBuffer());
- const tempPath = `${resolvedPath}.tmp-${Date.now()}`;
- await fsp.writeFile(tempPath, body);
- await fsp.rename(tempPath, resolvedPath);
-
- res.writeHead(200, {
- 'Content-Type':
- upstream.headers.get('content-type') || guessContentType(resolvedPath),
- 'Content-Length': body.length,
- 'Cache-Control': 'public, max-age=31536000, immutable'
- });
- res.end(body);
};
export { deepfilterAssetsRouteHandler };