Технический

Локальная диаризация говорящих: как мы создали на 100% приватный сервис заметок со встреч

OpenWhispr — это ассистент для встреч с открытым исходным кодом, в котором ваше аудио никогда не покидает устройство. Каждый голосовой отпечаток, каждая метка говорящего, каждый эмбеддинг — обрабатываются и хранятся на вашей собственной машине.

OpenWhispr

OpenWhispr

Инженерное дело

15 апреля 2026
Оглавление

Локальная диаризация говорящих — это процесс разметки того, кто и когда говорил в записи встречи, выполняемый целиком на устройстве пользователя, без передачи аудио, эмбеддингов или стенограмм на какой-либо внешний сервер. У стенограмм встреч есть проблема «кто что сказал». Zoom выдаёт вам сплошную стену текста. Большинство ИИ-сервисов для заметок — Otter, Fireflies, Read, Granola — решают её, загружая аудио вашей встречи на свои серверы, прогоняя там модель распознавания говорящих и возвращая размеченные сегменты. Ваш голос и голоса ваших коллег оказываются векторами в чужой базе данных. Мы этого не хотели — и наши пользователи тоже. OpenWhispr — это сервис заметок со встреч с открытым исходным кодом, построенный на противоположном принципе: аудио никогда не покидает ваше устройство, голосовые отпечатки живут в локальном файле SQLite на вашем диске, и ничто о встрече не загружается куда-либо, пока вы сами явно этого не выберете. Эта статья — технический разбор той части этого обещания, что касается диаризации: что это такое, какие модели мы выбрали, как они работают вместе и где именно находится каждый байт данных на каждом шаге.

Последнее обновление: 15 апреля 2026. Детали реализации опираются на функцию диаризации говорящих, добавленную в десктопное приложение OpenWhispr 14 апреля 2026 года. Каждое техническое утверждение ниже подтверждено первоисточниками или напрямую проверяемо в репозитории с открытым исходным кодом.

Снимок проверки фактов (два источника)

  • Диаризация говорящих отвечает на вопрос «кто и когда говорил». Её оценивают по таким общепринятым бенчмаркам, как NIST Rich Transcription, и эталонному конвейеру pyannote.audio. Оценка NIST RT · Статья о pyannote.audio (Bredin et al., 2020)
  • Мы используем pyannote-segmentation-3.0 и CAM++ из проекта 3D-Speaker. Оба имеют открытый исходный код, оба переносимы в ONNX и оба напрямую загружаются из релизов моделей sherpa-onnx. Карточка модели pyannote-segmentation-3.0 · Статья о CAM++ / 3D-Speaker (Wang et al., 2023)
  • Весь инференс выполняется через sherpa-onnx — нативный ONNX-бинарник: без среды Python, без PyTorch, без GPU. Мы фиксируем версию бинарника и его зависимостей на этапе сборки. sherpa-onnx на GitHub · Релизы sherpa-onnx
  • Голосовые отпечатки — это 512-мерные векторы float32, хранящиеся как локальные BLOB в SQLite, примерно 2 КБ на говорящего. Они никогда не покидают устройство. Документация по типам данных SQLite · ONNX Runtime
  • Во время диаризации с устройства не передаются ни аудио, ни эмбеддинги, ни стенограммы. Единственная сетевая активность во всей функции — однократная загрузка моделей при первом запуске, которую вы можете проверить сами в репозитории с открытым кодом. Политика конфиденциальности OpenWhispr · OpenWhispr на GitHub

Что на самом деле такое диаризация говорящих

Диаризация говорящих — это процесс разметки того, кто и когда говорил в записи, а не того, что именно было сказано. Транскрипция отвечает на вопрос «что», диаризация — на вопрос «кто». Эти задачи дополняют друг друга: хорошему сервису заметок со встреч нужны обе, и заставить их работать совместно сложнее, чем каждую по отдельности.

