![image](https://habrastorage.org/webt/2n/hd/xj/2nhdxjoufdo6vznkxpo_fo646f4.png)
Рынок голосовых ассистентов расширяется, особенно для русскоязычных пользователей. 2 недели назад Яндекс рассказала впервые про платформу Яндекс.Диалоги, 2 месяца назад Google представила возможность писать диалоги для Google Assistant на русском языке, 2 года назад со сцены Bill Graham Civic Auditorium Apple выпустила в открытое плавание SiriKit. Фактически, появляется новая отрасль разработки, где должны быть свои проектировщики, архитекторы и разработчики. Идеальный момент, чтобы поговорить про голосовые помощники и api для них.
В этой статье не будет подробных туториалов. Это статья об идеях и интересных технических деталях, на которых построены инструменты для сторонних разработчиков основных игроков рынка: Apple Siri, Google Assistant и Алисы от Яндекса.
Теорию без практики изучать скучно. Представим, что перед нами стоит задача от только что придуманной пиццерии «ДоРеМи». Руководство компании хочет, чтобы покупатель имел возможность узнать меню пиццерии и заказать пиццу голосом. Заказ еды оставим на вторую итерацию, а сейчас займемся меню. Добавим команду «Что входит в состав <Название пиццы>?». Если пользователь вводит некорректную команду, то вывод будет состоять из списка пицц. Задача простая. Идеально подходит, чтобы изучить технологию и подготовиться к дальнейшему расширению.
Первый пункт — поднять бэкэнд
ВНИМАНИЕ! Android разработчик поднимает сервер на node.js. Слабонервным лучше пропустить эту часть.
Сервер нам нужен для хранения информации о пиццах и для дальнейшего взаимодействия с апи ассистентов. Бэкэнд будет написан на node.js вместе с фреймворком express для настройки веб-приложения. Разворачивать будем на платформе Now от компании Zeit. Платформа бесплатна и проста в использовании. Вводим команду «now» в терминале для старта скрипта деплоя и в ответ получаем ссылку на наше веб-приложение.
Для инициализации проекта используем Express Generator. Результатом генерации будет отличный каркас для веб приложений, но здесь много лишнего для простого апи: шаблоны для страниц, страницы ошибок, папки для ресурсов. Оставим только самое необходимое.
![image](https://habrastorage.org/webt/_m/yq/ez/_myqezfuxkcsmfveksqe-6affba.png)
Базу данных использовать не будем. Данные у нас статические, достаточно будет одного объекта в js — список пицц с названиями и ингредиентами.
const pizzas = [
{
name: "Маргарита",
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Томаты", "Базилик"]
},
{
name: "Пепперони",
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Пепперони"]
},
{
name: "Вегетарианская",
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Грибы", "Маслины", "Зеленый перец", "Сыр Фета", "Томаты", "Орегано"]
},
{
name: "Четыре сыра",
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Сыр пармезан", "Сыр Чеддер", "Сыр Блючиз"]
},
{
name: "Гавайская",
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Куриное филе", "Ананасы"]
},
];
Добавим метод, который по объекту пиццы будет выводить ее состав. Если же пицца не найдена, то ответом будет меню пиццерии.
const pizzaInfo = {
getPizzaInfoByPizzaName: function (pizza) {
const wrapName = name => `"${name}"`
if (!pizza) {
const pizzaNames = pizzas.map(pizza => wrapName(pizza.name)).join(", ")
return `В ассортимент пиццерии "ДоРеМи" входят пиццы ${pizzaNames}. Могу рассказать состав каждой пиццы.`
}
const ingredients = pizza.ingredients.map(ingredient => ingredient.toLowerCase()).join(", ")
return `В пиццу ${wrapName(pizza.name)} входят ${ingredients}.`
},
};
Яндекс.Алиса. Начнем с простого
Яндекс.Диалоги — базовая комплектация машины, на которой можно ездить. Но все равно не хватает кондиционера. Платформа от Яндекса идеальна для изучения азов: простая, как три копейки, но при этом содержит в себе концепции, на которых построено большинство ассистентов.
Основная единица платформы — диалоги. Диалоги — скиллы, созданные сторонними разработчиками. Добавить новый функционал в основной разговор с ассистентом не получится. Хотелось бы взять фразу «Алиса, закажи мне пиццу», но пиццерий много. Пользователю придется сказать активационную команду: «Алиса, вызови мне ДоРеМи». Тогда сервис понимает, что нужно переключиться на диалог от «ДоРеМи». Мы принимаем власть в свои руки и управляем процессом на своем сервере, через реквесты и респонсы, используя технологию вебхуков.
Настройки, которые нужно прописать для создания диалога в личном кабинете: название, тематика диалога, активационное имя и url на сервер.
![image](https://habrastorage.org/webt/zz/tu/lf/zztulf2fisv-vpxzajlg8xcwh5k.png)
Дальше только настройка сервера на обработку запросов. Принимаем json, отправляем json. А еще проще, если отбросить все шелуху с приемом json, его парсингом, с извлечением данных и обратными действиями в процессе отправки ответа, то мы принимаем текст пользователя и возвращаем текст Алисы. Добро пожаловать в 70-е, во времена текстовых интерфейсов.
У нас есть строка с командой пользователя. Чтобы возвращать состав пиццы по команде пользователя, нам нужно вычленить название пиццы и в ответ прислать фразу. Вычленять будем обычным string.contains(фраза). Чтобы идея сработала, модернизируем наш список пицц, добавив к ним список основ (морфема слова без окончания), которые могут встречаться в запросе.
const pizzas = [
{
name: "Маргарита",
base_name: ["маргарит"],
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Томаты", "Базилик"]
},
{
name: "Пепперони",
base_name: ["пепперони", "пеперони", "пепирони"],
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Пепперони"]
},
{
name: "Вегетарианская",
base_name: ["вегетариан"],
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Грибы", "Маслины", "Зеленый перец", "Сыр Фета", "Томаты", "Орегано"]
},
{
name: "Четыре сыра",
base_name: ["четыр", "сыр"],
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Сыр пармезан", "Сыр Чеддер", "Сыр Блючиз"]
},
{
name: "Гавайская",
base_name: ["гавай"],
ingredients: ["Тесто", "Томатный соус", "Сыр Моцарелла", "Куриное филе", "Ананасы"]
},
];
Немного изменим функцию, которая возвращает состав пиццы по команде пользователя.
getPizzaInfoByUserCommand: function (command) {
command = command.toLowerCase();
const pizza = pizzas.find(pizza => (
pizza.base_name.some(base => (command.indexOf(base) !== -1))
))
return this.getPizzaInfoByPizzaName(pizza)
},
Обрабатываем JSON, отправляем корректный ответ и добавляем кнопку, которая перенаправит пользователя на сайт пиццерии. Кнопки в Алисе — единственная возможность привнести разнообразие в обычный текстовый вывод. На кнопку можно назначить реплику пользователя либо открытие браузера по url. Используйте deep linking, чтобы связать ассистент и приложение в один удобный процесс. Например, при заказе пиццы можно настроить переход на экран оплаты в приложении, где уже сохранены платежные данные или есть возможность оплатить через Google/Apple Pay.
var express = require('express');
var pizzaInfo = require('../pizza/pizza_info.js');
var router = express.Router();
/* GET home page. */
router.use('/', function (req, res, next) {
const body = req.body;
const commandText = body.request.command;
const answer = pizzaInfo.getPizzaInfoByUserCommand(commandText);
res.json({
"response": {
"text": answer,
"buttons": [{
"title": "Заказать",
"url": "https://doremi.fake/"
}
],
"end_session": false
},
"session": {
"session_id": body.session.session_id,
"message_id": body.session.message_id,
"user_id": body.session.user_id
},
"version": body.version
})
});
С помощью параметра tts(text-to-speach) можно настроить фонотеку голосового ответа Алисы: ударение, произношение и пробелы. В tts лучше передавать транскрипцию вместо орфографически правильного написания. Например, «пажалуста». Так речь Алисы будет естественнее.
Тестировать диалог проще некуда. В личном кабинете можно поговорить через консоль со своим диалогом и почитать json’ы.
![image](https://habrastorage.org/webt/do/ox/54/doox54gbnvwln9dy2biqsdcmuta.png)
На данный момент диалог в стадии черновика. Следующим шагом будет публикация в каталоге Яндекса. Перед публикацией он проходит проверку на соответствие требованиям Яндекса: достоверность информации, грамотность, этичность и остальные формальные качества.
Google Assistant. Новый уровень
Если Диалоги — это базовая комплектация автомобиля, то Actions on Google — комплектация с массажным креслом, автопилотом и персональным водителем, инструкция к которым прилагается на китайском языке. Инструмент от Google сильнее, богаче, но сложнее. И входной порог в технологию выше. У Яндекса гениально лаконичная и простая документация. Про Google сказать такое не могу. Actions on Google построен на тех же аксиомах, что и Диалоги: активационная команда, общение через апи, использование вебхуков, отделение стороннего диалога от основного.
Простота — главное преимущество и проблема Диалогов. Проблема в том, что всю архитектуру нужно строить самому. Простейший алгоритм вычленения частей из текста пользователя, реализованный выше, нельзя расширить на новые команды. Приходиться изобретать велосипеды. В такие моменты понимаешь, почему графический UI до сих рулит. Но Google реализовал продукты, которые освобождают разработчика от скучных скриптуемых процессов: классификация команд пользователя и работа с реквестами и респонсами. Первая задача решается фреймворком DialogFlow aka Api.Ai, вторая — объемной библиотекой под node.js. Нам остается подсоединить апи к Actions через node.js. На первый взгляд, что это лишнее усложнение, но сейчас я покажу, что этот подход выигрывает в проектах, где команд больше, чем одна.
DialogFlow решает типичную задачу машинного обучения — задачу классификации, в нашем случае классификация пользовательских команд по категориям. Для понимания и настройки работы фреймворка разберем два понятия из терминологии DialogFlow. Первое — Entities или сущности. Например, марки машин, города или названия пиццы. В настройках сущности мы указываем примеры сущности и ее синонимы. Алгоритм будет пытаться зацепиться за сущность на уровне основ слов. В случае успеха Google пришлет ее на сервер в качестве аргумента.
![image](https://habrastorage.org/webt/ty/ra/dc/tyradcea__lr28bwsxpm50rthig.png)
Второе понятие — Intents или действия — категории, по которым DialogFlow будет классифицировать команды пользователя. Мы добавляем примеры команд, по которым будет определяться интент. В примерах команд лучше использовать примеры сущностей, которые были добавлены на первом шаге. Так алгоритму будет проще научиться вычленять нужные нам аргументы. Главная фишка DialogFlow — на основе введенных шаблонов нейросети Google тренируются и генерируют новые ключевые фразы. Чем больше шаблонов мы добавим, тем корректнее будет определяться интент. Не забудем добавить идентификационное имя для интента, которым мы будем дальше пользоваться в коде.
![image](https://habrastorage.org/webt/ty/2t/zl/ty2tzlshnr6z3gczkwurxhsehva.png)
У интента есть имя, есть список параметров. Не хватает возвращаемого значения. В настройках можно добавить статические ответы. Динамические ответы — зона ответственности js кода. Далее я буду расхваливать вторую вещь, которая делает подход гугла еще круче — официальная библиотека к node.js. Она лишает радости парсить json и заниматься маршрутизацией интентов через длинные if’ы или switch-case блоки.
Инициализируем объект DialogflowApp, в конструктор передадим request и response. Через метод getArgument() мы получаем сущность из команды, с помощью tell() передаем ответ помощника, через handleRequest() настраиваем маршрутизацию в зависимости от интента.
const express = require('express');
const Assistant = require('actions-on-google').DialogflowApp;
const pizzaInfo = require('../pizza/pizza_info.js');
const app = express.Router();
// запрос на обработку вебхуков
app.use('/', function (req, res, next) {
// Инициализируем API.AI assistant объект.
const assistant = new Assistant({request: req, response: res});
const ASK_INGREDIENTS_ACTION = 'listOfIngredients'; // Название интента
const PIZZA_PARAMETER = 'pizza'; // Название сущности
function getIngredients(assistant) {
let pizzaName = assistant.getArgument(PIZZA_PARAMETER);
// Respond to the user with the current temperature.
assistant.tell(pizzaInfo.getPizzaInfoByUserCommand(pizzaName));
}
// Настраиваем маршрутизацию
let actionRouter = new Map();
actionRouter.set(ASK_INGREDIENTS_ACTION, getIngredients);
assistant.handleRequest(actionRouter);
});
module.exports = app;
DialogflowApp сделает всю грязную работу за нас. Нам остается только подготовить данные для вывода. А теперь представьте, как это облегчает работу, когда нам нужно настроить заказ пиццы, вывод меню или статуса заказа, поиск ближайшей пиццерии и еще пару команд. Сколько человеко-часов экономим этой технологией!
Первичное тестирование ответов мы можем провести сразу в личном кабинете.
![image](https://habrastorage.org/webt/ze/ji/i6/zejii6hjgos03bljzrgpm2j3g_q.png)
Для более скрупулезного тестирования есть симулятор или девайс с Google now.
![image](https://habrastorage.org/webt/lv/5r/md/lv5rmdg6c_3qv7b4dvbsafnaxha.png)
Ответ в Google Assistant может состоять не только из текста, но и разных ui элементов: кнопки, карточки, карусели, списки.
![image](https://habrastorage.org/webt/ji/ti/-v/jiti-vhkvyulggfiqesopdkva9c.png)
На этом стоит остановиться. Дальнейшие тонкости технологии — материал на несколько статей. Те основы, которые были рассказаны сейчас, уже дают огромную пользу в построении своего приложения для Google Assistant. Закон Парето в действии.
Apple SiriKit. Кратко о том, почему Siri отстает
Если Диалоги — это базовая комплектация автомобиля, а Actions on Google — полная комплектация, то SiriKit — это метро с двумя станциями на всю Москву.
Две особенности, которые делают подход Apple не таким, как все — привязанность к основному приложение и обязательное соответствие одному из сценариев использования, прописанных Apple, то есть полное отсутствие кастомизации разговора. По первому пункту все понятно — без основного приложение на девайсе не будет диалога в Siri. Ваш диалог лишь дополнение к приложению.
Второй пункт является главным недостатком SiriKit. Все диалоги, весь текст уже прописан. Можно только добавить немного синонимов в вокабуляр Siri или сверстать виджет, который появится по запросу. Это единственная свобода, которую дает Apple.
Вам повезло, если вы хотите сделать что-то похожее на команды из скриншота ниже. Нам не повезло.
![image](https://habrastorage.org/webt/oi/uu/b6/oiuub6ywamiy0xoqha7gnclphzo.png)
Если на WWDC 2018 Apple не поменяет кардинально подход к кастомным диалогам, то тогда Siri так и останется внизу топа. Голосовые помощники — это операционные системы будущего. Систему делают крутой приложения. Когда их нельзя нормально сделать, система проигрывает. Именно из-за этого IOS в топе. Именно из-за этого Siri отстает в гонке.
Экспертное мнение. Про Amazon Alexa, продакшн и будущее.
Думаю, что голосовая разработка на нашем рынке в ближайшее время перейдет из развлекательной штуки во что-то серьезное, в продакшн. Точкой отсчета, скорее всего, будет официальный анонс русскоязычного Google Assistant, то есть Google I/O 2018. Надо морально подготовиться и поучиться у западных коллег. Расспросил нашего друга, Максима Кокоша, Team Lead-а из Omnigon. Он работал с Assistant и Alexa.
![image](https://habrastorage.org/webt/la/d6/yj/lad6yjpzp0sg19bw2pr0dz01by4.jpeg)
Максим Кокош, Team Lead Omnigon
Расскажи в общих словах, что ты разрабатывал?
Я занимался доработкой одного скилла для Alexa и портированием другого c Alexa на Actions on Google при использовании DialogFlow. Причем в очень сжатые сроки, неделя была на портирование, неделя на доработку Alexa скила.
Про Амазоновский проект мы ничего не знаем.
Вот ты в статье написал про Алису, Siri, Google Assistant, а про Alexa нет. Это как сравнивать Android и Symbian и забыть про iOS.
Alexa — главный конкурент Google. Как показывает продакшн, пользователей у нее значительно больше. Комьюнити сильно больше. Документации больше. Да и самих скиллов тоже заметно больше.
Кстати, я бы не стал подход Алисы называть автомобилем. Двухколесная повозка на ослиной тяге, это максимум. По сравнению с Google Actions и Alexa, там все совсем плохо. Парсить руками string'и в 2018 году звучит как дикость.
Как ты думаешь, почему аудитория у Alexa намного больше?
Мне кажется, это из-за того, что Google позже вступил в игру. Очень мало вкладывается в рекламу. Хотя судя по тому, что ассистент есть или может быть установлен почти на каждом Android устройстве, они могли бы стать популярней.
В чем особенности Alexa?
В Alexа удобно работать с состоянием в рамках сессии. Например, ты просишь включить свет в ванной. Получается Intent «Включить свет», а entity — ванная. Затем ты говоришь: «Выключить». И вот тут нам пригодится контекст внутри сессии. Во время обработки интента мы можем выставить состояние «bathroom» и использовать его при получении следующих интентов. У Google есть Follow-up интенты, отвечающие аналогичным целям, но они не такие гибкие.
У Alexa явно говорится, как установить скиллы. Это знакомый пользователю подход — магазин скиллов. У Google скилы ставятся автоматически.
Review процесс очень строгий на обеих платформах. Ревьюверы следят, чтобы каждый response заканчивался точкой, чтобы взаимодействие с пользователем выглядело натурально, для этого у каждой платформы есть свои гайдлайны, чтобы не было грамматических ошибок, даже в описании, а текста там очень много. Ревью от Amazon обычно занимало 2-3 дня, в Google справились за 1 день.
Сама разработка Google Actions показалось более простой: захостил на Firebase action, подключил его в 2 клика, и вот у тебя уже все готово к разработке. Если ты хочешь делать запросы наружу, нужно платить. Если будешь обращаться только к сервисам Google, то можно и бесплатно. AWS, ввиду большей загруженности, выглядит запутанней.
У Google можно ботов делать при помощи Google таблиц, функциональность весьма ограничена, однако позволяет писать ботов без скилов программирования и подойдет для мелких задач.
DialogFlow также в теории позволяет в один клик подключать твоих ботов к слаку, Telegram, Cortana и тому подобному. Там куча интеграций, правда коллаборация с Alexa не работает.
![image](https://habrastorage.org/webt/db/tx/5u/dbtx5u2ixum14bizbnmlcx_iqy4.png)
В принципе, имея знания об Actions on Google, можешь работать с Alexa.
Мы с тобой люди из мобильной сферы. Отличаются ли бизнес-процессы мобильной и голосовой разработки?
На мой вкус, процесс весьма похож, интенты можно рассматривать как экраны, entity — как данные в них, также есть система состояний. С точки зрения тестирования тоже все похоже: требуется корректная обработка случаев с потерей интернета, неожиданных респонсов от API и т.д.
Может ли разработка диалогов стать такой же популярной, как сейчас мобильная разработка?
Мне кажется, ассистенты не смогут достичь популярности мобильных приложений, ибо не все сценарии можно переложить на устное взаимодействие + с экрана информация воспринимается намного быстрее, чем на слух. Также не всегда у пользователя есть возможность устно вести диалог с ассистентом, особенно когда это касается sensitive данных. Голосовые ассистенты могут быть достаточно утилитарны, например, «закажи такси» или «закажи пиццу», но надолго увлечь пользователя вряд ли удастся. Это, скорее, эффектное дополнение к дому или автомобилю.
Какое будущее у голосовых ассистентов?
Голосовые ассистенты займут свою нишу и достаточно большую. И станут таким же обычным делом, как Android Auto и CarPlay среди машин. Уже продано 20 миллионов Amazon Echo девайсов и 4.6 миллиона Google Home девайсов. Не стоит забывать, что множество Android-телефонов оборудованы Google Assistant.
Макс, спасибо большое за подробные ответы.
Надеюсь, они хоть как-то помогут. :)
Что нас ждет впереди
Всем диалоговым платформам есть куда расти, потолок еще далеко. Давно не было интриги в битве за наш голос.
Полезные ссылки
Комментарии (11)
vanxant
07.04.2018 21:47В связи с этим вопрос не совсем по теме. В смысле, не только про текст, полученный от голосовых помощников, а вообще из произвольного источника.
Так вот, есть ли «пониматоры» смысла текста для русского языка продвинутее String.contains()? Какая-то либа, куда можно залить список своих ключевых фраз с синонимами, а она будет обрабатывать текст и выдавать: такая-то фраза есть в утвердительном контексте («пицца с морепродуктами»), такая-то в отрицательном («точно не гавайская», «без ананасов»), такая-то фраза в неопределённом («может быть» и т.п.)?maxbach Автор
07.04.2018 23:23Под ваш запрос как раз подходит DialogFlow. Это не просто технология для Google Assistant, его можно настроить под любые ваши прихоти. Есть подобные технологии от Microsoft и Amazon. Думаю, у Apple есть что-то подобное
vanxant
08.04.2018 01:07как раз подходит DialogFlow
Как раз не подходит, ибо нет русского языка (или я ошибаюсь)? Может объявят, а может и нет — это как бы не «поддержка».
Ни МС, ни Амазон тоже не умеют в русский(alex4321
08.04.2018 01:48+1или я ошибаюсь
Именно так.
alex4321
08.04.2018 01:56Другой вопрос — насколько годный классификатор под капотом — сможет ли он годно отличить такие фразы.
vanxant
08.04.2018 02:10Да понятно, что мегамозг еще не придумали, но с другой стороны альтернатива у нас всё ещё String.contains и велосипеды на регекспах. А задачи, пусть и очень утилитарные, решать надо вот «прям завтра».
alexey-m-ukolov
08.04.2018 09:27Использую DialogFlow для персонального бота — никаких проблем с распознаванием пока не встречал, прекрасно меня понимает и вычленяет даты-числа-сущности из произвольного текста. Разумеется, предварительно пришлось его обучить языку предметной области, но это сделать довольно легко и интерфейс удобный.
chetl33
07.04.2018 23:26У Google Assistant вроде бы есть время ожидание ответа = 5-12 секунд(зависит от настроек). Как его увеличить?
Есть ли возможность вызвать Amazon Echo, Алису, Google Home программно? Например произошло событие, колонка включилась и что-то сказала.
Singaporian
Спасибо за статью! Отличная.