К нам в лабораторию приехал человекоподобный робот ростом 173 сантиметра и весом 75 килограммов — и он не понимал по-русски ни слова. Штатный голосовой стек работал на китайском, лежал на закрытой плате внутри корпуса, куда нет доступа, и заменить в нём язык было нельзя. Пришлось строить собственный: своё распознавание речи, свой синтез голоса, свою логику диалога.
Казалось бы, задача решённая — локальных голосовых ассистентов на Whisper и Silero сегодня собирают все кому не лень, и мы рассчитывали пройти этот путь по накатанной. Но одно дело колонка на полке, и совсем другое — двуногая машина, которая по команде голосом делает шаг. Половина проблем, с которыми мы столкнулись, выросла именно из этого различия: цена ошибки распознавания здесь не «ассистент не понял», а «робот сделал не то».
Эта статья — подробный разбор того, что пришлось перебрать и переделать, чтобы разговор с роботом стал живым: почему не подошёл ни один из готовых вариантов, как одна модель распознавания речи проиграла другой всухую, зачем в итоге понадобились две сразу, и почему полсекунды тишины после команды — это проблема, которую надо решать отдельно. Будет полезно тем, кто строит голосовое управление для железа, работает с ASR и TTS на русском или просто хочет посмотреть, из чего на практике состоит «робот, который вас понимает».

Меня зовут Антон Якимов, я руковожу лабораторией 361.Robotics. Мы тестируем роботов — берём новые платформы, собираем, нагружаем, гоняем неделями и ставим оценку по десятибалльной шкале. Пишем только о том, что видели и пробовали сами. Сейчас в парке четыре машины: UBTech Walker Tienkung, Unitree B2, Unitree G1 и Leju Kuavo 4Pro MAX B. С гуманоидами работаем с ноября 2025 года.
Герой этой статьи — Walker Tienkung, модель TK2301. С происхождением этой машины стоит разобраться отдельно, потому что оно объясняет некоторые вещи из того, с чем мы потом столкнулись на практике.
Робота разработал Пекинский центр инноваций гуманоидной робототехники (он же X-Humanoid) — государственно-частная структура, основанная в конце 2023 года. В его каталоге наш экземпляр называется 天工 2.0 Pro: те же 42 сустава с той же разбивкой, тот же рост 173 см, та же нетипичная составная батарея. А UBTech, под чьим брендом робот продаётся, это «инициатор и генеральный менеджер» этого центра, его крупный акционер, как следует из годового отчёта для Гонконгской биржи. Полное китайское имя продукта — 天工行者, «Tiangong Walker», где «Walker» и есть вклад UBTech.
Линейка Tiangong («Тяньгун») известна тем, что её представитель выиграл первый в мире полумарафон гуманоидных роботов в апреле 2025-го, пройдя 21 километр за 2 часа 40 минут. Наш экземпляр — не тот марафонец, а исследовательская версия: 42 сустава, до 550 TOPS вычислительной мощности на двух Jetson AGX Orin плюс x86-компьютер, батарея на три с половиной часа ходьбы.

Практический смысл понимания происхождения робота простой: документация у него двойная. Английские доки UBTech местами беднее китайских материалов центра, а исходная модель робота для расчётов лежит в открытом репозитории X-Humanoid. Когда в вендорских доках чего-то не хватает — а не хватает регулярно, — искать нужно в репозиториях Пекинского центра инноваций.
Для нас робот не игрушка для развлечения, а рабочая платформа: на ней мы проверяем, что гуманоид сегодняшнего поколения реально умеет делать за пределами вендорских демороликов.
Здесь стоит объяснить, зачем мы этим занимаемся. 361.Robotics — независимая R&D лаборатория, которая тестирует роботов и пишет о них честно: собираем, нагружаем, гоняем неделями и ставим оценку, рассказываем только о том, что видели сами. Гуманоиды перестали быть выставочной экзотикой: линейку Walker того же вендора уже ставят на конвейеры автозаводов, в логистику и дата-центры, Airbus взял промышленную версию на пробу в авиасборку, а сервисная модель работала роботом-гидом на Expo 2025 в Осаке. За этими сценариями — не «робот ради робота», а вполне конкретные задачи: провести экскурсию, поработать в цеху, встретить гостя в офисе, ответить на вопрос на стенде. Мы разбираемся, что из этого уже работает на нашей машине, а что пока остаётся красивым обещанием из презентации или видео.
И почти в каждом из этих сценариев узкое место — не ноги и не руки, а то, как с роботом вообще общаться. Гид, который понимает вопрос посетителя; ассистент в офисе, которому говоришь задачу голосом; помощник на площадке, отвечающий на реплику, — всё это упирается в живой разговор на языке пользователя. Поэтому голосовое управление — первое, за что мы взялись: без него любое взаимодействие с роботом сводится к пульту и ноутбуку, а это не то, ради чего покупают человекоподобную машину. А наш, как и большинство других моделей, ещё и говорил только по-китайски.
У вендора робота на самом деле есть не один, а два голосовых стека — и ни один из них нам не подошёл.
Первый — тот, что реально стоит и работает «из коробки». Он живёт на отдельной закрытой плате внутри корпуса, к которой нет доступа по SSH ни с одного из бортовых компьютеров, только по проприетарному сетевому протоколу. По документации официальный язык для него нигде явно не заявлен, но на практике распознавание речи и синтез голоса работают на китайском — так это видно в логах и слышно в голосовых ответах.
Второй — опциональный вендорский пакет. Он умеет работать через облачные Azure-сервисы распознавания и синтеза речи, и там русский или английский в принципе включается параметром. Но у нас на роботе этот пакет не установлен, а завязываться на облачный сервис конкретного вендора, до которого в любой момент можно потерять доступ, не хотелось.
Итого: штатное решение «как есть» — не вариант. Пришлось строить параллельный пайплайн с нуля: своё распознавание речи, свой синтез голоса, свою логику диалога.

