В статье пойдет речь о том, как из любого запроса на естественном языке получить реальные данные, с которыми может работать ваше приложение. А именно, о REST API сервиса SpeechMarkup, который преобразует обычную строчку текста в JSON со всеми найденными смысловыми сущностями с конкретными данными в каждой из них.
Да-да, это та самая технология, которая лежит в основе любого голосового ассистента и используется в поисковиках.
Она позволяет однозначно интерпретировать запрос и «понять», о чем говорит пользователь, а затем вернуть вашему приложению результат в виде обычного набора данных.
В статье я расскажу, для чего можно использовать данный API и приведу небольшой пример работающего приложения.
Для чего нам это?
Сегодня все пользовательские интерфейсы становятся все более минималистичными и простыми. Действительно, чем проще интерфейс, тем быстрее и комфортнее будет пользоваться вашим сервисом или приложением.
И вместо того, чтобы предлагать пользователю сложные формочки, в которых нужно переключаться между полями, что-то набирать, где-то что-то выбирать и т.д., бывает проще и удобней ввести несколько слов в одном поле.
Более того, например в Андроиде в любой момент можно нажать на микрофончик и произнести те данные, которые не хочется/неудобно/долго вбивать руками. В iOS ситуация с голосовым вводом тоже улучшилась в связи с поддержкой русского в диктовке. Уже сегодня ничего не мешает прикрутить голосовой ввод к своему приложению, поставить роботов в колл-центр или даже создать собственного голосового ассистента для умного дома.
Но даже если не брать во внимание распознавание речи (ситуация с которым хоть и далека от идеала, но улучшается год от года), то можно сказать, что во многих случаях замена форм на единственное поле с обычным текстовым вводом поможет сделать сервис более удобным и понятным.
Написал/сказал пользователь, скажем, «Два билета питер москва завтра утром», и ваш сервис тут же выдал подходящие рейсы! Или «В субботу в 6 вечера футбол» — и событие сохранилось в календаре! «Михалыч прийди завтра утром на работу пораньше» — и нужному контакту ушла sms, или назначилась задача в трекере задач (а лучше — и то и то).
Но не все так просто...
Ну хорошо, текст мы получили от пользователя (или от какой-нибудь системы распознавания речи), и что дальше с ним делать? Правильно — нужно просто выдернуть из него необходимые для нашего сервиса данные и все! Например, дату и время рейса, город отправки и прибытия. Или дату-время и текст напоминания.
Ну как просто… Выясняется, что довольно непросто…
С учетом того, что это естественный язык, с присущими ему особенностями, такими как морфология, произвольный порядок слов, ошибки распознавания и т.п., задача правильной интерпретации даже небольшого предложения в 5-10 слов становится действительно сложной.
Скажем, дату можно указать как абсолютную, так и относительную — "послезавтра" или "через два дня", "Второго декабря" или "В субботу". С временем — то же самое. А числа могут быть указаны и с помощью цифр и словами! У городов есть синонимы (Питер, Санкт-Петербург, Ленинград), их можно записать с дефисом и без (Нью-Йорк). А понять, что подстрока — это ФИО, а две рядом стоящие фамилии — это разные люди, еще сложнее…
Вам хочется решить это с помощью регекспов? Или копаться в премудростях NLP, мат-лингвистики, теории ИИ и т.п.? Вот и мне не хочется. Потому что мне нужно всего-лишь вытащить из строчки пару данных, которые необходимы логике моего приложения.
Что же делать?
Читать дальше
Потому что именно для решения этой задачи и нужен такой API как SpeechMarkup.
По сути он не выполняет распознавания речи. Он получает на вход обычную строчку, которую затем превращает в JSON, где указаны все сущности, приведенные к нужному формату. Скажем, «Через пять минут» превратится в «18:15», «В субботу» — в «15.11.2014» и т.д.
А точнее — вот пример ответа
{
"string": "через неделю васе пупкину из питера исполняется пятьдесят два года",
"tokens": [
{
"type": "Date",
"substring": "через неделю",
"formatted": "17.11.2014",
"value": {"day": 17, "month": 10, "year": 2014}
},
{
"type": "Person",
"substring": "васе пупкину",
"formatted": "Пупкин Вася",
"value": {"firstName": "Вася", "surName": "Пупкин"}
},
{ "type": "Text", "substring": "из", "value": "из" },
{
"type": "City",
"substring": "питера",
"value": [{"lat": 59.93863, "lon": 30.31413, "population": 5028000, "countryCode": "RU", "timezone": "Europe/Moscow",
"id": "498817", "name": "Санкт-Петербург"}]
},
{ "type": "Text", "substring": "исполняется", "value": "исполняется" },
{
"type": "Number",
"substring": "пятьдесят два",
"value": 52
},
{ "type": "Text", "substring": "года", "value": "года" }
]
}
Как видите, SpeechMarkup как бы «размечает» исходный текст данными, которые может найти, и возвращает в том же порядке, в котором они идут в тексте.
То есть, наше приложение может отправить строчку и получить обратно обычный JSON, где каждая сущность имеет свой тип и определенный формат, независимый от языка исходного запроса! Как написано в документации по REST API SpeechMarkup, на данный момент поддерживаются сущности типа дат, времени, чисел, городов и ФИО. Ну а все остальное помечается как обычный текст.
Важно отметить, что SpeechMarkup не работает с контекстом запроса. Другими словами, это задача конкрентного сервиса интерпретировать данные, полученные из текста. То есть, если вашему сервису не интересны, скажем, сущности ФИО, то он может игнорировать их разметку и работать с ними как с обычной строкой, если она ему нужна. Как это происходит — покажу на простом примере.
Простой пример приложения
В качестве примера использования API возьмем демо-проект, реализующий функциональность сервиса напоминаний. Конечно, использовать REST API может любое приложение на любой платформе, написанное на любом языке программирования, т.к. все что нужно — это отправить HTTP запрос с текстом и несколькими параметрами и получить обратно JSON. В данном примере мы используем JavaScript.
Итак, что делает наш тестовый сервис напоминаний? Сохраняет напоминания. Все что нужно от пользователя — это ввести текст, который затем будет интерпретирован, и если в нем есть все данные, то он превратится в напоминалку. Если в тексте присутствует чье-то имя, то оно дополнительно подсвечивается в элементе списка. Можно попробовать покликать на примеры.
Давайте посмотрим на ту часть JavaScript кода, которая отправляет текст запроса и получает обратно ответ, из которого конструирует элемент списка с данными о дате, времени и тексте напоминания.
Отправка текста с параметрами
$('#form').bind('submit', function(event) {
event.preventDefault();
var val = $.trim(text.val());
if (val) {
var date = new Date();
$.ajax({
url: 'http://markup.dusi.mobi/api/text',
type: 'GET',
data: {text: val, timestamp: date.getTime(), offset: date.getTimezoneOffset()},
success: onResult
});
}
return false;
});
Тут все просто. Когда пользователь отправляет форму, берем значение поля с текстом и отправляем его методом GET на
http://markup.dusi.mobi/api/text
Еще 2 дополнительных параметра нужны для правильного преобразования дат и времени из текста на стороне сервера SpeechMarkup. Это параметр timestamp, который представляет собой текущую дату-время клиента в миллисекундах, и параметр offset, содержащий смещение времени UTC в минутах. Их важно указывать, т.к. иначе сервер SpeechMarkup не узнает, что для клиента значит, например, «через 5 минут».
А вот так выглядит код, обрабатывающий ответ
function onResult(data) {
var resp = JSON.parse(data);
var item = createItem(resp);
if (!item.text) {
warning$.text('А что напомнить?');
} else if (!item.time) {
warning$.text('А во сколько напомнить?');
} else {
warning$.empty();
if (!item.date) {
item.datetime = moment();
if (item.time.value.hour < item.datetime.hour()) {
if (!item.time.value.part && item.time.value.hour < 12 && item.time.value.hour + 12 > item.datetime.hour()) {
item.time.value.hour += 12;
} else {
item.datetime.add(1, 'd');
}
}
item.datetime.hour(item.time.value.hour).minute(item.time.value.minute);
} else {
item.datetime = moment([item.date.value.year, item.date.value.month, item.date.value.day,
item.time.value.hour, item.time.value.minute]);
}
items.push(item);
appendItem(item, items.length - 1);
text.val('');
}
}
Так как мы работаем с датами и временем, то удобно воспользоваться библиотекой Moment.js.
Здесь немного больше кода, но он тоже простой, и что самое главное — он не оперирует текстом, не парсит его, а работает с уже готовыми данными, которые сформировал SpeechMarkup.
В этом коде мы пытаемся по имеющимся данным сконструировать напоминание. А именно, если не указан текст или время, то сказать об этом. А если все есть кроме даты, то понять по указанному времени, на какую дату создать напоминание.
В начале метода вы видели вызов createItem, который из ответа собирает объект для манипуляций. Вот его код
function createItem(resp) {
var tokens = resp.tokens;
var item = {text: tokens.length > 0 ? '' : resp.string};
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
switch (token.type) {
case 'Person': item.text = $.trim(item.text + ' ' + '<span class="label label-warning">' + token.substring + '</span>');
break;
case 'Date': item.date ? item.text = $.trim(item.text + ' ' + token.substring) : item.date = token;
break;
case 'Time': item.time ? item.text = $.trim(item.text + ' ' + token.substring) : item.time = token;
break;
default: item.text = $.trim(item.text + ' ' + token.substring);
}
}
return item;
}
Собственно это та часть, которая разбирает ответный JSON от сервера и либо добавляет какие-то сущности к тексту напоминания, либо к дате или времени.
Чтобы полностью понять, что такое token или substring, пройдемся немного по API SpeechMarkup.
API SpeechMarkup
Как мы уже видели, SpeechMarkup принимает на вход строчку и несколько дополнительных параметров, а на выходе отдает JSON с исходной строкой (поле string) и массивом найденных сущностей (поле tokens). Если массив пуст, значит специфических сущностей не найдено и все является обычным текстом (не забываем, что SpeechMarkup работает с определенным набором сущностей, которые в скором времени можно будет дополнять своими собственными).
Каждый token (сущность) — это объект, в котором указывается тип сущности (поле type), часть строки, к которой она относится (substring) и преобразованное конечное языконезависимое значение (value). Для типа Text это поле содержит саму подстроку.
Также может присутствовать необязательное поле formatted для компактного представления данных. Например, дата будет записана в формате «DD.MM.YYYY», время — «HH:mm:ss», а тип Person — в виде «Фамилия Имя Отчество».
Каждый тип сущности имеет свой формат значения в поле value. Для дат это объект с полями day, month и year. Для времени — hour, minute, second.
Для городов это не объект, а массив (т.к. существует много городов с одинаковым названием). В каждом городе есть координаты, численность населения, код страны и стандартное название.
В сущности типа Person (ФИО) есть поля firstName, surName и patrName, некоторые из которых могут отсутствовать, если пользователь указал, например, только имя.
Опираясь на эти данные, можно идти по всем токенам по порядку (т.к. они идут точно в том порядке, в котором указаны в изначальном тексте) и в зависимости от типа сущности и его значения, применять ту или иную логику.
Если в тексте время встречается несколько раз, то все кроме первого добавляются к тексту. То же и с датами. Если в тексте есть имя, то оно дополнительно выделяется в тексте.
В итоге
SpeechMarkup предлагает бесплатный API для разметки сущностей в запросах на естественном языке, что позволяет вашему приложению интерпретировать в том числе и речь, и обычный текстовый ввод. Со временем пользователи API также смогут создавать свои собственные сущности и логику их преобразования в данные, что позволит создавать обработчики более специфичных запросов.
Вот несколько ссылок, которые помогут узнать о проекте больше и быть в курсе нововведений:
Сайт проекта SpeechMarkup
Документация на GitHub
Сообщество разработчиков в Google+
Автор: morfeusys