diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..1f1ea4be --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfc92e62..1da7da39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6600e511 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Dockerfile.cuda b/Dockerfile.cuda new file mode 100644 index 00000000..b6e67296 --- /dev/null +++ b/Dockerfile.cuda @@ -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"] diff --git a/README.md b/README.md index 74672d6e..32bf7133 100644 --- a/README.md +++ b/README.md @@ -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. --- diff --git a/app/package.json b/app/package.json index 07028409..b9ef942a 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/components/AudioPlayer/AudioPlayer.tsx b/app/src/components/AudioPlayer/AudioPlayer.tsx index c49f7a38..6656c9d3 100644 --- a/app/src/components/AudioPlayer/AudioPlayer.tsx +++ b/app/src/components/AudioPlayer/AudioPlayer.tsx @@ -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 | null = null; rafId1 = requestAnimationFrame(() => { rafId2 = requestAnimationFrame(() => { diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx index dc835dfb..1fa586e8 100644 --- a/app/src/components/Generation/FloatingGenerateBox.tsx +++ b/app/src/components/Generation/FloatingGenerateBox.tsx @@ -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 ? ( - + ) : ( )} diff --git a/app/src/components/Generation/GenerationForm.tsx b/app/src/components/Generation/GenerationForm.tsx index edf7b749..d652a1d7 100644 --- a/app/src/components/Generation/GenerationForm.tsx +++ b/app/src/components/Generation/GenerationForm.tsx @@ -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() { Voice Profile {selectedProfile ? (
- + {selectedProfile.name} {selectedProfile.language}
@@ -171,14 +176,10 @@ export function GenerationForm() { /> - ) : ( diff --git a/app/src/components/ServerSettings/ModelProgress.tsx b/app/src/components/ServerSettings/ModelProgress.tsx index 30229c50..efd35427 100644 --- a/app/src/components/ServerSettings/ModelProgress.tsx +++ b/app/src/components/ServerSettings/ModelProgress.tsx @@ -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(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 ; + return ( + + ); case 'downloading': case 'extracting': - return ; + return ; default: return null; } diff --git a/app/src/components/ServerSettings/ProviderSettings.tsx b/app/src/components/ServerSettings/ProviderSettings.tsx index 41f820df..612b3983 100644 --- a/app/src/components/ServerSettings/ProviderSettings.tsx +++ b/app/src/components/ServerSettings/ProviderSettings.tsx @@ -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() {
- +
@@ -210,11 +211,7 @@ export function ProviderSettings() { disabled={downloadingProvider === 'pytorch-cuda'} > {downloadingProvider === 'pytorch-cuda' ? ( - + ) : ( <> @@ -257,11 +254,7 @@ export function ProviderSettings() { disabled={downloadingProvider === 'pytorch-cpu'} > {downloadingProvider === 'pytorch-cpu' ? ( - + ) : ( <> diff --git a/app/src/components/ServerSettings/ServerStatus.tsx b/app/src/components/ServerSettings/ServerStatus.tsx index 8b5e5197..8437552e 100644 --- a/app/src/components/ServerSettings/ServerStatus.tsx +++ b/app/src/components/ServerSettings/ServerStatus.tsx @@ -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 ? (
- + Checking connection...
) : error ? ( diff --git a/app/src/components/Sidebar.tsx b/app/src/components/Sidebar.tsx index 04442db0..39314788 100644 --- a/app/src/components/Sidebar.tsx +++ b/app/src/components/Sidebar.tsx @@ -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 ( - + )} diff --git a/app/src/components/StoriesTab/StoryChatItem.tsx b/app/src/components/StoriesTab/StoryChatItem.tsx index c1dd793f..ac16352a 100644 --- a/app/src/components/StoriesTab/StoryChatItem.tsx +++ b/app/src/components/StoriesTab/StoryChatItem.tsx @@ -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 */}
-
- {!avatarError ? ( - {`${item.profile_name} setAvatarError(true)} - /> - ) : ( - - )} -
+
{/* Content */} diff --git a/app/src/components/VoiceProfiles/ProfileAvatar.tsx b/app/src/components/VoiceProfiles/ProfileAvatar.tsx new file mode 100644 index 00000000..0743fb3f --- /dev/null +++ b/app/src/components/VoiceProfiles/ProfileAvatar.tsx @@ -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 ( +
+ {avatarUrl && !avatarError ? ( + {alt} setAvatarError(true)} + /> + ) : ( + + )} +
+ ); +} diff --git a/app/src/components/VoiceProfiles/ProfileCard.tsx b/app/src/components/VoiceProfiles/ProfileCard.tsx index 5e2588fa..40742f40 100644 --- a/app/src/components/VoiceProfiles/ProfileCard.tsx +++ b/app/src/components/VoiceProfiles/ProfileCard.tsx @@ -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) { > -
- {avatarUrl && !avatarError ? ( - {`${profile.name} setAvatarError(true)} - /> - ) : ( - - )} -
+ {profile.name}
diff --git a/app/src/components/VoicesTab/VoicesTab.tsx b/app/src/components/VoicesTab/VoicesTab.tsx index c1e98a33..f1b34fa3 100644 --- a/app/src/components/VoicesTab/VoicesTab.tsx +++ b/app/src/components/VoicesTab/VoicesTab.tsx @@ -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({
-
- -
+
{profile.name}
{profile.description && ( diff --git a/app/src/components/ui/radio-group.tsx b/app/src/components/ui/radio-group.tsx index c82678c3..a04aa433 100644 --- a/app/src/components/ui/radio-group.tsx +++ b/app/src/components/ui/radio-group.tsx @@ -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, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { - return ( - - ) -}) -RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + return ; +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; const RadioGroupItem = React.forwardRef< React.ElementRef, @@ -29,17 +23,21 @@ const RadioGroupItem = React.forwardRef< - + - ) -}) -RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; -export { RadioGroup, RadioGroupItem } +export { RadioGroup, RadioGroupItem }; diff --git a/app/src/lib/hooks/useGenerationForm.ts b/app/src/lib/hooks/useGenerationForm.ts index c6fdba50..70955824 100644 --- a/app/src/lib/hooks/useGenerationForm.ts +++ b/app/src/lib/hooks/useGenerationForm.ts @@ -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)); diff --git a/app/src/lib/hooks/useModelDownloadToast.tsx b/app/src/lib/hooks/useModelDownloadToast.tsx index 56c13b9d..10fb7bdc 100644 --- a/app/src/lib/hooks/useModelDownloadToast.tsx +++ b/app/src/lib/hooks/useModelDownloadToast.tsx @@ -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: (
- + Connecting to download...
), @@ -97,19 +98,35 @@ export function useModelDownloadToast({ switch (progress.status) { case 'complete': - statusIcon = ; + statusIcon = ( + + ); statusText = 'Download complete'; break; case 'error': - statusIcon = ; + statusIcon = ( + + ); statusText = `Error: ${progress.error || 'Unknown error'}`; break; case 'downloading': - statusIcon = ; + statusIcon = ( + + ); statusText = progress.filename || 'Downloading...'; break; case 'extracting': - statusIcon = ; + statusIcon = ( + + ); statusText = 'Extracting...'; break; } @@ -155,7 +172,11 @@ export function useModelDownloadToast({ toastUpdateRef.current({ title: (
- + {displayName}
), diff --git a/backend/main.py b/backend/main.py index f0c774a5..306a0639 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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 # ============================================ diff --git a/backend/providers/checksums.py b/backend/providers/checksums.py new file mode 100644 index 00000000..75798b10 --- /dev/null +++ b/backend/providers/checksums.py @@ -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...", +} diff --git a/bun.lock b/bun.lock index bcbf37be..add8f83f 100644 --- a/bun.lock +++ b/bun.lock @@ -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/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + "@iconify-json/svg-spinners": ["@iconify-json/svg-spinners@1.2.4", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-ayn0pogFPwJA1WFZpDnoq9/hjDxN+keeCMyThaX4d3gSJ3y0mdKUxIA/b1YXWGtY9wVtZmxwcvOIeEieG4+JNg=="], + + "@iconify/react": ["@iconify/react@6.0.2", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-SMmC2sactfpJD427WJEDN6PMyznTFMhByK9yLW0gOTtnjzzbsi/Ke/XqsumsavFPwNiXs8jSiYeZTmLCLwO+Fg=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], diff --git a/docker-compose.cpu.yml b/docker-compose.cpu.yml new file mode 100644 index 00000000..7093c8d7 --- /dev/null +++ b/docker-compose.cpu.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..f8225205 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/docs/mint.json b/docs/mint.json index c4f582ea..bbe0c409 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -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", diff --git a/docs/overview/docker.mdx b/docs/overview/docker.mdx new file mode 100644 index 00000000..8fff55c8 --- /dev/null +++ b/docs/overview/docker.mdx @@ -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 + + + + ```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. + + + + ```bash + docker run -p 8000:8000 \ + -v voicebox-data:/app/data \ + ghcr.io/jamiepine/voicebox:latest + ``` + + Open http://localhost:8000 in your browser. + + + + 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. + + + + + 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. + + +## 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 | + + + Pin to a specific version in production to avoid unexpected updates: + ```yaml + image: ghcr.io/jamiepine/voicebox:0.1.13-cuda + ``` + + +## 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 + + + + Stores voice profiles, generated audio, and database + + + Caches downloaded TTS/Whisper models (saves re-downloading) + + + + + Always mount `/app/data` to preserve your voice profiles and generations across container restarts. + + +## 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 + + + + Use g4dn.xlarge or p3.2xlarge with NVIDIA GPU + + + + ```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 + ``` + + + + ```bash + docker run -d --gpus all -p 8000:8000 \ + -v voicebox-data:/app/data \ + --restart unless-stopped \ + ghcr.io/jamiepine/voicebox:latest-cuda + ``` + + + +### DigitalOcean + + + + ```bash + doctl compute droplet create voicebox \ + --size gpu-h100x1-80gb \ + --image ubuntu-22-04-x64 \ + --region nyc3 + ``` + + + + ```bash + ssh root@ + curl -fsSL https://get.docker.com | sh + docker run -d --gpus all -p 80:8000 \ + ghcr.io/jamiepine/voicebox:latest-cuda + ``` + + + +### 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. + + + + Always get the newest version: + + ```bash + docker pull ghcr.io/jamiepine/voicebox:latest + docker compose up -d + ``` + + + + 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 + ``` + + + + 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 + ``` + + + +## 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 + + + ```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. + + + + 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 + ``` + + + + Change the host port: + + ```bash + docker run -p 8080:8000 ghcr.io/jamiepine/voicebox:latest + ``` + + Then open http://localhost:8080 + + + + Run with specific user: + + ```bash + docker run --user $(id -u):$(id -g) \ + -v $(pwd)/data:/app/data \ + ghcr.io/jamiepine/voicebox:latest + ``` + + +## 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 + + + + Integrate Voicebox into your applications + + + Connect desktop app to Docker backend + + diff --git a/docs/overview/installation.mdx b/docs/overview/installation.mdx index 04ddc5ba..508951c7 100644 --- a/docs/overview/installation.mdx +++ b/docs/overview/installation.mdx @@ -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. - + Download for Apple Silicon or Intel Macs Download MSI installer or Setup executable + + Run with web UI in a container + ### macOS @@ -61,7 +64,7 @@ Voicebox is available for macOS and Windows, with Linux builds coming soon. ### Linux - 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). ## First Launch diff --git a/docs/plans/DOCKER_DEPLOYMENT.md b/docs/plans/DOCKER_DEPLOYMENT.md index 6af65ed7..6312ed41 100644 --- a/docs/plans/DOCKER_DEPLOYMENT.md +++ b/docs/plans/DOCKER_DEPLOYMENT.md @@ -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 diff --git a/docs2/MIGRATION.md b/docs2/MIGRATION.md new file mode 100644 index 00000000..98f2be32 --- /dev/null +++ b/docs2/MIGRATION.md @@ -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: + +- `` → Simple div wrapper (images are zoomable by default in Fumadocs) +- `` → `` (Fumadocs component) +- `` → `` (with icon string → Lucide icon mapping) +- `` / `` → Direct mapping to Fumadocs components +- ``, ``, `` → `` +- `` → `` +- `` → `` +- `` / `` → HTML `
` / `` 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 diff --git a/docs2/content/docs/AUTOUPDATER.md b/docs2/content/docs/AUTOUPDATER.md new file mode 100644 index 00000000..76148c52 --- /dev/null +++ b/docs2/content/docs/AUTOUPDATER.md @@ -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`) diff --git a/docs2/content/docs/AUTOUPDATER_QUICKSTART.md b/docs2/content/docs/AUTOUPDATER_QUICKSTART.md new file mode 100644 index 00000000..5614536d --- /dev/null +++ b/docs2/content/docs/AUTOUPDATER_QUICKSTART.md @@ -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` diff --git a/docs2/content/docs/README.md b/docs2/content/docs/README.md new file mode 100644 index 00000000..66f7833d --- /dev/null +++ b/docs2/content/docs/README.md @@ -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. diff --git a/docs2/content/docs/TROUBLESHOOTING.md b/docs2/content/docs/TROUBLESHOOTING.md new file mode 100644 index 00000000..18a6c255 --- /dev/null +++ b/docs2/content/docs/TROUBLESHOOTING.md @@ -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). diff --git a/docs2/content/docs/api/meta.json b/docs2/content/docs/api/meta.json new file mode 100644 index 00000000..d23c38eb --- /dev/null +++ b/docs2/content/docs/api/meta.json @@ -0,0 +1,4 @@ +{ + "title": "API Reference", + "pages": ["overview", "authentication", "voice-profiles", "generation", "recordings"] +} diff --git a/docs2/content/docs/developer/contributing.mdx b/docs2/content/docs/developer/contributing.mdx new file mode 100644 index 00000000..8ae5914f --- /dev/null +++ b/docs2/content/docs/developer/contributing.mdx @@ -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 + + + + Found a bug? Open an issue with reproduction steps + + + Have an idea? Start a discussion or open an issue + + + Fix typos, add examples, or clarify instructions + + + Fix bugs, add features, or optimize performance + + + +## 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: + + + + **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` + + + + + 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 + + + + + - 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 + + + +### 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: + + + + Examples: + - "Add voice profile export functionality" + - "Fix audio playback stopping after 30 seconds" + - "Improve generation speed with caching" + + +{" "} + + Include: - What changes you made - Why you made them - How to test them - + Screenshots (for UI changes) - Reference related issues + + +{" "} + + - Update relevant docs if behavior changes - Add API documentation for new + endpoints - Update README if needed + + + + - [ ] Code follows style guidelines + - [ ] Documentation updated + - [ ] Changes tested + - [ ] No breaking changes (or documented) + - [ ] CHANGELOG.md updated + + + +## 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: + + + + In `backend/main.py`: + + ```python + @app.post("/api/new-endpoint") + async def new_endpoint(data: RequestModel) -> ResponseModel: + """Endpoint description.""" + # Implementation + return response + ``` + + + + + In `backend/models.py`: + + ```python + class RequestModel(BaseModel): + field: str + + class ResponseModel(BaseModel): + result: str + ``` + + + + + ```bash + bun run generate:api + ``` + + This updates the TypeScript client with type-safe bindings. + + + + + 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 + ``` + + + +## 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! 🎉 diff --git a/docs2/content/docs/developer/meta.json b/docs2/content/docs/developer/meta.json new file mode 100644 index 00000000..5c97f225 --- /dev/null +++ b/docs2/content/docs/developer/meta.json @@ -0,0 +1,17 @@ +{ + "title": "Developer", + "pages": [ + "setup", + "architecture", + "contributing", + "building", + "autoupdater", + "voice-profiles", + "tts-generation", + "history", + "stories", + "transcription", + "audio-channels", + "model-management" + ] +} diff --git a/docs2/content/docs/developer/setup.mdx b/docs2/content/docs/developer/setup.mdx new file mode 100644 index 00000000..be5b5eff --- /dev/null +++ b/docs2/content/docs/developer/setup.mdx @@ -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: + + + + [Download Bun](https://bun.sh) ```bash curl -fsSL https://bun.sh/install | + bash ``` + + + [Download Python](https://python.org) ```bash python --version ``` + + + [Install Rust](https://rustup.rs) ```bash rustc --version ``` + + + +## 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 +``` + + + The Makefile is available on macOS and Linux. Windows users should follow the + manual setup below. + + +## 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. + + + + 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` + + + + + 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 + + + + + + In dev mode, the app connects to your manually-started Python server. The + bundled server binary is only used in production builds. + + +### 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) + + + First-time usage will be slower due to model downloads, but subsequent runs + will use cached models. + + +## 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 + + + + Understand the system architecture + + + Read the contribution guidelines + + + Learn how to build production releases + + + Explore the REST API + + + +## Troubleshooting + + + + - 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 + + +{" "} + + - Ensure Rust is installed: `rustc --version` - Clean the build: `cd + tauri/src-tauri && cargo clean` - Try rebuilding: `bun run dev` + + + + - Ensure backend is running: `curl http://localhost:17493/openapi.json` + - Check network connectivity + - Verify the backend is accessible at localhost:17493 + + + +See the full [Troubleshooting Guide](/guides/troubleshooting) for more issues and solutions. diff --git a/docs2/content/docs/index.mdx b/docs2/content/docs/index.mdx new file mode 100644 index 00000000..1c2b401e --- /dev/null +++ b/docs2/content/docs/index.mdx @@ -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. + + + Voicebox App Screenshot + + +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 + + + + Instant cloning from just a few seconds of audio with Qwen3-TTS + + + Multi-track timeline for creating conversations and narratives + + + REST API for integrating voice synthesis into your apps + + + Everything runs on your machine - complete privacy + + + +## Get Started + + + + Download and install Voicebox on your machine + + + Get up and running in 5 minutes + + diff --git a/docs2/content/docs/meta.json b/docs2/content/docs/meta.json new file mode 100644 index 00000000..42d96896 --- /dev/null +++ b/docs2/content/docs/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Voicebox Documentation", + "pages": ["overview", "api-reference", "developer", "plans"] +} diff --git a/docs2/content/docs/overview/creating-voice-profiles.mdx b/docs2/content/docs/overview/creating-voice-profiles.mdx new file mode 100644 index 00000000..2894ea5d --- /dev/null +++ b/docs2/content/docs/overview/creating-voice-profiles.mdx @@ -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 + + + 10-30 seconds of clear speech + **Profiles** → **+ New Profile** + Add your audio file + Use the profile to generate speech + + +## Audio Requirements + +### Ideal Sample Characteristics + + + + **10-30 seconds** + + Too short: Poor quality + Too long: Unnecessary + + + + **Clear speech** + + No background noise + No music or overlapping voices + + + + **High fidelity** + + 44.1kHz or 48kHz sample rate + Minimal compression + + + + **Natural speech** + + Conversational tone + Complete sentences + + + + +### File Formats + +Supported formats: + +- **WAV** (recommended) - Lossless quality +- **MP3** - Acceptable, minimal compression +- **M4A** - Acceptable +- **FLAC** - Lossless alternative + +Use WAV for best results. Avoid heavily compressed formats. + +## Recording Tips + +### Environment + + + + - Record in a quiet room + - Turn off fans, AC, appliances + - Close windows to reduce outside noise + - Use soft furnishings to reduce echo + + +{" "} + + - 6-12 inches from mouth - Slight angle to reduce plosives (p, b, t) - Use a + pop filter if available - Maintain consistent distance + + + + - 44.1kHz or 48kHz sample rate + - 16-bit or 24-bit depth + - Mono is fine (stereo will be converted) + - Avoid automatic gain control + + + +### 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? + + + + Model learns a more complete representation + + + Handles different speaking styles better + + + Reduces artifacts and improves naturalness + + + More reliable across different texts + + + +### 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 + + + All samples should be from the **same speaker**. Mixing voices will produce + poor results. + + +## Processing Existing Audio + +If you have existing audio (podcasts, videos, etc.): + +### Extracting Clean Segments + + + + Look for segments with: + - Just the target speaker + - No background music + - Minimal noise + + +{" "} + + Tools like Audacity or Adobe Audition: - Cut out clean 10-30s segments - + Remove silence at start/end - Normalize volume if needed + + + + Save as high-quality WAV file + + + +### 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: + + + + Generate a simple phrase: + ``` + "Hello, this is a test of my voice profile." + ``` + + +{" "} + + Listen for: - Natural tone - Clear pronunciation - Proper prosody - Lack of + artifacts + + + + If quality is poor: + - Add more samples + - Try different source audio + - Check sample quality + + + +### Common Issues + + + + **Cause**: Poor quality samples or too short + + **Fix**: Use longer, higher quality samples + + + + + **Cause**: Sample tone doesn't match desired output + + **Fix**: Record samples in the style you want to generate + + + + + **Cause**: Background noise or audio issues in samples + + **Fix**: Clean up samples or re-record in quieter environment + + + + +## 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 + + + + Use your profile to generate speech + + + Create multi-voice narratives + + diff --git a/docs2/content/docs/overview/meta.json b/docs2/content/docs/overview/meta.json new file mode 100644 index 00000000..d3906004 --- /dev/null +++ b/docs2/content/docs/overview/meta.json @@ -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" + ] +} diff --git a/docs2/content/docs/overview/quick-start.mdx b/docs2/content/docs/overview/quick-start.mdx new file mode 100644 index 00000000..dac64fe4 --- /dev/null +++ b/docs2/content/docs/overview/quick-start.mdx @@ -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. + + + + Click the **Profiles** tab in the sidebar + + + + 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 + + + + + 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 + + + + + Click **Create Profile** to save + + + + + For best results, use clean audio with minimal background noise and consistent + speaking tone. + + +## Step 2: Generate Speech + +Now let's use your new voice profile to generate speech. + + + + Click the **Generate** tab in the sidebar + + +{" "} + + Choose your newly created profile from the dropdown + + + + Type or paste the text you want to generate: + + ``` + Hello! This is my first voice generation with Voicebox. + ``` + + + + + Click **Generate** and wait a few seconds + + + First generation may take longer due to model initialization. Subsequent generations will be faster. + + + + + + - Click **Play** to preview the audio + - Click **Download** to save the audio file + - The generation is also saved to your **History** + + + +## Step 3: Build a Story (Optional) + +The Stories Editor lets you create multi-voice narratives with a timeline-based interface. + + + + Navigate to **Stories** and click **+ New Story** + + +{" "} + + Click **+ Add Track** to create tracks for different speakers + + +{" "} + + - Drag generated audio from your History - Or generate new clips directly in + the timeline - Arrange clips on the timeline + + + + - Trim clips by dragging edges + - Adjust timing and spacing + - Click **Export** to render the final audio + + + +## What's Next? + + + + Learn advanced techniques for high-quality voice cloning + + + Integrate Voicebox into your own applications + + + Master the multi-track timeline editor + + + Connect to a GPU server for faster generation + + + +## Tips for Success + + + + - 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 + + +{" "} + + - 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 + + + + - **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 + + diff --git a/docs2/content/docs/plans/DOCKER_DEPLOYMENT.md b/docs2/content/docs/plans/DOCKER_DEPLOYMENT.md new file mode 100644 index 00000000..988dab91 --- /dev/null +++ b/docs2/content/docs/plans/DOCKER_DEPLOYMENT.md @@ -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@ +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/) diff --git a/docs2/content/docs/plans/EXTERNAL_PROVIDERS.md b/docs2/content/docs/plans/EXTERNAL_PROVIDERS.md new file mode 100644 index 00000000..24cea16f --- /dev/null +++ b/docs2/content/docs/plans/EXTERNAL_PROVIDERS.md @@ -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) diff --git a/docs2/content/docs/plans/MLX_AUDIO.md b/docs2/content/docs/plans/MLX_AUDIO.md new file mode 100644 index 00000000..021911fc --- /dev/null +++ b/docs2/content/docs/plans/MLX_AUDIO.md @@ -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. diff --git a/docs2/content/docs/plans/OPENAI_SUPPORT.md b/docs2/content/docs/plans/OPENAI_SUPPORT.md new file mode 100644 index 00000000..e570dcf0 --- /dev/null +++ b/docs2/content/docs/plans/OPENAI_SUPPORT.md @@ -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 diff --git a/docs2/content/docs/plans/meta.json b/docs2/content/docs/plans/meta.json new file mode 100644 index 00000000..fc3ea779 --- /dev/null +++ b/docs2/content/docs/plans/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Plans", + "pages": ["DOCKER_DEPLOYMENT", "EXTERNAL_PROVIDERS", "MLX_AUDIO", "OPENAI_SUPPORT"] +} diff --git a/docs2/lib/layout.shared.tsx b/docs2/lib/layout.shared.tsx new file mode 100644 index 00000000..1d8ed146 --- /dev/null +++ b/docs2/lib/layout.shared.tsx @@ -0,0 +1,9 @@ +import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; + +export function baseOptions(): BaseLayoutProps { + return { + nav: { + title: 'Voicebox', + }, + }; +} diff --git a/docs2/mdx-components.tsx b/docs2/mdx-components.tsx new file mode 100644 index 00000000..68824788 --- /dev/null +++ b/docs2/mdx-components.tsx @@ -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, + }; +} diff --git a/docs2/openapi.json b/docs2/openapi.json new file mode 100644 index 00000000..b573a920 --- /dev/null +++ b/docs2/openapi.json @@ -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." + } + } + } +} diff --git a/docs2/package.json b/docs2/package.json new file mode 100644 index 00000000..1d14cd8f --- /dev/null +++ b/docs2/package.json @@ -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" + } +} diff --git a/providers/README.md b/providers/README.md index 9ca3e03b..6f4a6f42 100644 --- a/providers/README.md +++ b/providers/README.md @@ -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 diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car index c9020db2..3df4bd6f 100644 Binary files a/tauri/src-tauri/gen/Assets.car and b/tauri/src-tauri/gen/Assets.car differ diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 255655aa..25251169 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -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::() { - 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] diff --git a/web/src/assets.d.ts b/web/src/assets.d.ts new file mode 100644 index 00000000..1c592325 --- /dev/null +++ b/web/src/assets.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const value: string; + export default value; +} diff --git a/web/src/platform/updater.ts b/web/src/platform/updater.ts index 32ed0148..2f8be445 100644 --- a/web/src/platform/updater.ts +++ b/web/src/platform/updater.ts @@ -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); diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 00000000..31a365f5 --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1,12 @@ +/// + +interface ImportMetaEnv { + readonly VITE_SERVER_URL?: string; + readonly VITE_APP_VERSION?: string; + readonly PROD?: boolean; + readonly DEV?: boolean; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/web/tsconfig.json b/web/tsconfig.json index 629bcb7f..1c148061 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -15,6 +15,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + "types": ["vite/client"], "baseUrl": ".", "paths": { "@/*": ["../app/src/*"]