Облачный ассистент работает приятно ровно до момента, когда задумаешься, куда уходит каждая произнесённая фраза. Запись поднимается на внешний сервер, обрабатывается, возвращается текстом, а по дороге живёт в чужих логах. Локальная схема убирает внешний контур полностью: микрофон, распознавание, поиск команды и синтез ответа работают на железе в собственной квартире. Разбираем, из каких блоков собирается такой ассистент, какие открытые инструменты закрывают каждый блок и во сколько это обходится по ресурсам.
Что не так с облачной схемой и чем отвечает локальная
Типовой путь голоса в облачном ассистенте состоит из четырёх прыжков. Устройство ловит активационную фразу, отправляет запись на сервер распознавания, текст уходит на сервер понимания намерений, ответ возвращается синтезированной речью. Три этапа из четырёх происходят вне дома.
Побочных эффектов два. Первый касается приватности: фразы, произнесённые в квартире, физически покидают её пределы, а с ними уходит статистика о распорядке дня. Второй касается надёжности. При пропадании интернета ассистент превращается в дорогую колонку, поскольку без внешнего сервиса не распознаёт даже команду включить свет.
Локальная схема меняет оба свойства. Аудио не выходит за пределы домашней сети, а работа не зависит от доступности чужих серверов. Расплата предсказуема: вычисления ложатся на собственное железо, и его приходится подбирать осознанно.
Требования к железу уже не те, что несколько лет назад. Компактные модели распознавания работают на четырёх ядрах обычного процессора, а для поиска активационной фразы хватает одноплатного компьютера.
Четыре независимых блока, из которых собирается ассистент
Разбивка на блоки полезна тем, что каждый можно заменить отдельно, не переписывая остальное.
Вот эти блоки по порядку:
- Активационное слово слушает микрофон постоянно и ищет одну короткую фразу, задача нарочно узкая, поэтому решается крошечной моделью с минимальным потреблением;
- Распознавание речи включается после срабатывания первого блока и превращает несколько секунд аудио в строку текста, здесь уже нужна полноценная языковая модель;
- Сопоставление намерения сравнивает текст с набором шаблонов и выдаёт структурированную команду с аргументами, где есть действие, объект и значение;
- Исполнение и ответ передаёт команду в систему автоматизации, а обратно возвращает фразу, которую озвучивает локальный синтезатор.
Между блоками удобно использовать протокол Wyoming. Он придуман как раз для того, чтобы разносить распознавание, синтез и поиск активационного слова по разным машинам и подключать их к Home Assistant как отдельные сервисы. Практический смысл простой: блок распознавания можно держать на мощном сервере в шкафу, а спутник с микрофоном поставить в комнате на слабом одноплатнике.
Выбор движка распознавания между потоковым Vosk и точным Whisper
Здесь проходит главная развилка всей конструкции.
Vosk это потоковое распознавание с малыми моделями. Русская компактная модель занимает около 50 МБ на диске и требует примерно 300 МБ памяти в работе. Полноразмерная русская модель даёт лучшую точность, но весит уже около 1,8 ГБ. Ключевое свойство Vosk не в размере, а в нулевой задержке: текст появляется по мере произнесения, а не после окончания фразы.
Whisper и его оптимизированные варианты устроены иначе. Модель получает законченный фрагмент аудио и отдаёт текст целиком. Точность выше, особенно на сложной акустике и при фоновом шуме, поддержка языков шире. Расплата в задержке и в аппетите к ресурсам: обработка занимает время, сравнимое с длительностью самой фразы.
Реализаций Whisper несколько. Оригинальная тянет тяжёлые зависимости, вариант на C работает быстрее, а переработка на CTranslate2 даёт кратный прирост скорости за счёт квантования весов. Для домашнего сервера интереснее последние две.
Практический вывод простой. Для короткой команды, произнесённой в тишине, потоковой компактной модели хватает с запасом, а отклик ощущается мгновенным. Для длинной фразы при работающем телевизоре разумнее взять Whisper в лёгкой реализации и смириться с паузой в секунду.
Есть и гибрид, где активационное слово ищет компактная модель, а команду распознаёт тяжёлая. Так совмещаются мгновенный отклик и точность.
Постоянная запись всего, что происходит в комнате, противоречит самой идее локального ассистента. Поэтому микрофон слушает не распознаватель, а лёгкая модель поиска активационной фразы.
Открытый проект openWakeWord решает эту задачу на небольших одноплатных компьютерах. Он принимает поток 16 кГц, разбитый на короткие окна, и выдаёт вероятность совпадения с заданной фразой. Потребление измеряется единицами процентов одного ядра.
Принципиальный момент: обработка идёт локально, наружу не уходит ничего. Спутник, стоящий в комнате, передаёт на сервер распознавания только тот фрагмент, который следует за сработавшим активационным словом.
Собственную фразу можно натренировать. Для этого достаточно небольшого набора записей, а на выходе получается модель в формате ONNX, которую подхватывает тот же сервис. Заготовка с готовыми фразами на английском работает из коробки, но для русского обычно тренируют свою.
Развёртывание сводится к запуску контейнера:
docker run -d --name wakeword \
--restart unless-stopped \
-p 10400:10400 \
rhasspy/wyoming-openwakeword \
--preload-model 'ok_nabu' \
--uri tcp://0.0.0.0:10400
Порт 10400 используется сервисом по умолчанию, и именно его нужно указывать при подключении к системе автоматизации.
Распознавание команд и сопоставление намерений без большой языковой модели
Полноценное понимание естественного языка для управления светом избыточно. Набор реальных команд редко превышает два десятка, а значит, достаточно сопоставления с шаблонами.
Подход работает через грамматики и слоты. В конфигурации описываются допустимые фразы с местами для значений:
intents:
TurnOnLight:
- "[включи] свет (в кухне | в спальне | в гостиной)"
- "включи свет"
SetTemperature:
- "(сделай | поставь) температуру <0-30> градусов"
PlayMusic:
- "включи музыку [на кухне]"
Квадратные скобки задают необязательную часть, круглые перечисляют варианты, угловые описывают диапазон числового слота. На выходе сопоставитель возвращает имя намерения и разобранные значения, которые система автоматизации уже умеет превращать в действия.
Такая схема даёт два преимущества перед свободным диалогом. Первое: реакция предсказуема и не зависит от настроения модели. Второе: несуществующую команду ассистент просто не поймёт и честно об этом скажет, вместо того чтобы изобретать действие, которого в доме нет.
Ограничение тоже очевидно. Фразу вне шаблона ассистент не примет, и список намерений придётся пополнять вручную. Для управления техникой это приемлемая цена.
Синтез ответа голосом, который не режет слух
Обратная связь обязательна, иначе непонятно, услышал ассистент команду или нет, и локальный синтез закрывает эту задачу без обращения наружу.
Piper это нейросетевой синтезатор для слабого железа: фразу в несколько слов он генерирует быстрее, чем она произносится, и работает на одноплатном компьютере.
Качество зависит от голоса, и подбор занимает десять минут прослушивания. Модель среднего размера предпочтительнее тяжёлой: на коротких служебных фразах разница почти неразличима, а ресурсов нужно меньше.
Имя голоса задают полностью: язык, регион, имя диктора и качество модели. Сокращённое значение ru_RU синтезатор не поймёт, поскольку неизвестно, какой файл модели загружать, и контейнер завершится с ошибкой. Для русского языка есть голоса dmitri, ruslan и irina в варианте medium, то есть ru_RU-irina-medium либо ru_RU-dmitri-medium.
Запуск опять же контейнером:
docker run -d --name tts \
--restart unless-stopped \
-p 10200:10200 \
-v ~/piper-voices:/data \
rhasspy/wyoming-piper \
--voice ru_RU-irina-medium \
--uri tcp://0.0.0.0:10200
Фразы для озвучивания держат короткими: ответ вида готово воспринимается лучше развёрнутого сообщения о выполненном действии и его параметрах.
Требования к железу, приемлемая задержка и сборка стека в одном файле
Собрать всё на одной машине реально, но требования стоит разделить по блокам.
Поиск активационного слова потребляет около пяти процентов ядра и пару сотен мегабайт памяти, и работает он постоянно.
Распознавание нагружает процессор рывками, только во время произнесения команды. Компактная потоковая модель укладывается в 300 МБ памяти и отрабатывает в реальном времени на четырёх старых ядрах, а тяжёлая модель в квантованном виде требует гигабайты памяти и заметное время на обработку секунды аудио.
Синтез самый дешёвый блок. Он включается на доли секунды и почти не влияет на общее потребление.
Ориентир для четырёхъядерного процессора с восемью гигабайтами памяти: активационное слово на одноплатнике в комнате, распознавание и синтез на сервере. Отклик укладывается примерно в секунду для потоковой модели и в две-три секунды для тяжёлой.
Разница между одной и тремя секундами кажется мелочью на бумаге и раздражает на практике каждый день, поэтому движок распознавания выбирают по фактической задержке, а не по точности в абстрактных тестах.
Что должно происходить с аудиопотоком на пути к распознавателю
Формат потока диктуется моделями: моно, 16 кГц, 16-битные отсчёты, в терминах ALSA это rate 16000, формат S16_LE, один канал. Запись с микрофона выглядит так:
arecord -D plughw:0 -r 16000 -f S16_LE -c 1 -t raw
Если звуковая карта отдаёт другой формат, пересчёт делает плагин plughw, но параметры лучше сразу выставить в конфигурации карты, иначе лишнее преобразование добавит задержки.
Между микрофоном и распознавателем обязательно стоит определение голосовой активности. Без него поток режется по фиксированным окнам и команда попадает в распознаватель разорванной, а с детектором запись идёт от начала речи до паузы.
Отдельная мелочь портит половину самодельных сборок: эхо собственного динамика, когда система пытается выполнить собственный голос как команду. Лечится разнесением динамика и микрофона, а в идеале подавлением эха на уровне спутника.
Когда блоки выбраны, их удобно держать в одном описании. Тогда конструкция запускается одной командой и так же одной командой обновляется.
services:
stt:
image: rhasspy/wyoming-faster-whisper
restart: unless-stopped
command: >
--model small-int8
--language ru
--uri tcp://0.0.0.0:10300
ports:
- "127.0.0.1:10300:10300"
volumes:
- ./models:/data
tts:
image: rhasspy/wyoming-piper
restart: unless-stopped
command: >
--voice ru_RU-irina-medium
--uri tcp://0.0.0.0:10200
ports:
- "127.0.0.1:10200:10200"
volumes:
- ./voices:/data
wakeword:
image: rhasspy/wyoming-openwakeword
restart: unless-stopped
command: >
--preload-model ok_nabu
--uri tcp://0.0.0.0:10400
ports:
- "127.0.0.1:10400:10400"
Суффикс int8 означает восьмибитное квантование весов: точность почти не страдает, а памяти требуется примерно в полтора раза меньше, чем у варианта с плавающей точкой.
Порты привязаны к локальному адресу, снаружи конструкция не видна, а спутник в комнате подключается к сервисам через адрес сервера в локальной сети.
Каталоги моделей и голосов объявлены томами не случайно. Весы занимают сотни мегабайт, и скачивать их заново после каждого пересоздания контейнера расточительно.
Исполнение команд берёт на себя система автоматизации. Home Assistant подключает все три сервиса как объекты протокола Wyoming и работает с ними штатно: сопоставляет намерения, дёргает сущности света и климата, возвращает текст ответа синтезатору. Без полноценной платформы третий блок заменяется скриптом, который по имени намерения вызывает команду и отдаёт текст ответа на вход синтезатора.
Локальная схема не догонит облачную по гибкости диалога. Зато она работает без интернета, не отправляет записи голоса наружу и позволяет заменить любой из четырёх блоков, не переделывая остальные. Для двух десятков бытовых команд этого достаточно.