Диаризация по-настоящему сложна по нескольким упрямым причинам. Два голоса могут накладываться в одном звонке. Просачивается фоновый шум. Короткие реплики («ага», «окей») почти не несут информации о говорящем. Новые участники появляются в середине встречи. И, в отличие от транскрипции, диаризации приходится принимать глобальные решения — она не может разметить седьмой сегмент, не сравнив его со вторым. Именно из-за этого глобального контекста большинство коммерческих решений выполняют диаризацию на сервере по полной записи, а затем возвращают размеченную стенограмму.

Одной транскрипции недостаточно. Встреча на десять человек с сорока пятью минутами перебиваний, превращённая в одну сплошную стену текста, нечитаема. Заметки со встречи — тот самый результат, который людям действительно нужен, — требуют привязки к говорящим, потому что задачи, решения и обязательства всегда привязаны к людям. «Алиса развернёт миграцию к пятнице» — это заметка. «Развернёт миграцию к пятнице» — это шум.

Диаризация — это не то же самое, что распознавание говорящего

Диаризация говорит: «эти три фрагмента — это Говорящий A; эти два — Говорящий B». Она не знает, кто такой Говорящий A. Распознавание говорящего присваивает Говорящему A имя, сопоставляя голосовой отпечаток с сохранённым профилем. OpenWhispr делает и то, и другое — диаризация запускается на каждой встрече, а распознавание включается, как только вы пометили кого-то по имени. И то, и другое остаётся локальным.

Четырёхэтапный локальный конвейер

Конвейер диаризации говорящих в OpenWhispr состоит из четырёх этапов: детекция голосовой активности, сегментация, эмбеддинг говорящего и кластеризация — за которыми следует шаг слияния, согласующий сегменты говорящих со стенограммой. Каждый этап — это отдельная ONNX-модель или нативный бинарник. Без среды Python, без PyTorch, без CUDA. Суммарный объём моделей на диске — около 45 МБ.

Местный канал диаризации

Захват звукаМикрофон + система, 16kHz
VADSilero, 2MB
Сегментацияpyannote 3.0
ВстраиваниеCAM++, 512-dim
КластеризацияAgglomerative, 0,5
ЭтикеткиПрофили совпадений в SQLite

Почему он остается локальным:

Каждый этап запускается на вашем устройстве с помощью одного двоичного файла sherpa-onnx — ни Python, ни облака.

~45MB из моделей ONNX загружены один раз, а затем кэшированы в ~/.cache/openwhispr/diarization-models/..

Вложения — это векторы 512-dim float32, хранящиеся как SQLite BLOBs на локальном диске.

Захват аудио с первого дня идёт по двум потокам. Микрофон по определению — «это вы», поэтому мы никогда не тратим вычисления на попытку диаризовать ваш собственный голос: каждый сегмент с микрофона помечается как вы по источнику, а не по сопоставлению голосов. Системное аудио (все остальные участники звонка) — единственное, что действительно нуждается в разделении по говорящим, что сразу же вдвое сокращает работу.

После захвата аудио четыре этапа выполняются последовательно. VAD отбрасывает тишину, чтобы дорогостоящий этап эмбеддинга никогда не тратил ресурсы на пустые кадры. Сегментация разрезает непрерывную речь на фрагменты с одним говорящим. Модель эмбеддинга превращает каждый фрагмент в 512-мерный голосовой отпечаток. Агломеративная кластеризация группирует похожие отпечатки, формируя итоговый набор идентификаторов говорящих. Последний проход сводит эти идентификаторы обратно в стенограмму по перекрытию временных меток, так что каждое слово в итоге получает метку говорящего.

Всё это работает внутри единственного дочернего процесса, порождённого приложением, — без сервера, без прослушиваемого порта, без IPC к чему-либо за пределами Electron. Убедиться в этом можно, прочитав src/helpers/diarization.js в репозитории с открытым исходным кодом.

Этап 1: Детекция голосовой активности (Silero)

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

