Я абсолютно уверен что скоро в telegram - перевод аудио-сообщений в текст будет функцией по-умолчанию, ну а пока хотел бы показать простенький пример как реализовать такой функционал в telegram-боте (которых уже сотни, но почему бы не посмотреть как это работает на примере).

Этот не шутка, это реальный диалог из моей рабочей переписки.
Этот не шутка, это реальный диалог из моей рабочей переписки.

Сразу оговорюсь что используемый в примере сервис Wit не совсем предназначен для перевода аудио-сообщений, у этого сервиса другое предназначение, более интересное о котором я возможно напишу позже, но раз у него такой функционал есть и он бесплатен, то почему бы и нет?

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

Регистрация бота

Бот в telegram создаётся папой-ботом @BotFather, маму-бота заменим мы.

А сам бот будет написан на самом «уважаемом» среди сообщества хабра - языке программирования PHP. Плюсом выбора этого языка является то что мы можем закинуть скрипт на абсолютно любой самый дешевый хостинг.

Сам же процесс создания бота не сложен, мы просто отвечаем на вопросы, а в конце получаем токен который понадобится для работы с API telegram.

Токен лучше никому не показывать ;-)
Токен лучше никому не показывать ;-)

Теперь нужно зарегистрировать обработчик нашего бота, для этого переходим по ссылке: https://api.telegram.org/bot<TOKEN>/setWebhook?url=<URL>

Где <TOKEN> это токен нашего бота, а <URL> путь к обработчику. Учтите что путь до обработчика должен начинаться с https.

В ответ должны увидеть что-то вроде этого:

{
    "ok":true,
    "result":true,
    "description":"Webhook is already set"
}

Теперь каждый раз когда бот получит сообщение, на наш обработчик будет отправлен POST запрос с JSON в теле запроса, там много чего интересного но так как наш бот будет выполнять только одну задачу, нас интересует наличие в нём аудио-сообщения.

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

Написание скрипта.

И так определим схему работы бота:

Вроде всё просто. Конечно мы могли бы отправить скачанное аудио-сообщение сразу в Wit без предварительного сохранения на диск, но аудио-файл скачанный из telegram кодирован в OGG (Кодек: opus, 48000 Hz, mono, fltp, 26 kb/s) к сожалению Wit такой формат не принимает, поэтому нам нужно конвертировать этот файл в любой другой формат на выбор:

  • audio/wav

  • audio/mpeg3

  • audio/ogg

  • audio/ulaw

  • audio/raw

Но я буду конвертировать из OGG в OGG с помощью ffmpeg кодеком vorbis что для Wit в самый раз.

Теперь приступим к программированию. Писать как я уже сказал ранее буду на PHP версии 8.0.

<?php

class VoioverBot {

	private string $url = "https://api.telegram.org/bot";

	function __construct(private string $wit, private string $tg)
	{
		$this->msg = json_decode(file_get_contents("php://input"), true);
		$this->url .= $tg;
	}

	// Ищем в сообщении аудио
	public function getAudio() : bool | string
	{
		if (!isset($this->msg["message"]["voice"]["file_id"])) return false;

		// Получаем информацию о файле аудио-сообщения
		$info = json_decode(@file_get_contents("{$this->url}/getFile?file_id={$this->msg["message"]["voice"]["file_id"]}"), true);
		if (!$info || !isset($info["result"]["file_path"])) return false;

		// Скачиваем аудио-сообщение
		$file = @file_get_contents("https://api.telegram.org/file/bot{$this->tg}/{$info["result"]["file_path"]}");
		if (!$file) return false;

		// Сохраняем аудио-сообщение во временный файл
		if (!file_put_contents("./{$this->msg["message"]["voice"]["file_id"]}", $file)) return false;

		// Конвертируем файл:
		$this->convertAudio();

		// Преобразуем ауди в текст
		return $this->getTranscription();
	}
	
	// Конвертируем аудио в подходящий формат
	private function convertAudio()
	{
		shell_exec("ffmpeg -i ./{$this->msg["message"]["voice"]["file_id"]} -f ogg ./{$this->msg["message"]["voice"]["file_id"]}.ogg");
	}
	
