Локальный анализ звонков без GPU. Что происходит на пути от аудио до резюме

Я хотел проверить не то, насколько хорошо локальная LLM пишет резюме на красивом тестовом диалоге, а что произойдет с ней на обычном телефонном звонке с музыкой, автоответчиком, перебиваниями и плохим звуком.
Я сделал desktop приложение на Tauri, Rust, React и SQLite. Оно распознаёт речь, разделяет спикеров, считает тональность, эмоции и риски, а в конце локальная модель пишет резюме и рекомендации. Всё считается на компьютере, интернет нужен только чтобы скачать модели. При парсинге звонков часто бывают имена, адреса и номера договоров, и отправлять их во внешний API не хочется.
Несколько пример записей я брал с открытых источников интернета.
Почему такие модели и такое железо
Я выбирал не самые простые и не самые тяжёлые модели. Скорее модели, которые реально можно зпустить на обычном рабочем ноутбуке. Тестовая машина, Intel Core i7 10870H, 16 ГБ RAM, Windows, только CPU. Для итогового анализа используются Qwen 2.5 1.5B, то есть модели, которым хватает пары гигабайт.
Показать, что 27B (на момент статьи вышла уже Bonsai-27B Ternary) или выше на хорошей видеокарте пишет аккуратные резюме, не сложно, но такой стенд есть далеко не у каждого. Меня интересовало другое, что получит человек с ноутбуком и без GPU, если соберет такой пайплайн и пустит на него живые записи, а не красивый тестовый диалог.
Коротко о результате. Шесть прогонов (три звонка -> два распознавателя речи) закончились без единой ошибки, и ни одно из шести резюме я бы не отдал менеджеру без правки. Больше половины времени съела диаризация, а не модель. Ошибки, которые попадали в итог, почти всегда рождались на ранних этапах, а не в самой модели.
Что внутри