Ниже — по шагам, какие решения на нашем пути оказались не такими простыми, как казалось на старте.
Первым делом нужно было решить, чем вообще превращать звук в текст. Казалось бы, выбор был очевиден — Whisper, открытая модель распознавания речи с приличным качеством на русском.
Вопрос был в том, какой конкретно вариант Whisper использовать: их несколько, и они по-разному ведут себя на маленьком бортовом железе. Изначально планировали faster-whisper — питоновскую обвязку поверх библиотеки CTranslate2, с INT8-квантизацией для экономии памяти. Но на бортовом Jetson-компьютере для неё не оказалось рабочей CUDA-сборки под его архитектуру процессора, то есть работать пришлось бы без GPU-ускорения. Перешли на whisper.cpp — реализацию на чистом C++, для которой CUDA-сборка под Jetson нормально компилируется.
Это был первый звоночек, на который стоило обратить больше внимания, чем я предполагал. В голове задача выглядела как «поставить готовую библиотеку и подключить микрофон» — а на деле полдня ушло на выяснение того, что для этого конкретного процессора нужной сборки просто не существует в природе. Я тогда ещё думал, что это досадная случайность. Забегая вперёд: случайностью это не было, так выглядела вся дальнейшая работа.
Дальше пошёл перебор версий модели внутри whisper.cpp. Начали с small, затем перешли на medium ради лучшего качества, а закончили на large-v3-turbo — у OpenAI это укороченный вариант большой модели: декодер урезан с 32 слоёв до 4, скорость получается на уровне medium, а качество распознавания — как у полной large-v3. Заодно ушли от идеи гонять модель как отдельный процесс на каждую фразу: это давало ощутимую задержку на инициализации GPU каждый раз. Подняли её как постоянно работающий фоновый сервис, к которому дальше просто получаем доступ по сети.
Довольно быстро всплыла проблема другого рода. Whisper — модель общего назначения, и она «придумывает» то, чего не было, когда фраза короткая, а звук неидеальный. Короткие командные слова она периодически распознавала как что-то совершенно постороннее: «беги» превращалось в английское «Begin», «встань» — в «Таня», а «помаши» — в «по машину». Команда «стой» временами просто терялась.
Для обычного диалога такое не критично — человек переспросит. Но для команд движения ошибка распознавания это фактически ошибочное действие робота. Разница между «пользователь переспросит» и «машина весом 75 кг сделала не то» довольно существенная.
Выглядит это, надо сказать, специфически. Вы стоите перед машиной с вас ростом, подвешенной на страховочных тросах, говорите ей «встань» — и видите в логе, что она услышала «Таня». Первые несколько раз это было смешно, но потом я поймал себя на мысли, что перед каждой командой непроизвольно ищу глазами кнопку аварийной остановки.
Отдельные галлюцинации Whisper приходят вообще не из речи, а из тишины. На неречевом сигнале — фоновый шум, пауза, случайный стук — модель уверенно выдаёт «Продолжение следует...». За тридцать дней в логах робота набралось 5506 таких срабатываний. Причина известная и описана в обсуждении самого Whisper: обучающие данные содержали субтитры к видео, где на участках тишины стоят титры и копирайты, и модель выучила связь «нет речи → выдать текст титров». В том же обсуждении перечислены и другие типовые артефакты русского Whisper — «Субтитры сделал…», «Редактор субтитров…».

