Всем привет. Меня зовут Игорь Филиппов и я веб-разработчик. Вы, вероятнее всего, знаете, как прочно ChatGPT закрепился в медийном пространстве. Ежедневно выходят сотни статей и видео на эту тему, предлагая разнообразные варианты применения.
Также регулярно выпускаются новые инструменты, использующие нейронные сети, которые пытаются сделать нашу жизнь лучше. Должен сказать, я до сих пор очень впечатлен всеми этими AI штуковинами и постоянно размышляю, как по максимуму применить их в моей обычной рутине.
Я активно использую Telegram для ежедневной коммуникации: начиная от друзей/родственников, заканчивая бесчисленным количеством рабочих чатов по проектам. Вы же все знаете эту шутку, про “больше чатов богу чатов”, да? А что, если в этих группах еще и много любителей голосовых сообщений? Быстро ухватить суть обсуждения без прослушивания каждого точно не получится. Поэтому появилась мысль, как можно оптимизировать время и эффективно решить эту задачу.
Кто-то может сказать, что подписка Telegram Premium как раз имеет такой функционал - распознавание любого аудио сообщения по клику на него. Но лично у меня было много претензий к нему, особенно к скорости. Давайте представим ситуацию: вы открываете чат, в нем 50+ новых сообщений и половина из них - голосовые по паре минут. У вас нет возможности быстро проскролить чат и влиться в контекст обсуждения, вам придется прокликивать каждое голосовое и ждать (бывает, очень долго) пока Telegram клиент отработает запрос.
Сначала мне в голову пришла идея создать бота, который автоматически под каждым сообщением оставляет свой реплай с полной расшифровкой аудио. Но в процессе разработки я подумал, что можно дополнительно проинтегрировать бота с ChatGPT - для получения краткого пересказа самого сообщения. Тем более, к тому моменту, когда я делал бота, Open AI только выпустила доступ к API.
Мне было очень интересно попробовать, руки зачесались, и я приступил к реализации. Своей разработкой решил поделиться с вами, вдруг, вам будет интересно или полезно.
![Исходное сообщение на целых 3 минуты, где человек просто объясняет, почему не захотел придти на встречу. Исходное сообщение на целых 3 минуты, где человек просто объясняет, почему не захотел придти на встречу.](https://habrastorage.org/getpro/habr/upload_files/667/3cc/242/6673cc242f682978a44227d8b1a152a0.jpg)
Задача #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. Может быть, благодаря сообществу, получится сделать еще что-нибудь интересное для развития бота.