Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров7.3K

Всем привет. Меня зовут Игорь Филиппов и я веб-разработчик. Вы, вероятнее всего, знаете, как прочно 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. Может быть, благодаря сообществу, получится сделать еще что-нибудь интересное для развития бота.

Теги:
Хабы:
+7
Комментарии12

Публикации

Истории

Работа

PHP программист
158 вакансий

Ближайшие события