Мы используем Silero VAD: ONNX-модель размером 2 МБ под лицензией MIT, фактически открытый отраслевой стандарт. Silero возвращает покадровую вероятность того, что текущее 32-миллисекундное окно содержит речь. На современном CPU затраты ничтожны — около 0,1 миллисекунды на 32-миллисекундный фрагмент, или примерно треть процента одного ядра.

Конвейер реального времени непрерывно прогоняет Silero по системному аудио во время звонка. Пороги, с которыми мы реально поставляем продукт, настроены под реальное аудио встреч, а не под чистый бенчмарк:

  • Размер окна: 512 семплов (32 мс при 16 кГц)
  • Порог речи: 0,15 — намеренно агрессивный, чтобы не упустить тихих или удалённых говорящих
  • Порог тишины: 0,08
  • Сегмент завершается после: 16 подряд идущих окон тишины (~512 мс)
  • Минимальный сегмент для эмбеддинга: 0,8 секунды
  • Частота идентификации в реальном времени: каждую 1 секунду, как только накопилось ≥1,6 секунды речи

Честный компромисс

Агрессивный порог речи 0,15 ловит тихих говорящих на удалённых микрофонах, но иногда ложно срабатывает на громких нажатиях клавиш или шуме в помещении. Фильтр минимального сегмента в 0,8 секунды отсеивает большинство таких ложных срабатываний до того, как они доходят до этапа эмбеддинга, так что точность на последующих этапах защищена.

Этап 2: Сегментация (pyannote 3.0)

Сегментация берёт непрерывную речь и разрезает её на фрагменты, каждый из которых содержит только одного говорящего, обнаруживая как точки смены говорящего, так и зоны наложения. Это тот этап, который решает, где проходят границы говорящих. Всё, что идёт после него, исходит из того, что эти границы верны.

Мы используем pyannote-segmentation-3.0, текущую версию модели от команды pyannote.audio, экспортированную в ONNX проектом sherpa-onnx . Pyannote — фактический академический эталон: конвейер pyannote.audio достигает уровня ошибок диаризации в диапазоне 12–15% на стандартных бенчмарках вроде AMI и CALLHOME — тех самых цифр, которые любят приводить коммерческие облачные провайдеры.

Почему именно порт в ONNX? Потому что тащить PyTorch внутрь приложения Electron — заведомо нежизнеспособная идея: один только рантайм весит примерно 1,5 ГБ и для быстрой работы фактически требует GPU. Экспорт в ONNX — это единственный файл размером 6,6 МБ, который работает на CPU через ONNX Runtime. Та же модель, ничтожная доля веса.

Мы вызываем её через офлайн-бинарник диаризации sherpa-onnx с несколькими флагами. Настройки минимальной длительности взяты из рекомендованных значений по умолчанию из статьи pyannote: короткие речевые всплески сливаются с прилегающей тишиной, а короткие паузы не разрывают непрерывный сегмент говорящего. На выходе — список пар «начало/конец», помеченных предварительными идентификаторами говорящих ( speaker_0, speaker_1 и т. д.).

Этап 3: Эмбеддинги говорящих (CAM++)

Эмбеддинг говорящего — это список чисел фиксированной длины, который представляет голос примерно так же, как хеш представляет файл. Две записи одного и того же голоса дают эмбеддинги, близкие в векторном пространстве; разные голоса дают эмбеддинги, далёкие друг от друга. «Близость» измеряется косинусным сходством — единственным числом от -1 до 1. Это математическое сердце всего остального в конвейере.

Наша модель эмбеддингов — CAM++ из проекта 3D-Speaker академии DAMO компании Alibaba, обученная на бенчмарке верификации говорящих VoxCeleb . Конкретный файл, который мы поставляем, — 3dspeaker_speech_campplus_sv_en_voxceleb_16k.onnx, ONNX-экспорт размером примерно 28 МБ, выдающий 512-мерный вектор float32 на каждый сегмент. Статья — arXiv:2303.00332 (Wang et al., 2023).

