Как написать своего бота, если устал от голосовых

от автора

Единственный  мессенджер, которым я пользуюсь — это Telegram. Мне нравится его простой и ненагруженный лишними элементами интерфейс. Но меня очень напрягают голосовые сообщения в диалогах и чатах. Я использую мессенджер для общения в текстовом формате. Мне гораздо удобнее  читать сообщения, а не слушать, что надиктовал собеседник. Если у меня появляется необходимость пообщаться голосом, я звоню. Плюс, как правило, чтение текста занимает меньше времени, чем его прослушивание. В общем, если вы, как и я, не любите голосовые сообщения в Telegram, возможно вам будет интересно почитать, как я решил эту проблему.

Небольшое отступление. Данную статью я хотел опубликовать ещё в середине июня, до выхода Telegram Premium, но после появления подписки я решил, что она стала неактуальной. Через полгода оказалось, что среди моих знакомых платной подпиской пользуется не такое уж большое количество человек, а значит, перевод голосовых сообщений остальным по-прежнему недоступен. Поэтому я решил всё же опубликовать статью.

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

Слушать, всё это мне было некогда, а ответить человеку надо. Я подумал, что так больше продолжаться не может и стал искать решение. Сначала я попробовал найти готового бота, который бы конвертировал аудио сообщение в текст. Но, оказалось, что у меня не самые лучшие скилы в поисковых операциях. Я нашёл только «мертвых» ботов или ботов с торчащей из всех щелей рекламой. Тогда я подумал: «Неужели я сам не смогу быстренько бота собрать?»

Забегая вперёд, я написал на python бота, перевод сообщений в котором основан на Google Speech Recognition, и развернул его на heroku. Здесь я не буду давать инструкцию по написанию бота, так как об этом существуют уже сотни статей. Я поделюсь опытом совмещения библиотек и получения пользы из этого процесса.

Выбираем библиотеки

Выбирая между aiogram и pyTelegramBotAPI, я выбрал вторую. На мой взгляд эта библиотека проще в использовании. Плюс меня подкупило, что всё необходимое для написания бота было тут же на гитхаб, а значит не надо рыскать в дебрях документации. Да возможно я чего-то не понимаю, но, в первую очередь, я хотел потратить на задачу не больше, чем два-три вечера. 

Теперь нужно найти подходящий сервис для конвертации. Я наткнулся на SpeechRecognition. Документация есть, можно использовать Google Speech Recognition без дополнительных регистраций и ограничений. Если честно, дальше я не стал ничего искать. Натыкался ещё на PocketSphinx, но для работы с этой библиотекой требуется скачать модель для распознавания и держать её локально, что для меня было неудобно. Я выбрал вариант использования API.

Определились — в бой

Теперь можно пробовать писать код. Код конвертера (converter.py) получился таким:

import speech_recognition as sr import os   class Converter:      def __init__(self, path_to_file: str, language: str = "ru-RU"):         self.language = language         self.wav_file = path_to_file      def audio_to_text(self) -> str:         r = sr.Recognizer()          with sr.AudioFile(self.wav_file) as source:             audio = r.record(source)             r.adjust_for_ambient_noise(source)          return r.recognize_google(audio, language=self.language)      def __del__(self):         os.remove(self.wav_file)

Класс достаточно простой. В момент инициализации передаём путь до файла для конвертации и, при необходимости, язык, на котором записано голосовое. Метод audio_to_text читает файл и возвращает текст в виде строки. Всё очень просто.

Код самого бота в bot.py:

import os import logging import telebot from telebot import types from convert import Converter  TOKEN = os.getenv('TOKEN')    # token for the telegram API is located in .env bot = telebot.TeleBot(TOKEN)  logging.basicConfig(level=logging.INFO,                     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logger = logging.getLogger()   @bot.message_handler(commands=['start']) def start(message: types.Message):     name = message.chat.first_name if message.chat.first_name else 'No_name'     logger.info(f"Chat {name} (ID: {message.chat.id}) started bot")     welcome_mess = 'Привет! Отправляй голосовое, я расшифрую!'     bot.send_message(message.chat.id, welcome_mess)   @bot.message_handler(content_types=['voice']) def get_audio_messages(message: types.Message):     file_id = message.voice.file_id     file_info = bot.get_file(file_id)     downloaded_file = bot.download_file(file_info.file_path)     file_name = str(message.message_id)     name = message.chat.first_name if message.chat.first_name else 'No_name'     logger.info(f"Chat {name} (ID: {message.chat.id}) download file {file_name}")      with open(file_name, 'wb') as new_file:         new_file.write(downloaded_file)     converter = Converter(file_name)     os.remove(file_name)     message_text = converter.audio_to_text()     del converter     bot.send_message(message.chat.id, message_text, reply_to_message_id=message.message_id)   if __name__ == '__main__':     logger.info("Starting bot")     bot.polling(none_stop=True, timeout=123) 

Здесь тоже всё довольно просто. Всего две функции. Первая обрабатывает команду /start и отправляет приветственное сообщение. Вторая будет работать только, если от пользователя получено голосовое. Если это происходит, то скачивается аудио файл, затем с помощью ранее написанного конвертера преобразуем голос в текст,  который отправляется ответом на голосовое сообщение.

И тут меня ждала неудача. Формат аудио сообщений в Telegram .ogg, а сервис SpeechRecognition ждёт .wav. Пришлось прикручивать медиаконвертер. Я не смог найти ничего лучше, чем воспользоваться ffmpeg. Почему мне не особо понравилось это решение? Потому что появилась необходимость ставить ffmpeg на машину или использовать Docker. Если кто-то знает вариант лучше, буду признателен за наводку. Ну ладно, данный вариант не сильно усложняет жизнь, поэтому я просто принял это как необходимость.

