From 7a85935eee441dd73061920d2c19316df1c28ec1 Mon Sep 17 00:00:00 2001 From: ServerBob Date: Thu, 5 Mar 2026 06:33:22 +0000 Subject: [PATCH] fix(voice): bundle deepfilter assets and enforce DSP toggle compatibility --- .../src/components/devices-provider/index.tsx | 9 +++ .../user-settings/devices/index.tsx | 62 +++++++++++++++---- .../src/components/voice-provider/index.tsx | 18 ++++-- apps/server/src/http/deepfilter-assets.ts | 43 +++++-------- 4 files changed, 87 insertions(+), 45 deletions(-) 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 };