Рецепт розробки бота під Telegram



Добрий день, шановні читачі Хабрахабра!

В цьому топіку я хочу поділитися з вами досвідом розробки бота під Telegram за 4 дні. Цей бот переводить всі голосові повідомлення, які отримує, в текст. Намагався зробити швидко, але якісно — підучив пару-трійку технологій. Постараюся максимально детально описати свій процес подолання помилок і перешкод; довести, що, навіть не маючи потрібних навичок, запустити свій продукт не так-то й складно.

Стаття може бути цікава як новачкам у програмуванні — побачити, скільки перешкод стоять на шляху у готового продукту, так і більш просунутим фахівцям — десь посміятись, десь поплакати, десь написати коментар «життєво».

Преамбула
І так, що ж може зробити один програміст за 4 дні?

Написати модуль, написати один екран iOS додатки, написати 15% складного бота під Telegram, підняти або опустити MMR в Dota2 або ранг у Overwatch. Може провести вихідні на природі, сходити в тренажерний зал, поплавати в басейні, написати 500-1000 рядків коду програми для клієнтів, яким ніколи і ніхто, крім друзів і родичів замовника, користуватися не буде. Може змотатися в Кремнієву Долину на останні гроші, подивитися пару-трійку серіалів, познайомитися з безліччю людей на одній-двох конференціях, повтикать в документацію Асемблера.

А що, якщо я скажу, що все це — повна нісенітниця? Що, якщо я вам відкрию секрет: все не так-то просто. Час — штука відносна, можна міряти її різними величинами і коефіцієнтами; мій улюблений критерій — ставлення до часу користь. Що, якщо я вам скажу, що програміст може запустити новий продукт на ринок за 4 дні? Заінтриговані? — Читайте далі.

Мотивація
До нас у спільноту програмістів в Telegram нещодавно додався «наш власний ивангай» і почав дико розкидатися направо й наліво голосовими повідомленнями (хто його знає, раптом хлопцеві просто складно писати пальцями по клавіатурі). Природно, людям набридло слухати по два-три години на добу повідомлення, щоб вникнути в суть розмови — і тут хтось запропонував створити бота, який автоматично переводив би все войсы в текст. Це саме той момент, коли загоряються очі.

Переклад голосу в текст
Я не знав, скільки часу займе у мене розробка такого бота, але вже мав достатньо досвіду за плечима з ботами з нашого теплого затишного чатика і фріланс-біржою в Telegram з відкритим вихідним кодом. Не довго думаючи, сів за розробку — етап підключення модулів Telegam Bot API пройшов, як по маслу. Що далі? Модуль розпізнавання мовлення — гуглим «voice recognition api» і отримуємо серед перших посилань список кращих сервісів. На чолі стоїть Google Speech API в бета-версії — його і візьмемо.

Отримуємо всі голосові повідомлення ботом і як-то направляємо в бік Google. Але, от невдача: майже всі npm модулі роботи з цим сервісом або застаріли, або банально не працюють. Пробуємо використовувати вбудоване API під Node.js написане фахівцями Google — працює. Що там за аутентифікації? Випробувавши безліч варіантів документації від Google, яка, до речі кажучи, теж 50 на 50 застаріла або не працює — і ось воно, ключики підійшли.

До речі, ось сам код, який я використовував:

Тисни мене!
const Speech = require('@google cloud/speech');

const speech = new Speech({
projectId: 'voicy-151205',
credentials: require('path/to/certificate/file.json')
});

speech.startRecognition(filepath, {
'encoding': 'LINEAR16',
'sampleRate': 16000,
'languageCode': 'en-US',
})
.then((results) => {
const operation = results[0];
return operation.promise();
})
.then((transcription) => {
console.log(transcription[0]);
})

Аудіо формати і обмеження Google
Що там по відправці войсов до Google? Ага, потрібно отримати аудіофайли з Telegram і відправляти їх на сервера Speech API — пробуємо, не працює. В чому справа? Формат .oga, звичайний для Telegram, не приймається — потрібно декодувати. Що у нас є для конвертації медіа? Звичайно ж, ffmpeg, знайомий ще з раннього дитинства (спасибі татові, який коли-то давно змусив мене в ньому розбиратися). Як його підключити до ноде? Опа! Є npm модуль, заточений спеціально під це. В який формат потрібно конвертувати .oga файли? Виявилося, що Google бере .flac — конвертуємо, пробуємо, все приймається, Google відповідає текстом, успіх.

Але, не тут-то було! Google не переводить в текст файли довше 60 секунд, якщо не завантажувати їх на Google Cloud Storage сервіс. Що ж таке? Відразу ж пробуємо модуль, написаний програмістами Google, навіть не дивимося в бік застарілих npm готових рішень. Відмінно, файли завантажуються, обробляються сервісом, повертається текст. До речі, дивне у Google поняття часу — аудіофайли довжиною у 30 секунд, чомусь, визначаються, як 60-ти секундні. Нічого страшного — пробуємо файл довше 30 секунд і наступаємо на чергові граблі — Google приймає довгі файли у кодуванні «LINEAR16».

Що ще за «LINEAR16»? Шукаємо в документації ffmpeg — нічого подібного немає. Відмінно, як так? Працюємо далі. Шукаємо, що за звір цей формат або кодек — виявляється, це «16 bit signed little endian» дані. Добре, що я прочитав пару книжок по Computer Science і знаю, що таке «16 bit» і чому це «little endian». Шукаємо, що ж із себе представляє цей формат у ffmpeg — ага: «s16le»! Пробуємо конвертувати — виходить, Google приймає і відповідає текстом, «Mission accomplished».

Обережно: нижче код, який допоміг мені з конвертацією (потрібно попередньо встановити ffmpeg на машину)!

Тисни мене!
const ffmpeg = require('fluent-ffmpeg');
const temp = require('temp');

ffmpeg.ffprobe(filepath, (err, info) => {
const fileSize = info.format.duration;
const output = temp.path({ suffix: '.flac' });

ffmpeg()
.on('end', () => console.log(output))
.input(filepath)
.setStartTime(0)
.duration(fileSize)
.output(output)
.audioFrequency(16000)
.toFormat('s16le')
.run();
});

Монетизація
Але, що це таке? Будь $2 за використання Speech API? Вони там в Google зовсім з дуба впали? Як це ми використовували більше двох годин перекладу голосу в текст? Так зовсім не піде — додадуть мого бота у 100-1000 чатів, і що мені далі робити? З сніданків заощадити на підтримку бота вже не вийде — не той масштаб. Потрібно прикручувати оплату. Нехай буде 600 безкоштовних секунд у кожного чату, а далі будемо запитувати покриття вартості Google Speech API.

Як прикрутити оплату до боту в Telegram? Чітких інструкцій, як і інструментів монетизації в Telegram ще не завезли — потрібно якось вирішувати питання зовні. Який платіжний сервіс використовувати? Так-так-так, недавно читав про хлопця, який став наймолодшим мільярдером — той створив свій платіжний сервіс. Гуглим, бачимо — Stripe, і його використовуємо. Що це у нас? У них є зручний Checkout — але стандартну форму ми, звичайно, використовувати не будемо, зробимо свою.

Фронтенд
Куди дивитися, щоб робити інтерактивні сторінки? Я ж iOS програміст, нещодавно поринув у серверну розробку — так, ноги злегка сполоснути — а тут фронтенд наспів, що ж за напасть? Гуглим швиденько, які технології використовуються для створення інтерактивних сайтів. Angular 2, jQuery, Vanila.js — найпростішим виглядає jQuery, його і візьмемо — тим більше, я з ним давно вже розважався. Гуглим YouTube уроки по jQuery — один трешак по 2-3 години, будемо розбиратися по ходу справи, туториалам і відповідей на Stack Overflow.

Швиденько малюємо структуру сайту — лого, форму, пару рядків тексту і кнопку. Як це зробити? Щось я чув про Bootstrap. Гуглим, проходимо пару уроків по Bootstrap 3 — ще гуглим, прикручуємо картинку, форму, кнопочку — ніби як, навіть адаптивно вийшло. Міняємо фон сайту з білого на щось більш креативне (злегка сіруватий), от і сайтик готовий.

