coqui-ai TTS: Ставим локально и отправляем голосовые в Telegram

В далёком 2001 году в мои руки попал любопытный диск “Системы распознавания речи”. Диск был заполнен различными STT и TTS программками под Windows и на те временя, такой софт казался каким-то чудом из чудес, не смотря на то, что синтез голоса звучал, мягко говоря, паршивенько.

С приходом эпохи нейросетей, синтез голоса поднялся на такой уровень, что порой его не сразу удаётся отличить от живого диктора. Однако, хорошие голосовые движки живут в основном в облаках и чаще всего, имеют ограничения на бесплатное использование. Но что-же делать, когда нам нужно синтезировать голос локально? Поднимем собственный сервер TTS, без регистрации и смс!

Порывшись в сети я наткнулся на подходящий открытый проект coqui-ai TTS и решил попробовать его установить.

В инструкции к софту имеется описание установки через Docker, но не понятным мне причинам, этот вариант у меня не заработал. Что-ж, пойдём другим путём:
Для установки нам понадобится: Debian 12, python3 и pip.

pip install TTS

После того, как пакет успешно установился, можно посмотреть список актуальных голосовых моделей, путём ввода команды tts –list_models

Как оказалось, отдельной голосовой модели для русского языка в списке нет. Однако, есть мультиязыковая модель tts_models/multilingual/multi-dataset/xtts_v2 внутри которой, в том числе, есть поддержка русского языка.

Теперь посмотрим, какие голоса (спикеры) поддерживаем модель:

tts --model_name "tts_models/multilingual/multi-dataset/xtts_v2" --list_speaker_idxs

Выбираем любой из голосов, и пробуем сгенерировать речь:

tts --text "Привет, дружок!" --model_name "tts_models/multilingual/multi-dataset/xtts_v2" --out_path /tmp/speech.wav --speaker_idx "Damien Black" --language_idx "ru"

В данной строке мы указали текст, имя модели, путь для сохранения результата, имя спикера и язык. В ходе выполнения этой строки, TTS сама скачает и запустит указанную модель. Следует учесть, что на запуск модели уходит некоторое время, по этому, для дальнейшей работы с TTS мы запустим её в качестве сервера с API.

И вот тут меня ждал некоторый сюрприз. В инструкции нет прямого указания на то, как это сделать правильно. Методом тыка в ChatGPT с пробами и ошибками, выяснилось, что сервер можно запустить так:

tts-server   --model_name "tts_models/multilingual/multi-dataset/xtts_v2"   --config_path "/root/.local/share/tts/tts_models--multilingual--multi-dataset--xtts_v2/config.json"   --model_path "/root/.local/share/tts/tts_models--multilingual--multi-dataset--xtts_v2"

Без явного указания путей эта штука ни в какую не работала. Добавим в конец &&, чтобы процесс ушёл в бэкграунд. Если всё ок, в выводе будет видно следующее:

 * Serving Flask app 'TTS.server.server'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (::)
 * Running on http://[::1]:5002
 * Running on http://[::1]:5002

Теперь сервер слушает на порту 5002 и ему можно отправлять запросы. И тут меня ждал сюрприз №2: нигде толком не написано, как, собственно, работать с API?

ChatGPT упёрто подсовывал мне варианты с методом POST, но как оказалось, рабочим вариантом оказался GET. Формируем тестовый запрос:

http://IP_ВАШЕГО_СЕРВЕРА:5002/api/tts?text=Примерно%20так%20звучит%20синтетический%20голос%20на%20русском%20языке.%20Ну%20как%20вам?%20Правда,%20неплохо?&language_id=ru&speaker_id=Dionisio%20Schuyler

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

Однако, иногда в синтез вкрадываются непонятные артифакты, как на следующем примере:

Текст не менялся, но в результате откуда-то взялся “пан” (или “пам”?) В общем, какой-то глюк…

Ну а теперь, попробуем сделать из этого что-то применимое на практике! Моя задумка была в том, чтобы научить телеграм бота отправлять голосовые сообщения в чатик. Для этой задачи был написан следующий пример кода, (как всегда) на php

Для успешной работы скрипта потребуется установить на сервер ffmpeg, поскольку для telegram наши аудио файлы придётся перекодировать в формат OGG.

<?php
// Функция для генерации речи через TTS-сервер
function generateTTS($text, $language, $speaker, $ttsUrl, $outputFile) {
    // Подготовка данных для запроса
    $params = http_build_query([
        'text' => $text,
        'language_id' => $language,
        'speaker_id' => $speaker,
    ]);
    
    // Выполнение GET-запроса к TTS-серверу
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $ttsUrl . "/api/tts?" . $params);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode === 200) {
        // Сохранение аудиофайла (например, в WAV)
        file_put_contents($outputFile, $response);
        return true;
    } else {
        echo "Ошибка при генерации речи: HTTP $httpCode\n";
        return false;
    }
}

// Функция для конвертации аудио в формат OGG
function convertToOgg($inputFile, $outputFile) {
    $command = "ffmpeg -y -i " . escapeshellarg($inputFile) . " -c:a libopus " . escapeshellarg($outputFile);
    exec($command, $output, $returnCode);
    
    if ($returnCode === 0) {
        echo "Аудиофайл успешно конвертирован в OGG: $outputFile\n";
        return true;
    } else {
        echo "Ошибка при конвертации аудио: " . implode("\n", $output) . "\n";
        return false;
    }
}

// Функция для отправки голосового сообщения в Telegram
function sendVoiceToTelegram($chatId, $voiceFile, $botToken) {
    // Подготовка данных для запроса
    $url = "https://api.telegram.org/bot$botToken/sendVoice";
    $postFields = [
        'chat_id' => $chatId,
        'voice' => new CURLFile($voiceFile)
    ];
    
    // Выполнение POST-запроса к Telegram API
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode === 200) {
        echo "Сообщение успешно отправлено в Telegram.\n";
    } else {
        echo "Ошибка при отправке сообщения в Telegram: HTTP $httpCode\n";
        echo "Ответ: $response\n";
    }
}

// Конфигурация
$ttsUrl = "http://IP_TTS_СЕРВЕРА:5002"; // URL TTS-сервера
$inputFile = "/tmp/speech.wav"; // Временный файл для оригинального аудио
$outputFile = "/tmp/speech.ogg"; // Файл для конвертации в OGG
$text = "Хочешь, я расскажу тебе сказку, дружок?"; // Текст для генерации
$language = "ru"; // Язык синтеза
$speaker = "Dionisio Schuyler"; // Говорун
$chatId = "........."; // ID чата в Telegram
$botToken = "......................."; // Токен вашего бота Telegram

// Генерация речи
if (generateTTS($text, $language, $speaker, $ttsUrl, $inputFile)) {
    // Конвертация в формат OGG
    if (convertToOgg($inputFile, $outputFile)) {
        // Отправка в Telegram
        sendVoiceToTelegram($chatId, $outputFile, $botToken);
    }
}

Позже, источником текста стал ChatGPT, но это уже немного другая история… :)

Итог:
Теперь у меня есть собственный TTS сервер. Не смотря на довольно долгий процесс генерации речи, такой вариант меня устраивает, ведь в мои задачи не входило генерировать речь на лету. Однако, если у вас имеется такое требование, синтез можно ускорить, переложив вычисления с процессора на приличную видеокарту с поддержкой CUDA. Поскольку у меня в сервере таковой пока не имеется, испытать движок на скорость вам придётся самостоятельно. ;)