Files
cinny/src/app/utils/lotusDenoiseUtils.test.ts
T
jaredandClaude Opus 4.8 ebc782b16c
CI / Build & Quality Checks (push) Successful in 11m15s
CI / Trigger Desktop Build (push) Successful in 18s
feat(denoise): browser-native default, quality-ordered model picker, wire native-NS
- Model dropdown is now ordered by quality/CPU, best first (DeepFilterNet 3 →
  DTLN → RNNoise → Speex); fix RNNoise's inaccurate "High" voice-quality label.
- When a user opts into the ML tier, default to the highest-quality model
  (DeepFilterNet 3). The tier default stays browser-native (known-good, best
  perceived in testing so far).
- Wire the "Series Suppression" (native-NS-before-ML) toggle into the real call
  path — it was applied only in the settings tester, so the tester could sound
  better than the actual call. Default it OFF (a single NS stage is best
  practice; it's an opt-in test aid).
- isMLDenoiseSupported now also requires WebAssembly, so ML isn't offered on
  strict-CSP shells where it would silently fall back to the raw mic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:02:41 -04:00

105 lines
4.3 KiB
TypeScript

import { test, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { DENOISE_MODELS, ML_DENOISE_REQUIREMENTS, isMLDenoiseSupported } from './lotusDenoiseUtils';
// ── Model catalog (data integrity) ──────────────────────────────────────────
test('DENOISE_MODELS lists the four models ordered best-quality (highest CPU) first', () => {
assert.deepEqual(
DENOISE_MODELS.map((m) => m.id),
['deepfilternet', 'dtln', 'rnnoise', 'speex'],
);
});
test('DENOISE_MODELS ids are unique', () => {
const ids = DENOISE_MODELS.map((m) => m.id);
assert.equal(new Set(ids).size, ids.length);
});
test('every model has non-empty display fields and valid rating enums', () => {
const transients = new Set(['Poor', 'Good', 'Excellent']);
const voice = new Set(['Moderate', 'High', 'Very High']);
for (const m of DENOISE_MODELS) {
for (const field of ['name', 'description', 'cpuUsage', 'binarySize'] as const) {
assert.ok(typeof m[field] === 'string' && m[field].length > 0, `${m.id}.${field} empty`);
}
assert.ok(transients.has(m.transients), `${m.id} bad transients: ${m.transients}`);
assert.ok(voice.has(m.voiceQuality), `${m.id} bad voiceQuality: ${m.voiceQuality}`);
}
});
test('ML_DENOISE_REQUIREMENTS is a non-empty list of strings', () => {
assert.ok(Array.isArray(ML_DENOISE_REQUIREMENTS) && ML_DENOISE_REQUIREMENTS.length > 0);
assert.ok(ML_DENOISE_REQUIREMENTS.every((r) => typeof r === 'string' && r.length > 0));
});
// ── isMLDenoiseSupported (feature detection) ────────────────────────────────
const g = globalThis as Record<string, unknown>;
const NAMES = ['window', 'navigator', 'AudioWorkletNode'] as const;
let saved: Record<string, PropertyDescriptor | undefined>;
const setGlobal = (name: string, value: unknown): void => {
Object.defineProperty(g, name, { value, configurable: true, writable: true });
};
const removeGlobal = (name: string): void => {
// Make a bare reference to `name` throw ReferenceError (simulate an absent
// global binding), as it would in a browser lacking the API entirely.
if (Object.getOwnPropertyDescriptor(g, name)) delete g[name];
};
const withMediaDevices = { mediaDevices: { getUserMedia: () => Promise.resolve() } };
beforeEach(() => {
saved = {};
for (const n of NAMES) saved[n] = Object.getOwnPropertyDescriptor(g, n);
});
afterEach(() => {
for (const n of NAMES) {
const d = saved[n];
if (d) Object.defineProperty(g, n, d);
else if (Object.getOwnPropertyDescriptor(g, n)) delete g[n];
}
});
test('returns false when there is no window (non-browser)', () => {
setGlobal('window', undefined);
assert.equal(isMLDenoiseSupported(), false);
});
test('returns true when AudioContext, AudioWorklet and getUserMedia are present', () => {
setGlobal('window', { AudioContext: function AudioContext() {} });
setGlobal('AudioWorkletNode', function AudioWorkletNode() {});
setGlobal('navigator', withMediaDevices);
assert.equal(isMLDenoiseSupported(), true);
});
test('accepts the legacy webkitAudioContext prefix', () => {
setGlobal('window', { webkitAudioContext: function webkitAudioContext() {} });
setGlobal('AudioWorkletNode', function AudioWorkletNode() {});
setGlobal('navigator', withMediaDevices);
assert.equal(isMLDenoiseSupported(), true);
});
test('returns false when getUserMedia is unavailable', () => {
setGlobal('window', { AudioContext: function AudioContext() {} });
setGlobal('AudioWorkletNode', function AudioWorkletNode() {});
setGlobal('navigator', {}); // no mediaDevices
assert.equal(isMLDenoiseSupported(), false);
});
test('returns false (does NOT throw) when the AudioWorkletNode binding is absent', () => {
// Regression: a bare `!!AudioWorkletNode` threw ReferenceError on browsers
// with AudioContext but no AudioWorkletNode; a detection helper must report
// false, not throw.
setGlobal('window', { AudioContext: function AudioContext() {} });
setGlobal('navigator', withMediaDevices);
removeGlobal('AudioWorkletNode');
let result: boolean | undefined;
assert.doesNotThrow(() => {
result = isMLDenoiseSupported();
});
assert.equal(result, false);
});