Создание аудиокниг локально с Docker и удаленным GPU: полный гайд | AiManual
AiManual Logo Ai / Manual.
03 Янв 2026 Гайд

Создаем аудиокнигу на ноутбуке с удаленным GPU: Docker, XTTS и немного магии

Пошаговый туториал по созданию аудиокниг на своем компьютере с использованием Docker, XTTS модели и удаленного GPU через SSH. Без облаков, без подписок.

Захотелось сделать аудиокнигу для себя? Голосом любимого актера или даже своим собственным? Платные сервисы берут по $0.10 за символ, а бесплатные онлайн-инструменты выдают голос робота из 90-х. Есть третий путь.

Собрать свой Audiobook Maker на локальной машине. Без интернета, без лимитов, без слежки за вашими текстами. И если у вас слабый ноутбук - не беда, можно арендовать удаленный GPU на пару часов и использовать его через SSH.

💡
Это не просто "еще один туториал по Docker". Я покажу реальную рабочую конфигурацию, которую сам использую для создания аудиокниг. С ошибками, костылями и работающими решениями.

Что у нас в коробке и почему это работает

Сердце системы - 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 часа. Итого меньше доллара.

Как это работает:

  1. Создаем инстанс с GPU на облачном провайдере
  2. Устанавливаем там Docker и NVIDIA Container Toolkit
  3. Запускаем наш контейнер на удаленной машине
  4. Пробрасываем порт 5000 к себе на локальную машину через SSH
  5. Работаем как будто GPU стоит у нас под столом

Команда для подключения:

ssh -L 5000:localhost:5000 user@remote-gpu-server -N

Теперь открываем в браузере localhost:5000 - и видим интерфейс Audiobook Maker, но все вычисления идут на удаленном GPU.

💡
Этот же подход работает для любых AI-задач. Хотите запустить голосового ассистента на RTX 3090, но у вас только ноутбук? SSH туннель решит проблему.

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. Вот как сократить это время:

  1. Параллельная обработка глав: Запускайте несколько контейнеров с разными главами. Один контейнер = одна глава.
  2. Увеличьте batch size: По умолчанию XTTS обрабатывает по одному предложению. Можно накопить 5-10 предложений и отправить пачкой.
  3. Используйте FP16: Модель поддерживает половинную точность. Ускорение ~40% с минимальной потерей качества.
  4. Кэшируйте эмбеддинги голоса: Вместо того чтобы каждый раз вычислять эмбеддинг из аудио, сохраните его после первого вычисления.

Вот модифицированный код с кэшированием эмбеддингов:

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%, потому что не нужно каждый раз анализировать голосовой образец.

💡
Если вам нужно обрабатывать действительно большие объемы текста, посмотрите как построить ML-песочницу на k8s. Масштабируйте обработку на несколько GPU и нод.

Что делать с результатом: постобработка аудио

Сгенерированные WAV-файлы - это еще не аудиокнига. Нужно:

  1. Нормализовать громкость: Разные главы могут иметь разную громкость. Используйте ffmpeg:
ffmpeg -i input.wav -af loudnorm=I=-16:LRA=11:TP=-1.5 output.wav
  1. Объединить главы: Создайте файл со списком глав и склейте:
# Создаем файл list.txt
file 'chapter1.wav'
file 'chapter2.wav'
# ...

# Объединяем
ffmpeg -f concat -safe 0 -i list.txt -c copy audiobook.wav
  1. Конвертировать в MP3: Для совместимости с плеерами:
ffmpeg -i audiobook.wav -codec:a libmp3lame -qscale:a 2 audiobook.mp3
  1. Добавить метаданные: Название, автор, обложка:
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-режим, если не горит.