SpeechMarkup API — превращаем речь в данные

в 20:27, , рубрики: api, natural language processing, nlp, исскусственный интелект, Программирование, разработка, распознавание речи

SpeechMarkup API — превращаем речь в данные
В статье пойдет речь о том, как из любого запроса на естественном языке получить реальные данные, с которыми может работать ваше приложение. А именно, о REST API сервиса SpeechMarkup, который преобразует обычную строчку текста в JSON со всеми найденными смысловыми сущностями с конкретными данными в каждой из них.

Да-да, это та самая технология, которая лежит в основе любого голосового ассистента и используется в поисковиках.
Она позволяет однозначно интерпретировать запрос и «понять», о чем говорит пользователь, а затем вернуть вашему приложению результат в виде обычного набора данных.

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

Для чего нам это?

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

Более того, например в Андроиде в любой момент можно нажать на микрофончик и произнести те данные, которые не хочется/неудобно/долго вбивать руками. В iOS ситуация с голосовым вводом тоже улучшилась в связи с поддержкой русского в диктовке. Уже сегодня ничего не мешает прикрутить голосовой ввод к своему приложению, поставить роботов в колл-центр или даже создать собственного голосового ассистента для умного дома.

SpeechMarkup API — превращаем речь в данные
Но даже если не брать во внимание распознавание речи (ситуация с которым хоть и далека от идеала, но улучшается год от года), то можно сказать, что во многих случаях замена форм на единственное поле с обычным текстовым вводом поможет сделать сервис более удобным и понятным.
Написал/сказал пользователь, скажем, «Два билета питер москва завтра утром», и ваш сервис тут же выдал подходящие рейсы! Или «В субботу в 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 не работает с контекстом запроса. Другими словами, это задача конкрентного сервиса интерпретировать данные, полученные из текста. То есть, если вашему сервису не интересны, скажем, сущности ФИО, то он может игнорировать их разметку и работать с ними как с обычной строкой, если она ему нужна. Как это происходит — покажу на простом примере.

Простой пример приложения

SpeechMarkup API — превращаем речь в данные
В качестве примера использования 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js