Разница с ошибками из прошлых примеров тут принципиальная. Там модель искажала сказанное, и это ловится порогом уверенности или вторым движком. Здесь она выдаёт связный, грамматически безупречный текст на пустом месте — и никакой порог его не отличит от настоящей фразы, потому что модель в нём полностью уверена. Дальше по пайплайну эта фраза выглядит как обычная реплика оператора и обрабатывается как обычная реплика.
Первое, что приходит в голову, — отсекать такое по громкости: раз речи не было, значит, было тихо. Не работает. На входе у нас стоит не порог по амплитуде, а Silero VAD — нейросетевой детектор речи, и до Whisper фрагмент доезжает только потому, что VAD уже счёл его речью. Галлюцинация рождается не на чистой тишине, а на пограничном сигнале: шорох одежды, стук, обрывок далёкого разговора. Для детектора это похоже на речь; Whisper с ним соглашается и делает то, что обязан, — выдаёт самый вероятный текст. А поднять порог детектора нельзя: команда «иди» длится около трёхсот миллисекунд, и, задушив шорохи, мы задушим вместе с ними короткие команды — ту самую задачу, которую перед этим решали отдельным движком.
Второе, что приходит в голову, — спросить саму модель. Whisper на каждом фрагменте отдаёт служебные оценки: среднюю уверенность в распознанном тексте и отдельно вероятность того, что речи не было вовсе. Звучит как готовое решение. Проверили у себя: две секунды цифровой тишины дали вероятность «речи не было» 0,00000003 и среднюю уверенность −0,073, живая речь — ровно ноль и −0,071. Модель сочинила текст из ничего и была в нём уверена так же, как в настоящей фразе. Причём на строгом нуле — из абсолютной тишины, где сомневаться, казалось бы, положено больше всего. В этом и суть галлюцинации: это не сомнение модели, а её уверенность. Спрашивать её саму бессмысленно — она искренне не знает, что выдумывает.
Полезная информация была раньше, на входе, и мы её сами выбрасывали. Детектор речи считает вероятность на каждом кусочке в тридцать миллисекунд, мы сравнивали её с порогом и само число дальше никак не использовали. Между тем настоящая короткая команда и шорох отличаются не громкостью, а формой во времени: у команды «иди» вероятность взлетает и держится плато почти всю её длину, у шороха — колеблется у границы, то заходя за неё, то падая. Порог видит только факт превышения и обе картины считает одинаковыми. Теперь робот сохраняет профиль целиком — какая доля кусочков уверенно речевая, насколько длинный непрерывный участок — и пишет его в лог рядом с распознанным текстом. Порог по этому профилю мы пока не выставили: его надо считать на живых записях, набрав статистику, а не выбирать эмпирически. Так что галлюцинации сейчас ловит список известных артефактов: не итоговое решение, а точный фикс под конкретную фразу с её пятью тысячами срабатываний.
У самого списка есть тонкость: паттерн требует, чтобы подозрительная фраза занимала реплику целиком — от первого символа до последнего. Если ловить «продолжение следует» где угодно внутри текста, робот пропустит живой вопрос вроде «а что там дальше, продолжение следует или на этом всё?» — фильтр против выдуманного текста не должен отсекать настоящий.
Кстати, о разговорах с машиной. Довольно быстро выяснилось, что обращаться к роботу «эй, ты» неудобно — и мы назвали его Василием. Имя прижилось моментально, а вот распознаванию оно далось тяжело: «Василий» регулярно превращался в «Селись». Робот, которого зовут Селись, — примерно та же степень абсурда, на которой начинаешь понимать, что систему придётся существенно переделывать.
Здесь мы всерьёз рассматривали переход на отечественную GigaAM-v3 от Сбера: у неё по публикациям хорошие показатели в русском языке. Собрали тестовый набор фраз и прогнали его через Whisper и GigaAM-v3.
Результат получился однозначным не в пользу GigaAM-v3: из двадцати тестовых фраз Whisper оказался точнее в тринадцати случаях, GigaAM-v3 — ни в одном, остальные вничью. Причина оказалась структурной: словарь GigaAM-v3 состоял всего из 34 кириллических символов, то есть модель физически не могла написать ни одного слова латиницей. А у нас в разговоре с роботом постоянно проскакивают технические термины на английском вперемешку с русской речью.
Выглядело это так (записывали живым голосом через микрофон робота):

Последняя строка — не опечатка: слово «Python» модель не транслитерировала, а заменила на «треть». Как видно, Whisper тоже не безгрешен — «ЦУДА» и «Пайдон» тому подтверждение, — но он хотя бы иногда пишет термины правильно, а GigaAM-v3 не могла в принципе.
Справедливости ради: сама по себе GigaAM-v3 — сильная модель, и наш результат не надо читать как «она хуже». В подробных бенчмарках на русском обе модели идут почти вровень по проценту ошибок, а на процессоре без видеокарты GigaAM работает в разы быстрее Whisper. Просто наш сценарий — разговор с постоянными англицизмами — попадает ровно в её слабое место; то же самое независимо замечали и другие. Будь у нас чисто русская речь без технических терминов, выбор мог оказаться другим.
Идею отложили, а не закрыли насовсем — незадолго до начала подготовки этого поста Сбер выпустил новую версию под названием GigaAM Multilingual, обученную сразу на нескольких языках, которая должна снимать именно эту проблему словаря. Перепроверили на свежих записях голоса с теми же техническими терминами. Увы, результат почти не изменился: GigaAM Multilingual технически уже умеет писать латиницей, но всё равно предпочитает транслитерировать привычные англицизмы в кириллицу. Для нашего разговора с постоянным переключением языков внутри одной фразы Whisper пока остаётся точнее.
Раз проблема только в коротких командах — логично добавить рядом специализированный движок и пробовать его первым.
Для короткого фиксированного набора команд подключили Vosk с grammar-режимом (модель vosk-model-small-ru-0.22 — только маленькие модели такого типа вообще поддерживают ограничение словаря на лету). Ему заранее скармливается список ожидаемых фраз, и он ищет среди них ближайшее совпадение, а не пытается распознать произвольную речь.
Каждая фраза сначала идёт через Vosk. Если тот уверенно узнал одну из заданных команд — эта фраза и используется дальше, Whisper для неё вообще не вызывается. Если Vosk не узнал ничего из своего списка — фраза уходит в Whisper как обычная реплика диалога. Для команд такой предфильтр надёжнее и в разы быстрее: счёт идёт на десятки миллисекунд вместо секунды.

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

