https://habrahabr.ru/post/346334/- Занимательные задачки
- Вконтакте API
- Node.JS
- JavaScript
- API
Привет, хабр. Обычно я пишу программы для неговорящих людей, но решил ударится в крайности и сделать продукт для говорящих людей. Я хочу рассказать о разработке бота для VK, который переводит пересланные ему голосовые сообщения в текст. Сначала я использовал
Yandex SpeechKit, но потом уперся в дневной лимит распознаваемых единиц и перешел на
wit.ai, об этом и хочу рассказать, а также о фреймворке для создания ботов vk с помощью node.js, болтовне гугловского
dialogflow.
0. ТЗ
Сделать бота для вк на node.js, который принимает голосовые сообщения, прямые или же пересланные, отправляет их в api распознавания речи и отвечает пользователю распознанным текстом. В ответ на текстовые сообщения бот должен поддерживать диалог с помощью сервиса google.
1. Стэк
Для работы с api vk я взял библиотеку
node-vk-bot-api, ссылку на которую нашел в официальной документации ВКонтакте, хотя у того же автора есть более крутая библиотека с поддержкой webhooks
botact.
Для работы с 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'а для обработки нативной речи для создания ботов с искусственным интеллектом, которая принимает на вход текстовую или голосовую фразу на естественном языке и пробует ответить с помощью нейросетки. Я использую лишь голосовое распознавание, так сказать штырюсь от побочки.
Платформа не имеет никаких лимитов, кроме ограничения в виде одного запроса в секунду, и то отслеживается самой платформой и запрос просто ставится в очередь.
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();
})
}
В консоле dialogflow включим и настроим готовый модуль Small Talk из агентов по умолчанию.Модуль попросит кастомизировать себя с помощью добавления тематических фраз, это займет много времени.Также необходимо включить 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()
Не самый чистый код, есть много косяков и глупостей, но всё же я надеюсь, что рассказал о полезных вам технологиях и вдохновил, что либо сделать с их помощью.
Результат:
Поскольку тестить бота сам не мог, не говорю, использовал другого бота, который переводил текст в речь.