Skip to content

Commit f772e49

Browse files
authored
Merge pull request #380 from bluecrystalsolutions/pr/docker-streaming
Add Docker deployment with reverse-proxy streaming support
2 parents f5a1c6a + b74c73f commit f772e49

7 files changed

Lines changed: 339 additions & 2 deletions

File tree

.dockerignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Model weights (mounted at runtime via volumes)
2+
models/
3+
data/
4+
lora/
5+
output/
6+
7+
# Git history
8+
.git/
9+
10+
# Python cache
11+
__pycache__/
12+
*.pyc
13+
*.pyo
14+
*.egg-info/
15+
.venv/
16+
venv/
17+
.venv-bench/
18+
19+
# Docker config (not needed inside image)
20+
docker/docker-compose.yml
21+
docker/nginx.conf
22+
docker/README.md
23+
24+
# IDE / OS
25+
.DS_Store
26+
.vscode/

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ voxcpm.egg-info
55
.DS_Store
66
./pretrained_models/
77
app_local.py
8+
9+
# Docker volume mount directories (large files, user-specific)
10+
models/
11+
data/
12+
lora/
13+
output/

docker/Dockerfile

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# ─────────────────────────────────────────────────────────────────────
2+
# VoxCPM Training WebUI — Docker image
3+
# ─────────────────────────────────────────────────────────────────────
4+
# Base: PyTorch with CUDA for GPU-accelerated LoRA fine-tuning.
5+
# Build context should be the project root:
6+
#
7+
# docker build -f docker/Dockerfile -t voxcpm-training .
8+
#
9+
# ─────────────────────────────────────────────────────────────────────
10+
FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel
11+
12+
LABEL maintainer="OpenBMB <openbmb@gmail.com>"
13+
LABEL description="VoxCPM LoRA Training WebUI with GPU support"
14+
15+
# Avoid interactive prompts during package installation
16+
ENV DEBIAN_FRONTEND=noninteractive
17+
18+
# System deps required by Python packages:
19+
# git — setuptools_scm needs it to resolve version in pyproject.toml
20+
# libsndfile1 — C library backing the 'soundfile' Python package
21+
# ffmpeg — audio codec support for torchaudio/librosa
22+
RUN apt-get update && apt-get install -y --no-install-recommends \
23+
git \
24+
libsndfile1 \
25+
ffmpeg \
26+
&& rm -rf /var/lib/apt/lists/*
27+
28+
WORKDIR /app
29+
30+
# Layer 1: Install dependencies only (cached unless pyproject.toml changes)
31+
# Create a minimal package stub so pip can resolve deps without real source.
32+
COPY pyproject.toml /app/
33+
RUN mkdir -p /app/src/voxcpm && echo '__version__ = "0.0.0"' > /app/src/voxcpm/__init__.py
34+
ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0
35+
RUN pip install --no-cache-dir -e .
36+
37+
# Layer 2: Copy full project source (cheap rebuild on code changes)
38+
COPY . /app/
39+
40+
# Create default directories and declare volumes
41+
RUN mkdir -p /app/lora /app/models /app/output /app/data
42+
VOLUME ["/app/models", "/app/lora", "/app/output", "/app/data"]
43+
44+
EXPOSE 7860
45+
46+
# Environment variables for configuration
47+
ENV GRADIO_SERVER_PORT=7860
48+
ENV GRADIO_ROOT_PATH=""
49+
ENV HF_HOME=/app/models
50+
51+
# Default: launch training WebUI
52+
CMD ["python", "lora_ft_webui.py"]

docker/README.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Docker Support for VoxCPM Training WebUI
2+
3+
Run the VoxCPM LoRA fine-tuning WebUI in a Docker container with full GPU support and nginx reverse proxy.
4+
5+
## Prerequisites
6+
7+
- Docker Engine 19.03+ with [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html)
8+
- NVIDIA GPU with CUDA 12.4+ compatible drivers
9+
- At least 16 GB GPU VRAM (24 GB+ recommended for larger models)
10+
11+
## Quick Start
12+
13+
```bash
14+
# From the project root directory:
15+
docker compose -f docker/docker-compose.yml up --build
16+
```
17+
18+
This starts:
19+
- **training-webui** — the Gradio-based training interface on port 7860
20+
- **nginx** — reverse proxy serving the WebUI at `http://localhost/webui/`
21+
22+
Access the WebUI at **http://localhost/webui/**.
23+
24+
## Volume Mounts
25+
26+
The compose file maps host directories to container paths. Create these directories at the project root before starting:
27+
28+
```
29+
VoxCPM/
30+
├── docker/
31+
│ ├── docker-compose.yml
32+
│ ├── Dockerfile
33+
│ └── nginx.conf
34+
├── models/ ← Pretrained model weights (or auto-downloaded via HF)
35+
│ ├── openbmb__VoxCPM2/
36+
│ └── openbmb__VoxCPM1.5/
37+
├── data/ ← Training manifests + audio files
38+
│ ├── train.jsonl
39+
│ ├── val.jsonl (optional)
40+
│ └── audio/
41+
│ ├── speaker1_001.wav
42+
│ └── ...
43+
├── lora/ ← LoRA training output (created automatically)
44+
│ └── my-voice-2024/
45+
│ ├── checkpoints/
46+
│ ├── logs/
47+
│ └── train_config.yaml
48+
└── output/ ← Additional training artifacts
49+
```
50+
51+
### Mount Reference
52+
53+
| Host Path | Container Path | Purpose |
54+
|-----------|---------------|---------|
55+
| `./models/` | `/app/models` | Pretrained model weights and HF cache (`HF_HOME`). Pre-populate with model dirs (e.g., `openbmb__VoxCPM2/`) or leave empty — models auto-download on first run and persist here. |
56+
| `./data/` | `/app/data` | Training data. Put JSONL manifests and audio files here. In the WebUI, reference paths as `/app/data/train.jsonl`. |
57+
| `./lora/` | `/app/lora` | LoRA checkpoint output. After training, find results in `lora/<run-name>/checkpoints/`. Also used to resume training from existing checkpoints. |
58+
| `./output/` | `/app/output` | Miscellaneous training artifacts. |
59+
60+
### Training Data Format
61+
62+
The train manifest is a JSONL file where each line references an audio file:
63+
64+
```json
65+
{"audio_path": "/app/data/audio/speaker1_001.wav", "text": "Hello world", "speaker": "speaker1"}
66+
```
67+
68+
Use absolute container paths (`/app/data/...`) in your manifest so the container can find the files.
69+
70+
### Models
71+
72+
If `models/openbmb__VoxCPM2/` exists on the host, the app loads directly from that path — no network access needed. If the directory is empty or missing, `from_pretrained` falls back to `snapshot_download` from HuggingFace Hub.
73+
74+
The Dockerfile sets `HF_HOME=/app/models` so any Hub downloads land in the same mounted volume. This means models persist across container restarts regardless of whether they were pre-populated or auto-downloaded.
75+
76+
**Recommended:** Pre-populate to avoid first-run download delay:
77+
78+
```bash
79+
huggingface-cli download openbmb/VoxCPM2 --local-dir ./models/openbmb__VoxCPM2
80+
```
81+
82+
The Dockerfile creates empty `/app/models`, `/app/lora`, `/app/output` directories, but the volume mounts override them with your host directories.
83+
84+
## Health Check
85+
86+
The nginx proxy forwards `GET /` to the training-webui backend, so load balancer health checks (AWS ALB, etc.) reflect real application health — returning 502 when the backend is down. This is separate from the WebUI at `/webui/`.
87+
88+
```bash
89+
curl http://localhost/
90+
```
91+
92+
## Direct Access (no proxy)
93+
94+
If you want to bypass nginx and access Gradio directly:
95+
96+
```bash
97+
docker compose -f docker/docker-compose.yml up --build training-webui
98+
```
99+
100+
Set `GRADIO_ROOT_PATH=` (empty) in the compose file when running without the proxy, then access at `http://localhost:7860`.
101+
102+
## Building Manually
103+
104+
```bash
105+
# Build the image
106+
docker build -f docker/Dockerfile -t voxcpm-training .
107+
108+
# Run with GPU access (no reverse proxy)
109+
docker run --gpus all -p 7860:7860 \
110+
-v ./models:/app/models \
111+
-v ./data:/app/data \
112+
-v ./lora:/app/lora \
113+
-v ./output:/app/output \
114+
voxcpm-training
115+
```
116+
117+
## Environment Variables
118+
119+
| Variable | Default | Description |
120+
|----------|---------|-------------|
121+
| `GRADIO_SERVER_PORT` | `7860` | Port for the WebUI server |
122+
| `GRADIO_ROOT_PATH` | `""` | URL prefix when behind a reverse proxy (e.g., `/webui`) |
123+
124+
## Reverse Proxy
125+
126+
The included `docker-compose.yml` ships with an nginx reverse proxy that serves the WebUI at `/webui/`. The `GRADIO_ROOT_PATH=/webui` env var ensures Gradio generates correct URLs for assets and WebSocket connections.
127+
128+
### Custom nginx config
129+
130+
Edit `docker/nginx.conf` to change the location prefix or add TLS.
131+
132+
### Traefik Example (labels)
133+
134+
```yaml
135+
labels:
136+
- "traefik.http.routers.voxcpm.rule=PathPrefix(`/webui`)"
137+
- "traefik.http.services.voxcpm.loadbalancer.server.port=7860"
138+
```
139+
140+
## Viewing Training Logs
141+
142+
Training subprocess output is streamed to stdout, visible via:
143+
144+
```bash
145+
docker compose -f docker/docker-compose.yml logs -f training-webui
146+
```
147+
148+
## Troubleshooting
149+
150+
- **"no NVIDIA GPU detected"**: Ensure the NVIDIA Container Toolkit is installed and `docker run --gpus all nvidia-smi` works.
151+
- **OOM errors**: Reduce batch size in the WebUI or use a GPU with more VRAM.
152+
- **WebUI not accessible**: Check that port 80 (nginx) or 7860 (direct) isn't blocked by a firewall.
153+
- **WebSocket errors behind proxy**: Ensure your proxy forwards `Upgrade` and `Connection` headers (the included nginx.conf handles this).
154+
- **Health check failing**: Ensure the training-webui container is running — `curl http://localhost/` proxies to the backend and returns 502 if it's unreachable.
155+
- **Mixed-content / audio not playing over HTTPS**: The nginx config uses `map $http_x_forwarded_proto` to pass the correct protocol through to Gradio. This ensures `https://` file URLs are generated when accessed via HTTPS through a load balancer.

docker/docker-compose.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
services:
2+
training-webui:
3+
build:
4+
context: ..
5+
dockerfile: docker/Dockerfile
6+
ports:
7+
- "7860:7860"
8+
volumes:
9+
# Pretrained model weights + HF cache (HF_HOME=/app/models in Dockerfile).
10+
# Pre-populate with model dirs, or leave empty — auto-downloads on first run.
11+
- ../models:/app/models
12+
13+
# Training data: JSONL manifests and audio files.
14+
# Reference paths inside the container as /app/data/train.jsonl etc.
15+
- ../data:/app/data
16+
17+
# LoRA training output — checkpoints, configs, logs.
18+
# Results appear in lora/<run-name>/checkpoints/ after training.
19+
- ../lora:/app/lora
20+
21+
# Additional training artifacts.
22+
- ../output:/app/output
23+
deploy:
24+
resources:
25+
reservations:
26+
devices:
27+
- driver: nvidia
28+
count: 1
29+
capabilities: [gpu]
30+
environment:
31+
- GRADIO_SERVER_PORT=7860
32+
- GRADIO_ROOT_PATH=/webui # Matches nginx location block
33+
restart: unless-stopped
34+
35+
nginx:
36+
image: nginx:alpine
37+
ports:
38+
- "80:80"
39+
volumes:
40+
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
41+
depends_on:
42+
- training-webui
43+
restart: unless-stopped

docker/nginx.conf

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Preserve X-Forwarded-Proto from upstream load balancer (e.g. AWS ALB).
2+
# If ALB already set it to "https", pass that through instead of $scheme
3+
# (which is "http" since ALB→nginx is unencrypted). Falls back to $scheme
4+
# when accessed directly (no upstream proxy).
5+
map $http_x_forwarded_proto $forwarded_proto {
6+
default $http_x_forwarded_proto;
7+
"" $scheme;
8+
}
9+
10+
server {
11+
listen 80;
12+
server_name _;
13+
14+
absolute_redirect off;
15+
16+
# Health check for load balancers (AWS ALB, etc.)
17+
location = / {
18+
proxy_pass http://training-webui:7860/;
19+
proxy_set_header Host $host;
20+
proxy_read_timeout 5s;
21+
proxy_connect_timeout 3s;
22+
access_log off;
23+
}
24+
25+
location = /manifest.json {
26+
return 200 '{"name":"VoxCPM Training","short_name":"VoxCPM","start_url":"/webui/"}';
27+
default_type application/json;
28+
}
29+
30+
location = /favicon.ico {
31+
return 204;
32+
access_log off;
33+
}
34+
35+
location /webui/ {
36+
proxy_pass http://training-webui:7860/;
37+
proxy_set_header Host $host;
38+
proxy_set_header X-Real-IP $remote_addr;
39+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
40+
proxy_set_header X-Forwarded-Proto $forwarded_proto;
41+
42+
# WebSocket support (required for Gradio)
43+
proxy_http_version 1.1;
44+
proxy_set_header Upgrade $http_upgrade;
45+
proxy_set_header Connection "upgrade";
46+
47+
# Increase timeouts for long-running training operations
48+
proxy_read_timeout 300s;
49+
proxy_send_timeout 300s;
50+
}
51+
}

lora_ft_webui.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ def run_process():
500500

501501
assert training_process.stdout is not None
502502
for line in training_process.stdout:
503+
print(line, end="", flush=True) # Stream to stdout (Docker logs)
503504
training_log += line
504505
# Keep log size manageable
505506
if len(training_log) > 100000:
@@ -1322,6 +1323,9 @@ def change_language(lang):
13221323
)
13231324

13241325
if __name__ == "__main__":
1325-
# Ensure lora directory exists
13261326
os.makedirs("lora", exist_ok=True)
1327-
app.queue().launch(server_name="0.0.0.0", server_port=7860)
1327+
port = int(os.environ.get("GRADIO_SERVER_PORT", "7860"))
1328+
root_path = os.environ.get("GRADIO_ROOT_PATH", "")
1329+
1330+
print(f"\U0001f399\ufe0f VoxCPM Training WebUI: http://0.0.0.0:{port}{root_path}", flush=True)
1331+
app.queue().launch(server_name="0.0.0.0", server_port=port, root_path=root_path)

0 commit comments

Comments
 (0)