javascript

Безлимитное распознавание речи. Или как я перевожу в боте голосовые сообщения в текст

  • среда, 10 января 2018 г. в 03:14:10
https://habrahabr.ru/post/346334/
  • Занимательные задачки
  • Вконтакте API
  • Node.JS
  • JavaScript
  • API


Привет, хабр. Обычно я пишу программы для неговорящих людей, но решил ударится в крайности и сделать продукт для говорящих людей. Я хочу рассказать о разработке бота для VK, который переводит пересланные ему голосовые сообщения в текст. Сначала я использовал Yandex SpeechKit, но потом уперся в дневной лимит распознаваемых единиц и перешел на wit.ai, об этом и хочу рассказать, а также о фреймворке для создания ботов vk с помощью node.js, болтовне гугловского dialogflow.

Обложка статьи на которой показан скришот сайта wit.ai



0. ТЗ



Сделать бота для вк на node.js, который принимает голосовые сообщения, прямые или же пересланные, отправляет их в api распознавания речи и отвечает пользователю распознанным текстом. В ответ на текстовые сообщения бот должен поддерживать диалог с помощью сервиса google.

1. Стэк



Для работы с api vk я взял библиотеку node-vk-bot-api, ссылку на которую нашел в официальной документации ВКонтакте, хотя у того же автора есть более крутая библиотека с поддержкой webhooks botact.

Скриншот из документации vk

Для работы с Google Dialogflow был взят пакет apiai.

Поскольку изначально предполагалось использовать api Яндекса, готовую библиотеку, которого я не нашел, файл для работы с голосовым api написал сам, используя модуль request-promise.

2. Разделение проекта



Комментарии в коде русские только в статье, так то я // true programmer

2.0 asr.js



В этом файле мы опишим функцию получания текста из буфера с аудиофайлом:
const fs = require('fs')
const request = require('request-promise')

const uri = `https://api.wit.ai/speech`
const apikey = '<...>' // получаем ключ доступа на wit.ai

module.exports = async (body) => {
  // отправляем post запрос с буфером аудио
  const response = await request.post({
    uri,
    headers: {
      'Accept': 'audio/x-mpeg-3',
      'Authorization': `Bearer ` + apikey,
      'Content-Type': 'audio/mpeg3',
      'Transfer-Encoding': 'chunked'
    },
    body
  })
  // парсим ответ и отдаем результат расшифровки
  return JSON.parse(response)._text
}


Вообще wit.ai — это платформа facebook'а для обработки нативной речи для создания ботов с искусственным интеллектом, которая принимает на вход текстовую или голосовую фразу на естественном языке и пробует ответить с помощью нейросетки. Я использую лишь голосовое распознавание, так сказать штырюсь от побочки.

Скриншот из документации wit.ai

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

1. assistent.js



В этом файле мы опишем работу с платформой dialogflow, которая будет отвечать на текстовые сообщения. Почему не wit.ai? У этого две причины. Во-первых, проект развлекательно-развивающий, соответственно целью было попробовать как можно больше технологий, во-вторых изначально использовался speechkit, только после блокировки из-за дневного лимита, было принято экстренное решение перейти на wit.

Код весьма банальный:

const apiai = require('apiai');
// для каждого пользователя будем использовать уникальный идентификатор сессии, но из-за того, что нам лень поднимать дб, мы будем его генерировать из 36 битного ключа, заменяя последние символы на id пользователя
// не делайте так
const uniqid = 'e4278f61-9437-4dff-a24b-aaaaaaaaaaaa';
const app = apiai(‘<...>’);
module.exports = (q, uid) => {
  // городим свой промис, так как библиотека стремная
  return new Promise((resolve, reject) => {
    uid = uid + ''
    // don’t touch, magic
    const cuniqid = uniqid.slice(0, uniqid.length - uid.length) + uid
    // запрос
    const request = app.textRequest(q, {
      sessionId: uniqid
    })
    // удача!
    request.on('response', (response) => {
      resolve(response)
    })
    // нееееееет!
    request.on('error', (error) => {
      reject(error);
    })
    // не знаю зачем, но надо
    request.end();
  })
}


