BlueV2 / docker /Dockerfile
notmax123's picture
Claude Opus 5 (1M context)
Name the voice files after the voices
d0a3534
Raw History Blame Contribute Delete
5.48 kB
# BlueTTS 2.5 ONNX + NVIDIA GPU for RunPod Serverless (no gradio).
# Build from repo root:
# docker buildx build --platform linux/amd64 -f docker/Dockerfile \
# -t maxme123/blue-v2:serverless-v8 --push .
#
# Russian is included: RUAccent + RUPhon and their ~1 GB of weights are baked in
# below so a cold worker does not download them on its first Russian job. They
# need torch, which is installed from the CPU wheel index — the front end runs on
# CPU by design (BLUE_RU_G2P_DEVICE), and the CUDA torch build would add ~2 GB of
# libraries that duplicate what the base image and onnxruntime-gpu already carry.
# Build with --build-arg WITH_RUSSIAN=0 for a torch-free image without Russian.
ARG CUDA_IMAGE=nvidia/cuda:12.6.3-cudnn-runtime-ubuntu24.04
FROM ${CUDA_IMAGE}
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:$PATH" \
PYTHONPATH=/app \
BLUE_DOCKER=1 \
BLUE_ONNX_GPU=1 \
BLUE_SERVE_MODE=serverless \
BLUE_SKIP_GRADIO=1 \
GRADIO_ANALYTICS_ENABLED=False \
HF_HUB_DISABLE_TELEMETRY=1 \
ORT_NUM_THREADS=4 \
MAX_CONCURRENCY=4
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
python3-venv \
python3-dev \
git \
ffmpeg \
espeak-ng \
libespeak-ng1 \
libsndfile1 \
ca-certificates \
curl \
&& python3 --version \
&& ffmpeg -version | head -1 \
&& python3 -m venv /opt/venv \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
ARG WITH_RUSSIAN=1
COPY docker/requirements-serverless.txt /app/docker/requirements-serverless.txt
COPY docker/requirements-russian.txt /app/docker/requirements-russian.txt
# Order matters, and not for readability: ruphon depends on the CPU `onnxruntime`,
# which unpacks over the same directory as `onnxruntime-gpu`. Whichever pip writes
# last wins, so the Russian deps go in first and the serverless set — which pins
# onnxruntime-gpu — goes in last. The assert below is what turns a future flip
# into a failed build instead of a silently CPU-bound image.
RUN pip install --upgrade pip setuptools wheel \
&& if [ "$WITH_RUSSIAN" = "1" ]; then \
pip install --index-url https://download.pytorch.org/whl/cpu torch \
&& pip install -r /app/docker/requirements-russian.txt; \
fi \
&& pip install -r /app/docker/requirements-serverless.txt \
&& python -c "import onnxruntime as ort, sys; \
p = ort.get_available_providers(); \
print('ORT', ort.__version__, p); \
sys.exit('CUDAExecutionProvider missing — the CPU onnxruntime wheel clobbered the GPU one; check install order above') \
if 'CUDAExecutionProvider' not in p else None" \
&& if [ "$WITH_RUSSIAN" != "1" ]; then \
python -c "import torch" 2>/dev/null && echo 'ERROR: torch should not be installed' && exit 1 || echo 'OK: no torch'; \
fi
COPY app.py download_models.py tts.json vocab.json /app/
COPY g2p /app/g2p
COPY voices /app/voices
COPY onnx_slim /app/onnx_slim
COPY renikud.onnx /app/renikud.onnx
COPY docker/entrypoint.sh /app/docker/entrypoint.sh
COPY docker/rp_handler.py /app/docker/rp_handler.py
# Fail the build, not the first job, if the bundle is incomplete. `stats.npz` is
# what the vocoder input is denormalized with and `uncond.npz` is what makes
# cfg_scale do anything, so a v2 bundle baked in here would be a silent
# quality regression rather than a crash.
#
# The freshness markers are (re)written here rather than copied, so the baked-in
# weights are trusted at startup. Without them every cold start re-downloads
# renikud.onnx — 310 MB on the critical path of each new serverless worker.
RUN chmod +x /app/docker/entrypoint.sh \
&& for f in vocoder.onnx text_encoder.onnx vector_estimator.onnx \
duration_predictor_style.onnx stats.npz uncond.npz vocab.json; do \
test -s "/app/onnx_slim/$f" || { echo "MISSING onnx_slim/$f"; exit 1; }; \
done \
&& test -s /app/renikud.onnx \
&& python -c "import sys, os; \
from download_models import VOICE_CATALOG, BLUE_REPO, ONNX_BUNDLE_STAMP, RENIKUD_REPO, RENIKUD_STAMP; \
missing=[m['file'] for m in VOICE_CATALOG.values() if not os.path.exists('/app/voices/'+m['file'])]; \
sys.exit('MISSING voices: %s' % missing) if missing else print('voices OK:', list(VOICE_CATALOG)); \
open('/app/.renikud_repo','w').write(RENIKUD_REPO+chr(10)+RENIKUD_STAMP+chr(10)); \
open('/app/onnx_slim/.repo_id','w').write(BLUE_REPO+chr(10)+ONNX_BUNDLE_STAMP+chr(10)); \
open('/app/voices/.repo_id','w').write(BLUE_REPO+chr(10)+ONNX_BUNDLE_STAMP+chr(10)); \
print('markers written')"
# Bake the Russian weights in. RUAccent's dictionary + omograph model and RUPhon's
# graph are ~1 GB fetched on first use, which would otherwise land on the first
# Russian job of every cold worker. Phonemizing one sentence is what triggers the
# download, and it doubles as a build-time check that the front end actually loads
# (the transformers/tokenizer patch in g2p/russian_g2p.py is version-sensitive).
RUN if [ "$WITH_RUSSIAN" = "1" ]; then \
python -c "from g2p.russian_g2p import phonemize_russian; \
print('ru warm:', phonemize_russian('Взгляну́в на Санкт-Петербург, я всё понял.'))"; \
fi
# No HEALTHCHECK — RunPod serverless uses the runpod SDK job loop, not HTTP probes.
CMD ["/app/docker/entrypoint.sh"]