Час jQuery! Вирішуємо винести оплату в окремий проект — у командному рядку запускаємо «express payments», отримуємо проект. Відмінно, куди заносити скрипти? Як, прямо в сайт? Щось не так — і справді, можна винести їх в окремий файл, цим і займемося. Прикручуємо обробку помилок на окремий лейбл, перевіряємо форму Stripe (благо, вони дають весь потрібний інтерфейс в Checkout модулі), прикручуємо наш новий проект до тій же базі даних, перевіряємо оплату — проходить, все працює. По дорозі, звичайно, через відсутність звичним фронтенд-розробникам інструментів, міріади раз перезавантажуємо веб-сторінку вручну.

Ось тут файлики, які у мене вийшли (обережно, не дуже чистий код):

Тисни мене!index.hjs
<!DOCTYPE html>
<html>
<head>
<title>Voicy payments</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="/javascript/global.js"></script>
<script src="https://checkout.stripe.com/checkout.js"></script>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<img src="/images/logo.png" alt="Voicy" class="center-block">
<h1 class="center-block text-center">Chat ID:</h1>
<h1 class="center-block text-center">{{ chatId }}</h1>
<p class="text-center">{{ seconds }} seconds are left in this chat.</p>
<p class="text-center">You can buy more seconds below.</p>
<p class="text-center"><b>$0.4 per 200 seconds</b></p>
<form>
<div class="center">
<form class="form-inline" id="buy">
<div class="form-group">
<input type="hidden" name="chatId" value="{{ chatId }}">
<input type="number" class="form-control" name="numberOfSeconds" placeholder="Enter number of seconds">
<small id="infoLabel" class="form-text text-info"></small>
<small id="errorLabel" class="form-text text-danger"></small>
<small id="successLabel" class="form-text text-success"></small>
</div>
<button type="submit" class="btn btn-primary center-block" id="buyButton">Buy</button>
</form>
</form>
</form>
</body>
</html>

global.js
$(document).ready(function() {
var chatId;
var amount;

var handler = StripeCheckout.configure({
key: '***',
image: 'https://pay.voicybot.com/images/stripe.png',
locale: 'auto',
// alipay: true,
// bitcoin: true,
closed: function() {
$("#successLabel").empty();
$("#errorLabel").empty();
},
token: function(token) {
$("#infoLabel").empty();
$("#successLabel").empty();
$("#errorLabel").empty();
$("#infoLabel").append('Processing payment on Voicy servers...');
$.ajax({
type: 'POST',
url: 'buy',
data: { 'token': token.id 'chatId': chatId, 'amount': amount },
dataType: 'json',
encode: true
})
.done(function(data) {
if (data['error']) {
$("#infoLabel").empty();
$("#successLabel").empty();
$("#errorLabel").empty();
$("#errorLabel").append(data['error']);
} else {
$("#infoLabel").empty();
$("#successLabel").empty();
$("#errorLabel").empty();
$("#successLabel").append('Thank you for the payment!');
}
});
}
});

// Close Checkout on page navigation:
window.addEventListener. ('popstate', function() {
$("#infoLabel").empty();
$("#successLabel").empty();
$("#errorLabel").empty();
handler.close();
});

// process the form
$('form').submit(function(event) {
event.preventDefault();
var seconds = $('input[name=numberOfSeconds]').val();
chatId = $('input[name=chatId]').val();
if (!seconds || seconds < 200) {
$("#infoLabel").empty();
$("#successLabel").empty();
$("#errorLabel").empty();
$("#errorLabel").append('Please purchase at least 200 seconds');
} else {
var purch = seconds * 0.002 * 100;
amount = seconds;
$("#infoLabel").empty();
$("#successLabel").empty();
$("#errorLabel").empty();
$("#infoLabel").append('Please pay at Stripe Checkout');
handler.open({
name: 'Voicy Bot',
description: 'Purchasing' + seconds + 'seconds',
currency: 'USD',
amount: purch,
// alipay: true,
// bitcoin: true
});
}
});
});

index.js
const express = require('express');
const router = express.Router();
const db = require('../helpers/db');
const stripe = require("stripe")("***");
const bot = require('../helpers/bot');

/** Process purchase */
router.post('/buy', (req, res, next) => {
const token = req.body.token;
const chatId = parseInt(req.body.chatId);
const amount = parseInt(req.body.amount);

var charge = stripe.charges.create({
amount: amount * 0.002 * 100,
source: token,
currency: "USD",
description: "Buying seconds for Voicy"
}, (err, charge) => {
if (err) {
res.send({ error: err.message });
} else {
db.findChat(chatId)
.then((chat) => {
chat.seconds = parseInt(chat.seconds) + amount;
return chat.save()
.then((newChat) => {
res.send({ success: true });
reportPaymentToChat(newChat, amount);
});
})
.catch((err) => {
res.send({ error: err.message });
})
}
});
});

/* GET home page. */
router.get('/:id', (req, res, next) => {
const chatId = parseInt(req.params.id);
db.findChat(chatId)
.then((chat) => {
if (!chat) {
const err = new Error();
err.status = 404;
err.message = 'No chat found';
throw err;
}
return chat;
})
.then((chat) => {
res.render('index', { 
chatId: chat.id
seconds: chat.seconds,
});
})
.catch(err => next(err));
});


Головний вебсайт
Будь хорошому проекту потрібен добротний вебсайт — але мені щось зовсім не хочеться витрачати гроші на хостинг. Купуємо доменне ім'я і направляємо його прямо на GitHub Pages, благо, з цим достатньо досвіду з попередніх проектів з відкритим кодом. Беремо стандартний шаблон, заповнюємо його потрібними відомостями, трохи модифікуємо index.html — готово.

Nginx і SSL
Але як мені довірятимуть користувачі, якщо форма оплати буде недоступна по https? Нічого страшного — плавали, знаємо! Запускаємо CertBot на сервері, отримуємо потрібні SSL сертифікати. Поки що додаток доступний на pay.*domain*.com:3000 — негоже сюди направляти користувачів. Налаштовуємо Nginx, щоб він перенаправляв всі реквесты з http, https і вішаємо проксі з 80 порту на 3000 — перевіряємо, заходимо на сайт, працює.

Якщо кому цікаво, ось файл налаштувань nginx:

Тисни мене!
# HTTP - redirect all requests to HTTPS:
server {
listen 80;
listen [::]:80 default_server ipv6only=on;
return 301 https://$host$request_uri;
}

# HTTPS - проксі requests on to local Node.js app:
server {
listen 443;
server_name your_domain_name;

ssl on;
# Use certificate and key provided by Lets Encrypt:
ssl_certificate /etc/letsencrypt/live/pay.voicybot.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pay.voicybot.com/privkey.pem;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers '***';
# Pass requests for / to localhost:3001:
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:3000/;
proxy_ssl_session_reuse off;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
}
}

Висновок
Ось так несподівано і обірвався моя розповідь — так як все виявилося готове: один сервер приймає всі голосові повідомлення і переводить їх в текст, другий відповідає за безпечну оплату часу обробки войсов, а GitHub роздає головний сайт проекту. Головне, що я хотів описати в цій статті — це те, що процес створення продукту хоч і трудомісткий, але може вміститися в 40-80 годин завдяки вже існуючим інструментам в Інтернеті.

Нагадаю, що цей маленький проект включає в себе:

  • Нейронну мережу від Google, конвертирующую голос в текст
  • Непоганий вебсайт, створений при відсутності яких-небудь дизайн або фронтенд навичок
  • Розумний інструмент оплати, який, до речі, працює у всіх браузерах і навіть з AliPay та Биткоинами
  • Базу даних чатів і користувачів, за якою завжди можна глянути статистику
  • Конвертацію аудіо файлів між різними форматами
  • Складний сервер з SSL, з двома піднятими комплексними програмами
  • Відсутність будь-якого людського фактора: все працює автоматично
Якщо ви все ще думаєте, що для створення власного продукту у вас недостатньо навичок — озирніться навколо, все вже зроблено за вас. На відміну від того ж 2000, сьогодні створення продукту не вимагає глибокого знання алгоритмів і структур даних. Так чого ж ви чекаєте?

Подяки
Величезне спасибі, що дочитали до кінця! Я з радістю відповім на усі коментарі до статті. Посилання на сам бот не додаю (хоч він і повністю робочий), так як це проти правил Хабрахабра.

Rock on.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.