Мы выбрали CAM++ вместо очевидной альтернативы, ECAPA-TDNN, по трём конкретным причинам: примерно вдвое меньше параметров, более низкий уровень равной ошибки на VoxCeleb1-O и более быстрый инференс на CPU. У модели MSDD от NVIDIA NeMo нет официального экспорта в ONNX, и она сильно завязана на PyTorch. Picovoice Falcon несёт коммерческую лицензию на каждое рабочее место. Фреймворк CoreML Speech от Apple работает только на macOS. CAM++ оказался единственным вариантом, удовлетворяющим нашим ограничениям, — кроссплатформенный, ONNX, открытая лицензия, быстрый на CPU.

Чтобы подавать данные в CAM++, мы поставляем собственный экстрактор признаков log-mel filterbank в src/helpers/speakerEmbeddings.js: 80 mel-полос, окна 25 миллисекунд, шаг 10 миллисекунд — стандартные параметры для моделей этого класса. Признаки поступают в ONNX-модель через onnxruntime-node, работающий в основном процессе Electron. На выходе получается Float32Array на 512 элементов, который мы записываем в SQLite как BLOB на 2048 байт.

Сам шаг сопоставления — простейшая из возможных операций: косинусное сходство между двумя векторами. Вот буквальный код, который решает, принадлежат ли два сегмента встречи одному и тому же говорящему:

// src/helpers/speakerEmbeddings.js
cosineSimilarity(a, b) {
  let dot = 0, normA = 0, normB = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  const denom = Math.sqrt(normA) * Math.sqrt(normB);
  return denom === 0 ? 0 : dot / denom;
}

Почему 512 измерений?

512 — это текущая золотая середина между точностью и затратами на хранение в таблице лидеров по верификации говорящих VoxCeleb. На 256 измерениях начинает теряться различающая способность на близких, но разных голосах; 1024 измерения добавляют хранение и вычисления при убывающей отдаче. 512 чисел float32 × 2 КБ × тысяча встреч по десять говорящих — это 20 МБ на диске. Это с запасом помещается в локальный файл SQLite.

Этап 4: Кластеризация — проблема «сколько говорящих?»

После эмбеддинга каждого сегмента у вас есть N векторов. Кластеризация группирует их в K говорящих — но сложность в том, что обычно вы не знаете K заранее. В звонке о продаже может быть 2 говорящих. На общем собрании — 8. Кластеризация с фиксированным K ломается в обоих случаях.

Мы используем агломеративную кластеризацию с порогом косинусного сходства 0,5: начинаем с того, что каждый сегмент — собственный кластер, сливаем два кластера с наибольшим сходством и повторяем, пока ни одна из оставшихся пар не превышает порог. Где остановиться, решает именно порог, а не целевое число. Это естественным образом масштабируется от двух говорящих до десяти без какой-либо настройки.

Сам вызов находится в src/helpers/diarization.js. Это запуск бинарника sherpa-onnx с несколькими флагами командной строки:

// src/helpers/diarization.js
const args = [
  `--segmentation.pyannote-model=${segPath}`,
  `--embedding.model=${embPath}`,
  `--clustering.num-clusters=${numSpeakers}`,    // -1 = auto
  `--clustering.cluster-threshold=${threshold}`, // 0.5 default
  "--min-duration-on=0.2",
  "--min-duration-off=0.5",
  wavPath,
];

На выходе — обычный текст: по строке на сегмент в формате start_sec -- end_sec speaker_NN. Мы разбираем это в массив объектов, а затем проходим по стенограмме, сопоставляя каждый сегмент стенограммы с тем сегментом диаризации, у которого максимальное перекрытие по времени. Сегменты, полученные с микрофона, всегда помечаются как you — источник всегда побеждает голос.

