Add Docker support and update dependencies

- Introduced Docker support with CPU-only and GPU-enabled configurations via Dockerfiles and docker-compose files.
- Added a .dockerignore file to exclude unnecessary files from Docker images.
- Updated bun.lock and package.json to include new dependencies for icon handling.
- Enhanced README with Docker usage instructions and deployment options.
- Refactored components to utilize new icon libraries for improved UI consistency.
This commit is contained in:
Jamie Pine
2026-02-02 02:19:05 -08:00
parent 4c4b3e5463
commit f090759d8f
61 changed files with 5982 additions and 394 deletions
+55
View File
@@ -0,0 +1,55 @@
# Dependencies
node_modules/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
# Build outputs
build/
*.egg-info/
.eggs/
target/
# Keep web/dist for the Docker image
!web/dist
# Development
.git/
.github/
.vscode/
.idea/
*.swp
*.swo
*~
# Data and logs
data/
*.log
*.sqlite
*.db
# OS
.DS_Store
Thumbs.db
# Documentation
docs/
landing/
mlx-test/
# Test files
*.test.ts
*.test.tsx
*.spec.ts
*.spec.tsx
# Keep these out
.env
.env.local
*.pem
*.key
credentials.json
+66
View File
@@ -299,3 +299,69 @@ jobs:
prerelease: false
args: ${{ matrix.args }}
includeUpdaterJson: true
# ============================================
# Build and Push Docker Images
# ============================================
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies and build web UI
run: |
bun install
cd web
bun run build
- name: Extract version from tag
id: version
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
else
VERSION="dev"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build and push CPU image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/jamiepine/voicebox:latest
ghcr.io/jamiepine/voicebox:${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push CUDA image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.cuda
platforms: linux/amd64
push: true
tags: |
ghcr.io/jamiepine/voicebox:latest-cuda
ghcr.io/jamiepine/voicebox:${{ steps.version.outputs.version }}-cuda
cache-from: type=gha
cache-to: type=gha,mode=max
+38
View File
@@ -0,0 +1,38 @@
# Base Dockerfile for Voicebox (CPU-only)
# For GPU support, use Dockerfile.cuda
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
ffmpeg \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy backend
COPY backend/ /app/backend/
COPY providers/ /app/providers/
# Copy pre-built web UI
COPY web/dist/ /app/web/dist/
# Install Python dependencies (without PyTorch - will be downloaded via provider system)
RUN python -m pip install --upgrade pip && \
pip install --no-cache-dir \
fastapi uvicorn[standard] pydantic sqlalchemy alembic \
librosa soundfile numpy python-multipart Pillow \
huggingface_hub transformers accelerate
# Create data directory for profiles/generations
RUN mkdir -p /app/data
# Health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \
CMD curl -f http://localhost:8000/health || exit 1
EXPOSE 8000
# Run server with web UI
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
+52
View File
@@ -0,0 +1,52 @@
# Dockerfile for Voicebox with NVIDIA GPU support (CUDA)
FROM nvidia/cuda:12.1.1-runtime-ubuntu22.04
WORKDIR /app
# Install Python 3.12
RUN apt-get update && apt-get install -y \
software-properties-common \
&& add-apt-repository ppa:deadsnakes/ppa \
&& apt-get update && apt-get install -y \
python3.12 \
python3.12-dev \
python3-pip \
ffmpeg \
curl \
&& rm -rf /var/lib/apt/lists/*
# Set Python 3.12 as default
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 && \
update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1
# Copy backend
COPY backend/ /app/backend/
COPY providers/ /app/providers/
# Copy pre-built web UI
COPY web/dist/ /app/web/dist/
# Install PyTorch with CUDA support first
RUN python -m pip install --upgrade pip && \
pip install --no-cache-dir \
torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
# Install remaining dependencies
RUN pip install --no-cache-dir \
fastapi uvicorn[standard] pydantic sqlalchemy alembic \
transformers accelerate huggingface_hub \
librosa soundfile numpy python-multipart Pillow \
qwen-tts
# Create data directory
RUN mkdir -p /app/data
# Health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \
CMD curl -f http://localhost:8000/health || exit 1
EXPOSE 8000
# Run server with web UI
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
+18 -2
View File
@@ -76,7 +76,7 @@ Download a voice model, clone any voice from a few seconds of audio, and compose
## Download
Voicebox is available now for macOS and Windows.
### Desktop App
| Platform | Download |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
@@ -85,7 +85,23 @@ Voicebox is available now for macOS and Windows.
| Windows (MSI) | [voicebox_0.1.0_x64_en-US.msi](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_0.1.0_x64_en-US.msi) |
| Windows (Setup) | [voicebox_0.1.0_x64-setup.exe](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_0.1.0_x64-setup.exe) |
> **Linux builds coming soon** — Currently blocked by GitHub runner disk space limitations.
> **Linux desktop builds coming soon** — Currently blocked by GitHub runner disk space limitations.
### Docker (Server Deployment)
Run Voicebox with the web UI in Docker:
```bash
# CPU-only (supports amd64 and arm64)
docker run -p 8000:8000 -v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest
# NVIDIA GPU
docker run --gpus all -p 8000:8000 -v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest-cuda
```
See [Docker Deployment Guide](docs/plans/DOCKER_DEPLOYMENT.md) for full documentation.
---
+2
View File
@@ -19,6 +19,8 @@
"@hookform/resolvers": "^3.9.0",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@iconify-json/svg-spinners": "^1.2.4",
"@iconify/react": "^6.0.2",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
@@ -460,7 +460,7 @@ export function AudioPlayer() {
// Use double requestAnimationFrame to ensure DOM is fully rendered
let rafId1: number;
let rafId2: number;
let timeoutId: number | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
rafId1 = requestAnimationFrame(() => {
rafId2 = requestAnimationFrame(() => {
@@ -1,7 +1,8 @@
import { SparklesIcon, TextSquareIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Icon } from '@iconify/react';
import { useMatchRoute } from '@tanstack/react-router';
import { AnimatePresence, motion } from 'framer-motion';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, TextSquareIcon, SparklesIcon } from '@hugeicons/core-free-icons';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
@@ -302,7 +303,7 @@ export function FloatingGenerateBox({
size="icon"
>
{isPending ? (
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={SparklesIcon} size={16} className="h-4 w-4" />
)}
@@ -1,5 +1,6 @@
import { Mic01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, Mic01Icon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
@@ -47,7 +48,11 @@ export function GenerationForm() {
<FormLabel>Voice Profile</FormLabel>
{selectedProfile ? (
<div className="mt-2 p-3 border rounded-md bg-muted/50 flex items-center gap-2">
<HugeiconsIcon icon={Mic01Icon} size={16} className="h-4 w-4 text-muted-foreground" />
<HugeiconsIcon
icon={Mic01Icon}
size={16}
className="h-4 w-4 text-muted-foreground"
/>
<span className="font-medium">{selectedProfile.name}</span>
<span className="text-sm text-muted-foreground">{selectedProfile.language}</span>
</div>
@@ -171,14 +176,10 @@ export function GenerationForm() {
/>
</div>
<Button
type="submit"
className="w-full"
disabled={isPending || !selectedProfileId}
>
<Button type="submit" className="w-full" disabled={isPending || !selectedProfileId}>
{isPending ? (
<>
<HugeiconsIcon icon={Loading01Icon} size={16} className="mr-2 h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
+25 -10
View File
@@ -1,13 +1,13 @@
import { HugeiconsIcon } from '@hugeicons/react';
import {
WaveIcon,
Download01Icon,
Archive01Icon,
Loading01Icon,
Delete01Icon,
Download01Icon,
MoreHorizontalIcon,
PlayIcon,
Delete01Icon,
WaveIcon,
} from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Icon } from '@iconify/react';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
@@ -55,7 +55,9 @@ export function HistoryTable() {
const [importDialogOpen, setImportDialogOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [generationToDelete, setGenerationToDelete] = useState<{ id: string; name: string } | null>(null);
const [generationToDelete, setGenerationToDelete] = useState<{ id: string; name: string } | null>(
null,
);
const limit = 20;
const { toast } = useToast();
@@ -223,7 +225,10 @@ export function HistoryTable() {
if (isLoading && page === 0) {
return (
<div className="flex items-center justify-center h-full">
<HugeiconsIcon icon={Loading01Icon} size={32} className="h-8 w-8 animate-spin text-muted-foreground" />
<Icon
icon="svg-spinners:ring-resize"
className="h-8 w-8 animate-spin text-muted-foreground"
/>
</div>
);
}
@@ -269,7 +274,11 @@ export function HistoryTable() {
>
{/* Waveform icon */}
<div className="flex items-center shrink-0">
<HugeiconsIcon icon={WaveIcon} size={20} className="h-5 w-5 text-muted-foreground" />
<HugeiconsIcon
icon={WaveIcon}
size={20}
className="h-5 w-5 text-muted-foreground"
/>
</div>
{/* Left side - Meta information */}
@@ -353,7 +362,12 @@ export function HistoryTable() {
{/* Load more trigger element */}
{hasMore && (
<div ref={loadMoreRef} className="flex items-center justify-center py-4">
{isFetching && <HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 animate-spin text-muted-foreground" />}
{isFetching && (
<Icon
icon="svg-spinners:ring-resize"
className="h-6 w-6 animate-spin text-muted-foreground"
/>
)}
</div>
)}
@@ -372,7 +386,8 @@ export function HistoryTable() {
<DialogHeader>
<DialogTitle>Delete Generation</DialogTitle>
<DialogDescription>
Are you sure you want to delete this generation from "{generationToDelete?.name}"? This action cannot be undone.
Are you sure you want to delete this generation from "{generationToDelete?.name}"?
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Delete01Icon, Download01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Download01Icon, Loading01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import {
AlertDialog,
@@ -154,7 +155,10 @@ export function ModelManagement() {
<CardContent className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 animate-spin text-muted-foreground" />
<Icon
icon="svg-spinners:ring-resize"
className="h-6 w-6 animate-spin text-muted-foreground"
/>
</div>
) : modelStatus ? (
<div className="space-y-4">
@@ -246,7 +250,7 @@ export function ModelManagement() {
>
{deleteMutation.isPending ? (
<>
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 mr-2 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
@@ -320,7 +324,7 @@ function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: M
</div>
) : showDownloading ? (
<Button size="sm" variant="outline" disabled>
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 mr-2 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 mr-2 animate-spin" />
Downloading...
</Button>
) : (
@@ -1,5 +1,6 @@
import { CancelCircleIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, CancelCircleIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
@@ -13,7 +14,11 @@ interface ModelProgressProps {
isDownloading?: boolean;
}
export function ModelProgress({ modelName, displayName, isDownloading = false }: ModelProgressProps) {
export function ModelProgress({
modelName,
displayName,
isDownloading = false,
}: ModelProgressProps) {
const [progress, setProgress] = useState<ModelProgressType | null>(null);
const serverUrl = useServerStore((state) => state.serverUrl);
@@ -75,10 +80,12 @@ export function ModelProgress({ modelName, displayName, isDownloading = false }:
const getStatusIcon = () => {
switch (progress.status) {
case 'error':
return <HugeiconsIcon icon={CancelCircleIcon} size={16} className="h-4 w-4 text-destructive" />;
return (
<HugeiconsIcon icon={CancelCircleIcon} size={16} className="h-4 w-4 text-destructive" />
);
case 'downloading':
case 'extracting':
return <HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />;
return <Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />;
default:
return null;
}
@@ -1,5 +1,6 @@
import { Delete01Icon, Download01Icon, Loading01Icon } from '@hugeicons/core-free-icons';
import { Delete01Icon, Download01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Icon } from '@iconify/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import {
@@ -162,7 +163,7 @@ export function ProviderSettings() {
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-6 w-6 animate-spin" />
</div>
</CardContent>
</Card>
@@ -210,11 +211,7 @@ export function ProviderSettings() {
disabled={downloadingProvider === 'pytorch-cuda'}
>
{downloadingProvider === 'pytorch-cuda' ? (
<HugeiconsIcon
icon={Loading01Icon}
size={16}
className="h-4 w-4 animate-spin"
/>
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
) : (
<>
<HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" />
@@ -257,11 +254,7 @@ export function ProviderSettings() {
disabled={downloadingProvider === 'pytorch-cpu'}
>
{downloadingProvider === 'pytorch-cpu' ? (
<HugeiconsIcon
icon={Loading01Icon}
size={16}
className="h-4 w-4 animate-spin"
/>
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
) : (
<>
<HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" />
@@ -1,5 +1,6 @@
import { CancelCircleIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, CancelCircleIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useServerHealth } from '@/lib/hooks/useServer';
@@ -33,7 +34,7 @@ export function ServerStatus() {
{isLoading ? (
<div className="flex items-center gap-2">
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
<span className="text-sm">Checking connection...</span>
</div>
) : error ? (
+13 -7
View File
@@ -1,6 +1,14 @@
import { Link, useMatchRoute } from '@tanstack/react-router';
import {
Book01Icon,
Mic01Icon,
PackageIcon,
ServerStack01Icon,
SpeakerIcon,
VolumeHighIcon,
} from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { PackageIcon, Book01Icon, Loading01Icon, Mic01Icon, McpServerIcon, SpeakerIcon, VolumeHighIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { Link, useMatchRoute } from '@tanstack/react-router';
import voiceboxLogo from '@/assets/voicebox-logo.png';
import { cn } from '@/lib/utils/cn';
import { useGenerationStore } from '@/stores/generationStore';
@@ -16,7 +24,7 @@ const tabs = [
{ id: 'voices', path: '/voices', icon: Mic01Icon, label: 'Voices' },
{ id: 'audio', path: '/audio', icon: SpeakerIcon, label: 'Audio' },
{ id: 'models', path: '/models', icon: PackageIcon, label: 'Models' },
{ id: 'server', path: '/server', icon: McpServerIcon, label: 'Server' },
{ id: 'server', path: '/server', icon: ServerStack01Icon, label: 'Server' },
];
export function Sidebar({ isMacOS }: SidebarProps) {
@@ -43,9 +51,7 @@ export function Sidebar({ isMacOS }: SidebarProps) {
const Icon = tab.icon;
// For index route, use exact match; for others, use default matching
const isActive =
tab.path === '/'
? matchRoute({ to: '/' })
: matchRoute({ to: tab.path });
tab.path === '/' ? matchRoute({ to: '/' }) : matchRoute({ to: tab.path });
return (
<Link
@@ -76,7 +82,7 @@ export function Sidebar({ isMacOS }: SidebarProps) {
isPlayerVisible ? 'mb-[120px]' : 'mb-0',
)}
>
<HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 text-accent animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-6 w-6 text-accent animate-spin" />
</div>
)}
</div>
@@ -1,8 +1,7 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { HugeiconsIcon } from '@hugeicons/react';
import { DragDropVerticalIcon, Mic01Icon, MoreHorizontalIcon, PlayIcon, Delete01Icon } from '@hugeicons/core-free-icons';
import { useState } from 'react';
import { DragDropVerticalIcon, MoreHorizontalIcon, PlayIcon, Delete01Icon } from '@hugeicons/core-free-icons';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -11,10 +10,10 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Textarea } from '@/components/ui/textarea';
import { ProfileAvatar } from '@/components/VoiceProfiles/ProfileAvatar';
import type { StoryItemDetail } from '@/lib/api/types';
import { cn } from '@/lib/utils/cn';
import { useStoryStore } from '@/stores/storyStore';
import { useServerStore } from '@/stores/serverStore';
interface StoryChatItemProps {
item: StoryItemDetail;
@@ -36,10 +35,6 @@ export function StoryChatItem({
isDragging,
}: StoryChatItemProps) {
const seek = useStoryStore((state) => state.seek);
const serverUrl = useServerStore((state) => state.serverUrl);
const [avatarError, setAvatarError] = useState(false);
const avatarUrl = `${serverUrl}/profiles/${item.profile_id}/avatar`;
// Check if this item is currently playing based on timecode
const itemStartMs = item.start_time_ms;
@@ -81,21 +76,12 @@ export function StoryChatItem({
{/* Voice Avatar */}
<div className="shrink-0">
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center overflow-hidden">
{!avatarError ? (
<img
src={avatarUrl}
alt={`${item.profile_name} avatar`}
className={cn(
'h-full w-full object-cover transition-all duration-200',
!isCurrentlyPlaying && 'grayscale'
)}
onError={() => setAvatarError(true)}
/>
) : (
<HugeiconsIcon icon={Mic01Icon} size={20} className="h-5 w-5 text-muted-foreground" />
)}
</div>
<ProfileAvatar
profileId={item.profile_id}
size="lg"
grayscale={!isCurrentlyPlaying}
alt={`${item.profile_name} avatar`}
/>
</div>
{/* Content */}
@@ -0,0 +1,80 @@
import { HugeiconsIcon } from '@hugeicons/react';
import { Mic01Icon } from '@hugeicons/core-free-icons';
import { useState } from 'react';
import { cn } from '@/lib/utils/cn';
import { useServerStore } from '@/stores/serverStore';
interface ProfileAvatarProps {
profileId: string;
avatarPath?: string | null;
size?: 'sm' | 'md' | 'lg' | 'xl';
grayscale?: boolean;
className?: string;
alt?: string;
}
const sizeClasses = {
sm: 'h-6 w-6',
md: 'h-8 w-8',
lg: 'h-10 w-10',
xl: 'h-24 w-24',
};
const iconSizes = {
sm: 14,
md: 16,
lg: 20,
xl: 40,
};
const iconClassNames = {
sm: 'h-3.5 w-3.5',
md: 'h-4 w-4',
lg: 'h-5 w-5',
xl: 'h-10 w-10',
};
export function ProfileAvatar({
profileId,
avatarPath,
size = 'md',
grayscale = false,
className,
alt = 'Profile avatar',
}: ProfileAvatarProps) {
const [avatarError, setAvatarError] = useState(false);
const serverUrl = useServerStore((state) => state.serverUrl);
// If avatarPath is explicitly null or empty string, don't try to load avatar
// Otherwise, always try to load (avatarPath might not be available in all contexts)
const avatarUrl =
avatarPath === null || avatarPath === '' ? null : `${serverUrl}/profiles/${profileId}/avatar`;
return (
<div
className={cn(
sizeClasses[size],
'rounded-full bg-muted flex items-center justify-center shrink-0 overflow-hidden',
className,
)}
>
{avatarUrl && !avatarError ? (
<img
src={avatarUrl}
alt={alt}
className={cn(
'h-full w-full object-cover transition-all duration-200',
grayscale && 'grayscale',
)}
onError={() => setAvatarError(true)}
/>
) : (
<HugeiconsIcon
icon={Mic01Icon}
size={iconSizes[size]}
className={cn(iconClassNames[size], 'text-muted-foreground')}
/>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import { HugeiconsIcon } from '@hugeicons/react';
import { Download01Icon, Edit01Icon, Mic01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { Download01Icon, Edit01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -13,10 +13,10 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { ProfileAvatar } from '@/components/VoiceProfiles/ProfileAvatar';
import type { VoiceProfileResponse } from '@/lib/api/types';
import { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';
import { cn } from '@/lib/utils/cn';
import { useServerStore } from '@/stores/serverStore';
import { useUIStore } from '@/stores/uiStore';
interface ProfileCardProps {
@@ -25,19 +25,15 @@ interface ProfileCardProps {
export function ProfileCard({ profile }: ProfileCardProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [avatarError, setAvatarError] = useState(false);
const deleteProfile = useDeleteProfile();
const exportProfile = useExportProfile();
const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);
const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen);
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId);
const serverUrl = useServerStore((state) => state.serverUrl);
const isSelected = selectedProfileId === profile.id;
const avatarUrl = profile.avatar_path ? `${serverUrl}/profiles/${profile.id}/avatar` : null;
const handleSelect = () => {
setSelectedProfileId(isSelected ? null : profile.id);
};
@@ -73,21 +69,13 @@ export function ProfileCard({ profile }: ProfileCardProps) {
>
<CardHeader className="p-3 pb-2">
<CardTitle className="flex items-center gap-1.5 text-base font-medium">
<div className="h-6 w-6 rounded-full bg-muted flex items-center justify-center shrink-0 overflow-hidden">
{avatarUrl && !avatarError ? (
<img
src={avatarUrl}
alt={`${profile.name} avatar`}
className={cn(
'h-full w-full object-cover transition-all duration-200',
!isSelected && 'grayscale',
)}
onError={() => setAvatarError(true)}
/>
) : (
<HugeiconsIcon icon={Mic01Icon} size={14} className="h-3.5 w-3.5 text-muted-foreground" />
)}
</div>
<ProfileAvatar
profileId={profile.id}
avatarPath={profile.avatar_path}
size="sm"
grayscale={!isSelected}
alt={`${profile.name} avatar`}
/>
<span className="break-words">{profile.name}</span>
</CardTitle>
</CardHeader>
+8 -4
View File
@@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { HugeiconsIcon } from '@hugeicons/react';
import { Edit01Icon, MoreHorizontalIcon, Add01Icon, Delete01Icon, Mic01Icon } from '@hugeicons/core-free-icons';
import { Edit01Icon, MoreHorizontalIcon, Add01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { useMemo, useRef } from 'react';
import { Button } from '@/components/ui/button';
import {
@@ -19,6 +19,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { ProfileForm } from '@/components/VoiceProfiles/ProfileForm';
import { ProfileAvatar } from '@/components/VoiceProfiles/ProfileAvatar';
import { apiClient } from '@/lib/api/client';
import type { VoiceProfileResponse } from '@/lib/api/types';
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
@@ -185,9 +186,12 @@ function VoiceRow({
<TableRow className="cursor-pointer" onClick={onEdit}>
<TableCell>
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
<HugeiconsIcon icon={Mic01Icon} size={16} className="h-4 w-4 text-muted-foreground" />
</div>
<ProfileAvatar
profileId={profile.id}
avatarPath={profile.avatar_path}
size="md"
alt={`${profile.name} avatar`}
/>
<div>
<div className="font-medium">{profile.name}</div>
{profile.description && (
+19 -21
View File
@@ -1,25 +1,19 @@
"use client"
'use client';
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { HugeiconsIcon } from '@hugeicons/react';
import { CircleIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import * as React from 'react';
import { cn } from "@/lib/utils/cn"
import { cn } from '@/lib/utils/cn';
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
@@ -29,17 +23,21 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-accent text-accent ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
'aspect-square h-4 w-4 rounded-full border border-accent text-accent ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<HugeiconsIcon icon={CircleIcon} size={10} className="h-2.5 w-2.5 fill-current text-current" />
<HugeiconsIcon
icon={CircleIcon}
size={10}
className="h-2.5 w-2.5 fill-current text-current"
/>
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem };
-5
View File
@@ -91,11 +91,6 @@ export function useGenerationForm(options: UseGenerationFormOptions = {}) {
instruct: data.instruct || undefined,
});
toast({
title: 'Generation complete!',
description: `Audio generated (${result.duration.toFixed(2)}s)`,
});
const audioUrl = apiClient.getAudioUrl(result.id);
setAudioWithAutoPlay(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
+28 -7
View File
@@ -1,5 +1,6 @@
import { CancelCircleIcon, CheckmarkCircle02Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { CheckmarkCircle02Icon, Loading01Icon, CancelCircleIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { useCallback, useEffect, useRef } from 'react';
import { Progress } from '@/components/ui/progress';
import { useToast } from '@/components/ui/use-toast';
@@ -60,7 +61,7 @@ export function useModelDownloadToast({
title: displayName,
description: (
<div className="flex items-center gap-2">
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
<span>Connecting to download...</span>
</div>
),
@@ -97,19 +98,35 @@ export function useModelDownloadToast({
switch (progress.status) {
case 'complete':
statusIcon = <HugeiconsIcon icon={CheckmarkCircle02Icon} size={16} className="h-4 w-4 text-green-500" />;
statusIcon = (
<HugeiconsIcon
icon={CheckmarkCircle02Icon}
size={16}
className="h-4 w-4 text-green-500"
/>
);
statusText = 'Download complete';
break;
case 'error':
statusIcon = <HugeiconsIcon icon={CancelCircleIcon} size={16} className="h-4 w-4 text-destructive" />;
statusIcon = (
<HugeiconsIcon
icon={CancelCircleIcon}
size={16}
className="h-4 w-4 text-destructive"
/>
);
statusText = `Error: ${progress.error || 'Unknown error'}`;
break;
case 'downloading':
statusIcon = <HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />;
statusIcon = (
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
);
statusText = progress.filename || 'Downloading...';
break;
case 'extracting':
statusIcon = <HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />;
statusIcon = (
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
);
statusText = 'Extracting...';
break;
}
@@ -155,7 +172,11 @@ export function useModelDownloadToast({
toastUpdateRef.current({
title: (
<div className="flex items-center gap-2">
<HugeiconsIcon icon={CheckmarkCircle02Icon} size={16} className="h-4 w-4 text-green-500" />
<HugeiconsIcon
icon={CheckmarkCircle02Icon}
size={16}
className="h-4 w-4 text-green-500"
/>
<span>{displayName}</span>
</div>
),
+12 -4
View File
@@ -59,10 +59,8 @@ app.add_middleware(
# ROOT & HEALTH ENDPOINTS
# ============================================
@app.get("/")
async def root():
"""Root endpoint."""
return {"message": "voicebox API", "version": __version__}
# Root endpoint removed - web UI served at / instead
# API info available at /health
@app.post("/shutdown")
@@ -1849,6 +1847,16 @@ async def get_active_tasks():
)
# ============================================
# WEB UI STATIC FILES
# ============================================
# Serve web UI at root if dist directory exists
_web_dist_path = Path(__file__).parent.parent / "web" / "dist"
if _web_dist_path.exists():
app.mount("/", StaticFiles(directory=str(_web_dist_path), html=True), name="web")
# ============================================
# STARTUP & SHUTDOWN
# ============================================
+11
View File
@@ -0,0 +1,11 @@
# Provider checksums - embedded at build time for security
# This file is auto-generated during CI builds
# In development, checksums are empty (verification is skipped)
PROVIDER_CHECKSUMS = {
# Populated during release builds with SHA256 checksums of provider binaries
# Example:
# "tts-provider-pytorch-cpu-windows.exe": "abc123...",
# "tts-provider-pytorch-cuda-windows.exe": "def456...",
# "tts-provider-pytorch-cuda-linux": "789xyz...",
}
+8
View File
@@ -21,6 +21,8 @@
"@hookform/resolvers": "^3.9.0",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@iconify-json/svg-spinners": "^1.2.4",
"@iconify/react": "^6.0.2",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
@@ -285,6 +287,12 @@
"@humanwhocodes/object-schema": ["@humanwhocodes/[email protected]", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
"@iconify-json/svg-spinners": ["@iconify-json/[email protected]", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-ayn0pogFPwJA1WFZpDnoq9/hjDxN+keeCMyThaX4d3gSJ3y0mdKUxIA/b1YXWGtY9wVtZmxwcvOIeEieG4+JNg=="],
"@iconify/react": ["@iconify/[email protected]", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-SMmC2sactfpJD427WJEDN6PMyznTFMhByK9yLW0gOTtnjzzbsi/Ke/XqsumsavFPwNiXs8jSiYeZTmLCLwO+Fg=="],
"@iconify/types": ["@iconify/[email protected]", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@img/colour": ["@img/[email protected]", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
"@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
+26
View File
@@ -0,0 +1,26 @@
version: '3.8'
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest
container_name: voicebox
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- LOG_LEVEL=info
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
voicebox-data:
driver: local
huggingface-cache:
driver: local
+34
View File
@@ -0,0 +1,34 @@
version: '3.8'
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest-cuda
container_name: voicebox
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- GPU_MEMORY_FRACTION=0.8
- LOG_LEVEL=info
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
voicebox-data:
driver: local
huggingface-cache:
driver: local
+1 -1
View File
@@ -40,7 +40,7 @@
{
"group": "Getting Started",
"icon": "rocket",
"pages": ["overview/introduction", "overview/installation", "overview/quick-start"]
"pages": ["overview/introduction", "overview/installation", "overview/docker", "overview/quick-start"]
},
{
"group": "Features",
+403
View File
@@ -0,0 +1,403 @@
---
title: "Docker Deployment"
description: "Run Voicebox in Docker with the web UI for server deployments"
---
## Overview
Voicebox is available as Docker images that include both the backend API and web UI. Run the full Voicebox experience in a container with a single command.
**What's included:**
- FastAPI backend with all TTS/Whisper capabilities
- Complete web UI (same React app as the desktop version)
- Provider download system (downloads PyTorch on first use)
- Multi-architecture support (amd64, arm64)
## Quick Start
<Tabs>
<Tab title="NVIDIA GPU">
```bash
docker run --gpus all -p 8000:8000 \
-v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest-cuda
```
Open http://localhost:8000 in your browser.
</Tab>
<Tab title="CPU Only">
```bash
docker run -p 8000:8000 \
-v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest
```
Open http://localhost:8000 in your browser.
</Tab>
<Tab title="Docker Compose">
Clone the repo or download `docker-compose.yml`:
```bash
# CUDA variant (default)
docker compose up -d
# CPU-only variant
docker compose -f docker-compose.cpu.yml up -d
```
Open http://localhost:8000 in your browser.
</Tab>
</Tabs>
<Note>
On first launch, you'll be prompted to download a TTS provider (PyTorch CPU ~300MB or PyTorch CUDA ~2.4GB). This happens once and is cached in the `huggingface-cache` volume.
</Note>
## Available Images
Images are automatically built and published to GitHub Container Registry on each release.
| Image | Description | Platforms |
|-------|-------------|-----------|
| `ghcr.io/jamiepine/voicebox:latest` | Latest CPU-only release | linux/amd64, linux/arm64 |
| `ghcr.io/jamiepine/voicebox:0.1.13` | Specific version (CPU) | linux/amd64, linux/arm64 |
| `ghcr.io/jamiepine/voicebox:latest-cuda` | Latest with NVIDIA GPU support | linux/amd64 |
| `ghcr.io/jamiepine/voicebox:0.1.13-cuda` | Specific version (CUDA) | linux/amd64 |
<Tip>
Pin to a specific version in production to avoid unexpected updates:
```yaml
image: ghcr.io/jamiepine/voicebox:0.1.13-cuda
```
</Tip>
## Docker Compose Examples
### GPU Deployment (Recommended)
```yaml
version: '3.8'
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest-cuda
container_name: voicebox
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- GPU_MEMORY_FRACTION=0.8
- LOG_LEVEL=info
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
voicebox-data:
huggingface-cache:
```
### CPU Deployment
```yaml
version: '3.8'
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest
container_name: voicebox
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- LOG_LEVEL=info
volumes:
voicebox-data:
huggingface-cache:
```
## Volume Mounts
<CardGroup cols={2}>
<Card title="voicebox-data" icon="database">
Stores voice profiles, generated audio, and database
</Card>
<Card title="huggingface-cache" icon="download">
Caches downloaded TTS/Whisper models (saves re-downloading)
</Card>
</CardGroup>
<Warning>
Always mount `/app/data` to preserve your voice profiles and generations across container restarts.
</Warning>
## Environment Variables
Configure Voicebox behavior with environment variables:
| Variable | Default | Description |
|----------|---------|-------------|
| `GPU_MEMORY_FRACTION` | `0.9` | Fraction of GPU memory to use (0.0-1.0) |
| `LOG_LEVEL` | `info` | Logging level: `debug`, `info`, `warning`, `error` |
| `DATA_DIR` | `/app/data` | Directory for profiles and generations |
Example:
```bash
docker run -e GPU_MEMORY_FRACTION=0.8 \
-e LOG_LEVEL=debug \
-p 8000:8000 \
ghcr.io/jamiepine/voicebox:latest-cuda
```
## Cloud Deployment
### AWS EC2
<Steps>
<Step title="Launch GPU Instance">
Use g4dn.xlarge or p3.2xlarge with NVIDIA GPU
</Step>
<Step title="Install Docker & NVIDIA Container Toolkit">
```bash
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Install NVIDIA Container Toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker
```
</Step>
<Step title="Deploy">
```bash
docker run -d --gpus all -p 8000:8000 \
-v voicebox-data:/app/data \
--restart unless-stopped \
ghcr.io/jamiepine/voicebox:latest-cuda
```
</Step>
</Steps>
### DigitalOcean
<Steps>
<Step title="Create GPU Droplet">
```bash
doctl compute droplet create voicebox \
--size gpu-h100x1-80gb \
--image ubuntu-22-04-x64 \
--region nyc3
```
</Step>
<Step title="SSH and Deploy">
```bash
ssh root@<droplet-ip>
curl -fsSL https://get.docker.com | sh
docker run -d --gpus all -p 80:8000 \
ghcr.io/jamiepine/voicebox:latest-cuda
```
</Step>
</Steps>
### Fly.io
Create `fly.toml`:
```toml
app = "voicebox"
[build]
image = "ghcr.io/jamiepine/voicebox:latest"
[[services]]
http_checks = []
internal_port = 8000
protocol = "tcp"
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[mounts]
source = "voicebox_data"
destination = "/app/data"
```
Deploy:
```bash
fly launch
fly deploy
```
## Updates
Docker images are automatically built and published on each GitHub release.
<Tabs>
<Tab title="Latest Tag">
Always get the newest version:
```bash
docker pull ghcr.io/jamiepine/voicebox:latest
docker compose up -d
```
</Tab>
<Tab title="Pinned Version">
Update to a specific version:
```yaml
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:0.1.13-cuda
```
```bash
docker compose pull
docker compose up -d
```
</Tab>
<Tab title="Automatic Updates">
Use Watchtower for automatic updates:
```yaml
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest-cuda
# ... other config ...
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 3600 # Check hourly
```
</Tab>
</Tabs>
## GPU Requirements
### NVIDIA GPU
Requires:
- **Docker version:** 19.03+
- **NVIDIA Driver:** 450.80.02+
- **NVIDIA Container Toolkit:** Installed and configured
Verify GPU access:
```bash
docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi
```
If this works, Voicebox will detect and use your GPU automatically.
### AMD GPU (ROCm)
AMD GPU support via ROCm is not currently available in pre-built images. If you need ROCm support, build a custom image using the ROCm base.
## Troubleshooting
### GPU Not Detected
<Accordion title="Check NVIDIA Docker">
```bash
# Verify NVIDIA Container Toolkit is installed
docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi
```
If this fails, reinstall NVIDIA Container Toolkit.
</Accordion>
<Accordion title="Insufficient GPU Memory">
Reduce GPU memory usage:
```bash
docker run -e GPU_MEMORY_FRACTION=0.5 \
--gpus all -p 8000:8000 \
ghcr.io/jamiepine/voicebox:latest-cuda
```
Or use CPU-only mode:
```bash
docker run -p 8000:8000 \
ghcr.io/jamiepine/voicebox:latest
```
</Accordion>
<Accordion title="Port Already in Use">
Change the host port:
```bash
docker run -p 8080:8000 ghcr.io/jamiepine/voicebox:latest
```
Then open http://localhost:8080
</Accordion>
<Accordion title="Permission Errors">
Run with specific user:
```bash
docker run --user $(id -u):$(id -g) \
-v $(pwd)/data:/app/data \
ghcr.io/jamiepine/voicebox:latest
```
</Accordion>
## Building From Source
If you need to customize the Docker image:
```bash
# Clone the repo
git clone https://github.com/jamiepine/voicebox.git
cd voicebox
# Build web UI
bun install
cd web && bun run build && cd ..
# Build Docker image
docker build -t voicebox:custom .
# Or CUDA variant
docker build -f Dockerfile.cuda -t voicebox:custom-cuda .
```
## Next Steps
<CardGroup cols={2}>
<Card title="API Reference" icon="code" href="/api/overview">
Integrate Voicebox into your applications
</Card>
<Card title="Remote Mode" icon="server" href="/overview/remote-mode">
Connect desktop app to Docker backend
</Card>
</CardGroup>
+5 -2
View File
@@ -7,13 +7,16 @@ description: "Download and install Voicebox on macOS, Windows, or Linux"
Voicebox is available for macOS and Windows, with Linux builds coming soon.
<CardGroup cols={2}>
<CardGroup cols={3}>
<Card title="macOS" icon="apple">
Download for Apple Silicon or Intel Macs
</Card>
<Card title="Windows" icon="windows">
Download MSI installer or Setup executable
</Card>
<Card title="Docker" icon="docker" href="/overview/docker">
Run with web UI in a container
</Card>
</CardGroup>
### macOS
@@ -61,7 +64,7 @@ Voicebox is available for macOS and Windows, with Linux builds coming soon.
### Linux
<Note>
Linux builds are coming soon. Currently blocked by GitHub runner disk space limitations.
Linux desktop builds are coming soon. For server deployments, use [Docker](/overview/docker).
</Note>
## First Launch
+72 -193
View File
@@ -1,24 +1,31 @@
# Docker Deployment Guide
**Status:** In Development for v0.2.0
**Requested By:** Reddit community ([thread](https://reddit.com/r/LocalLLaMA/...))
**Status:** Implemented
**Images:** `ghcr.io/jamiepine/voicebox`
## Overview
Docker support makes Voicebox easier to deploy, especially for:
Voicebox is available as Docker images with the full web UI included. Images are automatically built and published to GitHub Container Registry on each release.
- **Consistent Environments**: Same setup across dev/staging/prod
- **GPU Passthrough**: Easy NVIDIA/AMD GPU access
**What's included:**
- FastAPI backend with all TTS/Whisper capabilities
- Complete web UI (same React app as the Tauri desktop version)
- Provider download system (downloads TTS providers on first use, just like desktop)
- Multi-architecture support (amd64, arm64 for CPU variant)
Docker support is ideal for:
- **Server Deployments**: Run on headless Linux servers
- **Multi-User Setups**: Isolate instances per user/team
- **GPU Passthrough**: Easy NVIDIA GPU access
- **Consistent Environments**: Same setup across dev/staging/prod
- **Cloud Platforms**: Deploy to AWS, GCP, Azure, DigitalOcean
- **Multi-User Setups**: Isolate instances per user/team
## Quick Start
### Using Pre-Built Images (Recommended)
```bash
# CPU-only version
# CPU-only version (supports amd64 and arm64)
docker run -p 8000:8000 -v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest
@@ -26,184 +33,80 @@ docker run -p 8000:8000 -v voicebox-data:/app/data \
docker run --gpus all -p 8000:8000 -v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest-cuda
# AMD GPU version (experimental)
docker run --device=/dev/kfd --device=/dev/dri -p 8000:8000 \
-v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest-rocm
# Specific version (pinned for stability)
docker run -p 8000:8000 -v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:0.1.13
```
Then open: `http://localhost:8000`
The web UI will load automatically. On first use, you'll be prompted to download a TTS provider (PyTorch CPU ~300MB or PyTorch CUDA ~2.4GB).
### Using Docker Compose (Easiest)
Create `docker-compose.yml`:
Use the provided `docker-compose.yml` (CUDA) or `docker-compose.cpu.yml` in the repository root:
```yaml
version: '3.8'
```bash
# CUDA (default)
docker compose up -d
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest-cuda
ports:
- "8000:8000"
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- GPU_MEMORY_FRACTION=0.8 # Use 80% of GPU memory
- TTS_MODE=local
- WHISPER_MODE=local
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
voicebox-data:
huggingface-cache:
# Or CPU-only
docker compose -f docker-compose.cpu.yml up -d
```
Run:
```bash
docker compose up -d
To pin to a specific version, edit the compose file:
```yaml
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:0.1.13-cuda # Pinned version
```
## Building From Source
### Basic Dockerfile
```dockerfile
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
build-essential \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# Copy application
COPY backend/ /app/backend/
COPY requirements.txt /app/
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir git+https://github.com/QwenLM/Qwen3-TTS.git
# Create data directory
RUN mkdir -p /app/data
# Expose port
EXPOSE 8000
# Run server
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
See `Dockerfile` and `Dockerfile.cuda` in the repository root.
Build and run:
```bash
# Build web UI first
bun install
cd web && bun run build && cd ..
# Build CPU image
docker build -t voicebox .
docker run -p 8000:8000 -v $(pwd)/data:/app/data voicebox
docker run -p 8000:8000 -v voicebox-data:/app/data voicebox
# Or build CUDA image
docker build -f Dockerfile.cuda -t voicebox:cuda .
docker run --gpus all -p 8000:8000 -v voicebox-data:/app/data voicebox:cuda
```
### Multi-Stage Build (Optimized)
### Architecture
Smaller image size by separating build and runtime:
The Docker images include:
- **Backend**: FastAPI server with TTS/Whisper endpoints
- **Web UI**: Pre-built React app served as static files from the backend
- **Provider System**: Downloads PyTorch CPU/CUDA providers on first use (same UX as desktop app)
```dockerfile
# Dockerfile.optimized
# Stage 1: Build dependencies
FROM python:3.11-slim AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y \
git build-essential && \
rm -rf /var/lib/apt/lists/*
COPY backend/requirements.txt .
RUN pip install --no-cache-dir --target=/build/packages \
-r requirements.txt
RUN pip install --no-cache-dir --target=/build/packages \
git+https://github.com/QwenLM/Qwen3-TTS.git
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
# Install only runtime dependencies
RUN apt-get update && apt-get install -y \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# Copy installed packages from builder
COPY --from=builder /build/packages /usr/local/lib/python3.11/site-packages/
# Copy application code
COPY backend/ /app/backend/
# Create data directory
RUN mkdir -p /app/data
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
Build:
```bash
docker build -f Dockerfile.optimized -t voicebox:slim .
```
Images are automatically built on release and tagged with both version number and `latest`.
## GPU Support
### NVIDIA GPUs (CUDA)
**Dockerfile:**
```dockerfile
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
# Install Python
RUN apt-get update && apt-get install -y \
python3.11 python3-pip git ffmpeg && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install PyTorch with CUDA support
COPY backend/requirements.txt .
RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
# Install other dependencies
RUN pip3 install -r requirements.txt
RUN pip3 install git+https://github.com/QwenLM/Qwen3-TTS.git
COPY backend/ /app/backend/
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
The CUDA image includes PyTorch with CUDA 12.1 support:
**Run with GPU:**
```bash
docker run --gpus all -p 8000:8000 \
-v voicebox-data:/app/data \
voicebox:cuda
ghcr.io/jamiepine/voicebox:latest-cuda
```
**Docker Compose with GPU:**
```yaml
services:
voicebox:
image: voicebox:cuda
image: ghcr.io/jamiepine/voicebox:latest-cuda
deploy:
resources:
reservations:
@@ -213,47 +116,9 @@ services:
capabilities: [gpu]
```
### AMD GPUs (ROCm) - Experimental
### AMD GPUs (ROCm)
**Dockerfile:**
```dockerfile
FROM rocm/dev-ubuntu-22.04:6.0
# Install Python
RUN apt-get update && apt-get install -y \
python3.11 python3-pip git ffmpeg && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install PyTorch with ROCm support
COPY backend/requirements.txt .
RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.0
# Install other dependencies
RUN pip3 install -r requirements.txt
RUN pip3 install git+https://github.com/QwenLM/Qwen3-TTS.git
# Set ROCm environment variables
ENV HSA_OVERRIDE_GFX_VERSION=10.3.0
ENV ROCM_PATH=/opt/rocm
COPY backend/ /app/backend/
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
**Run with AMD GPU:**
```bash
docker run --device=/dev/kfd --device=/dev/dri \
--group-add video --ipc=host --cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
-p 8000:8000 -v voicebox-data:/app/data \
voicebox:rocm
```
**Note:** ROCm support varies by GPU model. Works best on Linux. See [AMD ROCm docs](https://rocm.docs.amd.com) for compatibility.
ROCm support is not currently available in pre-built images. If you need ROCm, build a custom image using the ROCm base and PyTorch ROCm builds.
## Volume Mounts
@@ -734,13 +599,27 @@ docker logs -f voicebox
docker compose logs -f voicebox
```
## Next Steps
## Updates
- [ ] Publish official images to GitHub Container Registry
- [ ] Add Kubernetes Helm charts
- [ ] Create Docker Desktop extension
- [ ] Add automated vulnerability scanning
- [ ] Support ARM64 builds for Raspberry Pi / Apple Silicon
Docker images are automatically built and published on each GitHub release. To update:
```bash
# Pull latest
docker pull ghcr.io/jamiepine/voicebox:latest
docker compose up -d
# Or pin to a specific version
docker pull ghcr.io/jamiepine/voicebox:0.1.13
```
For automatic updates, use [Watchtower](https://containrrr.dev/watchtower/).
## Future Enhancements
- Kubernetes Helm charts
- Docker Desktop extension
- Automated vulnerability scanning
- ROCm image variant
## Contributing
+94
View File
@@ -0,0 +1,94 @@
# Documentation Migration: Mintlify → Fumadocs
This document summarizes the migration of documentation from `/docs` (Mintlify) to `/docs2` (Fumadocs).
## What Was Done
### 1. Files Copied
- ✅ All 29 MDX files from `/docs` folders (overview, api, developer, plans)
- ✅ All 4 root-level markdown files (AUTOUPDATER.md, AUTOUPDATER_QUICKSTART.md, TROUBLESHOOTING.md, README.md)
- ✅ All images (3 webp files) → `public/images/`
- ✅ All logo files (2 png files) → `public/logo/`
### 2. Component Migration
Created compatibility layer in `components/mintlify-compat.tsx` that maps Mintlify components to Fumadocs equivalents:
- `<Frame>` → Simple div wrapper (images are zoomable by default in Fumadocs)
- `<CardGroup>``<Cards>` (Fumadocs component)
- `<Card>``<Card>` (with icon string → Lucide icon mapping)
- `<Steps>` / `<Step>` → Direct mapping to Fumadocs components
- `<Tip>`, `<Note>`, `<Info>``<Callout type="info">`
- `<Warning>``<Callout type="warn">`
- `<Danger>``<Callout type="error">`
- `<AccordionGroup>` / `<Accordion>` → HTML `<details>` / `<summary>` elements
### 3. Navigation Structure
Created `meta.json` files for each folder:
- `content/docs/meta.json` - Root documentation
- `content/docs/overview/meta.json` - Overview pages
- `content/docs/api/meta.json` - API reference
- `content/docs/developer/meta.json` - Developer docs
- `content/docs/plans/meta.json` - Plans/roadmap
### 4. Link Fixes
- Fixed incorrect `/guides/...` paths → `/overview/...`
- All internal links now use correct paths
### 5. Branding
- Updated `lib/layout.shared.tsx` to use "Voicebox" as the nav title
## File Structure
```
docs2/
├── components/
│ └── mintlify-compat.tsx # Mintlify → Fumadocs component mappings
├── content/docs/
│ ├── meta.json # Root navigation
│ ├── overview/ # 12 MDX files
│ ├── api/ # 5 MDX files
│ ├── developer/ # 12 MDX files
│ ├── plans/ # 4 MD files
│ └── *.md # 4 root markdown files
├── public/
│ ├── images/ # 3 webp files
│ └── logo/ # 2 png files
└── mdx-components.tsx # MDX component configuration
```
## Icon Mapping
The following icon strings are mapped to Lucide icons:
- `microphone` → Mic
- `film` → Film
- `code` → Code
- `shield` → Shield
- `download` → Download
- `rocket` → Rocket
- `apple` → Apple
- `windows` → Windows
- `server` → Server
- `user` → User
- `waveform` → Waveform
## Next Steps
1. **Test the build**: Run `npm run build` (requires Node.js >= 20.9.0)
2. **Start dev server**: Run `npm run dev` to preview
3. **Customize styling**: Update `app/global.css` if needed
4. **Add more icons**: Extend `iconMap` in `mintlify-compat.tsx` as needed
5. **Review navigation**: Adjust `meta.json` files to customize page order
## Notes
- Image paths (`/images/...`) work as-is since Next.js serves from `public/`
- All Mintlify components are now compatible with Fumadocs
- Navigation structure follows Fumadocs conventions
- No breaking changes to content - all MDX files work with compatibility layer
+203
View File
@@ -0,0 +1,203 @@
---
title: "Auto-Updater Documentation"
description: "How Voicebox automatic updates work for users and developers"
---
Voicebox includes automatic updates powered by Tauri's updater plugin. This document explains how it works for both users and developers.
## 1. Generate Signing Keys
Run this command to generate your signing keypair:
```bash
cd tauri && bun tauri signer generate -w ~/.tauri/voicebox.key
```
This creates:
- **Private key**: `~/.tauri/voicebox.key` (keep this secret!)
- **Public key**: `~/.tauri/voicebox.key.pub`
## 2. Update Configuration
Copy the content from `~/.tauri/voicebox.key.pub` and replace the placeholder in `tauri/src-tauri/tauri.conf.json`:
```json
{
"plugins": {
"updater": {
"pubkey": "PASTE_PUBLIC_KEY_CONTENT_HERE",
"endpoints": [
"https://github.com/YOUR_USERNAME/voicebox/releases/latest/download/latest.json"
]
}
}
}
```
Update the endpoint URL with your actual GitHub username/organization.
## 3. Building with Signatures
When building releases, set these environment variables:
**macOS/Linux:**
```bash
export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.tauri/voicebox.key)"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD=""
bun run build
```
**Windows PowerShell:**
```powershell
$env:TAURI_SIGNING_PRIVATE_KEY = Get-Content ~/.tauri/voicebox.key -Raw
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = ""
bun run build
```
## 4. GitHub Release Setup
When you create a GitHub release, the build process will generate:
- Installers for each platform
- `.sig` signature files
- `latest.json` update manifest
### Manual Release Process
1. Build the app with signing keys set
2. Create a new GitHub release
3. Upload all files from `tauri/src-tauri/target/release/bundle/`
4. Create `latest.json` in your release assets:
```json
{
"version": "0.2.0",
"notes": "Bug fixes and improvements",
"pub_date": "2026-01-25T12:00:00Z",
"platforms": {
"darwin-aarch64": {
"signature": "CONTENT_FROM_.app.tar.gz.sig",
"url": "https://github.com/YOUR_USERNAME/voicebox/releases/download/v0.2.0/voicebox_0.2.0_aarch64.dmg"
},
"darwin-x86_64": {
"signature": "CONTENT_FROM_.app.tar.gz.sig",
"url": "https://github.com/YOUR_USERNAME/voicebox/releases/download/v0.2.0/voicebox_0.2.0_x64.dmg"
},
"linux-x86_64": {
"signature": "CONTENT_FROM_.AppImage.sig",
"url": "https://github.com/YOUR_USERNAME/voicebox/releases/download/v0.2.0/voicebox_0.2.0_amd64.AppImage"
},
"windows-x86_64": {
"signature": "CONTENT_FROM_.msi.sig",
"url": "https://github.com/YOUR_USERNAME/voicebox/releases/download/v0.2.0/voicebox_0.2.0_x64_en-US.msi"
}
}
}
```
### Automated GitHub Actions (Recommended)
Create `.github/workflows/release.yml`:
```yaml
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
strategy:
matrix:
platform: [macos-latest, ubuntu-22.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install dependencies
run: bun install
- name: Build
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: bun run build
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
files: tauri/src-tauri/target/release/bundle/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
Add your private key to GitHub secrets:
- Go to Settings → Secrets and variables → Actions
- Add `TAURI_SIGNING_PRIVATE_KEY` with the content of `~/.tauri/voicebox.key`
- Add `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (empty string if no password)
## Frontend Integration
The frontend integration is complete with automatic update notifications and manual update checks:
- **Update Notification Banner** - Appears automatically when updates are available
- **Settings Panel** - Manual "Check for Updates" button in Settings tab
- **Update Hook** - React hook handles all update operations
See `docs/AUTOUPDATER_QUICKSTART.md` for a quick setup guide.
## Security Notes
- Never commit your private key to version control
- Store private keys securely (use GitHub secrets for CI/CD)
- The public key in `tauri.conf.json` is safe to commit
- Updates are cryptographically verified before installation
- HTTP endpoints are blocked by default (HTTPS only)
## Testing Updates
1. Build version 0.1.0 and install it
2. Update version in `tauri.conf.json` to 0.2.0
3. Build version 0.2.0 with signatures
4. Create a local server or GitHub release with `latest.json`
5. Run version 0.1.0 and trigger update check
6. Verify update downloads and installs correctly
## Troubleshooting
**"Invalid signature" error:**
- Verify public key matches the private key used to sign
- Ensure signature files (.sig) are uploaded correctly
**"No update available" when one exists:**
- Check endpoint URL is correct
- Verify `latest.json` format matches specification
- Ensure version in latest.json is higher than current version
**Build fails with signing:**
- Confirm environment variables are set correctly
- Check private key file exists and is readable
- Verify private key format (should start with `dW50cnVzdGVkIGNvbW1lbnQ6`)
@@ -0,0 +1,129 @@
---
title: "Autoupdater Quick Start"
description: "Quick guide to activate the Tauri v2 autoupdater"
---
The Tauri v2 autoupdater has been fully configured and integrated. Follow these steps to activate it.
## What's Already Done
✅ Rust plugin installed and initialized
✅ Tauri configuration set up with updater settings
✅ Permissions granted for update operations
✅ GitHub Actions workflow updated with signing support
✅ Frontend components created and integrated
✅ Update notifications on app startup
✅ Manual update check in Settings tab
## Required Steps (5 minutes)
### 1. Generate Signing Keys
```bash
bun run generate:keys
```
This creates:
- Private key: `~/.tauri/voicebox.key` (keep secret!)
- Public key: `~/.tauri/voicebox.key.pub` (safe to share)
### 2. Update Tauri Config
Open `tauri/src-tauri/tauri.conf.json` and:
1. Replace `"REPLACE_WITH_YOUR_PUBLIC_KEY"` with the content from `~/.tauri/voicebox.key.pub`
2. Update the endpoint URL with your GitHub username:
```json
"endpoints": [
"https://github.com/YOUR_USERNAME/voicebox/releases/latest/download/latest.json"
]
```
### 3. Add GitHub Secrets
Go to your repo Settings → Secrets and variables → Actions:
1. Add `TAURI_SIGNING_PRIVATE_KEY`:
```bash
cat ~/.tauri/voicebox.key
```
Copy the entire output and paste as the secret value
2. Add `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`:
Leave empty (or add your password if you set one)
### 4. Test the Setup
To test locally before creating a release:
```bash
bun run build:release
```
This will verify your keys are set up correctly.
## How It Works
### For Users
1. App checks for updates on startup (only in Tauri builds)
2. If an update is available, a banner appears at the top
3. Users can click "Install Now" to download and install
4. App restarts automatically after installation
### For Developers
1. Create a new git tag: `git tag v0.2.0 && git push --tags`
2. GitHub Actions builds signed releases for all platforms
3. Uploads installers and generates `latest.json` manifest
4. Users running older versions will be notified automatically
## UI Components
### Update Notification Banner
- Shows at top of app when update is available
- Appears automatically on startup
- Displays download/install progress
### Settings Panel
- Located in Settings tab
- Shows current version
- Manual "Check for Updates" button
- Update status and progress
## Troubleshooting
**"Public key not configured"**
- Make sure you copied the entire content from `voicebox.key.pub`
- The key should start with `dW50cnVzdGVkIGNvbW1lbnQ6`
**"Failed to check for updates"**
- Endpoint URL might be incorrect
- No releases published yet (expected for first setup)
**Build fails with signing error**
- Check that GitHub secrets are set correctly
- Verify private key file exists at `~/.tauri/voicebox.key`
## Next Release Workflow
1. Update version in `tauri/src-tauri/tauri.conf.json`
2. Commit changes
3. Create and push tag: `git tag v0.2.0 && git push --tags`
4. GitHub Actions will automatically build and create a draft release
5. Review the release and publish it
6. Users will be notified of the update
## See Also
- Full documentation: `docs/AUTOUPDATER.md`
- Build script: `scripts/prepare-release.sh`
- GitHub workflow: `.github/workflows/release.yml`
+67
View File
@@ -0,0 +1,67 @@
---
title: "Documentation README"
description: "Voicebox documentation development guide"
---
This directory contains the documentation for Voicebox, built with [Fumadocs](https://fumadocs.dev).
## Development
### Prerequisites
Install Mintlify globally using bun:
```bash
bun add -g mintlify
```
Or use the helper script:
```bash
bun run install:mintlify
```
### Running Locally
```bash
bun run dev
```
This will start the Mintlify dev server.
The docs will be available at `http://localhost:3000`
### Structure
```
docs/
├── mint.json # Mintlify configuration
├── custom.css # Custom styles
├── overview/ # Getting started & feature docs
├── guides/ # User guides
├── api/ # API reference
├── development/ # Developer documentation
├── logo/ # Logo assets
└── public/ # Static assets
```
### Writing Docs
- Use `.mdx` files for all documentation pages
- Follow the existing structure in `mint.json` for navigation
- Use Mintlify components for enhanced formatting (Card, CardGroup, Accordion, etc.)
- Reference the [Mintlify documentation](https://mintlify.com/docs) for available components
## Deployment
Docs are automatically deployed when changes are pushed to the main branch.
To manually deploy:
```bash
mintlify deploy
```
## Contributing
See [CONTRIBUTING.md](../CONTRIBUTING.md) for contribution guidelines.
+360
View File
@@ -0,0 +1,360 @@
---
title: "Troubleshooting Guide"
description: "Common issues and solutions for Voicebox"
---
Common issues and solutions for Voicebox.
## Installation Issues
### macOS: "Voicebox cannot be opened because it is from an unidentified developer"
**Solution:**
1. Right-click the `.dmg` file
2. Select "Open"
3. Click "Open" in the security dialog
4. Alternatively, go to System Settings → Privacy & Security → Allow Voicebox
### Windows: "Windows protected your PC"
**Solution:**
1. Click "More info"
2. Click "Run anyway"
3. Windows Defender may flag new software; this is normal for unsigned apps
### Linux: AppImage won't run
**Solution:**
```bash
chmod +x voicebox-*.AppImage
./voicebox-*.AppImage
```
## Runtime Issues
### Server won't start
**Symptoms:** App opens but shows "Server not connected"
**Solutions:**
1. **Check Python installation**
```bash
python --version # Should be 3.11+
```
2. **Check server binary exists**
- Look in `tauri/src-tauri/binaries/` for your platform
- Binary should match your system architecture
3. **Check permissions**
```bash
# macOS/Linux
chmod +x tauri/src-tauri/binaries/voicebox-server-*
```
4. **Check logs**
- macOS: Open Console.app and search for "voicebox"
- Linux: Check `~/.local/share/voicebox/` for logs
- Windows: Check Event Viewer
### "Model download failed"
**Symptoms:** First generation fails with download error
**Solutions:**
1. **Check internet connection**
- Models download from HuggingFace Hub (~2-4GB)
- First download may take several minutes
2. **Check disk space**
- Models are cached in `~/.cache/huggingface/`
- Ensure at least 5GB free space
3. **Manual download** (if automatic fails)
```bash
pip install huggingface_hub
huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-Base
```
### "Out of memory" errors
**Symptoms:** Generation fails with CUDA/VRAM errors
**Solutions:**
1. **Use smaller model**
- Switch to 0.6B model instead of 1.7B
- Settings → Model Management → Load 0.6B
2. **Close other applications**
- Free up GPU memory
- Close browser tabs, other ML apps
3. **Use CPU mode**
- Slower but works without GPU
- Backend automatically falls back to CPU
### MLX "Failed to load the default metallib" error (Apple Silicon)
**Symptoms:** Generation fails with "library not found" or "metallib" errors
**Solutions:**
1. **Rebuild server binary**
```bash
bun run build:server
```
The build script should automatically include MLX Metal shader libraries.
2. **Check MLX installation**
```bash
pip install -r backend/requirements-mlx.txt
```
3. **Verify backend detection**
- Check server logs for "Backend: MLX"
- If showing "Backend: PYTORCH", MLX may not be installed correctly
### Audio playback issues
**Symptoms:** Generated audio won't play
**Solutions:**
1. **Check audio format**
- Audio is saved as WAV files
- Ensure your system supports WAV playback
2. **Try downloading audio**
- Right-click → Download
- Play in external player
3. **Check browser permissions** (web version)
- Allow audio autoplay in browser settings
### Slow generation
**Symptoms:** Generation takes >30 seconds
**Solutions:**
1. **Check backend type** (Apple Silicon)
- Check Settings → Server Status
- Should show "Backend: MLX" on Apple Silicon
- If showing "Backend: PYTORCH", install MLX: `pip install -r backend/requirements-mlx.txt`
- MLX provides 4-5x faster inference on Apple Silicon
2. **Use GPU** (if available)
- Check Settings → Server Status
- Should show "GPU available: true"
- Apple Silicon: Should show "Metal (Apple Silicon via MLX)"
- Windows/Linux: Should show "CUDA" if GPU available
3. **Enable caching**
- Voice prompts are cached automatically
- Second generation with same voice should be faster
4. **Use smaller model**
- 0.6B model is faster than 1.7B
- Quality difference is minimal for most voices
5. **Check system resources**
- Close other CPU/GPU intensive apps
- Ensure adequate RAM (8GB+ recommended)
## API Issues
### "Connection refused" when using API
**Solutions:**
1. **Check server is running**
```bash
curl http://localhost:8000/health
```
2. **Check remote mode**
- If connecting remotely, ensure server is started with `--host 0.0.0.0`
- Check firewall settings
3. **Check port availability**
- Default port is 8000
- Ensure no other service is using it
### CORS errors in browser
**Solutions:**
1. **Use desktop app** (recommended)
- Desktop app doesn't have CORS restrictions
2. **Configure CORS** (for web deployment)
- Update `backend/main.py` CORS settings
- Add your domain to allowed origins
## Update Issues
### "Update check failed"
**Solutions:**
1. **Check internet connection**
- Updates are fetched from GitHub releases
2. **Check GitHub access**
- Ensure `github.com` is accessible
- Check firewall/proxy settings
3. **Manual update**
- Download latest release from GitHub
- Install manually
### "Invalid signature" error
**Solutions:**
1. **Re-download installer**
- Signature may be corrupted
- Download fresh copy from GitHub
2. **Check release integrity**
- Verify `.sig` file matches installer
- Report issue if signature is invalid
## Data Issues
### Profiles disappeared
**Solutions:**
1. **Check data directory**
- macOS: `~/Library/Application Support/voicebox/`
- Windows: `%APPDATA%/voicebox/`
- Linux: `~/.local/share/voicebox/`
2. **Check database**
- Database: `data/voicebox.db`
- Ensure file exists and is readable
3. **Restore from backup**
- Profiles can be exported/imported
- Check for backup files
### "Database locked" error
**Solutions:**
1. **Close other instances**
- Ensure only one Voicebox instance is running
2. **Restart app**
- Close and reopen Voicebox
3. **Check file permissions**
- Ensure database file is writable
- Check directory permissions
## Development Issues
### Build fails
**Solutions:**
1. **Check Rust installation**
```bash
rustc --version
rustup update
```
2. **Check Tauri dependencies**
```bash
cd tauri
bun install
```
3. **Clean build**
```bash
cd tauri/src-tauri
cargo clean
cd ../..
bun run build
```
### API client generation fails
**Solutions:**
1. **Start backend server**
```bash
bun run dev:server
```
2. **Check OpenAPI endpoint**
```bash
curl http://localhost:8000/openapi.json
```
3. **Regenerate client**
```bash
bun run generate:api
```
## Still Having Issues?
1. **Check existing issues**
- Search GitHub issues for similar problems
- Check closed issues for solutions
2. **Create new issue**
- Include:
- OS and version
- Voicebox version
- Steps to reproduce
- Error messages/logs
- Screenshots (if applicable)
3. **Get help**
- Check documentation in `docs/`
- Review `backend/README.md` for API details
- See `CONTRIBUTING.md` for development help
---
For more help, open an issue on [GitHub](https://github.com/jamiepine/voicebox/issues).
+4
View File
@@ -0,0 +1,4 @@
{
"title": "API Reference",
"pages": ["overview", "authentication", "voice-profiles", "generation", "recordings"]
}
@@ -0,0 +1,336 @@
---
title: "Contributing"
description: "How to contribute to Voicebox"
---
Thank you for your interest in contributing to Voicebox! This guide will help you get started.
## Code of Conduct
- Be respectful and inclusive
- Welcome newcomers and help them learn
- Focus on constructive feedback
- Respect different viewpoints and experiences
## Getting Started
Before you start contributing, make sure you have:
1. **Read the documentation** to understand how Voicebox works
2. **Set up your development environment** - see [Development Setup](/development/setup)
3. **Explored the codebase** to understand the project structure
4. **Checked existing issues** to see if someone else is working on something similar
## Ways to Contribute
<CardGroup cols={2}>
<Card title="Report Bugs" icon="bug">
Found a bug? Open an issue with reproduction steps
</Card>
<Card title="Request Features" icon="lightbulb">
Have an idea? Start a discussion or open an issue
</Card>
<Card title="Improve Docs" icon="book">
Fix typos, add examples, or clarify instructions
</Card>
<Card title="Write Code" icon="code">
Fix bugs, add features, or optimize performance
</Card>
</CardGroup>
## Development Workflow
### 1. Fork & Clone
```bash
# Fork the repository on GitHub
# Then clone your fork
git clone https://github.com/YOUR_USERNAME/voicebox.git
cd voicebox
```
### 2. Create a Branch
Use descriptive branch names:
```bash
# For features
git checkout -b feature/voice-effects
# For bug fixes
git checkout -b fix/audio-playback-issue
# For documentation
git checkout -b docs/api-examples
```
### 3. Make Your Changes
Follow these guidelines:
<AccordionGroup>
<Accordion title="Code Style">
**TypeScript/React:**
- Use TypeScript strict mode
- Prefer functional components with hooks
- Use named exports
- Format with Biome (runs automatically)
**Python:**
- Follow PEP 8
- Use type hints
- Use async/await for I/O
- Document functions with docstrings
**Rust:**
- Follow Rust conventions
- Use meaningful names
- Handle errors explicitly
- Run `rustfmt`
</Accordion>
<Accordion title="Commit Messages">
Write clear, descriptive commit messages:
```bash
# Good
git commit -m "Add voice profile export feature"
git commit -m "Fix audio playback stopping after 30 seconds"
# Avoid
git commit -m "Update code"
git commit -m "Fix bug"
```
Format:
- Use imperative mood ("Add feature" not "Added feature")
- Keep first line under 50 characters
- Add detailed description if needed
</Accordion>
<Accordion title="Testing">
- Test your changes manually in the app
- Ensure backend API endpoints work
- Check for TypeScript/Python errors
- Verify UI components render correctly
- Add automated tests when possible
</Accordion>
</AccordionGroup>
### 4. Push & Create PR
```bash
# Push your branch
git push origin feature/your-feature-name
# Then create a pull request on GitHub
```
## Pull Request Guidelines
When creating a pull request:
<Steps>
<Step title="Use a Clear Title">
Examples:
- "Add voice profile export functionality"
- "Fix audio playback stopping after 30 seconds"
- "Improve generation speed with caching"
</Step>
{" "}
<Step title="Provide Description">
Include: - What changes you made - Why you made them - How to test them -
Screenshots (for UI changes) - Reference related issues
</Step>
{" "}
<Step title="Update Documentation">
- Update relevant docs if behavior changes - Add API documentation for new
endpoints - Update README if needed
</Step>
<Step title="Check the Checklist">
- [ ] Code follows style guidelines
- [ ] Documentation updated
- [ ] Changes tested
- [ ] No breaking changes (or documented)
- [ ] CHANGELOG.md updated
</Step>
</Steps>
## Project Structure
Understanding the codebase:
```
voicebox/
├── app/ # Shared React frontend
│ ├── src/
│ │ ├── components/ # UI components
│ │ ├── lib/ # Utilities and API client
│ │ ├── hooks/ # React hooks
│ │ └── stores/ # Zustand state stores
├── backend/ # Python FastAPI server
│ ├── main.py # API routes
│ ├── tts.py # Voice synthesis logic
│ ├── database.py # SQLite operations
│ └── models.py # Pydantic models
├── tauri/ # Desktop app wrapper
│ └── src-tauri/ # Rust backend
├── web/ # Web deployment
├── landing/ # Marketing website
└── scripts/ # Build & release scripts
```
## Areas for Contribution
### Bug Fixes
- Check [existing issues](https://github.com/jamiepine/voicebox/issues) for bugs
- Test your fix thoroughly
- Add regression tests if possible
### New Features
- Check the [roadmap](https://github.com/jamiepine/voicebox#roadmap) for planned features
- Discuss major features in an issue first
- Keep features focused and well-scoped
### Documentation
- Improve clarity and fix typos
- Add code examples
- Create tutorials or guides
- Document API endpoints
### UI/UX Improvements
- Improve accessibility
- Enhance visual design
- Optimize performance
- Add animations/transitions
### Infrastructure
- Improve build process
- Add CI/CD improvements
- Optimize bundle size
- Add testing infrastructure
## API Development
When adding new API endpoints:
<Steps>
<Step title="Add Route">
In `backend/main.py`:
```python
@app.post("/api/new-endpoint")
async def new_endpoint(data: RequestModel) -> ResponseModel:
"""Endpoint description."""
# Implementation
return response
```
</Step>
<Step title="Create Models">
In `backend/models.py`:
```python
class RequestModel(BaseModel):
field: str
class ResponseModel(BaseModel):
result: str
```
</Step>
<Step title="Regenerate Client">
```bash
bun run generate:api
```
This updates the TypeScript client with type-safe bindings.
</Step>
<Step title="Update Docs">
The API documentation is automatically generated from the OpenAPI schema. Ensure your endpoint has proper docstrings and type hints, then regenerate the docs:
```bash
bun run generate:api
```
</Step>
</Steps>
## Testing
Currently testing is primarily manual. When adding tests:
**Backend:**
```bash
cd backend
pytest
```
**Frontend:**
```bash
bun run test
```
**E2E (future):**
```bash
bun run test:e2e
```
## Release Process
Releases are managed by maintainers using `bumpversion`:
```bash
# Bump version (patch, minor, or major)
bumpversion patch
# Push with tags
git push && git push --tags
```
GitHub Actions automatically builds and publishes releases when tags are pushed.
## Community
- **GitHub Issues:** Bug reports and feature requests
- **GitHub Discussions:** General questions and ideas
- **Discord:** Real-time chat (coming soon)
## Recognition
Contributors are recognized in:
- [CHANGELOG.md](https://github.com/jamiepine/voicebox/blob/main/CHANGELOG.md)
- GitHub contributor list
- Release notes
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
## Questions?
If you have questions:
1. Check the [documentation](/overview/introduction)
2. Search [existing issues](https://github.com/jamiepine/voicebox/issues)
3. Open a new issue or discussion
4. See [CONTRIBUTING.md](https://github.com/jamiepine/voicebox/blob/main/CONTRIBUTING.md) in the repo
Thank you for contributing to Voicebox! 🎉
+17
View File
@@ -0,0 +1,17 @@
{
"title": "Developer",
"pages": [
"setup",
"architecture",
"contributing",
"building",
"autoupdater",
"voice-profiles",
"tts-generation",
"history",
"stories",
"transcription",
"audio-channels",
"model-management"
]
}
+245
View File
@@ -0,0 +1,245 @@
---
title: "Development Setup"
description: "Set up your local development environment for Voicebox"
---
## Prerequisites
Before you begin, ensure you have the following installed:
<CardGroup cols={3}>
<Card title="Bun" icon="package">
[Download Bun](https://bun.sh) ```bash curl -fsSL https://bun.sh/install |
bash ```
</Card>
<Card title="Python 3.11+" icon="python">
[Download Python](https://python.org) ```bash python --version ```
</Card>
<Card title="Rust" icon="rust">
[Install Rust](https://rustup.rs) ```bash rustc --version ```
</Card>
</CardGroup>
## Clone the Repository
```bash
git clone https://github.com/jamiepine/voicebox.git
cd voicebox
```
## Quick Setup (Recommended)
The easiest way to get started is using the Makefile:
```bash
# Setup everything
make setup
# Start development
make dev
```
<Note>
The Makefile is available on macOS and Linux. Windows users should follow the
manual setup below.
</Note>
## Manual Setup
### 1. Install JavaScript Dependencies
```bash
bun install
```
This installs dependencies for:
- `app/` - Shared React frontend
- `tauri/` - Tauri desktop wrapper
- `web/` - Web deployment wrapper
### 2. Set Up Python Backend
```bash
cd backend
# Create virtual environment
python -m venv venv
# Activate virtual environment
source venv/bin/activate # macOS/Linux
# or
venv\Scripts\activate # Windows
# Install Python dependencies
pip install -r requirements.txt
# Install MLX dependencies (Apple Silicon only - for faster inference)
# On Apple Silicon, this enables native Metal acceleration
if [[ $(uname -m) == "arm64" ]]; then
pip install -r requirements-mlx.txt
fi
# Install Qwen3-TTS
pip install git+https://github.com/QwenLM/Qwen3-TTS.git
```
## Running in Development
Development requires **two terminals**: one for the Python backend, one for the Tauri app.
<Tabs>
<Tab title="Terminal 1: Backend">
Start the Python server first:
```bash
cd backend
source venv/bin/activate # Activate venv
bun run dev:server
```
Or manually:
```bash
uvicorn main:app --reload --port 17493
```
Backend will be available at `http://localhost:17493`
</Tab>
<Tab title="Terminal 2: Desktop App">
Then start the Tauri app:
```bash
bun run dev
```
This will:
- Create a placeholder sidecar binary
- Start Vite dev server on port 5173
- Launch Tauri window
- Enable hot reload
</Tab>
</Tabs>
<Info>
In dev mode, the app connects to your manually-started Python server. The
bundled server binary is only used in production builds.
</Info>
### Optional: Web App
```bash
bun run dev:web
```
Web app will be available at `http://localhost:5174`
## Model Downloads
Models are automatically downloaded from HuggingFace Hub on first use:
- **Whisper** (transcription): Auto-downloads on first transcription
- **Qwen3-TTS** (voice cloning): Auto-downloads on first generation (~2-4GB)
<Warning>
First-time usage will be slower due to model downloads, but subsequent runs
will use cached models.
</Warning>
## Project Structure
```
voicebox/
├── app/ # Shared React frontend
│ └── src/
│ ├── components/ # UI components
│ ├── lib/ # Utilities and API client
│ └── hooks/ # React hooks
├── backend/ # Python FastAPI server
│ ├── main.py # API routes
│ ├── tts.py # Voice synthesis
│ └── database.py # SQLite operations
├── tauri/ # Desktop app wrapper
│ └── src-tauri/ # Rust backend
├── web/ # Web deployment
├── landing/ # Marketing website
└── scripts/ # Build & release scripts
```
## Available Make Commands
Run `make help` to see all available commands:
```bash
make setup # Install all dependencies
make dev # Start development servers
make dev-web # Start web development server
make build # Build desktop app
make build-web # Build web app
make clean # Clean build artifacts
make test # Run tests
```
## Generate OpenAPI Client
After starting the backend server, generate the TypeScript API client:
```bash
./scripts/generate-api.sh
# or
bun run generate:api
```
This downloads the OpenAPI schema and generates the TypeScript client in `app/src/lib/api/`
## Next Steps
<CardGroup cols={2}>
<Card
title="Architecture"
icon="diagram-project"
href="/development/architecture"
>
Understand the system architecture
</Card>
<Card
title="Contributing"
icon="code-pull-request"
href="/development/contributing"
>
Read the contribution guidelines
</Card>
<Card title="Building" icon="hammer" href="/development/building">
Learn how to build production releases
</Card>
<Card title="API Reference" icon="code" href="/api-reference">
Explore the REST API
</Card>
</CardGroup>
## Troubleshooting
<AccordionGroup>
<Accordion title="Backend won't start">
- Check Python version (must be 3.11+)
- Ensure virtual environment is activated
- Verify all dependencies are installed: `pip install -r requirements.txt`
- Check if port 17493 is available
</Accordion>
{" "}
<Accordion title="Tauri build fails">
- Ensure Rust is installed: `rustc --version` - Clean the build: `cd
tauri/src-tauri && cargo clean` - Try rebuilding: `bun run dev`
</Accordion>
<Accordion title="OpenAPI client generation fails">
- Ensure backend is running: `curl http://localhost:17493/openapi.json`
- Check network connectivity
- Verify the backend is accessible at localhost:17493
</Accordion>
</AccordionGroup>
See the full [Troubleshooting Guide](/guides/troubleshooting) for more issues and solutions.
+50
View File
@@ -0,0 +1,50 @@
---
title: "Voicebox Documentation"
description: "Welcome to Voicebox - the open-source voice synthesis studio"
---
## What is Voicebox?
Voicebox is a **local-first voice cloning studio** with DAW-like features for professional voice synthesis. Think of it as the **Ollama for voice** — download models, clone voices, and generate speech entirely on your machine.
<Frame>
<img src="/images/app-screenshot-1.webp" alt="Voicebox App Screenshot" />
</Frame>
Unlike cloud services that lock your voice data behind subscriptions, Voicebox gives you:
- **Complete privacy** — models and voice data stay on your machine
- **Professional tools** — multi-track timeline editor, audio trimming, conversation mixing
- **Model flexibility** — currently powered by Qwen3-TTS, with support for XTTS, Bark, and other models coming soon
- **API-first** — use the desktop app or integrate voice synthesis into your own projects
- **Native performance** — built with Tauri (Rust), not Electron
Download a voice model, clone any voice from a few seconds of audio, and compose multi-voice projects with studio-grade editing tools. No Python install required, no cloud dependency, no limits.
## Key Features
<CardGroup cols={2}>
<Card title="Voice Cloning" icon="microphone">
Instant cloning from just a few seconds of audio with Qwen3-TTS
</Card>
<Card title="Stories Editor" icon="film">
Multi-track timeline for creating conversations and narratives
</Card>
<Card title="Full API" icon="code">
REST API for integrating voice synthesis into your apps
</Card>
<Card title="Local-First" icon="shield">
Everything runs on your machine - complete privacy
</Card>
</CardGroup>
## Get Started
<CardGroup cols={2}>
<Card title="Installation" icon="download" href="/docs/overview/installation">
Download and install Voicebox on your machine
</Card>
<Card title="Quick Start" icon="rocket" href="/docs/overview/quick-start">
Get up and running in 5 minutes
</Card>
</CardGroup>
+4
View File
@@ -0,0 +1,4 @@
{
"title": "Voicebox Documentation",
"pages": ["overview", "api-reference", "developer", "plans"]
}
@@ -0,0 +1,297 @@
---
title: "Creating Voice Profiles"
description: "Advanced guide to creating high-quality voice profiles"
---
## Overview
Voice profiles are the foundation of voice cloning in Voicebox. This guide covers best practices for creating professional-quality voice profiles.
## Quick Start
<Steps>
<Step title="Prepare Audio">10-30 seconds of clear speech</Step>
<Step title="Create Profile">**Profiles** → **+ New Profile**</Step>
<Step title="Upload Sample">Add your audio file</Step>
<Step title="Generate">Use the profile to generate speech</Step>
</Steps>
## Audio Requirements
### Ideal Sample Characteristics
<CardGroup cols={2}>
<Card title="Duration" icon="clock">
**10-30 seconds**
Too short: Poor quality
Too long: Unnecessary
</Card>
<Card title="Clarity" icon="volume">
**Clear speech**
No background noise
No music or overlapping voices
</Card>
<Card title="Quality" icon="sparkles">
**High fidelity**
44.1kHz or 48kHz sample rate
Minimal compression
</Card>
<Card title="Content" icon="microphone">
**Natural speech**
Conversational tone
Complete sentences
</Card>
</CardGroup>
### File Formats
Supported formats:
- **WAV** (recommended) - Lossless quality
- **MP3** - Acceptable, minimal compression
- **M4A** - Acceptable
- **FLAC** - Lossless alternative
<Tip>Use WAV for best results. Avoid heavily compressed formats.</Tip>
## Recording Tips
### Environment
<AccordionGroup>
<Accordion title="Quiet Space">
- Record in a quiet room
- Turn off fans, AC, appliances
- Close windows to reduce outside noise
- Use soft furnishings to reduce echo
</Accordion>
{" "}
<Accordion title="Microphone Placement">
- 6-12 inches from mouth - Slight angle to reduce plosives (p, b, t) - Use a
pop filter if available - Maintain consistent distance
</Accordion>
<Accordion title="Recording Settings">
- 44.1kHz or 48kHz sample rate
- 16-bit or 24-bit depth
- Mono is fine (stereo will be converted)
- Avoid automatic gain control
</Accordion>
</AccordionGroup>
### Speaking
- **Natural pace** - Don't rush or speak too slowly
- **Clear articulation** - Pronounce words clearly
- **Consistent volume** - Maintain steady loudness
- **Normal tone** - Speak as you normally would
- **Complete sentences** - Avoid fragments or "ums"
## Multiple Samples
Adding multiple samples can significantly improve quality:
### Why Multiple Samples?
<CardGroup cols={2}>
<Card title="Robustness" icon="shield">
Model learns a more complete representation
</Card>
<Card title="Versatility" icon="palette">
Handles different speaking styles better
</Card>
<Card title="Quality" icon="star">
Reduces artifacts and improves naturalness
</Card>
<Card title="Consistency" icon="check">
More reliable across different texts
</Card>
</CardGroup>
### Sample Variety
Consider adding samples with:
1. **Different tones**
- Casual conversation
- Professional/formal
- Excited/enthusiastic
- Calm/serious
2. **Different content**
- Narratives
- Questions
- Statements
- Emotions (happy, sad, neutral)
3. **Different recording conditions**
- Studio quality
- Phone call quality (if needed)
- Room acoustics
<Warning>
All samples should be from the **same speaker**. Mixing voices will produce
poor results.
</Warning>
## Processing Existing Audio
If you have existing audio (podcasts, videos, etc.):
### Extracting Clean Segments
<Steps>
<Step title="Find Clean Speech">
Look for segments with:
- Just the target speaker
- No background music
- Minimal noise
</Step>
{" "}
<Step title="Use Audio Editor">
Tools like Audacity or Adobe Audition: - Cut out clean 10-30s segments -
Remove silence at start/end - Normalize volume if needed
</Step>
<Step title="Export as WAV">
Save as high-quality WAV file
</Step>
</Steps>
### Noise Reduction
If you have light background noise:
```
1. Use noise reduction in Audacity:
- Select noise-only section
- Get Noise Profile
- Select full audio
- Apply noise reduction (gentle settings)
2. Avoid over-processing:
- Can introduce artifacts
- May reduce voice quality
```
## Testing & Iteration
### Test Your Profile
After creating a profile:
<Steps>
<Step title="Generate Test">
Generate a simple phrase:
```
"Hello, this is a test of my voice profile."
```
</Step>
{" "}
<Step title="Evaluate Quality">
Listen for: - Natural tone - Clear pronunciation - Proper prosody - Lack of
artifacts
</Step>
<Step title="Iterate">
If quality is poor:
- Add more samples
- Try different source audio
- Check sample quality
</Step>
</Steps>
### Common Issues
<AccordionGroup>
<Accordion title="Robotic Voice">
**Cause**: Poor quality samples or too short
**Fix**: Use longer, higher quality samples
</Accordion>
<Accordion title="Wrong Tone">
**Cause**: Sample tone doesn't match desired output
**Fix**: Record samples in the style you want to generate
</Accordion>
<Accordion title="Artifacts/Glitches">
**Cause**: Background noise or audio issues in samples
**Fix**: Clean up samples or re-record in quieter environment
</Accordion>
</AccordionGroup>
## Advanced Tips
### Celebrity/Character Voices
For cloning public figures or characters:
1. **Legal considerations** - Ensure you have rights or it's fair use
2. **Source quality** - Find high-quality interview audio or clean clips
3. **Consistency** - Use clips where they speak similarly
4. **Multiple samples** - Very important for recognizable voices
### Accent & Dialect
The model will preserve accent and dialect:
- British English will generate British English
- Southern accent will produce Southern accent
- Regional pronunciations will be maintained
### Emotion Transfer
The emotional tone of samples affects generation:
- Energetic samples → Energetic output
- Calm samples → Calm output
- Mix samples for versatile profile
## Managing Profiles
### Organization
- **Descriptive names** - "John Smith - Professional Narrator"
- **Add descriptions** - Note recording conditions, use cases
- **Language tags** - Mark the primary language
- **Archive unused** - Keep profile list manageable
### Export/Import
- **Export** profiles to share or backup
- **Import** from colleagues or teammates
- Profiles include voice embeddings, not original audio
## Next Steps
<CardGroup cols={2}>
<Card
title="Generate Speech"
icon="waveform"
href="/overview/generating-speech"
>
Use your profile to generate speech
</Card>
<Card title="Build Stories" icon="film" href="/overview/building-stories">
Create multi-voice narratives
</Card>
</CardGroup>
+17
View File
@@ -0,0 +1,17 @@
{
"title": "Overview",
"pages": [
"introduction",
"installation",
"quick-start",
"voice-cloning",
"stories-editor",
"recording-transcription",
"generation-history",
"remote-mode",
"creating-voice-profiles",
"generating-speech",
"building-stories",
"troubleshooting"
]
}
+166
View File
@@ -0,0 +1,166 @@
---
title: "Quick Start"
description: "Get started with Voicebox in 5 minutes"
---
This guide will walk you through creating your first voice profile and generating speech.
## Prerequisites
Make sure you have [installed Voicebox](/overview/installation) and launched the app.
## Step 1: Create a Voice Profile
Voice profiles are the foundation of Voicebox. Each profile contains voice samples that the AI uses to clone the voice.
<Steps>
<Step title="Navigate to Profiles">
Click the **Profiles** tab in the sidebar
</Step>
<Step title="Create New Profile">
Click the **+ New Profile** button
Fill in the details:
- **Name:** A descriptive name (e.g., "John Smith")
- **Language:** Select the primary language
- **Description:** Optional notes about the voice
</Step>
<Step title="Add Voice Sample">
You have two options:
**Option A: Upload Audio**
- Click **Upload Sample**
- Select an audio file (WAV, MP3, or M4A)
- Ideal length: 10-30 seconds of clear speech
**Option B: Record Live**
- Click **Record Sample**
- Speak clearly for 10-30 seconds
- Click stop when finished
</Step>
<Step title="Save Profile">
Click **Create Profile** to save
</Step>
</Steps>
<Tip>
For best results, use clean audio with minimal background noise and consistent
speaking tone.
</Tip>
## Step 2: Generate Speech
Now let's use your new voice profile to generate speech.
<Steps>
<Step title="Go to Generation">
Click the **Generate** tab in the sidebar
</Step>
{" "}
<Step title="Select Voice Profile">
Choose your newly created profile from the dropdown
</Step>
<Step title="Enter Text">
Type or paste the text you want to generate:
```
Hello! This is my first voice generation with Voicebox.
```
</Step>
<Step title="Generate">
Click **Generate** and wait a few seconds
<Note>
First generation may take longer due to model initialization. Subsequent generations will be faster.
</Note>
</Step>
<Step title="Play & Download">
- Click **Play** to preview the audio
- Click **Download** to save the audio file
- The generation is also saved to your **History**
</Step>
</Steps>
## Step 3: Build a Story (Optional)
The Stories Editor lets you create multi-voice narratives with a timeline-based interface.
<Steps>
<Step title="Create New Story">
Navigate to **Stories** and click **+ New Story**
</Step>
{" "}
<Step title="Add Voice Tracks">
Click **+ Add Track** to create tracks for different speakers
</Step>
{" "}
<Step title="Add Audio Clips">
- Drag generated audio from your History - Or generate new clips directly in
the timeline - Arrange clips on the timeline
</Step>
<Step title="Edit & Export">
- Trim clips by dragging edges
- Adjust timing and spacing
- Click **Export** to render the final audio
</Step>
</Steps>
## What's Next?
<CardGroup cols={2}>
<Card
title="Voice Cloning Guide"
icon="microphone"
href="/overview/creating-voice-profiles"
>
Learn advanced techniques for high-quality voice cloning
</Card>
<Card title="API Integration" icon="code" href="/api-reference">
Integrate Voicebox into your own applications
</Card>
<Card title="Stories Editor" icon="film" href="/overview/stories-editor">
Master the multi-track timeline editor
</Card>
<Card title="Remote Mode" icon="server" href="/overview/remote-mode">
Connect to a GPU server for faster generation
</Card>
</CardGroup>
## Tips for Success
<AccordionGroup>
<Accordion title="Getting the Best Voice Quality">
- Use 10-30 seconds of clear, consistent speech
- Avoid background noise and echo
- Multiple samples from the same speaker improve quality
- Match the speaking style you want to generate
</Accordion>
{" "}
<Accordion title="Improving Generation Speed">
- Use a CUDA-capable GPU for 5-10x faster generation - Enable voice prompt
caching for repeated generations - Consider running the backend on a remote
GPU server
</Accordion>
<Accordion title="Troubleshooting Common Issues">
- **Server won't start:** Check if port 17493 is available
- **Poor audio quality:** Try adding more voice samples
- **Slow generation:** Verify GPU acceleration is enabled
- See the full [Troubleshooting Guide](/overview/troubleshooting) for more
</Accordion>
</AccordionGroup>
@@ -0,0 +1,786 @@
---
title: "Docker Deployment Guide"
description: "Docker deployment guide for Voicebox (In Development)"
---
**Status:** In Development for v0.2.0
**Requested By:** Reddit community ([thread](https://reddit.com/r/LocalLLaMA/...))
## Overview
Docker support makes Voicebox easier to deploy, especially for:
- **Consistent Environments**: Same setup across dev/staging/prod
- **GPU Passthrough**: Easy NVIDIA/AMD GPU access
- **Server Deployments**: Run on headless Linux servers
- **Multi-User Setups**: Isolate instances per user/team
- **Cloud Platforms**: Deploy to AWS, GCP, Azure, DigitalOcean
## Quick Start
### Using Pre-Built Images (Recommended)
```bash
# CPU-only version
docker run -p 8000:8000 -v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest
# NVIDIA GPU version
docker run --gpus all -p 8000:8000 -v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest-cuda
# AMD GPU version (experimental)
docker run --device=/dev/kfd --device=/dev/dri -p 8000:8000 \
-v voicebox-data:/app/data \
ghcr.io/jamiepine/voicebox:latest-rocm
```
Then open: `http://localhost:8000`
### Using Docker Compose (Easiest)
Create `docker-compose.yml`:
```yaml
version: "3.8"
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest-cuda
ports:
- "8000:8000"
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- GPU_MEMORY_FRACTION=0.8 # Use 80% of GPU memory
- TTS_MODE=local
- WHISPER_MODE=local
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
voicebox-data:
huggingface-cache:
```
Run:
```bash
docker compose up -d
```
## Building From Source
### Basic Dockerfile
```dockerfile
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
build-essential \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# Copy application
COPY backend/ /app/backend/
COPY requirements.txt /app/
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir git+https://github.com/QwenLM/Qwen3-TTS.git
# Create data directory
RUN mkdir -p /app/data
# Expose port
EXPOSE 8000
# Run server
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
Build and run:
```bash
docker build -t voicebox .
docker run -p 8000:8000 -v $(pwd)/data:/app/data voicebox
```
### Multi-Stage Build (Optimized)
Smaller image size by separating build and runtime:
```dockerfile
# Dockerfile.optimized
# Stage 1: Build dependencies
FROM python:3.11-slim AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y \
git build-essential && \
rm -rf /var/lib/apt/lists/*
COPY backend/requirements.txt .
RUN pip install --no-cache-dir --target=/build/packages \
-r requirements.txt
RUN pip install --no-cache-dir --target=/build/packages \
git+https://github.com/QwenLM/Qwen3-TTS.git
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
# Install only runtime dependencies
RUN apt-get update && apt-get install -y \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# Copy installed packages from builder
COPY --from=builder /build/packages /usr/local/lib/python3.11/site-packages/
# Copy application code
COPY backend/ /app/backend/
# Create data directory
RUN mkdir -p /app/data
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
Build:
```bash
docker build -f Dockerfile.optimized -t voicebox:slim .
```
## GPU Support
### NVIDIA GPUs (CUDA)
**Dockerfile:**
```dockerfile
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
# Install Python
RUN apt-get update && apt-get install -y \
python3.11 python3-pip git ffmpeg && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install PyTorch with CUDA support
COPY backend/requirements.txt .
RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
# Install other dependencies
RUN pip3 install -r requirements.txt
RUN pip3 install git+https://github.com/QwenLM/Qwen3-TTS.git
COPY backend/ /app/backend/
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
**Run with GPU:**
```bash
docker run --gpus all -p 8000:8000 \
-v voicebox-data:/app/data \
voicebox:cuda
```
**Docker Compose with GPU:**
```yaml
services:
voicebox:
image: voicebox:cuda
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
```
### AMD GPUs (ROCm) - Experimental
**Dockerfile:**
```dockerfile
FROM rocm/dev-ubuntu-22.04:6.0
# Install Python
RUN apt-get update && apt-get install -y \
python3.11 python3-pip git ffmpeg && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install PyTorch with ROCm support
COPY backend/requirements.txt .
RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.0
# Install other dependencies
RUN pip3 install -r requirements.txt
RUN pip3 install git+https://github.com/QwenLM/Qwen3-TTS.git
# Set ROCm environment variables
ENV HSA_OVERRIDE_GFX_VERSION=10.3.0
ENV ROCM_PATH=/opt/rocm
COPY backend/ /app/backend/
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
**Run with AMD GPU:**
```bash
docker run --device=/dev/kfd --device=/dev/dri \
--group-add video --ipc=host --cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
-p 8000:8000 -v voicebox-data:/app/data \
voicebox:rocm
```
**Note:** ROCm support varies by GPU model. Works best on Linux. See [AMD ROCm docs](https://rocm.docs.amd.com) for compatibility.
## Volume Mounts
### Essential Volumes
```bash
docker run -v voicebox-data:/app/data \ # Profiles, generations, history
-v huggingface-cache:/root/.cache/huggingface \ # Downloaded models
-p 8000:8000 voicebox
```
### Development Volume Mounts
For development with hot-reload:
```bash
docker run -v $(pwd)/backend:/app/backend \ # Live code changes
-v voicebox-data:/app/data \
-e RELOAD=true \
-p 8000:8000 voicebox
```
### Custom Model Storage
Use external model directory:
```bash
docker run -v /path/to/models:/models \
-e MODELS_DIR=/models \
-v voicebox-data:/app/data \
-p 8000:8000 voicebox
```
## Environment Variables
Configure Voicebox via environment variables:
```bash
docker run -e TTS_MODE=local \
-e WHISPER_MODE=openai-api \
-e OPENAI_API_KEY=sk-... \
-e GPU_MEMORY_FRACTION=0.8 \
-e LOG_LEVEL=info \
-p 8000:8000 voicebox
```
### Available Variables
| Variable | Default | Description |
| --------------------- | ------------- | -------------------------------------------------- |
| `TTS_MODE` | `local` | TTS provider: `local`, `remote` |
| `TTS_REMOTE_URL` | - | URL for remote TTS server |
| `WHISPER_MODE` | `local` | Whisper provider: `local`, `openai-api`, `remote` |
| `WHISPER_REMOTE_URL` | - | URL for remote Whisper server |
| `OPENAI_API_KEY` | - | OpenAI API key (if using OpenAI Whisper) |
| `GPU_MEMORY_FRACTION` | `0.9` | Fraction of GPU memory to use (0.0-1.0) |
| `DATA_DIR` | `/app/data` | Directory for profiles/generations |
| `MODELS_DIR` | `/app/models` | Directory for local models |
| `LOG_LEVEL` | `info` | Logging level: `debug`, `info`, `warning`, `error` |
| `RELOAD` | `false` | Enable hot-reload for development |
## Complete Docker Compose Examples
### Production Deployment
```yaml
# docker-compose.prod.yml
version: "3.8"
services:
voicebox:
image: ghcr.io/jamiepine/voicebox:latest-cuda
container_name: voicebox
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- TTS_MODE=local
- WHISPER_MODE=local
- GPU_MEMORY_FRACTION=0.8
- LOG_LEVEL=info
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
voicebox-data:
driver: local
huggingface-cache:
driver: local
```
Run:
```bash
docker compose -f docker-compose.prod.yml up -d
```
### Development Setup
```yaml
# docker-compose.dev.yml
version: "3.8"
services:
voicebox:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./backend:/app/backend:ro
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- RELOAD=true
- LOG_LEVEL=debug
- TTS_MODE=local
command: uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
volumes:
voicebox-data:
huggingface-cache:
```
### Multi-Service Stack
Full stack with reverse proxy and monitoring:
```yaml
# docker-compose.stack.yml
version: "3.8"
services:
# Main Voicebox app
voicebox:
image: ghcr.io/jamiepine/voicebox:latest-cuda
restart: unless-stopped
volumes:
- voicebox-data:/app/data
- huggingface-cache:/root/.cache/huggingface
environment:
- TTS_MODE=local
- WHISPER_MODE=local
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
# Nginx reverse proxy
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- voicebox
# Prometheus monitoring (optional)
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
volumes:
voicebox-data:
huggingface-cache:
prometheus-data:
```
## Cloud Deployment
### AWS EC2
1. **Launch GPU Instance** (g4dn.xlarge or p3.2xlarge)
2. **Install Docker + nvidia-docker:**
```bash
# Amazon Linux 2
sudo yum install -y docker
sudo systemctl start docker
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update && sudo apt-get install -y nvidia-docker2
sudo systemctl restart docker
```
3. **Deploy:**
```bash
docker run --gpus all -d -p 80:8000 \
-v voicebox-data:/app/data \
--restart unless-stopped \
ghcr.io/jamiepine/voicebox:latest-cuda
```
### DigitalOcean
Use GPU Droplet + Docker:
```bash
# Create droplet via CLI
doctl compute droplet create voicebox \
--size gpu-h100x1-80gb \
--image ubuntu-22-04-x64 \
--region nyc3
# SSH and deploy
ssh root@<droplet-ip>
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
docker run --gpus all -d -p 80:8000 voicebox:cuda
```
### Google Cloud Run (CPU-only)
```bash
# Build and push
docker build -t gcr.io/your-project/voicebox .
docker push gcr.io/your-project/voicebox
# Deploy to Cloud Run
gcloud run deploy voicebox \
--image gcr.io/your-project/voicebox \
--platform managed \
--region us-central1 \
--memory 4Gi \
--cpu 2 \
--port 8000
```
### Fly.io
Create `fly.toml`:
```toml
app = "voicebox"
[build]
image = "ghcr.io/jamiepine/voicebox:latest"
[[services]]
http_checks = []
internal_port = 8000
protocol = "tcp"
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[mounts]
source = "voicebox_data"
destination = "/app/data"
```
Deploy:
```bash
fly launch
fly deploy
```
## Troubleshooting
### GPU Not Detected
**Check NVIDIA Docker:**
```bash
docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi
```
If this fails, reinstall nvidia-docker2.
**Check AMD ROCm:**
```bash
docker run --rm --device=/dev/kfd --device=/dev/dri rocm/dev-ubuntu-22.04:6.0 rocminfo
```
### Permission Errors
Container can't write to volumes:
```bash
# Fix permissions
docker run --user $(id -u):$(id -g) -v $(pwd)/data:/app/data voicebox
```
### Out of Memory
Reduce GPU memory usage:
```bash
docker run -e GPU_MEMORY_FRACTION=0.5 voicebox
```
Or use CPU-only:
```bash
docker run -e DEVICE=cpu voicebox
```
### Model Download Fails
Ensure HuggingFace cache is writable:
```bash
docker run -v huggingface-cache:/root/.cache/huggingface voicebox
```
Or use host cache:
```bash
docker run -v ~/.cache/huggingface:/root/.cache/huggingface voicebox
```
### Port Already in Use
Change host port:
```bash
docker run -p 8080:8000 voicebox # Use port 8080 instead
```
## Security Best Practices
### 1. Don't Run as Root
Create non-root user in Dockerfile:
```dockerfile
RUN useradd -m -u 1000 voicebox
USER voicebox
```
### 2. Use Secrets for API Keys
Don't put API keys in docker-compose.yml:
```bash
# Use Docker secrets
echo "sk-your-key" | docker secret create openai_key -
docker service create \
--secret openai_key \
-e OPENAI_API_KEY_FILE=/run/secrets/openai_key \
voicebox
```
### 3. Network Isolation
Use internal networks for multi-container setups:
```yaml
services:
voicebox:
networks:
- internal
nginx:
networks:
- internal
- external
ports:
- "80:80"
networks:
internal:
internal: true
external:
```
### 4. Resource Limits
Prevent resource exhaustion:
```yaml
services:
voicebox:
deploy:
resources:
limits:
cpus: "4"
memory: 8G
reservations:
cpus: "2"
memory: 4G
```
## Performance Tuning
### GPU Memory Management
```bash
# Use 80% of GPU (default 90%)
docker run -e GPU_MEMORY_FRACTION=0.8 voicebox
# Allow GPU memory growth (prevents OOM)
docker run -e TF_FORCE_GPU_ALLOW_GROWTH=true voicebox
```
### Model Caching
Pre-download models to volume:
```bash
# Download models first
docker run --rm -v huggingface-cache:/root/.cache/huggingface \
voicebox python -c "
from transformers import WhisperProcessor, WhisperForConditionalGeneration
WhisperProcessor.from_pretrained('openai/whisper-base')
WhisperForConditionalGeneration.from_pretrained('openai/whisper-base')
"
# Then run normally
docker run -v huggingface-cache:/root/.cache/huggingface voicebox
```
### Multi-Worker Setup
Use uvicorn workers for better throughput:
```dockerfile
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
```
## Monitoring
### Health Checks
Built-in health endpoint:
```bash
curl http://localhost:8000/health
```
Docker health check:
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
```
### Prometheus Metrics
Add metrics exporter:
```python
# backend/main.py
from prometheus_fastapi_instrumentator import Instrumentator
Instrumentator().instrument(app).expose(app)
```
Then scrape `/metrics` with Prometheus.
### Logs
View container logs:
```bash
docker logs -f voicebox
# Or with compose
docker compose logs -f voicebox
```
## Next Steps
- [ ] Publish official images to GitHub Container Registry
- [ ] Add Kubernetes Helm charts
- [ ] Create Docker Desktop extension
- [ ] Add automated vulnerability scanning
- [ ] Support ARM64 builds for Raspberry Pi / Apple Silicon
## Contributing
Help improve Docker support:
1. Test on different platforms (AMD GPU, ARM64, etc.)
2. Submit Dockerfile optimizations
3. Share deployment configurations
4. Report issues: [GitHub Issues](https://github.com/jamiepine/voicebox/issues)
## Resources
- [Docker Documentation](https://docs.docker.com)
- [NVIDIA Container Toolkit](https://github.com/NVIDIA/nvidia-docker)
- [AMD ROCm Docker](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html)
- [Docker Compose Reference](https://docs.docker.com/compose/compose-file/)
@@ -0,0 +1,461 @@
---
title: "External Provider Support"
description: "External provider support for Voicebox (Planned)"
---
**Status:** Planned for v0.2.0
**Discussion:** [Reddit Thread](https://reddit.com/r/LocalLLaMA/...)
## Overview
External provider support allows you to connect Voicebox to remotely-hosted TTS and Whisper services instead of running models locally. This is useful for:
- **Existing GPU Infrastructure**: You already have Qwen3-TTS running on a GPU server
- **AMD GPU Users**: Run models on your AMD hardware, use Voicebox as the UI
- **Cloud Deployments**: Host models on Modal, Replicate, RunPod, etc.
- **Team Sharing**: Multiple users share one GPU server running models
- **Mixed Deployments**: Local Whisper + remote TTS, or vice versa
## Architecture
```
┌─────────────────┐ HTTP/API ┌──────────────────┐
│ Voicebox UI │ ───────────────────────> │ Your TTS Server │
│ + Backend │ │ (Qwen3-TTS on │
│ │ <─────────────────────── │ AMD/NVIDIA GPU)│
│ - Profiles │ Audio + Metadata └──────────────────┘
│ - History │
│ - Audio Edit │ HTTP/API ┌──────────────────┐
│ - UI │ ───────────────────────> │ Whisper Service │
└─────────────────┘ │ (OpenAI API or │
│ self-hosted) │
└──────────────────┘
```
**What Voicebox Still Handles:**
- Voice profile management
- Generation history
- Audio trimming/editing
- Multi-track story editor
- UI/UX layer
**What External Providers Handle:**
- Model inference (TTS generation, transcription)
- GPU allocation
- Model loading/caching
## Configuration
### Environment Variables
```bash
# TTS Provider
TTS_MODE=remote # local | remote
TTS_REMOTE_URL=http://192.168.1.100:8000 # Your TTS server URL
TTS_API_KEY=your-api-key # Optional authentication
# Whisper Provider
WHISPER_MODE=openai-api # local | openai-api | remote
WHISPER_REMOTE_URL=http://localhost:9000 # For self-hosted Whisper
OPENAI_API_KEY=sk-... # For OpenAI Whisper API
```
### Voicebox Config UI (Planned)
Settings page will include:
- Provider selection dropdowns
- URL/API key inputs
- Connection test button
- Latency/status indicators
## Hosting External Services
### Option 1: Simple FastAPI Server (Recommended)
Create a lightweight server to expose your local Qwen3-TTS model:
```python
# tts_server.py
from fastapi import FastAPI, UploadFile, File
from qwen_tts import Qwen3TTSModel
import numpy as np
import base64
app = FastAPI()
model = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-Base",
device_map="cuda" # or "cpu" for AMD ROCm: use torch+rocm
)
@app.post("/v1/generate")
async def generate(
text: str,
voice_prompt: dict,
language: str = "en",
seed: int = None
):
"""Generate speech from text using voice prompt."""
audio, sample_rate = model.generate_voice_clone(
text=text,
voice_clone_prompt=voice_prompt,
)
# Return as base64 for transport
audio_bytes = audio.tobytes()
return {
"audio": base64.b64encode(audio_bytes).decode(),
"sample_rate": sample_rate,
"dtype": str(audio.dtype)
}
@app.post("/v1/create_voice_prompt")
async def create_voice_prompt(
audio: UploadFile = File(...),
reference_text: str = ""
):
"""Create voice prompt from reference audio."""
# Save uploaded audio temporarily
audio_path = f"/tmp/{audio.filename}"
with open(audio_path, "wb") as f:
f.write(await audio.read())
# Create voice prompt
voice_prompt = model.create_voice_clone_prompt(
ref_audio=audio_path,
ref_text=reference_text,
)
return {"voice_prompt": voice_prompt}
@app.get("/health")
async def health():
return {
"status": "healthy",
"model": "Qwen3-TTS-12Hz-1.7B-Base",
"device": str(model.device)
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
```
**Run it:**
```bash
# Install dependencies
pip install fastapi uvicorn qwen-tts torch
# For AMD GPUs, use ROCm PyTorch:
pip install torch --index-url https://download.pytorch.org/whl/rocm6.4
# Start server
python tts_server.py
```
### Option 2: vLLM (If Supported)
```bash
vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-Base \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.9
```
### Option 3: Cloud Platforms
**Modal.com Example:**
```python
import modal
app = modal.App("qwen-tts")
image = modal.Image.debian_slim().pip_install("qwen-tts", "torch")
@app.function(gpu="A10G", image=image)
@modal.web_endpoint(method="POST")
def generate(text: str, voice_prompt: dict):
from qwen_tts import Qwen3TTSModel
model = Qwen3TTSModel.from_pretrained("Qwen/Qwen3-TTS-12Hz-1.7B-Base")
audio, sr = model.generate_voice_clone(text, voice_prompt)
return {"audio": audio.tolist(), "sample_rate": sr}
```
Deploy: `modal deploy tts_server.py`
Get URL: `https://yourapp--generate.modal.run`
## API Specification
External TTS providers must implement these endpoints:
### `POST /v1/generate`
Generate speech from text.
**Request:**
```json
{
"text": "Hello, this is a test.",
"voice_prompt": {
/* voice prompt object */
},
"language": "en",
"seed": 12345
}
```
**Response:**
```json
{
"audio": "base64-encoded-audio-bytes",
"sample_rate": 24000,
"dtype": "float32"
}
```
### `POST /v1/create_voice_prompt`
Create a voice prompt from reference audio.
**Request:** (multipart/form-data)
- `audio`: Audio file upload
- `reference_text`: Transcript of the audio
**Response:**
```json
{
"voice_prompt": {
/* voice prompt object */
}
}
```
### `GET /health`
Health check endpoint.
**Response:**
```json
{
"status": "healthy",
"model": "Qwen3-TTS-12Hz-1.7B-Base",
"device": "cuda:0"
}
```
## Whisper External Providers
### OpenAI Whisper API
Simply set:
```bash
WHISPER_MODE=openai-api
OPENAI_API_KEY=sk-...
```
Voicebox will use OpenAI's Whisper API automatically.
### Self-Hosted Whisper
Run your own Whisper server:
```python
# whisper_server.py
from fastapi import FastAPI, UploadFile, File
from transformers import WhisperProcessor, WhisperForConditionalGeneration
import librosa
app = FastAPI()
processor = WhisperProcessor.from_pretrained("openai/whisper-base")
model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-base")
@app.post("/v1/transcribe")
async def transcribe(audio: UploadFile = File(...), language: str = None):
# Load audio
audio_path = f"/tmp/{audio.filename}"
with open(audio_path, "wb") as f:
f.write(await audio.read())
audio_data, sr = librosa.load(audio_path, sr=16000)
# Process
inputs = processor(audio_data, sampling_rate=16000, return_tensors="pt")
predicted_ids = model.generate(inputs["input_features"])
transcription = processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
return {"text": transcription}
```
Configure Voicebox:
```bash
WHISPER_MODE=remote
WHISPER_REMOTE_URL=http://localhost:9000
```
## Use Cases
### 1. AMD GPU User with Existing Setup
**Scenario:** You have a Radeon 7900 XTX running Qwen3-TTS on Linux.
**Setup:**
1. Run `tts_server.py` on your AMD box (ROCm PyTorch)
2. Configure Voicebox: `TTS_MODE=remote`, `TTS_REMOTE_URL=http://amd-box:8000`
3. Use Voicebox UI for profiles, generation, editing
4. TTS happens on your AMD GPU
### 2. Team Deployment
**Scenario:** 5 team members, 1 GPU server.
**Setup:**
1. Deploy TTS server on shared GPU box
2. Each person runs Voicebox desktop app locally
3. All point to same `TTS_REMOTE_URL`
4. Profiles and history stay local per user
5. GPU usage is shared
### 3. Hybrid Local/Remote
**Scenario:** Fast local Whisper, heavy TTS on cloud.
**Setup:**
```bash
TTS_MODE=remote
TTS_REMOTE_URL=https://your-modal-app.modal.run
WHISPER_MODE=local # Fast transcription on your CPU
```
### 4. OpenAI Whisper + Self-Hosted TTS
**Scenario:** Use OpenAI's API for transcription, run TTS locally.
**Setup:**
```bash
TTS_MODE=local
WHISPER_MODE=openai-api
OPENAI_API_KEY=sk-...
```
## Security Considerations
### Authentication
Add API key authentication to your external server:
```python
from fastapi import Header, HTTPException
API_KEY = "your-secret-key"
async def verify_api_key(x_api_key: str = Header(...)):
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
@app.post("/v1/generate", dependencies=[Depends(verify_api_key)])
async def generate(...):
...
```
Configure Voicebox:
```bash
TTS_API_KEY=your-secret-key
```
### Network Security
- **VPN/Tailscale**: Use private network for remote servers
- **HTTPS**: Use reverse proxy (nginx/Caddy) with SSL certificates
- **Firewall**: Restrict access to known IPs
### Rate Limiting
Protect your external server:
```python
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.post("/v1/generate")
@limiter.limit("10/minute")
async def generate(...):
...
```
## Performance Considerations
### Latency
External providers add network latency:
- **Local network**: ~10-50ms overhead (negligible)
- **Same datacenter**: ~1-5ms overhead
- **Cross-region cloud**: 50-200ms+ overhead
For real-time applications, keep TTS server on local network or same cloud region.
### Caching
Implement response caching on external server:
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def get_cached_generation(text, voice_prompt_hash, language, seed):
return model.generate_voice_clone(text, voice_prompt)
```
### Load Balancing
For high-traffic deployments, run multiple TTS servers behind a load balancer:
```
Voicebox ──> Load Balancer ──> TTS Server 1 (GPU 1)
├──> TTS Server 2 (GPU 2)
└──> TTS Server 3 (GPU 3)
```
## Future Enhancements
- [ ] **Provider Marketplace**: Built-in directory of compatible providers
- [ ] **Automatic Fallback**: If remote fails, fallback to local
- [ ] **Cost Tracking**: Monitor API usage and costs
- [ ] **Performance Metrics**: Latency, throughput dashboards
- [ ] **Multi-Provider**: Use different providers for different voices/languages
## Contributing
If you build an external provider, please share:
1. Server implementation
2. Performance benchmarks
3. Deployment guide
Submit to: [GitHub Discussions](https://github.com/jamiepine/voicebox/discussions)
## Questions?
- **Discord**: [Join the community](https://discord.gg/...)
- **GitHub**: [Open an issue](https://github.com/jamiepine/voicebox/issues)
- **Docs**: [Full documentation](https://voicebox.sh/docs)
+431
View File
@@ -0,0 +1,431 @@
---
title: "MLX Audio Integration"
description: "MLX Audio integration for Voicebox (Validated)"
---
**Status:** Validated ✅
**Context:** [mlx-audio v0.3.1 release](https://github.com/Blaizzy/mlx-audio)
## Validation Results
We validated mlx-audio in an isolated environment (`mlx-test/`). Key findings:
| Metric | Result |
| --------------- | ------------------------------------------- |
| MLX Version | 0.30.4 |
| Model Load Time | ~1s (after initial download) |
| Generation RTF | **0.5-0.6x** (1.7-2x faster than real-time) |
| Test Hardware | Apple Silicon Mac |
### Model Mapping
| voicebox (PyTorch) | mlx-audio (MLX) |
| ------------------------------- | --------------------------------------------- |
| `Qwen/Qwen3-TTS-12Hz-1.7B-Base` | `mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16` |
| `Qwen/Qwen3-TTS-12Hz-0.6B-Base` | (not yet converted) |
### mlx-audio API
The API uses a **generator-based streaming pattern**:
```python
from mlx_audio.tts import load
model = load("mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16")
# generate() yields GenerationResult objects
for result in model.generate("Hello world"):
audio = result.audio # numpy array of samples
sample_rate = result.sample_rate # 24000
rtf = result.real_time_factor # e.g., 0.55
```
### Known Warnings (harmless)
```
You are using a model of type qwen3_tts to instantiate a model of type .
The tokenizer you are loading... with an incorrect regex pattern...
```
These warnings appear but don't affect functionality or output quality.
### Demo Script
Run `mlx-test/demo.py` to test:
```bash
cd mlx-test && source venv/bin/activate && python demo.py "Your text here"
```
## Problem
Apple Silicon users are stuck on CPU inference while Windows and Linux users get CUDA acceleration. The current PyTorch MPS backend has stability issues (lines 34-36 in `backend/tts.py` and `backend/transcribe.py`), forcing a CPU fallback that makes voicebox significantly slower on M1/M2/M3 Macs.
This creates a poor experience for a large portion of users who bought Apple Silicon specifically for ML workloads.
## Solution
Integrate [mlx-audio](https://github.com/Blaizzy/mlx-audio) as the inference engine for macOS Apple Silicon builds. MLX is Apple's native ML framework, optimized for Metal and the unified memory architecture. It's fast, stable, and already supports the same Qwen3-TTS models we use.
**Key wins:**
- Native GPU acceleration on Apple Silicon (no more CPU fallback)
- Streaming TTS support (faster perceived latency)
- Memory optimizations (run larger models on less RAM)
- Fixed 0.6B silence bug that we currently ship
- Same Qwen3-TTS models (zero migration cost for users)
## Architecture
### Current Stack
```
┌─────────────────────────┐
│ PyTorch + Qwen3-TTS │
│ (CPU only on macOS) │
└─────────────────────────┘
```
### Proposed Stack
```
┌─────────────────────────────────────────┐
│ Platform Detection at Runtime │
└─────────────────────────────────────────┘
├─── Apple Silicon (aarch64-darwin)
│ ┌─────────────────────────┐
│ │ MLX Audio Backend │
│ │ - Qwen3-TTS (mlx) │
│ │ - Whisper (mlx) │
│ │ - Streaming support │
│ └─────────────────────────┘
└─── Other (x86_64, Windows, Linux)
┌─────────────────────────┐
│ PyTorch Backend │
│ - Qwen3-TTS (pytorch) │
│ - Whisper (pytorch) │
│ - CUDA if available │
└─────────────────────────┘
```
## Implementation Phases
### Phase 1: Platform Detection & Dependency Management
Create a backend that switches between PyTorch and MLX based on runtime platform detection.
**New files:**
- `backend/platform.py` - Detect Apple Silicon, return backend type
- `backend/backends/__init__.py` - Backend factory pattern
- `backend/requirements-mlx.txt` - MLX-specific deps (macOS only)
**Modified files:**
- `backend/requirements.txt` - Keep PyTorch as default
- `backend/main.py` - Import from backend factory instead of direct imports
**Platform detection logic:**
```python
def get_backend_type() -> str:
"""Detect best backend for current platform."""
if platform.system() == "Darwin" and platform.machine() == "arm64":
# Apple Silicon detected
try:
import mlx
return "mlx"
except ImportError:
return "pytorch" # Fallback if mlx not installed
return "pytorch"
```
### Phase 2: MLX Backend Implementation
Create parallel implementations of TTS and STT using mlx-audio.
**New files:**
- `backend/backends/mlx_backend.py` - MLX inference engine
- `backend/backends/pytorch_backend.py` - Refactor current code into backend
**Interface both backends must implement:**
```python
class TTSBackend(Protocol):
async def load_model(self, model_size: str) -> None: ...
async def create_voice_prompt(self, audio_path: str, reference_text: str) -> dict: ...
async def generate(self, text: str, voice_prompt: dict, **kwargs) -> Tuple[np.ndarray, int]: ...
async def generate_streaming(self, text: str, voice_prompt: dict, **kwargs) -> AsyncIterator[bytes]: ...
def unload_model(self) -> None: ...
class STTBackend(Protocol):
async def load_model(self, model_size: str) -> None: ...
async def transcribe(self, audio_path: str, language: Optional[str]) -> str: ...
def unload_model(self) -> None: ...
```
**MLX backend implementation notes:**
mlx-audio's `generate()` returns a generator by default (streaming is built-in):
```python
# MLX backend wrapper
from mlx_audio.tts import load
class MLXTTSBackend:
def __init__(self):
self.model = None
async def load_model(self, model_size: str) -> None:
model_map = {
"1.7B": "mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16",
# "0.6B": needs conversion to mlx format
}
self.model = load(model_map[model_size])
async def generate(self, text: str, voice_prompt: dict, **kwargs) -> Tuple[np.ndarray, int]:
# Collect all chunks from generator
chunks = []
for result in self.model.generate(text): # TODO: add voice_prompt support
chunks.append(np.array(result.audio))
return np.concatenate(chunks), 24000
```
**MLX-specific features to expose:**
- Streaming TTS (new endpoint: `/api/generate/stream`)
- Memory-optimized model loading
- Qwen3-ASR for transcription (in addition to Whisper)
### Phase 3: API Layer Updates
Update FastAPI endpoints to support new streaming capabilities and maintain backward compatibility.
**Modified files:**
- `backend/main.py` - Add streaming endpoints
- `backend/tts.py` - Refactor to use backend abstraction
- `backend/transcribe.py` - Refactor to use backend abstraction
**New endpoints:**
```python
@app.post("/api/generate/stream")
async def generate_stream(...) -> StreamingResponse:
"""Stream TTS chunks as they're generated (MLX only)."""
backend = get_backend()
if not hasattr(backend, 'generate_streaming'):
raise HTTPException(501, "Streaming not supported on this backend")
return StreamingResponse(backend.generate_streaming(...), media_type="audio/wav")
```
**Backward compatibility:**
- Keep all existing `/api/generate` endpoints unchanged
- PyTorch backend users see no behavior change
- MLX users automatically get faster inference, streaming is opt-in
### Phase 4: Frontend Integration
Add UI indicators for backend type and streaming progress.
**Modified files:**
- `app/src/hooks/useGenerationForm.tsx` - Add streaming support
- `app/src/components/GenerationForm.tsx` - Show backend badge, streaming toggle
- `app/src/lib/api.ts` - Add streaming API client
**UI additions:**
- Badge showing current backend ("MLX" or "PyTorch")
- Toggle for streaming mode (disabled if PyTorch)
- Real-time streaming playback (WaveSurfer progressive loading)
### Phase 5: Build & Distribution
Create separate installers for MLX (Apple Silicon) and PyTorch (Universal).
**Modified files:**
- `tauri/src-tauri/tauri.conf.json` - Add target-specific builds
- `.github/workflows/release.yml` - Build both variants
**Build matrix:**
```yaml
- target: aarch64-apple-darwin
backend: mlx
installer: voicebox-macos-silicon-{version}.dmg
- target: x86_64-apple-darwin
backend: pytorch
installer: voicebox-macos-intel-{version}.dmg
- target: x86_64-pc-windows-msvc
backend: pytorch
installer: voicebox-windows-{version}.exe
```
**Installation flow:**
- Auto-detect architecture, recommend correct installer
- MLX installer includes `mlx-audio` in embedded Python
- PyTorch installer includes `torch` in embedded Python
- Both can coexist (different backend, same profile format)
### Phase 6: Testing & Validation
Ensure both backends produce compatible outputs.
**New files:**
- `backend/tests/test_backend_parity.py` - Verify both backends produce similar audio
- `backend/tests/test_streaming.py` - Streaming-specific tests
**Test scenarios:**
- Same voice prompt on both backends → similar (not identical) audio output
- Profile created on MLX → loads on PyTorch (and vice versa)
- Streaming chunks assemble into valid WAV file
- Model downloads work on both backends
- Memory usage stays within bounds
### Phase 7: Documentation
Update user-facing docs and developer guides.
**New files:**
- `docs/developer/BACKENDS.md` - Guide for adding new backends
- `docs/overview/performance.md` - Backend comparison benchmarks
**Modified files:**
- `README.md` - Note Apple Silicon acceleration
- `docs/TROUBLESHOOTING.md` - Add MLX-specific issues
**Key docs to write:**
- Which installer to download (architecture detection)
- Performance comparison (MLX vs PyTorch on same M2 hardware)
- How streaming mode works
- How to force PyTorch on Apple Silicon (for debugging)
## Technical Decisions
### Why Dual Backend Instead of MLX-Only?
**Pros of dual backend:**
- Windows and Intel Mac users unaffected
- Easier testing (can compare outputs)
- Fallback if MLX has issues
**Cons of dual backend:**
- More code to maintain
- Two dependency trees
- Build complexity (separate installers)
**Decision:** Dual backend. The maintenance cost is worth it to avoid breaking existing users and to have a fallback.
### Why Separate Installers Instead of Runtime Detection?
**Pros of separate installers:**
- Smaller bundle size (don't ship both PyTorch and MLX)
- Clearer to users which version they have
- Easier to debug (no "which backend am I running?" confusion)
- Can optimize each build for its target
**Cons:**
- More installers to build and test
- Users might download the wrong one
**Decision:** Separate installers. Bundle size matters (PyTorch + MLX would be huge), and we can auto-detect architecture on the download page.
### Streaming vs Batch Generation
MLX supports streaming, PyTorch doesn't (without significant work). Should streaming be:
1. MLX-only feature (✅ chosen)
2. Implemented for both (lots of work)
3. Not exposed at all (wasted opportunity)
**Decision:** MLX-only. Expose as opt-in feature with graceful degradation (button disabled on PyTorch backend).
## Migration Path
Nothing needs migrating, macos users will just notice a speed-boost in inference
**Data format compatibility:**
- Profiles (SQLite) → no schema changes needed
- Voice prompts (cached) → backend-agnostic (just numpy arrays)
- Audio files → unchanged
## Performance Expectations
### Measured Results (from validation)
| Metric | MLX (measured) | PyTorch CPU (estimated) |
| ----------------------- | -------------- | ----------------------- |
| **6s audio generation** | ~3-4s | ~10-15s |
| **Real-time factor** | 0.5-0.6x | 2-3x |
| **Model load (cached)** | ~1s | ~3-5s |
### TTS Generation (1.7B model, ~20s output)
- **PyTorch CPU (M2 Max):** ~45-60s (slower than real-time)
- **MLX (M2 Max):** ~8-12s (faster than real-time)
- **Improvement:** ~4-5x faster
### Whisper Transcription (10s audio clip)
- **PyTorch CPU:** ~5-8s
- **MLX:** ~1-2s
- **Improvement:** ~3-4x faster
### Memory Usage (1.7B model)
- **PyTorch:** ~8-10GB (no GPU offload, so CPU RAM)
- **MLX:** ~4-6GB (unified memory, better optimization)
- **Improvement:** ~40% less RAM
Full benchmarks will be in `docs/overview/performance.md` after Phase 6.
## Open Questions
- **Should we support Qwen3-ASR (MLX-only) in addition to Whisper?** Adds another model option but increases complexity. Probably phase 8+. - Sure
- **Should we backport streaming to PyTorch?** Would require chunking and callback-based generation. Probably not worth it given mlx-audio already has it. - No
- **What's the auto-update UX for migrating PyTorch→MLX users?** Needs design. Don't want to force reinstall, but also want to make upgrade obvious. - it just updates, users see nothing
- **Do we expose backend selection in settings or hide it?** Leaning toward auto-detect only, with env var override for power users.
## Success Metrics
How we'll know this worked:
1. **Performance:** Apple Silicon users report generation faster than real-time
2. **Adoption:** >80% of macOS downloads are MLX build within 1 month
3. **Stability:** <5% increase in bug reports (backend abstraction doesn't introduce regressions)
4. **Feedback:** Positive sentiment in Discord/GitHub about macOS performance
## Related Work
- [PyTorch MPS tracking issue](https://github.com/pytorch/pytorch/issues/77764) - Why we can't use MPS directly
- [mlx-audio server implementation](https://github.com/Blaizzy/mlx-audio/blob/main/examples/server.py) - Reference for streaming API
- [MLX Whisper benchmarks](https://github.com/ml-explore/mlx-examples/tree/main/whisper) - Performance data
## Next Steps
1. ~~Validate mlx-audio can load Qwen3-TTS models (quick test)~~ ✅ Done - see `mlx-test/`
2. Get approval on dual-backend architecture
3. Start Phase 1 (platform detection)
## Questions?
Feedback welcome in GitHub discussions or Discord.
+238
View File
@@ -0,0 +1,238 @@
---
title: "OpenAI API Compatibility"
description: "OpenAI API compatibility for Voicebox (Planned)"
---
**Status:** Planned for v0.2.0
**Issue:** [#10 OpenAI API compatibility](https://github.com/jamiepine/voicebox/issues/10)
## Overview
This feature exposes OpenAI-compatible endpoints from Voicebox, allowing any tool, library, or application that speaks the OpenAI Audio API to use Voicebox as a drop-in local replacement.
```mermaid
flowchart LR
subgraph clients [External Clients]
SDK[OpenAI SDK]
Curl[curl / HTTP]
Apps[Third-party Apps]
end
subgraph voicebox [Voicebox Server]
OpenAI["/v1/audio/* endpoints"]
TTS[TTSModel]
Whisper[WhisperModel]
Profiles[Voice Profiles]
end
SDK --> OpenAI
Curl --> OpenAI
Apps --> OpenAI
OpenAI --> TTS
OpenAI --> Whisper
OpenAI --> Profiles
```
## Use Cases
- **OpenAI SDK users**: `openai.audio.speech.create()` works with Voicebox
- **LLM frameworks**: LangChain, AutoGen, etc. can use Voicebox for TTS
- **Shell scripts**: `curl` commands copy-pasted from OpenAI docs work
- **Existing integrations**: Any tool expecting OpenAI's API works without code changes
## Endpoints to Implement
### 1. `POST /v1/audio/speech` (TTS)
OpenAI spec: https://platform.openai.com/docs/api-reference/audio/createSpeech
**Request:**
```json
{
"model": "tts-1",
"input": "Hello world!",
"voice": "alloy",
"response_format": "mp3",
"speed": 1.0
}
```
**Response:** Audio file (mp3, wav, opus, aac, flac, pcm)
**Voice Mapping Strategy:**
- `voice` parameter maps to Voicebox profile names (case-insensitive)
- If no match, use a configurable default profile
- Support special syntax: `voice: "profile:uuid"` for explicit profile ID
### 2. `POST /v1/audio/transcriptions` (Whisper)
OpenAI spec: https://platform.openai.com/docs/api-reference/audio/createTranscription
**Request:** (multipart/form-data)
- `file`: Audio file
- `model`: "whisper-1"
- `language`: Optional language hint
- `response_format`: json, text, srt, verbose_json, vtt
**Response:**
```json
{
"text": "Hello world!"
}
```
## Implementation Details
### New File: `backend/openai_compat.py`
Create a dedicated module with an APIRouter for OpenAI-compatible endpoints:
```python
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Literal, Optional
router = APIRouter(prefix="/v1/audio", tags=["OpenAI Compatible"])
class SpeechRequest(BaseModel):
model: str = "tts-1"
input: str
voice: str = "alloy"
response_format: Literal["mp3", "wav", "opus", "aac", "flac", "pcm"] = "mp3"
speed: float = 1.0
@router.post("/speech")
async def create_speech(request: SpeechRequest, db: Session = Depends(get_db)):
# 1. Map voice name to profile
# 2. Generate audio using existing TTSModel
# 3. Convert to requested format
# 4. Return audio stream
...
@router.post("/transcriptions")
async def create_transcription(
file: UploadFile = File(...),
model: str = Form("whisper-1"),
language: Optional[str] = Form(None),
response_format: str = Form("json"),
):
# 1. Save uploaded file
# 2. Transcribe using existing WhisperModel
# 3. Return in requested format
...
```
### Voice Profile Resolution
Add helper in [backend/profiles.py](backend/profiles.py):
```python
async def resolve_voice_for_openai(voice: str, db: Session) -> Optional[VoiceProfile]:
"""
Resolve OpenAI voice parameter to a Voicebox profile.
Priority:
1. Exact profile name match (case-insensitive)
2. Profile ID match (if voice starts with "profile:")
3. Default profile from config
4. First available profile
"""
...
```
### Audio Format Conversion
Add conversion utilities in [backend/utils/audio.py](backend/utils/audio.py):
```python
def convert_audio_format(
audio: np.ndarray,
sample_rate: int,
target_format: str, # mp3, wav, opus, aac, flac, pcm
) -> bytes:
"""Convert audio to target format using ffmpeg or pydub."""
...
```
### Configuration
Add to [backend/config.py](backend/config.py):
```python
# OpenAI API Compatibility
OPENAI_COMPAT_ENABLED = True
OPENAI_COMPAT_DEFAULT_VOICE = None # Profile ID or name for default voice
OPENAI_COMPAT_REQUIRE_AUTH = False # Require API key validation
OPENAI_COMPAT_API_KEY = None # If set, validate against this
```
### Integration with main.py
In [backend/main.py](backend/main.py), include the router:
```python
from . import openai_compat
# Add OpenAI-compatible routes
if config.OPENAI_COMPAT_ENABLED:
app.include_router(openai_compat.router)
```
## Streaming Support (Future Enhancement)
Initial implementation returns complete audio. Streaming can be added later:
```python
@router.post("/speech")
async def create_speech(request: SpeechRequest):
if request.stream:
return StreamingResponse(
generate_audio_chunks(request),
media_type=f"audio/{request.response_format}"
)
...
```
## Testing
Example usage after implementation:
```bash
# TTS with curl
curl http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model": "tts-1", "input": "Hello!", "voice": "MyProfile"}' \
--output speech.mp3
# With OpenAI Python SDK
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
response = client.audio.speech.create(
model="tts-1",
voice="MyProfile",
input="Hello world!"
)
response.stream_to_file("output.mp3")
# Transcription
curl http://localhost:8000/v1/audio/transcriptions \
-F file=@audio.mp3 \
-F model="whisper-1"
```
## Security Considerations
- Optional API key validation (for shared deployments)
- Rate limiting on endpoints
- Input length limits (same as existing `/generate` endpoint)
## Dependencies
- `pydub` or `ffmpeg-python` for audio format conversion (mp3, opus, etc.)
- No changes to existing TTS/Whisper model code
+4
View File
@@ -0,0 +1,4 @@
{
"title": "Plans",
"pages": ["DOCKER_DEPLOYMENT", "EXTERNAL_PROVIDERS", "MLX_AUDIO", "OPENAI_SUPPORT"]
}
+9
View File
@@ -0,0 +1,9 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: 'Voicebox',
},
};
}
+39
View File
@@ -0,0 +1,39 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
import { APIPage } from '@/components/api-page';
import {
Accordion,
AccordionGroup,
CardGroup,
Danger,
Frame,
Info,
MintlifyCard,
Note,
Step,
Steps,
Tip,
Warning,
} from '@/components/mintlify-compat';
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
// Mintlify compatibility components
Frame,
CardGroup,
Card: MintlifyCard,
Steps,
Step,
Tip,
Note,
Warning,
Info,
Danger,
AccordionGroup,
Accordion,
// OpenAPI component
APIPage,
...components,
};
}
+877
View File
@@ -0,0 +1,877 @@
{
"openapi": "3.1.0",
"info": {
"title": "voicebox API",
"description": "Production-quality Qwen3-TTS voice cloning API",
"version": "0.1.0"
},
"servers": [{ "url": "http://localhost:8000", "description": "Local development server" }],
"paths": {
"/": {
"get": {
"summary": "Root",
"description": "Root endpoint.",
"operationId": "root__get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
}
}
}
},
"/health": {
"get": {
"summary": "Health",
"description": "Health check endpoint.",
"operationId": "health_health_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/HealthResponse" } }
}
}
}
}
},
"/profiles": {
"get": {
"summary": "List Profiles",
"description": "List all voice profiles.",
"operationId": "list_profiles_profiles_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": { "$ref": "#/components/schemas/VoiceProfileResponse" },
"type": "array",
"title": "Response List Profiles Profiles Get"
}
}
}
}
}
},
"post": {
"summary": "Create Profile",
"description": "Create a new voice profile.",
"operationId": "create_profile_profiles_post",
"requestBody": {
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/VoiceProfileCreate" } }
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/VoiceProfileResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/profiles/{profile_id}": {
"get": {
"summary": "Get Profile",
"description": "Get a voice profile by ID.",
"operationId": "get_profile_profiles__profile_id__get",
"parameters": [
{
"name": "profile_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Profile Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/VoiceProfileResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
},
"put": {
"summary": "Update Profile",
"description": "Update a voice profile.",
"operationId": "update_profile_profiles__profile_id__put",
"parameters": [
{
"name": "profile_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Profile Id" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/VoiceProfileCreate" } }
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/VoiceProfileResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
},
"delete": {
"summary": "Delete Profile",
"description": "Delete a voice profile.",
"operationId": "delete_profile_profiles__profile_id__delete",
"parameters": [
{
"name": "profile_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Profile Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/profiles/{profile_id}/samples": {
"post": {
"summary": "Add Profile Sample",
"description": "Add a sample to a voice profile.",
"operationId": "add_profile_sample_profiles__profile_id__samples_post",
"parameters": [
{
"name": "profile_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Profile Id" }
}
],
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_add_profile_sample_profiles__profile_id__samples_post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ProfileSampleResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
},
"get": {
"summary": "Get Profile Samples",
"description": "Get all samples for a profile.",
"operationId": "get_profile_samples_profiles__profile_id__samples_get",
"parameters": [
{
"name": "profile_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Profile Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/ProfileSampleResponse" },
"title": "Response Get Profile Samples Profiles Profile Id Samples Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/profiles/samples/{sample_id}": {
"delete": {
"summary": "Delete Profile Sample",
"description": "Delete a profile sample.",
"operationId": "delete_profile_sample_profiles_samples__sample_id__delete",
"parameters": [
{
"name": "sample_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Sample Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/generate": {
"post": {
"summary": "Generate Speech",
"description": "Generate speech from text using a voice profile.",
"operationId": "generate_speech_generate_post",
"requestBody": {
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/GenerationRequest" } }
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/GenerationResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/history": {
"get": {
"summary": "List History",
"description": "List generation history with optional filters.",
"operationId": "list_history_history_get",
"parameters": [
{
"name": "profile_id",
"in": "query",
"required": false,
"schema": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Profile Id" }
},
{
"name": "search",
"in": "query",
"required": false,
"schema": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Search" }
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": { "type": "integer", "default": 50, "title": "Limit" }
},
{
"name": "offset",
"in": "query",
"required": false,
"schema": { "type": "integer", "default": 0, "title": "Offset" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HistoryListResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/history/{generation_id}": {
"get": {
"summary": "Get Generation",
"description": "Get a generation by ID.",
"operationId": "get_generation_history__generation_id__get",
"parameters": [
{
"name": "generation_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Generation Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/HistoryResponse" } }
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
},
"delete": {
"summary": "Delete Generation",
"description": "Delete a generation.",
"operationId": "delete_generation_history__generation_id__delete",
"parameters": [
{
"name": "generation_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Generation Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/history/stats": {
"get": {
"summary": "Get Stats",
"description": "Get generation statistics.",
"operationId": "get_stats_history_stats_get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
}
}
}
},
"/transcribe": {
"post": {
"summary": "Transcribe Audio",
"description": "Transcribe audio file to text.",
"operationId": "transcribe_audio_transcribe_post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": { "$ref": "#/components/schemas/Body_transcribe_audio_transcribe_post" }
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/TranscriptionResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/audio/{generation_id}": {
"get": {
"summary": "Get Audio",
"description": "Serve generated audio file.",
"operationId": "get_audio_audio__generation_id__get",
"parameters": [
{
"name": "generation_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Generation Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/models/load": {
"post": {
"summary": "Load Model",
"description": "Manually load TTS model.",
"operationId": "load_model_models_load_post",
"parameters": [
{
"name": "model_size",
"in": "query",
"required": false,
"schema": { "type": "string", "default": "1.7B", "title": "Model Size" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/models/unload": {
"post": {
"summary": "Unload Model",
"description": "Unload TTS model to free memory.",
"operationId": "unload_model_models_unload_post",
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
}
}
}
},
"/models/progress/{model_name}": {
"get": {
"summary": "Get Model Progress",
"description": "Get model download progress via Server-Sent Events.",
"operationId": "get_model_progress_models_progress__model_name__get",
"parameters": [
{
"name": "model_name",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Model Name" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/models/status": {
"get": {
"summary": "Get Model Status",
"description": "Get status of all available models.",
"operationId": "get_model_status_models_status_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ModelStatusListResponse" }
}
}
}
}
}
},
"/models/download": {
"post": {
"summary": "Trigger Model Download",
"description": "Trigger download of a specific model.",
"operationId": "trigger_model_download_models_download_post",
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ModelDownloadRequest" }
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"Body_add_profile_sample_profiles__profile_id__samples_post": {
"properties": {
"file": { "type": "string", "format": "binary", "title": "File" },
"reference_text": { "type": "string", "title": "Reference Text" }
},
"type": "object",
"required": ["file", "reference_text"],
"title": "Body_add_profile_sample_profiles__profile_id__samples_post"
},
"Body_transcribe_audio_transcribe_post": {
"properties": {
"file": { "type": "string", "format": "binary", "title": "File" },
"language": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Language" }
},
"type": "object",
"required": ["file"],
"title": "Body_transcribe_audio_transcribe_post"
},
"GenerationRequest": {
"properties": {
"profile_id": { "type": "string", "title": "Profile Id" },
"text": { "type": "string", "maxLength": 5000, "minLength": 1, "title": "Text" },
"language": {
"type": "string",
"pattern": "^(en|zh)$",
"title": "Language",
"default": "en"
},
"seed": {
"anyOf": [{ "type": "integer", "minimum": 0.0 }, { "type": "null" }],
"title": "Seed"
},
"model_size": {
"anyOf": [{ "type": "string", "pattern": "^(1\\.7B|0\\.6B)$" }, { "type": "null" }],
"title": "Model Size",
"default": "1.7B"
}
},
"type": "object",
"required": ["profile_id", "text"],
"title": "GenerationRequest",
"description": "Request model for voice generation."
},
"GenerationResponse": {
"properties": {
"id": { "type": "string", "title": "Id" },
"profile_id": { "type": "string", "title": "Profile Id" },
"text": { "type": "string", "title": "Text" },
"language": { "type": "string", "title": "Language" },
"audio_path": { "type": "string", "title": "Audio Path" },
"duration": { "type": "number", "title": "Duration" },
"seed": { "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Seed" },
"created_at": { "type": "string", "format": "date-time", "title": "Created At" }
},
"type": "object",
"required": [
"id",
"profile_id",
"text",
"language",
"audio_path",
"duration",
"seed",
"created_at"
],
"title": "GenerationResponse",
"description": "Response model for voice generation."
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": { "$ref": "#/components/schemas/ValidationError" },
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"HealthResponse": {
"properties": {
"status": { "type": "string", "title": "Status" },
"model_loaded": { "type": "boolean", "title": "Model Loaded" },
"model_downloaded": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Model Downloaded"
},
"model_size": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Model Size"
},
"gpu_available": { "type": "boolean", "title": "Gpu Available" },
"vram_used_mb": {
"anyOf": [{ "type": "number" }, { "type": "null" }],
"title": "Vram Used Mb"
}
},
"type": "object",
"required": ["status", "model_loaded", "gpu_available"],
"title": "HealthResponse",
"description": "Response model for health check."
},
"HistoryListResponse": {
"properties": {
"items": {
"items": { "$ref": "#/components/schemas/HistoryResponse" },
"type": "array",
"title": "Items"
},
"total": { "type": "integer", "title": "Total" }
},
"type": "object",
"required": ["items", "total"],
"title": "HistoryListResponse",
"description": "Response model for history list."
},
"HistoryResponse": {
"properties": {
"id": { "type": "string", "title": "Id" },
"profile_id": { "type": "string", "title": "Profile Id" },
"profile_name": { "type": "string", "title": "Profile Name" },
"text": { "type": "string", "title": "Text" },
"language": { "type": "string", "title": "Language" },
"audio_path": { "type": "string", "title": "Audio Path" },
"duration": { "type": "number", "title": "Duration" },
"seed": { "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Seed" },
"created_at": { "type": "string", "format": "date-time", "title": "Created At" }
},
"type": "object",
"required": [
"id",
"profile_id",
"profile_name",
"text",
"language",
"audio_path",
"duration",
"seed",
"created_at"
],
"title": "HistoryResponse",
"description": "Response model for history entry (includes profile name)."
},
"ModelDownloadRequest": {
"properties": { "model_name": { "type": "string", "title": "Model Name" } },
"type": "object",
"required": ["model_name"],
"title": "ModelDownloadRequest",
"description": "Request model for triggering model download."
},
"ModelStatus": {
"properties": {
"model_name": { "type": "string", "title": "Model Name" },
"display_name": { "type": "string", "title": "Display Name" },
"downloaded": { "type": "boolean", "title": "Downloaded" },
"size_mb": { "anyOf": [{ "type": "number" }, { "type": "null" }], "title": "Size Mb" },
"loaded": { "type": "boolean", "title": "Loaded", "default": false }
},
"type": "object",
"required": ["model_name", "display_name", "downloaded"],
"title": "ModelStatus",
"description": "Response model for model status."
},
"ModelStatusListResponse": {
"properties": {
"models": {
"items": { "$ref": "#/components/schemas/ModelStatus" },
"type": "array",
"title": "Models"
}
},
"type": "object",
"required": ["models"],
"title": "ModelStatusListResponse",
"description": "Response model for model status list."
},
"ProfileSampleResponse": {
"properties": {
"id": { "type": "string", "title": "Id" },
"profile_id": { "type": "string", "title": "Profile Id" },
"audio_path": { "type": "string", "title": "Audio Path" },
"reference_text": { "type": "string", "title": "Reference Text" }
},
"type": "object",
"required": ["id", "profile_id", "audio_path", "reference_text"],
"title": "ProfileSampleResponse",
"description": "Response model for profile sample."
},
"TranscriptionResponse": {
"properties": {
"text": { "type": "string", "title": "Text" },
"duration": { "type": "number", "title": "Duration" }
},
"type": "object",
"required": ["text", "duration"],
"title": "TranscriptionResponse",
"description": "Response model for transcription."
},
"ValidationError": {
"properties": {
"loc": {
"items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
"type": "array",
"title": "Location"
},
"msg": { "type": "string", "title": "Message" },
"type": { "type": "string", "title": "Error Type" }
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError"
},
"VoiceProfileCreate": {
"properties": {
"name": { "type": "string", "maxLength": 100, "minLength": 1, "title": "Name" },
"description": {
"anyOf": [{ "type": "string", "maxLength": 500 }, { "type": "null" }],
"title": "Description"
},
"language": {
"type": "string",
"pattern": "^(en|zh)$",
"title": "Language",
"default": "en"
}
},
"type": "object",
"required": ["name"],
"title": "VoiceProfileCreate",
"description": "Request model for creating a voice profile."
},
"VoiceProfileResponse": {
"properties": {
"id": { "type": "string", "title": "Id" },
"name": { "type": "string", "title": "Name" },
"description": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Description"
},
"language": { "type": "string", "title": "Language" },
"created_at": { "type": "string", "format": "date-time", "title": "Created At" },
"updated_at": { "type": "string", "format": "date-time", "title": "Updated At" }
},
"type": "object",
"required": ["id", "name", "description", "language", "created_at", "updated_at"],
"title": "VoiceProfileResponse",
"description": "Response model for voice profile."
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
{
"name": "example-next-mdx",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "fumadocs-mdx && next build",
"dev": "fumadocs-mdx && next dev",
"start": "next start",
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"fumadocs-core": "^16.4.11",
"fumadocs-mdx": "13",
"fumadocs-openapi": "^10.2.7",
"fumadocs-ui": "^16.4.11",
"lucide-react": "^0.546.0",
"next": "^16.1.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"shiki": "^3.22.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.15",
"@types/mdx": "^2.0.13",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.15",
"typescript": "^5.9.3"
}
}
+49 -28
View File
@@ -43,24 +43,27 @@ Voicebox uses a **pluggable provider architecture** that separates the main appl
## Platform Behavior
| Platform | App Size | TTS Backend | Provider Download |
|----------|----------|-------------|-------------------|
| macOS (Apple Silicon) | ~300MB | MLX bundled | Not needed |
| macOS (Intel) | ~300MB | PyTorch bundled | Not needed |
| Windows | ~150MB | None bundled | Required |
| Linux | ~150MB | None bundled | Required |
| Platform | App Size | TTS Backend | Provider Download |
| --------------------- | -------- | --------------- | ----------------- |
| macOS (Apple Silicon) | ~300MB | MLX bundled | Not needed |
| macOS (Intel) | ~300MB | PyTorch bundled | Not needed |
| Windows | ~150MB | None bundled | Required |
| Linux | ~150MB | None bundled | Required |
### macOS (Apple Silicon)
- MLX backend is **bundled** in the app
- Works immediately after install
- Uses Metal for GPU acceleration
### macOS (Intel)
- PyTorch backend is **bundled** in the app
- Works immediately after install
- Uses CPU inference
### Windows / Linux
- **No TTS bundled** - keeps app small (~150MB)
- On first use, prompts to download a provider
- Provider options:
@@ -108,7 +111,7 @@ On macOS, the `BundledProvider` directly calls the bundled `backends/` code:
class BundledProvider:
def __init__(self):
self._backend = get_tts_backend() # MLX or PyTorch
async def generate(self, text, voice_prompt, ...):
return await self._backend.generate(text, voice_prompt, ...)
```
@@ -122,7 +125,7 @@ On Windows/Linux, the `LocalProvider` communicates with a standalone provider vi
class LocalProvider:
def __init__(self, base_url: str):
self.base_url = base_url # e.g., "http://127.0.0.1:8765"
async def generate(self, text, voice_prompt, ...):
response = await self.client.post(
f"{self.base_url}/tts/generate",
@@ -149,68 +152,82 @@ async def generate(text: str, voice_prompt: dict, ...):
All providers (local or remote) must implement these HTTP endpoints:
### POST /tts/generate
Generate speech from text.
**Request:**
```json
{
"text": "Hello world!",
"voice_prompt": { /* voice embedding */ },
"language": "en",
"seed": 12345,
"model_size": "1.7B"
"text": "Hello world!",
"voice_prompt": {
/* voice embedding */
},
"language": "en",
"seed": 12345,
"model_size": "1.7B"
}
```
**Response:**
```json
{
"audio": "base64-encoded-wav",
"sample_rate": 24000,
"duration": 2.5
"audio": "base64-encoded-wav",
"sample_rate": 24000,
"duration": 2.5
}
```
### POST /tts/create_voice_prompt
Create voice embedding from reference audio.
**Request:** `multipart/form-data`
- `audio`: Audio file
- `reference_text`: Transcript
**Response:**
```json
{
"voice_prompt": { /* voice embedding */ },
"was_cached": false
"voice_prompt": {
/* voice embedding */
},
"was_cached": false
}
```
### GET /tts/health
Health check.
**Response:**
```json
{
"status": "healthy",
"provider": "pytorch-cuda",
"version": "1.0.0",
"model": "1.7B",
"device": "cuda:0"
"status": "healthy",
"provider": "pytorch-cuda",
"version": "1.0.0",
"model": "1.7B",
"device": "cuda:0"
}
```
### GET /tts/status
Model status.
**Response:**
```json
{
"model_loaded": true,
"model_size": "1.7B",
"available_sizes": ["0.6B", "1.7B"],
"gpu_available": true,
"vram_used_mb": 1234
"model_loaded": true,
"model_size": "1.7B",
"available_sizes": ["0.6B", "1.7B"],
"gpu_available": true,
"vram_used_mb": 1234
}
```
@@ -245,10 +262,12 @@ Model status.
## Building Providers
### Prerequisites
- Python 3.12
- PyInstaller
### Build PyTorch CPU Provider
```bash
cd providers/pytorch-cpu
pip install -r requirements.txt
@@ -257,6 +276,7 @@ python build.py
```
### Build PyTorch CUDA Provider
```bash
cd providers/pytorch-cuda
pip install torch --index-url https://download.pytorch.org/whl/cu121
@@ -273,6 +293,7 @@ Providers have **independent versions** from the app:
- **Provider version:** `v1.0.0` (rare updates)
Providers only need updates when:
- TTS model changes (new Qwen3-TTS version)
- API spec changes
- Bug fixes in inference code
Binary file not shown.
+15 -28
View File
@@ -423,43 +423,30 @@ fn is_process_running(pid: u32) -> bool {
false
}
/// Kill entire Windows process tree by enumerating children
/// Kill entire Windows process tree using taskkill's built-in /T flag
/// This is more reliable than WMIC-based enumeration (WMIC is deprecated on Windows 11)
#[cfg(windows)]
fn kill_windows_process_tree(parent_pid: u32) -> Result<(), String> {
use std::process::Command;
// Find all child processes using WMIC
let output = Command::new("wmic")
.args([
"process",
"where",
&format!("ParentProcessId={}", parent_pid),
"get",
"ProcessId"
])
// taskkill with /T kills the entire process tree
// /F = force, /T = tree (kill child processes)
let result = Command::new("taskkill")
.args(["/PID", &parent_pid.to_string(), "/T", "/F"])
.output();
if let Ok(output) = output {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines().skip(1) { // Skip header
if let Ok(child_pid) = line.trim().parse::<u32>() {
println!("Found child process: {}", child_pid);
// Recursively kill child's children
let _ = kill_windows_process_tree(child_pid);
// Kill the child
let _ = Command::new("taskkill")
.args(["/PID", &child_pid.to_string(), "/F"])
.output();
match result {
Ok(output) => {
if output.status.success() {
println!("Successfully killed process tree for PID {}", parent_pid);
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("taskkill stderr: {}", stderr);
}
Ok(())
}
Err(e) => Err(format!("Failed to run taskkill: {}", e))
}
// Kill the parent process
let _ = Command::new("taskkill")
.args(["/PID", &parent_pid.to_string(), "/F"])
.output();
Ok(())
}
#[command]
+4
View File
@@ -0,0 +1,4 @@
declare module '*.png' {
const value: string;
export default value;
}
-4
View File
@@ -11,10 +11,6 @@ class WebUpdater implements PlatformUpdater {
private subscribers: Set<(status: UpdateStatus) => void> = new Set();
private notifySubscribers() {
this.subscribers.forEach((callback) => callback(this.status));
}
subscribe(callback: (status: UpdateStatus) => void): () => void {
this.subscribers.add(callback);
callback(this.status);
+12
View File
@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_SERVER_URL?: string;
readonly VITE_APP_VERSION?: string;
readonly PROD?: boolean;
readonly DEV?: boolean;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+1
View File
@@ -15,6 +15,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["../app/src/*"]