Интеграция Peito Denoise

Peito Denoise — реалтайм ИИ-шумодав для браузера, Electron и Capacitor-WebView. Эта страница проведёт от установки пакета до обработанного аудиотрека в вашем приложении.

Установка

Пакет ставится из npm. onnxruntime-web — peer-зависимость, нужна для нейробэкенда dfn3:

npm i peito-denoise onnxruntime-web

Пакет содержит JS-бандл, типы и AudioWorklet-процессор. Тяжёлые ассеты (модели, wasm) в пакет не входят — их хостите вы (см. ниже).

Ассеты

Скопируйте в свою папку статики (например, public/):

ЧтоОткудаКуда
AudioWorklet-процессор node_modules/peito-denoise/dist/denoise-processor.js public/denoise-processor.js
Модели DFN3 (~8.5 МБ) дистрибутив моделей (*.onnx + config.ini) public/models/dfn3/
ORT: ESM-модуль + wasm node_modules/onnxruntime-web/dist/ (ort.bundle.min.mjs и ort-wasm-simd-threaded.jsep.{wasm,mjs}) public/ort/

Проще всего скопировать весь каталог onnxruntime-web/dist/ в public/ort/.

ORT грузится в воркере по URL, а не bare-спецификатором. Module-воркер не резолвит import 'onnxruntime-web' и не поддерживает import maps, поэтому воркер импортирует ORT из /ort/ort.bundle.min.mjs, а wasm — из /ort/ (тот же origin). Оба пути переопределяются: assets.ortUrl (ESM-модуль) и assets.cdnUrl (каталог wasm). Именно поэтому ort.bundle.min.mjs обязателен в раздаче — без него dfn3 не стартует.

Воркер-чанк. Инференс dfn3 идёт в отдельном module-воркере (dist/assets/denoise-worker-<hash>.js). Бандлеры со встроенной поддержкой воркеров (Vite) подхватывают его сами. При ручной статической раздаче копируйте весь каталог dist/assets/ целиком — имя содержит хеш и меняется между версиями.

Почему worklet лежит в public/, а не импортируется в бандл: AudioWorklet загружается только по URL (audioWorklet.addModule) отдельным запросом, в бандл он не попадает, поэтому хеш сборки к /denoise-processor.js не добавляется — и сборку это не ломает. Если нужен cache-busting от бандлера, получите хешированный URL ассета и передайте его в workletUrl:

// Vite: файл попадёт в assets с хешом, копировать в public/ не нужно
import workletUrl from 'peito-denoise/denoise-processor.js?url';
const node = new DenoiseNode({ backend: 'dfn3', workletUrl });

// Webpack 5 — аналогично:
const workletUrl = new URL('peito-denoise/denoise-processor.js', import.meta.url).href;

Заголовки COOP/COEP

Конвейер использует SharedArrayBuffer (lock-free ring между AudioWorklet и воркером), поэтому странице нужна cross-origin isolation. Сервер должен отдавать:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

На реальном домене обязателен HTTPS: SharedArrayBuffer требует secure context (http://localhost — исключение, там всё работает). Если сторонние ресурсы (CDN, S3) блокируются COEP — выставьте на них Cross-Origin-Resource-Policy: cross-origin или используйте COEP: credentialless.

Пример для Vite dev-сервера:

// vite.config.ts
export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
});

Диагностика. Если изоляции нет, init() бросает PeitoError(E_COOP_COEP) ещё до запроса активации — симптом «скрипт загрузился, а fetch на активацию не ушёл». Проверяйте заранее:

if (!crossOriginIsolated) {
  // нет COOP/COEP — SharedArrayBuffer недоступен, конвейер не поднимется
}
import { detectCapabilities } from 'peito-denoise';
console.log(detectCapabilities()); // sharedArrayBuffer, crossOriginIsolated, simd, threads…

Быстрый старт