«Колечко из поклоном» — это Vosk, собравший фразу из обрывков двух фраз, которые были у него в списке. Спасает здесь порог уверенности: почти всё из этой таблицы его не проходит и спокойно уходит в Whisper, как и задумано.
Почти — но не всё. Один случай оказался серьёзнее: в вопросе, начинавшемся со слова «спроси», Vosk услышал команду «проверь себя» с уверенностью 0.78. Порог тогда стоял ниже, команда прошла как настоящая — и Василий вместо ответа на вопрос ушёл в полную самодиагностику на несколько секунд.
Ровно та ошибка, ради предотвращения которой всё это и строилось, только пришла она с неожиданной стороны: не команду перепутали с командой, а обычный вопрос — с командой. Вылечили добавлением в грамматику «якорных» фраз, на которые такие вопросы цепляются охотнее, чем на команды, и подъёмом порога с 0.6 до 0.85.
На этом задача распознавания — превратить звук в текст — закрыта: короткие команды ловит Vosk, всё остальное уходит в Whisper. Но получить текст мало. Дальше необходимо понять, что этот текст значит — просьбу сделать шаг или приглашение поболтать. Оба движка распознавания, Vosk и Whisper, тут уже ни при чём: они работали со звуком, а следующий шаг — про смысл готового текста, и решается он совсем другим инструментом.
Итак, на выходе распознавания — готовый текст фразы, неважно, дал его Vosk или Whisper. Теперь по полученному тексту нужно определить, это команда роботу («иди», «помаши», «сделай самодиагностику») или обычная реплика из диалога.
Первая версия решала это самым ленивым способом: отдавала LLM список функций и говорила «выбери нужную». Работает же — модель умная, разберётся. Модель, как выяснилось, разбиралась через раз: могла проигнорировать команду и вежливо ответить текстом, особенно если реплика распозналась неидеально. Просишь пройтись — получаешь рассуждение о том, как приятно было бы пройтись.
Здесь тоже попробовали пару вариантов. Готовую библиотеку для роутинга по смыслу пробовали подключить как есть, но она тянула за собой цепочку зависимостей, конфликтовавшую с уже установленными у нас библиотеками распознавания и синтеза речи. Совместить не вышло — решили обойтись меньшим числом компонентов и свести всё к одному прямому запросу на сходство.
Для самого сравнения смысла попробовали одну лёгкую многоязычную модель эмбеддингов (multilingual-e5-small) — и она повела себя странно: почти любые две фразы получали практически одинаковую оценку схожести (косинусная близость упиралась в 0.99 у всего подряд), роутер не мог никого отличить друг от друга. Заменили её на USER-bge-m3 — русскоязычную модель эмбеддингов, которая развела команды и обычную болтовню чисто, с заметным разрывом в показателях схожести. Важно: это замена внутри роутера, одной эмбеддинг-модели на другую, — к другим компонентам пайплайна она отношения не имеет.
Итоговая схема: не заставлять LLM решать каждый раз, а поставить перед ней отдельный маленький сервис — роутер команд на эмбеддингах. Он сравнивает смысл (не текст дословно, а именно смысл) входящей фразы с заранее заданным набором эталонных команд и, если сходство выше порога, направляет прямо в исполнение, минуя LLM целиком.
Это сразу решило две проблемы. Команды стали исполняться моментально, без ожидания ответа большой модели. И устойчиво к неточностям распознавания — роутер по смыслу узнаёт «давай, шагай» как то же самое, что и эталонное «иди», даже если дословно текст немного другой. Всё, что не попало ни в одну из заданных команд, идёт дальше в LLM как обычная реплика диалога.
Похожим путём пошли и коллеги из Wiren Board — они собрали локального ассистента, где эмбеддинги тоже разбирают команды по смыслу. Разница в том, что у них эмбеддинги заменяют понимание речи целиком — большой модели в контуре нет вообще. У нас же роутер стоит именно перед LLM и снимает с неё только то, что она делает ненадёжно, оставляя ей весь свободный диалог.

