Пишем Telegram бота для распознавания голосовых сообщений и их обработки с помощью AI

от автора

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

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

Я активно использую Telegram для ежедневной коммуникации: начиная от друзей/родственников, заканчивая бесчисленным количеством рабочих чатов по проектам. Вы же все знаете эту шутку, про “больше чатов богу чатов”, да? А что, если в этих группах еще и много любителей голосовых сообщений? Быстро ухватить суть обсуждения без прослушивания каждого точно не получится. Поэтому появилась мысль, как можно оптимизировать время и эффективно решить эту задачу.

Кто-то может сказать, что подписка Telegram Premium как раз имеет такой функционал — распознавание любого аудио сообщения по клику на него. Но лично у меня было много претензий к нему, особенно к скорости. Давайте представим ситуацию: вы открываете чат, в нем 50+ новых сообщений и половина из них — голосовые по паре минут. У вас нет возможности быстро проскролить чат и влиться в контекст обсуждения, вам придется прокликивать каждое голосовое и ждать (бывает, очень долго) пока Telegram клиент отработает запрос.

Сначала мне в голову пришла идея создать бота, который автоматически под каждым сообщением оставляет свой реплай с полной расшифровкой аудио. Но в процессе разработки я подумал, что можно дополнительно проинтегрировать бота с ChatGPT — для получения краткого пересказа самого сообщения. Тем более, к тому моменту, когда я делал бота, Open AI только выпустила доступ к API.

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

Исходное сообщение на целых 3 минуты, где человек просто объясняет, почему не захотел придти на встречу.

Исходное сообщение на целых 3 минуты, где человек просто объясняет, почему не захотел придти на встречу.

Задача #1 – Получить расшифровку аудио

Конечно же, я решил по максимуму использовать существующие решения. Выбор пал на speech-to-text сервис от Яндекса. У меня уже был опыт работы с Yandex SpeechKit, и в первую очередь я решил использовать именно его.

Первая проблема, с которой я столкнулся – Yandex SpeechKit не поддерживает ogg формат, в котором Telegram отдает аудио. Вторая – Yandex SpeechKit обрабатывает аудио длительностью не больше 30 секунд.

Ок, это решаемо, благодаря прекрасному инструменту, настоящему швейцарскому ножу для работы с аудио/видео — ffmpeg.

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

Вот пример вызова ffmpeg из терминала:

ffmpeg -i /path/to/origin/file -f segment -segment_time 30 -c copy \"%03d.ogg\"

Кстати, в качестве источника ffmpeg без проблем принимает и любой url из интернета, не только локальные файлы.

Но в последствии пришлось отказаться от Яндекса. Из-за того, что мы режем фрагменты ровно по 30 секунд, то с очень большой вероятностью попадаем в середину слова, что вызывает некоторые дыры или ошибки в итоговом склеенном тексте.

Следующий выбор пал на модель whisper от все того же OpenAI. На мой взгляд, этот сервис работает значительно лучше. Во-первых, нет ограничения на длительность файла, только на размер — не больше 25 мб (а этого с головой хватит даже для очень больших голосовых). Во-вторых, нет необходимости передавать исходный язык — whisper автоматически его определяет и отдает результат строкой. Тестировались: английский, турецкий, украинский, русский, испанский. Качество распознавания очень хорошее и, на мой субъективный взгляд, лучше, чем у аналогичного сервиса от Яндекс. В-третьих, whisper понимает, когда человек делает паузы, задает вопросы, восклицает, поэтому в ответ он отдает размеченный текст с пунктуацией. Благодаря этому визуально текст выглядит значительно приятнее. И наконец, последний аргумент в пользу whisper — цена. Он кратно дешевле.

Так как бота я писал на php, соответственно и примеры код-сниппетов тоже будут на php.

Вот так мы можем конвернуть из ogg в любой формат данных с помощью ffmpeg через системный вызов:

<?php  namespace App\AudioConverter;  class AudioConverter {     public function convertAudio(string $pathToSource): AudioConversionResult     {         $outputFileName = 'temp_ffmpeg_output.wav';          $cmd = "ffmpeg -y -i \"$pathToSource\" -b:a 128k $outputFileName 2>&1";          $ffmpegStdout = popen($cmd, 'r');          $stdout = '';          if (is_resource($ffmpegStdout)) {             while (!feof($ffmpegStdout)) {                 $stdout .= fread($ffmpegStdout, 4096);             }         }          pclose($ffmpegStdout);          preg_match('/Duration: (.*?),/', $stdout, $matches);         $timeDuration = $matches[1];          list($hours, $minutes, $seconds) = explode(':', $timeDuration);          $totalSeconds = ((int)$hours * 3600) + ((int)$minutes * 60) + (int)$seconds;          $roundedSeconds = ceil($totalSeconds);          return new AudioConversionResult(             pathToFile: $outputFileName,             duration: $roundedSeconds         );     } }

Я очень хотел сделать на стороне бота подсчет затрат, чтобы видеть статистику использования, поэтому так неизящно приходится вызывать ffmpeg для того, чтобы захватить stdout и затем регулярным выражением вытащить длительность аудио.

Для того, чтобы получить распознанный текст сообщения, сделаем запрос к API:

<?php  namespace App\Clients\OpenAI;  use App\Clients\OpenAI\Exceptions\TooLargeFileException; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Utils;  class OpenAIApiClient {     // some other methods here...       public function getTranscription(string $pathToFileAudio): string     {         $outputFileSize = filesize($pathToFileAudio);          // Check if the file size is greater than 25 MB (in bytes)         $maxFileSize = 25 * 1024 * 1024; // 25 MB in bytes          if ($outputFileSize > $maxFileSize) {             throw new TooLargeFileException("Max audio file size is $maxFileSize but $outputFileSize was given.");         }          $headers = [             'Authorization' => 'Bearer ' . $this->apiKey         ];         $options = [             'multipart' => [                 [                     'name' => 'model',                     'contents' => 'whisper-1'                 ],                 [                     'name' => 'file',                     'contents' => Utils::tryFopen($pathToFileAudio, 'r'),                     'filename' => $pathToFileAudio,                     'headers' => [                         'Content-Type' => '<Content-type header>'                     ]                 ]             ]         ];          $request = new Request('POST', 'https://api.openai.com/v1/audio/transcriptions', $headers);          $response = $this->httpClient->sendAsync($request, $options)->wait();          return json_decode((string)$response->getBody(), true)['text'];     } }

Задача #2 – Получить краткое содержание ответа

После того, как мы преобразовали голосовое сообщение в текст, нам нужно получить его краткий пересказ. Тут все очень просто, и выбирать в настоящее время особо не из чего. Используем модель gpt-3.5-turbo. Опять же спасибо OpenAI за API.

Должен сказать, что использование API от нейронных сетей — штука довольна забавная. По факту, у вас есть один endpoint, и в каждом запросе вы пишите сопроводительное сообщение на любом естественном языке о том, что и в каком формате хотите получить в ответ.

В случае с gpt-3.5-turbo мы должны передать просто массив сообщений. Сообщения могут быть трех типов (ролей):

  • «system» — сообщения для языковой модели, где мы вводим какую-то мета информацию и сообщаем ей, что мы от нее хотим;

  • «assistant» — то, что языковая модель уже сгенерировала ранее (либо мы хотим заставить ее так думать);

  • «user» — пользовательские сообщения.

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

POST https://api.openai.com/v1/chat/completions

{   "model": "gpt-3.5-turbo",   "messages": [     {       "role": "system",       "content": "Ты должен дать краткое описание сообщения в 1-2 предложениях. Ответ дай на русском языке."     },     {       "role": "user",       "content": "... сюда отправим текст распознанного аудио-сообщения..."     }   ] }

Кстати, у меня были эксперименты, когда я просил модель отдать мне результат в формате json. В целом это работает, правда, иногда она добавляет ненужные текстовые прелюдии перед json, так что приходится очищать строку.

Задача #3 – Отправить результат в telegram чат

К сожалению, сервис от OpenAI в частности chat/completions нельзя назвать стабильным. Несколько раз в день я получаю сообщение об ошибке, где он возвращает json с текстом, что сервис временно недоступен. Также на практике оказалось, что ± 60% сообщений после распознавания остаются короткими, длиной не больше 200-300 символов. Решил, что будем использовать ChatGPT только для действительно длинных сообщений (>300 символов), а для небольших будем просто отдавать оригинальный текст.

Для короткого голосового:

<?php  namespace App\Telegram\Messages;  class VoiceMessageRecognitionResultOnlyMessage extends Message {     protected string $recognizedText;      public function __construct(string $recognizedText)     {         $this->recognizedText = $recognizedText;     }      public function getMessageContent(): array     {         return [             "?<b>Распознанное сообщение:</b>",             "",             $this->recognizedText         ];     }      public function getParseMode(): string     {         return Message::PARSE_MODE_HTML;     } }

Для длинного голосового:

<?php  namespace App\Telegram\Messages;  class VoiceMessageSummaryWithRecognitionResultMessage extends Message {     protected string $recognizedText;      protected string $summary;      public function __construct(string $recognizedText, string $summary)     {         $this->recognizedText = $recognizedText;         $this->summary = $summary;     }      public function getMessageContent(): array     {         return [             "✂️<b>Краткое содержание:</b>",             "",             $this->summary,             "",             "?<b>Распознанное сообщение:</b>",             "",             $this->recognizedText         ];     }      public function getParseMode(): string     {         return Message::PARSE_MODE_HTML;     } }

Текущая статистика и стоимость использования API от OpenAI

Ниже статистика использования бота за последний месяц:

Total audio recognition count: 1075 Total summarize count: 482  Total audio recognition cost: 4.2397$ Total summarize cost: 0.09$  Total cost: 4.3297$

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

Заключение

По личному опыту и первому фидбеку бот получился очень полезным. Сейчас я добавляю его во все общие чаты с друзьями, где я состою, и мне прямо очень нравится этот дополнительный функционал. Особенно чудно, когда видишь голосовое на 5 минут, и краткое содержание “Автор сообщения отвергает предложенную ему встречу и обосновывает причину отказа”. В групповых чатах бот автоматически обрабатывает все аудио сообщения, присылает краткое содержание и расшифровку. Голосовые из личной переписки можно отправлять боту напрямую.

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


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


Комментарии

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

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