Минимальная интеграция: микрофон → шумодав → динамики. init() вызывайте после жеста пользователя (клик), иначе браузер не даст запустить AudioContext.

import { DenoiseNode } from 'peito-denoise';

const node = new DenoiseNode({
  backend: 'dfn3',                        // нейромодель; 'spectral' — DSP без ассетов
  workletUrl: '/denoise-processor.js',    // скопированный worklet (это и есть дефолт)
});
await node.init();

// граф: любой источник → шумодав → куда угодно
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const src = node.audioContext.createMediaStreamSource(stream);
node.connectSource(src);
node.connect(node.audioContext.destination);

Бэкенды: dfn3 (DeepFilterNet3, нейросеть, лучшее качество речи), spectral (DSP-гейт, ~24 дБ на стационарном шуме, без ассетов), passthrough (обход, для A/B).

На слабых CPU включите многопоточный wasm-инференс — threads: 2–4 в опциях (см. справочник): время кадра dfn3 падает примерно вдвое.

MediaStreamTrack → чистый трек

Для WebRTC-звонков удобнее получить обработанный MediaStreamTrack и отдать его в PeerConnection:

const rawTrack = stream.getAudioTracks()[0];
const cleanTrack = node.createProcessedTrack(rawTrack);

// дальше как обычно:
pc.addTrack(cleanTrack, new MediaStream([cleanTrack]));

LiveKit

Адаптер под TrackProcessor. createLiveKitProcessor принимает тот же объект опций, что и DenoiseNode — обязательны activation (иначе dfn3 не получит модель) и context (running-контекст). Адаптер сам вызывает start() после инициализации.

import { createLiveKitProcessor } from 'peito-denoise';

// running-контекст создаём в рамках жеста пользователя (клик по «Позвонить»)
const ctx = new AudioContext({ sampleRate: 48000 });
await ctx.resume();

const proc = createLiveKitProcessor({
  backend: 'dfn3',
  suppressionLevel: 70,
  activation: {
    key: 'pk_…',                                  // ключ из личного кабинета
    activateUrl: 'https://peito.ru/api/activate', // или ваш прокси (см. про CORS ниже)
  },
  context: ctx,                                   // без него worklet отдаёт тишину/сырой звук
});

await localAudioTrack.setProcessor(proc);         // адаптер сам вызовет start()

// метрики и ошибки — через proc.denoiser
proc.denoiser?.on('metrics', (m) => updateUi(m));
proc.denoiser?.on('error', (e) => console.warn(e.code, e.where, e.error));

Почему «Hello World» из старых доков не запускался: без activation у dfn3 нет модели; без running-контекста worklet отдаёт тишину/сырой звук («активация есть, а шум не режется»); без раздачи ассетов (worklet, воркер, ORT) конвейер не грузится. Все три обязательны.

