Единственный  мессенджер, которым я пользуюсь — это 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, а для локальной разработки оставим polling. Поэтому добавим переменную в 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 и последующего отлова наших сообщения. При этом оставляем запуск polling для локальной разработки.

На 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 боту. Этой командой пользователь сможет выбрать, что конвертировать в чате. Ну и раз мы добавили базу данных, будем логировать каждый запрос на перевод в базу. Позже можно будет собрать какую-нибудь статистику по любителям поговорить голосом среди пользователей в чате.

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

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

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

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

Комментарии (25)


  1. akakoychenko
    18.11.2022 14:31
    +5

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


    1. balamutick
      18.11.2022 14:45
      +1

      Тут двояко, иногда можно вытерпеть голосовое, если человек ну прям никак не может набрать текстом сейчас (идёт, бежит, едет в трясущей маршрутке). Иногда надо очень очень быстро сообщить информацию, и голосовое это даже быстрее чем позвонить.

      И иногда этот человек, это - ты.

      Раздражает, что люди злоупотребляют голосовыми и часто звонками по телефону.

      Одна из моих любимых статей, это «Звонки — это всегда не вовремя» (в гугле на vc выпадает первой)


      1. Sancho_SP
        18.11.2022 15:32
        +2

        Скажем иначе: иногда этот человек - клиент.


    1. DGG
      19.11.2022 19:57

      Вот набросал автоответчик/автоудаление на звуковые.

      Hidden text
      import time
      import sys
      import getpass
      from telethon import events
      from telethon.sync import TelegramClient
      from telethon.tl.types import InputPeerUser
      from telethon.errors import SessionPasswordNeededError
      
      # Log in to your Telegram core: https://my.telegram.org/apps.
      # get your api_id, api_hash 
      api_id = None #1234567
      api_hash = None #'b00bab00bab00bab00bab00bab00ba000'
      # your phone number 
      phone = None #'+1234567890'
      
      if not api_id or not api_hash :
          print('Set api_id and api_hash')
          sys.exit(1)
      
      client = TelegramClient('my_novoice_bot', api_id, api_hash)
      client.connect() 
      
      # in case of script ran first time it will 
      # ask either to input token or otp sent to 
      # number or sent or your telegram id  
      if not client.is_user_authorized(): 
          client.send_code_request(phone) 
          client.sign_in(phone) # signing in the client 
          try:
              client.sign_in(code=input('Enter code: '))
          except SessionPasswordNeededError:
              client.sign_in(password=getpass.getpass()) 
      
      ################################
      @client.on(events.NewMessage(incoming=True ))  
      async def handler(event):
          if event.is_private:
              if event.message.voice != None:
                  time.sleep(1)
                  await event.reply('К сожалению, собеседник не принимает звуковые сообщения.')
                  await client.delete_messages(event.chat_id, event.id)
      ################################
       
      print(time.asctime(), '-', 'Start auto-replying...')
      client.run_until_disconnected()
      print(time.asctime(), '-', 'Stopped!') 


  1. igor6130
    18.11.2022 14:40

    А на Хероку у вас бесплатный план? Если да, то куда планируете переходить после 28-го числа?

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

    @server.post('/' + TOKEN)
    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

    Ну это так, ни к чему не обязывающая ремарочка.


    1. leo_l Автор
      19.11.2022 08:54

      Да, сейчас бесплатный тариф. Пока не решил куда перейти.

      За ремарку спасибо!


  1. S0mbre
    18.11.2022 15:30
    +1

    Спасибо за статью. Насколько ремарок:

    • Было бы удобнее, если это технически возможно, чтобы бот отслеживал голосовые сам в фоне и кидал тексты непосредственно в тот же чат. Конечно, если автозанрузка медиа включена (у меня всегда выключена).

    • Вообще жаль, что эта фича до сих пор не реализована в самом тг... вроде 21 век на дворе, ничего супер сложного. Вот это было бы killa feature.

    • Heroku здесь не должен быть камнем преткновения. Можно запросто развернуть на любом любом VPS / виртуальном хостинге за несколько сот рублей в год или бесплатно. Flask + Gunicorn / uWSGI / и тд в помощь.


    1. venanen
      18.11.2022 15:50

      Такая фича реализована, но для подписки премиум. Киллер-фичей это точно не станет, потому что ВК ее уже давно изобрела.
      P.S. Это где можно VPS за несколько сот рублей в год купить? (Не сарказм, реально интересно).


      1. S0mbre
        21.11.2022 03:12

        https://habr.com/ru/post/472454/ здесь нормальный обзор, плюс я еще могу посоветовать jino.ru (у самого там парочка VPS)


        1. Radish
          21.11.2022 07:01

          У Джино нет бесплатных vps, а что есть стоит не пару сотен в год, далеко


    1. Radish
      19.11.2022 05:07

      Не подскажете этот любой vps за несколько сот рублей в год?


      1. S0mbre
        21.11.2022 03:12

        UP


    1. leo_l Автор
      19.11.2022 09:10

      В групповых чатах бот расшифровку медиа там и приводит. А вот в приватных чатах это вряд ли возможно.

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

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


    1. osmanpasha
      19.11.2022 09:51

      А подскажите VPS за несколько сот рублей в год или бесплатно? Я знаю только за несколько сот рублей в месяц, а бесплатно - только Oracle Cloud, которая и раньше не всякую карту принимала, а теперь вообще отключила российских пользователей


      1. S0mbre
        21.11.2022 03:14

  1. ABATAPA
    18.11.2022 16:11
    +2

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

    Действительно. :)
    https://habr.com/ru/post/591563/
    https://vc.ru/tribuna/86795-bot-dlya-telegram-kotoryy-preobrazovyvaet-golosovye-soobshcheniya-v-tekst
    и там же из комментариев:


    1. UksusoFF
      18.11.2022 19:07

      И у Сбера еще Салют.


    1. Mayurifag
      18.11.2022 20:33

      Voicy сейчас становится платным и надо его селфхостить (то есть в любом случае на деплой время придется потратить и на впс если нет под рукой).

      https://blog.borodutch.com/im-sunsetting-voicy/

      А раньше им было некомфортно пользоваться из-за фраз про события на Украине. Я не против, но их, кажется, нельзя было отключить и я бы предпочёл, всё же, их отсутствие.


    1. leo_l Автор
      19.11.2022 09:23

      Спасибо! Эти варианты я упустил из виду:) В целом я допускал, что плохо искал. Отчасти поэтому и сделать решил его за пару вечеров, а не сильно дольше. Чтобы на повторное изобретение колеса не так уж много времени ушло :)


  1. Emulyator
    18.11.2022 17:06

    На нескольких курсах по питону, что мне попадались, в итоге пишут бота для телеги, причем с использованием библиотеки telebot. Когда интересуешься у тех, кто профессионально делает ботов, рекомендуют использовать aiogram. Хочу уточнить у автора и тех кто разбирается, имеет ли смысл вникать в телебота, или проще сразу aiogram осваивать или еще какой вариант?


    1. thunderspb
      18.11.2022 20:20
      +1

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


      1. telpos
        20.11.2022 09:22
        +1

        Не все новички могут с наскока в asyncio. А использовать можно и то, и то. Если понятен asyncio, то есть смысл начинать с aiogram


    1. leo_l Автор
      19.11.2022 09:58
      +1

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

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


  1. Povtor
    20.11.2022 15:23

    У меня есть товарищ, предпочитает общаться голосовыми по whatsapp. Сделал с ним группу в Телеграм, подключил туда бота@smartspeech_sber_bot

    Объяснил товарищу, что не надо в личку слать, присылай в нашу с тобой группу для расшифровок твоих голосовых. Пару раз он туда прислал, а потом продолжил слать в личку в whatsapp.

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


    1. citius
      21.11.2022 14:08

      Начни ему отвечать также голосовыми, но максимально мнэээээкая и растягивая время на пару минут чтобы сказать 2-3 осмысленных слова.