Поставил ffmpeg и немного преобразовал код converter.py до такого вида:

class Converter:      def __init__(self, path_to_file: str, language: str = "ru-RU"):         self.language = language         subprocess.run(['ffmpeg', '-v', 'quiet', '-i', path_to_file, path_to_file.replace(".ogg", ".wav")])         self.wav_file = path_to_file.replace(".ogg", ".wav")

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

В bot.py вообще добавилось только расширение файла:

@bot.message_handler(content_types=['voice']) def get_audio_messages(message: types.Message):     file_id = message.voice.file_id     file_info = bot.get_file(file_id)     downloaded_file = bot.download_file(file_info.file_path)     file_name = str(message.message_id) + '.ogg'     name = message.chat.first_name if message.chat.first_name else 'No_name'     logger.info(f"Chat {name} (ID: {message.chat.id}) download file {file_name}")      with open(file_name, 'wb') as new_file:         new_file.write(downloaded_file)     converter = Converter(file_name)     os.remove(file_name)     message_text = converter.audio_to_text()     del converter     bot.send_message(message.chat.id, message_text, reply_to_message_id=message.message_id) 

Теперь всё работает. Ну что mission complete? Неа.. Нам же задеплоить куда-то надо. Тут я не долго думая выбрал heroku. Ранее я слышал про него, но не работал с ним лично. Решил попробовать. В принципе сервис удобный, но, к сожалению, расширить бесплатный режим привязкой карты из-за некоторых событий не удалось.

Итак, для деплоя осталось сделать две вещи: перейти на webhook и перенести код в контейнер. Причину необходимости docker я описал выше — нужно устанавливать ffmpeg. Получаем вот такой Dockerfile:

FROM python:3.10-alpine  RUN apk add -q --progress --update --no-cache ffmpeg  COPY ./requirements.txt . RUN pip install --upgrade pip && \     pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r ./requirements.txt && \     pip install --no-cache /wheels/*  COPY . /app WORKDIR /app CMD python ./bot.py

Для экономии бесплатного лимита и ресурсов хостинга используем webhook, а для локальной разработки оставим pooling. Поэтому добавим переменную в env MODE, у себя в среде разработки заполним значением dev, а в heroku укажем prod. Теперь добавим логику для запуска нашего бота:

if __name__ == '__main__':     logger.info("Starting bot")     if os.getenv('MODE') == 'dev':         bot.polling(none_stop=True, timeout=123)     else:         server = Flask(__name__)           @server.route('/' + TOKEN, methods=['POST'])         def get_message():             json_string = request.get_data().decode('utf-8')             update = telebot.types.Update.de_json(json_string)             bot.process_new_updates([update])             return "!", 200           @server.route("/")         def webhook():             bot.remove_webhook()             url = f'https://{os.getenv('HEROKU_APP_NAME')}.herokuapp.com/{TOKEN}'             bot.set_webhook(url=url)             return "!", 200           server.run(host="0.0.0.0", port=int(os.environ.get('PORT', 5000)))

Если запускаемся не локально, то используем Flask. Добавляем два метода для установки webhook и последующего отлова наших сообщения. При этом оставляем запуск pooling для локальной разработки.

На heroku осталось добавить переменные MODE, TOKEN, HEROKU_APP_NAME, PORT для того, чтобы всё корректно работало. После чего выполним в терминале heroku container:push web и heroku container:release web. Всё готово. Ах да, ещё requirements.txt приведу для полной честности:

SpeechRecognition==3.8.1 pyTelegramBotAPI==4.5.1

Видео — новый тренд

Итак, всё заработало! Справились за пару вечеров. Добавляю в групповой чат, а один участник берёт и вместо голосового отправляет видеосообщение, которое бот, естественно, не переводит. Я немного поматерился подумал и дописал существующий метод:

@bot.message_handler(content_types=['voice', 'video_note']) def get_audio_messages(message: types.Message):     file_id = message.voice.file_id if message.content_type in ['voice'] else message.video_note.file_id     file_info = bot.get_file(file_id)     downloaded_file = bot.download_file(file_info.file_path)     file_name = str(message.message_id) + '.ogg'     name = message.chat.first_name if message.chat.first_name else 'No_name'     logger.info(f"Chat {name} (ID: {message.chat.id}) download file {file_name}")      with open(file_name, 'wb') as new_file:         new_file.write(downloaded_file)     converter = Converter(file_name)     os.remove(file_name)     message_text = converter.audio_to_text()     del converter     bot.send_message(message.chat.id, message_text, reply_to_message_id=message.message_id)

Такое небольшое изменение позволяет работать ещё и с видеосообщениями.

Делаем деплой. И тут в голове возникает вопрос: А что если человек захочет конвертировать только видео или только аудио? Нужна возможность настройки бота в зависимости от чата, а значит настала пора использовать базу данных. В качестве базы данных выбор пал на  PostgreSQL.

Добавляем небольшое количество строк кода и команду /settings боту. Этой командой пользователь сможет выбрать, что конвертировать в чате. Ну и раз мы добавили базу данных, будем логировать каждый запрос на перевод в базу. Позже можно будет собрать какую-нибудь статистику по любителям поговорить голосом среди пользователей в чате.

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

Вместо заключения

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

Возможно в процессе использования бота у меня появятся идеи по его доработке. Вашим предложениям тоже буду рад!


ссылка на оригинал статьи https://habr.com/ru/post/700214/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *