mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
fix: docker frontend + docs cleanup
This commit is contained in:
@@ -77,6 +77,7 @@ def create_app() -> FastAPI:
|
|||||||
_configure_cors(application)
|
_configure_cors(application)
|
||||||
register_routers(application)
|
register_routers(application)
|
||||||
_register_lifecycle(application)
|
_register_lifecycle(application)
|
||||||
|
_mount_frontend(application)
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
|
||||||
@@ -104,6 +105,43 @@ def _configure_cors(application: FastAPI) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mount_frontend(application: FastAPI) -> None:
|
||||||
|
"""Serve the built web frontend when present (Docker / web deployment).
|
||||||
|
|
||||||
|
The Dockerfile copies the Vite build output to ``/app/frontend/``. When
|
||||||
|
that directory exists we mount static assets and add a catch-all route so
|
||||||
|
the React SPA handles client-side routing. In dev or API-only mode the
|
||||||
|
directory is absent and this function is a no-op.
|
||||||
|
"""
|
||||||
|
frontend_dir = Path(__file__).resolve().parent.parent / "frontend"
|
||||||
|
if not frontend_dir.is_dir():
|
||||||
|
return
|
||||||
|
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
|
# Mount hashed assets (JS, CSS, images) that Vite places under /assets
|
||||||
|
assets_dir = frontend_dir / "assets"
|
||||||
|
if assets_dir.is_dir():
|
||||||
|
application.mount(
|
||||||
|
"/assets",
|
||||||
|
StaticFiles(directory=str(assets_dir)),
|
||||||
|
name="frontend-assets",
|
||||||
|
)
|
||||||
|
|
||||||
|
# SPA catch-all: serve files if they exist, otherwise index.html for
|
||||||
|
# client-side routes like /voices, /stories, /models, etc.
|
||||||
|
@application.get("/{full_path:path}")
|
||||||
|
async def serve_spa(full_path: str):
|
||||||
|
file_path = (frontend_dir / full_path).resolve()
|
||||||
|
# Guard against path traversal — only serve files inside frontend_dir
|
||||||
|
if full_path and file_path.is_file() and str(file_path).startswith(str(frontend_dir)):
|
||||||
|
return FileResponse(file_path)
|
||||||
|
return FileResponse(frontend_dir / "index.html", media_type="text/html")
|
||||||
|
|
||||||
|
logger.info("Frontend: serving SPA from %s", frontend_dir)
|
||||||
|
|
||||||
|
|
||||||
def _get_gpu_status() -> str:
|
def _get_gpu_status() -> str:
|
||||||
"""Return a human-readable string describing GPU availability."""
|
"""Return a human-readable string describing GPU availability."""
|
||||||
backend_type = get_backend_type()
|
backend_type = get_backend_type()
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .. import config, models
|
from .. import config, models
|
||||||
@@ -15,12 +17,18 @@ from ..utils.platform_detect import get_backend_type
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Frontend build directory — present in Docker, absent in dev/API-only mode
|
||||||
|
_frontend_dir = Path(__file__).resolve().parent.parent.parent / "frontend"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""Root endpoint."""
|
"""Root endpoint — serves SPA index.html in Docker, JSON otherwise."""
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
|
||||||
|
index = _frontend_dir / "index.html"
|
||||||
|
if index.is_file():
|
||||||
|
return FileResponse(index, media_type="text/html")
|
||||||
return {"message": "voicebox API", "version": __version__}
|
return {"message": "voicebox API", "version": __version__}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -6,11 +6,7 @@ This is a Next.js application generated with
|
|||||||
Run development server:
|
Run development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
bun run dev
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open http://localhost:3000 with your browser to see the result.
|
Open http://localhost:3000 with your browser to see the result.
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
|
||||||
import { baseOptions } from '@/lib/layout.shared';
|
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
|
||||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
redirect('/docs');
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { source } from '@/lib/source';
|
|
||||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||||
import { baseOptions } from '@/lib/layout.shared';
|
import { baseOptions } from '@/lib/layout.shared';
|
||||||
|
import { source } from '@/lib/source';
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
export default function Layout({ children }: LayoutProps<'/[[...slug]]'>) {
|
||||||
return (
|
return (
|
||||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||||
{children}
|
{children}
|
||||||
@@ -7,7 +7,7 @@ import { APIPage } from '@/components/api-page';
|
|||||||
import { getPageImage, source } from '@/lib/source';
|
import { getPageImage, source } from '@/lib/source';
|
||||||
import { getMDXComponents } from '@/mdx-components';
|
import { getMDXComponents } from '@/mdx-components';
|
||||||
|
|
||||||
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
export default async function Page(props: PageProps<'/[[...slug]]'>) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const page = source.getPage(params.slug);
|
const page = source.getPage(params.slug);
|
||||||
if (!page) notFound();
|
if (!page) notFound();
|
||||||
@@ -59,7 +59,7 @@ export async function generateStaticParams() {
|
|||||||
return source.generateParams();
|
return source.generateParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
|
export async function generateMetadata(props: PageProps<'/[[...slug]]'>): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const page = source.getPage(params.slug);
|
const page = source.getPage(params.slug);
|
||||||
if (!page) notFound();
|
if (!page) notFound();
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
---
|
||||||
|
title: "Docker Deployment"
|
||||||
|
description: "Run Voicebox as a headless server with a web UI using Docker"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Voicebox can run as a Docker container with a full web UI -- no desktop app required. This is ideal for headless servers, shared GPU machines, or self-hosted deployments.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/jamiepine/voicebox.git
|
||||||
|
cd voicebox
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:17493](http://localhost:17493) in your browser. The full Voicebox UI is served directly from the backend.
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
The first build takes a few minutes (compiling the frontend, installing Python dependencies). Subsequent starts are fast thanks to Docker layer caching.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The Docker image uses a 3-stage build:
|
||||||
|
|
||||||
|
1. **Frontend** -- builds the React SPA with Bun and Vite
|
||||||
|
2. **Backend** -- installs Python dependencies and TTS model packages
|
||||||
|
3. **Runtime** -- combines both into a minimal image running the FastAPI server
|
||||||
|
|
||||||
|
The backend serves the web UI automatically when the built frontend is present. All API routes work exactly as they do in the desktop app.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### docker-compose.yml
|
||||||
|
|
||||||
|
The default `docker-compose.yml` binds to localhost only, mounts persistent volumes for data and model cache, and sets sensible resource limits:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
voicebox:
|
||||||
|
build: .
|
||||||
|
container_name: voicebox
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:17493:17493"
|
||||||
|
volumes:
|
||||||
|
- ./output:/app/data/generations
|
||||||
|
- voicebox-data:/app/data
|
||||||
|
- huggingface-cache:/home/voicebox/.cache/huggingface
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '4'
|
||||||
|
memory: 8G
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exposing to Your Network
|
||||||
|
|
||||||
|
By default the container only listens on `127.0.0.1`. To allow other machines on your network to connect, change the port binding:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:17493:17493"
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="warn">
|
||||||
|
The API has no built-in authentication. Only expose to trusted networks, or put a reverse proxy with auth in front of it.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `LOG_LEVEL` | `info` | Logging verbosity (`debug`, `info`, `warning`, `error`) |
|
||||||
|
| `VOICEBOX_MODELS_DIR` | (HuggingFace cache) | Custom path for model storage |
|
||||||
|
| `VOICEBOX_CORS_ORIGINS` | (local origins) | Additional CORS origins, comma-separated |
|
||||||
|
|
||||||
|
### Resource Limits
|
||||||
|
|
||||||
|
The default compose file limits the container to 4 CPUs and 8GB RAM. Adjust these based on your hardware:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '8'
|
||||||
|
memory: 16G
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
TTS model inference is memory-intensive. 8GB is the minimum for running a single engine. 16GB+ is recommended if you want multiple engines loaded simultaneously.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
| Volume | Container Path | Purpose |
|
||||||
|
|--------|---------------|---------|
|
||||||
|
| `./output` | `/app/data/generations` | Generated audio files (bind-mount, easy access from host) |
|
||||||
|
| `voicebox-data` | `/app/data` | Profiles, database, cache |
|
||||||
|
| `huggingface-cache` | `/home/voicebox/.cache/huggingface` | Downloaded models (persists across rebuilds) |
|
||||||
|
|
||||||
|
The `huggingface-cache` volume is important -- without it, models would be re-downloaded every time the container is rebuilt.
|
||||||
|
|
||||||
|
## GPU Acceleration
|
||||||
|
|
||||||
|
### NVIDIA GPU (CUDA)
|
||||||
|
|
||||||
|
To use your NVIDIA GPU inside the container, install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and add GPU access to your compose file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
voicebox:
|
||||||
|
build: .
|
||||||
|
# ... existing config ...
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
```
|
||||||
|
|
||||||
|
### AMD GPU (ROCm)
|
||||||
|
|
||||||
|
For AMD GPUs, use the ROCm runtime:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
voicebox:
|
||||||
|
build: .
|
||||||
|
# ... existing config ...
|
||||||
|
devices:
|
||||||
|
- /dev/kfd
|
||||||
|
- /dev/dri
|
||||||
|
group_add:
|
||||||
|
- video
|
||||||
|
```
|
||||||
|
|
||||||
|
### CPU Only
|
||||||
|
|
||||||
|
The default configuration runs on CPU. This works fine but generation will be slower. LuxTTS is the fastest engine on CPU (150x realtime).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
The Docker image follows security best practices:
|
||||||
|
|
||||||
|
- **Non-root user** -- the server runs as `voicebox`, not `root`
|
||||||
|
- **Localhost binding** -- only accessible from the host machine by default
|
||||||
|
- **Health checks** -- automatic restart if the server hangs (`/health` endpoint polled every 30s)
|
||||||
|
- **CORS restricted** -- only local origins allowed by default
|
||||||
|
|
||||||
|
### Running Behind a Reverse Proxy
|
||||||
|
|
||||||
|
For production deployments, put Voicebox behind nginx or Caddy with TLS and authentication:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name voicebox.example.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/ssl/certs/voicebox.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/private/voicebox.key;
|
||||||
|
|
||||||
|
auth_basic "Voicebox";
|
||||||
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:17493;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container starts but UI shows JSON
|
||||||
|
|
||||||
|
If you see `{"message": "voicebox API", ...}` instead of the web UI, the frontend build may have failed during the Docker build. Check the build logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for errors in the "Build frontend" stage.
|
||||||
|
|
||||||
|
### Models downloading on every restart
|
||||||
|
|
||||||
|
Make sure the `huggingface-cache` volume is configured. Without it, the model cache is lost when the container stops:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- huggingface-cache:/home/voicebox/.cache/huggingface
|
||||||
|
```
|
||||||
|
|
||||||
|
### Out of memory
|
||||||
|
|
||||||
|
TTS models are large. If the container is killed by the OOM killer, increase the memory limit:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 16G
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what's using port 17493
|
||||||
|
lsof -i :17493
|
||||||
|
|
||||||
|
# Or use a different port
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:17493"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prebuilt Images (Coming Soon)
|
||||||
|
|
||||||
|
We plan to publish prebuilt Docker images to GitHub Container Registry so you won't need to build locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Not available yet — coming in a future release
|
||||||
|
docker run -p 17493:17493 ghcr.io/jamiepine/voicebox:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The CPU image will be ~3-4 GB (Python + PyTorch + TTS packages). A separate CUDA tag (~6-8 GB) will be available for NVIDIA GPU users. This is normal for ML containers.
|
||||||
|
|
||||||
|
For now, use `docker compose up` to build from source as described above.
|
||||||
|
|
||||||
|
## Connecting the Desktop App
|
||||||
|
|
||||||
|
You can also use the desktop app as a frontend for a Docker-hosted backend. In the desktop app, go to **Settings -> Server**, enable **Remote Mode**, and enter `http://<server-ip>:17493`.
|
||||||
|
|
||||||
|
See the [Remote Mode guide](/overview/remote-mode) for details.
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"introduction",
|
"introduction",
|
||||||
"installation",
|
"installation",
|
||||||
|
"docker",
|
||||||
"quick-start",
|
"quick-start",
|
||||||
"voice-cloning",
|
"voice-cloning",
|
||||||
"stories-editor",
|
"stories-editor",
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import { docs } from '@/.source';
|
|
||||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||||
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
|
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
|
||||||
|
import { docs } from '@/.source';
|
||||||
|
|
||||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||||
export const source = loader({
|
export const source = loader({
|
||||||
baseUrl: '/docs',
|
baseUrl: '/',
|
||||||
source: docs.toFumadocsSource(),
|
source: docs.toFumadocsSource(),
|
||||||
plugins: [lucideIconsPlugin()],
|
plugins: [lucideIconsPlugin()],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user