SpeechMarkup API — превращаем речь в данные
В статье пойдет речь о том, как из любого запроса на естественном языке получить реальные данные, с которыми может работать ваше приложение. А именно, о 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 + ' ' + '' + token.substring + ''); 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.
Как мы уже видели, 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+