В прошлой статье я рассказывал, как мы строили русскоязычный голосовой стек для китайского гуманоида Walker Tienkung TK2301 в нашей лаборатории 361.Robotics — своё распознавание речи, синтез и логику диалога. Там я обещал отдельно разобрать, откуда этот стек берёт звук: штатный микрофон робота для живой работы не годится, и мы завели внешний. Это та самая обещанная история — и она оказалась заметно длиннее, чем ожидалось.
Штатный микрофонный массив встроен в корпус, и производитель прямо в инструкции рекомендует вставать примерно в полутора метрах перед роботом лицом к нему, чтобы разговаривать. На практике это означает, что оператор должен стоять почти вплотную, иначе робот его или не услышит, или не поймёт. Даже для демонстраций это уже неудобно, не говоря о реальном взаимодействии, а вдобавок небезопасно: у робота есть подвижные части, и стоять в полутора метрах от работающей машины ростом с человека, подавая ей команды, — совсем не то, чего хотелось бы.
Решение напрашивалось само: вынести микрофон наружу, на радиоканал, чтобы оператор мог держать его в руке или на воротнике и свободно перемещаться вокруг робота. Звучит как рутинная задача — воткнул USB, написал pip install, готово. Мы тоже так думали. Реальность заняла два полных дня, и с каждым шагом всё оказывалось не так просто, как выглядело на первый взгляд.
Дальше — история о том, как «сменить источник звука» превратилось в разбор того, сколько неочевидных состояний прячется за фразой «микрофон отвалился». Будет полезно тем, кто заводит нестандартную периферию на роботах, работает с беспроводными радиосистемами или просто любит истории, где простая на бумаге задача разматывается в клубок.

С чем мы имели дело на старте
У робота три бортовых компьютера. Один (на архитектуре x86) отвечает за основные движения и служит хабом для периферии — это единственная машина, у которой есть свободные USB-порты, выведенные наружу корпуса. Второй (Jetson-плата, дальше в тексте — Jetson) занимается всем «интеллектуальным» — зрением, LLM и голосом. Третий занят отдельной задачей (захватом объектов и навигацией) и в этой истории не участвует, а вот два первых — как раз главные герои.
Голосовой диалог у нас работает через собственный пайплайн (тот самый из прошлой статьи, не вендорский). Крутится он на Jetson и на входе ждёт поток аудио-чанков — а что происходит с ними дальше, распознавание и синтез, для этой истории неважно. Важно одно: у пайплайна уже был готовый интерфейс «дай мне аудио-чанки», и заменить нужно было только источник этих чанков — со встроенного массива на внешний микрофон, не трогая ничего дальше по цепочке.
Ещё одна деталь, которая пригодится дальше по тексту: у робота есть пульт дистанционного управления с рычажками и джойстиками для базового управления движением. Один из рычажков, назовём его рычажком E, мы задействовали под собственную задачу — включение и выключение голосового режима, тем более что «из коробки» он заодно включал запрограммированные движения, что тоже нам пригодилось. Поднял рычажок — робот начинает слушать, опустил — перестаёт. Это тот самый «рычажок оператора», который будет упоминаться дальше.
Требование к замене было ровно одно: тот же интерфейс на выходе (чанки PCM-звука), чтобы всё, что дальше по пайплайну, вообще не заметило подмены источника.
Почему нельзя было просто воткнуть USB?
Первая идея была самой очевидной — открыть корпус, найти, куда физически подключён встроенный микрофонный массив, и воткнуть туда внешний. Идея не пережила первую же проверку: свободные USB-порты снаружи корпуса есть только у машины, которая отвечает за движение, а голосовой пайплайн живёт на Jetson — у него порты наружу не выведены вообще. Добраться до Jetson можно было только разобрав корпус, а на такой риск для рабочей машины мы не пошли.
Это стало первым архитектурным сдвигом: микрофон физически можно воткнуть только в компьютер с движением, а обрабатывающий его код живёт на Jetson — на разных машинах, и связать их предстояло по сети.
Само железо выбрали простое: беспроводной микрофон-петличку с USB-приёмником — компактная радиосистема, приёмник у которой определяется системой как обычная USB-звуковая карта. Просто и понятно, никакого специального SDK не нужно — думали мы.

Раз физически соединить USB нельзя, а внутренняя сеть между бортовыми компьютерами есть — решено было транслировать звук по TCP. На машине с движением, где физически сидит приёмник, подняли маленький сервис: он захватывает аудио с микрофона через стандартный Linux-инструмент записи звука и отдаёт сырой поток по TCP. На Jetson, где живёт голосовой пайплайн, — клиент, который читает этот поток и скармливает его дальше в тот же VAD/ASR-конвейер, что раньше получал звук от встроенного массива.

Первый прогон — распознавание речи совпало слово в слово с тем, что было сказано. Казалось, задача закрыта. Она была закрыта только наполовину: скоро выяснилось, что беспроводной микрофон и его приёмник могут отключаться, и вот тут начинается настоящая история.
Как понять, что микрофон «отвалился», если ошибки нет?
У обычного проводного микрофона, если его выдернуть, операционная система сразу скажет: «устройства нет». У радиосистемы всё хитрее: приёмник остаётся физически подключён и продолжает как ни в чём не бывало отдавать поток — просто когда передатчик выключен или уехал за пределы радиуса действия, в этом потоке одни цифровые нули. Никакой ошибки, никакого сигнала «потеряна связь» — просто гробовая тишина, которую система формально считает валидным аудио.
Для голосового пайплайна это означало: оператор поднимает рычажок E, чтобы робот начал слушать, а VAD честно ждёт речь в потоке нулей — и не дожидается никогда. Робот просто молчаливо «глохнет», и понять, что произошло — микрофон выключили, он разрядился или отвалилась радиосвязь — невозможно без специальной диагностики.

Стало ясно: одного лишь «транслировать звук по TCP» мало, нужна отдельная логика диагностики состояния микрофонного тракта. Решили делать её на два канала одновременно: индикатор на корпусе робота (загорается определённым цветом, когда с голосовым трактом что-то не так) — для визуальной оценки состояния, и голосовая подсказка через те же встроенные динамики, что озвучивают ответы робота, — чтобы оператор сразу понимал не просто «что-то сломалось», а что именно сломалось и как это исправить. Индикатор один на все виды проблем, а подсказки должны различаться по смыслу — значит, нужно уметь различать сами проблемы, а не просто ловить факт «звука нет». Логика должна была различать три разных состояния:
всё в порядке, и микрофон и USB-приёмник подключены и работают штатно;
USB-приёмник физически на месте, но микрофон молчит (выключен, разряжен, вне зоны действия);
сам USB-приёмник пропал из системы (кто-то выдернул его из робота).
Кейсы 2 и 3 требуют разного текста подсказки оператору — «включите микрофон» это не то же самое, что «подключите приёмник». Но снаружи, со стороны TCP-потока, оба выглядят одинаково: поток либо с нулями, либо оборвался. Как их различить?
TCP — это просто поток байт без границ сообщений. Первая идея была «воткнуть маркер-байт» в поток при потере устройства и ловить его на другой стороне. Идея не выжила первой же проверки: маркер может слиться с остатком буфера от предыдущего чтения, и что тогда получит клиент — кусок маркера пополам с кусками звука? Ненадёжно в принципе.
Решение пришло из смежной области: в профессиональных радиосистемах для концертного звука эта проблема решена десятилетия назад — при потере радиосвязи приёмник аппаратно «заглушает» (squelch) выход в цифровые нули, и приложение определяет обрыв связи по тишине, а не по отдельному сигналу. Мы повторили тот же принцип программно: сервер на машине с приёмником сам меряет громкость потока, и если несколько секунд подряд абсолютная тишина — честно закрывает TCP-соединение. Клиент видит разрыв связи и после этого отдельным HTTP-запросом спрашивает «а устройство физически на месте?» — и вот тут уже различает «молчит» от «пропал».

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

Первым делом голосовые подсказки начали зацикливаться. Робот говорил «потеряна связь с приёмником», через секунду — «голосовой режим активирован», через секунду снова «потеряна связь» — и так по кругу. Причина оказалась в мелочи: когда клиент переподключался к серверу, он оптимистично считал связь восстановленной сразу при успешном TCP-подключении — ещё до того, как реально проверил, есть ли устройство. На один короткий момент между переподключениями получалась ложная картина «всё хорошо», фоновый наблюдатель успевал это заметить и озвучить, а через долю секунды настоящая проверка возвращала всё на место. Исправили — стали проверять статус устройства сразу при подключении, а не после.
Заодно всплыл ещё один похожий баг: в самой первой версии наблюдателя было «не повторяй одну и ту же подсказку слишком часто» — защита от спама одинаковыми фразами. Но эта защита рассчитывала на то, что состояние стабилизируется хотя бы на пару секунд, а на практике клиент при проблеме переподключается каждую секунду и на миг мигает то одним состоянием, то другим — защита от спама никогда не успевала сработать и просто блокировала все подсказки вообще. Убрали её полностью — к более осмысленной логике на её замену вернёмся чуть ниже.
Следующий сценарий поймали тоже только вживую: приёмник отключили, потом снова подключили, а микрофон при этом остался выключен. Ожидаемая фраза — что-то вроде «приёмник снова на связи, но микрофон всё ещё выключен». А робот озвучивал тот же текст, что и при обычной потере связи — «потеряна связь с микрофоном». Формально верно (текущее состояние действительно «микрофон молчит»), но по смыслу вводило в заблуждение: только что стало лучше (приёмник вернулся), а фраза звучала как будто стало хуже. Пришлось завести отдельную голосовую подсказку специально для этого перехода — не «потеряна связь», а «приёмник снова на связи, но микрофон всё ещё отключён». Казалось бы мелочь, но именно такие мелочи и определяют, доверяет ли оператор голосовым подсказкам робота или воспринимает их как шум.
И зеркальная проблема к предыдущей: подключили приёмник обратно при включённом микрофоне — и робот всё равно на пару секунд объявлял «микрофон отключён», хотя тот всё время работал. Причина — гонка между физическим восстановлением USB-устройства и реальным появлением звука в потоке: операционной системе после переподключения нужно время, чтобы «осознать» устройство заново, а в этот промежуток проверка уже отвечала «устройство есть», но реального звука ещё не было. Решение — небольшое окно ожидания именно перед голосовой подсказкой (не перед самой диагностикой): если приёмник только что вернулся, не спешить с объявлением, а подождать пару секунд, пока станет окончательно ясно — пришёл реальный звук или нет — и озвучить уже устоявшийся результат, а не мгновенный снимок переходного состояния.
Общий знаменатель всех этих багов один: «технически верно» и «понятно оператору» — разные требования. Диагностика могла быть абсолютно точной в каждый момент времени и при этом складываться в бессмысленную для человека кашу из противоречивых объявлений.
Почему переподключение занимало десять секунд вместо двух?
Даже с окном ожидания задержка после физического переподключения оставалась заметно больше, чем должна быть. Начали копать глубже — и настоящий виновник оказался неожиданным. Дело было не в самом USB и не в нашем коде вообще, а в конфликте с системным звуковым сервером на машине с приёмником: при каждом переподключении устройства этот сервер пытался сам его перехватить, у него это не получалось (звуковая подсистема была занята нашим процессом захвата), он падал с ошибкой и снова пытался — и пока шла эта внутренняя борьба за устройство, наш процесс захвата не мог стабильно его открыть.
Реального «железного» времени на восстановление радиосвязи требовалось секунду-две, а из-за этой скрытой борьбы за ресурс уходило до десяти секунд. Починили одной системной настройкой: явно сказали звуковому серверу не трогать конкретно это устройство. После этого переподключение стало происходить почти мгновенно, ровно за то время, которое и требуется реальной электронике. Полезный вывод на будущее: если задержка непропорционально велика, ищите не только свой код, но и то, кто ещё в системе претендует на то же устройство.
Осталась последняя тонкость — из тех, что не мешают сегодня, но обязательно выстрелят завтра. Окно ожидания перед голосовой подсказкой было подобрано вручную как число в конфиге. Но у сервера, который слушает поток и решает «это тишина, закрываю соединение», есть свой собственный таймаут ожидания тишины — и если кто-то в будущем поменяет один из этих двух таймаутов, не поменяв второй, окно ожидания снова начнёт истекать слишком рано или слишком поздно, и все голосовые кейсы снова разъедутся.
Решили не полагаться на две независимые константы в двух разных процессах на двух разных компьютерах, а сделать сервер источником истины: он сам сообщает клиенту свой таймаут тишины через тот же статус-запрос, которым клиент и так уже пользуется, а клиент вычисляет своё окно ожидания от этого значения плюс небольшой запас. Поменяется таймаут на сервере — клиент подхватит это автоматически, без правки кода на другой машине.

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

