From 5668aff528fa936a08d509687f0d1afd41646f9f Mon Sep 17 00:00:00 2001 From: debpalash Date: Tue, 14 Apr 2026 04:35:04 +0530 Subject: [PATCH] feat: add Docker support for containerized deployment and serve static frontend from backend --- .dockerignore | 36 ++++++++++++++++++++++++++ Dockerfile | 61 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- backend/main.py | 11 +++++++- docker-compose.yml | 23 +++++++++++++++++ frontend/src/App.jsx | 2 +- 6 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f6e9a769 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# OmniVoice Docker Ignore + +# Virtual environments +.venv/ +venv/ +env/ + +# Node modules +node_modules/ +bun.lockb +.turbo/ + +# Python caches +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ + +# Databases and user data +# We don't want to copy existing local data into the image layout! +*.db +*.sqlite +*.sqlite3 +omnivoice_data/ +frontend/dist/ + +# Git internals +.git/ +.github/ + +# IDE files +.DS_Store +.vscode/ +.idea/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1ab950a6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# ========================================== +# Builder Stage: Compile React Frontend +# ========================================== +FROM oven/bun:1-alpine AS frontend-builder +WORKDIR /app/frontend + +# Copy frontend specifications +COPY frontend/package.json ./ +COPY frontend/bun.lock ./ + +# Install dependencies fast +RUN bun install --frozen-lockfile + +# Copy frontend source and build static files +COPY frontend/ ./ +# Output goes to /app/frontend/dist +RUN bun run build + +# ========================================== +# Runtime Stage: Python & PyTorch Backend +# ========================================== +FROM pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime AS runtime +WORKDIR /app + +# Enable unbuffered logs and optimizations +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV HF_HOME=/app/omnivoice_data/huggingface + +# Install system dependencies (FFmpeg is critical for torchaudio/scene splitting) +RUN apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg \ + libsndfile1 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install `uv` for blazing-fast reliable pip resolution +RUN pip install --no-cache-dir uv + +# Copy python packaging specs +COPY pyproject.toml uv.lock ./ + +# Native wheels from PyPI embed CUDA matching `torch >= 2.4` standard index +# By installing via `uv`, the process completes exponentially faster +RUN uv pip install --system --no-cache -e . + +# Copy application source +COPY backend/ ./backend/ +COPY omnivoice/ ./omnivoice/ + +# Copy the pre-built React frontend from the builder stage +COPY --from=frontend-builder /app/frontend/dist ./frontend/dist + +# Expose the single unified API and UI port +EXPOSE 8000 + +# Mount points for persistent data (sqlite db, user voices, huggingface cache) +VOLUME ["/app/omnivoice_data"] + +# Bind to 0.0.0.0 for external access +ENTRYPOINT ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index 75b09815..32679093 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ The studio is highly functional today, but we are aggressively expanding. Watch - [x] **Advanced Export Suite** — VTT subtitles, per-segment WAV ZIP, compressed MP3, and stem export (vocals + background separate). - [x] **Streaming TTS** — Chunked WAV streaming with progressive download and auto-playback. - [ ] **Native Desktop Applications** — Dedicated client apps for macOS, Windows, and Linux. -- [ ] **One-Click Deployment** — Docker image packages engineered for zero-config GPU passthrough. +- [x] **One-Click Deployment** — Docker image packages engineered for zero-config GPU passthrough. --- diff --git a/backend/main.py b/backend/main.py index 6e02aa80..3991f840 100644 --- a/backend/main.py +++ b/backend/main.py @@ -19,7 +19,7 @@ import soundfile as sf import torch import torchaudio from fastapi import FastAPI, File, Form, UploadFile, HTTPException, Query -from fastapi.responses import FileResponse, Response, StreamingResponse +from fastapi.responses import FileResponse, Response, StreamingResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel @@ -1887,6 +1887,15 @@ async def delete_project(project_id: str): conn.close() return {"deleted": project_id} +# Mount frontend at root. Placed last so it doesn't shadow API routes. +frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist") +if os.path.exists(frontend_path): + app.mount("/", StaticFiles(directory=frontend_path, html=True), name="frontend") +else: + @app.get("/") + def _dev_fallback(): + return RedirectResponse(url="http://localhost:5173") + if __name__ == "__main__": import uvicorn diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4d92239f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + omnivoice: + build: . + container_name: omnivoice-studio + restart: unless-stopped + ports: + - "8000:8000" + volumes: + # Map the backend data directory to host for persistent SQLite, voices, and history + - ./omnivoice_data:/app/omnivoice_data + environment: + # Optional: set this parameter to use Pyannote Speaker Diarization + - HF_TOKEN=${HF_TOKEN:-} + # Zero-config GPU Passthrough (Requires NVIDIA Container Toolkit on host) + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d868851e..d0feb33c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -94,7 +94,7 @@ const LANG_CODES = [ {code: 'yo', label: 'Yoruba'}, {code: 'zu', label: 'Zulu'} ]; -const API = "http://localhost:8000"; +const API = import.meta.env.DEV ? "http://localhost:8000" : ""; function formatTime(s) { const m = Math.floor(s / 60);