diff --git a/backend/app.py b/backend/app.py index fb6dc89c..f652d149 100644 --- a/backend/app.py +++ b/backend/app.py @@ -77,6 +77,7 @@ def create_app() -> FastAPI: _configure_cors(application) register_routers(application) _register_lifecycle(application) + _mount_frontend(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: """Return a human-readable string describing GPU availability.""" backend_type = get_backend_type() diff --git a/backend/routes/health.py b/backend/routes/health.py index e48d5689..0053f423 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -3,9 +3,11 @@ import asyncio import os import signal +from pathlib import Path import torch from fastapi import APIRouter, Depends +from fastapi.responses import FileResponse from sqlalchemy.orm import Session from .. import config, models @@ -15,12 +17,18 @@ from ..utils.platform_detect import get_backend_type 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("/") async def root(): - """Root endpoint.""" + """Root endpoint — serves SPA index.html in Docker, JSON otherwise.""" 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__} diff --git a/docs/README.md b/docs/README.md index 68109407..54c796b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,11 +6,7 @@ This is a Next.js application generated with Run development server: ```bash -npm run dev -# or -pnpm dev -# or -yarn dev +bun run dev ``` Open http://localhost:3000 with your browser to see the result. diff --git a/docs/app/(home)/layout.tsx b/docs/app/(home)/layout.tsx deleted file mode 100644 index 77379fac..00000000 --- a/docs/app/(home)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import { baseOptions } from '@/lib/layout.shared'; - -export default function Layout({ children }: LayoutProps<'/'>) { - return {children}; -} diff --git a/docs/app/(home)/page.tsx b/docs/app/(home)/page.tsx deleted file mode 100644 index 4393163a..00000000 --- a/docs/app/(home)/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from 'next/navigation'; - -export default function HomePage() { - redirect('/docs'); -} diff --git a/docs/app/docs/layout.tsx b/docs/app/[[...slug]]/layout.tsx similarity index 77% rename from docs/app/docs/layout.tsx rename to docs/app/[[...slug]]/layout.tsx index 299d2e28..cfae6acd 100644 --- a/docs/app/docs/layout.tsx +++ b/docs/app/[[...slug]]/layout.tsx @@ -1,8 +1,8 @@ -import { source } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; 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 ( {children} diff --git a/docs/app/docs/[[...slug]]/page.tsx b/docs/app/[[...slug]]/page.tsx similarity index 92% rename from docs/app/docs/[[...slug]]/page.tsx rename to docs/app/[[...slug]]/page.tsx index 35aca33e..908c0aeb 100644 --- a/docs/app/docs/[[...slug]]/page.tsx +++ b/docs/app/[[...slug]]/page.tsx @@ -7,7 +7,7 @@ import { APIPage } from '@/components/api-page'; import { getPageImage, source } from '@/lib/source'; 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 page = source.getPage(params.slug); if (!page) notFound(); @@ -59,7 +59,7 @@ export async function generateStaticParams() { return source.generateParams(); } -export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise { +export async function generateMetadata(props: PageProps<'/[[...slug]]'>): Promise { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); diff --git a/docs/content/docs/overview/docker.mdx b/docs/content/docs/overview/docker.mdx new file mode 100644 index 00000000..8090dbe0 --- /dev/null +++ b/docs/content/docs/overview/docker.mdx @@ -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. + + + The first build takes a few minutes (compiling the frontend, installing Python dependencies). Subsequent starts are fast thanks to Docker layer caching. + + +## 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" +``` + + + The API has no built-in authentication. Only expose to trusted networks, or put a reverse proxy with auth in front of it. + + +### 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 +``` + + + 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. + + +## 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://:17493`. + +See the [Remote Mode guide](/overview/remote-mode) for details. diff --git a/docs/content/docs/overview/meta.json b/docs/content/docs/overview/meta.json index 3ac2fc15..17dab7bf 100644 --- a/docs/content/docs/overview/meta.json +++ b/docs/content/docs/overview/meta.json @@ -4,6 +4,7 @@ "pages": [ "introduction", "installation", + "docker", "quick-start", "voice-cloning", "stories-editor", diff --git a/docs/lib/source.ts b/docs/lib/source.ts index c829e387..15b9ccbe 100644 --- a/docs/lib/source.ts +++ b/docs/lib/source.ts @@ -1,10 +1,10 @@ -import { docs } from '@/.source'; import { type InferPageType, loader } from 'fumadocs-core/source'; import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; +import { docs } from '@/.source'; // See https://fumadocs.dev/docs/headless/source-api for more info export const source = loader({ - baseUrl: '/docs', + baseUrl: '/', source: docs.toFumadocsSource(), plugins: [lucideIconsPlugin()], }); diff --git a/docs/MIGRATION.md b/docs/notes/MIGRATION.md similarity index 100% rename from docs/MIGRATION.md rename to docs/notes/MIGRATION.md diff --git a/docs/RELEASE_v0.2.0.md b/docs/notes/RELEASE_v0.2.0.md similarity index 100% rename from docs/RELEASE_v0.2.0.md rename to docs/notes/RELEASE_v0.2.0.md diff --git a/docs/issue-pain-points.md b/docs/notes/issue-pain-points.md similarity index 100% rename from docs/issue-pain-points.md rename to docs/notes/issue-pain-points.md