Этап | Модель | Размер |
STT | GigaAM v3 | ~224 МБ |
STT | Whisper Turbo Q5 / Full | ~574 МБ / ~1,6 ГБ |
Диаризация | speakrs (pyannote community-1: segmentation-3.0 + wespeaker-resnet34 + PLDA + VBx) | ~60 МБ |
Тональность | RuBERT-tiny2 | ~32 МБ |
Эмоции и риски | DeBERTa | ~131 МБ |
LLM | Qwen2.5-1.5B | ~1.6 ГБ |
Все модели одновременно в памяти не держатся каждая грузится перед своим этапом и выгружается после. LLM работает в отдельном процессе llama-server, тяжёлые операции отправляю в фоновых процесс (все как обычно).
Как я мерил
Три звонка: 3:16, 6:43 и 9:29. Все настоящие записи с настоящим шумом, музыкой в трубке и людьми, которые перебивают друг друга.
Каждый звонок прогоняется через два распознавателя, GigaAM и Whisper Full. Дальше всё одинаково, тот же Qwen, temp 0.2, контекст 6144, в LLM уходит не больше 4000 символов транскрипта.
Замер времени, по одному прогону на конфигурацию. Это не бенчмарк, а три случая, на которых видно, где цепочка ломается.
Эталонных резюме у меня не было. Правильность я оценивал сам, сверяя вывод с тем, что происходит в записи. Ниже в каждом случае написано, что в звонке было на самом деле.
Время, главный потребитель не там, где я ждал
Звонок | Распознаватель | STT, с | Диаризация, с | Refine ролей, с | Резюме, с | Итого | Итого / длина звонка |
3:16 | GigaAM | 35 | 141 | 15 | 25 | 3:41 | 1,1 |
3:16 | Whisper | 172 | 152 | 42 | 40 | 6:51 | 2,1 |
6:43 | GigaAM | 65 | 343 | 61 | 29 | 8:25 | 1,25 |
6:43 | Whisper | 589 | 322 | 89 | 36 | 17:23 | 2,6 |
9:29 | GigaAM | 149 | 441 | 135 | 62 | 13:13 | 1,4 |
9:29 | Whisper | 876 | 443 | 98 | 35 | 24:17 | 2,6 |
Я ожидал, что дольше всего будет думать LLM. Она занимает от 25 до 62 секунд на резюме. Диаризация же на CPU идёт со скоростью примерно 0,7-0,85 от длины записи и в прогонах с GigaAM занимает 64%, 68% и 56% всего времени. Whisper на CPU тоже недешев, 0,9 длины на коротком звонке и около 1,5 на длинных.
Итог для практики на ноутбуке весь пайплайн работает дольше самого разговора, даже с самым быстрым распознавателем. Для разбора записей задним числом это терпимо, для чего-то близкого к реальному времени нет.
Про диаризацию отдельно. Я использую speakrs (порт pyannote community-1), пороги onset 0.4 / offset 0.35, min_on 6 / min_off 12 кадров, перед инференсом моно-сигнал проходит компрессию (-30 дБ, 5:1) и gate, потому что голый gain в моно-миксе бесполезен и просто масштабирует обоих собеседников. Я пробовал разные настройки порогов и получил практически одинаковый результат. Поэтому проблема оказалась не в том, что я просто выбрал неправильный порог. Я также посмотрел в сторону Sortformer и WhisperX, но в моей конфигурации они не давали очевидного преимущества.
Что на самом деле в записях
Чтобы дальше было с чем сравнивать, коротко о том, что происходит в каждом звонке.
3:16, техподдержка. Клиентка недовольна, что в сетевом окружении Windows у неё отображаются 59 компьютеров с чужими названиями. Оператор объясняет, что это устройства в общей локальной сети, убрать их можно только отключив доступ к сети. Клиентка говорит, что тогда подумает о смене провайдера, задает второй вопрос про настройку компьютера после переустановки в магазине, и оператор переключает её на специалиста.
6:43, горячая линия. Исходящий звонок магазина клиентке по заявке. Оператор подтверждает доставку курьером до дома, сроки (пятница или вторник) и что доставка бесплатна. Клиентка ранее покупала предыдущую версию товара. Оператор обещает уточнить у руководства скидку от 500 рублей и уговаривает оставить отзыв в интернете. Клиентка отказывается написать отзыв.
9:29, провайдер. Клиент из другого города звонит, чтобы отключить услугу. Первые полторы минуты занимает голосовое меню автоответчика, которое перечисляет доступные услуги.
Случай 1. Автоответчик как суть разговора
Это самый показательный эпизод. Меню автоответчика перечисляет, какие услуги можно подключить. GigaAM честно расшифровал меню, диаризация склеила его с приветствием оператора и началом разговора в один кусок на две с половиной минуты, и LLM решила, что клиент звонит подключать дополнительные услуги. Резюме получилось про подключение доп. услуг, хотя человек звонил отключать.
С Whisper всё выглядело лучше, тема определена верно (Отключение услуг). Но в LLM уходит максимум 4000 символов, а транскрипт длиннее, поэтому модель физически не видела конец звонка. Резюме описывало в основном начало разговора. В поле оператор у Whisper осталось пустое значение, модель вместо номера спикера вернула служебный код из приветствия, парсер его отбросил. Пустое поле честнее, чем в прогоне с GigaAM, где локальная LLM уверенно назвала оператором клиента.
Ни один этап при этом не упал, JSON получился валидным.
Случай 2. Звонок от пожилого человека, одна метафора превратилась в совет
Транскрипты двух движков на одном и том же месте, середина звонка (клиентка описывает свою проблему, имена и нецензурное название компьютера убраны)
GigaAM
S2 00:06-00:29 скаите пожалуйста вот ясегодня ездила меняла новый компютер отела поменять из за чего из за того что у меня в сети оказывается вот я нажимаю кнопку пуск и сеть и у меня пятдесят девять наени всяких компютеровпктам пк даже есть пк что это такое S1 00:30-00:36 ну это тоьзвауя S2 00:36-00:40 да ас нет такойнелзя это убрать S1 00:41-00:48 вообще насттер идеттоодл котора... S1 02:24-02:28 самое вот ч соседи по квартире вот есть куда вы от них денетесь никуда
Whisper Full
S1 00:00-00:04 S1, здравствуйте.
S2 00:04-00:05 Добрый вечер.
S1 00:05-00:06 Скажите,
S2 00:06-00:30 пожалуйста, вот я сегодня ездила, меняла новый компьютер, хотела поменять. Из-за чего? Из-за того, что у меня в сети оказывается... Вот я нажимаю кнопку пуск и сеть. И у меня 59 наименований всяких компьютеров. ПК Кирилл, , ПК Андрей, там, ПК, даже есть [...] ПК. Что это такое? Ну, это
S1 00:30-00:35 пользователи вашей мини-локальной сети, то есть пользователи, которые находятся в вашем доме. Ну, я
S2 00:35-00:41 думаю, что [...] у нас нет такой. А нельзя это убрать? Вообще, это
S1 00:41-00:49 уже настройка компьютера идет. То есть это активные подключения... ...
S1 02:24-02:28 же самое, что соседи по квартире. Вот вы есть, куда вы от них денете? Никуда.
В середине звонка GigaAM выдаёт фонетическое крошево (вообще насттер идеттоодл котора), объяснение оператора про локальную сеть потерялось целиком, а фраза про соседей по квартире, которую оператор произнёс как аналогию, осталась. Ниже то, что сделала с этим LLM.
GigaAM + Qwen
{
"title": "Совет по удалению имени компьютера",
"tags": [
"консультация",
"оплата",
"ссылка"
],
"summary": "Пользователь обратился с вопросом о том, как удалить имя компьютера, которое не соответствует его личному компьютеру. Оператор предложил обратиться к соседям по квартире и подумать о переносе в другую компанию.",
"recommendations": [
"Перезвонить оператору для уточнения информации",
"Уточнить, если соседи по квартире не могут помочь"
],
"operator": {
"speaker": 0,
"reason": "не указано"
}
}Whisper Full + Qwen
{
"title": "Предложение по сетевому окружению",
"tags": [
"сетевое окру",
"оптимизация",
"конфликт"
],
"summary": "Клиенту объясняется, что 59 компьютеров в сети могут быть пользователем мини-локальной сети, и что это не проблема, но клиенту не нравится, что он не может отключить доступ к сети.",
"recommendations": [
"Предложить клиенту перейти в другую компанию, если проблема не решается"
],
"operator": {
"speaker": 1,
"reason": "Предложил клиенту перейти в другую компанию"
}
}Whisper здесь ближе к сути, но тоже ошибается, мысль надо подумать, перейти в другую фирму произносит клиентка, а LLM присвоила её оператору (Предложил клиенту перейти в другую компанию) и сделала из неё рекомендацию. В GigaAM версии та же реплика оказалась в резюме вместе с соседями по квартире, которых модель восприняла буквально. В обоих случаях в резюме нет главного, клиентку перевели на специалиста для второго вопроса.
Тег сетевое окру это отдельная история, о ней ниже, в разделе про баги.
Случай 3. Звонок в дверь
Начало звонка на горячей линии у двух распознавателей
GigaAM: а здравствуйте правильно попа да дравствуйте да это интернет магазинпять минуок е
Whisper: ЗВОНОК В ДВЕРЬ. АЛЛО. Здравствуйте, правильно попал? Да, здравствуйте, да. Это интернет-магазин, процессор для доления волос на всех данных в городах. 5 минут так есть пообщаться?
GigaAM на плохом аудио выдаёт заметную кашу и сам это показывает. Whisper выдаёт гладкий текст, в том числе выдуманный, фраза процессор для доления волос на всех данных в городах стоит на месте, где GigaAM слышит интернет магазин… пять минут. А звонок в дверь в самом начале это, похоже, подпись к гудкам, а не речь. LLM взяла её как название
GigaAM + Qwen
{
"title": "Заявка на доставку",
"tags": [
"заявка",
"доставка",
"заявка"
],
"summary": "Звонок с клиентом о заказе доставки товара. Клиент указывает, что товар доставляется до дома, а сроки доставки указаны. Оператор предлагает контактные данные для связи.",
"operator": {
"speaker": 1,
"reason": "предложил контактные данные"
}
}Whisper Full + Qwen
{
"title": "Звонок в дверь",
"tags": [
"консультация",
"оплата",
"доставка"
],
"summary": "Оператор предлагает услуги интернет-магазина и обсуждает сроки доставки.",
"operator": {
"speaker": 1,
"reason": "описывает процесс звонка"
}
}Название у GigaAM лучше, а резюме хуже, в нем роли перепутаны (доставку и сроки называет оператор, а не клиент), а предложил контактные данные это исказившаяся реплика про телефон горячей линии на коробке. У Whisper название из подписи к звуку, зато резюме в целом по делу. Оба резюме пропустили самую содержательную часть разговора, обсуждение отзыва в интернете и скидки. Здесь дело не в обрезке, а в транскрипте Whisper отзыв упоминается примерно на 2350-м символе, скидка на 3250-м, то есть в пределах 4000 символов, которые видит модель. Модель это видела и не включила в резюме. Она отрезала другое, транскрипт занимает 5023 символа, и последняя пятая часть с адресом, условиями оплаты и прощанием до LLM не дошла.
Итого по двум звонкам, победитель меняется. На пожилом человеке Whisper ближе к сути, на горячей линии по названию ближе GigaAM. По этим данным нельзя сказать, что Whisper в целом точнее, а GigaAM просто быстрее. Кроме самого очевидного вывода, GigaAM в этих тестах оказался в 5–9 раз быстрее.
Пунктуация и тональность
У GigaAM в моей конфигурации нет пунктуации. Сплиттер, который режет текст на куски для модели тональности, ищет . ! ? …, не находит и превращает весь звонок в один чанк:
GigaAM, горячая линия 6:43
{ "chunks": ["negative"], "confidence": 0.63, "label": "negative", "score": 19 }
Whisper, тот же звонок: 17 чанков
{ "chunks": ["neutral", "neutral", "positive", "neutral", "neutral", "neutral", "neutral", "neutral", "neutral", "positive", "neutral", "neutral", "neutral", "neutral", "neutral", "neutral", "positive"], "confidence": 0.70, "label": "neutral", "score": 50 }
Вердикт "негативный звонок, 19 баллов” у GigaAM это не настроение разговора, а артефакт единственного чанка. Так было во всех трёх звонках, 1 чанк у GigaAM (оценки 29, 19 и 27) против 9, 17 и 22 у Whisper. Красивое число в интерфейсе уверенно рисует то, чего в данных нет.
Диаризация, 18 реплик в минуту и роли, которые меняются местами
Формат результата диаризации:
{
"meta": {
"speaker_count": 2,
"overlaps": [
[
8.27,
8.48
],
[
12.18,
12.68
],
[
14.63,
14.84
],
"..."
]
},
"turns": [
{
"s": 4.25,
"e": 4.77,
"sp": "SPEAKER_00"
},
{
"s": 5.92,
"e": 8.47,
"sp": "SPEAKER_01"
},
{
"s": 8.30,
"e": 9.68,
"sp": "SPEAKER_00"
},
{
"s": 9.68,
"e": 11.32,
"sp": "SPEAKER_01"
},
"..."
]
}Количество реплик, 60 на звонке 3:16, 126 на 6:43 и 155 на 9:29. Это 18, 19 и 16 реплик в минуту, почти константа. Похоже, что диаризация дробит речь по своим настройкам, а не по структуре диалога (это гипотеза, я её пока не проверял на ручной разметке). Хорошо видно, как это выглядит в тексте, реплики режутся посреди фразы, а слово прилипает к соседнему спикеру.
S1 00:05-00:06 Скажите, S2 00:06-00:30 пожалуйста, вот я сегодня ездила, меняла новый компьютер... Что это такое? Ну, это S1 00:30-00:35 пользователи вашей мини-локальной сети... Ну, я S2 00:35-00:41 думаю, что [...] у нас нет такой. А нельзя это убрать? Вообще, это S1 00:41-00:49 уже настройка компьютера идет...
На горячей линии всё хуже, метки спикеров меняются местами прямо посреди звонка (Whisper-прогон)
S1 00:00-00:05 ...Здравствуйте, [имя] правильно попал? Да -> оператор (+ да клиентки) S2 00:17-00:35 Смотрите, мы обработали вашу заявку, можем в [город] доставить... -> оператор, но уже S2 S1 01:24-01:32 Просто я говорю, заказывала старую еще версию... -> клиентка, но уже S1
Оператор в этом звонке сначала S1, потом S2, клиентка наоборот. Дальше пайплайн отдаёт LLM реплики с номерами спикеров и просит определить, кто из них оператор. Когда метки шатаются, ответ модели ничего не значит, а в итоговом JSON он выглядит как факт ("operator": { "speaker": 1 }).
Я пробовал чинить это постобработкой без новых весов. Нормализовал метки, склеивал спикеров по косинусной близости, пробовал kmeans2, переприсваивал реплики с низкой уверенностью и рассчитывал turn_confidence по косинусной марже.
Особенно плохо обрабатываются короткие реплики и перебивания вроде да, в среду?. Когда человек говорит буквально долю секунды поверх другого, понять, кому принадлежит такая фраза, уже сложно даже после дополнительной обработки. Поэтому такие случаи я заранее не пытался исправлять полностью.
JSON валиден и неверен
Все шесть ответов LLM разобрались парсером и легли в базу без единой ошибки. Что в них при этом лежит:
durationMin: 45 минут в звонке на 3:16 (у GigaAM и Whisper), 30 и 45 в звонке на 6:43. Настоящая длительность модели не передаётся, и она её придумывает.
Оценка коучинга 75-80 и оценка качества 85-90 в четырёх разобранных прогонах. Для звонка, где клиентка уходит, обещая сменить компанию, и для звонка, где оператор уговаривает написать отзыв. Число выглядит убедительно, а меняется мало. В прогоне на 9:29 разница между двумя движками была около 15 баллов. Как KPI это использовать нельзя, максимум как подсказку наставнику.
Тег оплата в звонке про имя компьютера, тег заявка дважды подряд.
В двух прогонах GigaAM блок qa модель положила на верхний уровень JSON, а не внутрь coaching, как требует схема. Парсер этого не заметил и записал coaching.qa = null. Оценка 85 и комментарий модели (пользователь не смог настроиться после перезвонки оператора, но остался доволен качеством обслуживания) пропали молча.
Cырой ответ Qwen (GigaAM разговор пожилого человека) qa не там, где ждёт схема
{
"title": "Совет по удалению имени компьютера",
"coaching": {
"score": 75,
"session": {
"topic": "Удаление имени компьютера",
"durationMin": 45,
"tags": [
"консультация",
"оплата",
"ссылка"
]
}
},
"qa": {
"score": 85,
"comment": "Пользователь не смог настроиться после перезвонки оператора, но остался доволен качеством обслуживания",
"criteria": [
{
"name": "решение вопроса",
"score": 80
}
]
}
}Я нарочно сделал пайплайн fail-soft, если один этап не сработал, остальные результаты сохраняются. Это защищает от падения системы, но не от неправильного результата. Ошибки выше ни один этап не считает ошибкой.
Баги, а не слабость модели
Пока я разбирал прогоны, набрался список того, что можно починить в самом пайплайне, а не менять модель. Он оказался длиннее, чем я ожидал.
Паника truncate(24) на кириллице. Обрезка тегов шла по байтам и падала посреди символа. Умирал фоновый поток, и я молча получал пустое резюме. Поймал на аудио пожилого человека, исправил границей по символам и добавил юнит-тест. Это лучший пример fail-soft, который мне удалось найти. Система работает, а результата нет.
Тег сетевое окру. Тот же лимит в 24 байта режет слово, а не мысль. Лимит нужно считать в символах.
Потолок GigaAM ~200 секунд. Энкодер падает (Mul broadcast 5000×14242), а в продакшене оконного инференса пока нет. В бенче я резал на окна по 120с с перекрытием 1с.
Бюджет токенов. n_predict 512 -> 700 -> 900 -> 1200, на каждом шаге JSON с коучингом обрезался (no json object на срезанном {“spe). Парсер сканирует баланс скобок.
Refine ролей нестабилен. В двух прогонах он вернул ноль исправлений, на пожилом человеке с GigaAM (23 строки за 15с) и на горячей линии с Whisper (57 строк за 89с). Молчаливый пропуск без диагностики, причину я пока не установил. Кроме того, у refine нет порога уверенности, все правки применяются.
Whisper на музыке и IVR. Декодер падает и перезапускается, куски пропадают из транскрипта.
Мелочи с форматами. Whisper-GGUF (bad magic, whisper-rs вендорит whisper.cpp без GGUF-лоадера), пришлось вернуться к обычным .bin. Progress-callback whisper.cpp роняет бинарь на Windows (0xC0000139).
Что я думаю по итогам
Малые локальные модели не подвели в том смысле, в котором это обычно говорят. На входе у них лежали фонетическая каша или гладкая выдумка вместо текста, метки спикеров, которые шатаются, и обрезанный контекст. LLM на 1,5B параметров при таком входе не могла дать хороший результат. Я не знаю, как Qwen2.5 1.5B показала бы себя на чистом входе, потому что в этом эксперименте я отдельно её на такой задаче не проверял.
Три вещи, которые я вынес
Технический успех и содержательный успех разные. Все шесть прогонов завершились без единой ошибки, JSON везде валиден. Проверять нужно смысл, а не статус.
Ошибка раннего этапа доезжает до конца. Схема сначала факты, потом интерпретация работает, только если факты верны. Меню автоответчика, подпись к гудкам, шатающиеся метки спикеров дальше по цепочке становятся фактами, и ни один последующий этап не обязан это заметить.
Цифра в интерфейсе не означает метрику. Оценка 19 по тональности, коучинг 75-80, качество 85-90, все выглядят убедительно и почти не связаны с содержанием звонка.
Что я не проверил
Всего три звонка, один компьютер, одна конфигурация LLM. Выводы это наблюдения, не статистика.
Звонок / STT | Слов | Чанков тональности (score) | Название резюме | Главная проблема |
3:16 GigaAM | 280 | 1 (29) | Совет по удалению имени компьютера | Пропущено объяснение оператора, аналогия про соседей понята буквально |
3:16 Whisper | 438 | 9 (47) | Предложение по сетевому окружению | Реплика клиентки приписана оператору |
6:43 GigaAM | 462 | 1 (19, negative) | Заявка на доставку | Роли в резюме перепутаны, отзыв и скидка пропущены |
6:43 Whisper | 813 | 17 (50) | Звонок в дверь | Название из подписи к звуку, LLM видит 4000 из 5023 символов |
9:29 GigaAM | 726 | 1 (27) | Подключение доп. услуг | Меню автоответчика принято за суть разговора |
9:29 Whisper | 1021 | 45 (22) | Отключение услуг | Тема верна, но резюме по началу звонка, оператор не определен |
Здесь важно сделать одно уточнение. Этот эксперимент не показывает, что Qwen2.5 1.5B плохо умеет составлять резюме из нормального текста.
Я не давал ей заранее подготовленные расшифровки и не проверял её отдельно как модель для суммаризации. В эксперименте Qwen получала результат всего предыдущего пайплайна. Вместе с текстом туда могли попасть ошибки распознавания, перепутанные роли спикеров и неполный контекст.
Поэтому основной вывод эксперимента для меня оказался немного другим. Проблема локального анализа звонков начинается не с LLM.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.