feat(call): denoise asset smoke check at ML-tier call start

HEAD-checks the copied denoise worklet/wasm/model assets for the selected model
and console.warns a single line listing anything missing — a silent asset skew
between the EC fork's expectations and vite's copied files would otherwise
disable noise suppression with no signal. Fire-and-forget; never blocks call
setup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 00:19:16 -04:00
parent 96f7187031
commit 34d9272790
2 changed files with 70 additions and 0 deletions
+7
View File
@@ -27,6 +27,7 @@ import {
} from './types'; } from './types';
import { CallControl } from './CallControl'; import { CallControl } from './CallControl';
import { CallControlState } from './CallControlState'; import { CallControlState } from './CallControlState';
import { verifyDenoiseAssets } from './denoiseSmokeCheck';
// Maximum time to wait for the embedded Element Call iframe to progress from // Maximum time to wait for the embedded Element Call iframe to progress from
// initial load to a ready/joined state. If it hasn't by then, we assume the // initial load to a ready/joined state. If it hasn't by then, we assume the
@@ -205,6 +206,12 @@ export class CallEmbed {
params.append('lotusModel', denoiseModel); params.append('lotusModel', denoiseModel);
params.append('lotusGate', denoiseGate.toString()); params.append('lotusGate', denoiseGate.toString());
params.append('lotusGateThreshold', denoiseGateThreshold.toString()); params.append('lotusGateThreshold', denoiseGateThreshold.toString());
// [lotus] Fire-and-forget: confirm the fork's ML-denoise assets are
// actually served under public/element-call/denoise/ (they're copied by
// vite.config.js at build time). Warns once if the copy step regressed;
// never blocks call start.
verifyDenoiseAssets(denoiseModel).catch(() => undefined);
} }
if (CallEmbed.startingCall(intent)) { if (CallEmbed.startingCall(intent)) {
+63
View File
@@ -0,0 +1,63 @@
import { trimTrailingSlash } from '../../utils/common';
// Denoise assets copied into public/element-call/denoise/ by vite.config.js's
// lotusDenoise() plugin. The filenames here MUST match what that plugin writes
// (and what the fork's TrackProcessor fetches at runtime). Grouped per model so
// the smoke-check only probes what the active call will actually load.
const DENOISE_ASSETS: Record<string, readonly string[]> = {
rnnoise: ['rnnoiseWorklet.js', 'rnnoise.wasm', 'rnnoise_simd.wasm'],
speex: ['speexWorklet.js', 'speex.wasm'],
dtln: ['workadventure/audio-worklet.js'],
deepfilternet: [
'deepfilternet/index.esm.js',
'deepfilternet/v2/pkg/df_bg.wasm',
'deepfilternet/v2/models/DeepFilterNet3_onnx.tar.gz',
],
};
// The noise-gate worklet is a shared asset the build ships for every model
// (loaded when the gate is enabled), so probe it regardless of the model.
const SHARED_ASSETS: readonly string[] = ['noiseGateWorklet.js'];
/**
* Fire-and-forget smoke-check for the ML-denoise asset contract.
*
* The fork's in-source denoiser (lotusDenoiseSource) loads its worklet/wasm/ESM
* from `public/element-call/denoise/` at runtime; if the build's asset copy
* step regressed, those fetches 404 and denoise silently degrades to a raw mic.
* This HEAD-fetches the critical assets for the selected model and emits a
* single console.warn listing any that are missing. No UI, no throw — purely a
* developer/operator breadcrumb.
*
* @param model the selected denoise model (defaults to rnnoise)
* @returns true if every probed asset responded OK, false otherwise
*/
export async function verifyDenoiseAssets(model = 'rnnoise'): Promise<boolean> {
const base = new URL(
`${trimTrailingSlash(import.meta.env.BASE_URL)}/public/element-call/denoise/`,
window.location.origin,
);
const names = [...(DENOISE_ASSETS[model] ?? DENOISE_ASSETS.rnnoise), ...SHARED_ASSETS];
const results = await Promise.all(
names.map(async (name): Promise<string | null> => {
try {
const res = await fetch(new URL(name, base).href, { method: 'HEAD' });
return res.ok ? null : name;
} catch {
return name;
}
}),
);
const missing = results.filter((n): n is string => n !== null);
if (missing.length > 0) {
console.warn(
`[lotus-denoise] ML denoise assets missing under ${base.href} (model="${model}"): ${missing.join(
', ',
)} — the in-source denoiser will fall back to a raw mic. Check vite.config.js lotusDenoise().`,
);
return false;
}
return true;
}