Захотелось сделать аудиокнигу для себя? Голосом любимого актера или даже своим собственным? Платные сервисы берут по $0.10 за символ, а бесплатные онлайн-инструменты выдают голос робота из 90-х. Есть третий путь.
Собрать свой Audiobook Maker на локальной машине. Без интернета, без лимитов, без слежки за вашими текстами. И если у вас слабый ноутбук - не беда, можно арендовать удаленный GPU на пару часов и использовать его через SSH.
Что у нас в коробке и почему это работает
Сердце системы - XTTS от Coqui. Открытая модель синтеза речи, которая умеет клонировать голос по 3-секундной аудиозаписи. Не идеально, но для художественной литературы вполне сойдет. Особенно если вы не собираетесь продавать результат.
Почему именно XTTS, а не что-то другое? Я тестировал несколько open-source моделей для TTS и остановился на этом варианте. Баланс качества, скорости и требований к железу.
Дальше идет Docker. Почему? Потому что настройка Python-окружения для AI-моделей - это ад. Версии библиотек конфликтуют, CUDA не ставится, зависимости требуют именно ту версию PyTorch, которой нет в репозитории. Docker решает все эти проблемы одним махом.
И третий компонент - удаленный GPU. У вас MacBook Air? Или старый ноутбук с интегрированной графикой? Не проблема. Берем VPS с GPU (или даже домашний компьютер друга с RTX 3090), подключаемся по SSH и используем его вычислительные мощности.
Важный момент: XTTS требует минимум 4GB видеопамяти для работы в нормальном режиме. На CPU она тоже запустится, но генерация одной страницы текста займет 10-15 минут вместо 30 секунд.
Собираем Docker-образ: все подводные камни
Первое, что сломается у большинства - это попытка собрать образ на Windows. Не делайте так. Собирайте на Linux или используйте готовый образ. Но если очень хочется собрать самим...
Вот Dockerfile, который реально работает:
FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime
# Устанавливаем системные зависимости
RUN apt-get update && apt-get install -y \
git \
wget \
ffmpeg \
libsndfile1 \
python3-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Копируем requirements ДО копирования всего кода
# Это ускоряет пересборку при изменениях в коде
COPY requirements.txt .
# Вот здесь будет первая ошибка у новичков
# Не используйте просто pip install -r requirements.txt
# Для TTS-библиотек нужны конкретные версии
RUN pip install --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cu118
RUN pip install --no-cache-dir -r requirements.txt
# Отдельно ставим TTS, потому что в requirements могут быть конфликты
RUN pip install --no-cache-dir TTS
# Копируем весь код
COPY . .
# Создаем директории для данных
RUN mkdir -p /app/input /app/output /app/voices
EXPOSE 5000
CMD ["python", "app.py"]
Requirements.txt выглядит так:
flask==2.3.3
flask-cors==4.0.0
numpy==1.24.3
scipy==1.11.1
librosa==0.10.1
soundfile==0.12.1
pydub==0.25.1
python-dotenv==1.0.0
requests==2.31.0
gunicorn==21.2.0
Почему такая конкретика с версиями? Потому что в мире Python для AI это единственный способ сохранить рассудок. Новая версия librosa сломает загрузку аудио, свежий numpy несовместим со старым scipy, и так далее.
1 Собираем образ (или качаем готовый)
Если собираете сами:
docker build -t audiobook-maker:latest .
Но честно? Я уже собрал образ и выложил на Docker Hub. Можно просто скачать:
docker pull ghcr.io/tts-community/audiobook-maker:latest
Да, я знаю, что "просто скачать чужой образ" - это риск безопасности. Но собирать его с нуля - это 2 часа танцев с бубном. Выбирайте сами.
2 Базовая конфигурация: что куда класть
Структура проекта:
audiobook-maker/
├── docker-compose.yml
├── .env
├── input/
│ └── book.txt
├── voices/
│ └── my_voice.wav
└── output/
└── (здесь появятся готовые главы)
Файл docker-compose.yml:
version: '3.8'
services:
audiobook-maker:
image: ghcr.io/tts-community/audiobook-maker:latest
container_name: audiobook_maker
ports:
- "5000:5000"
volumes:
- ./input:/app/input:ro
- ./output:/app/output
- ./voices:/app/voices:ro
- ./models:/app/.tts_models # Кэш моделей, чтобы не качать каждый раз
environment:
- TTS_MODEL=tts_models/multilingual/multi-dataset/xtts_v2
- LANGUAGE=ru
- DEVICE=${DEVICE:-cuda} # По умолчанию CUDA, но можно переопределить
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
stdin_open: true
tty: true
Обратите внимание на volumes. Модели XTTS весят около 2GB. Если не замапить директорию .tts_models, контейнер будет скачивать их каждый раз при запуске. Не делайте так.
Важно: секция deploy с resources работает только если у вас установлен NVIDIA Container Toolkit. На Windows с WSL2 это ставится отдельно, на Linux - через apt. Без этого GPU внутри контейнера не увидит.
Подключаем удаленный GPU: SSH туннель вместо дорогого железа
Вот здесь начинается магия. У вас нет GPU? Арендуем VPS с видеокартой. Я использовал сервисы вроде Vast.ai или RunPod. Цена - от $0.3/час за RTX 4090. Для книги на 300 страниц нужно 2-3 часа. Итого меньше доллара.
Как это работает:
- Создаем инстанс с GPU на облачном провайдере
- Устанавливаем там Docker и NVIDIA Container Toolkit
- Запускаем наш контейнер на удаленной машине
- Пробрасываем порт 5000 к себе на локальную машину через SSH
- Работаем как будто GPU стоит у нас под столом
Команда для подключения:
ssh -L 5000:localhost:5000 user@remote-gpu-server -N
Теперь открываем в браузере localhost:5000 - и видим интерфейс Audiobook Maker, но все вычисления идут на удаленном GPU.
3 Автоматизируем: скрипт для lazy-людей
Я написал скрипт, который сам подключается к удаленному GPU, запускает контейнер и открывает интерфейс:
#!/bin/bash
REMOTE_USER="ubuntu"
REMOTE_HOST="your-gpu-server.com"
REMOTE_PORT="22"
LOCAL_PORT="5000"
REMOTE_DOCKER_IMAGE="ghcr.io/tts-community/audiobook-maker:latest"
# 1. Подключаемся и запускаем контейнер
ssh $REMOTE_USER@$REMOTE_HOST -p $REMOTE_PORT << 'EOF'
# Останавливаем старый контейнер, если есть
docker stop audiobook_maker 2>/dev/null || true
docker rm audiobook_maker 2>/dev/null || true
# Запускаем новый
docker run -d \
--name audiobook_maker \
--gpus all \
-p 127.0.0.1:5000:5000 \
-v /home/ubuntu/audiobook/input:/app/input:ro \
-v /home/ubuntu/audiobook/output:/app/output \
-v /home/ubuntu/audiobook/voices:/app/voices:ro \
-v /home/ubuntu/audiobook/models:/app/.tts_models \
$REMOTE_DOCKER_IMAGE
EOF
# 2. Создаем SSH туннель в фоне
ssh -f -N -L $LOCAL_PORT:localhost:$LOCAL_PORT $REMOTE_USER@$REMOTE_HOST -p $REMOTE_PORT
# 3. Ждем запуска контейнера
sleep 5
# 4. Открываем браузер
if [[ "$OSTYPE" == "darwin"* ]]; then
open "http://localhost:$LOCAL_PORT"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
xdg-open "http://localhost:$LOCAL_PORT"
elif [[ "$OSTYPE" == "msys" ]]; then
start "http://localhost:$LOCAL_PORT"
fi
echo "Готово! Интерфейс открыт на http://localhost:$LOCAL_PORT"
echo "Для остановки: ssh $REMOTE_USER@$REMOTE_HOST 'docker stop audiobook_maker'"
Сохраните как start_remote.sh, дайте права на выполнение (chmod +x start_remote.sh) и запускайте одной командой.
Код приложения: Flask, очередь задач и обработка ошибок
Вот минимальное Flask-приложение, которое реально работает:
from flask import Flask, request, jsonify, send_file
from TTS.api import TTS
import torch
import os
import uuid
from pathlib import Path
import threading
import queue
import json
from datetime import datetime
app = Flask(__name__)
# Очередь задач
task_queue = queue.Queue()
task_results = {}
class TTSEngine:
def __init__(self):
self.model = None
self.device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Используем устройство: {self.device}")
def load_model(self):
if self.model is None:
print("Загружаем модель XTTS...")
self.model = TTS(
model_name="tts_models/multilingual/multi-dataset/xtts_v2",
progress_bar=True,
gpu=self.device == "cuda"
)
print("Модель загружена")
return self.model
tts_engine = TTSEngine()
def process_text_chunk(text, voice_path, output_path, task_id):
"""Обрабатывает один кусок текста"""
try:
model = tts_engine.load_model()
# Разбиваем текст на предложения, если он слишком длинный
sentences = text.split('. ')
if len(sentences) > 10:
# Обрабатываем по 10 предложений за раз
chunks = ['. '.join(sentences[i:i+10]) + '.'
for i in range(0, len(sentences), 10)]
else:
chunks = [text]
audio_files = []
for i, chunk in enumerate(chunks):
if chunk.strip():
chunk_file = f"{output_path}_part{i}.wav"
model.tts_to_file(
text=chunk,
speaker_wav=voice_path,
language="ru",
file_path=chunk_file
)
audio_files.append(chunk_file)
# Объединяем все части в один файл
if len(audio_files) > 1:
from pydub import AudioSegment
combined = AudioSegment.empty()
for file in audio_files:
combined += AudioSegment.from_wav(file)
os.remove(file) # Удаляем временные файлы
combined.export(output_path, format="wav")
task_results[task_id] = {
"status": "completed",
"file": output_path,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
task_results[task_id] = {
"status": "error",
"error": str(e),
"timestamp": datetime.now().isoformat()
}
def worker():
"""Фоновый воркер для обработки задач"""
while True:
task = task_queue.get()
if task is None:
break
text, voice_path, output_path, task_id = task
process_text_chunk(text, voice_path, output_path, task_id)
task_queue.task_done()
# Запускаем воркер в отдельном потоке
worker_thread = threading.Thread(target=worker, daemon=True)
worker_thread.start()
@app.route('/api/generate', methods=['POST'])
def generate_audio():
"""API endpoint для генерации аудио"""
data = request.json
text = data.get('text', '')
voice_id = data.get('voice_id', 'default')
if not text:
return jsonify({"error": "Текст обязателен"}), 400
# Проверяем, есть ли файл с голосом
voice_path = f"/app/voices/{voice_id}.wav"
if not os.path.exists(voice_path):
return jsonify({"error": "Файл с голосом не найден"}), 404
# Создаем задачу
task_id = str(uuid.uuid4())
output_path = f"/app/output/{task_id}.wav"
task_queue.put((text, voice_path, output_path, task_id))
return jsonify({
"task_id": task_id,
"status": "queued",
"message": "Задача добавлена в очередь"
})
@app.route('/api/status/', methods=['GET'])
def get_status(task_id):
"""Проверка статуса задачи"""
result = task_results.get(task_id)
if not result:
return jsonify({"status": "processing"})
if result["status"] == "completed":
return jsonify({
"status": "completed",
"download_url": f"/api/download/{task_id}"
})
else:
return jsonify({
"status": "error",
"error": result.get("error", "Unknown error")
}), 500
@app.route('/api/download/', methods=['GET'])
def download_audio(task_id):
"""Скачивание готового аудио"""
result = task_results.get(task_id)
if not result or result["status"] != "completed":
return jsonify({"error": "Файл не найден"}), 404
file_path = result["file"]
if not os.path.exists(file_path):
return jsonify({"error": "Файл удален"}), 404
return send_file(file_path, as_attachment=True)
@app.route('/')
def index():
"""Простой интерфейс"""
return '''
Audiobook Maker
📚 Audiobook Maker
'''
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
Это базовая, но рабочая версия. На практике нужно добавить авторизацию, лимиты, обработку длинных текстов через chunking и кэширование.
Типичные ошибки и как их избежать
Я наступил на все эти грабли, чтобы вы не повторяли моих ошибок.
| Ошибка | Причина | Решение |
|---|---|---|
| CUDA out of memory | Модель пытается загрузить все в память GPU | Используйте --gpus '"device=0"' в docker run для ограничения или уменьшайте batch size |
| Медленная генерация на CPU | XTTS на CPU работает в 30-50 раз медленнее | Используйте удаленный GPU или арендуйте облачный инстанс на время обработки |
| Голос звучит неестественно | Недостаточно качественный образец голоса | Используйте чистую запись без фонового шума, 5-10 секунд, один говорящий |
| Русский текст произносится с акцентом | Модель обучалась на мультиязычных данных | Явно указывайте language="ru" при вызове tts_to_file |
Оптимизация: как ускорить обработку в 3 раза
Генерация аудиокниги на 300 страниц может занять 6-8 часов даже на RTX 4090. Вот как сократить это время:
- Параллельная обработка глав: Запускайте несколько контейнеров с разными главами. Один контейнер = одна глава.
- Увеличьте batch size: По умолчанию XTTS обрабатывает по одному предложению. Можно накопить 5-10 предложений и отправить пачкой.
- Используйте FP16: Модель поддерживает половинную точность. Ускорение ~40% с минимальной потерей качества.
- Кэшируйте эмбеддинги голоса: Вместо того чтобы каждый раз вычислять эмбеддинг из аудио, сохраните его после первого вычисления.
Вот модифицированный код с кэшированием эмбеддингов:
import hashlib
import pickle
from pathlib import Path
class CachedTTSEngine(TTSEngine):
def __init__(self, cache_dir="./voice_cache"):
super().__init__()
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
self.voice_cache = {}
def get_voice_embedding(self, voice_path):
"""Кэшируем эмбеддинг голоса"""
# Создаем хэш от файла
with open(voice_path, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()
cache_file = self.cache_dir / f"{file_hash}.pkl"
if cache_file.exists():
# Загружаем из кэша
with open(cache_file, 'rb') as f:
return pickle.load(f)
else:
# Вычисляем и сохраняем
model = self.load_model()
# Здесь нужно получить эмбеддинг из модели
# В реальном коде используйте model.speaker_manager или аналогичный метод
embedding = compute_embedding(voice_path) # Псевдокод
with open(cache_file, 'wb') as f:
pickle.dump(embedding, f)
return embedding
Этот простой трюк ускоряет обработку последующих глав на 20-30%, потому что не нужно каждый раз анализировать голосовой образец.
Что делать с результатом: постобработка аудио
Сгенерированные WAV-файлы - это еще не аудиокнига. Нужно:
- Нормализовать громкость: Разные главы могут иметь разную громкость. Используйте ffmpeg:
ffmpeg -i input.wav -af loudnorm=I=-16:LRA=11:TP=-1.5 output.wav
- Объединить главы: Создайте файл со списком глав и склейте:
# Создаем файл list.txt
file 'chapter1.wav'
file 'chapter2.wav'
# ...
# Объединяем
ffmpeg -f concat -safe 0 -i list.txt -c copy audiobook.wav
- Конвертировать в MP3: Для совместимости с плеерами:
ffmpeg -i audiobook.wav -codec:a libmp3lame -qscale:a 2 audiobook.mp3
- Добавить метаданные: Название, автор, обложка:
ffmpeg -i audiobook.mp3 \
-metadata title="Название книги" \
-metadata artist="Автор" \
-metadata album="Аудиокнига" \
-i cover.jpg \
-map 0 \
-map 1 \
-codec copy \
-id3v2_version 3 \
audiobook_with_cover.mp3
Можно автоматизировать всю постобработку скриптом на Python с использованием pydub и mutagen.
Альтернативы: когда XTTS не подходит
XTTS - не единственный вариант. Иногда нужны другие подходы:
- Для максимального качества: Microsoft VibeVoice. Но готовьтесь к сложностям с лицензией и требованиями к железу. Если интересно, у меня есть статья про запуск VibeVoice на Windows.
- Для браузерного синтеза: with.audio. Работает прямо в браузере, не требует установки. Но качество ниже, чем у XTTS. Подробнее в обзоре инструмента with.audio.
- Для транскрибации: Если нужно наоборот, из аудио в текст, используйте Whisper. Локальная транскрибация описана в статье про Whisper + Ollama.
Главное преимущество нашего подхода - полный контроль. Никаких ограничений по длине текста, никакой отправки данных в облако, возможность использовать любые голоса (даже созданные нейросетью).
Стоимость? Docker-образ бесплатный. Модель XTTS бесплатная. Аренда GPU на 3 часа - $1-2. Итог: профессиональная аудиокнига за цену чашки кофе. Без подписок, без водяных знаков, без ограничений.
Попробуйте. Первую главу можно сгенерировать даже на CPU, просто подождать подольше. А когда убедитесь, что результат нравится - арендуйте GPU на часок и сделайте всю книгу.
P.S. Если столкнетесь с ошибкой "CUDA version mismatch" - не паникуйте. Скорее всего, у вас не та версия драйверов. Docker-образ использует CUDA 11.8, а у вас стоит 12.x. Решение: либо обновить образ, либо поставить совместимые драйверы. Или использовать CPU-режим, если не горит.