fix(voice): bundle deepfilter assets and enforce DSP toggle compatibility
This commit is contained in:
parent
1512c2a895
commit
7a85935eee
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
|||
<Group label="Echo cancellation">
|
||||
<Switch
|
||||
checked={!!values.echoCancellation}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange('echoCancellation', checked)
|
||||
}
|
||||
onCheckedChange={handleEchoCancellationToggle}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group label="Noise suppression">
|
||||
<Switch
|
||||
checked={!!values.noiseSuppression}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange('noiseSuppression', checked)
|
||||
}
|
||||
onCheckedChange={handleNoiseSuppressionToggle}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{hasWasmSuppression ? (
|
||||
<div className="text-xs text-muted-foreground max-w-xs">
|
||||
Browser echo/noise/AGC are disabled while RNNoise or DeepFilterNet is active.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Group label="Enhanced noise suppression">
|
||||
<Switch
|
||||
checked={!!values.noiseSuppressionEnhanced}
|
||||
|
|
@ -227,7 +269,7 @@ const Devices = memo(() => {
|
|||
description="Speech-safe gate to reduce key clicks while keeping voice pickup."
|
||||
>
|
||||
<Switch
|
||||
checked={values.keyboardSuppression !== false}
|
||||
checked={!!values.keyboardSuppression}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange('keyboardSuppression', checked)
|
||||
}
|
||||
|
|
@ -279,9 +321,7 @@ const Devices = memo(() => {
|
|||
<Group label="Automatic gain control">
|
||||
<Switch
|
||||
checked={!!values.autoGainControl}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange('autoGainControl', checked)
|
||||
}
|
||||
onCheckedChange={handleAutoGainControlToggle}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue