Дом с голосовым управлением: мой опыт реализации

Эта статья для всех, кого интересует интеграция голосового интерфейса управления разнообразными устройствами в собственный дом.

Дальнейшее повествование основано на моем личном практическом опыте. Любопытный читатель сможет найти для себя множество увлекательных технических деталей, разбавленных актами эпизодической экспрессии. Статья будет интересна как начинающим, так и профессионалам, которые давно хотели проникнуться темой голосового управления.

Пролог

Тема умных домов, несомненно, на хайпе. Тем не менее для нашей страны это, к сожалению, пока еще темный лес. И дело тут вовсе не в инженерии как таковой. Дело в конечном потребителе и спросе на все эти умные «приблуды».

Существует множество факторов, которые в какой-то степени могут отталкивать наше население от того, чтобы впустить технологии в свой дом. Здесь сказывается и цена вопроса: далеко не каждый гражданин нашей страны может позволить себе покупку какого-либо дорогостоящего smart-устройства. Если же рассматривать комплексную интеграцию подобного рода решений в свою обитель, то тут важно осознавать, что это повлечет за собой то, что мы привыкли называть капитальным ремонтом, поскольку вам, вероятнее всего, придется менять и проводку, и трубы, и полы, что в итоге потянет за собой и все остальное.

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

Лично я начал интересоваться IoT-тематикой уже после того, как был сделан капитальный ремонт. Соответственно, я не имел возможности заранее спроектировать и спланировать интеграцию всех этих умных штуковин в свою квартиру. Когда же появились знания, желание и возможности, основным критерием подобной интеграции для меня послужила минимизация расходов и деструктивной активности по отношению к своей обители. С технологической точки зрения я фокусировался на нестандартных решениях, направленных прежде всего на UX. Так и зародилась тема голосового управления собственным домом.

То, что последует далее, — мой личный опыт. Он может отличаться от вашего. Тем не менее я уверен, что каждый из читателей сможет вынести для себя что-то новое, интересное и полезное.

Немного о себе. Я работаю в компании Waverley Software на позиции Technical QA Manager. Несмотря на свой title, я занимаюсь не только вопросами обеспечения качества, вовлечен еще и в разработку, DevOps-практики, а также в улучшение процессов в компании. Я член программного комитета и постоянный спикер крупнейших украинских конференций по обеспечению качества: Selenium Camp и QA Fest. Консультирую, менторю, провожу тренинги. Мои контакты можно найти в профиле. Так что по любым вопросам — you’re welcome.

Проблематика

Когда и зачем нам может понадобиться голосовое управление? Я приведу буквально несколько примеров, дабы ввести вас в контекст.

Ситуация номер один: представьте, что вы захотели каких-то вкусняшек. Вы направляетесь на кухню, включаете свет, набираете печенек/конфет/[ваш вариант], наливаете чай и направляетесь к выходу по своим делам.

Дойдя до двери, вы понимаете, что надо выключить свет. Нас ведь еще с детства приучили к экономии электроэнергии, не так ли? Но обе руки-то заняты! Казалось бы, решение очень простое: поставить все на стол (освободив руки), выключить свет, снова взять чашку с вкусняшками и отправиться по своим делам. Раз плюнуть! Но...

Человек-то — существо ленивое. И в таких ситуациях, как правило, наш мозг начинает проводить некие оптимизации с целью минимизации общего количества действий, которые нужно осуществить для решения подобного рода задач. И что в итоге оказывается наиболее простым вариантом выключения света, на наш взгляд? Конечно же, использование любых свободных частей тела: локтя, колена и т. п. — у кого на что хватит гибкости и растяжки.

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

Ситуация номер два: вы очень ждали какое-то событие и хотели посмотреть его по телевизору. К примеру, финал Лиги чемпионов по футболу или «Україна має талант». Вы набираете попкорн, пивасик и усаживаетесь поудобнее в кресло за минуту до начала.

Оглядываетесь вокруг, а пульта-то нигде нет! И ладно, если он окажется где-то неподалеку, спрятанный за подушкой. Но в таких ситуациях, как правило, отлично работает закон Мерфи: пульт оказывается в совершенно непредсказуемом месте. Злость, разочарование, желание уничтожать — те чувства, которые вы наверняка испытаете в процессе поиска.

Ситуация номер три: вы пришли уставшие с работы, еле добрели до спальни, закинули телефон куда-нибудь подальше (чтобы утром хоть как-то проснуться по будильнику), завалились в кровать, и тут к вам приходит осознание того, что вы забыли выключить свет в коридоре.

Я уверен, что каждый из читателей, покопавшись у себя в голове, сможет найти с десяток аналогичных примеров из жизни, которые работают лично для вас и периодически доставляют вам неудобства.

Было время, когда я много думал над тем, как можно минимизировать количество таких ситуаций в жизни. Чтобы понять, откуда растут ноги у всех этих неудобств, давайте попробуем проанализировать интерфейсы, с которыми мы ежедневно взаимодействуем.

Еще с детства нас приучили, что для управления телевизором нужно воспользоваться пультом дистанционного управления. Конечно, не у всех было такое счастливое детство. Лично я застал период, когда каналы надо было накручивать на самом телевизоре. Но тем не менее мы будем рассматривать более современный период.

Что еще? Для манипуляций со светом мы привыкли к обычным кнопочным выключателям. Более модный и молодежный инструмент управления умными устройствами — мобильный телефон и т. д.

Теперь давайте подумаем: а что объединяет все эти интерфейсы в контексте нашего с ними взаимодействия?

Это наши руки! Соответственно, практически всегда, когда они будут заняты, либо интересующий нас объект будет находиться вне поля нашего зрения/досягаемости, у нас это будет вызывать определенные неудобства, а порой и негативные эмоции (исходя из вышеописанных примеров).

Так вот, главный вопрос теперь заключается в том, можно ли как-то упростить себе жизнь при попадании в подобные ситуации.

Конечно, можно. И как вы уже, наверное, догадались, далее речь пойдет о совершенно нетипичном для нас интерфейсе взаимодействия с окружающими нас устройствами — голосовом. Сразу хотелось бы сделать небольшую оговорку: мы будем рассматривать его не как замену существующим, а именно как дополнение. Тут важно осознавать, что голосовой интерфейс — это не панацея. Помимо вышеописанных тактильных примеров, можно придумать аналогичные, когда и голос будет доставлять неудобства. Допустим, вы заработались ночью и направляетесь в спальню. Все уже спят. Вам нужно установить будильник. Не будете же вы делать это голосом! Скорее всего, вы воспользуетесь мобильным телефоном.

Таким образом, дальнейшее повествование я буду основывать на концепции улучшения UX за счет расширения списка доступных конечным пользователям (коими мы с вами и являемся) интерфейсов, чтобы дать им свободу выбора в зависимости от ситуации, в которой они окажутся.

Низкоуровневое управление устройствами

Внутренняя коммуникация

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

Последующее изложение будет основано на упомянутом в самом начале критерии — минимизации потенциальных расходов. В центральной части приведенного выше изображения можно лицезреть бюджетную плату NodeMCU V3 на базе чипа ESP8266, представляющего собой Wi-Fi-модуль. В буквальном смысле для нас это означает, что эта плата при желании может стать полноценным «жителем» нашей локальной подсети. Зачем это может нам понадобиться, мы узнаем чуть позже. А пока обратим внимание на то, что изображено вокруг. Это различные сенсоры и приемопередатчики, которые при подключении к нашей плате позволят ей отправлять и принимать какую-либо информацию об окружающем нас мире.

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

Наверняка, заходя в магазин светильников, вы обращали внимание на то, что некоторые люстры продаются в комплекте с подобного рода пультами:

С помощью такого пульта мы можем включить/выключить свет. По сути, он содержит в себе радиопередатчик, работающий на частоте 433 МГц. Соответственно, в люстре у нас присутствует радиоприемник, умеющий интерпретировать сигналы, отправляющиеся с пульта при нажатии на кнопки A и B.

С точки зрения удобства использования такой пульт не представляет особой ценности. Но чисто технически, обладая знаниями о том, какую именно информацию он отправляет на люстру, мы можем без особого труда проэмулировать его работу посредством радиопередатчика, подключенного к NodeMCU.

То же касается и других устройств (например, светодиодных лент), использующих радиочастоты для внешней коммуникации.

Еще одним хорошим примером могут послужить пульты от кондиционера/телевизора. Но тут уже для коммуникации используется инфракрасный сигнал. Соответственно, для эмуляции работы таких пультов нам понадобится инфракрасный передатчик.

Такая эмуляция позволит нам программно управлять теми устройствами, к которым мы так привыкли, даже если в них нет никаких smart-функций.

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

Внешняя коммуникация

Когда и зачем нам может понадобиться внешняя коммуникация с микроконтроллерами?

Современные методы управления умными устройствами зачастую предполагают их доступность из любой точки мира. Если следовать подобной тенденции, то нам нужно обеспечить доступ к нашим микроконтроллерам извне. Но тут у нас вырисовывается некая развилка, предполагающая выбор: ходить ли на устройства напрямую или через какой-то коммуникационный узел? Здесь же следует задать и другой вопрос: каким способом будет осуществляться взаимодействие?

У себя я организовывал следующую схему. Внешние запросы прилетают посредством REST на локально поднятый веб-сервер по защищенному соединению. Для этого я покупал доменное имя и SSL-сертификат. При этом сервер, проанализировав запрос, рассылает сообщения микроконтроллерам в локальной подсети по MQTT-протоколу. Этот вариант показался мне самым простым и относительно надежным с учетом того, что все это делалось на энтузиазме в свободное время. При коммерческой разработке безопасности, конечно же, следует уделить намного больше внимания. Но рассматриваемый случай совсем не относится к тем, когда следует параноить.

Железо сервера

Чтобы не держать ресурсоемкий PC 24/7 для решения таких простых задач, в качестве железа был выбран Raspberry Pi 3 Model B+ (к слову, 4-й версии тогда еще не существовало). Я брал его в комплекте с корпусом, дополнительными радиаторами и хорошим кулером. На кулере рекомендую не экономить, так как китайские варианты выдают просто космический уровень шума. Чувствуешь себя как на аэродроме. После нескольких часов сидения рядом с таким устройством начинает болеть голова. Я долго искал варианты, оптимальные с точки зрения уровня шума. В итоге о своем выборе ни капельки не пожалел. Этот setup стоит у меня в спальне и совершенно не мешает ни работе, ни сну.

Софт, необходимый для разработки

При любых раскладах для реализации конечной цели нам понадобится умение программировать. Наш конкретный случай предполагает базовые знания C/C++. Но в целом сложность кода не настолько высокая, чтобы испугать разработчика с опытом в каком-либо другом популярном ныне языке программирования.

Если говорить о среде разработки, лично я попробовал следующие варианты: Arduino, Atom, VS Code и CLion. Первые два я отнес бы к категории либо «для начинающих», либо «набросать по-быстрому». Если же вы разработчик с опытом, ценящий скорость и качество разработки, то рекомендую обратить внимание на последние два варианта. VS Code — бесплатный, CLion — платный.

В пару ко всем этим IDE (кроме Arduino) нам потребуется еще и плагин PlatformIO, существенно ускоряющий процесс разработки. И вот тут появляется очень большая разница между VSCode и CLion. В первом варианте плагин мощный и удобный, во втором — неожиданно убогий и баговый. Если сравнивать исключительно две эти IDE, то для меня CLion выглядит более функциональным (возможно, из-за того, что я уже привык к другим продуктам JetBrains). Поэтому тут все очень индивидуально, it’s up to you, на чем акцентировать внимание — на качестве плагина или на IDE.

По библиотекам все сложно. Их много, и все они платформозависимые. Мне лично было очень непривычно и жутко неудобно переиспользовать те скетчи (runnable code snippets), которыми кишит интернет. Потому я написал парочку оберток, которые значительно ускоряют прототипирование: arduino-network и arduino-sensors-wrappers. Они могут пригодиться вам для quick start.

Практический пример

Чуть ранее я затронул тему управления обычными устройствами. Теперь пришло время разобрать эту часть более подробно на примере простой LED-лампочки — рядового «жителя» любой люстры.

Мы можем управлять ею только с помощью выключателя. Но нас ведь интересует именно программное управление, не так ли? Какие у нас есть для этого опции?

Первый вариант, который сейчас активно появляется на нашем рынке, — это Sonoff. Выглядит неплохо, но я даже не стал смотреть в его сторону ввиду того, что ему нужен прямой подвод к фазе. А у меня монтажные коробки расположены таким образом, что ради подключения этого выключателя пришлось бы рвать обои и долбить стены. Сразу вспоминаем второй критерий (деструктивная активность по отношению к своей квартире).

Из недорогих альтернативных вариантов начал смотреть на китайские опции вроде такого выключателя. У них часто бывают скидки, что дает возможность покупки чуть ли не вдвое дешевле Sonoff. Из плюсов: работает от батарейки CR2032, легко клеится на стену без каких-либо разрушений, содержит встроенный радиопередатчик 433 МГц, возможна опция покупки вместе с радиореле (об этом чуть дальше). Если же вам не важна эстетика, то можно взять и более дешевый кнопочный вариант с аналогичным функционалом.

Итак, допустим, мы взяли себе такой радиовыключатель.

Как теперь связать его с нашей LED-лампочкой? Вспоминаем ранее рассмотренный радиопульт, который продается в комплекте с некоторыми люстрами. Я уже упоминал о том, что он взаимодействует с неким приемником. Соответственно, если в нашем выключателе есть радиопередатчик, нам не хватает только приемника. А что из себя представляет приемник в случае обычной лампочки? Это радиореле, которое может быть спрятано где-то на подходе (к примеру, в полом плафоне). По ссылке выше есть опция покупки выключателя вместе с радиореле. Но, несмотря на его весьма неплохое production-ready-состояние, я бы обратил внимание на какие-то альтернативные варианты. Вот, к примеру, такой — за $2-4 из Китая.

Огромным преимуществом этого варианта является то, что у него, в отличие от предшественника, есть опция гибкого обучения в нескольких режимах. Подобного рода реле приходят к нам с чистой памятью, и мы сами должны обучить их распознавать сигналы выключателя. Существует два наиболее популярных режима обучения: self-lock и interlock. Первый предполагает, что одна и та же кнопка выключателя будет работать и на замыкание, и на размыкание реле (то есть свет будет и включаться, и выключаться по нажатию одной и той же кнопки). Второй же режим предполагает включение по одной кнопке, а выключение — по другой. Для достижения нашей конечной цели более гибким вариантом обучения является режим interlock — и с точки зрения случайных срабатываний (поясню чуть позже), и с точки зрения отслеживания изменения состояния. Реле, идущее в комплекте с выключателем, позволяет обучаться лишь в режиме self-lock. В целом, если вас это устраивает, то можно покупать и набором.

Итак, у нас есть три звена: лампочка, реле и выключатель. Процесс обучения подробно расписан и показан на страницах товаров, так что я не буду акцентировать на этом внимание. Как нам теперь узнать коды, отправляемые выключателем на реле, для получения возможности управления с помощью NodeMCU?

Для этого нам понадобится все та же NodeMCU в комплекте с радиоприемником для прослушивания радиоэфира.

Я бы рекомендовал обратить внимание еще и на специальный адаптер. Он идеально подойдет для разработки по ряду причин. Во-первых, к нему можно подключить нормальный блок питания до 24 В. Во-вторых, у него есть дополнительно 4 пары пинов (5V-GRD). Это очень важно ввиду того, что большинство необходимых нам приемопередатчиков требуют для работы 5 В, чего не может напрямую обеспечить NodeMCU.

Подключившись к нашей плате через serial port, мы можем наблюдать, что происходит в логах при нажатии на кнопки выключателя.

Код для этого нам понадобится элементарный (с использованием ранее упомянутых библиотек):

#include "RFReceiver.hpp"

RFReceiver* receiver;

void setup() {
    Serial.begin(BAUD_RATE);
    receiver = new RFReceiver(RF_RECEIVER_PIN);
}

void loop() {
    receiver->listen();
}

После получения кодов включения и выключения (выделены на изображении выше) наша схема взаимодействия преобразится следующим образом:

Disclaimer. Изображенная схема подразумевает подключение реле в сеть 220 В. Если вы никогда не работали с высоким напряжением, обязательно пригласите человека, который расскажет вам основные правила безопасности и поможет сделать все под присмотром.

Окей, теперь давайте разберем нашу схему по порядку. Для проверки того, что все отрабатывает корректно, нам нужны радиопередатчик и возможность управления платой с помощью внешних команд. Как я уже упоминал ранее, использовать мы будем MQTT, соответственно, наша плата должна выступать клиентом, подписанным на какой-то уникальный для нее topic. К примеру:

home/bedroom/device/927a15b2-d6fd-4f3a-a1c3-fdc7ccf25d45

Помимо этого, нам следует согласовать формат сообщений. Чтобы немного упростить себе жизнь при дальнейшей интеграции с другими компонентами нашей системы, можно сразу заложить поддержку JSON.

Чтобы улучшить читабельность кода, можно добавить дополнительный уровень абстракции для управления подконтрольной лампой:

#include "BedroomLamp.hpp"

const unsigned int BedroomLamp::RC_SWITCH_PROTOCOL = 1;
const unsigned int BedroomLamp::RC_SWITCH_LENGTH = 24;
// Intercepted codes
const unsigned long BedroomLamp::TURN_ON_CODE = 180356;
const unsigned long BedroomLamp::TURN_OFF_CODE = 180353;

BedroomLamp::BedroomLamp(uint8_t transmitterPin) : Lamp(transmitterPin) {
    Lamp::switchProtocol(RC_SWITCH_PROTOCOL);
}

void BedroomLamp::changeState(bool shouldTurnOn) {
    if (shouldTurnOn) {
        Lamp::turnOn(TURN_ON_CODE, RC_SWITCH_LENGTH);
    } else {
        Lamp::turnOff(TURN_OFF_CODE, RC_SWITCH_LENGTH);
    }
}

При этом основной управляющий код может выглядеть следующим образом:

#include "EntryPoint.hpp"
#include "BedroomLamp.hpp"
#include "MqttClient.hpp"
#include "OtaServer.hpp"
#include "WifiClient.hpp"

MqttConfig *mqttConfig;
MqttClient *mqttClient;
OtaServer *otaServer;
WifiClient *wifiClient;
BedroomLamp *bedroomLamp;

void setup() {
    Serial.begin(BAUD_RATE);
    initNetwork();
    initSensors();
}

void initNetwork() {
    wifiClient = new WifiClient(DEVICE_IP, MASK, GATEWAY, WIFI_SSID, WIFI_PASSWORD);
    otaServer = new OtaServer();
    mqttConfig = new MqttConfig(MQTT_BROKER_IP, MQTT_BROKER_PORT, MQTT_USERNAME, MQTT_PASSWORD, mqttCallback);
    mqttClient = new MqttClient(mqttConfig, wifiClient);
}

void initSensors() {
    bedroomLamp = new BedroomLamp(RF_TRANSMITTER_PIN);
}

void mqttCallback(const char *topic, const byte *payload, unsigned int length) {
    // mqtt payload processing
    // bool value = …
    bedroomLamp->changeState(value);
}

void loop() {
    otaServer->listen();

    if (!mqttClient->isConnected()) {
        mqttClient->connect([] { mqttClient->subscribe(Topic::DEVICE.c_str()); });
    }

    mqttClient->keepAlive();
}

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

Уже в текущем варианте мы можем совершенно спокойно управлять нашими устройствами из любой точки мира при условии проброса порта MQTT-брокера в мир. Но нам это не понадобится, потому что мы будем придерживаться первоначальной схемы.

Голосовое управление

Два лагеря

Итак, мы уже приблизительно поняли, как происходит общение с устройствами на самом низком уровне. Теперь попытаемся понять, каким образом звуковая волна (в виде голосовой команды) может быть трансформирована в радиосигнал, отправляемый на реле для манипуляции светом.

Как вы уже, наверное, успели догадаться, нам никак не обойтись без системы распознавания речи. И тут можно выделить два больших лагеря: готовые устройства и собственные «велосипеды». Вам придется заранее определиться с выбором, дабы не утонуть в пучине сложностей, с которыми у вас потенциально может не хватить сил бороться.

VS

Если говорить о готовых решениях, тут в лидерах продукты от Amazon и Google. В случае же изобретения своего собственного решения, помимо железной составляющей, которой я коснусь чуть позже, есть еще и довольно крупная софтверная часть. Я лично игрался с CMU Sphinx и Kaldi (распознавание речи), а также с Marry TTS (синтез речи). Хотя, насколько я знаю, на рынке уже есть и неплохие альтернативы. Тем не менее акцентировать внимание я буду именно на Kaldi — инструменте, с которым знаком лично.

Свой «велосипед»

Как я уже отмечал ранее, для реализации нашей цели — голосового управления — нам необходима система распознавания речи, причем довольно хорошая. Когда я занимался изучением этого вопроса, основными критериями для меня были вменяемая точность распознавания, open-source, возможность локального развертывания и поддержка русского языка. Все эти критерии отлично покрывались Kaldi.

Итак, допустим, мы решили строить свой «велосипед». Что нам понадобится по софту?

  • Kaldi Docker image: я настоятельно рекомендую пользоваться Docker и не пробовать разворачивать все на вашей основной системе, если только вы не хотите прострелить себе обе ноги. Этот образ содержит уже готовую русскую модель и WebSocket server (Python).
  • Socket client: ввиду наличия сокет-сервера берем любой клиент (независимо от языка).
  • Wake word detection engine: если мы планируем делать все по образу и подобию готовых решений, нам понадобится обучить модель реагировать на какое-нибудь активационное слово или фразу. Это необходимо и с точки зрения защиты приватности, и чтобы не дергать лишний раз наш ASR. Приведенный по ссылке сервис позволяет вам сделать это совершенно бесплатно, пригласив любого желающего поучаствовать в обучении. К слову, я уже начинал обучать модель реагировать на фразу «Эй, Мэри». Так что, желающие помочь, welcome.

Далее последует пример NestJS-сервиса, работающего с HotwordDetector и Kaldi. Но то же самое можно сделать и на любом другом языке. Идея заключается в отправке голосового потока — Kaldi, только после срабатывания активационного слова. Здесь же предусмотрена и симуляция работы тайм-аута у реальных колонок при молчании пользователя.

Ввиду того, что транскрипт может прийти неточный (с довольно высокой вероятностью), для корректной интерпретации команд нам понадобится еще какой-нибудь хороший алгоритм сравнения строк. Я использовал библиотеку string-similarity, основанную на коэффициенте Дайса.

При этом обработчик полученного транскрипта от Kaldi может тут же отправлять команду на наш микроконтроллер с помощью MQTT. Эту часть я пока опускаю, чтобы не смещать фокус в сторону коммуникаций. Пока акцентируем внимание на ASR-части.

@Injectable()
export class ASRService {
  @Inject()
  private readonly kaldiSocketService: KaldiSocketService
  @Inject(WINSTON)
  private readonly logger: Logger
  private readonly detector: HotwordDetector

  constructor() {
    this.detector = new HotwordDetector(DETECTOR_OPTIONS, [HOT_WORD_MODEL_OPTIONS], RECORDER_DATA, console)
      .on('error', err => this.logger.error(err))
      .on('hotword', (index, hotword, buffer) => {
        this.logger.info('[WAKE WORD DETECTED] %s', hotword)
        this.detector.stop()
        this.kaldiSocketService.startRecognition(this.start)
      })
  }

  public start = () => {
    this.detector.start()
  }
}
@Injectable()
export class KaldiSocketService {
  @Inject(WINSTON)
  private readonly logger: Logger
  private readonly recorder: AudioRecorder = new AudioRecorder(RECORDER_OPTIONS, console)
  private socketClient: WebSocket
  private pauseTimeout: NodeJS.Timeout | null = null

  public connect(): void {
    if (this.socketClient) {
      this.socketClient.removeAllListeners()
    }

    this.socketClient = new WebSocket(KALDI_SERVER_URL)
      .on('open', () => {
        this.logger.info('Connected to Kaldi server')
      })
      .on('close', () => {
        this.logger.info('Disconnected from Kaldi server')
      })
      .on('message', (data: string) => this.handleMessage(data))
  }

  public startRecognition(restartFn: () => void) {
    if (!this.isSocketAlive()) {
      this.connect()
    }

    if (this.recorder) {
      this.recorder.stop()
    }

    this.recorder
      .start()
      .stream()
      .on('error', err => this.logger.error(err))
      .on('data', chunk => this.sendChunk(chunk))
      .on('end', () => restartFn())
    this.startPauseTimer(this.stopRecognition)
  }

  public stopRecognition = (): void => {
    this.recorder.stop()
  }

  public sendChunk = (chunk: any): void => {
    this.socketClient.send(chunk)
  }

  public disconnect = () => {
    this.sendChunk('{"eof" : 1}')
    this.stopPauseTimer()
  }

  private async handleMessage(data: string): Promise<void> {
    const kaldiPayload: KaldiPayload = JSON.parse(data)

    if (kaldiPayload.result && kaldiPayload.result.length > 0) {
      this.logger.info('[FINAL TRANSCRIBE] %s', kaldiPayload.text)
      const match: BestMatch = compare(kaldiPayload.text, AVAILABLE_COMMANDS)
      const command: string = match.bestMatch.target
      this.logger.info('[MATCHING COMMAND] %s -> %s', kaldiPayload.text, command, match.bestMatch.rating)

      if (match.bestMatch.rating > SIMILARITY_LEVEL && POWER_COMMANDS.includes(command)) {
        // publish MQTT message
        this.startPauseTimer(this.stopRecognition)
      } else if (command === STOP_WORD) {
        this.disconnect()
      }
    } else if (kaldiPayload.partial) {
      this.logger.info('[PARTIAL TRANSCRIBE] %s', kaldiPayload.partial)
      this.stopPauseTimer()
    }
  }

  private startPauseTimer = (callback: () => void): void => {
    this.pauseTimeout = setTimeout(callback, SILENCE_TIMEOUT)
  }

  private stopPauseTimer = (): void => {
    if (this.pauseTimeout) {
      clearTimeout(this.pauseTimeout)
      this.pauseTimeout = null
    }
  }

  private isSocketAlive(): boolean {
    return this.socketClient.readyState === this.socketClient.OPEN
  }
}

В чем подвох?

Казалось бы, вот же она — почти идеальная система голосового управления собственным домом! Да еще и на русском языке! Дайте две!

Но не тут-то было. Давайте теперь посмотрим на то, о чем не было упомянуто и к чему предстоит готовиться:

  • ASR-домен. Скорее всего, «поиграться» вам хватит и того, что уже рассмотрено ранее. Но со временем вы начнете натыкаться на множество проблем (точность распознавания, ложные срабатывания, фильтрация шумов и т. п.), которые потребуют от вас определенных знаний в области распознавания речи. И тут придется довольно далеко выйти из своей зоны комфорта.
  • Упрочнение модели wake word. Для демоцелей вам хватит и нескольких семплов. Но, чтобы выйти на production-ready-уровень, придется собирать с миру по нитке для улучшения модели.
  • Микрофон с высокой долей вероятности станет убийцей всей вашей затеи со своим «велосипедом». Дело в том, что в серийном производстве у нас нет универсальных решений, покрывающих все нюансы использования в контексте умного дома. Голосовое управление, как идея, будет иметь право на жизнь тогда и только тогда, когда мы сможем предоставить конечному пользователю возможность быть услышанным из любой точки помещения. Где бы вы ни находились в пределах комнаты, ваш продукт должен слышать и понимать вас вне зависимости от того, присутствуют вокруг посторонние шумы или нет.

В свое время я споткнулся именно на проблеме микрофона. Дело в том, что свои тесты я проводил на лэптопе и беспроводной гарнитуре в идеальных условиях. Но когда дело дошло до реального использования, я понял, что ни один из бюджетных микрофонов не приспособлен для решения таких задач. А на дорогостоящее железо я даже не стал смотреть, потому что это разрушило бы наш первый критерий — минимизацию расходов. В итоге затею с голосовым управлением на собственном «велосипеде» пришлось приостановить.

Первая надежда

Прошло некоторое время после не совсем удавшегося эксперимента с Kaldi. И тут случилось то, что заставило меня вернуться к былой затее. К нам в компанию зашел проект, связанный с разработкой умного ассистента. В результате я получил возможность пощупать готовые решения от Amazon и Google, к которым изначально относился весьма скептически, исходя из разных соображений: это и зависимость от облаков, и защита приватности, и ряд неопределенностей, связанных со всеми сопутствующими расходами. Спустя полтора года различного рода экспериментов у меня накопилось достаточно информации для сравнения как между провайдерами в целом, так и с заброшенным ранее вариантом с Kaldi.

Железо готовых решений

То, что практически сразу бросилось в глаза уже при первом тестировании, — это способность умных колонок слышать и понимать собеседника на относительно большом расстоянии (взять, к примеру, комнату длиной 6-8 метров), несмотря на посторонние шумы. Мне тут же стало жутко интересно, как работает их железо. Ведь, по сути, тот же Amazon Echo Dot 3 можно было купить по $22 на Black Friday. А это намного дешевле всех модных микрофонов, которые совсем не решают описанных выше проблем.

Из того, что удалось нарыть, самым вменяемым оказалось видео с Intel AI DevCon. Если кратко, то там приводилась следующая схема:

Тут мы видим целый массив из четырех микрофонов (характерно для Echo Dot 3, для других моделей может отличаться), динамик и чип с DSP-алгоритмами на борту.

Из алгоритмов можно выделить следующие:

  • wake word detection;
  • audio feedback disposal;
  • acoustic echo cancellation;
  • beamforming.
Более подробно о них можно узнать из видео, на которое я сослался выше.

По Google ничего вменяемого, в плане железа, найти не получилось. Но, судя по наблюдениям, работают их умные колонки приблизительно на том же уровне, что и амазоновские. Соответственно, можно сделать допущение о том, что в плане железа и возможностей они стараются не отставать от прямых конкурентов.

В результате мощная железная составляющая по относительно невысокой цене автоматически выводит готовые решения в лидеры по сравнению с собственным «велосипедом».

Не все так гладко

Несмотря на, казалось бы, идеальное железо, готовые решения тоже не безгрешны. Ввиду того, что умные колонки нацелены не только на домен smart home, разработчики борются еще и с другими проблемами, наиболее острой среди которых, пожалуй, является отсутствие понимания контекста обрабатываемой информации. Эту тему троллят по всем фронтам. Чтобы читатель понял, о чем идет речь, предлагаю посмотреть следующее видео:

Приблизительно таким же образом моя супруга общается дома с Alexa. Но в защиту устройств от Amazon необходимо отметить, что в этом видео можно совершенно спокойно подставлять вместо Alexa любую существующую альтернативу, будь то Google, Siri и прочие, — результат будет неизменным.

Боевые испытания

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

Такой вариант проходит бета-тест у меня дома уже около года. Результаты наблюдений я приведу чуть позже. Для начала нужно избавиться от неизвестных в правой части, а именно понять, как Alexa интерпретирует пользовательские команды и что с ними происходит дальше.

Когда пользователь выражает намерение взаимодействовать с каким-либо устройством, происходит следующее:

  • голосовой поток после предобработки на железе улетает в Amazon Cloud, где в конечном счете попадает на так называемый Alexa Skill;
  • на скиле формируется специализированный запрос (директива), содержащий информацию о пользовательском намерении;
  • директива улетает на Lambda function (back-end нашего скила), где мы обязаны ее обработать;
  • на этом полномочия Amazon заканчиваются.

Alexa Skill

Alexa Skill — это, по сути, программное обеспечение для колонки. При этом хостится все на стороне Amazon. Существует несколько типов скилов, которые вы можете разрабатывать. Но нам необходим лишь smart home skill. Quick start guide можно найти в официальном GitHub-репозитории. Если вы решили следовать именно ему, то можете пропустить эту часть. Но вынужден предупредить, что, несмотря на заявленные в документации 15 минут, можете смело выделять неделю своего времени на то, чтобы разобраться во всем с нуля.

Для старта вам понадобится зарегистрировать основной Amazon-аккаунт и аккаунт разработчика. Скилы создаются именно из-под Dev-аккаунта. Но основной нам понадобится для создания Lambda function (об этом чуть позже). Сразу стоит отметить то, что при регистрации вам обязательно придется привязать платежную карту. При этом с вас снимут $1, а затем вернут обратно, чтобы проверить вашу платежеспособность. Но не волнуйтесь, создание скилов не будет стоить вам ни копейки.

Итак, после регистрации идем в dev console и жмем Create Skill.


На следующей странице даем имя нашему скилу и не забываем выбрать тип:


После создания скила вы увидите следующую страницу:


И тут нас подстерегает первая неожиданность от Amazon. В качестве back-end для нашего скила мы можем использовать только AWS Lambda (в отличие от других типов скилов). К чему это приведет, я расскажу чуть позже. Lambda у нас пока нет, поэтому делать тут нечего. Следуем ссылке в самом низу — Setup Account Linking.

Страница довольно важная, так как здесь вам придется сконфигурировать доступ к своему oauth-провайдеру. Шаг обязательный, без него вы не сможете идти дальше. Здесь вам придется сделать выбор: использовать либо built-in-поддержку Amazon Oauth, либо какой-то существующий сервис вроде Auth0, либо свой «велосипед». Я покажу пример конфигурации для первых двух вариантов.

Amazon Oauth

Идем на страницу Login with Amazon и создаем новый security profile.


Заполняем обязательные поля (privacy notice URL для тестовых целей можно поставить фейковый — http://example.com/privacy.html) и сохраняем. На этом этапе у нас уже сгенерированы client id и secret, которые понадобятся нам на скиле.


Но нам нужно выполнить еще один шаг. Открываем Web Settings нашего профиля на редактирование и обновляем поле Allowed Return URLs данными из поля Allowed Redirect URLs нашего скила.


На самом же скиле нужно заполнить client id / secret сгенерированными значениями из профиля:

Остальные поля заполняем так, как показано на скрине выше. Сохраняем, Oauth настроен!

Auth0

Создаем Auth0-аккаунт (лучше воспользоваться вариантом с соцсетями). Затем создаем новый Machine to Machine Application.


Далее нам нужно сделать то же, что мы уже делали в случае с Amazon security profile: заполнить Allowed Callback URLs значениями из скила:


Ну а на самом скиле уже надо будет заполнить client id / secret, сгенерированный на Auth0. Остальные поля придется заполнять в соответствии с требованиями Auth0 и именем вашего домена.

AWS Lambda

Как я уже упоминал ранее, в качестве back-end для обработки запросов с Alexa Skill нам понадобится Lambda function. Для ее создания вам придется воспользоваться уже основным AWS-аккаунтом.


Здесь, задав имя, все остальное можно оставить по умолчанию, если только вам не нужен другой runtime. Мы будем использовать пример на базе JS/TypeScript. Вы же можете выбрать более близкий вам язык, но тогда придется адаптировать код.


Далее на главной странице нам нужно добавить Smart Home trigger.


При этом вам сразу придется указать ID вашего скила, который можно скопировать на главной странице:


Для того чтобы теперь связать функцию с нашим скилом, необходимо скопировать ARN:

Затем ARN надо вставить в поле Default Endpoint на главной, то есть в итоге у нас образуется жесткая двусторонняя связь между скилом и Lambda.

Итак, у нас все готово для разработки back-end.

Alexa Smart Home Skill API

Как я уже отмечал ранее, на нашем back-end (Lambda) понадобится реализовать обработчики запросов, прилетающих из скила. И тут начинается ад, потому что придется долго и нудно «курить» официальную документацию. Основная проблема тут в том, что готового SDK под smart home skills пока еще не существует, поэтому многие вещи придется описывать с нуля. На просторах GitHub можно найти разные «велосипеды» пользователей, которые столкнулись с такой же проблемой. Но после изучения исходников становится понятно, что код строился в основном под свои специфические кейсы.

Теперь о проблеме AWS Lambda. Была бы возможность, я бы вообще от нее отказался, потому что это чуть ли не основной bottleneck в контексте smart home skills. Приведу простой пример. Приходите вы такие домой с работы (никто не обращался к вашим устройствам в течение дня), просите Alexa включить свет. И что, думаете, он вот так сразу и загорится? А вот и нет! Cold start нашей «лямбды» просто уничтожает весь UX. Задержка просто нереальная для таких задач. Да, все последующие запросы будут проходить довольно шустро, но первый очень разочаровывает. Политика Amazon тут предельно ясна: подсадить пользователя на как можно большее количество своих сервисов. Но не давать разработчикам права выбора (как в случае с другими скилами) — это просто свинство.

На самом деле, в процессе написания этой статьи Amazon объявил о новой фиче Provisioned Concurrency, которая якобы должна решить проблему холодного старта. Но пока не совсем очевидно, будет ли существовать какая-нибудь лимитированная free-версия. В противном случае это будет выглядеть весьма странно: заманивать разработчиков бесплатностью написания скилов, но в то же время добавлять ложку дегтя, которая выглядит как «хочешь улучшить UX — плати».

Опять же по теме API: Amazon постарался сделать его гранулярным. Это вроде как и хорошо, дает определенную гибкость, но при большом количестве многофункциональных устройств и ввиду отсутствия SDK сильно замедляет процесс реализации.

Для quick start с лампочкой вам понадобится реализовать несколько ключевых обработчиков:

  • Authorization — срабатывает при активации пользовательского аккаунта (account linking).
  • Discovery — вызывается для опроса устройств, находящихся в локальной подсети с колонкой (тут важно быстро вернуть список доступных, пусть даже виртуальных).
  • ReportState — срабатывает в случае необходимости запросить состояние конкретного устройства (актуально для мобильного приложения).
  • PowerController — обработчик команд включения/выключения (срабатывает по ключевым словам — on/off).

Схематически процесс преобразования голоса в API-запрос выглядит следующим образом (на примере Discovery):

Когда мы просим Alexa найти устройства, в облаке формируется директива, содержащая разнообразную информацию. Но для нас наиболее актуальными являются Interface и Header Name. Эти два поля дают нам возможность однозначно идентифицировать обработчик, который необходимо вызвать в ответ на прилетевший запрос. В этом конкретном примере мы обязаны вернуть детальный список устройств (Endpoints). При этом в числе endpoint-характеристик присутствуют два важных поля: Endpoint ID (уникальный идентификатор устройства) и Friendly Name (имя, на которое устройство будет откликаться). Чтобы подробнее понять важность этих полей, давайте рассмотрим еще одну схему использования уже управляющей команды включения света:

Когда мы произносим «Alexa, light on», в облаке происходит поиск соответствия озвученного friendly name — light одному из ID ранее найденных устройств (endpoint id). Таким образом, в директиве у нас прилетает уже идентификатор устройства, а не его имя. При этом интерфейс определяется на основании самой команды on, которая, согласно документации, привязана к PowerController. Header же опирается на само значение: в случае с on — это TurnOn, а для off было бы TurnOff. Вот такой вот нехитрый принцип.

Следует также отметить, что реализация так называемых контроллеров напрямую зависит от тех свойств, что мы вернули на фазе Discovery для каждого из устройств. Например, светодиодную ленту мы можем как включать/выключать, так и менять ее яркость. А это означает, что в процессе поиска такой ленты мы должны обозначить поддержку PowerController и BrightnessController. Таким образом, когда пользователь захочет включить ленту, сработает первый, изменить яркость — второй.

Ну а что мы будем делать с этой информацией дальше, это наше личное дело. Как я уже отмечал ранее, на этом полномочия Amazon заканчиваются.

Помимо официальной документации, желательно обратить внимание на примеры payload для разных типов запросов. Они очень сильно упростят процесс реализации обработчиков. Ну и рекомендую еще ознакомиться с примером моего «велосипеда», который можно взять за шаблон для smart home back-end. Это первая версия, которая довольно долгое время проходила боевые испытания.

Но с недавнего времени я тестирую и альтернативный подход — использование Lambda в качестве прокси на свой собственный back-end. Это было сделано по ряду причин. Часть из них раскроется позже. Но основная — это максимальная независимость от облачных сервисов. Если вдруг Amazon через время опомнится и даст возможность ходить напрямую на свой back-end, это сможет свести к нулю затраты на потенциальную миграцию.

Пример реализации Authorization handler на собственном back-end NestJS:

@Injectable()
export class AuthorizationHandler implements RequestHandler {
  @Inject()
  private readonly requestProvider: RequestProvider

  public handle(): SmartHomeResponse {
    return {
      event: {
        header: {
          namespace: Interface.AUTHORIZATION,
          name: HeaderName.ACCEPT_GRANT_RESPONSE,
          payloadVersion: this.requestProvider.request.directive.header.payloadVersion,
          messageId: uuid4()
        },
        payload: {}
      }
    }
  }

  public canHandle(): boolean {
    return this.requestProvider.canHandle(this.name)
  }

  public get name(): RequestIdentifier {
    return Interface.AUTHORIZATION
  }
}

Как видно из кода, нет ничего сверхъестественного. Важно лишь следовать формату, описанному в официальной документации.

А что же у Google?

Забегая вперед, скажу, что в настоящее время я обкатываю похожую схему smart home и c колонкой Google Home.

На первый взгляд, все очень похоже на вариант с Amazon. Вместо Alexa Skill используется аналогичный Google-сервис под названием Actions on Google. Но тут есть несколько ключевых отличий:

  • у Google нет встроенной поддержки oauth, поэтому придется либо подключать свое решение, либо воспользоваться готовым провайдером вроде Auth0;
  • Google позволяет ходить напрямую на свой собственный back-end без каких-либо serverless-зависимостей.

Последний пункт в моем личном рейтинге возносит Google на вершину хит-парада, потому что таким образом мы можем свести к минимуму количество облачных зависимостей. Более того, сейчас в developer preview находится killer feature, которая может потенциально вывести Google и в мировой топ, — возможность развертывания собственного кода непосредственно на колонке, что в сочетании с их SDK даст возможность общаться с нашими микроконтроллерами напрямую. Заявленная задержка в 300 мс выглядит многообещающе.

Наиболее внимательные читатели наверняка заметили еще один дополнительный узел — БД. Первую версию своего решения я строил с минимальным числом зависимостей. Соответственно, состояния хранились в памяти самих микроконтроллеров. Но если вы обратите внимание на любой из популярных how-to по созданию smart home skills от Amazon или Google, вы заметите рекомендации относительно использования DynamoDB/Firestore для хранения состояний/информации о ваших устройствах. Я настоятельно не рекомендую верить в эти «разводы», если вы хотите обойтись бесплатным использованием ваших скилов. Чисто технически с DynamoDB еще можно «выехать» на бесплатном тарифе. Но с Firestore я бы точно не связывался. Более того, с учетом того, что все это делается лично для себя, а не с коммерческой целью, сильно завязываться на «зоопарк» облачных сервисов я бы не стал.

В результате я локально поднял Cassandra у себя в Docker. Но с точки зрения common sense в условиях домашних нагрузок можно смело брать и любую другую БД.

Smart Home Action

Здесь процесс аналогичен амазоновскому: регистрация, создание проекта, базовая настройка самого action + oauth. Следует также отметить, что вам выдадут на виртуальный счет $300 на год. Можете делать с ними что хотите. Но рекомендую не подключать лишние сервисы, чтобы в один прекрасный день не получить письмо счастья с кругленькой суммой.

Итак, идем на Actions on Google и создаем новый проект:


При выборе типа проекта нужно обязательно выбрать Smart Home (изменить свой выбор потом не получится):


В процессе quick setup придется заполнить разную информацию. Display Name может быть произвольным. Эта опция нужна лишь для voice / chat bots. Smart Home actions (по аналогии с Alexa Skills) явно не вызываются:


На странице Actions появляется коронная фича Google:

Здесь нужно в поле Fulfillment указать URL к вашему back-end. Для тестовых целей можете воспользоваться ngrok-сервисом для проброса локального порта наружу. Но в идеале рекомендую купить себе домен и SSL-сертификат. Local home configuration понадобится для той самой фичи, которая сейчас находится в preview, — развертывания своего кода на колонке. Но пока я еще не тестировал ее. Потому оставляем поле пустым.

Account linking. Тут все немного сложнее, чем у Amazon. Как я уже упоминал ранее, встроенной поддержки oauth у Google нет. Соответственно, рассматривать мы будем пример с Auth0. Процесс создания приложения выглядит идентичным тому, что мы делали для Amazon. Но есть следующие нюансы:


Allowed Callback URLs должны вести, с одной стороны, на /userinfo, с другой — на ваш Google-проект, который был создан чуть раньше. Ну и Allowed Web Origins должен соответствовать гугловскому oauth redirect URL. Но это еще не все. У Google есть открытый баг, который магическим образом влияет на интеграцию с Auth0. Расписывать его детально я не буду, но суть в том, что refresh token не обновится без задания audience на стороне Google. А Google, в свою очередь, не позволяет никоим образом его задать. Соответственно, единственным рабочим, но костыльным решением стало глобальное добавление audience на уровне самого Auth0: в General Settings вашего API нужно присвоить полю Identifier следующее значение: https://your.domain.eu.auth0.com/api/v2/ (your.domain, естественно, нужно заменить на вашу конфигурацию).

Теперь мы можем обновить action. Помимо client id / secret, остальные поля нужно заполнить следующим образом:


Будьте внимательны. В официальных guides отсутствует offline_access scope, который необходим для корректного обновления refresh token. Сохраняем, теперь наш action готов к использованию.

Отмечу еще одну деталь: в отличие от обычных actions для создания voice / chat bots, вы не сможете протестировать smart home action в эмуляторе. Вам в любом случае понадобится back-end.

Google Smart Home Skill API

По сравнению с Amazon Google API на первый взгляд выглядит намного проще. Хоть идея и аналогичная, с точки зрения реализации обработчиков есть кое-какие глобальные отличия. Следующий маппинг позволит уловить их суть:


У Google API более обобщенный. Например, здесь вы не встретите ярко выраженный Discovery. По сути, Sync handler включает в себя и Authorization, и Discovery, то есть срабатывает он при линковке аккаунта пользователя, но тут же мы обязаны вернуть список доступных устройств в ответе.

Альтернативой ReportState является Query, который также срабатывает при опросе состояния устройства через мобильное приложение.

Пожалуй, наиболее существенное отличие между API Google и Amazon — это набор управляющих обработчиков. Если у Amazon все они разбиты относительно свойств самих устройств, то у Google есть всего один Execute — handler, который срабатывает на любую управляющую команду. Ну а в такой ситуации наша задача немного усложняется, поэтому придется уделить чуть больше внимания вопросу дизайна наших обработчиков.

Буквально несколько примеров по подобию Amazon:


Как я уже отмечал выше, ярко выраженной фазы поиска тут нет, потому о новых устройствах можно узнать либо во время привязки аккаунта через мобильное приложение, либо посредством ручной отправки Sync-запроса. Сам обработчик при этом идентифицируется лишь на основании поля Intent.

Ввиду того, что специфика маппинга ID на Name аналогична амазоновской, процесс интерпретации управляющей команды можно обобщить до следующей схемы:


Сам Execute request при этом стал сложнее за счет ранее упомянутого обобщения.

Custom back-end

Пришло время затронуть тему кода. Ввиду незавершенности нового подхода к организации локального back-end я буду приводить пока лишь code snippets, чтобы ввести вас в контекст. Финальная версия вскоре будет доступна на GitHub.

Я приведу пример с Google, но похожую технику можно применить и по отношению к Amazon. Начнем с контроллера, куда мы приходим с Actions on Google:

@Controller()
@UseGuards(GoogleAuth0Guard)
export class DeviceController {
  @Inject()
  private readonly actionService: ActionService

  @Post('/smarthome')
  public async customActionHandler(): Promise<SmartHomeV1Response> {
    return this.actionService.handleRequest()
  }
}

Здесь всегда будет единая точка входа для любого реквеста. С учетом использования Auth0 мы можем подключить свой Guard для аутентификации пользователя:

@Injectable()
export class GoogleAuth0Guard extends BaseGuard implements CanActivate {
  public get type(): GuardType {
    return GuardType.GOOGLE
  }

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const host: HttpArgumentsHost = context.switchToHttp()
    const request: Request = host.getRequest()
    const { authorization } = request.headers
    return this.isVerified(request, authorization && authorization.split(' ').pop())
  }
}

Как бы тогда выглядел ActionService?

@Injectable()
export class ActionService {
  @Inject()
  private readonly syncHandler: SyncHandler
  @Inject()
  private readonly queryHandler: QueryHandler
  @Inject()
  private readonly executeHandler: ExecuteHandler
  @Inject()
  private readonly disconnectHandler: DisconnectHandler

  public handleRequest(): SmartHomeResponse {
    return this.handlers.find(handler => handler.canHandle()).handle()
  }

  public get handlers(): RequestHandler[] {
    return [this.syncHandler, this.queryHandler, this.executeHandler, this.disconnectHandler]
  }
}

Этот сервис может служить точкой входа для постобработки запроса. Для нас важно идентифицировать требуемый обработчик и выполнить управляющий код по отношению к устройству, к которому обратился пользователь посредством голоса.

Если, например, мы хотим отдать список доступных пользователю устройств при синхронизации, этого можно добиться путем извлечения соответствующих данных из Cassandra DB по user id.

@Injectable()
export class SyncHandler implements RequestHandler {
  @Inject()
  private readonly deviceService: DeviceService

  @Inject()
  private readonly requestProvider: RequestProvider

  public get name(): SmartHomeV1Intents {
    return Action.SYNC
  }

  public async handle(): Promise<SmartHomeV1Response> {
    const devices: DeviceDto[] = await this.deviceService.getDevicesByUserId(this.requestProvider.userId)
    return {
      requestId: this.requestProvider.request.requestId,
      payload: {
        agentUserId: this.requestProvider.userId,
        devices: devices.map(device => _.omit(device, 'agentUserId'))
      }
    }
  }

