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

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


  1. kiff
    05.06.2023 17:38

    Так как в премиум подписке ТГ есть функция аудио в текст, было бы неплохо сравнить данные подходы.


    1. play_to Автор
      05.06.2023 17:38

      Так я же в статье как раз писал про это :)

      Тем более, основная идея не просто в реализации speech-to-text, а в использовании нейронки для получения супер краткого пересказа голосового, без воды.


      1. 0x6b73ca
        05.06.2023 17:38

        А можно пример json с ошибкой?


        1. play_to Автор
          05.06.2023 17:38

          С ошибкой чего? О том, что сервис недоступен?)


  1. GritsanY
    05.06.2023 17:38

    Нужна платная подписка, чтобы через OpenAI API речь распознавать в аудио?


    1. play_to Автор
      05.06.2023 17:38

      На старте, OpenAI, если я не ошибаюсь, даёт 18$ на тесты. Но карту нужно привязать, да. У них биллинг через stripe.


  1. edvardpotter
    05.06.2023 17:38

    А с оплатой OpenAI API проблем не было? Или оплачивать только через карточку зарубежного банка?


    1. play_to Автор
      05.06.2023 17:38

      У них биллинг через Stripe, поэтому карты РФ и РБ не принимаются :(


  1. NickyX3
    05.06.2023 17:38
    +1

    И наконец, последний аргумент в пользу whisper - цена. Он кратно дешевле.

    Он вообще-то в опесорсе, ставите себе на сервер и никаких ограничений + умеет чисто на CPU, правда медленнее, чем с GPU.


    1. play_to Автор
      05.06.2023 17:38

      Круто, не знал. Надо будет заняться вопросом на выходных)


      1. NickyX3
        05.06.2023 17:38
        +1

        Более того, он и под Windows взлетает с полтычка даже на "полярисах", но в любом варианте требуется CPU не ниже intel core третьего поколения (там добавили FP16).

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


        1. play_to Автор
          05.06.2023 17:38

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