- BrainTools - https://www.braintools.ru -

Написал локального ИИ-ассистента для зумов: диаризация, граф знаний и ноль облаков

Полгода назад я прикинул, сколько рабочих созвонов ежедневно улетает на чужие сервера через модные ИИ-ноутейкеры. Стало как-то неуютно. Otter, Granola, Fireflies — все льют разговоры в облако. Granola в этом году вообще отличилась: The Verge раскопали, что заметки по дефолту доступны любому по ссылке, а на пользовательских данных крутят модели (opt-out ожидаемо зарыт в Enterprise-тарифе).

Решил проверить, можно ли собрать ассистента целиком на своём железе. Спойлер: можно. Работает каждый день на M1 Max под Apache 2.0.

Что получилось

Charoite слушает микрофон и системный звук (через BlackHole, никаких криповых ботов в звонке), транскрибирует на лету, различает голоса, отвечает на вопросы по ходу встречи. А после стопа — обновляет граф знаний в Obsidian: люди, системы, решения, сквозные темы.

Всё крутится локально. GigaAM v3 для русского STT, qwen3.6:35b-a3b и qwen3.5:4b через Ollama, ERes2Net для эмбеддингов голосов.

Про эхо отвечу сразу, пока не набежали в комменты. Микрофон и системный звук пишутся в разные каналы. Когда в BlackHole идёт речь, одновременный микрофонный чанк просто дропается — такой вот дешёвый канальный AEC. В оффлайн-проходе то же самое делается точнее, фильтром по перекрытию интервалов. Так что сидеть в наушниках не обязательно.

Ниже — самые интересные грабли. Их было много.

STT: почему GigaAM, а не Whisper

Нужен был нормальный русский и скорость. GigaAM v3 (Сбер, MIT, крутится через onnx_asr) жует 3-секундный чанк за 0.1–0.6 секунды на M1 Max. И главное — ставит пунктуацию из коробки. Хвостовой знак вопроса триггерит у меня быстрый ответ. Собеседник что-то спросил — через пару секунд на экране висит черновик ответа от первого лица.

Почему не Whisper-large-v3-turbo? На коротких русских чанках он люто галлюцинирует. Он заточен под 30-секундные окна, и обрезки без паддинга просто ломают ему механизм внимания [1]. (Для английского в конфиге есть Parakeet TDT — у него WER 6.32% против 7.44% у Whisper на Open ASR Leaderboard).

Диаризация: два прохода вместо одного

Диаризация на лету через эмбеддинги — та еще боль [2]. Все, кто пилил продукты на pyannote, в курсе: DER около 19% на датасете AMI, метки скачут прямо посреди фразы.

Я бенчил три модели эмбеддингов на своих реальных записях: ERes2Net, CAM++ и TitaNet. Выиграла ERes2Net — лучше всего сепарирует своих и чужих. Но чуда не случилось, распределения всё равно пересекаются. У одного и того же человека косинус между чанками обычно держится высоко, а на плохом телефонном канале проседает до 0.29 (при том, что чужие голоса там же дают ≤0.16).

Поэтому в лайве работает не жёсткий порог, а гистерезис. Смена спикера триггерится, только если другой голос стабильно ближе. Серая зона тянется за текущим спикером, новый голос заводится по двум согласным чанкам подряд. Один провалившийся чанк никого не дробит.

А после встречи запускается второй проход. Полная запись диаризуется заново по каналам, эхо режется по перекрытию. Кластеры, наговорившие меньше 25 секунд суммарно за всю встречу, вливаются в соседей — это осколки, а не люди (однажды так вылезло 23 «голоса»). Сегменты заново транскрибируются, и текст собирается в чистые абзацы по говорящим. Живая версия остаётся черновиком рядом.

Имена: ловушка обращений

Самый тупой баг прилетел откуда не ждали. Модель должна мапить имена на метки «Собеседник N» по стенограмме. На первом же групповом созвоне кто-то говорит: «Саш, ну кто это заапрувит?». И LLM радостно вешает на говорящего ярлык «Саша».

Обращение — это почти всегда имя другого человека, но логика [3] модели тут пасует. Плюс звательные падежи. Плюс всякие междометия типа «Опа», которые она принимает за имя.

Лечится двумя слоями. Во-первых, промпт явно объясняет разницу: «имя внутри реплики — это обращение; говорящий — тот, кто ответил следом». Во-вторых, фильтр в постобработке отклоняет имя, если оно звучит только в репликах самой метки и это не самопредставление. Ну и жёсткое правило: имя владельца компа никогда не угадывается — берется только из конфига.

Граф знаний: markdown вместо графовой БД

Markdown вместо тяжелых баз. Каждая встреча после стопа парсится в заметку с вики-ссылками. Узлы (Люди/Системы) апдейтятся датированными фактами. Старое не затирается, а помечается как вытесненное. Сквозные темы живут в «Ядрах»: у каждого свой статус и хроника по встречам.

На следующем созвоне, если зацепили старую тему, в тезисы прилетает алертом: «⏮ обсуждали 15.07, статус был такой-то». Матчинг тем пока наивный — стемминг по обрезанным окончаниям. Для коротких названий ядер этого хватает, честная лемматизация в планах.

По сути, это трёхслойная архитектура а-ля Graphiti/Zep (эпизоды → сущности → сообщества), только на чистом маркдауне. Зато работает grep, Obsidian и git.

Перед выбором модели я закопался в свежие статьи по темпоральным графам знаний (Zep, ATOM, T-GRAG, LightRAG). Вывод: планка для structured-экстракции — это модели 30B-класса. Всё, что ниже, начинает сыпать битым JSON и терять онтологию. Поэтому основная модель у меня qwen3.6:35b-a3b. На реальной стенограмме она отдает первый токен за 0.27 с, а полный ответ за 2.2 с (против 1.08 с и 4.5 с у плотной gemma4:26b). MoE с ~3B активных параметров реально быстр на генерации. Prefill длинного контекста, понятно, упирается в память [4], поэтому контекст везде жёстко ограничен.

Саммари, которое реально прочитать за минуту

После встречи генерится выжимка по жесткому фреймворку. Сначала суть одной строкой (BLUF — Bottom Line Up Front, как в военных сводках). Потом темы списком, принятые решения, экшены в формате «кто-что-срок» и открытые вопросы.

В конце идет связка с прошлыми созвонами: «было: перенос релиза (15.07) — сегодня: подтвердили, срок август». Этот кусок собирается из хроники Ядер и двух предыдущих саммари (с отсечкой по дате, чтобы будущее не утекало в прошлое при перегенерации).

И никаких markdown-таблиц. Plain-текст читается везде, а таблицы без рендера превращаются в кровавую кашу.

Грабли Ollama, о которых лучше знать заранее

Первый факап — контекст. У некоторых моделей в Modelfile захардкожено 262144. Если не передать явный num_ctx, Ollama честно пытается аллоцировать под него KV-кэш. Лишние гигабайты улетают в unified memory, мак уходит в жесткий своп, генерация падает на дно (а на тачках послабее это тупо OOM). Потерял на этом вечер. Теперь num_ctx: 8192 гвоздями прибито в каждом вызове.

Второй прикол — бюджет оперативки. 32 ГБ — это qwen3.6 (23 ГБ) плюс одна лёгкая моделька. Когда я на радостях подкинул третью, всё встало колом: Ollama начала вытеснять модели по кругу. В общем, две модели. Не три.

Кстати, про лёгкую модель: qwen3.5:4b против gemma4 (E4B). На моих задачах qwen точнее в классификации вопросов, быстрее в тезисах (2.9 с против 3.3 с) и жрет втрое меньше RAM в дефолтных сборках Ollama (3.4 против 9.6 ГБ). И да, это сравнение того, что реально скачивается по умолчанию, а не параметров при равном кванте. Пользователю в итоге важен резидентный размер.

Куда уходят данные?

Никуда. Сетевые вызовы идут строго на localhost к своей же Ollama. Проверяется не честным словом разработчика, а хоть через Wireshark, хоть через LuLu. Телеметрии ноль.

Полная аудиозапись лежит два дня (чисто для оффлайн-пересборки) и сносится. Эмбеддинги голосов живут в оперативке только во время звонка — слепков на диске нет. Фича с узнаванием голосов между встречами (когда я её допилю) этот принцип не сломает: она будет строго опциональной, с хранением эмбеддингов локально и явным opt-in. По дефолту биометрия по-прежнему не пишется. Облачный слой (разбор встречи через Claude CLI по подписке) в коде существует, но выключен по умолчанию и честно задокументирован.

Что дальше

Пилить меню-бар аппку под macOS, английские промпты и опциональное узнавание голосов между встречами (эмбеддинг → узел человека в графе). С последним пока нормально не справился ни один коммерческий продукт на рынке. Ну и просмотрщик графа.

Код, бенчи и полная документация лежат на Гитахбе [5].

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

Автор: Charoiteai

Источник [6]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/33385

URLs in this post:

[1] внимания: http://www.braintools.ru/article/7595

[2] боль: http://www.braintools.ru/article/9901

[3] логика: http://www.braintools.ru/article/7640

[4] память: http://www.braintools.ru/article/4140

[5] Код, бенчи и полная документация лежат на Гитахбе: https://github.com/charoiteai/Charoite_audio

[6] Источник: https://habr.com/ru/articles/1061864/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1061864

www.BrainTools.ru

Rambler's Top100