  public canHandle(): boolean {
    return this.requestProvider.canHandle(this.name)
  }
}

Подобным же образом реализуются и остальные обработчики. При большом желании API можно продумать так, чтобы он был достаточно универсальным, и вести работу со всеми тремя рассмотренными решениями одновременно.

Демо

В следующем видео вы сможете наблюдать все 3 решения в действии. Сразу хотелось бы отметить, что в случае с Amazon Lambda была уже разогрета, в своем же «велосипеде» — с использованием Kaldi — присутствует определенная задержка в получении final transcribe от сервера.

Эпилог

Все мы прекрасно осознаем, что технологии в настоящее время развиваются семимильными шагами. И то, что было невозможно еще десять лет назад, сейчас уже активно распространяется в массы. Интеграция голосового управления в собственный дом не исключение.

Эта таблица подводит итог по вышеизложенному материалу.

PointEcho Dot (3rd Gen)Google Nest MiniCustom Bicycle
Price$49.99$49~= Microphone price
Microphone42-
Speech to TextBuilt-inBuilt-inKaldi ASR
Text to SpeechBuilt-inBuilt-inMarry TTS
Custom Commands--+
UA/RU Language--+
FrontendAlexa SkillsActions on GoogleAny
BackendAWS LambdaAnyAny
Local DeploymentAVSDev Preview+
AuthOAuthOAuthAny
Wake WordFixed — AlexaFixed — Hey/OK GoogleCustom — Snowboy
Far FieldBuilt-inBuilt-in-
Public APIGranularGenericLow level
Smart Home SDKAVS onlyLocal Deployment Only-
Speaker RecognitionIn-app Training (no API yet)In-app Training (no API yet)-
Programming LanguagesJS, Java, Python, C#, Go (AWS Lambda), C/C++ (embedded)Any (backend), JS (local deployment), C/C++ (embedded)Any (backend / frontend), C/C++ (embedded)
Setup ComplexityHighHighHigh

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

А вот готовые решения, на мой взгляд, не смогут набрать серьезные обороты, пока английский язык не станет одним из обязательных у нас в стране. Все-таки ни украинский, ни русский пока не поддерживаются ни одним из лидеров рынка. И каких-то конкретных обещаний в этом направлении пока не слышно.

Для англоговорящей части населения все сводится к следующему выбору.

С одной стороны, можно купить готовое устройство с поддержкой Alexa/Google и уповать на удачу. Ведь, если исходить из рейтинга скилов многих вендоров, люди очень негодуют от того, что их устройства либо не обнаруживаются, либо глючат при голосовом управлении. В целом это неудивительно, если принять во внимание рассмотренные выше проблемы.

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

В целом, если у кого-то есть идеи, предложения и желание развивать это направление у нас в стране, пишите.

Похожие статьи:
Уявіть, що у вас є реальна можливість змінити щось одне — найважливіше — в процесах рекрутингу, що б це було? Саме на це питання DOU...
Привіт, мене звати Аліна, я керівниця HR-департаменту компанії EVNE Developers. У статті хочу поділитися власними спостереженнями...
В этот раз DOU Ревизор побывал в офисе Freetour.com Limited — продуктовой IТ-компании, которая предоставляет сервис бронирования...
Компания Microsoft опубликовала итоги своего третьего квартала текущего года. В целом инвесторы и рынок оценили результаты...
Настало время поговорить о многих вещах: курсах и собеседованиях, тестовых заданиях и стартапах, мотивации жить...
Яндекс.Метрика