CORS и прокси. В 0.2.0 activateUrl по умолчанию абсолютный (https://peito.ru/api/activate) — сервер Peito отдаёт CORS-заголовки, так что с браузера он доступен напрямую. Если хотите проксировать активацию через свой бэкенд — переопределите activation.activateUrl и activation.refreshUrl. Расшифрованные модели активация отдаёт по /api/assets?…; они кэшируются после первой загрузки (см. Предзагрузка и кеш).

Управление в рантайме

Все параметры меняются на лету, без пересоздания графа:

node.setSuppression(80);            // сила подавления 0..100
node.setMix(0.5);                   // 0 = dry (сырой), 1 = wet (обработанный)
node.setBypass(true);               // мгновенный обход для A/B-сравнения
node.switchBackend('spectral');     // хот-свап бэкенда внутри воркера

node.setParam('highpassHz', 0);     // выключить входной high-pass (по умолчанию 80 Гц)

node.pause();                       // воркер простаивает — экономия CPU
await node.start();                 // возобновление (+ поднимает AudioContext)
node.destroy();                     // полная очистка ресурсов

AutoGain

Дотягивает тихую речь до целевого уровня: VAD-гейт (не усиливает шум в паузах), посэмпловое сглаживание и lookahead-лимитер — без щелчков и клиппинга.

node.setAutoGain({
  enabled: true,
  targetDb: -18,     // целевой уровень речи
  maxGainDb: 24,     // потолок буста
});

Входной high-pass (ветер, гул, DC)

Перед моделью стоит high-pass фильтр (Butterworth 2-го порядка, по умолчанию 80 Гц): он срезает то, что живёт ниже речи и плохо даётся нейросети — ветер в капсюль, хендлинг-шум, гул сети, DC-смещение. На 40 Гц затухание −12 дБ, на 20 Гц −24 дБ; от 300 Гц фильтр прозрачен, речь не задевается.

new DenoiseNode({ backend: 'dfn3', highpassHz: 100 }); // своя частота среза
node.setParam('highpassHz', 0);                        // выключить на лету

Фильтр применяется до разделения на raw/processed: dry/wet-микс и аварийный fail-open не дают скачков уровня на НЧ. Остаточную среднечастотную турбулентность сильного ветра без поедания речи не уберёт ни одна модель — здесь поможет только ветрозащита/поп-фильтр на микрофоне.

Предзагрузка и кеш

Ассеты кешируются в CacheStorage, поэтому загрузка происходит максимум один раз, а повторный запуск шумодава — почти мгновенный.

preloadDenoise() прогревает кеш заранее (wasm + ORT + self-host-модели), чтобы первый init() стартовал уже из кеша и работал офлайн:

import { preloadDenoise, clearDenoiseCache } from 'peito-denoise';

await preloadDenoise({
  onProgress: (p) => console.log(`${p.loaded} / ${p.total}`),
});

// сбросить оба кеша (ассеты + модели), напр. при обновлении моделей
await clearDenoiseCache();

Итог по скорости: первая активация скачивает модели один раз; все последующие запуски (реактивация, перезагрузка вкладки) поднимают dfn3 из кеша за десятки миллисекунд. Кеш инвалидируется автоматически при ротации ключа шифрования на сервере.

Наблюдаемость

Шина событий даёт живые метрики и детекторы искажений — проблемы видно в телеметрии, а не «на слух»:

node.on('metrics', (e) => {
  // reductionDb, inferMs p50/p95, латентность, VAD, under/overruns…
  console.log('подавление:', e.data.reductionDb, 'дБ');
});
node.on('artifact', (e) => console.warn('искажение:', e.kind, e.frame));
node.on('error', (e) => console.error(e));
node.on('backendSwitch', (e) => console.log('fail-open/восстановление', e));

// последние N секунд raw/processed — готовые WAV-блобы для офлайн-репро
const { raw, processed } = await node.dumpWav(); // { raw: Blob, processed: Blob }

Детекторы: clip, click (разрыв), musical (музыкальный шум), oversuppress (модель ест речь), nan/inf, dead (тишина).

Встроенный watchdog — fail-open: если воркер замолкает, узел автоматически переключается на сырой звук (а не в тишину) и сам восстанавливается, когда воркер оживает.

Дебаг-панель

Отдельный вход — не попадает в прод-бандл, если не импортирован:

import { createDebugPanel } from 'peito-denoise/panel';

const panel = createDebugPanel(node, document.getElementById('debug'));
// метрики с зонами, лампы детекторов, журнал, start/pause/bypass
panel.destroy(); // снять панель

Лицензирование

Основной путь для всех планов (Trial = Pro по фичам, отличается сроком и доменами) — онлайн-активация: клиент обменивает короткий публичный ключ (pk_… из личного кабинета) на session-токен, сервер проверяет домен, срок и отзыв и выдаёт зашифрованные модели по короткоживущим ссылкам. Без успешной активации нейробэкенд не получает веса — «пропустить проверку» на клиенте нельзя.

const node = new DenoiseNode({
  backend: 'dfn3',
  activation: { key: 'pk_…' },  // по умолчанию → https://peito.ru/api/activate
});
await node.init();              // активация отклонена → init() бросает
node.session;                   // текущий session-токен
activateUrl и refreshUrl по умолчанию ведут на сервер Peito (https://peito.ru/api/…) — библиотека встраивается в чужие продукты, поэтому дефолт абсолютный. Переопределяйте эти поля, только если проксируете активацию через свой бэкенд.

Дальше библиотека сама продлевает session в фоне (refresh на ~60% TTL, по умолчанию TTL 15 минут). Если продлить не удалось — она реактивируется ключом (модели не перекачиваются). Если и это не прошло (ключ отозван, срок истёк, сервер недоступен) — шумодав отключается в fail-open: звук продолжает идти, но без обработки; попытки реактивации идут раз в минуту, при успехе обработка включается обратно. Оба перехода видны в событии backendSwitch.

Дополнительно ключ можно проверять и офлайн — по подписи ECDSA P-256 (WebCrypto): домены (включая wildcard), срок, набор фич. Работает без сети, но не видит отзыв ключа до истечения срока — используйте как дополнение к активации или для окружений без сети.

const node = new DenoiseNode({
  backend: 'dfn3',
  license: {
    key: MY_LICENSE_KEY,
    mode: 'enforce',   // блокирует init() без валидного ключа; 'warn' — только событие error
  },
});
await node.init();
await node.hasFeature('dfn3'); // проверка фичи из лицензии

Ошибки и коды

Все ошибки конвейера — кодовые: у каждой стабильный код E_*. Ошибки видны тремя способами, и по умолчанию их не нужно специально ловить, чтобы увидеть:

import { DenoiseNode, PeitoError, PeitoErrorCode } from 'peito-denoise';

const node = new DenoiseNode({
  backend: 'dfn3',
  activation: { key: 'pk_…' },
  debug: { verbosity: 'error' },   // 'silent' | 'error' | 'info' | 'debug'
});

// рантайм-ошибки (fail-open, watchdog, refresh лицензии, artefact-детекторы)
node.on('error', (e) => {
  console.warn(e.code, e.where, e.error, e.fatal);
  if (e.code === PeitoErrorCode.LICENSE_REFRESH) notifyUser('лицензия истекла');
});

try {
  await node.init();               // фатальные пути бросают PeitoError
} catch (e) {
  if (e instanceof PeitoError && e.code === PeitoErrorCode.COOP_COEP) {
    // нет cross-origin isolation — проверьте заголовки COOP/COEP
  }
  throw e;
}

Полный список кодов:

КодКлассКогда возникает и что делать
E_COOP_COEPinit · фатальноНет SharedArrayBuffer / cross-origin isolation. Отдавайте страницу по HTTPS с заголовками COOP: same-origin и COEP: require-corp.
E_WORKLET_LOADinit · фатальноНе удалось загрузить AudioWorklet-процессор. Проверьте workletUrl и что denoise-processor.js реально раздаётся.
E_WORKER_INITinit · фатальноВоркер не смог поднять бэкенд: не загрузились wasm/модель или упал backend.init. Проверьте assets/activation и доступность моделей.
E_WORKER_TIMEOUTinit · фатальноВоркер не сообщил о готовности за initTimeoutMs. Медленная сеть/большая модель — увеличьте initTimeoutMs или предзагрузите ассеты (preloadDenoise).
E_WORKER_CRASHрантайм · фатальноНеобработанная ошибка воркера в рантайме. Смотрите error; частая причина — несовместимая сборка ort/модели.
E_ACTIVATIONinit · фатальноОнлайн-активация отклонена/недоступна: неверный или отозванный ключ, чужой домен, сетевая ошибка. Проверьте ключ и activateUrl.
E_LICENSE_INVALIDinit · фатально в enforceОфлайн-проверка лицензии не прошла (подпись, домен, срок, фичи). В режиме enforce запуск блокируется, в warn — только событие.
E_SAMPLE_RATEрантайм · предупреждениеsampleRate переданного AudioContext ≠ ожидаемого. Создавайте контекст с нужным sampleRate или не передавайте свой.
E_SELFTESTрантайм · предупреждениеСамотест после init дал недопустимый выход (NaN, клиппинг или тишина). Бэкенд/модель, вероятно, неисправны.
E_PROCESS_FRAMEрантайм · предупреждениеИсключение при обработке аудиокадра. Кадр пропущен; при повторении проверьте бэкенд.
E_BACKEND_SWITCHрантайм · предупреждениеРантайм-смена бэкенда не удалась. Прежний бэкенд снят — вызовите switchBackend повторно или пересоздайте узел.
E_ACTIVATION_ANOMALYрантайм · предупреждениеСервер пометил активацию как аномальную (один ключ с многих доменов). Обработка продолжается — проверьте распространение ключа.
E_LICENSE_REFRESHрантайм · предупреждениеНе удалось продлить session и реактивировать ключ. Шумодав перешёл в fail-open (сырой звук); попытки продолжаются раз в минуту.
E_WATCHDOGрантайм · предупреждениеВоркер перестал двигать кадры дольше watchdogMs. Временный fail-open на сырой звук; при оживлении воркера обработка вернётся.

Свой бэкенд (своя модель)

RNNoise, DTLN, GTCRN или произвольный ONNX подключаются плагином — конвейер трогать не нужно. Реализуйте интерфейс DenoiseBackend и зарегистрируйте:

import { registerBackend } from 'peito-denoise';
import type { DenoiseBackend } from 'peito-denoise';

class MyOnnxBackend implements DenoiseBackend {
  // init, processFrame(input, out), setParam, reset, destroy,
  // опционально lastDebug() — маска/VAD для спектрограммы и детекторов
}

registerBackend('onnx', () => new MyOnnxBackend()); // имя — из BackendName ('rnnoise', 'dtln', 'gtcrn', 'onnx')
const node = new DenoiseNode({ backend: 'onnx' });

Справочник опций DenoiseNode

ОпцияПо умолчаниюОписание
backendБэкенд: 'dfn3' | 'spectral' | 'passthrough' | свой.
workletUrl/denoise-processor.jsURL AudioWorklet-процессора.
contextсоздаётсяГотовый AudioContext, если хотите свой.
suppressionLevel60Сила подавления, 0..100.
mix1Dry/wet-микс, 0..1.
bypassfalseМгновенный обход модели (сырой звук на выход), для A/B.
sampleRate48000Частота дискретизации графа. dfn3 работает на 48 кГц.
assets/models/dfn3/, /ort/Пути: modelUrl (модели), cdnUrl (каталог ORT wasm), ortUrl (ESM-модуль ORT, по умолчанию ${cdnUrl}ort.bundle.min.mjs).
threads1Потоки wasm-инференса. На слабых CPU 2–4 заметно снижают время кадра; требует COOP/COEP (он и так обязателен).
highpassHz80High-pass на входе: срезает ветер в микрофон, хендлинг-шум и DC до модели. 0 — выключить.
activationОнлайн-активация: { key, activateUrl?, refreshUrl? }. Обязательна для dfn3 (выдаёт модель). URL по умолчанию — сервер Peito.
licenseбез проверкиОфлайн-проверка: ключ + режим enforce/warn, опц. onlineCheck.
initTimeoutMs20000Таймаут готовности воркера в init(). По истечении — PeitoError(E_WORKER_TIMEOUT).
fallbackЦепочка деградации бэкендов, напр. ['dfn3','spectral'].
debug{ verbosity: 'error' }Логирование в консоль: 'silent' | 'error' | 'info' | 'debug'; metricsHz.
captureSeconds8Сколько секунд держать для dumpWav().

Остались вопросы? Посмотрите живое демо — там весь конвейер с метриками и спектрограммами, или начните с Trial-лицензии.