Reorganize ML noise suppression settings UI
Move the model comparison out of the always-visible Noise Suppression description and into the ML-only sub-settings. Add a compact info card for the selected model (CPU / voice quality / transients / download) plus a collapsible 4-model comparison. Group ML sub-settings into Model, Enhancements, and Test & calibrate sections with clear labels and separators. Fix invented --lt-border-color token and hardcoded rgba background to real TDS tokens. Build the model dropdown and DenoiseTester labels/compare buttons from DENOISE_MODELS so DeepFilterNet 3 is handled correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Box, Button, Text } from 'folds';
|
import { Box, Button, Text } from 'folds';
|
||||||
import { DenoiseModelId } from '../../../state/settings';
|
import { DenoiseModelId } from '../../../state/settings';
|
||||||
|
import { DENOISE_MODELS } from '../../../utils/lotusDenoiseUtils';
|
||||||
import {
|
import {
|
||||||
DenoiseNode,
|
DenoiseNode,
|
||||||
buildGateNode,
|
buildGateNode,
|
||||||
@@ -307,7 +308,7 @@ export function DenoiseTester({ model, useGate, gateThreshold, nativeNS }: Denoi
|
|||||||
[stopLive, stopPlayback],
|
[stopLive, stopPlayback],
|
||||||
);
|
);
|
||||||
|
|
||||||
const modelLabel = model === 'rnnoise' ? 'RNNoise' : model === 'speex' ? 'Speex' : 'DTLN';
|
const modelLabel = DENOISE_MODELS.find((m) => m.id === model)?.name ?? model;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
@@ -367,6 +368,7 @@ export function DenoiseTester({ model, useGate, gateThreshold, nativeNS }: Denoi
|
|||||||
{ label: 'RNNoise', model: 'rnnoise' },
|
{ label: 'RNNoise', model: 'rnnoise' },
|
||||||
{ label: 'Speex', model: 'speex' },
|
{ label: 'Speex', model: 'speex' },
|
||||||
{ label: 'DTLN', model: 'dtln' },
|
{ label: 'DTLN', model: 'dtln' },
|
||||||
|
{ label: 'DeepFilterNet', model: 'deepfilternet' },
|
||||||
] as const
|
] as const
|
||||||
).map((b) => (
|
).map((b) => (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1240,6 +1240,7 @@ function Calls() {
|
|||||||
const deafenBind = useKeyBind(setDeafenKey);
|
const deafenBind = useKeyBind(setDeafenKey);
|
||||||
|
|
||||||
const mlSupported = isMLDenoiseSupported();
|
const mlSupported = isMLDenoiseSupported();
|
||||||
|
const selectedDenoiseModel = DENOISE_MODELS.find((m) => m.id === callDenoiseModel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
@@ -1258,46 +1259,9 @@ function Calls() {
|
|||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Text>
|
<Text>
|
||||||
Filter background noise from your mic during calls. Browser-native uses the built-in
|
Filter background noise from your mic during calls. Browser-native uses the built-in
|
||||||
WebRTC suppressor (Google NSNet2).
|
WebRTC suppressor (Google NSNet2). ML runs a dedicated model for stronger removal.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Box direction="Column" gap="100" style={{ overflowX: 'auto' }}>
|
|
||||||
<Box
|
|
||||||
direction="Row"
|
|
||||||
gap="100"
|
|
||||||
style={{ borderBottom: '1px solid var(--lt-border-color)', paddingBottom: '4px' }}
|
|
||||||
>
|
|
||||||
<Box style={{ width: '120px' }}>
|
|
||||||
<Text size="T200">Model</Text>
|
|
||||||
</Box>
|
|
||||||
<Box style={{ width: '80px' }}>
|
|
||||||
<Text size="T200">CPU</Text>
|
|
||||||
</Box>
|
|
||||||
<Box style={{ width: '80px' }}>
|
|
||||||
<Text size="T200">Quality</Text>
|
|
||||||
</Box>
|
|
||||||
<Box grow="Yes">
|
|
||||||
<Text size="T200">Transients</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{DENOISE_MODELS.map((model) => (
|
|
||||||
<Box key={model.id} direction="Row" gap="100">
|
|
||||||
<Box style={{ width: '120px' }}>
|
|
||||||
<Text size="T200">{model.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box style={{ width: '80px' }}>
|
|
||||||
<Text size="T200">{model.cpuUsage}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box style={{ width: '80px' }}>
|
|
||||||
<Text size="T200">{model.voiceQuality}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box grow="Yes">
|
|
||||||
<Text size="T200">{model.transients}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{!mlSupported && (
|
{!mlSupported && (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="T200" priority="400">
|
<Text size="T200" priority="400">
|
||||||
@@ -1312,11 +1276,6 @@ function Calls() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{callNoiseSuppression === 'ml' && (
|
|
||||||
<Text size="T200" priority="400">
|
|
||||||
Note: Applying changes requires rejoining the call.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
after={
|
after={
|
||||||
@@ -1339,73 +1298,174 @@ function Calls() {
|
|||||||
{callNoiseSuppression === 'ml' && (
|
{callNoiseSuppression === 'ml' && (
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="300"
|
gap="400"
|
||||||
style={{
|
style={{
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
marginTop: '8px',
|
marginTop: '8px',
|
||||||
borderTop: '1px solid var(--lt-border-color)',
|
borderTop: '1px solid var(--border-color)',
|
||||||
background: 'rgba(0,0,0,0.1)',
|
background: 'var(--bg-card)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SettingTile
|
{/* ── Model selection ───────────────────────────────────────── */}
|
||||||
title="ML Model"
|
<Box direction="Column" gap="200">
|
||||||
description="Choose the machine learning model to use for noise removal."
|
<Text size="L400">Model</Text>
|
||||||
after={
|
<SettingTile
|
||||||
<SettingsSelect<DenoiseModelId>
|
title="ML Model"
|
||||||
value={callDenoiseModel}
|
description="Choose the machine learning model used for noise removal. Heavier models clean more aggressively at a higher CPU cost."
|
||||||
onChange={setCallDenoiseModel}
|
after={
|
||||||
options={[
|
<SettingsSelect<DenoiseModelId>
|
||||||
{ value: 'rnnoise', label: 'RNNoise' },
|
value={callDenoiseModel}
|
||||||
{ value: 'speex', label: 'Speex (Legacy)' },
|
onChange={setCallDenoiseModel}
|
||||||
{ value: 'dtln', label: 'DTLN (beta)' },
|
options={DENOISE_MODELS.map((m) => ({ value: m.id, label: m.name }))}
|
||||||
{ value: 'deepfilternet', label: 'DeepFilterNet 3 (beta)' },
|
/>
|
||||||
]}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
{selectedDenoiseModel && (
|
||||||
/>
|
<Box
|
||||||
|
direction="Column"
|
||||||
<SettingTile
|
gap="200"
|
||||||
title="Series Suppression"
|
style={{
|
||||||
description="Run the browser's native stationary noise filter before the ML model. Recommended for eliminating fan hum."
|
padding: '12px',
|
||||||
after={
|
borderRadius: '8px',
|
||||||
<Switch
|
border: '1px solid var(--border-color)',
|
||||||
variant="Primary"
|
background: 'var(--bg-input)',
|
||||||
value={callDenoiseNativeNS}
|
}}
|
||||||
onChange={setCallDenoiseNativeNS}
|
>
|
||||||
/>
|
<Text size="T300">{selectedDenoiseModel.name}</Text>
|
||||||
}
|
<Text size="T200" priority="300">
|
||||||
/>
|
{selectedDenoiseModel.description}
|
||||||
|
</Text>
|
||||||
<SettingTile
|
<Box direction="Row" gap="400" wrap="Wrap">
|
||||||
title="Noise Gate"
|
{(
|
||||||
description="Hard-cut audio when you aren't speaking to ensure absolute silence between sentences."
|
[
|
||||||
after={
|
{ label: 'CPU', value: selectedDenoiseModel.cpuUsage },
|
||||||
<Switch variant="Primary" value={callDenoiseGate} onChange={setCallDenoiseGate} />
|
{ label: 'Voice quality', value: selectedDenoiseModel.voiceQuality },
|
||||||
}
|
{ label: 'Transients', value: selectedDenoiseModel.transients },
|
||||||
/>
|
{ label: 'Download', value: selectedDenoiseModel.binarySize },
|
||||||
|
] as const
|
||||||
{callDenoiseGate && (
|
).map((stat) => (
|
||||||
<Box direction="Column" gap="100">
|
<Box key={stat.label} direction="Column" gap="100">
|
||||||
<Box direction="Row" justifyContent="SpaceBetween">
|
<Text size="T200" priority="300">
|
||||||
<Text size="T200">Gate Threshold</Text>
|
{stat.label}
|
||||||
<Text size="T200">{callDenoiseGateThreshold} dB</Text>
|
</Text>
|
||||||
|
<Text size="T300">{stat.value}</Text>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<input
|
)}
|
||||||
type="range"
|
|
||||||
min="-100"
|
|
||||||
max="0"
|
|
||||||
step="1"
|
|
||||||
value={callDenoiseGateThreshold}
|
|
||||||
onChange={(e) => setCallDenoiseGateThreshold(parseInt(e.target.value, 10))}
|
|
||||||
style={{ width: '100%', accentColor: 'var(--accent-orange)' }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style={{ cursor: 'pointer' }}>
|
||||||
|
<Text as="span" size="T200" priority="300">
|
||||||
|
Compare all models
|
||||||
|
</Text>
|
||||||
|
</summary>
|
||||||
|
<Box direction="Column" gap="100" style={{ overflowX: 'auto', marginTop: '8px' }}>
|
||||||
|
<Box
|
||||||
|
direction="Row"
|
||||||
|
gap="100"
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid var(--border-color)',
|
||||||
|
paddingBottom: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box style={{ width: '160px' }}>
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
Model
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box style={{ width: '80px' }}>
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
CPU
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box style={{ width: '90px' }}>
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
Quality
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
Transients
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{DENOISE_MODELS.map((model) => (
|
||||||
|
<Box key={model.id} direction="Row" gap="100">
|
||||||
|
<Box style={{ width: '160px' }}>
|
||||||
|
<Text size="T200">{model.name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box style={{ width: '80px' }}>
|
||||||
|
<Text size="T200">{model.cpuUsage}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box style={{ width: '90px' }}>
|
||||||
|
<Text size="T200">{model.voiceQuality}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="T200">{model.transients}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<Text size="T200" priority="400">
|
||||||
|
Note: Applying changes requires rejoining the call.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ── Enhancement toggles ───────────────────────────────────── */}
|
||||||
|
<Box
|
||||||
|
direction="Column"
|
||||||
|
gap="300"
|
||||||
|
style={{ paddingTop: '12px', borderTop: '1px solid var(--border-color)' }}
|
||||||
|
>
|
||||||
|
<Text size="L400">Enhancements</Text>
|
||||||
|
<SettingTile
|
||||||
|
title="Series Suppression"
|
||||||
|
description="Run the browser's native stationary noise filter before the ML model. Recommended for eliminating fan hum."
|
||||||
|
after={
|
||||||
|
<Switch
|
||||||
|
variant="Primary"
|
||||||
|
value={callDenoiseNativeNS}
|
||||||
|
onChange={setCallDenoiseNativeNS}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingTile
|
||||||
|
title="Noise Gate"
|
||||||
|
description="Hard-cut audio when you aren't speaking to ensure absolute silence between sentences."
|
||||||
|
after={
|
||||||
|
<Switch variant="Primary" value={callDenoiseGate} onChange={setCallDenoiseGate} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{callDenoiseGate && (
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Box direction="Row" justifyContent="SpaceBetween">
|
||||||
|
<Text size="T200">Gate Threshold</Text>
|
||||||
|
<Text size="T200">{callDenoiseGateThreshold} dB</Text>
|
||||||
|
</Box>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="-100"
|
||||||
|
max="0"
|
||||||
|
step="1"
|
||||||
|
value={callDenoiseGateThreshold}
|
||||||
|
onChange={(e) => setCallDenoiseGateThreshold(parseInt(e.target.value, 10))}
|
||||||
|
style={{ width: '100%', accentColor: 'var(--accent-orange)' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ── Test & calibrate ──────────────────────────────────────── */}
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="200"
|
gap="200"
|
||||||
style={{ paddingTop: '8px', borderTop: '1px solid var(--border-color)' }}
|
style={{ paddingTop: '12px', borderTop: '1px solid var(--border-color)' }}
|
||||||
>
|
>
|
||||||
<Text size="L400">Test & calibrate</Text>
|
<Text size="L400">Test & calibrate</Text>
|
||||||
<Text size="T200" priority="300">
|
<Text size="T200" priority="300">
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
* Detection utilities for Lotus ML noise suppression (RNNoise).
|
* Detection utilities for Lotus ML noise suppression (RNNoise).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { DenoiseModelId } from '../state/settings';
|
||||||
|
|
||||||
export type DenoiseModel = {
|
export type DenoiseModel = {
|
||||||
id: string;
|
id: DenoiseModelId;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
cpuUsage: string;
|
cpuUsage: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user