При этом прямой вызов функций самой моделью — то, с чего мы начинали, — из системы никуда не убрали, он остался запасным путём. Тут важно уточнить, что именно оказалось ненадёжным. Не сам по себе tool-calling, а его принудительный режим: когда модель обязывали на конкретной фразе выбрать функцию из списка, она всё равно нередко игнорировала вызов команды и отвечала текстом — ровно та проблема, ради которой затевался роутер. А вот обычный tool-calling, где вызвать функцию — лишь один из вариантов на усмотрение модели, работает вполне прилично. Роутер просто снял с него нагрузку по частым командам, где цена ошибки высока, и оставил ему всё остальное: команды, которых нет в его списке, случаи, когда он промахнулся, и действия по ходу свободного разговора — когда робот сам решает, скажем, кивнуть или показать жест в ответ на реплику, а не по прямому приказу. Поэтому роутер — это быстрый обход для частых команд, а не единственный путь к моторам.
С озвучкой ответов тоже не обошлось без сюрпризов. Начинали с открытого движка Piper — приличное качество, лёгкий, отлично работает на процессоре без видеокарты.
Через какое-то время экспериментов устроили честное сравнение нескольких вариантов синтеза на одних и тех же фразах, включая ещё один открытый вариант посерьёзнее (XTTS) и Silero. Победил Silero: по субъективному ощущению он звучит естественнее Piper на длинных фразах и лучше держит интонацию, а прежний голос Piper на этом фоне стал казаться «механистичным».
XTTS проиграл не по качеству, а по скорости, и с разгромным счётом. У него есть подкупающая особенность — полсотни готовых голосов плюс возможность клонировать любой по короткому образцу. Но на тестовой фразе он думал 88,8 секунды. Удобная метрика здесь — во сколько раз синтез дольше, чем звучит готовый результат: у XTTS вышло больше восьми, то есть на каждую секунду речи он тратил восемь секунд работы. Приговор, спорить не о чем.
Спорить, как выяснилось, было о чём. Когда я вернулся к этим замерам, чтобы аккуратно выписать цифры, меня смутила именно их величина: восьмикратное отставание — это слишком много даже для тяжёлой модели. Полез проверять, чем вообще считался тот тест, и нашёл. Каждый движок я тестировал в отдельном изолированном окружении, чтобы библиотеки не конфликтовали между собой, — и при установке Coqui молча притащил в это окружение собственную сборку torchaudio, из которой поддержка видеокарты просто вырезана. Она перекрыла системную, с GPU. То есть весь замер XTTS честно крутился на CPU, пока рядом простаивала видеокарта Jetson.
Обиднее всего, что окружение было заведено с флагом «использовать и системные библиотеки тоже» — я был уверен, что подстраховался. Не помогает: если тот же пакет установлен локально, побеждает всегда локальный.
Пришлось повторить замеры начисто на видеокарте. XTTS ускорился в семь с половиной раз — и всё равно остался примерно в паритете: секунда работы на секунду звучания, плюс-минус. Silero в тех же условиях выдавал фразу за 99 миллисекунд, в полсотни раз быстрее её собственной длительности. Разрыв между движками так и остался почти стократным. Решение о выборе Silero не изменилось, а вот обоснование, которым я этот отказ объяснял, было некорректным, и это неприятно.
Заодно нашлась деталь, которая почти спасла XTTS: у него есть режим выдачи звука порциями, когда воспроизведение начинается, не дожидаясь конца синтеза. Задержка до первого звука падала с 9,3 секунды до 0,65 — выглядит как победа. Но она мнимая: раз синтез идёт медленнее, чем звучит результат, плеер воспроизводит готовые куски быстрее, чем модель успевает генерировать новые, и на длинной фразе речь неизбежно прервётся где-нибудь в середине. Красиво начать и захлебнуться в середине ответа — не самый лучший пользовательский опыт.
Довершили картину не в пользу XTTS и особенности помельче. Первая: XTTS для русского вообще не понимает ударений — ни в каком виде: разметка, которую Silero принимает как подсказку, тут просто уезжает в модель мусорными символами, а без ударений русская речь зачастую звучит неправильно, особенно на коротких фразах, когда модели недостаточно контекста, чтобы правильно поставить ударение. Вторая: есть жёсткий лимит около 180 символов на фразу, причём лишнее обрезается молча. И модель склонна «дофантазировать» хвост фразы — сам мейнтейнер называет это неустранимым свойством архитектуры.

Переезд на Silero неожиданно вскрыл целый букет мелких проблем, которые до этого были незаметны — семь разных багов, каждый со своей отдельной причиной.
Часть из них сейчас уже вызывает улыбку. Например, синтезатор молча проглатывал целые предложения без единой ошибки в логах. Разбирались долго: причиной оказалась внутренняя нестыковка в модуле расстановки ударений (без верно поставленного ударения русская речь звучит неестественно — «зАмок» вместо «замОк»), которая кидала исключение при инициализации, а код перед этим тихо это исключение проглатывал и просто ничего не озвучивал. Убрали тихое проглатывание ошибок — сразу стало видно, что чинить.
Но в тот момент, впрочем, было не смешно совсем. Молчащий робот при полностью чистых логах — худший тип неисправности из всех возможных: чинить нечего, потому что система уверена, что у неё всё хорошо. Я успел перепроверить громкость, звуковую карту, права доступа к устройству и даже физически подёргать провод динамика, прежде чем догадался, что проблема совсем в другом месте и текст до синтезатора попросту не доезжает.
Отдельно ловили баг, когда LED-индикатор на роботе на секунды дольше положенного держался в состоянии «говорю» уже после того, как звук закончился, — гонка между двумя параллельными потоками, читающими один и тот же вывод. Ещё хвост последней фразы иногда обрывался на середине слова, потому что момент «можно закрывать поток» считался от времени постановки в очередь, а не от реального окончания воспроизведения. Если в очереди накопилось несколько фраз подряд, расчёт съезжал и обрезал последнюю фразу раньше времени.
Отдельно всплыла забавная проблема: числа в ответах пропадали. Спрашиваешь у Василия год выпуска чего-нибудь, он бодро отвечает — и в этом ответе на месте года просто ничего нет. Оказалось, синтезатор ожидает числа словами, а не цифрами, и всё, что записано цифрами, просто игнорирует. Логично, если задуматься; но задуматься об этом заранее почему-то мы не догадались.
Вылечили шагом нормализации текста перед озвучкой (через открытую библиотеку num2words), который разворачивает «2024» в «две тысячи двадцать четыре» в нужном падеже. Заодно попросили саму модель диалога по возможности писать числа словами — на всякий случай.
Отдельная небольшая победа нашлась там, где не ждали: у Silero из коробки есть возможность считать не только на процессоре, но и на видеокарте. Перевод на GPU дал ощутимое ускорение — самая первая, «холодная» фраза в сессии стала синтезироваться почти в пять раз быстрее, а прогретые повторные фразы — почти вдвое быстрее прежнего.
Но и тут не всё сработало. Была идея включить дополнительный режим автотюнинга видеокарты под конкретную задачу — на практике он вместо ускорения сделал прогретый синтез медленнее в несколько раз. Видимо, из-за того, что разная длина каждой новой фразы заставляла эту настройку пересчитываться заново. От неё отказались.

Хорошо, допустим, каждый отдельный шаг пайплайна работает правильно. Остаётся ещё одна задача — не точность текста, а «ощущение» обычного разговора.
Вся цепочка (распознать → обработать → подумать → ответить → озвучить) занимает не доли секунды, а несколько секунд — особенно если LLM по дороге лезет в интернет. Человек, который только что договорил фразу, в тишине после неё за пару секунд уже начинает сомневаться, что его вообще услышали.
С колонкой на столе это было бы просто неприятно. С двуногой машиной ростом с человека — это ещё и вопрос доверия: оператор, сказавший «стой» и не услышавший никакой реакции, начинает повторять команду громче и тянуться к кнопке аварийной остановки. Робот должен подтверждать, что услышал, раньше, чем успеет начать обрабатывать вопрос или выполнять движение.
Знаю это по себе. Пока пауза не была вылечена, я ловил себя на том, что разговариваю с Василием как с плохо работающим телефоном: сказал команду, подождал, повторил громче и отчётливее, потом ещё раз — а он в этот момент честно её выполнял, просто молча. В паре случаев я успевал повторить команду трижды, и робот получал три одинаковых запроса подряд. Ничего страшного не происходило, но ощущение «он меня игнорирует» никуда не девалось. Тишина, оказывается, воспринимается не как «идёт обработка», а как «сломалось».
Решение — короткие фразы-заполнители, которые робот произносит почти сразу после того, как оператор закончил говорить, ещё до того, как распознавание речи вообще началось, не то что ответ LLM. Общие фразы («момент», «секундочку», «сейчас») заранее синтезированы и лежат готовыми файлами на диске — их не нужно на лету озвучивать, только включить воспроизведение. За счёт этого ощущаемая пауза между концом фразы человека и первой реакцией робота упала с нескольких секунд до долей секунды.
Отдельно завели набор более специфичных фраз-заполнителей под конкретные действия. Если робот собирается искать что-то в интернете, честнее сказать «секунду, ищу», а не нейтральное «минутку». Если запускается самодиагностика, которая реально может занять несколько секунд, — отдельная фраза под это, а не молчание, которое легко спутать с зависанием.
Мелкая деталь, которая один раз аукнулась: фраза-заполнитель под самодиагностику по формулировке дублировала то, что робот и так скажет по её окончании. Звучало так, будто Василий дважды докладывает одно и то же. Переделали.

Ещё один источник задержки был там, где не ожидали — не в самом распознавании или синтезе, а в том, как эти куски были состыкованы друг с другом.
Ответ от LLM формируется не единым куском, а по предложениям, и озвучивался он тоже по предложениям. Но по ошибке в логике код ждал, пока будет готово не первое предложение, а уже второе, прежде чем начинал проигрывать первое. На фразах после физических действий робота — например, после жеста или движения — это ощущалось как заметный лишний секундный лаг перед тем, как Василий вообще начинал говорить.
Злого умысла не было: просто сигнал «следующее предложение началось» использовался как подтверждение, что предыдущее точно готово, вместо того чтобы полагаться на готовность самого первого предложения напрямую.
И отдельная, чуть более «железная» проблема — обрезание самого первого звука в начале почти каждой фразы, включая фразы-заполнители. Усилитель динамика робота экономит энергию и слегка «засыпает» при долгой тишине, а когда его резко будят звуком — самое начало этого звука частично теряется, пока он просыпается. Лечится это не программной магией, а хитростью на уровне самого звукового сигнала: перед каждой фразой в поток добавляется короткий, неслышимый человеку низкочастотный сигнал, который гарантированно будит усилитель заранее, ещё до того, как начнёт звучать сама речь.
На софт я грешил долго и упорно. Подозревал буферизацию, драйвер, формат сэмплов, размер аудиобуфера — всё, что обычно бывает виновато, когда звук приходит не целиком. Развязка наступила, когда я заметил закономерность: если фразы идут одна за другой, обрезается только первая, а если робот помолчал минуту — снова обрезается. Это уже не похоже на баг в коде, это похоже на что-то, что «остывает». Дальше оставалось сообразить, что остывать в этой цепочке может только усилитель. Полезный вывод на будущее: если поведение зависит от того, сколько система простояла без дела, ищите причину не в софте.
Отдельная история — откуда вообще берётся звук. Штатный микрофонный массив робота живёт на той самой закрытой плате (на предыдущем фото — горизонтальная полоска над LED-индикатором), и качество на дистанции больше пары метров оставляет желать лучшего. Для демонстраций и работы в шумном помещении мы добавили внешний беспроводной микрофон: петличка BOYA плюс USB-приёмник, который втыкается прямо в порт USB 3.0 на спине робота.


Про то, как этот микрофон заводился и какие грабли там были, расскажу отдельно — там своя история, от стабильного определения имени приёмника микрофона в системе до детектора «передатчик уснул».
Голосовой диалог — это не только «услышал-ответил». Мы постепенно добавляли модели инструменты (в терминологии агентных LLM это называется tools) — отдельные функции, которые модель может вызвать по ходу разговора.
Сейчас это поиск в интернете для вопросов не из её знаний, запоминание фактов между сессиями («запомни, что…») и их последующее припоминание, и, конечно, все физические действия робота: жесты, движение, самодиагностика. Тот самый роутер команд из предыдущего раздела в итоге тоже оформлен как набор инструментов, просто срабатывающих без похода в саму модель.
Каждый новый инструмент — это не только код исполнения, но и то, как модель понимает, когда его стоит вызвать, а когда не стоит. Это отдельная, менее заметная со стороны работа, никак не связанная с «чистым» распознаванием и синтезом.
Если свести всю работу к трём главным улучшениям, получится так:

Отдельно стоит сказать про то, что движок «мозга» стал сменным — но об этом ниже.
Сначала важная оговорка: голосом мы занялись далеко не сразу. Робот приехал в середине апреля, и первые недели ушли совсем на другое — поднять сеть, разобраться в бортовых компьютерах и ROS-стеке, научиться безопасно включать и выключать машину, понять, что вообще умеет вендорский софт с учётом привычно скудной вендорской документации. К архитектуре голосового диалога подступились только в начале мая, почти через месяц после распаковки. Голос — не то, с чего начинают знакомство с гуманоидом.
Дальше работа шла два месяца, но не сплошным потоком: параллельно хватало других задач, и голосовой пайплайн реализовывался поэтапно. Если сложить только те дни, когда я реально занимался именно им, выходит примерно месяц чистого времени — около восьмидесяти пяти коммитов, распределённых по двум с половиной десяткам рабочих дней.
Самое любопытное в этой арифметике — распределение. Первый черновик пайплайна (микрофон, распознавание, синтез, ответ) заработал на третий день. Весь остальной месяц ушёл на то, чтобы он заработал прилично.
Что съело время на самом деле: каждый четвёртый коммит — это чистое исправление ошибки, а не новая функциональность. Больше всего досталось звуковому тракту — синтез, воспроизведение, работа с аудиоустройством. Распознавание, вопреки ожиданиям, отняло вдвое меньше усилий: там достаточно было выбрать правильный движок и не мешать ему.
Оглядываясь назад, я вижу закономерность: время уходило не на то, что казалось сложным на старте. Подобрать модель распознавания и настроить синтез — задачи с понятным решением, они делаются за считаные дни. А вот заставить всё это вести себя прилично в живом разговоре — не глотать слоги, не молчать в паузах, не путать вопрос с командой — оказалось долгой историей без единого большого решения.

Честный список того, что в нашем изначальном плане ещё не закрыто.
Робот слышит сам себя. Полноценного эхоподавления (AEC) у нас нет и не будет: микрофон и динамик находятся на разных, никак не синхронизированных между собой устройствах, а классический AEC требует общего тракта. Обходимся мёртвой зоной — не слушаем микрофон, пока говорим сами, плюс небольшой запас после — и сверкой распознанного текста с тем, что робот только что произнёс. Второе оказалось важнее: задержка эха плавает от двадцати миллисекунд до двух третей секунды, и одной временной зоной её не покрыть. Сравнивать при этом приходится не наборы слов, а их последовательности: в диалоге «Меня зовут Василий» и «Меня зовут Антон» совпадают на две трети, и фильтр по пересечению слов пропускал ответ оператора вместе с эхом.
Wake-word и естественное прерывание. Обращение по имени работает: робот ждёт «Василий» и включается только после него. А вот остановиться на середине фразы, если его перебили, он пока не умеет — нужно слушать микрофон во время собственной речи, а это ровно то, от чего защищает мёртвая зона. Развязать одно от другого — отдельная задача.
Главное, что я вынес из этой работы: в робототехнике не бывает «просто прикрутить готовое». Каждый компонент, который в мире веб-разработки ставится одной командой, здесь упирается либо в архитектуру процессора, либо в закрытую вендорскую плату, либо в засыпающий усилитель. Начиная заново, я бы закладывал на интеграцию готовых решений не меньше времени, чем на собственный код, — а не наоборот, как мне казалось на старте.
Второе наблюдение, менее очевидное. Я думал, что делаю техническую систему, а по факту половину времени занимался тем, что в разговоре называется «манерами»: чтобы Василий не молчал в паузах, не перебивал сам себя, не глотал первый слог и не отвечал на вопрос запуском самодиагностики. Технически всё работало и до этого. Но разговор близкий к «живому» получился только после того, как мы занялись именно этим.
Пайплайн сложился не за один подход, как задумывалось на старте, а постепенно, шаг за шагом, по мере того как всплывали конкретные проблемы: сначала базовое распознавание и синтез, затем гибридный ASR под короткие команды, затем роутер смысла поверх LLM, затем доводка синтеза, затем борьба за живой темп разговора.
На каждом шаге было своё отдельное «не взлетело с первого раза» — в этом, собственно, и дух истории. Было много последовательных маленьких решений, каждое из которых на своём этапе казалось «ну это же должно просто заработать».
Что здесь принципиально важно: весь пайплайн построен так, что каждый компонент движка «мозга» — то, что превращает распознанный текст в ответ и решение, — можно менять, не затрагивая всё остальное. Распознавание речи, роутер команд, синтез голоса работают одинаково независимо от того, какая именно модель формулирует ответы. Так и было задумано с самого начала: не привязываться к одному конкретному поставщику библиотеки или одной конкретной модели.
Со стороны сейчас это выглядит буднично: человек говорит «иди», робот через долю секунды отвечает «момент» — и машина весом 75 кг делает шаг. Но за этой долей секунды — гибридный ASR, чтобы «иди» не превратилось в что-нибудь постороннее; роутер, чтобы не ждать большую модель; заранее синтезированная фраза-заполнитель, чтобы не было тишины; и неслышимый низкочастотный сигнал перед ней, чтобы усилитель успел проснуться и не съел первый слог.
По статье эти решения разбросаны по отдельным разделам — а работают они как единый конвейер с несколькими развилками. Вот он целиком, от звука до ответа:

Голос — только одна из историй, которые накопились за время работы с этой машиной. Отдельного рассказа заслуживают внешний микрофон и то, как он заводился; попытки заставить робота ходить по программной команде, минуя пульт; и самодиагностика, которая упорно зависала на правой руке. Если тема окажется интересной — продолжим.
Буду рад вопросам в комментариях и отдельно интересно мнение тех, кто решал похожую задачу, — какой стек распознавания вы бы выбрали для сценария с постоянным переключением между русским и английским внутри одной фразы? Мы остановились на связке из двух движков, но я не уверен, что это единственный разумный путь.
Сейчас у нас в лаборатории работают четыре платформы: гуманоиды Unitree G1 EDU+, Leju Kuavo 4Pro и UBTech Walker Tienkung — Embodied Intelligence, а также промышленный квадрупед Unitree B2. Мы активно экспериментируем с ними и обучаем роботов сценариям реального применения и решению бизнес-задач. О других кейсах расскажем в будущих статьях на Хабре. А пока мы их пишем, читайте наш Telegram-канал 361.Robotics о робототехнике, воплощенном ИИ и новой экономике вокруг них.
Если вы хотите приобрести робота для своей компании или личного использования, мы можем помочь с поставкой гуманоидов, квадрупедов и другой робототехники из Китая, а также адаптировать оборудование под задачи заказчика.
Мы открыты к обмену опытом, сотрудничеству, совместной работе над интересными проектами и просто знакомству с увлеченными людьми. Хотите увидеть современных роботов воочию — напишите нам и приезжайте на экскурсию в лабораторию 361.Robotics в Москву (м. Павелецкая).