Скриншот из настроек платформы с включенным пунктом Small Talk

В консоле dialogflow включим и настроим готовый модуль Small Talk из агентов по умолчанию.

Скриншот из настроек, добавление ответов на фразы Я очень зол, Я вернулся и мне скучно

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

Скриншот настройки Default FallBack Intent

Также необходимо включить Default FallBack Intent, который будет отвечать в случае непонятных запросов.

В итоге получается такая вот беседа:

Скриншот из переписки с ботом

2. index.js



Собираем всё это в главном файле.

const vk = require('api.vk.com')
const request = require('request-promise')
const Bot = require('node-vk-bot-api')

const asr = require('./asr')
const assistent = require('./assistent')
const token = require('./token');

// оборачиваем в промис старенький модуль для работы с api
const api = (method, options) => {
  return new Promise((resolve, reject) => {
    vk(method, options, (err, result) => {
      if (err) return reject(err)
      resolve(result)
    })
  })
}

// создаем экземпляр бота
const bot = new Bot({ token })

// вешаем событие на входящие сообщение
bot.on(async (object) => {
  // для простоты создадим один catcher ошибок, и при появлении онных будем отвечать пользователю о неудаче
  try {
    //асинхронно помечаем прочитанность и устанавливаем статус печатанья
    api('messages.setActivity', { access_token: token, type: 'typing', user_id: object.user_id })
    api('messages.markAsRead', { access_token: token, message_ids: object.message_id })
    
//переменная, в которую попадет uri аудиосообщения
    let uri

    // проверяем наличие прикрепленных сущностей 
    if (object.attachments.length != 0) {
      // получаем подробную информацию о сообщении
      const [msg] = (await api('messages.getById', { access_token: token, message_ids: object.message_id, v: 5.67 })).items
      // проверяем, прикреплена ли вообще аудио запись
      if (msg.attachments[0].type != 'audio' || msg.attachments[0].type != 'doc' || msg.attachments[0].doc.type != 5) {
        uri = null
      }
      // выковырываем uri
      try {
        if (msg.attachments[0].type === 'doc') uri = msg.attachments[0].doc.preview.audio_msg.link_mp3
        else if (msg.attachments[0].type === 'audio') uri = msg.attachments[0].audio.url
      } catch (e) {
        uri = null
      }

      // обрабатываем пересланное голосовое сообщение
    } else if (object.forward != null) {
      try {
        uri = object.forward.attachments[0].doc.preview.audio_msg.link_mp3
      }
      catch (e) {
        uri = null
      }
    } else {
      uri = null
    }
    // ошибка авторских прав на аудиозапись
    if (uri === '') {
      throw ('Не могу послушать это аудио')
    }
    if (uri == null) {
      // если есть текстовое cообщение, отправляем в dialogflow
      if (object.body != '') {
        const { speech } = (await assistent(object.body, object.user_id)).result.fulfillment
        object.reply(speech)
        return
      }
      else {
        throw ('Мы не нашли голосовых сообщений. Отправь нам свой голос или перешли его')
      }
    }
    // получаем буфер с аудио
    const audio = await request.get({ uri, encoding: null })
    let phrase
    try {
      phrase = await asr(audio)
    }
    catch (e) {
      console.error('asr error', e)
      throw ('сбой в распознавании речи')
    }
    // отправляем результат
    if (phrase != null) {
      object.reply(phrase, (err, mesid) => { });
    } else {
      throw ('Не удалось распознать')
    }
  } catch (error) {
    // логируем ошибку и отвечаем пользователю
    console.error(error)
    object.reply('string' == typeof error ? error : 'Произошла непонятная ошибка. Я не знаю, что делать!')
  }
})

// не забываем про главное, включаем long pong.
bot.listen()


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

Результат:


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

Скриншот, где бот переводит пересланное ему голосовое сообщение от другого бота