Порог 0,5 — не догадка. Мы подобрали его с помощью собственного набора для оценки (scripts/meeting-diarization-eval.js) на реальных записях встреч. При более высоких порогах кластеров слишком мало — один говорящий разбивается на двух. При более низких порогах кластеров слишком много — два похожих голоса сливаются. 0,5 — золотая середина, к которой мы пришли.

Диаризация в реальном времени: метки прямо во время речи

Только пакетная диаризация точна, но незаметна во время звонка. Только потоковая — мгновенна, но шумна. OpenWhispr запускает обе: метки в реальном времени появляются во время встречи, а полный пакетный проход уточняет их потом.

Потоковый путь находится в src/helpers/liveSpeakerIdentifier.js. Он перехватывает поток системного аудио на частоте 16 кГц, подаёт каждый 32-миллисекундный кадр в Silero VAD и накапливает речевые кадры, пока сегмент либо не закроется (после ~512 мс тишины), либо не достигнет частоты идентификации в реальном времени (каждую 1 секунду, как только накопилось ≥1,6 секунды). При срабатывании он извлекает эмбеддинг CAM++, сравнивает его с хранящейся в памяти картой эмбеддингов активного звонка и сохранёнными профилями говорящих и генерирует IPC-событие — meeting-speaker-identified — с наилучшим совпадением. Сторона React подхватывает его и обновляет пузырёк стенограммы на месте.

Как только человек вручную задал имя говорящего в интерфейсе, это назначение становится заблокированным. Послевстречный пакетный проход не имеет права его перезаписывать, даже если его собственный анализ с ним не согласен. Это поведение блокировки находится в src/utils/transcriptSpeakerState.ts, и оно важно: худший возможный пользовательский опыт — позволить автоматическому проходу молча перезаписать исправление пользователя.

Когда запись останавливается, мы запускаем полную офлайн-диаризацию по целому WAV-файлу. У пакетного прохода есть полный контекст — он видит всю встречу целиком, а не только последние 1,6 секунды — и выдаёт более чистую итоговую разметку. Пакетный результат согласуется с метками реального времени, соблюдает каждую блокировку пользователя и заменяет предварительные догадки более точной группировкой.

Почему гибрид лучше любого из подходов по отдельности

Только потоковый подход быстр, но шумен на коротких сегментах. Только пакетный точен, но молчит до конца встречи. Гибрид даёт и то, и другое — мгновенную обратную связь во время звонка и итоговую стенограмму, отражающую анализ с полным контекстом. Цена — один дополнительный шаг согласования, который один раз выполняется в фоне и фактически бесплатен.

Голосовые профили: запоминание говорящих между встречами

Стоит вам один раз пометить говорящего как «Алиса» на одной встрече, и OpenWhispr будет помечать её автоматически на каждой будущей встрече — без каких-либо ваших действий и без того, чтобы её голосовой отпечаток когда-либо покидал ваше устройство.

Механизм прост: центроидный эмбеддинг каждого говорящего сохраняется в вашей локальной базе данных SQLite. Когда на будущем звонке появляются новые эмбеддинги, мы сравниваем их по косинусному сходству с каждым сохранённым профилем. Совпадение выше порога автоматически присваивает имя. Ничто из этого не касается сервера. Три таблицы, которые за это отвечают, — это обычный SQLite:

-- src/helpers/database.js
CREATE TABLE speaker_profiles (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  display_name TEXT NOT NULL,
  email TEXT,
  embedding BLOB NOT NULL,          -- 512 float32s (~2KB)
  sample_count INTEGER DEFAULT 1,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE speaker_mappings (
  note_id INTEGER NOT NULL,
  speaker_id TEXT NOT NULL,
  profile_id INTEGER,
  display_name TEXT NOT NULL,
  PRIMARY KEY (note_id, speaker_id),
  FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
  FOREIGN KEY (profile_id) REFERENCES speaker_profiles(id) ON DELETE SET NULL
);

CREATE TABLE note_speaker_embeddings (
  note_id INTEGER NOT NULL,
  speaker_id TEXT NOT NULL,
  embedding BLOB NOT NULL,
  PRIMARY KEY (note_id, speaker_id),
  FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
);

Сопоставление трёхуровневое — чтобы сбалансировать уверенность и ложные срабатывания:

  • Косинус ≥ 0,70: автоподтверждение. Метка появляется немедленно без запроса.
  • 0,55 ≤ Косинус < 0,70: предложение. Интерфейс показывает «Это Алиса?» с кнопками подтвердить / отклонить и ждёт вашего ввода.
  • Косинус < 0,55: остаётся анонимным. Сегмент остаётся Говорящим N, пока вы не назовёте его вручную.

Когда вы подтверждаете совпадение, сохранённый центроид обновляется через скользящее среднее, а не полной перезаписью. Формула проста: new_avg = (stored_avg × sample_count + new_embedding) / (sample_count + 1). Это справляется с дрейфом голоса — холодное утро, другой микрофон, плохой кабель — не ломая будущие совпадения. Чем больше образцов голоса Алисы видела система, тем стабильнее становится её профиль.

Новые профили также запускают ретроактивную переразметку. Когда вы впервые называете говорящего, фоновая задача проходит по сохранённым эмбеддингам каждой прошлой встречи, находит совпадения и обновляет ранее безымянных говорящих в исторических стенограммах. Заблокированные пользователем сопоставления никогда не затрагиваются. В результате: один раз назвав Алису, вы ретроактивно помечаете её во всех прошлых звонках, в которых она участвовала.

Если подключён Google Calendar и у встречи есть участники, интерфейс выбора говорящего заранее заполняется их именами и адресами электронной почты. Один клик сопоставляет Говорящий 2 alice@example.com и сохраняется навсегда — участники и голосовые профили остаются связанными, так что правильные имена отображаются даже на разных устройствах.

Где на самом деле живут ваши данные

Аудио никогда не покидает ваше устройство. Это не маркетинговая фраза — это механическое свойство кода, и поскольку у OpenWhispr открытый исходный код, вы можете проверить это сами. Вот что именно происходит с каждым фрагментом данных на каждом шаге.

ЧтоГде это хранитсяПокидает ли устройство?
Сырое аудио встречиВременный каталог ОС, удаляется после диаризацииНет — никогда
ONNX-модели диаризации~/.cache/openwhispr/diarization-models/Загружаются один раз при первом запуске, затем больше никогда
Эмбеддинги говорящих (голосовые отпечатки)Локальный BLOB в SQLiteНет — никогда
Сопоставления говорящего и имениЛокальная таблица SQLiteНет — никогда
Текст стенограммыЛокальный SQLite (опциональная облачная синхронизация, если вы её включите)Только если вы согласитесь

Сырое PCM-аудио со встречи записывается в файл во временном каталоге вашей ОС, передаётся локально порождённому бинарнику sherpa-onnx и удаляется в блоке finallyпо завершении диаризации. В конвейере диаризации OpenWhispr нет ни одного пути кода, который открывает сетевой сокет для загрузки аудио. Поскольку код открыт, вы можете выполнить grep по src/helpers/diarization.js, src/helpers/liveSpeakerIdentifier.js, и src/helpers/speakerEmbeddings.js на предмет fetch, axios, или http и убедиться в этом самостоятельно.

IPC между основным процессом и процессом-рендерером Electron использует встроенный contextBridge — канал уровня ядра внутри единственного процесса, а не сетевой сокет. Эмбеддинги говорящих и голосовые профили хранятся как BLOB в SQLite в том же файле базы данных, что и ваши заметки, с теми же правами доступа на уровне ОС.

Одно исключение, без утаивания

Единственная сетевая активность в этой функции — однократная загрузка моделей при первом запуске: три ONNX-файла, скачиваемые из CDN релизов sherpa-onnx на GitHub. Мы прямо это документируем, потому что «один сетевой вызов при настройке, ноль после» — это совсем другая история приватности, чем «всегда включённый облачный инференс», и как ассистент для встреч с открытым кодом мы предпочтём сказать вам об этом, а не дать вам обнаружить это самим.

Честные ограничения

Локальная диаризация хороша, но не идеальна. Вот случаи, где она по-настоящему пасует, и что мы с каждым из них делаем.

  • Накладывающиеся говорящие. Когда два человека говорят одновременно, это по своей природе несёт потери для разметки одной меткой. Pyannote 3.0 обнаруживает зону наложения, но единственного speaker_id на сегмент недостаточно, чтобы представить «говорят оба». Мы помечаем такие сегменты как предварительные и даём вам исправить их в интерфейсе.
  • Точность на холодном старте. Первая встреча с новым коллегой использует только обобщённые признаки, обученные на VoxCeleb. Стоит вам один раз пометить его и накопить в профиле несколько образцов, как последующие встречи становятся заметно точнее.
  • Короткие реплики. «Ага», «окей», короткий смешок — всё, что короче ~0,8 секунды, не может дать надёжный эмбеддинг. Мы отбрасываем такие сегменты на этапе эмбеддинга и переходим к распространению меток из окружающего контекста.
  • Дальнее поле или аудио с низким SNR. Кто-то на громкой связи в шумной комнате будет деградировать плавно, но заметно. Автоматическая регулировка усиления и настройка VAD помогают, но никакая изощрённость модели не способна полностью преодолеть плохую физику.
  • Инференс только на CPU. С GPU было бы быстрее, но мы намеренно его не требуем. На практике пропускной способности современного CPU достаточно: ~100 миллисекунд на эмбеддинг и около 30 секунд пакетной диаризации для 45-минутной встречи на Mac с M1.

Мы публикуем наш набор для оценки ( (scripts/meeting-diarization-eval.js) ) в репозитории с открытым кодом, чтобы вы могли измерить всё это сами, на собственных записях — а не на наших тщательно отобранных демо-роликах. Это кажется честнее, чем глянцевая цифра в маркетинговой презентации.

Почему мы выбрали Sherpa-ONNX

Нам нужен был рантайм диаризации, который кроссплатформенный, без Python, быстрый на CPU и с открытым исходным кодом. Sherpa-ONNX оказался единственным вариантом, удовлетворяющим всем четырём условиям.

Наши требования были конкретными. Он должен был работать на macOS (как Intel, так и Apple Silicon), Windows x64 и Linux x64 без отдельных веток кода. Он не мог тащить с собой PyTorch — один только рантайм весит около 1,5 ГБ и по сути требует GPU, чтобы быть пригодным к работе при интерактивных задержках. Он не мог требовать CUDA. И лицензия должна была быть достаточно свободной, чтобы поставлять её в коммерческом, но открытом десктопном приложении.

Вот что мы реально рассматривали:

  • pyannote.audio (Python): исключён. Тащить PyTorch в приложение Electron — заведомо нежизнеспособная идея.
  • NVIDIA NeMo MSDD: зависит от PyTorch, нет официального экспорта в ONNX, ориентирован на GPU.
  • Picovoice Falcon: коммерческая лицензия на каждого активного пользователя плюс облачная активация при первом запуске.
  • Apple CoreML + фреймворк Speech: только macOS, нет кроссплатформенного паритета.
  • sherpa-onnx: Apache 2.0, нативный ONNX-бинарник, ~35 МБ моделей, интерфейс «запусти и читай stdout», активно поддерживается командой k2-fsa. Тот же тулчейн обеспечивает работу ASR в продакшене других выпущенных продуктов.

Одна честная оговорка: офлайн-бинарник диаризации говорящих в sherpa-onnx относительно нов. Мы фиксируем версию на v1.12.23 и прогоняем весь наш набор для оценки против каждого обновления, прежде чем сдвинуть фиксацию. Для столь несущей конструкции это правильный компромисс.

Часто задаваемые вопросы

Есть ли приватный сервис заметок со встреч, который не загружает моё аудио в облако?
Да. OpenWhispr — это бесплатный сервис заметок со встреч с открытым исходным кодом, в котором ваше аудио никогда не покидает устройство. Транскрипция выполняется локально с помощью OpenAI Whisper или NVIDIA Parakeet, диаризация говорящих выполняется локально с помощью сегментации pyannote и эмбеддингов CAM++, а голосовые отпечатки хранятся в локальном файле SQLite на вашей собственной машине — не на наших серверах.
Могут ли Otter, Fireflies или Granola видеть мои встречи?
Да. Облачные ИИ-сервисы для заметок загружают аудио ваших встреч на свои серверы, чтобы транскрибировать и диаризовать его. Метки говорящих, стенограммы и голосовые эмбеддинги вычисляются в их инфраструктуре и хранятся в их базах данных. Если вам нужна альтернатива, которая держит каждый байт аудио на вашем устройстве, вам нужен локально-ориентированный инструмент вроде OpenWhispr.
Что такое диаризация говорящих?
Диаризация говорящих — это процесс разметки того, кто и когда говорил в записи, в отличие от транскрипции, которая отвечает на вопрос, что было сказано. Диаризованная стенограмма показывает каждую реплику с меткой говорящего — Алиса, Боб, Говорящий 3 — вместо одной сплошной стены текста.
Действительно ли локальная диаризация говорящих работает, или это шаг назад по сравнению с облаком?
Она работает. Конвейер OpenWhispr — pyannote-segmentation-3.0 плюс эмбеддинги CAM++ из проекта 3D-Speaker, запускаемые через sherpa-onnx — достигает уровней ошибок диаризации в том же диапазоне, что и коммерческие облачные API, на стандартных бенчмарках. Главные честные компромиссы — обработка накладывающихся говорящих и точность на холодном старте для голосов, которые система ещё ни разу не слышала.
Сколько места на диске занимают локальные модели диаризации?
Около 45 МБ в сумме: сегментация pyannote (6,6 МБ) плюс модель эмбеддинга говорящего CAM++ (28 МБ) плюс Silero VAD (2 МБ). Все три загружаются один раз при первом запуске и кэшируются в ~/.cache/openwhispr/diarization-models/. После этого они остаются на диске и никогда не загружаются повторно.
Нужен ли GPU, чтобы запустить локальную диаризацию OpenWhispr?
Нет. Всё работает на CPU через ONNX Runtime. Современные Apple Silicon и недавние процессоры x86 справляются с этим без труда — на практике около 30 секунд пакетной диаризации для 45-минутной встречи на Mac с M1, при этом метки говорящих в реальном времени появляются в течение секунды-двух во время самого звонка.
OpenWhispr действительно с открытым исходным кодом?
Да. Код диаризации, конвейер транскрипции и остальная часть десктопного приложения имеют открытый исходный код на GitHub. Заявления о приватности в этой статье напрямую проверяемы: выполните grep по вспомогательным функциям диаризации на предмет fetch, axios или http — и вы не найдёте ни одного сетевого вызова в пути диаризации.
Что происходит, когда два человека перебивают друг друга в звонке?
Pyannote 3.0 явно обнаруживает и аннотирует зоны наложения, но разметка одной меткой во время наложения по своей природе несёт потери. Мы помечаем такие сегменты как предварительные в стенограмме и даём вам исправить их, кликнув по метке говорящего. Ваши ручные исправления блокируются и никогда не перезаписываются последующими автоматическими проходами.

Попробуйте приватный сервис заметок со встреч

OpenWhispr — это бесплатный ассистент для встреч с открытым исходным кодом, в котором ваше аудио никогда не покидает устройство. Локальная диаризация, локальная транскрипция, локальные голосовые профили. Работает на macOS, Windows и Linux.

Аккаунт не нужен · Работает офлайн · Открытый код навсегда