	// Переводим голос в текст используя API wit
	private function getTranscription() : bool | string
	{
		$context = stream_context_create([
			'http' => [
				'method' => 'POST',
				'header' => "Authorization: Bearer {$this->wit}\r\n" .
							"Content-Type: audio/ogg",
				'content' => file_get_contents("./{$this->msg["message"]["voice"]["file_id"]}.ogg"),
				'timeout' => 20
			],
		]);
		$answer = json_decode(file_get_contents("https://api.wit.ai/speech?v=20200422", false, $context), true);
		// Временные файлы можно удалить:
		unlink("./{$this->msg["message"]["voice"]["file_id"]}");
		unlink("./{$this->msg["message"]["voice"]["file_id"]}.ogg");
		return (isset($answer['_text']) && !empty($answer['_text'])) ? $answer['_text'] : false;

	}

	// Отправляем текст в чат
	public function sendMessage($text) : bool
	{
		$context = stream_context_create([
			'http' => [
				'method' => 'POST',
				'header' => 'Content-Type: application/json' . PHP_EOL,
				'content' => json_encode([
					'chat_id' => $this->msg["message"]["chat"]["id"],
					'text' => "✍ <b>{$this->msg['message']['from']['first_name']} " . 
								"{$this->msg['message']['from']['last_name']}</b>\r\n{$text}",
					'parse_mode' => "HTML"
				])
			]
		]);
		$result = file_get_contents("{$this->url}/sendMessage", false, $context);
		return $result ? true : false;
	}
}

$vbot = new Voiover("ТОКЕН Wit", "ТОКЕН telegram");
$voice = $vbot->getAudio();
if ($voice) $vbot->sendMessage($voice);

Это базовый код, и в нём много недостатков:

  • Скрипт не проверяет от кого приходят запросы

  • Если сообщение по какой-то причине не отправилось то оно не будет отправлено повторно.

  • Максимальная длина аудио-сообщений для Wit всего 20 секунд.

P.S перед публикацией я обнаружил что буквально за день до этого на хабре появилась схожая статья Распознавание речи в Telegram «на лету», но на языке GO.

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


  1. casnerano
    07.11.2021 19:52
    +3

    Достаточно часто замечаю, в различных примерах используют объединение типов, таким же образом, как и вы:

    public function getAudio() : bool | string;

    но сам тип bool используется только для возвращения false.
    Если же ваш метод вернет true, то вся логика приложения будет нарушена.
    Как минимум мы можете использовать ?string и возвращать null.


    1. powernic
      08.11.2021 19:51

      Лучше всего в этом месте бросить исключение, а в возвращаевом типе вместо bool | string оставить только string


      1. NiceDay
        09.11.2021 21:13

        не всегда, исключение это дополнительные накладные расходы с последующей раскруткой стека.


    1. NiceDay
      09.11.2021 21:16

      c PHP8 можно указывать false в пересекающихся типах.
      например,

      function stringOrFalse(string|false $arg): string|false
      {
          return $arg;
      }

      вполне валидная запись


  1. NiceDay
    09.11.2021 22:05
    +2

    Вроде всё просто. Конечно мы могли бы отправить скачанное аудио-сообщение сразу в Wit без предварительного сохранения на диск, но аудио-файл скачанный из telegram кодирован в OGG


    ну, мы все еще можем отправить его без сохранения, в каком бы формате он ни был.
    $witToken = '<witToken>';
    $witUri = 'https://api.wit.ai/speech?v=20211109&';
    
    $audioUri = "https://api.telegram.org/file/bot{$this->tg}/{$info["result"]["file_path"]}";
    
    $command = "curl -s '{$audioUri}'"
        . " | ffmpeg -nostdin -y -hide_banner -loglevel warning -i pipe:0 -f ogg pipe:1"
        . " | curl -s -H 'Authorization: Bearer {$witToken}' -H 'Content-Type: audio/ogg' -d @- '{$witUri}'";
    
    $response = shell_exec($command);
    $json = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
    
    var_dump($json);